From 8cf40a73646a9448746fc4043c6410ae92172e7c Mon Sep 17 00:00:00 2001 From: Andrew Lukoshko Date: Sat, 8 May 2021 00:31:14 +0300 Subject: Add AlmaLinux OS support (#872) AlmaLinux OS is RHEL-compatible so all the changes needed are trivial. --- systemd/cloud-init-generator.tmpl | 2 +- systemd/cloud-init.service.tmpl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) (limited to 'systemd') diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 0773356b..9b103ef9 100755 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,7 +83,7 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["rhel", "fedora", "centos"] %} +{% if variant in ["almalinux", "rhel", "fedora", "centos"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index f140344d..a5c51277 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -10,7 +10,7 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["centos", "fedora", "rhel"] %} +{% if variant in ["almalinux", "centos", "fedora", "rhel"] %} After=network.service After=NetworkManager.service {% endif %} -- cgit v1.2.3 From 7c1d27b8836c266f6db1e179b98ef32effeb750e Mon Sep 17 00:00:00 2001 From: Louis Abel Date: Tue, 25 May 2021 06:25:41 -0700 Subject: Add Rocky Linux support to cloud-init (#906) Rocky Linux is a RHEL-compatible distribution so all changes that have been made should be trivial. --- README.md | 2 +- cloudinit/config/cc_ntp.py | 2 +- cloudinit/config/cc_yum_add_repo.py | 4 ++-- cloudinit/distros/__init__.py | 2 +- cloudinit/distros/rocky.py | 9 +++++++++ cloudinit/net/sysconfig.py | 2 +- cloudinit/tests/.test_util.py.swp | Bin 0 -> 16384 bytes cloudinit/tests/test_util.py | 35 ++++++++++++++++++++++++++++++++++- cloudinit/util.py | 2 +- config/cloud.cfg.tmpl | 7 ++++--- systemd/cloud-init-generator.tmpl | 2 +- systemd/cloud-init.service.tmpl | 2 +- tests/unittests/test_cli.py | 3 ++- tools/.github-cla-signers | 1 + tools/render-cloudcfg | 4 ++-- 15 files changed, 61 insertions(+), 16 deletions(-) create mode 100644 cloudinit/distros/rocky.py create mode 100644 cloudinit/tests/.test_util.py.swp (limited to 'systemd') diff --git a/README.md b/README.md index 01fd3b07..bf232eba 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
Digital Ocean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| +| Alpine Linux
ArchLinux
Debian
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
Digital Ocean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index 41c278ff..70c24610 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -25,7 +25,7 @@ frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 distros = ['almalinux', 'alpine', 'centos', 'debian', 'fedora', 'opensuse', - 'rhel', 'sles', 'ubuntu'] + 'rhel', 'rocky', 'sles', 'ubuntu'] NTP_CLIENT_CONFIG = { 'chrony': { diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index db513ed7..7daa6bd9 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -18,7 +18,7 @@ entry, the config entry will be skipped. **Module frequency:** per always -**Supported distros:** almalinux, centos, fedora, rhel +**Supported distros:** almalinux, centos, fedora, rhel, rocky **Config keys**:: @@ -36,7 +36,7 @@ from configparser import ConfigParser from cloudinit import util -distros = ['almalinux', 'centos', 'fedora', 'rhel'] +distros = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky'] def _canonicalize_id(repo_id): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 107b928c..57e33621 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -46,7 +46,7 @@ OSFAMILIES = { 'debian': ['debian', 'ubuntu'], 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], - 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'rhel'], + 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'rhel', 'rocky'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/rocky.py b/cloudinit/distros/rocky.py new file mode 100644 index 00000000..edb3165d --- /dev/null +++ b/cloudinit/distros/rocky.py @@ -0,0 +1,9 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 089b44b2..3a433c99 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -19,7 +19,7 @@ from .network_state import ( LOG = logging.getLogger(__name__) NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" -KNOWN_DISTROS = ['almalinux', 'centos', 'fedora', 'rhel', 'suse'] +KNOWN_DISTROS = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky', 'suse'] def _make_header(sep='#'): diff --git a/cloudinit/tests/.test_util.py.swp b/cloudinit/tests/.test_util.py.swp new file mode 100644 index 00000000..78ef5865 Binary files /dev/null and b/cloudinit/tests/.test_util.py.swp differ diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index a4c02877..f9bc31be 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -124,6 +124,22 @@ OS_RELEASE_ALMALINUX_8 = dedent("""\ ALMALINUX_MANTISBT_PROJECT_VERSION="8.3" """) +OS_RELEASE_ROCKY_8 = dedent("""\ + NAME="Rocky Linux" + VERSION="8.3 (Green Obsidian)" + ID="rocky" + ID_LIKE="rhel fedora" + VERSION_ID="8.3" + PLATFORM_ID="platform:el8" + PRETTY_NAME="Rocky Linux 8.3 (Green Obsidian)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:rocky:rocky:8" + HOME_URL="https://rockylinux.org/" + BUG_REPORT_URL="https://bugs.rockylinux.org/" + ROCKY_SUPPORT_PRODUCT="Rocky Linux" + ROCKY_SUPPORT_PRODUCT_VERSION="8" +""") + REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" REDHAT_RELEASE_REDHAT_6 = ( @@ -132,7 +148,8 @@ REDHAT_RELEASE_REDHAT_7 = ( "Red Hat Enterprise Linux Server release 7.5 (Maipo)") REDHAT_RELEASE_ALMALINUX_8 = ( "AlmaLinux release 8.3 (Purple Manul)") - +REDHAT_RELEASE_ROCKY_8 = ( + "Rocky Linux release 8.3 (Green Obsidian)") OS_RELEASE_DEBIAN = dedent("""\ PRETTY_NAME="Debian GNU/Linux 9 (stretch)" @@ -537,6 +554,22 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): + """Verify rocky linux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_ROCKY_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_rocky8_osrelease(self, m_os_release, m_path_exists): + """Verify rocky linux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_ROCKY_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_debian(self, m_os_release, m_path_exists): """Verify we get the correct name and release name on Debian.""" diff --git a/cloudinit/util.py b/cloudinit/util.py index fdea1181..2de1123e 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -522,7 +522,7 @@ def system_info(): linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'fedora', - 'rhel', 'suse'): + 'rhel', 'rocky', 'suse'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 8656daa7..2f6c3a7d 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -21,7 +21,8 @@ disable_root: false disable_root: true {% endif %} -{% if variant in ["almalinux", "alpine", "amazon", "centos", "fedora", "rhel"] %} +{% if variant in ["almalinux", "alpine", "amazon", "centos", "fedora", + "rhel", "rocky"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} resize_rootfs: noblock @@ -154,7 +155,7 @@ cloud_final_modules: system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "rhel", + "fedora", "freebsd", "netbsd", "openbsd", "rhel", "rocky", "suse", "ubuntu"] %} distro: {{ variant }} {% else %} @@ -207,7 +208,7 @@ system_info: security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh {% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "fedora", - "rhel", "suse"] %} + "rhel", "rocky", "suse"] %} # Default user name + that default users groups (if added/used) default_user: {% if variant == "amazon" %} diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 9b103ef9..0713db16 100755 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,7 +83,7 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["almalinux", "rhel", "fedora", "centos"] %} +{% if variant in ["almalinux", "rhel", "fedora", "centos", "rocky"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index a5c51277..4da1a905 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -10,7 +10,7 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["almalinux", "centos", "fedora", "rhel"] %} +{% if variant in ["almalinux", "centos", "fedora", "rhel", "rocky"] %} After=network.service After=NetworkManager.service {% endif %} diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index f5cf514d..fbc6ec11 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -224,7 +224,8 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): self._call_main(['cloud-init', 'devel', 'schema', '--docs', 'all']) expected_doc_sections = [ '**Supported distros:** all', - '**Supported distros:** almalinux, alpine, centos, debian, fedora', + ('**Supported distros:** almalinux, alpine, centos, debian, ' + 'fedora, opensuse, rhel, rocky, sles, ubuntu'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index 6f626643..14916d31 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -35,6 +35,7 @@ manuelisimo marlluslustosa matthewruffell mitechie +nazunalika nicolasbock nishigori olivierlemasle diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index f5990748..9ec554bd 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -5,8 +5,8 @@ import os import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "rhel", "suse", "ubuntu", - "unknown"] + "fedora", "freebsd", "netbsd", "openbsd", "rhel", "suse", "rocky", + "ubuntu", "unknown"] if "avoid-pep8-E402-import-not-top-of-file": -- cgit v1.2.3 From 35aa9db6f8e2ba05d366776c0e8d97f52217e930 Mon Sep 17 00:00:00 2001 From: sshedi <53473811+sshedi@users.noreply.github.com> Date: Fri, 18 Jun 2021 22:23:44 +0530 Subject: Add support for VMware PhotonOS (#909) Also added a new (currently experimental) systemd-networkd renderer, and includes a small refactor to cc_resolv_conf.py to support the resolved.conf used by systemd-resolved. --- README.md | 2 +- cloudinit/cmd/devel/net_convert.py | 11 +- cloudinit/config/cc_ntp.py | 26 +- cloudinit/config/cc_resolv_conf.py | 25 +- cloudinit/config/cc_yum_add_repo.py | 4 +- cloudinit/config/tests/test_resolv_conf.py | 28 +- cloudinit/distros/__init__.py | 4 +- cloudinit/distros/arch.py | 1 - cloudinit/distros/gentoo.py | 1 - cloudinit/distros/opensuse.py | 1 - cloudinit/distros/photon.py | 355 +++++++++++++++++++++ cloudinit/distros/rhel.py | 1 - cloudinit/net/networkd.py | 246 ++++++++++++++ cloudinit/net/renderers.py | 4 +- cloudinit/tests/test_util.py | 20 ++ cloudinit/util.py | 4 +- config/cloud.cfg.tmpl | 52 ++- systemd/cloud-init.service.tmpl | 2 + templates/chrony.conf.photon.tmpl | 48 +++ templates/hosts.photon.tmpl | 22 ++ templates/ntp.conf.photon.tmpl | 61 ++++ templates/resolv.conf.tmpl | 2 +- templates/systemd.resolved.conf.tmpl | 15 + tests/cloud_tests/util.py | 2 +- tests/unittests/test_cli.py | 2 +- tests/unittests/test_distros/test_netconfig.py | 99 +++++- .../test_handler/test_handler_set_hostname.py | 26 ++ tests/unittests/test_net.py | 244 +++++++++++++- tests/unittests/test_render_cloudcfg.py | 3 +- tools/render-cloudcfg | 4 +- 30 files changed, 1256 insertions(+), 59 deletions(-) create mode 100644 cloudinit/distros/photon.py create mode 100644 cloudinit/net/networkd.py create mode 100644 templates/chrony.conf.photon.tmpl create mode 100644 templates/hosts.photon.tmpl create mode 100644 templates/ntp.conf.photon.tmpl create mode 100644 templates/systemd.resolved.conf.tmpl (limited to 'systemd') diff --git a/README.md b/README.md index 6f7e4c99..462e3204 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| ## To start developing cloud-init diff --git a/cloudinit/cmd/devel/net_convert.py b/cloudinit/cmd/devel/net_convert.py index 0668ffa3..5c649fd0 100755 --- a/cloudinit/cmd/devel/net_convert.py +++ b/cloudinit/cmd/devel/net_convert.py @@ -11,7 +11,7 @@ from cloudinit.sources import DataSourceAzure as azure from cloudinit.sources import DataSourceOVF as ovf from cloudinit import distros, safeyaml -from cloudinit.net import eni, netplan, network_state, sysconfig +from cloudinit.net import eni, netplan, networkd, network_state, sysconfig from cloudinit import log NAME = 'net-convert' @@ -51,7 +51,7 @@ def get_parser(parser=None): parser.add_argument("--debug", action='store_true', help='enable debug logging to stderr.') parser.add_argument("-O", "--output-kind", - choices=['eni', 'netplan', 'sysconfig'], + choices=['eni', 'netplan', 'networkd', 'sysconfig'], required=True, help="The network config format to emit") return parser @@ -118,9 +118,14 @@ def handle_args(name, args): config['netplan_path'] = config['netplan_path'][1:] # enable some netplan features config['features'] = ['dhcp-use-domains', 'ipv6-mtu'] - else: + elif args.output_kind == "networkd": + r_cls = networkd.Renderer + config = distro.renderer_configs.get('networkd') + elif args.output_kind == "sysconfig": r_cls = sysconfig.Renderer config = distro.renderer_configs.get('sysconfig') + else: + raise RuntimeError("Invalid output_kind") r = r_cls(config=config) sys.stderr.write(''.join([ diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index 70c24610..acf3251d 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -25,7 +25,7 @@ frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 distros = ['almalinux', 'alpine', 'centos', 'debian', 'fedora', 'opensuse', - 'rhel', 'rocky', 'sles', 'ubuntu'] + 'photon', 'rhel', 'rocky', 'sles', 'ubuntu'] NTP_CLIENT_CONFIG = { 'chrony': { @@ -80,24 +80,37 @@ DISTRO_CLIENT_CONFIG = { 'confpath': '/etc/chrony/chrony.conf', }, }, - 'rhel': { + 'opensuse': { + 'chrony': { + 'service_name': 'chronyd', + }, 'ntp': { + 'confpath': '/etc/ntp.conf', 'service_name': 'ntpd', }, - 'chrony': { - 'service_name': 'chronyd', + 'systemd-timesyncd': { + 'check_exe': '/usr/lib/systemd/systemd-timesyncd', }, }, - 'opensuse': { + 'photon': { 'chrony': { 'service_name': 'chronyd', }, 'ntp': { - 'confpath': '/etc/ntp.conf', 'service_name': 'ntpd', + 'confpath': '/etc/ntp.conf' }, 'systemd-timesyncd': { 'check_exe': '/usr/lib/systemd/systemd-timesyncd', + 'confpath': '/etc/systemd/timesyncd.conf', + }, + }, + 'rhel': { + 'ntp': { + 'service_name': 'ntpd', + }, + 'chrony': { + 'service_name': 'chronyd', }, }, 'sles': { @@ -551,7 +564,6 @@ def handle(name, cfg, cloud, log, _args): # Select which client is going to be used and get the configuration ntp_client_config = select_ntp_client(ntp_cfg.get('ntp_client'), cloud.distro) - # Allow user ntp config to override distro configurations ntp_client_config = util.mergemanydict( [ntp_client_config, ntp_cfg.get('config', {})], reverse=True) diff --git a/cloudinit/config/cc_resolv_conf.py b/cloudinit/config/cc_resolv_conf.py index 466dad03..c51967e2 100644 --- a/cloudinit/config/cc_resolv_conf.py +++ b/cloudinit/config/cc_resolv_conf.py @@ -30,7 +30,7 @@ are configured correctly. **Module frequency:** per instance -**Supported distros:** alpine, fedora, rhel, sles +**Supported distros:** alpine, fedora, photon, rhel, sles **Config keys**:: @@ -47,18 +47,23 @@ are configured correctly. """ from cloudinit import log as logging -from cloudinit.settings import PER_INSTANCE from cloudinit import templater +from cloudinit.settings import PER_INSTANCE from cloudinit import util LOG = logging.getLogger(__name__) frequency = PER_INSTANCE -distros = ['alpine', 'fedora', 'opensuse', 'rhel', 'sles'] +distros = ['alpine', 'fedora', 'opensuse', 'photon', 'rhel', 'sles'] + +RESOLVE_CONFIG_TEMPLATE_MAP = { + '/etc/resolv.conf': 'resolv.conf', + '/etc/systemd/resolved.conf': 'systemd.resolved.conf', +} -def generate_resolv_conf(template_fn, params, target_fname="/etc/resolv.conf"): +def generate_resolv_conf(template_fn, params, target_fname): flags = [] false_flags = [] @@ -104,12 +109,18 @@ def handle(name, cfg, cloud, log, _args): if "resolv_conf" not in cfg: log.warning("manage_resolv_conf True but no parameters provided!") - template_fn = cloud.get_template_filename('resolv.conf') - if not template_fn: + try: + template_fn = cloud.get_template_filename( + RESOLVE_CONFIG_TEMPLATE_MAP[cloud.distro.resolv_conf_fn]) + except KeyError: log.warning("No template found, not rendering /etc/resolv.conf") return - generate_resolv_conf(template_fn=template_fn, params=cfg["resolv_conf"]) + generate_resolv_conf( + template_fn=template_fn, + params=cfg["resolv_conf"], + target_fname=cloud.disro.resolve_conf_fn + ) return # vi: ts=4 expandtab diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index 7daa6bd9..67f09686 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -18,7 +18,7 @@ entry, the config entry will be skipped. **Module frequency:** per always -**Supported distros:** almalinux, centos, fedora, rhel, rocky +**Supported distros:** almalinux, centos, fedora, photon, rhel, rocky **Config keys**:: @@ -36,7 +36,7 @@ from configparser import ConfigParser from cloudinit import util -distros = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky'] +distros = ['almalinux', 'centos', 'fedora', 'photon', 'rhel', 'rocky'] def _canonicalize_id(repo_id): diff --git a/cloudinit/config/tests/test_resolv_conf.py b/cloudinit/config/tests/test_resolv_conf.py index 6546a0b5..45a06c22 100644 --- a/cloudinit/config/tests/test_resolv_conf.py +++ b/cloudinit/config/tests/test_resolv_conf.py @@ -1,9 +1,8 @@ -from unittest import mock - import pytest +from unittest import mock from cloudinit.config.cc_resolv_conf import generate_resolv_conf - +from tests.unittests.test_distros.test_create_users import MyBaseDistro EXPECTED_HEADER = """\ # Your system has been configured with 'manage-resolv-conf' set to true. @@ -14,22 +13,28 @@ EXPECTED_HEADER = """\ class TestGenerateResolvConf: + + dist = MyBaseDistro() + tmpl_fn = "templates/resolv.conf.tmpl" + @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") - def test_default_target_fname_is_etc_resolvconf(self, m_render_to_file): - generate_resolv_conf("templates/resolv.conf.tmpl", mock.MagicMock()) + def test_dist_resolv_conf_fn(self, m_render_to_file): + self.dist.resolve_conf_fn = "/tmp/resolv-test.conf" + generate_resolv_conf(self.tmpl_fn, + mock.MagicMock(), + self.dist.resolve_conf_fn) assert [ - mock.call(mock.ANY, "/etc/resolv.conf", mock.ANY) + mock.call(mock.ANY, self.dist.resolve_conf_fn, mock.ANY) ] == m_render_to_file.call_args_list @mock.patch("cloudinit.config.cc_resolv_conf.templater.render_to_file") def test_target_fname_is_used_if_passed(self, m_render_to_file): - generate_resolv_conf( - "templates/resolv.conf.tmpl", mock.MagicMock(), "/use/this/path" - ) + path = "/use/this/path" + generate_resolv_conf(self.tmpl_fn, mock.MagicMock(), path) assert [ - mock.call(mock.ANY, "/use/this/path", mock.ANY) + mock.call(mock.ANY, path, mock.ANY) ] == m_render_to_file.call_args_list # Patch in templater so we can assert on the actual generated content @@ -75,7 +80,8 @@ class TestGenerateResolvConf: def test_flags_and_options( self, m_write_file, params, expected_extra_line ): - generate_resolv_conf("templates/resolv.conf.tmpl", params) + target_fn = "/etc/resolv.conf" + generate_resolv_conf(self.tmpl_fn, params, target_fn) expected_content = EXPECTED_HEADER if expected_extra_line is not None: diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 57e33621..4991f42b 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -46,7 +46,8 @@ OSFAMILIES = { 'debian': ['debian', 'ubuntu'], 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], - 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'rhel', 'rocky'], + 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'photon', 'rhel', + 'rocky'], 'suse': ['opensuse', 'sles'], } @@ -80,6 +81,7 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): _ci_pkl_version = 1 prefer_fqdn = False + resolve_conf_fn = "/etc/resolv.conf" def __init__(self, name, cfg, paths): self._paths = paths diff --git a/cloudinit/distros/arch.py b/cloudinit/distros/arch.py index f8385f7f..246e6fe7 100644 --- a/cloudinit/distros/arch.py +++ b/cloudinit/distros/arch.py @@ -25,7 +25,6 @@ LOG = logging.getLogger(__name__) class Distro(distros.Distro): locale_gen_fn = "/etc/locale.gen" network_conf_dir = "/etc/netctl" - resolve_conf_fn = "/etc/resolv.conf" init_cmd = ['systemctl'] # init scripts renderer_configs = { "netplan": {"netplan_path": "/etc/netplan/50-cloud-init.yaml", diff --git a/cloudinit/distros/gentoo.py b/cloudinit/distros/gentoo.py index e9b82602..68c03e7f 100644 --- a/cloudinit/distros/gentoo.py +++ b/cloudinit/distros/gentoo.py @@ -23,7 +23,6 @@ LOG = logging.getLogger(__name__) class Distro(distros.Distro): locale_conf_fn = '/etc/locale.gen' network_conf_fn = '/etc/conf.d/net' - resolve_conf_fn = '/etc/resolv.conf' hostname_conf_fn = '/etc/conf.d/hostname' init_cmd = ['rc-service'] # init scripts diff --git a/cloudinit/distros/opensuse.py b/cloudinit/distros/opensuse.py index 7ca0ef99..270cc189 100644 --- a/cloudinit/distros/opensuse.py +++ b/cloudinit/distros/opensuse.py @@ -27,7 +27,6 @@ class Distro(distros.Distro): locale_conf_fn = '/etc/sysconfig/language' network_conf_fn = '/etc/sysconfig/network/config' network_script_tpl = '/etc/sysconfig/network/ifcfg-%s' - resolve_conf_fn = '/etc/resolv.conf' route_conf_tpl = '/etc/sysconfig/network/ifroute-%s' systemd_hostname_conf_fn = '/etc/hostname' systemd_locale_conf_fn = '/etc/locale.conf' diff --git a/cloudinit/distros/photon.py b/cloudinit/distros/photon.py new file mode 100644 index 00000000..8b78f98f --- /dev/null +++ b/cloudinit/distros/photon.py @@ -0,0 +1,355 @@ +#!/usr/bin/env python3 +# vi: ts=4 expandtab +# +# Copyright (C) 2021 VMware Inc. +# +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit import util +from cloudinit import subp +from cloudinit import distros +from cloudinit import helpers +from cloudinit import log as logging +from cloudinit.distros import net_util +from cloudinit.settings import PER_INSTANCE +from cloudinit.distros import rhel_util as rhutil +from cloudinit.net.network_state import mask_to_net_prefix +from cloudinit.distros.parsers.hostname import HostnameConf + +LOG = logging.getLogger(__name__) + + +class Distro(distros.Distro): + hostname_conf_fn = '/etc/hostname' + network_conf_dir = '/etc/systemd/network/' + systemd_locale_conf_fn = '/etc/locale.conf' + resolve_conf_fn = '/etc/systemd/resolved.conf' + + renderer_configs = { + 'networkd': { + 'resolv_conf_fn': resolve_conf_fn, + 'network_conf_dir': network_conf_dir, + } + } + + # Should be fqdn if we can use it + prefer_fqdn = True + + def __init__(self, name, cfg, paths): + distros.Distro.__init__(self, name, cfg, paths) + # This will be used to restrict certain + # calls from repeatly happening (when they + # should only happen say once per instance...) + self._runner = helpers.Runners(paths) + self.osfamily = 'photon' + self.init_cmd = ['systemctl'] + + def exec_cmd(self, cmd, capture=False): + LOG.debug('Attempting to run: %s', cmd) + try: + (out, err) = subp.subp(cmd, capture=capture) + if err: + LOG.warning('Running %s resulted in stderr output: %s', + cmd, err) + return True, out, err + except subp.ProcessExecutionError: + util.logexc(LOG, 'Command %s failed', cmd) + return False, None, None + + def apply_locale(self, locale, out_fn=None): + # This has a dependancy on glibc-i18n, user need to manually install it + # and enable the option in cloud.cfg + if not out_fn: + out_fn = self.systemd_locale_conf_fn + + locale_cfg = { + 'LANG': locale, + } + + rhutil.update_sysconfig_file(out_fn, locale_cfg) + + # rhutil will modify /etc/locale.conf + # For locale change to take effect, reboot is needed or we can restart + # systemd-localed. This is equivalent of localectl + cmd = ['systemctl', 'restart', 'systemd-localed'] + _ret, _out, _err = self.exec_cmd(cmd) + + def install_packages(self, pkglist): + # self.update_package_sources() + self.package_command('install', pkgs=pkglist) + + def _write_network_config(self, netconfig): + if isinstance(netconfig, str): + self._write_network_(netconfig) + return + return self._supported_write_network_config(netconfig) + + def _bring_up_interfaces(self, device_names): + cmd = ['systemctl', 'restart', 'systemd-networkd', 'systemd-resolved'] + LOG.debug('Attempting to run bring up interfaces using command %s', + cmd) + ret, _out, _err = self.exec_cmd(cmd) + return ret + + def _write_hostname(self, hostname, out_fn): + conf = None + try: + # Try to update the previous one + # Let's see if we can read it first. + conf = HostnameConf(util.load_file(out_fn)) + conf.parse() + except IOError: + pass + if not conf: + conf = HostnameConf('') + conf.set_hostname(hostname) + util.write_file(out_fn, str(conf), mode=0o644) + + def _read_system_hostname(self): + sys_hostname = self._read_hostname(self.hostname_conf_fn) + return (self.hostname_conf_fn, sys_hostname) + + def _read_hostname(self, filename, default=None): + _ret, out, _err = self.exec_cmd(['hostname']) + + return out if out else default + + def _get_localhost_ip(self): + return '127.0.1.1' + + def set_timezone(self, tz): + distros.set_etc_timezone(tz=tz, tz_file=self._find_tz_file(tz)) + + def package_command(self, command, args=None, pkgs=None): + if pkgs is None: + pkgs = [] + + cmd = ['tdnf', '-y'] + if args and isinstance(args, str): + cmd.append(args) + elif args and isinstance(args, list): + cmd.extend(args) + + cmd.append(command) + + pkglist = util.expand_package_list('%s-%s', pkgs) + cmd.extend(pkglist) + + # Allow the output of this to flow outwards (ie not be captured) + _ret, _out, _err = self.exec_cmd(cmd, capture=False) + + def update_package_sources(self): + self._runner.run('update-sources', self.package_command, + ['makecache'], freq=PER_INSTANCE) + + def _generate_resolv_conf(self): + resolv_conf_fn = self.resolve_conf_fn + resolv_templ_fn = 'systemd.resolved.conf' + + return resolv_conf_fn, resolv_templ_fn + + def _write_network_(self, settings): + entries = net_util.translate_network(settings) + LOG.debug('Translated ubuntu style network settings %s into %s', + settings, entries) + route_entries = [] + route_entries = translate_routes(settings) + dev_names = entries.keys() + nameservers = [] + searchdomains = [] + # Format for systemd + for (dev, info) in entries.items(): + if 'dns-nameservers' in info: + nameservers.extend(info['dns-nameservers']) + if 'dns-search' in info: + searchdomains.extend(info['dns-search']) + if dev == 'lo': + continue + + net_fn = self.network_conf_dir + '10-cloud-init-' + dev + net_fn += '.network' + dhcp_enabled = 'no' + if info.get('bootproto') == 'dhcp': + if (settings.find('inet dhcp') >= 0 and + settings.find('inet6 dhcp') >= 0): + dhcp_enabled = 'yes' + else: + if info.get('inet6') is True: + dhcp_enabled = 'ipv6' + else: + dhcp_enabled = 'ipv4' + + net_cfg = { + 'Name': dev, + 'DHCP': dhcp_enabled, + } + + if info.get('hwaddress'): + net_cfg['MACAddress'] = info.get('hwaddress') + if info.get('address'): + net_cfg['Address'] = '%s' % (info.get('address')) + if info.get('netmask'): + net_cfg['Address'] += '/%s' % ( + mask_to_net_prefix(info.get('netmask'))) + if info.get('gateway'): + net_cfg['Gateway'] = info.get('gateway') + if info.get('dns-nameservers'): + net_cfg['DNS'] = str( + tuple(info.get('dns-nameservers'))).replace(',', '') + if info.get('dns-search'): + net_cfg['Domains'] = str( + tuple(info.get('dns-search'))).replace(',', '') + route_entry = [] + if dev in route_entries: + route_entry = route_entries[dev] + route_index = 0 + found = True + while found: + route_name = 'routes.' + str(route_index) + if route_name in route_entries[dev]: + val = str(tuple(route_entries[dev][route_name])) + val = val.replace(',', '') + if val: + net_cfg[route_name] = val + else: + found = False + route_index += 1 + + if info.get('auto'): + self._write_interface_file(net_fn, net_cfg, route_entry) + + resolve_data = [] + new_resolve_data = [] + with open(self.resolve_conf_fn, 'r') as rf: + resolve_data = rf.readlines() + LOG.debug('Old Resolve Data\n') + LOG.debug('%s', resolve_data) + for item in resolve_data: + if ((nameservers and ('DNS=' in item)) or + (searchdomains and ('Domains=' in item))): + continue + else: + new_resolve_data.append(item) + + new_resolve_data = new_resolve_data + \ + convert_resolv_conf(nameservers, searchdomains) + LOG.debug('New resolve data\n') + LOG.debug('%s', new_resolve_data) + if nameservers or searchdomains: + util.write_file(self.resolve_conf_fn, ''.join(new_resolve_data)) + + return dev_names + + def _write_interface_file(self, net_fn, net_cfg, route_entry): + if not net_cfg['Name']: + return + content = '[Match]\n' + content += 'Name=%s\n' % (net_cfg['Name']) + if 'MACAddress' in net_cfg: + content += 'MACAddress=%s\n' % (net_cfg['MACAddress']) + content += '[Network]\n' + + if 'DHCP' in net_cfg and net_cfg['DHCP'] in {'yes', 'ipv4', 'ipv6'}: + content += 'DHCP=%s\n' % (net_cfg['DHCP']) + else: + if 'Address' in net_cfg: + content += 'Address=%s\n' % (net_cfg['Address']) + if 'Gateway' in net_cfg: + content += 'Gateway=%s\n' % (net_cfg['Gateway']) + if 'DHCP' in net_cfg and net_cfg['DHCP'] == 'no': + content += 'DHCP=%s\n' % (net_cfg['DHCP']) + + route_index = 0 + found = True + if route_entry: + while found: + route_name = 'routes.' + str(route_index) + if route_name in route_entry: + content += '[Route]\n' + if len(route_entry[route_name]) != 2: + continue + content += 'Gateway=%s\n' % ( + route_entry[route_name][0]) + content += 'Destination=%s\n' % ( + route_entry[route_name][1]) + else: + found = False + route_index += 1 + + util.write_file(net_fn, content) + + +def convert_resolv_conf(nameservers, searchdomains): + ''' Returns a string formatted for resolv.conf ''' + result = [] + if nameservers: + nslist = 'DNS=' + for ns in nameservers: + nslist = nslist + '%s ' % ns + nslist = nslist + '\n' + result.append(str(nslist)) + if searchdomains: + sdlist = 'Domains=' + for sd in searchdomains: + sdlist = sdlist + '%s ' % sd + sdlist = sdlist + '\n' + result.append(str(sdlist)) + return result + + +def translate_routes(settings): + entries = [] + for line in settings.splitlines(): + line = line.strip() + if not line or line.startswith('#'): + continue + split_up = line.split(None, 1) + if len(split_up) <= 1: + continue + entries.append(split_up) + consume = {} + ifaces = [] + for (cmd, args) in entries: + if cmd == 'iface': + if consume: + ifaces.append(consume) + consume = {} + consume[cmd] = args + else: + consume[cmd] = args + + absorb = False + for (cmd, args) in consume.items(): + if cmd == 'iface': + absorb = True + if absorb: + ifaces.append(consume) + out_ifaces = {} + for info in ifaces: + if 'iface' not in info: + continue + iface_details = info['iface'].split(None) + dev_name = None + if len(iface_details) >= 1: + dev = iface_details[0].strip().lower() + if dev: + dev_name = dev + if not dev_name: + continue + route_info = {} + route_index = 0 + found = True + while found: + route_name = 'routes.' + str(route_index) + if route_name in info: + val = info[route_name].split() + if val: + route_info[route_name] = val + else: + found = False + route_index += 1 + if dev_name in out_ifaces: + out_ifaces[dev_name].update(route_info) + else: + out_ifaces[dev_name] = route_info + return out_ifaces diff --git a/cloudinit/distros/rhel.py b/cloudinit/distros/rhel.py index 0c00a531..80a6f1d8 100644 --- a/cloudinit/distros/rhel.py +++ b/cloudinit/distros/rhel.py @@ -36,7 +36,6 @@ class Distro(distros.Distro): hostname_conf_fn = "/etc/sysconfig/network" systemd_hostname_conf_fn = "/etc/hostname" network_script_tpl = '/etc/sysconfig/network-scripts/ifcfg-%s' - resolve_conf_fn = "/etc/resolv.conf" tz_local_fn = "/etc/localtime" usr_lib_exec = "/usr/libexec" renderer_configs = { diff --git a/cloudinit/net/networkd.py b/cloudinit/net/networkd.py new file mode 100644 index 00000000..71f87995 --- /dev/null +++ b/cloudinit/net/networkd.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python3 +# vi: ts=4 expandtab +# +# Copyright (C) 2021 VMware Inc. +# +# Author: Shreenidhi Shedi +# +# This file is part of cloud-init. See LICENSE file for license information. + +import os + + +from . import renderer +from cloudinit import util +from cloudinit import subp +from cloudinit import log as logging +from collections import OrderedDict + +LOG = logging.getLogger(__name__) + + +class CfgParser: + def __init__(self): + self.conf_dict = OrderedDict({ + 'Match': [], + 'Link': [], + 'Network': [], + 'DHCPv4': [], + 'DHCPv6': [], + 'Address': [], + 'Route': [], + }) + + def update_section(self, sec, key, val): + for k in self.conf_dict.keys(): + if k == sec: + self.conf_dict[k].append(key+'='+str(val)) + self.conf_dict[k].sort() + + def get_final_conf(self): + contents = '' + for k, v in self.conf_dict.items(): + if not v: + continue + contents += '['+k+']\n' + for e in v: + contents += e + '\n' + contents += '\n' + + return contents + + def dump_data(self, target_fn): + if not target_fn: + LOG.warning('Target file not given') + return + + contents = self.get_final_conf() + LOG.debug('Final content: %s', contents) + util.write_file(target_fn, contents) + + +class Renderer(renderer.Renderer): + """ + Renders network information in /etc/systemd/network + + This Renderer is currently experimental and doesn't support all the + use cases supported by the other renderers yet. + """ + + def __init__(self, config=None): + if not config: + config = {} + self.resolved_conf = config.get('resolved_conf_fn', + '/etc/systemd/resolved.conf') + self.network_conf_dir = config.get('network_conf_dir', + '/etc/systemd/network/') + + def generate_match_section(self, iface, cfg): + sec = 'Match' + match_dict = { + 'name': 'Name', + 'driver': 'Driver', + 'mac_address': 'MACAddress' + } + + if not iface: + return + + for k, v in match_dict.items(): + if k in iface and iface[k]: + cfg.update_section(sec, v, iface[k]) + + return iface['name'] + + def generate_link_section(self, iface, cfg): + sec = 'Link' + + if not iface: + return + + if 'mtu' in iface and iface['mtu']: + cfg.update_section(sec, 'MTUBytes', iface['mtu']) + + def parse_routes(self, conf, cfg): + sec = 'Route' + for k, v in conf.items(): + if k == 'gateway': + cfg.update_section(sec, 'Gateway', v) + elif k == 'network': + tmp = v + if 'prefix' in conf: + tmp += '/' + str(conf['prefix']) + cfg.update_section(sec, 'Destination', tmp) + elif k == 'metric': + cfg.update_section(sec, 'Metric', v) + + def parse_subnets(self, iface, cfg): + dhcp = 'no' + for e in iface.get('subnets', []): + t = e['type'] + if t == 'dhcp4' or t == 'dhcp': + if dhcp == 'no': + dhcp = 'ipv4' + elif dhcp == 'ipv6': + dhcp = 'yes' + elif t == 'dhcp6': + if dhcp == 'no': + dhcp = 'ipv6' + elif dhcp == 'ipv4': + dhcp = 'yes' + if 'routes' in e and e['routes']: + for i in e['routes']: + self.parse_routes(i, cfg) + elif 'address' in e: + for k, v in e.items(): + if k == 'address': + tmp = v + if 'prefix' in e: + tmp += '/' + str(e['prefix']) + cfg.update_section('Address', 'Address', tmp) + elif k == 'gateway': + cfg.update_section('Route', 'Gateway', v) + elif k == 'dns_nameservers': + cfg.update_section('Network', 'DNS', ' '.join(v)) + elif k == 'dns_search': + cfg.update_section('Network', 'Domains', ' '.join(v)) + + cfg.update_section('Network', 'DHCP', dhcp) + + # This is to accommodate extra keys present in VMware config + def dhcp_domain(self, d, cfg): + for item in ['dhcp4domain', 'dhcp6domain']: + if item not in d: + continue + ret = str(d[item]).casefold() + try: + ret = util.translate_bool(ret) + ret = 'yes' if ret else 'no' + except ValueError: + if ret != 'route': + LOG.warning('Invalid dhcp4domain value - %s', ret) + ret = 'no' + if item == 'dhcp4domain': + section = 'DHCPv4' + else: + section = 'DHCPv6' + cfg.update_section(section, 'UseDomains', ret) + + def parse_dns(self, iface, cfg, ns): + sec = 'Network' + + dns_cfg_map = { + 'search': 'Domains', + 'nameservers': 'DNS', + 'addresses': 'DNS', + } + + dns = iface.get('dns') + if not dns and ns.version == 1: + dns = { + 'search': ns.dns_searchdomains, + 'nameservers': ns.dns_nameservers, + } + elif not dns and ns.version == 2: + return + + for k, v in dns_cfg_map.items(): + if k in dns and dns[k]: + cfg.update_section(sec, v, ' '.join(dns[k])) + + def create_network_file(self, link, conf, nwk_dir): + net_fn_owner = 'systemd-network' + + LOG.debug('Setting Networking Config for %s', link) + + net_fn = nwk_dir + '10-cloud-init-' + link + '.network' + util.write_file(net_fn, conf) + util.chownbyname(net_fn, net_fn_owner, net_fn_owner) + + def render_network_state(self, network_state, templates=None, target=None): + fp_nwkd = self.network_conf_dir + if target: + fp_nwkd = subp.target_path(target) + fp_nwkd + + util.ensure_dir(os.path.dirname(fp_nwkd)) + + ret_dict = self._render_content(network_state) + for k, v in ret_dict.items(): + self.create_network_file(k, v, fp_nwkd) + + def _render_content(self, ns): + ret_dict = {} + for iface in ns.iter_interfaces(): + cfg = CfgParser() + + link = self.generate_match_section(iface, cfg) + self.generate_link_section(iface, cfg) + self.parse_subnets(iface, cfg) + self.parse_dns(iface, cfg, ns) + + for route in ns.iter_routes(): + self.parse_routes(route, cfg) + + if ns.version == 2: + name = iface['name'] + # network state doesn't give dhcp domain info + # using ns.config as a workaround here + self.dhcp_domain(ns.config['ethernets'][name], cfg) + + ret_dict.update({link: cfg.get_final_conf()}) + + return ret_dict + + +def available(target=None): + expected = ['systemctl'] + search = ['/usr/bin', '/bin'] + for p in expected: + if not subp.which(p, search=search, target=target): + return False + return True + + +def network_state_to_networkd(ns): + renderer = Renderer({}) + return renderer._render_content(ns) diff --git a/cloudinit/net/renderers.py b/cloudinit/net/renderers.py index e2de4d55..c3931a98 100644 --- a/cloudinit/net/renderers.py +++ b/cloudinit/net/renderers.py @@ -4,6 +4,7 @@ from . import eni from . import freebsd from . import netbsd from . import netplan +from . import networkd from . import RendererNotFoundError from . import openbsd from . import sysconfig @@ -13,12 +14,13 @@ NAME_TO_RENDERER = { "freebsd": freebsd, "netbsd": netbsd, "netplan": netplan, + "networkd": networkd, "openbsd": openbsd, "sysconfig": sysconfig, } DEFAULT_PRIORITY = ["eni", "sysconfig", "netplan", "freebsd", - "netbsd", "openbsd"] + "netbsd", "openbsd", "networkd"] def search(priority=None, target=None, first=False): diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index f9bc31be..a1ccb1dc 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -177,6 +177,17 @@ OS_RELEASE_UBUNTU = dedent("""\ UBUNTU_CODENAME=xenial\n """) +OS_RELEASE_PHOTON = ("""\ + NAME="VMware Photon OS" + VERSION="4.0" + ID=photon + VERSION_ID=4.0 + PRETTY_NAME="VMware Photon OS/Linux" + ANSI_COLOR="1;34" + HOME_URL="https://vmware.github.io/photon/" + BUG_REPORT_URL="https://github.com/vmware/photon/issues" +""") + class FakeCloud(object): @@ -609,6 +620,15 @@ class TestGetLinuxDistro(CiTestCase): self.assertEqual( ('opensuse-tumbleweed', '20180920', platform.machine()), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_photon_os_release(self, m_os_release, m_path_exists): + """Verify we get the correct name and machine arch on PhotonOS""" + m_os_release.return_value = OS_RELEASE_PHOTON + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual( + ('photon', '4.0', 'VMware Photon OS/Linux'), dist) + @mock.patch('platform.system') @mock.patch('platform.dist', create=True) def test_get_linux_distro_no_data(self, m_platform_dist, diff --git a/cloudinit/util.py b/cloudinit/util.py index f95dc435..7995c6c8 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -483,6 +483,8 @@ def get_linux_distro(): # which will include both version codename and architecture # on all distributions. flavor = platform.machine() + elif distro_name == 'photon': + flavor = os_release.get('PRETTY_NAME', '') else: flavor = os_release.get('VERSION_CODENAME', '') if not flavor: @@ -531,7 +533,7 @@ def system_info(): linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'fedora', - 'rhel', 'rocky', 'suse'): + 'photon', 'rhel', 'rocky', 'suse'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 586384e4..d6dbb833 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -11,11 +11,21 @@ syslog_fix_perms: root:root # when a 'default' entry is found it will reference the 'default_user' # from the distro configuration specified below users: +{% if variant in ["photon"] %} + - name: root + lock_passwd: false +{% else %} - default +{% endif %} + +# VMware guest customization. +{% if variant in ["photon"] %} +disable_vmware_customization: true +{% endif %} # If this is set, 'root' will not be able to ssh in and they # will get a message to login instead as the default $user -{% if variant in ["freebsd"] %} +{% if variant in ["freebsd", "photon"] %} disable_root: false {% else %} disable_root: true @@ -38,6 +48,16 @@ preserve_hostname: false # This should not be required, but leave it in place until the real cause of # not finding -any- datasources is resolved. datasource_list: ['NoCloud', 'ConfigDrive', 'Azure', 'OpenStack', 'Ec2'] +{% elif variant in ["photon"] %} +# Datasources to check for cloud-config +datasource_list: [ + NoCloud, + ConfigDrive, + OVF, + OpenStack, + VMwareGuestInfo, + None + ] {% endif %} # Example datasource config # datasource: @@ -72,11 +92,13 @@ cloud_init_modules: - set_hostname - update_hostname - update_etc_hosts -{% if variant in ["alpine"] %} +{% if variant in ["alpine", "photon"] %} - resolv_conf {% endif %} {% if not variant.endswith("bsd") %} +{% if variant not in ["photon"] %} - ca-certs +{% endif %} - rsyslog {% endif %} - users-groups @@ -90,11 +112,15 @@ cloud_config_modules: - emit_upstart - snap {% endif %} +{% if variant not in ["photon"] %} - ssh-import-id - locale +{% endif %} - set-passwords -{% if variant in ["rhel", "fedora"] %} +{% if variant in ["rhel", "fedora", "photon"] %} +{% if variant not in ["photon"] %} - spacewalk +{% endif %} - yum-add-repo {% endif %} {% if variant in ["ubuntu", "unknown", "debian"] %} @@ -155,8 +181,8 @@ cloud_final_modules: system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "rhel", "rocky", - "suse", "ubuntu"] %} + "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", + "rocky", "suse", "ubuntu"] %} distro: {{ variant }} {% elif variant in ["dragonfly"] %} distro: dragonflybsd @@ -276,6 +302,22 @@ system_info: groups: [wheel] sudo: ["ALL=(ALL) NOPASSWD:ALL"] shell: /bin/ksh +{% elif variant == "photon" %} + default_user: + name: photon + lock_passwd: True + gecos: PhotonOS + groups: [wheel] + sudo: ["ALL=(ALL) NOPASSWD:ALL"] + shell: /bin/bash + # Other config here will be given to the distro class and/or path classes + paths: + cloud_dir: /var/lib/cloud/ + templates_dir: /etc/cloud/templates/ + + ssh_svcname: sshd + +#manage_etc_hosts: true {% endif %} {% if variant in ["freebsd", "netbsd", "openbsd"] %} network: diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index 4da1a905..c773e411 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -1,7 +1,9 @@ ## template:jinja [Unit] Description=Initial cloud-init job (metadata service crawler) +{% if variant not in ["photon"] %} DefaultDependencies=no +{% endif %} Wants=cloud-init-local.service Wants=sshd-keygen.service Wants=sshd.service diff --git a/templates/chrony.conf.photon.tmpl b/templates/chrony.conf.photon.tmpl new file mode 100644 index 00000000..8551f793 --- /dev/null +++ b/templates/chrony.conf.photon.tmpl @@ -0,0 +1,48 @@ +## template:jinja +# Use public servers from the pool.ntp.org project. +# Please consider joining the pool (http://www.pool.ntp.org/join.html). +{% if pools %}# pools +{% endif %} +{% for pool in pools -%} +pool {{pool}} iburst +{% endfor %} +{%- if servers %}# servers +{% endif %} +{% for server in servers -%} +server {{server}} iburst +{% endfor %} + +# Record the rate at which the system clock gains/losses time. +driftfile /var/lib/chrony/drift + +# Allow the system clock to be stepped in the first three updates +# if its offset is larger than 1 second. +makestep 1.0 3 + +# Enable kernel synchronization of the real-time clock (RTC). +rtcsync + +# Enable hardware timestamping on all interfaces that support it. +#hwtimestamp * + +# Increase the minimum number of selectable sources required to adjust +# the system clock. +#minsources 2 + +# Allow NTP client access from local network. +#allow 192.168.0.0/16 + +# Serve time even if not synchronized to a time source. +#local stratum 10 + +# Specify file containing keys for NTP authentication. +#keyfile /etc/chrony.keys + +# Get TAI-UTC offset and leap seconds from the system tz database. +leapsectz right/UTC + +# Specify directory for log files. +logdir /var/log/chrony + +# Select which information is logged. +#log measurements statistics tracking diff --git a/templates/hosts.photon.tmpl b/templates/hosts.photon.tmpl new file mode 100644 index 00000000..0fd6f722 --- /dev/null +++ b/templates/hosts.photon.tmpl @@ -0,0 +1,22 @@ +## template:jinja +{# +This file /etc/cloud/templates/hosts.photon.tmpl is only utilized +if enabled in cloud-config. Specifically, in order to enable it +you need to add the following to config: + manage_etc_hosts: True +-#} +# Your system has configured 'manage_etc_hosts' as True. +# As a result, if you wish for changes to this file to persist +# then you will need to either +# a.) make changes to the master file in /etc/cloud/templates/hosts.photon.tmpl +# b.) change or remove the value of 'manage_etc_hosts' in +# /etc/cloud/cloud.cfg or cloud-config from user-data +# +# The following lines are desirable for IPv4 capable hosts +127.0.0.1 {{fqdn}} {{hostname}} +127.0.0.1 localhost.localdomain localhost +127.0.0.1 localhost4.localdomain4 localhost4 + +# The following lines are desirable for IPv6 capable hosts +::1 {{fqdn}} {{hostname}} +::1 localhost6.localdomain6 localhost6 diff --git a/templates/ntp.conf.photon.tmpl b/templates/ntp.conf.photon.tmpl new file mode 100644 index 00000000..4d4910d1 --- /dev/null +++ b/templates/ntp.conf.photon.tmpl @@ -0,0 +1,61 @@ +## template:jinja + +# For more information about this file, see the man pages +# ntp.conf(5), ntp_acc(5), ntp_auth(5), ntp_clock(5), ntp_misc(5), ntp_mon(5). + +driftfile /var/lib/ntp/drift + +# Permit time synchronization with our time source, but do not +# permit the source to query or modify the service on this system. +restrict default kod nomodify notrap nopeer noquery +restrict -6 default kod nomodify notrap nopeer noquery + +# Permit all access over the loopback interface. This could +# be tightened as well, but to do so would effect some of +# the administrative functions. +restrict 127.0.0.1 +restrict -6 ::1 + +# Hosts on local network are less restricted. +#restrict 192.168.1.0 mask 255.255.255.0 nomodify notrap + +# Use public servers from the pool.ntp.org project. +# Please consider joining the pool (http://www.pool.ntp.org/join.html). +{% if pools %}# pools +{% endif %} +{% for pool in pools -%} +pool {{pool}} iburst +{% endfor %} +{%- if servers %}# servers +{% endif %} +{% for server in servers -%} +server {{server}} iburst +{% endfor %} + +#broadcast 192.168.1.255 autokey # broadcast server +#broadcastclient # broadcast client +#broadcast 224.0.1.1 autokey # multicast server +#multicastclient 224.0.1.1 # multicast client +#manycastserver 239.255.254.254 # manycast server +#manycastclient 239.255.254.254 autokey # manycast client + +# Enable public key cryptography. +#crypto + +includefile /etc/ntp/crypto/pw + +# Key file containing the keys and key identifiers used when operating +# with symmetric key cryptography. +keys /etc/ntp/keys + +# Specify the key identifiers which are trusted. +#trustedkey 4 8 42 + +# Specify the key identifier to use with the ntpdc utility. +#requestkey 8 + +# Specify the key identifier to use with the ntpq utility. +#controlkey 8 + +# Enable writing of statistics records. +#statistics clockstats cryptostats loopstats peerstats diff --git a/templates/resolv.conf.tmpl b/templates/resolv.conf.tmpl index f870be67..72a37bf7 100644 --- a/templates/resolv.conf.tmpl +++ b/templates/resolv.conf.tmpl @@ -22,7 +22,7 @@ domain {{domain}} sortlist {% for sort in sortlist %}{{sort}} {% endfor %} {% endif %} {# - Flags and options are required to be on the + Flags and options are required to be on the same line preceded by "options" keyword #} {% if options or flags %} diff --git a/templates/systemd.resolved.conf.tmpl b/templates/systemd.resolved.conf.tmpl new file mode 100644 index 00000000..fca50d37 --- /dev/null +++ b/templates/systemd.resolved.conf.tmpl @@ -0,0 +1,15 @@ +## template:jinja +# Your system has been configured with 'manage-resolv-conf' set to true. +# As a result, cloud-init has written this file with configuration data +# that it has been provided. Cloud-init, by default, will write this file +# a single time (PER_ONCE). +# +[Resolve] +LLMNR=false +{% if nameservers is defined %} +DNS={% for server in nameservers %}{{server}} {% endfor %} +{% endif %} + +{% if searchdomains is defined %} +Domains={% for search in searchdomains %}{{search}} {% endfor %} +{% endif %} diff --git a/tests/cloud_tests/util.py b/tests/cloud_tests/util.py index 7dcccbdd..49baadb0 100644 --- a/tests/cloud_tests/util.py +++ b/tests/cloud_tests/util.py @@ -23,7 +23,7 @@ from tests.cloud_tests import LOG OS_FAMILY_MAPPING = { 'debian': ['debian', 'ubuntu'], - 'redhat': ['centos', 'rhel', 'fedora'], + 'redhat': ['centos', 'photon', 'rhel', 'fedora'], 'gentoo': ['gentoo'], 'freebsd': ['freebsd'], 'suse': ['sles'], diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index fbc6ec11..fdb4026c 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -225,7 +225,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): expected_doc_sections = [ '**Supported distros:** all', ('**Supported distros:** almalinux, alpine, centos, debian, ' - 'fedora, opensuse, rhel, rocky, sles, ubuntu'), + 'fedora, opensuse, photon, rhel, rocky, sles, ubuntu'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tests/unittests/test_distros/test_netconfig.py b/tests/unittests/test_distros/test_netconfig.py index a1df066a..562ee04a 100644 --- a/tests/unittests/test_distros/test_netconfig.py +++ b/tests/unittests/test_distros/test_netconfig.py @@ -2,6 +2,7 @@ import copy import os +import re from io import StringIO from textwrap import dedent from unittest import mock @@ -15,7 +16,6 @@ from cloudinit.tests.helpers import ( from cloudinit import subp from cloudinit import util - BASE_NET_CFG = ''' auto lo iface lo inet loopback @@ -771,6 +771,103 @@ class TestNetCfgDistroArch(TestNetCfgDistroBase): with_netplan=True) +class TestNetCfgDistroPhoton(TestNetCfgDistroBase): + + def setUp(self): + super(TestNetCfgDistroPhoton, self).setUp() + self.distro = self._get_distro('photon', renderers=['networkd']) + + def create_conf_dict(self, contents): + content_dict = {} + for line in contents: + if line: + line = line.strip() + if line and re.search(r'^\[(.+)\]$', line): + content_dict[line] = [] + key = line + elif line: + assert key + content_dict[key].append(line) + + return content_dict + + def compare_dicts(self, actual, expected): + for k, v in actual.items(): + self.assertEqual(sorted(expected[k]), sorted(v)) + + def _apply_and_verify(self, apply_fn, config, expected_cfgs=None, + bringup=False): + if not expected_cfgs: + raise ValueError('expected_cfg must not be None') + + tmpd = None + with mock.patch('cloudinit.net.networkd.available') as m_avail: + m_avail.return_value = True + with self.reRooted(tmpd) as tmpd: + apply_fn(config, bringup) + + results = dir2dict(tmpd) + for cfgpath, expected in expected_cfgs.items(): + actual = self.create_conf_dict(results[cfgpath].splitlines()) + self.compare_dicts(actual, expected) + self.assertEqual(0o644, get_mode(cfgpath, tmpd)) + + def nwk_file_path(self, ifname): + return '/etc/systemd/network/10-cloud-init-%s.network' % ifname + + def net_cfg_1(self, ifname): + ret = """\ + [Match] + Name=%s + [Network] + DHCP=no + [Address] + Address=192.168.1.5/24 + [Route] + Gateway=192.168.1.254""" % ifname + return ret + + def net_cfg_2(self, ifname): + ret = """\ + [Match] + Name=%s + [Network] + DHCP=ipv4""" % ifname + return ret + + def test_photon_network_config_v1(self): + tmp = self.net_cfg_1('eth0').splitlines() + expected_eth0 = self.create_conf_dict(tmp) + + tmp = self.net_cfg_2('eth1').splitlines() + expected_eth1 = self.create_conf_dict(tmp) + + expected_cfgs = { + self.nwk_file_path('eth0'): expected_eth0, + self.nwk_file_path('eth1'): expected_eth1, + } + + self._apply_and_verify(self.distro.apply_network_config, + V1_NET_CFG, + expected_cfgs.copy()) + + def test_photon_network_config_v2(self): + tmp = self.net_cfg_1('eth7').splitlines() + expected_eth7 = self.create_conf_dict(tmp) + + tmp = self.net_cfg_2('eth9').splitlines() + expected_eth9 = self.create_conf_dict(tmp) + + expected_cfgs = { + self.nwk_file_path('eth7'): expected_eth7, + self.nwk_file_path('eth9'): expected_eth9, + } + + self._apply_and_verify(self.distro.apply_network_config, + V2_NET_CFG, + expected_cfgs.copy()) + + def get_mode(path, target=None): return os.stat(subp.target_path(target, path)).st_mode & 0o777 diff --git a/tests/unittests/test_handler/test_handler_set_hostname.py b/tests/unittests/test_handler/test_handler_set_hostname.py index 73641b70..32ca3b7e 100644 --- a/tests/unittests/test_handler/test_handler_set_hostname.py +++ b/tests/unittests/test_handler/test_handler_set_hostname.py @@ -120,6 +120,32 @@ class TestHostname(t_help.FilesystemMockingTestCase): contents = util.load_file(distro.hostname_conf_fn) self.assertEqual('blah', contents.strip()) + @mock.patch('cloudinit.distros.Distro.uses_systemd', return_value=False) + def test_photon_hostname(self, m_uses_systemd): + cfg1 = { + 'hostname': 'photon', + 'prefer_fqdn_over_hostname': True, + 'fqdn': 'test1.vmware.com', + } + cfg2 = { + 'hostname': 'photon', + 'prefer_fqdn_over_hostname': False, + 'fqdn': 'test2.vmware.com', + } + + ds = None + distro = self._fetch_distro('photon', cfg1) + paths = helpers.Paths({'cloud_dir': self.tmp}) + cc = cloud.Cloud(ds, paths, {}, distro, None) + self.patchUtils(self.tmp) + for c in [cfg1, cfg2]: + cc_set_hostname.handle('cc_set_hostname', c, cc, LOG, []) + contents = util.load_file(distro.hostname_conf_fn, decode=True) + if c['prefer_fqdn_over_hostname']: + self.assertEqual(contents.strip(), c['fqdn']) + else: + self.assertEqual(contents.strip(), c['hostname']) + def test_multiple_calls_skips_unchanged_hostname(self): """Only new hostname or fqdn values will generate a hostname call.""" distro = self._fetch_distro('debian') diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index b72a62b8..b2ddbf99 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -5,7 +5,7 @@ from cloudinit import distros from cloudinit.net import cmdline from cloudinit.net import ( eni, interface_has_own_mac, natural_sort_key, netplan, network_state, - renderers, sysconfig) + renderers, sysconfig, networkd) from cloudinit.sources.helpers import openstack from cloudinit import temp_utils from cloudinit import subp @@ -821,6 +821,28 @@ iface eth1 inet static NETWORK_CONFIGS = { 'small': { + 'expected_networkd_eth99': textwrap.dedent("""\ + [Match] + Name=eth99 + MACAddress=c0:d6:9f:2c:e8:80 + [Network] + DHCP=ipv4 + Domains=wark.maas + DNS=1.2.3.4 5.6.7.8 + [Route] + Gateway=65.61.151.37 + Destination=0.0.0.0/0 + Metric=10000 + """).rstrip(' '), + 'expected_networkd_eth1': textwrap.dedent("""\ + [Match] + Name=eth1 + MACAddress=cf:d6:af:48:e8:80 + [Network] + DHCP=no + Domains=wark.maas + DNS=1.2.3.4 5.6.7.8 + """).rstrip(' '), 'expected_eni': textwrap.dedent("""\ auto lo iface lo inet loopback @@ -938,6 +960,12 @@ NETWORK_CONFIGS = { """), }, 'v4_and_v6': { + 'expected_networkd': textwrap.dedent("""\ + [Match] + Name=iface0 + [Network] + DHCP=yes + """).rstrip(' '), 'expected_eni': textwrap.dedent("""\ auto lo iface lo inet loopback @@ -973,6 +1001,17 @@ NETWORK_CONFIGS = { """).rstrip(' '), }, 'v4_and_v6_static': { + 'expected_networkd': textwrap.dedent("""\ + [Match] + Name=iface0 + [Link] + MTUBytes=8999 + [Network] + DHCP=no + [Address] + Address=192.168.14.2/24 + Address=2001:1::1/64 + """).rstrip(' '), 'expected_eni': textwrap.dedent("""\ auto lo iface lo inet loopback @@ -1059,6 +1098,12 @@ NETWORK_CONFIGS = { """).rstrip(' '), }, 'dhcpv6_only': { + 'expected_networkd': textwrap.dedent("""\ + [Match] + Name=iface0 + [Network] + DHCP=ipv6 + """).rstrip(' '), 'expected_eni': textwrap.dedent("""\ auto lo iface lo inet loopback @@ -4986,26 +5031,199 @@ class TestEniRoundTrip(CiTestCase): files['/etc/network/interfaces'].splitlines()) +class TestNetworkdNetRendering(CiTestCase): + + def create_conf_dict(self, contents): + content_dict = {} + for line in contents: + if line: + line = line.strip() + if line and re.search(r'^\[(.+)\]$', line): + content_dict[line] = [] + key = line + elif line: + content_dict[key].append(line) + + return content_dict + + def compare_dicts(self, actual, expected): + for k, v in actual.items(): + self.assertEqual(sorted(expected[k]), sorted(v)) + + @mock.patch("cloudinit.net.util.chownbyname", return_value=True) + @mock.patch("cloudinit.net.util.get_cmdline", return_value="root=myroot") + @mock.patch("cloudinit.net.sys_dev_path") + @mock.patch("cloudinit.net.read_sys_net") + @mock.patch("cloudinit.net.get_devicelist") + def test_networkd_default_generation(self, mock_get_devicelist, + mock_read_sys_net, + mock_sys_dev_path, + m_get_cmdline, + m_chown): + tmp_dir = self.tmp_dir() + _setup_test(tmp_dir, mock_get_devicelist, + mock_read_sys_net, mock_sys_dev_path) + + network_cfg = net.generate_fallback_config() + ns = network_state.parse_net_config_data(network_cfg, + skip_broken=False) + + render_dir = os.path.join(tmp_dir, "render") + os.makedirs(render_dir) + + render_target = 'etc/systemd/network/10-cloud-init-eth1000.network' + renderer = networkd.Renderer({}) + renderer.render_network_state(ns, target=render_dir) + + self.assertTrue(os.path.exists(os.path.join(render_dir, + render_target))) + with open(os.path.join(render_dir, render_target)) as fh: + contents = fh.readlines() + + actual = self.create_conf_dict(contents) + print(actual) + + expected = textwrap.dedent("""\ + [Match] + Name=eth1000 + MACAddress=07-1c-c6-75-a4-be + [Network] + DHCP=ipv4""").rstrip(' ') + + expected = self.create_conf_dict(expected.splitlines()) + + self.compare_dicts(actual, expected) + + +class TestNetworkdRoundTrip(CiTestCase): + + def create_conf_dict(self, contents): + content_dict = {} + for line in contents: + if line: + line = line.strip() + if line and re.search(r'^\[(.+)\]$', line): + content_dict[line] = [] + key = line + elif line: + content_dict[key].append(line) + + return content_dict + + def compare_dicts(self, actual, expected): + for k, v in actual.items(): + self.assertEqual(sorted(expected[k]), sorted(v)) + + def _render_and_read(self, network_config=None, state=None, nwkd_path=None, + dir=None): + if dir is None: + dir = self.tmp_dir() + + if network_config: + ns = network_state.parse_net_config_data(network_config) + elif state: + ns = state + else: + raise ValueError("Expected data or state, got neither") + + if not nwkd_path: + nwkd_path = '/etc/systemd/network/' + + renderer = networkd.Renderer(config={'network_conf_dir': nwkd_path}) + + renderer.render_network_state(ns, target=dir) + return dir2dict(dir) + + @mock.patch("cloudinit.net.util.chownbyname", return_value=True) + def testsimple_render_small_networkd(self, m_chown): + nwk_fn1 = '/etc/systemd/network/10-cloud-init-eth99.network' + nwk_fn2 = '/etc/systemd/network/10-cloud-init-eth1.network' + entry = NETWORK_CONFIGS['small'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + + actual = files[nwk_fn1].splitlines() + actual = self.create_conf_dict(actual) + + expected = entry['expected_networkd_eth99'].splitlines() + expected = self.create_conf_dict(expected) + + self.compare_dicts(actual, expected) + + actual = files[nwk_fn2].splitlines() + actual = self.create_conf_dict(actual) + + expected = entry['expected_networkd_eth1'].splitlines() + expected = self.create_conf_dict(expected) + + self.compare_dicts(actual, expected) + + @mock.patch("cloudinit.net.util.chownbyname", return_value=True) + def testsimple_render_v4_and_v6(self, m_chown): + nwk_fn = '/etc/systemd/network/10-cloud-init-iface0.network' + entry = NETWORK_CONFIGS['v4_and_v6'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + + actual = files[nwk_fn].splitlines() + actual = self.create_conf_dict(actual) + + expected = entry['expected_networkd'].splitlines() + expected = self.create_conf_dict(expected) + + self.compare_dicts(actual, expected) + + @mock.patch("cloudinit.net.util.chownbyname", return_value=True) + def testsimple_render_v4_and_v6_static(self, m_chown): + nwk_fn = '/etc/systemd/network/10-cloud-init-iface0.network' + entry = NETWORK_CONFIGS['v4_and_v6_static'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + + actual = files[nwk_fn].splitlines() + actual = self.create_conf_dict(actual) + + expected = entry['expected_networkd'].splitlines() + expected = self.create_conf_dict(expected) + + self.compare_dicts(actual, expected) + + @mock.patch("cloudinit.net.util.chownbyname", return_value=True) + def testsimple_render_dhcpv6_only(self, m_chown): + nwk_fn = '/etc/systemd/network/10-cloud-init-iface0.network' + entry = NETWORK_CONFIGS['dhcpv6_only'] + files = self._render_and_read(network_config=yaml.load(entry['yaml'])) + + actual = files[nwk_fn].splitlines() + actual = self.create_conf_dict(actual) + + expected = entry['expected_networkd'].splitlines() + expected = self.create_conf_dict(expected) + + self.compare_dicts(actual, expected) + + class TestRenderersSelect: @pytest.mark.parametrize( - 'renderer_selected,netplan,eni,nm,scfg,sys', ( + 'renderer_selected,netplan,eni,nm,scfg,sys,networkd', ( # -netplan -ifupdown -nm -scfg -sys raises error - (net.RendererNotFoundError, False, False, False, False, False), + (net.RendererNotFoundError, False, False, False, False, False, + False), # -netplan +ifupdown -nm -scfg -sys selects eni - ('eni', False, True, False, False, False), + ('eni', False, True, False, False, False, False), # +netplan +ifupdown -nm -scfg -sys selects eni - ('eni', True, True, False, False, False), + ('eni', True, True, False, False, False, False), # +netplan -ifupdown -nm -scfg -sys selects netplan - ('netplan', True, False, False, False, False), + ('netplan', True, False, False, False, False, False), # Ubuntu with Network-Manager installed # +netplan -ifupdown +nm -scfg -sys selects netplan - ('netplan', True, False, True, False, False), + ('netplan', True, False, True, False, False, False), # Centos/OpenSuse with Network-Manager installed selects sysconfig # -netplan -ifupdown +nm -scfg +sys selects netplan - ('sysconfig', False, False, True, False, True), + ('sysconfig', False, False, True, False, True, False), + # -netplan -ifupdown -nm -scfg -sys +networkd selects networkd + ('networkd', False, False, False, False, False, True), ), ) + @mock.patch("cloudinit.net.renderers.networkd.available") @mock.patch("cloudinit.net.renderers.netplan.available") @mock.patch("cloudinit.net.renderers.sysconfig.available") @mock.patch("cloudinit.net.renderers.sysconfig.available_sysconfig") @@ -5013,7 +5231,8 @@ class TestRenderersSelect: @mock.patch("cloudinit.net.renderers.eni.available") def test_valid_renderer_from_defaults_depending_on_availability( self, m_eni_avail, m_nm_avail, m_scfg_avail, m_sys_avail, - m_netplan_avail, renderer_selected, netplan, eni, nm, scfg, sys + m_netplan_avail, m_networkd_avail, renderer_selected, + netplan, eni, nm, scfg, sys, networkd ): """Assert proper renderer per DEFAULT_PRIORITY given availability.""" m_eni_avail.return_value = eni # ifupdown pkg presence @@ -5021,6 +5240,7 @@ class TestRenderersSelect: m_scfg_avail.return_value = scfg # sysconfig presence m_sys_avail.return_value = sys # sysconfig/ifup/down presence m_netplan_avail.return_value = netplan # netplan presence + m_networkd_avail.return_value = networkd # networkd presence if isinstance(renderer_selected, str): (renderer_name, _rnd_class) = renderers.select( priority=renderers.DEFAULT_PRIORITY @@ -5094,6 +5314,12 @@ class TestNetRenderers(CiTestCase): result = sysconfig.available() self.assertTrue(result) + @mock.patch("cloudinit.net.renderers.networkd.available") + def test_networkd_available(self, m_nwkd_avail): + m_nwkd_avail.return_value = True + found = renderers.search(priority=['networkd'], first=False) + self.assertEqual('networkd', found[0][0]) + @mock.patch( "cloudinit.net.is_openvswitch_internal_interface", diff --git a/tests/unittests/test_render_cloudcfg.py b/tests/unittests/test_render_cloudcfg.py index 495e2669..275879af 100644 --- a/tests/unittests/test_render_cloudcfg.py +++ b/tests/unittests/test_render_cloudcfg.py @@ -10,7 +10,8 @@ from cloudinit import util # TODO(Look to align with tools.render-cloudcfg or cloudinit.distos.OSFAMILIES) DISTRO_VARIANTS = ["amazon", "arch", "centos", "debian", "fedora", "freebsd", - "netbsd", "openbsd", "rhel", "suse", "ubuntu", "unknown"] + "netbsd", "openbsd", "photon", "rhel", "suse", "ubuntu", + "unknown"] @pytest.mark.allow_subp_for(sys.executable) diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 9ec554bd..7e667de4 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -5,8 +5,8 @@ import os import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "rhel", "suse", "rocky", - "ubuntu", "unknown"] + "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", + "suse","rocky", "ubuntu", "unknown"] if "avoid-pep8-E402-import-not-top-of-file": -- cgit v1.2.3 From 184c836a16e9954a2cba11ae21f07923077ec904 Mon Sep 17 00:00:00 2001 From: James Falcon Date: Mon, 19 Jul 2021 14:13:21 -0500 Subject: Initial hotplug support (#936) Adds a udev script which will invoke a hotplug hook script on all net add events. The script will write some udev arguments to a systemd FIFO socket (to ensure we have only instance of cloud-init running at a time), which is then read by a new service that calls a new 'cloud-init devel hotplug-hook' command to handle the new event. This hotplug-hook command will: - Fetch the pickled datsource - Verify that the hotplug event is supported/enabled - Update the metadata for the datasource - Ensure the hotplugged device exists within the datasource - Apply the config change on the datasource metadata - Bring up the new interface (or apply global network configuration) - Save the updated metadata back to the pickle cache Also scattered in some unrelated typing where helpful --- bash_completion/cloud-init | 5 +- cloudinit/cmd/devel/hotplug_hook.py | 236 ++++++++++++++++++++++++ cloudinit/cmd/devel/parser.py | 3 + cloudinit/distros/__init__.py | 11 +- cloudinit/event.py | 1 + cloudinit/net/activators.py | 174 +++++++++++++---- cloudinit/sources/DataSourceConfigDrive.py | 10 +- cloudinit/sources/DataSourceEc2.py | 7 + cloudinit/sources/DataSourceOpenStack.py | 11 +- cloudinit/sources/__init__.py | 3 +- cloudinit/stages.py | 4 +- doc/rtd/topics/cli.rst | 4 + doc/rtd/topics/events.rst | 10 +- packages/redhat/cloud-init.spec.in | 7 + setup.py | 2 + systemd/cloud-init-generator.tmpl | 0 systemd/cloud-init-hotplugd.service | 22 +++ systemd/cloud-init-hotplugd.socket | 13 ++ tests/integration_tests/modules/test_hotplug.py | 94 ++++++++++ tests/unittests/cmd/devel/test_hotplug_hook.py | 218 ++++++++++++++++++++++ tests/unittests/test_net_activators.py | 135 ++++++++++---- tools/hook-hotplug | 21 +++ udev/10-cloud-init-hook-hotplug.rules | 6 + 23 files changed, 906 insertions(+), 91 deletions(-) create mode 100644 cloudinit/cmd/devel/hotplug_hook.py mode change 100755 => 100644 systemd/cloud-init-generator.tmpl create mode 100644 systemd/cloud-init-hotplugd.service create mode 100644 systemd/cloud-init-hotplugd.socket create mode 100644 tests/integration_tests/modules/test_hotplug.py create mode 100644 tests/unittests/cmd/devel/test_hotplug_hook.py create mode 100755 tools/hook-hotplug create mode 100644 udev/10-cloud-init-hook-hotplug.rules (limited to 'systemd') diff --git a/bash_completion/cloud-init b/bash_completion/cloud-init index a9577e9d..b9f137b1 100644 --- a/bash_completion/cloud-init +++ b/bash_completion/cloud-init @@ -28,7 +28,7 @@ _cloudinit_complete() COMPREPLY=($(compgen -W "--help --tarfile --include-userdata" -- $cur_word)) ;; devel) - COMPREPLY=($(compgen -W "--help schema net-convert" -- $cur_word)) + COMPREPLY=($(compgen -W "--help hotplug-hook schema net-convert" -- $cur_word)) ;; dhclient-hook) COMPREPLY=($(compgen -W "--help up down" -- $cur_word)) @@ -64,6 +64,9 @@ _cloudinit_complete() --frequency) COMPREPLY=($(compgen -W "--help instance always once" -- $cur_word)) ;; + hotplug-hook) + COMPREPLY=($(compgen -W "--help" -- $cur_word)) + ;; net-convert) COMPREPLY=($(compgen -W "--help --network-data --kind --directory --output-kind" -- $cur_word)) ;; diff --git a/cloudinit/cmd/devel/hotplug_hook.py b/cloudinit/cmd/devel/hotplug_hook.py new file mode 100644 index 00000000..0282f24a --- /dev/null +++ b/cloudinit/cmd/devel/hotplug_hook.py @@ -0,0 +1,236 @@ +# This file is part of cloud-init. See LICENSE file for license information. +"""Handle reconfiguration on hotplug events""" +import abc +import argparse +import os +import time + +from cloudinit import log +from cloudinit import reporting +from cloudinit.event import EventScope, EventType +from cloudinit.net import activators, read_sys_net_safe +from cloudinit.net.network_state import parse_net_config_data +from cloudinit.reporting import events +from cloudinit.stages import Init +from cloudinit.sources import DataSource + + +LOG = log.getLogger(__name__) +NAME = 'hotplug-hook' + + +def get_parser(parser=None): + """Build or extend an arg parser for hotplug-hook utility. + + @param parser: Optional existing ArgumentParser instance representing the + subcommand which will be extended to support the args of this utility. + + @returns: ArgumentParser with proper argument configuration. + """ + if not parser: + parser = argparse.ArgumentParser(prog=NAME, description=__doc__) + + parser.description = __doc__ + parser.add_argument("-d", "--devpath", required=True, + metavar="PATH", + help="sysfs path to hotplugged device") + parser.add_argument("-s", "--subsystem", required=True, + help="subsystem to act on", + choices=['net']) + parser.add_argument("-u", "--udevaction", required=True, + help="action to take", + choices=['add', 'remove']) + + return parser + + +class UeventHandler(abc.ABC): + def __init__(self, id, datasource, devpath, action, success_fn): + self.id = id + self.datasource = datasource # type: DataSource + self.devpath = devpath + self.action = action + self.success_fn = success_fn + + @abc.abstractmethod + def apply(self): + raise NotImplementedError() + + @property + @abc.abstractmethod + def config(self): + raise NotImplementedError() + + @abc.abstractmethod + def device_detected(self) -> bool: + raise NotImplementedError() + + def detect_hotplugged_device(self): + detect_presence = None + if self.action == 'add': + detect_presence = True + elif self.action == 'remove': + detect_presence = False + else: + raise ValueError('Unknown action: %s' % self.action) + + if detect_presence != self.device_detected(): + raise RuntimeError( + 'Failed to detect %s in updated metadata' % self.id) + + def success(self): + return self.success_fn() + + def update_metadata(self): + result = self.datasource.update_metadata_if_supported([ + EventType.HOTPLUG]) + if not result: + raise RuntimeError( + 'Datasource %s not updated for ' + 'event %s' % (self.datasource, EventType.HOTPLUG) + ) + return result + + +class NetHandler(UeventHandler): + def __init__(self, datasource, devpath, action, success_fn): + # convert devpath to mac address + id = read_sys_net_safe(os.path.basename(devpath), 'address') + super().__init__(id, datasource, devpath, action, success_fn) + + def apply(self): + self.datasource.distro.apply_network_config( + self.config, + bring_up=False, + ) + interface_name = os.path.basename(self.devpath) + activator = activators.select_activator() + if self.action == 'add': + if not activator.bring_up_interface(interface_name): + raise RuntimeError( + 'Failed to bring up device: {}'.format(self.devpath)) + elif self.action == 'remove': + if not activator.bring_down_interface(interface_name): + raise RuntimeError( + 'Failed to bring down device: {}'.format(self.devpath)) + + @property + def config(self): + return self.datasource.network_config + + def device_detected(self) -> bool: + netstate = parse_net_config_data(self.config) + found = [ + iface for iface in netstate.iter_interfaces() + if iface.get('mac_address') == self.id + ] + LOG.debug('Ifaces with ID=%s : %s', self.id, found) + return len(found) > 0 + + +SUBSYSTEM_PROPERTES_MAP = { + 'net': (NetHandler, EventScope.NETWORK), +} + + +def handle_hotplug( + hotplug_init: Init, devpath, subsystem, udevaction +): + handler_cls, event_scope = SUBSYSTEM_PROPERTES_MAP.get( + subsystem, (None, None) + ) + if handler_cls is None: + raise Exception( + 'hotplug-hook: cannot handle events for subsystem: {}'.format( + subsystem)) + + LOG.debug('Fetching datasource') + datasource = hotplug_init.fetch(existing="trust") + + if not hotplug_init.update_event_enabled( + event_source_type=EventType.HOTPLUG, + scope=EventScope.NETWORK + ): + LOG.debug('hotplug not enabled for event of type %s', event_scope) + return + + LOG.debug('Creating %s event handler', subsystem) + event_handler = handler_cls( + datasource=datasource, + devpath=devpath, + action=udevaction, + success_fn=hotplug_init._write_to_cache + ) # type: UeventHandler + wait_times = [1, 3, 5, 10, 30] + for attempt, wait in enumerate(wait_times): + LOG.debug( + 'subsystem=%s update attempt %s/%s', + subsystem, + attempt, + len(wait_times) + ) + try: + LOG.debug('Refreshing metadata') + event_handler.update_metadata() + LOG.debug('Detecting device in updated metadata') + event_handler.detect_hotplugged_device() + LOG.debug('Applying config change') + event_handler.apply() + LOG.debug('Updating cache') + event_handler.success() + break + except Exception as e: + LOG.debug('Exception while processing hotplug event. %s', e) + time.sleep(wait) + last_exception = e + else: + raise last_exception # type: ignore + + +def handle_args(name, args): + # Note that if an exception happens between now and when logging is + # setup, we'll only see it in the journal + hotplug_reporter = events.ReportEventStack( + name, __doc__, reporting_enabled=True + ) + + hotplug_init = Init(ds_deps=[], reporter=hotplug_reporter) + hotplug_init.read_cfg() + + log.setupLogging(hotplug_init.cfg) + if 'reporting' in hotplug_init.cfg: + reporting.update_configuration(hotplug_init.cfg.get('reporting')) + + # Logging isn't going to be setup until now + LOG.debug( + '%s called with the following arguments: {udevaction: %s, ' + 'subsystem: %s, devpath: %s}', + name, args.udevaction, args.subsystem, args.devpath + ) + LOG.debug( + '%s called with the following arguments:\n' + 'udevaction: %s\n' + 'subsystem: %s\n' + 'devpath: %s', + name, args.udevaction, args.subsystem, args.devpath + ) + + with hotplug_reporter: + try: + handle_hotplug( + hotplug_init=hotplug_init, + devpath=args.devpath, + subsystem=args.subsystem, + udevaction=args.udevaction, + ) + except Exception: + LOG.exception('Received fatal exception handling hotplug!') + raise + + LOG.debug('Exiting hotplug handler') + reporting.flush_events() + + +if __name__ == '__main__': + args = get_parser().parse_args() + handle_args(NAME, args) diff --git a/cloudinit/cmd/devel/parser.py b/cloudinit/cmd/devel/parser.py index 1a3c46a4..be304630 100644 --- a/cloudinit/cmd/devel/parser.py +++ b/cloudinit/cmd/devel/parser.py @@ -7,6 +7,7 @@ import argparse from cloudinit.config import schema +from . import hotplug_hook from . import net_convert from . import render from . import make_mime @@ -21,6 +22,8 @@ def get_parser(parser=None): subparsers.required = True subcmds = [ + (hotplug_hook.NAME, hotplug_hook.__doc__, + hotplug_hook.get_parser, hotplug_hook.handle_args), ('schema', 'Validate cloud-config files for document schema', schema.get_parser, schema.handle_schema_args), (net_convert.NAME, net_convert.__doc__, diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 2caa8bc2..7bdf2197 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -206,8 +206,15 @@ class Distro(persistence.CloudInitPickleMixin, metaclass=abc.ABCMeta): def generate_fallback_config(self): return net.generate_fallback_config() - def apply_network_config(self, netconfig, bring_up=False): - # apply network config netconfig + def apply_network_config(self, netconfig, bring_up=False) -> bool: + """Apply the network config. + + If bring_up is True, attempt to bring up the passed in devices. If + devices is None, attempt to bring up devices returned by + _write_network_config. + + Returns True if any devices failed to come up, otherwise False. + """ # This method is preferred to apply_network which only takes # a much less complete network config format (interfaces(5)). network_state = parse_net_config_data(netconfig) diff --git a/cloudinit/event.py b/cloudinit/event.py index 76a0afc6..53ad4c25 100644 --- a/cloudinit/event.py +++ b/cloudinit/event.py @@ -29,6 +29,7 @@ class EventType(Enum): BOOT = "boot" BOOT_NEW_INSTANCE = "boot-new-instance" BOOT_LEGACY = "boot-legacy" + HOTPLUG = 'hotplug' def __str__(self): # pylint: disable=invalid-str-returned return self.value diff --git a/cloudinit/net/activators.py b/cloudinit/net/activators.py index 34fee3bf..84aaafc9 100644 --- a/cloudinit/net/activators.py +++ b/cloudinit/net/activators.py @@ -15,31 +15,80 @@ from cloudinit.net.sysconfig import NM_CFG_FILE LOG = logging.getLogger(__name__) +def _alter_interface(cmd, device_name) -> bool: + LOG.debug("Attempting command %s for device %s", cmd, device_name) + try: + (_out, err) = subp.subp(cmd) + if len(err): + LOG.warning("Running %s resulted in stderr output: %s", + cmd, err) + return True + except subp.ProcessExecutionError: + util.logexc(LOG, "Running interface command %s failed", cmd) + return False + + class NetworkActivator(ABC): @staticmethod @abstractmethod def available() -> bool: + """Return True if activator is available, otherwise return False.""" raise NotImplementedError() @staticmethod @abstractmethod def bring_up_interface(device_name: str) -> bool: + """Bring up interface. + + Return True is successful, otherwise return False + """ + raise NotImplementedError() + + @staticmethod + @abstractmethod + def bring_down_interface(device_name: str) -> bool: + """Bring down interface. + + Return True is successful, otherwise return False + """ raise NotImplementedError() @classmethod def bring_up_interfaces(cls, device_names: Iterable[str]) -> bool: - all_succeeded = True - for device in device_names: - if not cls.bring_up_interface(device): - all_succeeded = False - return all_succeeded + """Bring up specified list of interfaces. + + Return True is successful, otherwise return False + """ + return all(cls.bring_up_interface(device) for device in device_names) @classmethod def bring_up_all_interfaces(cls, network_state: NetworkState) -> bool: + """Bring up all interfaces. + + Return True is successful, otherwise return False + """ return cls.bring_up_interfaces( [i['name'] for i in network_state.iter_interfaces()] ) + @classmethod + def bring_down_interfaces(cls, device_names: Iterable[str]) -> bool: + """Bring down specified list of interfaces. + + Return True is successful, otherwise return False + """ + return all(cls.bring_down_interface(device) for device in device_names) + + @classmethod + def bring_down_all_interfaces(cls, network_state: NetworkState) -> bool: + """Bring down all interfaces. + + Return True is successful, otherwise return False + """ + return cls.bring_down_interfaces( + [i['name'] for i in network_state.iter_interfaces()] + ) + class IfUpDownActivator(NetworkActivator): # Note that we're not overriding bring_up_interfaces to pass something @@ -53,24 +102,27 @@ class IfUpDownActivator(NetworkActivator): @staticmethod def bring_up_interface(device_name: str) -> bool: - """Bring up interface using ifup.""" + """Bring up interface using ifup. + + Return True is successful, otherwise return False + """ cmd = ['ifup', device_name] - LOG.debug("Attempting to run bring up interface %s using command %s", - device_name, cmd) - try: - (_out, err) = subp.subp(cmd) - if len(err): - LOG.warning("Running %s resulted in stderr output: %s", - cmd, err) - return True - except subp.ProcessExecutionError: - util.logexc(LOG, "Running interface command %s failed", cmd) - return False + return _alter_interface(cmd, device_name) + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Bring up interface using ifup. + + Return True is successful, otherwise return False + """ + cmd = ['ifdown', device_name] + return _alter_interface(cmd, device_name) class NetworkManagerActivator(NetworkActivator): @staticmethod def available(target=None) -> bool: + """ Return true if network manager can be used on this system.""" config_present = os.path.isfile( subp.target_path(target, path=NM_CFG_FILE) ) @@ -79,44 +131,86 @@ class NetworkManagerActivator(NetworkActivator): @staticmethod def bring_up_interface(device_name: str) -> bool: - try: - subp.subp(['nmcli', 'connection', 'up', device_name]) - except subp.ProcessExecutionError: - util.logexc(LOG, "nmcli failed to bring up {}".format(device_name)) - return False - return True + """Bring up interface using nmcli. + + Return True is successful, otherwise return False + """ + cmd = ['nmcli', 'connection', 'up', 'ifname', device_name] + return _alter_interface(cmd, device_name) + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Bring down interface using nmcli. + + Return True is successful, otherwise return False + """ + cmd = ['nmcli', 'connection', 'down', device_name] + return _alter_interface(cmd, device_name) class NetplanActivator(NetworkActivator): + NETPLAN_CMD = ['netplan', 'apply'] + @staticmethod def available(target=None) -> bool: + """ Return true if netplan can be used on this system.""" return netplan_available(target=target) - @staticmethod - def _apply_netplan(): - LOG.debug('Applying current netplan config') - try: - subp.subp(['netplan', 'apply'], capture=True) - except subp.ProcessExecutionError: - util.logexc(LOG, "netplan apply failed") - return False - return True - @staticmethod def bring_up_interface(device_name: str) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ LOG.debug("Calling 'netplan apply' rather than " - "bringing up individual interfaces") - return NetplanActivator._apply_netplan() + "altering individual interfaces") + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') @staticmethod def bring_up_interfaces(device_names: Iterable[str]) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ LOG.debug("Calling 'netplan apply' rather than " - "bringing up individual interfaces") - return NetplanActivator._apply_netplan() + "altering individual interfaces") + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') @staticmethod def bring_up_all_interfaces(network_state: NetworkState) -> bool: - return NetplanActivator._apply_netplan() + """Apply netplan config. + + Return True is successful, otherwise return False + """ + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') + + @staticmethod + def bring_down_interface(device_name: str) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug("Calling 'netplan apply' rather than " + "altering individual interfaces") + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') + + @staticmethod + def bring_down_interfaces(device_names: Iterable[str]) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + LOG.debug("Calling 'netplan apply' rather than " + "altering individual interfaces") + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') + + @staticmethod + def bring_down_all_interfaces(network_state: NetworkState) -> bool: + """Apply netplan config. + + Return True is successful, otherwise return False + """ + return _alter_interface(NetplanActivator.NETPLAN_CMD, 'all') # This section is mostly copied and pasted from renderers.py. An abstract @@ -153,4 +247,6 @@ def select_activator(priority=None, target=None) -> Type[NetworkActivator]: raise RuntimeError( "No available network activators found%s. Searched " "through list: %s" % (tmsg, priority)) - return found[0] + selected = found[0] + LOG.debug('Using selected activator: %s', selected) + return selected diff --git a/cloudinit/sources/DataSourceConfigDrive.py b/cloudinit/sources/DataSourceConfigDrive.py index 62756cf7..19c8d126 100644 --- a/cloudinit/sources/DataSourceConfigDrive.py +++ b/cloudinit/sources/DataSourceConfigDrive.py @@ -12,9 +12,8 @@ from cloudinit import log as logging from cloudinit import sources from cloudinit import subp from cloudinit import util - +from cloudinit.event import EventScope, EventType from cloudinit.net import eni - from cloudinit.sources.DataSourceIBMCloud import get_ibm_platform from cloudinit.sources.helpers import openstack @@ -37,6 +36,13 @@ class DataSourceConfigDrive(openstack.SourceMixin, sources.DataSource): dsname = 'ConfigDrive' + supported_update_events = {EventScope.NETWORK: { + EventType.BOOT_NEW_INSTANCE, + EventType.BOOT, + EventType.BOOT_LEGACY, + EventType.HOTPLUG, + }} + def __init__(self, sys_cfg, distro, paths): super(DataSourceConfigDrive, self).__init__(sys_cfg, distro, paths) self.source = None diff --git a/cloudinit/sources/DataSourceEc2.py b/cloudinit/sources/DataSourceEc2.py index 8a7f7c60..700437b0 100644 --- a/cloudinit/sources/DataSourceEc2.py +++ b/cloudinit/sources/DataSourceEc2.py @@ -76,6 +76,13 @@ class DataSourceEc2(sources.DataSource): # Whether we want to get network configuration from the metadata service. perform_dhcp_setup = False + supported_update_events = {EventScope.NETWORK: { + EventType.BOOT_NEW_INSTANCE, + EventType.BOOT, + EventType.BOOT_LEGACY, + EventType.HOTPLUG, + }} + def __init__(self, sys_cfg, distro, paths): super(DataSourceEc2, self).__init__(sys_cfg, distro, paths) self.metadata_address = None diff --git a/cloudinit/sources/DataSourceOpenStack.py b/cloudinit/sources/DataSourceOpenStack.py index 619a171e..a85b71d7 100644 --- a/cloudinit/sources/DataSourceOpenStack.py +++ b/cloudinit/sources/DataSourceOpenStack.py @@ -8,11 +8,11 @@ import time from cloudinit import dmi from cloudinit import log as logging -from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError from cloudinit import sources from cloudinit import url_helper from cloudinit import util - +from cloudinit.event import EventScope, EventType +from cloudinit.net.dhcp import EphemeralDHCPv4, NoDHCPLeaseError from cloudinit.sources.helpers import openstack from cloudinit.sources import DataSourceOracle as oracle @@ -46,6 +46,13 @@ class DataSourceOpenStack(openstack.SourceMixin, sources.DataSource): # Whether we want to get network configuration from the metadata service. perform_dhcp_setup = False + supported_update_events = {EventScope.NETWORK: { + EventType.BOOT_NEW_INSTANCE, + EventType.BOOT, + EventType.BOOT_LEGACY, + EventType.HOTPLUG + }} + def __init__(self, sys_cfg, distro, paths): super(DataSourceOpenStack, self).__init__(sys_cfg, distro, paths) self.metadata_address = None diff --git a/cloudinit/sources/__init__.py b/cloudinit/sources/__init__.py index 9d25b0ee..bf6bf139 100644 --- a/cloudinit/sources/__init__.py +++ b/cloudinit/sources/__init__.py @@ -23,6 +23,7 @@ from cloudinit import type_utils from cloudinit import user_data as ud from cloudinit import util from cloudinit.atomic_helper import write_json +from cloudinit.distros import Distro from cloudinit.event import EventScope, EventType from cloudinit.filters import launch_index from cloudinit.persistence import CloudInitPickleMixin @@ -215,7 +216,7 @@ class DataSource(CloudInitPickleMixin, metaclass=abc.ABCMeta): _ci_pkl_version = 1 - def __init__(self, sys_cfg, distro, paths, ud_proc=None): + def __init__(self, sys_cfg, distro: Distro, paths, ud_proc=None): self.sys_cfg = sys_cfg self.distro = distro self.paths = paths diff --git a/cloudinit/stages.py b/cloudinit/stages.py index 06e0d9b1..bc164fa0 100644 --- a/cloudinit/stages.py +++ b/cloudinit/stages.py @@ -241,7 +241,7 @@ class Init(object): else: return (None, "cache invalid in datasource: %s" % ds) - def _get_data_source(self, existing): + def _get_data_source(self, existing) -> sources.DataSource: if self.datasource is not NULL_DATA_SOURCE: return self.datasource @@ -267,7 +267,7 @@ class Init(object): cfg_list, pkg_list, self.reporter) LOG.info("Loaded datasource %s - %s", dsname, ds) - self.datasource = ds + self.datasource = ds # type: sources.DataSource # Ensure we adjust our path members datasource # now that we have one (thus allowing ipath to be used) self._reset() diff --git a/doc/rtd/topics/cli.rst b/doc/rtd/topics/cli.rst index 0ff230b5..b6115ed6 100644 --- a/doc/rtd/topics/cli.rst +++ b/doc/rtd/topics/cli.rst @@ -119,6 +119,10 @@ Current subcommands: schema errors locally without the need for deployment. Schema validation is work in progress and supports a subset of cloud-config modules. + * ``hotplug-hook``: respond to newly added system devices by retrieving + updated system metadata and bringing up/down the corresponding device. + This command is intended to be called via a systemd service and is + not considered user-accessible except for debugging purposes. .. _cli_features: diff --git a/doc/rtd/topics/events.rst b/doc/rtd/topics/events.rst index 463208cc..984e7577 100644 --- a/doc/rtd/topics/events.rst +++ b/doc/rtd/topics/events.rst @@ -20,11 +20,11 @@ event types: boot: once during Local stage, then again in Network stage. As this behavior was previously the default behavior, this option exists to prevent regressing such behavior. +- **HOTPLUG**: Dynamic add of a system device Future work will likely include infrastructure and support for the following events: -- **HOTPLUG**: Dynamic add of a system device - **METADATA_CHANGE**: An instance's metadata has change - **USER_REQUEST**: Directed request to update @@ -64,6 +64,12 @@ arbitrary values can be used. Each ``scope`` requires a ``when`` element to specify which events are to allowed to be handled. +Hotplug +======= +When the hotplug event is supported by the data source and configured in +user data, cloud-init will respond to the addition or removal of network +interfaces to the system. In addition to fetching and updating the system +metadata, cloud-init will also bring up/down the newly added interface. Examples ======== @@ -77,7 +83,7 @@ On every boot, apply network configuration found in the datasource. # apply network config on every boot updates: network: - when: ['boot'] + when: ['boot', 'hotplug'] .. _Cloud-init: https://launchpad.net/cloud-init .. vi: textwidth=78 diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in index 16138012..b930709b 100644 --- a/packages/redhat/cloud-init.spec.in +++ b/packages/redhat/cloud-init.spec.in @@ -119,6 +119,12 @@ version_pys=$(cd "$RPM_BUILD_ROOT" && find . -name version.py -type f) ( cd "$RPM_BUILD_ROOT" && sed -i "s,@@PACKAGED_VERSION@@,%{version}-%{release}," $version_pys ) +# patch hotplug /usr/libexec script path +hotplug_file=$(cd "$RPM_BUILD_ROOT" && find . -name 10-cloud-init-hook-hotplug.rules -type f) + +( cd "$RPM_BUILD_ROOT" && + sed -i "s,/usr/lib,%{_libexecdir}," $hotplug_file ) + %clean rm -rf $RPM_BUILD_ROOT @@ -172,6 +178,7 @@ fi %files /lib/udev/rules.d/66-azure-ephemeral.rules +/lib/udev/rules.d/10-cloud-init-hook-hotplug.rules %if "%{init_system}" == "systemd" /usr/lib/systemd/system-generators/cloud-init-generator diff --git a/setup.py b/setup.py index dcbe0843..7fa03e63 100755 --- a/setup.py +++ b/setup.py @@ -128,6 +128,7 @@ INITSYS_FILES = { 'systemd': [render_tmpl(f) for f in (glob('systemd/*.tmpl') + glob('systemd/*.service') + + glob('systemd/*.socket') + glob('systemd/*.target')) if (is_f(f) and not is_generator(f))], 'systemd.generators': [ @@ -249,6 +250,7 @@ data_files = [ (ETC + '/cloud/cloud.cfg.d', glob('config/cloud.cfg.d/*')), (ETC + '/cloud/templates', glob('templates/*')), (USR_LIB_EXEC + '/cloud-init', ['tools/ds-identify', + 'tools/hook-hotplug', 'tools/uncloud-init', 'tools/write-ssh-key-fingerprints']), (USR + '/share/bash-completion/completions', diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl old mode 100755 new mode 100644 diff --git a/systemd/cloud-init-hotplugd.service b/systemd/cloud-init-hotplugd.service new file mode 100644 index 00000000..b64632ef --- /dev/null +++ b/systemd/cloud-init-hotplugd.service @@ -0,0 +1,22 @@ +# Paired with cloud-init-hotplugd.socket to read from the FIFO +# /run/cloud-init/hook-hotplug-cmd which is created during a udev network +# add or remove event as processed by 10-cloud-init-hook-hotplug.rules. + +# On start, read args from the FIFO, process and provide structured arguments +# to `cloud-init devel hotplug-hook` which will setup or teardown network +# devices as configured by user-data. + +# Known bug with an enforcing SELinux policy: LP: #1936229 +# cloud-init-hotplud.service will read args from file descriptor 3 + +[Unit] +Description=cloud-init hotplug hook daemon +After=cloud-init-hotplugd.socket + +[Service] +Type=simple +ExecStart=/bin/bash -c 'read args <&3; echo "args=$args"; \ + exec /usr/bin/cloud-init devel hotplug-hook $args; \ + exit 0' +SyslogIdentifier=cloud-init-hotplugd +TimeoutStopSec=5 diff --git a/systemd/cloud-init-hotplugd.socket b/systemd/cloud-init-hotplugd.socket new file mode 100644 index 00000000..aa093016 --- /dev/null +++ b/systemd/cloud-init-hotplugd.socket @@ -0,0 +1,13 @@ +# cloud-init-hotplugd.socket listens on the FIFO file +# /run/cloud-init/hook-hotplug-cmd which is created during a udev network +# add or remove event as processed by 10-cloud-init-hook-hotplug.rules. + +# Known bug with an enforcing SELinux policy: LP: #1936229 +[Unit] +Description=cloud-init hotplug hook socket + +[Socket] +ListenFIFO=/run/cloud-init/hook-hotplug-cmd + +[Install] +WantedBy=cloud-init.target diff --git a/tests/integration_tests/modules/test_hotplug.py b/tests/integration_tests/modules/test_hotplug.py new file mode 100644 index 00000000..b683566f --- /dev/null +++ b/tests/integration_tests/modules/test_hotplug.py @@ -0,0 +1,94 @@ +import pytest +import time +import yaml +from collections import namedtuple + +from tests.integration_tests.instances import IntegrationInstance + +USER_DATA = """\ +#cloud-config +updates: + network: + when: ['hotplug'] +""" + +ip_addr = namedtuple('ip_addr', 'interface state ip4 ip6') + + +def _wait_till_hotplug_complete(client, expected_runs=1): + for _ in range(60): + log = client.read_from_file('/var/log/cloud-init.log') + if log.count('Exiting hotplug handler') == expected_runs: + return log + time.sleep(1) + raise Exception('Waiting for hotplug handler failed') + + +def _get_ip_addr(client): + ips = [] + lines = client.execute('ip --brief addr').split('\n') + for line in lines: + attributes = line.split() + interface, state = attributes[0], attributes[1] + ip4_cidr = attributes[2] if len(attributes) > 2 else None + ip6_cidr = attributes[3] if len(attributes) > 3 else None + ip4 = ip4_cidr.split('/')[0] if ip4_cidr else None + ip6 = ip6_cidr.split('/')[0] if ip6_cidr else None + ip = ip_addr(interface, state, ip4, ip6) + ips.append(ip) + return ips + + +@pytest.mark.openstack +@pytest.mark.user_data(USER_DATA) +def test_hotplug_add_remove(client: IntegrationInstance): + ips_before = _get_ip_addr(client) + log = client.read_from_file('/var/log/cloud-init.log') + assert 'Exiting hotplug handler' not in log + + # Add new NIC + added_ip = client.instance.add_network_interface() + _wait_till_hotplug_complete(client) + ips_after_add = _get_ip_addr(client) + new_addition = [ip for ip in ips_after_add if ip.ip4 == added_ip][0] + + assert len(ips_after_add) == len(ips_before) + 1 + assert added_ip not in [ip.ip4 for ip in ips_before] + assert added_ip in [ip.ip4 for ip in ips_after_add] + assert new_addition.state == 'UP' + + netplan_cfg = client.read_from_file('/etc/netplan/50-cloud-init.yaml') + config = yaml.safe_load(netplan_cfg) + assert new_addition.interface in config['network']['ethernets'] + + # Remove new NIC + client.instance.remove_network_interface(added_ip) + _wait_till_hotplug_complete(client, expected_runs=2) + ips_after_remove = _get_ip_addr(client) + assert len(ips_after_remove) == len(ips_before) + assert added_ip not in [ip.ip4 for ip in ips_after_remove] + + netplan_cfg = client.read_from_file('/etc/netplan/50-cloud-init.yaml') + config = yaml.safe_load(netplan_cfg) + assert new_addition.interface not in config['network']['ethernets'] + + +@pytest.mark.openstack +def test_no_hotplug_in_userdata(client: IntegrationInstance): + ips_before = _get_ip_addr(client) + log = client.read_from_file('/var/log/cloud-init.log') + assert 'Exiting hotplug handler' not in log + + # Add new NIC + client.instance.add_network_interface() + _wait_till_hotplug_complete(client) + log = client.read_from_file('/var/log/cloud-init.log') + assert 'hotplug not enabled for event of type network' in log + + ips_after_add = _get_ip_addr(client) + if len(ips_after_add) == len(ips_before) + 1: + # We can see the device, but it should not have been brought up + new_ip = [ip for ip in ips_after_add if ip not in ips_before][0] + assert new_ip.state == 'DOWN' + else: + assert len(ips_after_add) == len(ips_before) diff --git a/tests/unittests/cmd/devel/test_hotplug_hook.py b/tests/unittests/cmd/devel/test_hotplug_hook.py new file mode 100644 index 00000000..63d2490e --- /dev/null +++ b/tests/unittests/cmd/devel/test_hotplug_hook.py @@ -0,0 +1,218 @@ +import pytest +from collections import namedtuple +from unittest import mock +from unittest.mock import call + +from cloudinit.cmd.devel.hotplug_hook import handle_hotplug +from cloudinit.distros import Distro +from cloudinit.event import EventType +from cloudinit.net.activators import NetworkActivator +from cloudinit.net.network_state import NetworkState +from cloudinit.sources import DataSource +from cloudinit.stages import Init + + +hotplug_args = namedtuple('hotplug_args', 'udevaction, subsystem, devpath') +FAKE_MAC = '11:22:33:44:55:66' + + +@pytest.yield_fixture +def mocks(): + m_init = mock.MagicMock(spec=Init) + m_distro = mock.MagicMock(spec=Distro) + m_datasource = mock.MagicMock(spec=DataSource) + m_datasource.distro = m_distro + m_init.datasource = m_datasource + m_init.fetch.return_value = m_datasource + + read_sys_net = mock.patch( + 'cloudinit.cmd.devel.hotplug_hook.read_sys_net_safe', + return_value=FAKE_MAC + ) + + m_network_state = mock.MagicMock(spec=NetworkState) + parse_net = mock.patch( + 'cloudinit.cmd.devel.hotplug_hook.parse_net_config_data', + return_value=m_network_state + ) + + m_activator = mock.MagicMock(spec=NetworkActivator) + select_activator = mock.patch( + 'cloudinit.cmd.devel.hotplug_hook.activators.select_activator', + return_value=m_activator + ) + + sleep = mock.patch('time.sleep') + + read_sys_net.start() + parse_net.start() + select_activator.start() + m_sleep = sleep.start() + + yield namedtuple('mocks', 'm_init m_network_state m_activator m_sleep')( + m_init=m_init, + m_network_state=m_network_state, + m_activator=m_activator, + m_sleep=m_sleep, + ) + + read_sys_net.stop() + parse_net.stop() + select_activator.stop() + sleep.stop() + + +class TestUnsupportedActions: + def test_unsupported_subsystem(self, mocks): + with pytest.raises( + Exception, + match='cannot handle events for subsystem: not_real' + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + subsystem='not_real', + udevaction='add' + ) + + def test_unsupported_udevaction(self, mocks): + with pytest.raises(ValueError, match='Unknown action: not_real'): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='not_real', + subsystem='net' + ) + + +class TestHotplug: + def test_succcessful_add(self, mocks): + init = mocks.m_init + mocks.m_network_state.iter_interfaces.return_value = [{ + 'mac_address': FAKE_MAC, + }] + handle_hotplug( + hotplug_init=init, + devpath='/dev/fake', + udevaction='add', + subsystem='net' + ) + init.datasource.update_metadata_if_supported.assert_called_once_with([ + EventType.HOTPLUG + ]) + mocks.m_activator.bring_up_interface.assert_called_once_with('fake') + mocks.m_activator.bring_down_interface.assert_not_called() + init._write_to_cache.assert_called_once_with() + + def test_successful_remove(self, mocks): + init = mocks.m_init + mocks.m_network_state.iter_interfaces.return_value = [{}] + handle_hotplug( + hotplug_init=init, + devpath='/dev/fake', + udevaction='remove', + subsystem='net' + ) + init.datasource.update_metadata_if_supported.assert_called_once_with([ + EventType.HOTPLUG + ]) + mocks.m_activator.bring_down_interface.assert_called_once_with('fake') + mocks.m_activator.bring_up_interface.assert_not_called() + init._write_to_cache.assert_called_once_with() + + def test_update_event_disabled(self, mocks, caplog): + init = mocks.m_init + init.update_event_enabled.return_value = False + handle_hotplug( + hotplug_init=init, + devpath='/dev/fake', + udevaction='remove', + subsystem='net' + ) + assert 'hotplug not enabled for event of type' in caplog.text + init.datasource.update_metadata_if_supported.assert_not_called() + mocks.m_activator.bring_up_interface.assert_not_called() + mocks.m_activator.bring_down_interface.assert_not_called() + init._write_to_cache.assert_not_called() + + def test_update_metadata_failed(self, mocks): + mocks.m_init.datasource.update_metadata_if_supported.return_value = \ + False + with pytest.raises( + RuntimeError, match='Datasource .* not updated for event hotplug' + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='remove', + subsystem='net' + ) + + def test_detect_hotplugged_device_not_detected_on_add(self, mocks): + mocks.m_network_state.iter_interfaces.return_value = [{}] + with pytest.raises( + RuntimeError, + match='Failed to detect {} in updated metadata'.format(FAKE_MAC) + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='add', + subsystem='net' + ) + + def test_detect_hotplugged_device_detected_on_remove(self, mocks): + mocks.m_network_state.iter_interfaces.return_value = [{ + 'mac_address': FAKE_MAC, + }] + with pytest.raises( + RuntimeError, + match='Failed to detect .* in updated metadata' + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='remove', + subsystem='net' + ) + + def test_apply_failed_on_add(self, mocks): + mocks.m_network_state.iter_interfaces.return_value = [{ + 'mac_address': FAKE_MAC, + }] + mocks.m_activator.bring_up_interface.return_value = False + with pytest.raises( + RuntimeError, match='Failed to bring up device: /dev/fake' + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='add', + subsystem='net' + ) + + def test_apply_failed_on_remove(self, mocks): + mocks.m_network_state.iter_interfaces.return_value = [{}] + mocks.m_activator.bring_down_interface.return_value = False + with pytest.raises( + RuntimeError, match='Failed to bring down device: /dev/fake' + ): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='remove', + subsystem='net' + ) + + def test_retry(self, mocks): + with pytest.raises(RuntimeError): + handle_hotplug( + hotplug_init=mocks.m_init, + devpath='/dev/fake', + udevaction='add', + subsystem='net' + ) + assert mocks.m_sleep.call_count == 5 + assert mocks.m_sleep.call_args_list == [ + call(1), call(3), call(5), call(10), call(30) + ] diff --git a/tests/unittests/test_net_activators.py b/tests/unittests/test_net_activators.py index f11486ff..db825c35 100644 --- a/tests/unittests/test_net_activators.py +++ b/tests/unittests/test_net_activators.py @@ -35,32 +35,8 @@ ethernets: dhcp4: true """ -IF_UP_DOWN_AVAILABLE_CALLS = [ - (('ifquery',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), - (('ifup',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), - (('ifdown',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), -] - -IF_UP_DOWN_CALL_LIST = [ - ((['ifup', 'eth0'], ), {}), - ((['ifup', 'eth1'], ), {}), -] - -NETPLAN_AVAILABLE_CALLS = [ - (('netplan',), {'search': ['/usr/sbin', '/sbin'], 'target': None}), -] - NETPLAN_CALL_LIST = [ - ((['netplan', 'apply'], ), {'capture': True}), -] - -NETWORK_MANAGER_AVAILABLE_CALLS = [ - (('nmcli',), {'target': None}), -] - -NETWORK_MANAGER_CALL_LIST = [ - ((['nmcli', 'connection', 'up', 'eth0'], ), {}), - ((['nmcli', 'connection', 'up', 'eth1'], ), {}), + ((['netplan', 'apply'], ), {}), ] @@ -126,23 +102,54 @@ class TestSearchAndSelect: select_activator() -@pytest.mark.parametrize('activator, available_calls, expected_call_list', [ - (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS, IF_UP_DOWN_CALL_LIST), - (NetplanActivator, NETPLAN_AVAILABLE_CALLS, NETPLAN_CALL_LIST), - (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS, - NETWORK_MANAGER_CALL_LIST), +IF_UP_DOWN_AVAILABLE_CALLS = [ + (('ifquery',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), + (('ifup',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), + (('ifdown',), {'search': ['/sbin', '/usr/sbin'], 'target': None}), +] + +NETPLAN_AVAILABLE_CALLS = [ + (('netplan',), {'search': ['/usr/sbin', '/sbin'], 'target': None}), +] + +NETWORK_MANAGER_AVAILABLE_CALLS = [ + (('nmcli',), {'target': None}), +] + + +@pytest.mark.parametrize('activator, available_calls', [ + (IfUpDownActivator, IF_UP_DOWN_AVAILABLE_CALLS), + (NetplanActivator, NETPLAN_AVAILABLE_CALLS), + (NetworkManagerActivator, NETWORK_MANAGER_AVAILABLE_CALLS), ]) -class TestIfUpDownActivator: +class TestActivatorsAvailable: def test_available( - self, activator, available_calls, expected_call_list, available_mocks + self, activator, available_calls, available_mocks ): activator.available() assert available_mocks.m_which.call_args_list == available_calls + +IF_UP_DOWN_BRING_UP_CALL_LIST = [ + ((['ifup', 'eth0'], ), {}), + ((['ifup', 'eth1'], ), {}), +] + +NETWORK_MANAGER_BRING_UP_CALL_LIST = [ + ((['nmcli', 'connection', 'up', 'ifname', 'eth0'], ), {}), + ((['nmcli', 'connection', 'up', 'ifname', 'eth1'], ), {}), +] + + +@pytest.mark.parametrize('activator, expected_call_list', [ + (IfUpDownActivator, IF_UP_DOWN_BRING_UP_CALL_LIST), + (NetplanActivator, NETPLAN_CALL_LIST), + (NetworkManagerActivator, NETWORK_MANAGER_BRING_UP_CALL_LIST), +]) +class TestActivatorsBringUp: @patch('cloudinit.subp.subp', return_value=('', '')) def test_bring_up_interface( - self, m_subp, activator, available_calls, expected_call_list, - available_mocks + self, m_subp, activator, expected_call_list, available_mocks ): activator.bring_up_interface('eth0') assert len(m_subp.call_args_list) == 1 @@ -150,16 +157,14 @@ class TestIfUpDownActivator: @patch('cloudinit.subp.subp', return_value=('', '')) def test_bring_up_interfaces( - self, m_subp, activator, available_calls, expected_call_list, - available_mocks + self, m_subp, activator, expected_call_list, available_mocks ): activator.bring_up_interfaces(['eth0', 'eth1']) assert expected_call_list == m_subp.call_args_list @patch('cloudinit.subp.subp', return_value=('', '')) def test_bring_up_all_interfaces_v1( - self, m_subp, activator, available_calls, expected_call_list, - available_mocks + self, m_subp, activator, expected_call_list, available_mocks ): network_state = parse_net_config_data(load(V1_CONFIG)) activator.bring_up_all_interfaces(network_state) @@ -168,10 +173,60 @@ class TestIfUpDownActivator: @patch('cloudinit.subp.subp', return_value=('', '')) def test_bring_up_all_interfaces_v2( - self, m_subp, activator, available_calls, expected_call_list, - available_mocks + self, m_subp, activator, expected_call_list, available_mocks ): network_state = parse_net_config_data(load(V2_CONFIG)) activator.bring_up_all_interfaces(network_state) for call in m_subp.call_args_list: assert call in expected_call_list + + +IF_UP_DOWN_BRING_DOWN_CALL_LIST = [ + ((['ifdown', 'eth0'], ), {}), + ((['ifdown', 'eth1'], ), {}), +] + +NETWORK_MANAGER_BRING_DOWN_CALL_LIST = [ + ((['nmcli', 'connection', 'down', 'eth0'], ), {}), + ((['nmcli', 'connection', 'down', 'eth1'], ), {}), +] + + +@pytest.mark.parametrize('activator, expected_call_list', [ + (IfUpDownActivator, IF_UP_DOWN_BRING_DOWN_CALL_LIST), + (NetplanActivator, NETPLAN_CALL_LIST), + (NetworkManagerActivator, NETWORK_MANAGER_BRING_DOWN_CALL_LIST), +]) +class TestActivatorsBringDown: + @patch('cloudinit.subp.subp', return_value=('', '')) + def test_bring_down_interface( + self, m_subp, activator, expected_call_list, available_mocks + ): + activator.bring_down_interface('eth0') + assert len(m_subp.call_args_list) == 1 + assert m_subp.call_args_list[0] == expected_call_list[0] + + @patch('cloudinit.subp.subp', return_value=('', '')) + def test_bring_down_interfaces( + self, m_subp, activator, expected_call_list, available_mocks + ): + activator.bring_down_interfaces(['eth0', 'eth1']) + assert expected_call_list == m_subp.call_args_list + + @patch('cloudinit.subp.subp', return_value=('', '')) + def test_bring_down_all_interfaces_v1( + self, m_subp, activator, expected_call_list, available_mocks + ): + network_state = parse_net_config_data(load(V1_CONFIG)) + activator.bring_down_all_interfaces(network_state) + for call in m_subp.call_args_list: + assert call in expected_call_list + + @patch('cloudinit.subp.subp', return_value=('', '')) + def test_bring_down_all_interfaces_v2( + self, m_subp, activator, expected_call_list, available_mocks + ): + network_state = parse_net_config_data(load(V2_CONFIG)) + activator.bring_down_all_interfaces(network_state) + for call in m_subp.call_args_list: + assert call in expected_call_list diff --git a/tools/hook-hotplug b/tools/hook-hotplug new file mode 100755 index 00000000..34e95929 --- /dev/null +++ b/tools/hook-hotplug @@ -0,0 +1,21 @@ +#!/bin/bash +# This file is part of cloud-init. See LICENSE file for license information. + +# This script checks if cloud-init has hotplug hooked and if +# cloud-init has finished; if so invoke cloud-init hotplug-hook + +is_finished() { + [ -e /run/cloud-init/result.json ] +} + +if is_finished; then + # open cloud-init's hotplug-hook fifo rw + exec 3<>/run/cloud-init/hook-hotplug-cmd + env_params=( + --devpath="${DEVPATH}" + --subsystem="${SUBSYSTEM}" + --udevaction="${ACTION}" + ) + # write params to cloud-init's hotplug-hook fifo + echo "${env_params[@]}" >&3 +fi diff --git a/udev/10-cloud-init-hook-hotplug.rules b/udev/10-cloud-init-hook-hotplug.rules new file mode 100644 index 00000000..2e382679 --- /dev/null +++ b/udev/10-cloud-init-hook-hotplug.rules @@ -0,0 +1,6 @@ +# This file is part of cloud-init. See LICENSE file for license information. +# Handle device adds only +ACTION!="add|remove", GOTO="cloudinit_end" +LABEL="cloudinit_hook" +SUBSYSTEM=="net|block", RUN+="/usr/lib/cloud-init/hook-hotplug" +LABEL="cloudinit_end" -- cgit v1.2.3 From 4257e30ac4b8730af35c078f2df0a2234dd19ffa Mon Sep 17 00:00:00 2001 From: eb3095 <45504889+eb3095@users.noreply.github.com> Date: Wed, 21 Jul 2021 16:48:05 -0400 Subject: Add VZLinux support (#951) Virtuozzo Linux is a distro based off of CentOS 8, similar to Alma Linux and Rocky Linux. --- README.md | 2 +- cloudinit/config/cc_ntp.py | 2 +- cloudinit/config/cc_yum_add_repo.py | 6 ++++-- cloudinit/distros/__init__.py | 2 +- cloudinit/distros/virtuozzo.py | 9 +++++++++ cloudinit/net/sysconfig.py | 3 ++- cloudinit/tests/test_util.py | 32 ++++++++++++++++++++++++++++++++ cloudinit/util.py | 17 ++++++++++++++++- config/cloud.cfg.tmpl | 6 +++--- systemd/cloud-init-generator.tmpl | 2 +- systemd/cloud-init.service.tmpl | 2 +- tests/unittests/test_cli.py | 2 +- tools/render-cloudcfg | 2 +- 13 files changed, 73 insertions(+), 14 deletions(-) create mode 100644 cloudinit/distros/virtuozzo.py (limited to 'systemd') diff --git a/README.md b/README.md index 462e3204..832d8b43 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index acf3251d..d227efb9 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -25,7 +25,7 @@ frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 distros = ['almalinux', 'alpine', 'centos', 'debian', 'fedora', 'opensuse', - 'photon', 'rhel', 'rocky', 'sles', 'ubuntu'] + 'photon', 'rhel', 'rocky', 'sles', 'ubuntu', 'virtuozzo'] NTP_CLIENT_CONFIG = { 'chrony': { diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index 67f09686..6e6133d1 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -18,7 +18,8 @@ entry, the config entry will be skipped. **Module frequency:** per always -**Supported distros:** almalinux, centos, fedora, photon, rhel, rocky +**Supported distros:** almalinux, centos, fedora, photon, rhel, rocky, + virtuozzo **Config keys**:: @@ -36,7 +37,8 @@ from configparser import ConfigParser from cloudinit import util -distros = ['almalinux', 'centos', 'fedora', 'photon', 'rhel', 'rocky'] +distros = ['almalinux', 'centos', 'fedora', 'photon', 'rhel', 'rocky', + 'virtuozzo'] def _canonicalize_id(repo_id): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 40c4f2ac..7b813167 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -50,7 +50,7 @@ OSFAMILIES = { 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'photon', 'rhel', - 'rocky'], + 'rocky', 'virtuozzo'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/virtuozzo.py b/cloudinit/distros/virtuozzo.py new file mode 100644 index 00000000..edb3165d --- /dev/null +++ b/cloudinit/distros/virtuozzo.py @@ -0,0 +1,9 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 8031cd3a..49f52e9d 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -18,7 +18,8 @@ from .network_state import ( is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES) LOG = logging.getLogger(__name__) -KNOWN_DISTROS = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky', 'suse'] +KNOWN_DISTROS = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky', 'suse', + 'virtuozzo'] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index a1ccb1dc..bd7720d1 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -140,6 +140,20 @@ OS_RELEASE_ROCKY_8 = dedent("""\ ROCKY_SUPPORT_PRODUCT_VERSION="8" """) +OS_RELEASE_VIRTUOZZO_8 = dedent("""\ + NAME="Virtuozzo Linux" + VERSION="8" + ID="virtuozzo" + ID_LIKE="rhel fedora" + VERSION_ID="8" + PLATFORM_ID="platform:el8" + PRETTY_NAME="Virtuozzo Linux" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:virtuozzoproject:vzlinux:8" + HOME_URL="https://www.vzlinux.org" + BUG_REPORT_URL="https://bugs.openvz.org" +""") + REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" REDHAT_RELEASE_REDHAT_6 = ( @@ -150,6 +164,8 @@ REDHAT_RELEASE_ALMALINUX_8 = ( "AlmaLinux release 8.3 (Purple Manul)") REDHAT_RELEASE_ROCKY_8 = ( "Rocky Linux release 8.3 (Green Obsidian)") +REDHAT_RELEASE_VIRTUOZZO_8 = ( + "Virtuozzo Linux release 8") OS_RELEASE_DEBIAN = dedent("""\ PRETTY_NAME="Debian GNU/Linux 9 (stretch)" @@ -581,6 +597,22 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('rocky', '8.3', 'Green Obsidian'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_virtuozzo8_rhrelease(self, m_os_release, m_path_exists): + """Verify virtuozzo linux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_VIRTUOZZO_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_virtuozzo8_osrelease(self, m_os_release, m_path_exists): + """Verify virtuozzo linux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_VIRTUOZZO_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_debian(self, m_os_release, m_path_exists): """Verify we get the correct name and release name on Debian.""" diff --git a/cloudinit/util.py b/cloudinit/util.py index 7995c6c8..3bed1aed 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -453,9 +453,19 @@ def _parse_redhat_release(release_file=None): redhat_regex = ( r'(?P.+) release (?P[\d\.]+) ' r'\((?P[^)]+)\)') + + # Virtuozzo deviates here + if "Virtuozzo" in redhat_release: + redhat_regex = r'(?P.+) release (?P[\d\.]+)' + match = re.match(redhat_regex, redhat_release) if match: group = match.groupdict() + + # Virtuozzo has no codename in this file + if "Virtuozzo" in group['name']: + group['codename'] = group['name'] + group['name'] = group['name'].lower().partition(' linux')[0] if group['name'] == 'red hat enterprise': group['name'] = 'redhat' @@ -470,9 +480,11 @@ def get_linux_distro(): distro_version = '' flavor = '' os_release = {} + os_release_rhel = False if os.path.exists('/etc/os-release'): os_release = load_shell_content(load_file('/etc/os-release')) if not os_release: + os_release_rhel = True os_release = _parse_redhat_release() if os_release: distro_name = os_release.get('ID', '') @@ -485,6 +497,9 @@ def get_linux_distro(): flavor = platform.machine() elif distro_name == 'photon': flavor = os_release.get('PRETTY_NAME', '') + elif distro_name == 'virtuozzo' and not os_release_rhel: + # Only use this if the redhat file is not parsed + flavor = os_release.get('PRETTY_NAME', '') else: flavor = os_release.get('VERSION_CODENAME', '') if not flavor: @@ -533,7 +548,7 @@ def system_info(): linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'fedora', - 'photon', 'rhel', 'rocky', 'suse'): + 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index cb2a625b..f918d919 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -32,7 +32,7 @@ disable_root: true {% endif %} {% if variant in ["almalinux", "alpine", "amazon", "centos", "fedora", - "rhel", "rocky"] %} + "rhel", "rocky", "virtuozzo"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} resize_rootfs: noblock @@ -174,7 +174,7 @@ system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "debian", "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", - "rocky", "suse", "ubuntu"] %} + "rocky", "suse", "ubuntu", "virtuozzo"] %} distro: {{ variant }} {% elif variant in ["dragonfly"] %} distro: dragonflybsd @@ -228,7 +228,7 @@ system_info: security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh {% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "fedora", - "rhel", "rocky", "suse"] %} + "rhel", "rocky", "suse", "virtuozzo"] %} # Default user name + that default users groups (if added/used) default_user: {% if variant == "amazon" %} diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 0713db16..1d6af5ae 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,7 +83,7 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["almalinux", "rhel", "fedora", "centos", "rocky"] %} +{% if variant in ["almalinux", "rhel", "fedora", "centos", "rocky", "virtuozzo"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index c773e411..cab3ec51 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -12,7 +12,7 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["almalinux", "centos", "fedora", "rhel", "rocky"] %} +{% if variant in ["almalinux", "centos", "fedora", "rhel", "rocky", "virtuozzo"] %} After=network.service After=NetworkManager.service {% endif %} diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index fdb4026c..90d8f7b9 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -225,7 +225,7 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): expected_doc_sections = [ '**Supported distros:** all', ('**Supported distros:** almalinux, alpine, centos, debian, ' - 'fedora, opensuse, photon, rhel, rocky, sles, ubuntu'), + 'fedora, opensuse, photon, rhel, rocky, sles, ubuntu, virtuozzo'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 7e667de4..227bd8ab 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -6,7 +6,7 @@ import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "debian", "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", - "suse","rocky", "ubuntu", "unknown"] + "suse","rocky", "ubuntu", "unknown", "virtuozzo"] if "avoid-pep8-E402-import-not-top-of-file": -- cgit v1.2.3 From 3d9c862b6ded798031fad827328fa437bc14ac97 Mon Sep 17 00:00:00 2001 From: Aleksander Baranowski Date: Thu, 5 Aug 2021 18:32:36 +0200 Subject: Add support for EuroLinux 7 && EuroLinux 8 (#957) --- README.md | 2 +- cloudinit/config/cc_ntp.py | 9 +++-- cloudinit/config/cc_yum_add_repo.py | 8 ++-- cloudinit/distros/__init__.py | 4 +- cloudinit/distros/eurolinux.py | 9 +++++ cloudinit/net/sysconfig.py | 4 +- cloudinit/tests/.test_util.py.swp | Bin 16384 -> 0 bytes cloudinit/tests/test_util.py | 66 ++++++++++++++++++++++++++++++++ cloudinit/util.py | 4 +- config/cloud.cfg.tmpl | 12 +++--- doc/rtd/topics/availability.rst | 2 +- packages/pkg-deps.json | 14 +++++++ systemd/cloud-init-generator.tmpl | 3 +- systemd/cloud-init.service.tmpl | 3 +- tests/unittests/test_cli.py | 3 +- tests/unittests/test_net.py | 1 + tests/unittests/test_render_cloudcfg.py | 6 +-- tools/read-dependencies | 8 +++- tools/render-cloudcfg | 4 +- 19 files changed, 130 insertions(+), 32 deletions(-) create mode 100644 cloudinit/distros/eurolinux.py delete mode 100644 cloudinit/tests/.test_util.py.swp (limited to 'systemd') diff --git a/README.md b/README.md index 832d8b43..caf9a6e9 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index d227efb9..7c371a49 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -24,8 +24,9 @@ LOG = logging.getLogger(__name__) frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 -distros = ['almalinux', 'alpine', 'centos', 'debian', 'fedora', 'opensuse', - 'photon', 'rhel', 'rocky', 'sles', 'ubuntu', 'virtuozzo'] +distros = ['almalinux', 'alpine', 'centos', 'debian', 'eurolinux', 'fedora', + 'opensuse', 'photon', 'rhel', 'rocky', 'sles', 'ubuntu', + 'virtuozzo'] NTP_CLIENT_CONFIG = { 'chrony': { @@ -405,9 +406,9 @@ def generate_server_names(distro): # For legal reasons x.pool.sles.ntp.org does not exist, # use the opensuse pool pool_distro = 'opensuse' - elif distro == 'alpine': + elif distro == 'alpine' or distro == 'eurolinux': # Alpine-specific pool (i.e. x.alpine.pool.ntp.org) does not exist - # so use general x.pool.ntp.org instead. + # so use general x.pool.ntp.org instead. The same applies to EuroLinux pool_distro = '' for x in range(0, NR_POOL_SERVERS): diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index 6e6133d1..b7a48dcc 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -18,8 +18,8 @@ entry, the config entry will be skipped. **Module frequency:** per always -**Supported distros:** almalinux, centos, fedora, photon, rhel, rocky, - virtuozzo +**Supported distros:** almalinux, centos, eurolinux, fedora, photon, rhel, + rocky, virtuozzo **Config keys**:: @@ -37,8 +37,8 @@ from configparser import ConfigParser from cloudinit import util -distros = ['almalinux', 'centos', 'fedora', 'photon', 'rhel', 'rocky', - 'virtuozzo'] +distros = ['almalinux', 'centos', 'eurolinux', 'fedora', 'photon', 'rhel', + 'rocky', 'virtuozzo'] def _canonicalize_id(repo_id): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 7b813167..a634623a 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -49,8 +49,8 @@ OSFAMILIES = { 'debian': ['debian', 'ubuntu'], 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], - 'redhat': ['almalinux', 'amazon', 'centos', 'fedora', 'photon', 'rhel', - 'rocky', 'virtuozzo'], + 'redhat': ['almalinux', 'amazon', 'centos', 'eurolinux', 'fedora', + 'photon', 'rhel', 'rocky', 'virtuozzo'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/eurolinux.py b/cloudinit/distros/eurolinux.py new file mode 100644 index 00000000..edb3165d --- /dev/null +++ b/cloudinit/distros/eurolinux.py @@ -0,0 +1,9 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 49f52e9d..06f7255e 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -18,8 +18,8 @@ from .network_state import ( is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES) LOG = logging.getLogger(__name__) -KNOWN_DISTROS = ['almalinux', 'centos', 'fedora', 'rhel', 'rocky', 'suse', - 'virtuozzo'] +KNOWN_DISTROS = ['almalinux', 'centos', 'eurolinux', 'fedora', 'rhel', 'rocky', + 'suse', 'virtuozzo'] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" diff --git a/cloudinit/tests/.test_util.py.swp b/cloudinit/tests/.test_util.py.swp deleted file mode 100644 index 78ef5865..00000000 Binary files a/cloudinit/tests/.test_util.py.swp and /dev/null differ diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index bd7720d1..9dd01158 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -124,6 +124,38 @@ OS_RELEASE_ALMALINUX_8 = dedent("""\ ALMALINUX_MANTISBT_PROJECT_VERSION="8.3" """) +OS_RELEASE_EUROLINUX_7 = dedent("""\ + VERSION="7.9 (Minsk)" + ID="eurolinux" + ID_LIKE="rhel scientific centos fedora" + VERSION_ID="7.9" + PRETTY_NAME="EuroLinux 7.9 (Minsk)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:eurolinux:eurolinux:7.9:GA" + HOME_URL="http://www.euro-linux.com/" + BUG_REPORT_URL="mailto:support@euro-linux.com" + REDHAT_BUGZILLA_PRODUCT="EuroLinux 7" + REDHAT_BUGZILLA_PRODUCT_VERSION=7.9 + REDHAT_SUPPORT_PRODUCT="EuroLinux" + REDHAT_SUPPORT_PRODUCT_VERSION="7.9" +""") + +OS_RELEASE_EUROLINUX_8 = dedent("""\ + NAME="EuroLinux" + VERSION="8.4 (Vaduz)" + ID="eurolinux" + ID_LIKE="rhel fedora centos" + VERSION_ID="8.4" + PLATFORM_ID="platform:el8" + PRETTY_NAME="EuroLinux 8.4 (Vaduz)" + ANSI_COLOR="0;34" + CPE_NAME="cpe:/o:eurolinux:eurolinux:8" + HOME_URL="https://www.euro-linux.com/" + BUG_REPORT_URL="https://github.com/EuroLinux/eurolinux-distro-bugs-and-rfc/" + REDHAT_SUPPORT_PRODUCT="EuroLinux" + REDHAT_SUPPORT_PRODUCT_VERSION="8" +""") + OS_RELEASE_ROCKY_8 = dedent("""\ NAME="Rocky Linux" VERSION="8.3 (Green Obsidian)" @@ -162,6 +194,8 @@ REDHAT_RELEASE_REDHAT_7 = ( "Red Hat Enterprise Linux Server release 7.5 (Maipo)") REDHAT_RELEASE_ALMALINUX_8 = ( "AlmaLinux release 8.3 (Purple Manul)") +REDHAT_RELEASE_EUROLINUX_7 = "EuroLinux release 7.9 (Minsk)" +REDHAT_RELEASE_EUROLINUX_8 = "EuroLinux release 8.4 (Vaduz)" REDHAT_RELEASE_ROCKY_8 = ( "Rocky Linux release 8.3 (Green Obsidian)") REDHAT_RELEASE_VIRTUOZZO_8 = ( @@ -581,6 +615,38 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('almalinux', '8.3', 'Purple Manul'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux7_rhrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 7 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_7 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux7_osrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 7 read from os-release.""" + m_os_release.return_value = OS_RELEASE_EUROLINUX_7 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '7.9', 'Minsk'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux8_rhrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_EUROLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_eurolinux8_osrelease(self, m_os_release, m_path_exists): + """Verify eurolinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_EUROLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): """Verify rocky linux 8 read from redhat-release.""" diff --git a/cloudinit/util.py b/cloudinit/util.py index 3bed1aed..d3ced463 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -547,8 +547,8 @@ def system_info(): if system == "linux": linux_dist = info['dist'][0].lower() if linux_dist in ( - 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'fedora', - 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): + 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'eurolinux', + 'fedora', 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 2314d893..825deff4 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -32,8 +32,8 @@ disable_root: false disable_root: true {% endif %} -{% if variant in ["almalinux", "alpine", "amazon", "centos", "fedora", - "rhel", "rocky", "virtuozzo"] %} +{% if variant in ["almalinux", "alpine", "amazon", "centos", "eurolinux", + "fedora", "rhel", "rocky", "virtuozzo"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} resize_rootfs: noblock @@ -174,8 +174,8 @@ cloud_final_modules: system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", - "rocky", "suse", "ubuntu", "virtuozzo"] %} + "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", + "photon", "rhel", "rocky", "suse", "ubuntu", "virtuozzo"] %} distro: {{ variant }} {% elif variant in ["dragonfly"] %} distro: dragonflybsd @@ -228,8 +228,8 @@ system_info: primary: http://ports.ubuntu.com/ubuntu-ports security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh -{% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "fedora", - "rhel", "rocky", "suse", "virtuozzo"] %} +{% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "eurolinux", + "fedora", "rhel", "rocky", "suse", "virtuozzo"] %} # Default user name + that default users groups (if added/used) default_user: {% if variant == "amazon" %} diff --git a/doc/rtd/topics/availability.rst b/doc/rtd/topics/availability.rst index b84b6076..e0644534 100644 --- a/doc/rtd/topics/availability.rst +++ b/doc/rtd/topics/availability.rst @@ -27,7 +27,7 @@ OpenBSD and DragonFlyBSD: - NetBSD - OpenBSD - Photon OS -- RHEL/CentOS +- RHEL/CentOS/AlmaLinux/Rocky Linux/EuroLinux - SLES/openSUSE - Ubuntu diff --git a/packages/pkg-deps.json b/packages/pkg-deps.json index 80028396..eaf13469 100644 --- a/packages/pkg-deps.json +++ b/packages/pkg-deps.json @@ -27,6 +27,20 @@ "sudo" ] }, + "eurolinux" : { + "build-requires" : [ + "python3-devel" + ], + "requires" : [ + "e2fsprogs", + "iproute", + "net-tools", + "procps", + "rsyslog", + "shadow-utils", + "sudo" + ] + }, "redhat" : { "build-requires" : [ "python3-devel" diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 1d6af5ae..3dbe5947 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,7 +83,8 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["almalinux", "rhel", "fedora", "centos", "rocky", "virtuozzo"] %} +{% if variant in ["almalinux", "centos", "eurolinux", "fedora", "rhel", + "rocky", "virtuozzo"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index cab3ec51..636f59be 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -12,7 +12,8 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["almalinux", "centos", "fedora", "rhel", "rocky", "virtuozzo"] %} +{% if variant in ["almalinux", "centos", "eurolinux", "fedora", "rhel", + "rocky", "virtuozzo"] %} After=network.service After=NetworkManager.service {% endif %} diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index 90d8f7b9..a39e1d0c 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -225,7 +225,8 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): expected_doc_sections = [ '**Supported distros:** all', ('**Supported distros:** almalinux, alpine, centos, debian, ' - 'fedora, opensuse, photon, rhel, rocky, sles, ubuntu, virtuozzo'), + 'eurolinux, fedora, opensuse, photon, rhel, rocky, sles, ubuntu, ' + 'virtuozzo'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py index 43e209c1..fc77b11e 100644 --- a/tests/unittests/test_net.py +++ b/tests/unittests/test_net.py @@ -5308,6 +5308,7 @@ class TestNetRenderers(CiTestCase): ('opensuse-tumbleweed', '', ''), ('sles', '', ''), ('centos', '', ''), + ('eurolinux', '', ''), ('fedora', '', ''), ('redhat', '', ''), ] diff --git a/tests/unittests/test_render_cloudcfg.py b/tests/unittests/test_render_cloudcfg.py index 275879af..00d50e66 100644 --- a/tests/unittests/test_render_cloudcfg.py +++ b/tests/unittests/test_render_cloudcfg.py @@ -9,9 +9,9 @@ from cloudinit import subp from cloudinit import util # TODO(Look to align with tools.render-cloudcfg or cloudinit.distos.OSFAMILIES) -DISTRO_VARIANTS = ["amazon", "arch", "centos", "debian", "fedora", "freebsd", - "netbsd", "openbsd", "photon", "rhel", "suse", "ubuntu", - "unknown"] +DISTRO_VARIANTS = ["amazon", "arch", "centos", "debian", "eurolinux", "fedora", + "freebsd", "netbsd", "openbsd", "photon", "rhel", "suse", + "ubuntu", "unknown"] @pytest.mark.allow_subp_for(sys.executable) diff --git a/tools/read-dependencies b/tools/read-dependencies index e52720d4..810154e4 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -23,6 +23,7 @@ DEFAULT_REQUIREMENTS = 'requirements.txt' # Map the appropriate package dir needed for each distro choice DISTRO_PKG_TYPE_MAP = { 'centos': 'redhat', + 'eurolinux': 'redhat', 'rocky': 'redhat', 'redhat': 'redhat', 'debian': 'debian', @@ -68,11 +69,13 @@ ZYPPER_INSTALL = [ DRY_DISTRO_INSTALL_PKG_CMD = { 'rocky': ['yum', 'install', '--assumeyes'], 'centos': ['yum', 'install', '--assumeyes'], + 'eurolinux': ['yum', 'install', '--assumeyes'], 'redhat': ['yum', 'install', '--assumeyes'], } DISTRO_INSTALL_PKG_CMD = { 'rocky': MAYBE_RELIABLE_YUM_INSTALL, + 'eurolinux': MAYBE_RELIABLE_YUM_INSTALL, 'centos': MAYBE_RELIABLE_YUM_INSTALL, 'redhat': MAYBE_RELIABLE_YUM_INSTALL, 'debian': ['apt', 'install', '-y'], @@ -85,6 +88,7 @@ DISTRO_INSTALL_PKG_CMD = { # List of base system packages required to enable ci automation CI_SYSTEM_BASE_PKGS = { 'common': ['make', 'sudo', 'tar'], + 'eurolinux': ['python3-tox'], 'redhat': ['python3-tox'], 'centos': ['python3-tox'], 'ubuntu': ['devscripts', 'python3-dev', 'libssl-dev', 'tox', 'sbuild'], @@ -277,10 +281,10 @@ def pkg_install(pkg_list, distro, test_distro=False, dry_run=False): cmd = DRY_DISTRO_INSTALL_PKG_CMD[distro] install_cmd.extend(cmd) - if distro in ['centos', 'redhat', 'rocky']: + if distro in ['centos', 'redhat', 'rocky', 'eurolinux']: # CentOS and Redhat need epel-release to access oauthlib and jsonschema subprocess.check_call(install_cmd + ['epel-release']) - if distro in ['suse', 'opensuse', 'redhat', 'rocky', 'centos']: + if distro in ['suse', 'opensuse', 'redhat', 'rocky', 'centos', 'eurolinux']: pkg_list.append('rpm-build') subprocess.check_call(install_cmd + pkg_list) diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 227bd8ab..30f82521 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -5,8 +5,8 @@ import os import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "debian", - "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", - "suse","rocky", "ubuntu", "unknown", "virtuozzo"] + "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "photon", + "rhel", "suse","rocky", "ubuntu", "unknown", "virtuozzo"] if "avoid-pep8-E402-import-not-top-of-file": -- cgit v1.2.3 From f6c71fd758a3589ac5f62fd251d60b00edf5bb1c Mon Sep 17 00:00:00 2001 From: Alexandr Kravchenko Date: Wed, 1 Sep 2021 22:01:09 +0300 Subject: Add CloudLinux OS support (#1003) https://www.cloudlinux.com/ --- README.md | 2 +- cloudinit/config/cc_ntp.py | 6 +++--- cloudinit/config/cc_yum_add_repo.py | 8 ++++---- cloudinit/distros/__init__.py | 4 ++-- cloudinit/distros/cloudlinux.py | 9 +++++++++ cloudinit/net/sysconfig.py | 4 ++-- cloudinit/tests/test_util.py | 33 ++++++++++++++++++++++++++++++++- cloudinit/util.py | 5 +++-- config/cloud.cfg.tmpl | 6 +++--- systemd/cloud-init-generator.tmpl | 2 +- systemd/cloud-init.service.tmpl | 2 +- tests/unittests/test_cli.py | 6 +++--- tools/.github-cla-signers | 1 + tools/render-cloudcfg | 2 +- 14 files changed, 66 insertions(+), 24 deletions(-) create mode 100644 cloudinit/distros/cloudlinux.py (limited to 'systemd') diff --git a/README.md b/README.md index 5828c2fa..b705a065 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux/CloudLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index 7c371a49..e2231cbb 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -24,9 +24,9 @@ LOG = logging.getLogger(__name__) frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 -distros = ['almalinux', 'alpine', 'centos', 'debian', 'eurolinux', 'fedora', - 'opensuse', 'photon', 'rhel', 'rocky', 'sles', 'ubuntu', - 'virtuozzo'] +distros = ['almalinux', 'alpine', 'centos', 'cloudlinux', 'debian', + 'eurolinux', 'fedora', 'opensuse', 'photon', 'rhel', 'rocky', + 'sles', 'ubuntu', 'virtuozzo'] NTP_CLIENT_CONFIG = { 'chrony': { diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index b7a48dcc..899cb082 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -18,8 +18,8 @@ entry, the config entry will be skipped. **Module frequency:** per always -**Supported distros:** almalinux, centos, eurolinux, fedora, photon, rhel, - rocky, virtuozzo +**Supported distros:** almalinux, centos, cloudlinux, eurolinux, fedora, + photon, rhel, rocky, virtuozzo **Config keys**:: @@ -37,8 +37,8 @@ from configparser import ConfigParser from cloudinit import util -distros = ['almalinux', 'centos', 'eurolinux', 'fedora', 'photon', 'rhel', - 'rocky', 'virtuozzo'] +distros = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora', + 'photon', 'rhel', 'rocky', 'virtuozzo'] def _canonicalize_id(repo_id): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index 2e629143..a0526948 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -49,8 +49,8 @@ OSFAMILIES = { 'debian': ['debian', 'ubuntu'], 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], - 'redhat': ['almalinux', 'amazon', 'centos', 'eurolinux', 'fedora', - 'photon', 'rhel', 'rocky', 'virtuozzo'], + 'redhat': ['almalinux', 'amazon', 'centos', 'cloudlinux', 'eurolinux', + 'fedora', 'photon', 'rhel', 'rocky', 'virtuozzo'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/cloudlinux.py b/cloudinit/distros/cloudlinux.py new file mode 100644 index 00000000..edb3165d --- /dev/null +++ b/cloudinit/distros/cloudlinux.py @@ -0,0 +1,9 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 06f7255e..7b8e4da7 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -18,8 +18,8 @@ from .network_state import ( is_ipv6_addr, net_prefix_to_ipv4_mask, subnet_is_ipv6, IPV6_DYNAMIC_TYPES) LOG = logging.getLogger(__name__) -KNOWN_DISTROS = ['almalinux', 'centos', 'eurolinux', 'fedora', 'rhel', 'rocky', - 'suse', 'virtuozzo'] +KNOWN_DISTROS = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora', + 'rhel', 'rocky', 'suse', 'virtuozzo'] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index 9dd01158..f11cfb27 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -186,6 +186,20 @@ OS_RELEASE_VIRTUOZZO_8 = dedent("""\ BUG_REPORT_URL="https://bugs.openvz.org" """) +OS_RELEASE_CLOUDLINUX_8 = dedent("""\ + NAME="CloudLinux" + VERSION="8.4 (Valery Rozhdestvensky)" + ID="cloudlinux" + ID_LIKE="rhel fedora centos" + VERSION_ID="8.4" + PLATFORM_ID="platform:el8" + PRETTY_NAME="CloudLinux 8.4 (Valery Rozhdestvensky)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:cloudlinux:cloudlinux:8.4:GA:server" + HOME_URL="https://www.cloudlinux.com/" + BUG_REPORT_URL="https://www.cloudlinux.com/support" +""") + REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" REDHAT_RELEASE_REDHAT_6 = ( @@ -200,7 +214,8 @@ REDHAT_RELEASE_ROCKY_8 = ( "Rocky Linux release 8.3 (Green Obsidian)") REDHAT_RELEASE_VIRTUOZZO_8 = ( "Virtuozzo Linux release 8") - +REDHAT_RELEASE_CLOUDLINUX_8 = ( + "CloudLinux release 8.4 (Valery Rozhdestvensky)") OS_RELEASE_DEBIAN = dedent("""\ PRETTY_NAME="Debian GNU/Linux 9 (stretch)" NAME="Debian GNU/Linux" @@ -679,6 +694,22 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('virtuozzo', '8', 'Virtuozzo Linux'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_cloud8_rhrelease(self, m_os_release, m_path_exists): + """Verify cloudlinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_CLOUDLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_cloud8_osrelease(self, m_os_release, m_path_exists): + """Verify cloudlinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_CLOUDLINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('cloudlinux', '8.4', 'Valery Rozhdestvensky'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_debian(self, m_os_release, m_path_exists): """Verify we get the correct name and release name on Debian.""" diff --git a/cloudinit/util.py b/cloudinit/util.py index c53f6453..894245bf 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -548,8 +548,9 @@ def system_info(): if system == "linux": linux_dist = info['dist'][0].lower() if linux_dist in ( - 'almalinux', 'alpine', 'arch', 'centos', 'debian', 'eurolinux', - 'fedora', 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): + 'almalinux', 'alpine', 'arch', 'centos', 'cloudlinux', + 'debian', 'eurolinux', 'fedora', 'photon', 'rhel', 'rocky', + 'suse', 'virtuozzo'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index 825deff4..e5fbc10b 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -32,7 +32,7 @@ disable_root: false disable_root: true {% endif %} -{% if variant in ["almalinux", "alpine", "amazon", "centos", "eurolinux", +{% if variant in ["almalinux", "alpine", "amazon", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", "rocky", "virtuozzo"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} @@ -173,7 +173,7 @@ cloud_final_modules: # (not accessible to handlers/transforms) system_info: # This will affect which distro class gets used -{% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "debian", +{% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", "rocky", "suse", "ubuntu", "virtuozzo"] %} distro: {{ variant }} @@ -228,7 +228,7 @@ system_info: primary: http://ports.ubuntu.com/ubuntu-ports security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh -{% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "eurolinux", +{% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", "rocky", "suse", "virtuozzo"] %} # Default user name + that default users groups (if added/used) default_user: diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 3dbe5947..7fe009ec 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,7 +83,7 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["almalinux", "centos", "eurolinux", "fedora", "rhel", +{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", "rocky", "virtuozzo"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index 636f59be..6c3a8f20 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -12,7 +12,7 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["almalinux", "centos", "eurolinux", "fedora", "rhel", +{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", "rocky", "virtuozzo"] %} After=network.service After=NetworkManager.service diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index a39e1d0c..b8ae9e47 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -224,9 +224,9 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): self._call_main(['cloud-init', 'devel', 'schema', '--docs', 'all']) expected_doc_sections = [ '**Supported distros:** all', - ('**Supported distros:** almalinux, alpine, centos, debian, ' - 'eurolinux, fedora, opensuse, photon, rhel, rocky, sles, ubuntu, ' - 'virtuozzo'), + ('**Supported distros:** almalinux, alpine, centos, cloudlinux, ' + 'debian, eurolinux, fedora, opensuse, photon, rhel, rocky, ' + 'sles, ubuntu, virtuozzo'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index 4fa108aa..ba0c6d79 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -28,6 +28,7 @@ hamalq impl irishgordo izzyleung +JohnKepplers johnsonshi jordimassaguerpla jqueuniet diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 30f82521..78a48c30 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -4,7 +4,7 @@ import argparse import os import sys -VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "debian", +VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "photon", "rhel", "suse","rocky", "ubuntu", "unknown", "virtuozzo"] -- cgit v1.2.3 From f4c47e3e25d1fb79e2673e37f8fc67750d025be2 Mon Sep 17 00:00:00 2001 From: zhuzaifangxuele <52022596+zhuzaifangxuele@users.noreply.github.com> Date: Tue, 14 Sep 2021 02:21:28 +0800 Subject: Support openEuler OS (#1012) openEuler Homepage: https://www.openeuler.org/en/ --- README.md | 2 +- cloudinit/config/cc_ntp.py | 4 ++-- cloudinit/config/cc_yum_add_repo.py | 4 ++-- cloudinit/distros/__init__.py | 2 +- cloudinit/distros/openEuler.py | 9 +++++++++ cloudinit/net/sysconfig.py | 2 +- cloudinit/tests/test_util.py | 17 +++++++++++++++++ cloudinit/util.py | 4 ++-- config/cloud.cfg.tmpl | 6 +++--- systemd/cloud-init-generator.tmpl | 4 ++-- systemd/cloud-init.service.tmpl | 4 ++-- tests/unittests/test_cli.py | 4 ++-- tools/.github-cla-signers | 1 + tools/render-cloudcfg | 2 +- 14 files changed, 46 insertions(+), 19 deletions(-) create mode 100644 cloudinit/distros/openEuler.py (limited to 'systemd') diff --git a/README.md b/README.md index b705a065..e96541ef 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux/CloudLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
openEuler
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux/CloudLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index e2231cbb..f4468c9d 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -25,8 +25,8 @@ frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 distros = ['almalinux', 'alpine', 'centos', 'cloudlinux', 'debian', - 'eurolinux', 'fedora', 'opensuse', 'photon', 'rhel', 'rocky', - 'sles', 'ubuntu', 'virtuozzo'] + 'eurolinux', 'fedora', 'openEuler', 'opensuse', 'photon', + 'rhel', 'rocky', 'sles', 'ubuntu', 'virtuozzo'] NTP_CLIENT_CONFIG = { 'chrony': { diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index 899cb082..bcca86cb 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -19,7 +19,7 @@ entry, the config entry will be skipped. **Module frequency:** per always **Supported distros:** almalinux, centos, cloudlinux, eurolinux, fedora, - photon, rhel, rocky, virtuozzo + openEuler, photon, rhel, rocky, virtuozzo **Config keys**:: @@ -38,7 +38,7 @@ from configparser import ConfigParser from cloudinit import util distros = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora', - 'photon', 'rhel', 'rocky', 'virtuozzo'] + 'openEuler', 'photon', 'rhel', 'rocky', 'virtuozzo'] def _canonicalize_id(repo_id): diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index a0526948..63e78591 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -50,7 +50,7 @@ OSFAMILIES = { 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], 'redhat': ['almalinux', 'amazon', 'centos', 'cloudlinux', 'eurolinux', - 'fedora', 'photon', 'rhel', 'rocky', 'virtuozzo'], + 'fedora', 'openEuler', 'photon', 'rhel', 'rocky', 'virtuozzo'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/openEuler.py b/cloudinit/distros/openEuler.py new file mode 100644 index 00000000..edb3165d --- /dev/null +++ b/cloudinit/distros/openEuler.py @@ -0,0 +1,9 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass + +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index 7b8e4da7..ef4543b4 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -19,7 +19,7 @@ from .network_state import ( LOG = logging.getLogger(__name__) KNOWN_DISTROS = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora', - 'rhel', 'rocky', 'suse', 'virtuozzo'] + 'openEuler', 'rhel', 'rocky', 'suse', 'virtuozzo'] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" diff --git a/cloudinit/tests/test_util.py b/cloudinit/tests/test_util.py index f11cfb27..977ad8e0 100644 --- a/cloudinit/tests/test_util.py +++ b/cloudinit/tests/test_util.py @@ -200,6 +200,15 @@ OS_RELEASE_CLOUDLINUX_8 = dedent("""\ BUG_REPORT_URL="https://www.cloudlinux.com/support" """) +OS_RELEASE_OPENEULER_20 = dedent("""\ + NAME="openEuler" + VERSION="20.03 (LTS-SP2)" + ID="openEuler" + VERSION_ID="20.03" + PRETTY_NAME="openEuler 20.03 (LTS-SP2)" + ANSI_COLOR="0;31" +""") + REDHAT_RELEASE_CENTOS_6 = "CentOS release 6.10 (Final)" REDHAT_RELEASE_CENTOS_7 = "CentOS Linux release 7.5.1804 (Core)" REDHAT_RELEASE_REDHAT_6 = ( @@ -718,6 +727,14 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('debian', '9', 'stretch'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_openeuler(self, m_os_release, m_path_exists): + """Verify get the correct name and release name on Openeuler.""" + m_os_release.return_value = OS_RELEASE_OPENEULER_20 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('openEuler', '20.03', 'LTS-SP2'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_opensuse(self, m_os_release, m_path_exists): """Verify we get the correct name and machine arch on openSUSE diff --git a/cloudinit/util.py b/cloudinit/util.py index 894245bf..9662b30b 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -549,8 +549,8 @@ def system_info(): linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'cloudlinux', - 'debian', 'eurolinux', 'fedora', 'photon', 'rhel', 'rocky', - 'suse', 'virtuozzo'): + 'debian', 'eurolinux', 'fedora', 'openEuler', 'photon', + 'rhel', 'rocky', 'suse', 'virtuozzo'): var = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): var = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index e5fbc10b..de1d75e5 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -33,7 +33,7 @@ disable_root: true {% endif %} {% if variant in ["almalinux", "alpine", "amazon", "centos", "cloudlinux", "eurolinux", - "fedora", "rhel", "rocky", "virtuozzo"] %} + "fedora", "openEuler", "rhel", "rocky", "virtuozzo"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} resize_rootfs: noblock @@ -174,7 +174,7 @@ cloud_final_modules: system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", - "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", + "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "openEuler", "photon", "rhel", "rocky", "suse", "ubuntu", "virtuozzo"] %} distro: {{ variant }} {% elif variant in ["dragonfly"] %} @@ -229,7 +229,7 @@ system_info: security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh {% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "eurolinux", - "fedora", "rhel", "rocky", "suse", "virtuozzo"] %} + "fedora", "openEuler", "rhel", "rocky", "suse", "virtuozzo"] %} # Default user name + that default users groups (if added/used) default_user: {% if variant == "amazon" %} diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 7fe009ec..7d1e7256 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -83,8 +83,8 @@ default() { check_for_datasource() { local ds_rc="" -{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", - "rocky", "virtuozzo"] %} +{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", + "openEuler", "rhel", "rocky", "virtuozzo"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index 6c3a8f20..de3f3d91 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -12,8 +12,8 @@ After=systemd-networkd-wait-online.service {% if variant in ["ubuntu", "unknown", "debian"] %} After=networking.service {% endif %} -{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", "rhel", - "rocky", "virtuozzo"] %} +{% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", + "openEuler", "rhel", "rocky", "virtuozzo"] %} After=network.service After=NetworkManager.service {% endif %} diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index b8ae9e47..1459fd9c 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -225,8 +225,8 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): expected_doc_sections = [ '**Supported distros:** all', ('**Supported distros:** almalinux, alpine, centos, cloudlinux, ' - 'debian, eurolinux, fedora, opensuse, photon, rhel, rocky, ' - 'sles, ubuntu, virtuozzo'), + 'debian, eurolinux, fedora, openEuler, opensuse, photon, rhel, ' + 'rocky, sles, ubuntu, virtuozzo'), '**Config schema**:\n **resize_rootfs:** (true/false/noblock)', '**Examples**::\n\n runcmd:\n - [ ls, -l, / ]\n' ] diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index d1939718..fa395553 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -68,3 +68,4 @@ Vultaire WebSpider xiachen-rh xnox +zhuzaifangxuele diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 78a48c30..186d61b7 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -5,7 +5,7 @@ import os import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", - "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "photon", + "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "openEuler", "photon", "rhel", "suse","rocky", "ubuntu", "unknown", "virtuozzo"] -- cgit v1.2.3 From b3e31ba228d32c318872fb68edda272f679e1004 Mon Sep 17 00:00:00 2001 From: Ryan Harper Date: Tue, 12 Oct 2021 09:31:36 -0500 Subject: Inhibit sshd-keygen@.service if cloud-init is active (#1028) In some cloud-init enabled images the sshd-keygen@.service may race with cloud-init and prevent ssh host keys from being generated or generating host keys twice slowing boot and consuming additional entropy during boot. This drop-in unit adds a condition to the sshd-keygen@.service which prevents running if cloud-init is active. --- packages/redhat/cloud-init.spec.in | 1 + packages/suse/cloud-init.spec.in | 1 + setup.py | 5 ++++- systemd/disable-sshd-keygen-if-cloud-init-active.conf | 8 ++++++++ 4 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 systemd/disable-sshd-keygen-if-cloud-init-active.conf (limited to 'systemd') diff --git a/packages/redhat/cloud-init.spec.in b/packages/redhat/cloud-init.spec.in index b930709b..22db4b50 100644 --- a/packages/redhat/cloud-init.spec.in +++ b/packages/redhat/cloud-init.spec.in @@ -182,6 +182,7 @@ fi %if "%{init_system}" == "systemd" /usr/lib/systemd/system-generators/cloud-init-generator +%{_sysconfdir}/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf %{_unitdir}/cloud-* %else %attr(0755, root, root) %{_initddir}/cloud-config diff --git a/packages/suse/cloud-init.spec.in b/packages/suse/cloud-init.spec.in index 004b875f..da8107b4 100644 --- a/packages/suse/cloud-init.spec.in +++ b/packages/suse/cloud-init.spec.in @@ -126,6 +126,7 @@ version_pys=$(cd "%{buildroot}" && find . -name version.py -type f) %{_sysconfdir}/dhcp/dhclient-exit-hooks.d/hook-dhclient %{_sysconfdir}/NetworkManager/dispatcher.d/hook-network-manager +%{_sysconfdir}/systemd/system/sshd-keygen@.service.d/disable-sshd-keygen-if-cloud-init-active.conf # Python code is here... %{python_sitelib}/* diff --git a/setup.py b/setup.py index 100575ff..6e463bf8 100755 --- a/setup.py +++ b/setup.py @@ -40,6 +40,7 @@ def is_generator(p): def pkg_config_read(library, var): fallbacks = { 'systemd': { + 'systemdsystemconfdir': '/etc/systemd/system', 'systemdsystemunitdir': '/lib/systemd/system', 'systemdsystemgeneratordir': '/lib/systemd/system-generators', } @@ -270,7 +271,9 @@ if not platform.system().endswith('BSD'): (ETC + '/NetworkManager/dispatcher.d/', ['tools/hook-network-manager']), (ETC + '/dhcp/dhclient-exit-hooks.d/', ['tools/hook-dhclient']), - (LIB + '/udev/rules.d', [f for f in glob('udev/*.rules')]) + (LIB + '/udev/rules.d', [f for f in glob('udev/*.rules')]), + (ETC + '/systemd/system/sshd-keygen@.service.d/', + ['systemd/disable-sshd-keygen-if-cloud-init-active.conf']), ]) # Use a subclass for install that handles # adding on the right init system configuration files diff --git a/systemd/disable-sshd-keygen-if-cloud-init-active.conf b/systemd/disable-sshd-keygen-if-cloud-init-active.conf new file mode 100644 index 00000000..71e35876 --- /dev/null +++ b/systemd/disable-sshd-keygen-if-cloud-init-active.conf @@ -0,0 +1,8 @@ +# In some cloud-init enabled images the sshd-keygen template service may race +# with cloud-init during boot causing issues with host key generation. This +# drop-in config adds a condition to sshd-keygen@.service if it exists and +# prevents the sshd-keygen units from running *if* cloud-init is going to run. +# +[Unit] +ConditionPathExists=!/run/systemd/generator.early/multi-user.target.wants/cloud-init.target +EOF -- cgit v1.2.3 From a8380a125d40ff0ae88f2ba25a518346f2063a1a Mon Sep 17 00:00:00 2001 From: Emanuele Giuseppe Esposito Date: Tue, 26 Oct 2021 16:15:47 +0200 Subject: remove unnecessary EOF string in disable-sshd-keygen-if-cloud-init-active.conf (#1075) Running 'systemd-analyze verify cloud-init-local.service' triggers the following warning: disable-sshhd-keygen-if-cloud-init-active.conf:8: Missing '=', ignoring line. The string "EOF" is probably a typo, so remove it. Signed-off-by: Emanuele Giuseppe Esposito --- systemd/disable-sshd-keygen-if-cloud-init-active.conf | 1 - 1 file changed, 1 deletion(-) (limited to 'systemd') diff --git a/systemd/disable-sshd-keygen-if-cloud-init-active.conf b/systemd/disable-sshd-keygen-if-cloud-init-active.conf index 71e35876..1a5d7a5a 100644 --- a/systemd/disable-sshd-keygen-if-cloud-init-active.conf +++ b/systemd/disable-sshd-keygen-if-cloud-init-active.conf @@ -5,4 +5,3 @@ # [Unit] ConditionPathExists=!/run/systemd/generator.early/multi-user.target.wants/cloud-init.target -EOF -- cgit v1.2.3 From 0ed00ad9cdebc2d4dabd8bd6d7c901584963def5 Mon Sep 17 00:00:00 2001 From: Haruki TSURUMOTO Date: Tue, 7 Dec 2021 23:23:45 +0900 Subject: Add miraclelinux support (#1128) --- README.md | 2 +- cloudinit/config/cc_ntp.py | 4 ++-- cloudinit/config/cc_yum_add_repo.py | 2 +- cloudinit/distros/__init__.py | 3 ++- cloudinit/distros/miraclelinux.py | 8 ++++++++ cloudinit/net/sysconfig.py | 3 ++- cloudinit/util.py | 4 ++-- config/cloud.cfg.tmpl | 6 +++--- systemd/cloud-init-generator.tmpl | 2 +- systemd/cloud-init.service.tmpl | 2 +- tests/unittests/test_cli.py | 5 +++-- tests/unittests/test_util.py | 38 +++++++++++++++++++++++++++++++++++++ tools/.github-cla-signers | 1 + tools/read-dependencies | 4 ++++ tools/render-cloudcfg | 2 +- 15 files changed, 70 insertions(+), 16 deletions(-) create mode 100644 cloudinit/distros/miraclelinux.py (limited to 'systemd') diff --git a/README.md b/README.md index 27098b11..f2a745f8 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ get in contact with that distribution and send them our way! | Supported OSes | Supported Public Clouds | Supported Private Clouds | | --- | --- | --- | -| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
openEuler
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux/CloudLinux
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| +| Alpine Linux
ArchLinux
Debian
DragonFlyBSD
Fedora
FreeBSD
Gentoo Linux
NetBSD
OpenBSD
openEuler
RHEL/CentOS/AlmaLinux/Rocky/PhotonOS/Virtuozzo/EuroLinux/CloudLinux/MIRACLE LINUX
SLES/openSUSE
Ubuntu










| Amazon Web Services
Microsoft Azure
Google Cloud Platform
Oracle Cloud Infrastructure
Softlayer
Rackspace Public Cloud
IBM Cloud
DigitalOcean
Bigstep
Hetzner
Joyent
CloudSigma
Alibaba Cloud
OVH
OpenNebula
Exoscale
Scaleway
CloudStack
AltCloud
SmartOS
HyperOne
Vultr
Rootbox
| Bare metal installs
OpenStack
LXD
KVM
Metal-as-a-Service (MAAS)
VMware















| ## To start developing cloud-init diff --git a/cloudinit/config/cc_ntp.py b/cloudinit/config/cc_ntp.py index 9c085a04..c55d5d86 100644 --- a/cloudinit/config/cc_ntp.py +++ b/cloudinit/config/cc_ntp.py @@ -25,8 +25,8 @@ frequency = PER_INSTANCE NTP_CONF = '/etc/ntp.conf' NR_POOL_SERVERS = 4 distros = ['almalinux', 'alpine', 'centos', 'cloudlinux', 'debian', - 'eurolinux', 'fedora', 'openEuler', 'opensuse', 'photon', - 'rhel', 'rocky', 'sles', 'ubuntu', 'virtuozzo'] + 'eurolinux', 'fedora', 'miraclelinux', 'openEuler', 'opensuse', + 'photon', 'rhel', 'rocky', 'sles', 'ubuntu', 'virtuozzo'] NTP_CLIENT_CONFIG = { 'chrony': { diff --git a/cloudinit/config/cc_yum_add_repo.py b/cloudinit/config/cc_yum_add_repo.py index d66d3ae4..046a2852 100644 --- a/cloudinit/config/cc_yum_add_repo.py +++ b/cloudinit/config/cc_yum_add_repo.py @@ -19,7 +19,7 @@ entry, the config entry will be skipped. **Module frequency:** always **Supported distros:** almalinux, centos, cloudlinux, eurolinux, fedora, - openEuler, photon, rhel, rocky, virtuozzo + miraclelinux, openEuler, photon, rhel, rocky, virtuozzo **Config keys**:: diff --git a/cloudinit/distros/__init__.py b/cloudinit/distros/__init__.py index fe44f20e..742804ea 100755 --- a/cloudinit/distros/__init__.py +++ b/cloudinit/distros/__init__.py @@ -50,7 +50,8 @@ OSFAMILIES = { 'freebsd': ['freebsd'], 'gentoo': ['gentoo'], 'redhat': ['almalinux', 'amazon', 'centos', 'cloudlinux', 'eurolinux', - 'fedora', 'openEuler', 'photon', 'rhel', 'rocky', 'virtuozzo'], + 'fedora', 'miraclelinux', 'openEuler', 'photon', 'rhel', + 'rocky', 'virtuozzo'], 'suse': ['opensuse', 'sles'], } diff --git a/cloudinit/distros/miraclelinux.py b/cloudinit/distros/miraclelinux.py new file mode 100644 index 00000000..c7753387 --- /dev/null +++ b/cloudinit/distros/miraclelinux.py @@ -0,0 +1,8 @@ +# This file is part of cloud-init. See LICENSE file for license information. + +from cloudinit.distros import rhel + + +class Distro(rhel.Distro): + pass +# vi: ts=4 expandtab diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py index ef4543b4..85342219 100644 --- a/cloudinit/net/sysconfig.py +++ b/cloudinit/net/sysconfig.py @@ -19,7 +19,8 @@ from .network_state import ( LOG = logging.getLogger(__name__) KNOWN_DISTROS = ['almalinux', 'centos', 'cloudlinux', 'eurolinux', 'fedora', - 'openEuler', 'rhel', 'rocky', 'suse', 'virtuozzo'] + 'miraclelinux', 'openEuler', 'rhel', 'rocky', 'suse', + 'virtuozzo'] NM_CFG_FILE = "/etc/NetworkManager/NetworkManager.conf" diff --git a/cloudinit/util.py b/cloudinit/util.py index 1b462a38..cad087a1 100644 --- a/cloudinit/util.py +++ b/cloudinit/util.py @@ -540,8 +540,8 @@ def _get_variant(info): linux_dist = info['dist'][0].lower() if linux_dist in ( 'almalinux', 'alpine', 'arch', 'centos', 'cloudlinux', - 'debian', 'eurolinux', 'fedora', 'openeuler', 'photon', - 'rhel', 'rocky', 'suse', 'virtuozzo'): + 'debian', 'eurolinux', 'fedora', 'miraclelinux', 'openeuler', + 'photon', 'rhel', 'rocky', 'suse', 'virtuozzo'): variant = linux_dist elif linux_dist in ('ubuntu', 'linuxmint', 'mint'): variant = 'ubuntu' diff --git a/config/cloud.cfg.tmpl b/config/cloud.cfg.tmpl index b66bbe60..741b23d5 100644 --- a/config/cloud.cfg.tmpl +++ b/config/cloud.cfg.tmpl @@ -33,7 +33,7 @@ disable_root: true {% endif %} {% if variant in ["almalinux", "alpine", "amazon", "centos", "cloudlinux", "eurolinux", - "fedora", "openEuler", "rhel", "rocky", "virtuozzo"] %} + "fedora", "miraclelinux", "openEuler", "rhel", "rocky", "virtuozzo"] %} mount_default_fields: [~, ~, 'auto', 'defaults,nofail', '0', '2'] {% if variant == "amazon" %} resize_rootfs: noblock @@ -176,7 +176,7 @@ cloud_final_modules: system_info: # This will affect which distro class gets used {% if variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", - "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "openEuler", + "eurolinux", "fedora", "freebsd", "netbsd", "miraclelinux", "openbsd", "openEuler", "photon", "rhel", "rocky", "suse", "ubuntu", "virtuozzo"] %} distro: {{ variant }} {% elif variant in ["dragonfly"] %} @@ -231,7 +231,7 @@ system_info: security: http://ports.ubuntu.com/ubuntu-ports ssh_svcname: ssh {% elif variant in ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "eurolinux", - "fedora", "openEuler", "rhel", "rocky", "suse", "virtuozzo"] %} + "fedora", "miraclelinux", "openEuler", "rhel", "rocky", "suse", "virtuozzo"] %} # Default user name + that default users groups (if added/used) default_user: {% if variant == "amazon" %} diff --git a/systemd/cloud-init-generator.tmpl b/systemd/cloud-init-generator.tmpl index 7d1e7256..74d47428 100644 --- a/systemd/cloud-init-generator.tmpl +++ b/systemd/cloud-init-generator.tmpl @@ -84,7 +84,7 @@ default() { check_for_datasource() { local ds_rc="" {% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", - "openEuler", "rhel", "rocky", "virtuozzo"] %} + "miraclelinux", "openEuler", "rhel", "rocky", "virtuozzo"] %} local dsidentify="/usr/libexec/cloud-init/ds-identify" {% else %} local dsidentify="/usr/lib/cloud-init/ds-identify" diff --git a/systemd/cloud-init.service.tmpl b/systemd/cloud-init.service.tmpl index de3f3d91..e71e5679 100644 --- a/systemd/cloud-init.service.tmpl +++ b/systemd/cloud-init.service.tmpl @@ -13,7 +13,7 @@ After=systemd-networkd-wait-online.service After=networking.service {% endif %} {% if variant in ["almalinux", "centos", "cloudlinux", "eurolinux", "fedora", - "openEuler", "rhel", "rocky", "virtuozzo"] %} + "miraclelinux", "openEuler", "rhel", "rocky", "virtuozzo"] %} After=network.service After=NetworkManager.service {% endif %} diff --git a/tests/unittests/test_cli.py b/tests/unittests/test_cli.py index d0162673..e30e89a7 100644 --- a/tests/unittests/test_cli.py +++ b/tests/unittests/test_cli.py @@ -236,8 +236,9 @@ class TestCLI(test_helpers.FilesystemMockingTestCase): "**Supported distros:** all", ( "**Supported distros:** almalinux, alpine, centos, " - "cloudlinux, debian, eurolinux, fedora, openEuler, " - "opensuse, photon, rhel, rocky, sles, ubuntu, virtuozzo" + "cloudlinux, debian, eurolinux, fedora, miraclelinux, " + "openEuler, opensuse, photon, rhel, rocky, sles, ubuntu, " + "virtuozzo" ), "**Config schema**:\n **resize_rootfs:** " "(true/false/noblock)", diff --git a/tests/unittests/test_util.py b/tests/unittests/test_util.py index 1290cbc6..3b76ead8 100644 --- a/tests/unittests/test_util.py +++ b/tests/unittests/test_util.py @@ -185,6 +185,25 @@ OS_RELEASE_EUROLINUX_8 = dedent( """ ) +OS_RELEASE_MIRACLELINUX_8 = dedent( + """\ + NAME="MIRACLE LINUX" + VERSION="8.4 (Peony)" + ID="miraclelinux" + ID_LIKE="rhel fedora" + PLATFORM_ID="platform:el8" + VERSION_ID="8" + PRETTY_NAME="MIRACLE LINUX 8.4 (Peony)" + ANSI_COLOR="0;31" + CPE_NAME="cpe:/o:cybertrust_japan:miracle_linux:8" + HOME_URL="https://www.cybertrust.co.jp/miracle-linux/" + DOCUMENTATION_URL="https://www.miraclelinux.com/support/miraclelinux8" + BUG_REPORT_URL="https://bugzilla.asianux.com/" + MIRACLELINUX_SUPPORT_PRODUCT="MIRACLE LINUX" + MIRACLELINUX_SUPPORT_PRODUCT_VERSION="8" +""" +) + OS_RELEASE_ROCKY_8 = dedent( """\ NAME="Rocky Linux" @@ -255,6 +274,7 @@ REDHAT_RELEASE_REDHAT_7 = "Red Hat Enterprise Linux Server release 7.5 (Maipo)" REDHAT_RELEASE_ALMALINUX_8 = "AlmaLinux release 8.3 (Purple Manul)" REDHAT_RELEASE_EUROLINUX_7 = "EuroLinux release 7.9 (Minsk)" REDHAT_RELEASE_EUROLINUX_8 = "EuroLinux release 8.4 (Vaduz)" +REDHAT_RELEASE_MIRACLELINUX_8 = "MIRACLE LINUX release 8.4 (Peony)" REDHAT_RELEASE_ROCKY_8 = "Rocky Linux release 8.3 (Green Obsidian)" REDHAT_RELEASE_VIRTUOZZO_8 = "Virtuozzo Linux release 8" REDHAT_RELEASE_CLOUDLINUX_8 = "CloudLinux release 8.4 (Valery Rozhdestvensky)" @@ -754,6 +774,24 @@ class TestGetLinuxDistro(CiTestCase): dist = util.get_linux_distro() self.assertEqual(('eurolinux', '8.4', 'Vaduz'), dist) + @mock.patch('cloudinit.util.load_file') + def test_get_linux_miraclelinux8_rhrelease(self, m_os_release, + m_path_exists): + """Verify miraclelinux 8 read from redhat-release.""" + m_os_release.return_value = REDHAT_RELEASE_MIRACLELINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.redhat_release_exists + dist = util.get_linux_distro() + self.assertEqual(('miracle', '8.4', 'Peony'), dist) + + @mock.patch('cloudinit.util.load_file') + def test_get_linux_miraclelinux8_osrelease(self, m_os_release, + m_path_exists): + """Verify miraclelinux 8 read from os-release.""" + m_os_release.return_value = OS_RELEASE_MIRACLELINUX_8 + m_path_exists.side_effect = TestGetLinuxDistro.os_release_exists + dist = util.get_linux_distro() + self.assertEqual(('miraclelinux', '8', 'Peony'), dist) + @mock.patch('cloudinit.util.load_file') def test_get_linux_rocky8_rhrelease(self, m_os_release, m_path_exists): """Verify rocky linux 8 read from redhat-release.""" diff --git a/tools/.github-cla-signers b/tools/.github-cla-signers index 492ed15e..a2da8a62 100644 --- a/tools/.github-cla-signers +++ b/tools/.github-cla-signers @@ -73,6 +73,7 @@ timothegenzmer tnt-dev tomponline tsanghan +tSU-RooT vteratipally Vultaire WebSpider diff --git a/tools/read-dependencies b/tools/read-dependencies index 810154e4..efa5879c 100755 --- a/tools/read-dependencies +++ b/tools/read-dependencies @@ -24,6 +24,7 @@ DEFAULT_REQUIREMENTS = 'requirements.txt' DISTRO_PKG_TYPE_MAP = { 'centos': 'redhat', 'eurolinux': 'redhat', + 'miraclelinux': 'redhat', 'rocky': 'redhat', 'redhat': 'redhat', 'debian': 'debian', @@ -70,12 +71,14 @@ DRY_DISTRO_INSTALL_PKG_CMD = { 'rocky': ['yum', 'install', '--assumeyes'], 'centos': ['yum', 'install', '--assumeyes'], 'eurolinux': ['yum', 'install', '--assumeyes'], + 'miraclelinux': ['yum', 'install', '--assumeyes'], 'redhat': ['yum', 'install', '--assumeyes'], } DISTRO_INSTALL_PKG_CMD = { 'rocky': MAYBE_RELIABLE_YUM_INSTALL, 'eurolinux': MAYBE_RELIABLE_YUM_INSTALL, + 'miraclelinux': MAYBE_RELIABLE_YUM_INSTALL, 'centos': MAYBE_RELIABLE_YUM_INSTALL, 'redhat': MAYBE_RELIABLE_YUM_INSTALL, 'debian': ['apt', 'install', '-y'], @@ -89,6 +92,7 @@ DISTRO_INSTALL_PKG_CMD = { CI_SYSTEM_BASE_PKGS = { 'common': ['make', 'sudo', 'tar'], 'eurolinux': ['python3-tox'], + 'miraclelinux': ['python3-tox'], 'redhat': ['python3-tox'], 'centos': ['python3-tox'], 'ubuntu': ['devscripts', 'python3-dev', 'libssl-dev', 'tox', 'sbuild'], diff --git a/tools/render-cloudcfg b/tools/render-cloudcfg index 186d61b7..6642bd58 100755 --- a/tools/render-cloudcfg +++ b/tools/render-cloudcfg @@ -5,7 +5,7 @@ import os import sys VARIANTS = ["almalinux", "alpine", "amazon", "arch", "centos", "cloudlinux", "debian", - "eurolinux", "fedora", "freebsd", "netbsd", "openbsd", "openEuler", "photon", + "eurolinux", "fedora", "freebsd", "miraclelinux", "netbsd", "openbsd", "openEuler", "photon", "rhel", "suse","rocky", "ubuntu", "unknown", "virtuozzo"] -- cgit v1.2.3