From da7a5784733d5d0706c517bafb2215e3ea9f10f8 Mon Sep 17 00:00:00 2001 From: Alexander Gordeev Date: Fri, 5 Aug 2016 19:02:36 +0300 Subject: [PATCH] Configure admin NIC prior the first system boot Having admin NIC confugured in the middle of system booting is very fragile and error-prone approach. It's better to configure it ahead of first booting time. Also, there's no need for freaky networking restarting inside of cloud-init boothooks anymore. Change-Id: I4c278341e8b40eb8d9b100bed1d9a650f27b5c5b Related-Bug: #1583815 --- cloud-init-templates/boothook_centos.jinja2 | 16 --- .../boothook_fuel_6.1_centos.jinja2 | 16 --- .../boothook_fuel_6.1_ubuntu.jinja2 | 20 --- .../boothook_fuel_7.0_centos.jinja2 | 16 --- .../boothook_fuel_7.0_ubuntu.jinja2 | 20 --- .../boothook_fuel_8.0_ubuntu.jinja2 | 20 --- .../boothook_fuel_9.0_ubuntu.jinja2 | 22 +--- cloud-init-templates/boothook_ubuntu.jinja2 | 20 --- fuel_agent/errors.py | 4 + fuel_agent/manager.py | 39 +++--- fuel_agent/tests/test_manager.py | 56 ++++----- fuel_agent/tests/test_provision_utils.py | 116 ++++++++++++++++++ fuel_agent/utils/provision.py | 100 +++++++++++++++ 13 files changed, 263 insertions(+), 202 deletions(-) create mode 100644 fuel_agent/tests/test_provision_utils.py create mode 100644 fuel_agent/utils/provision.py diff --git a/cloud-init-templates/boothook_centos.jinja2 b/cloud-init-templates/boothook_centos.jinja2 index bc81fd77..5e39e14d 100644 --- a/cloud-init-templates/boothook_centos.jinja2 +++ b/cloud-init-templates/boothook_centos.jinja2 @@ -15,22 +15,6 @@ cloud-init-per instance disable_selinux_on_the_fly setenforce 0 cloud-init-per instance disable_selinux sed -i 's/^SELINUX=.*/SELINUX=disabled/g' /etc/sysconfig/selinux - -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 service network stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -cloud-init-per instance configure_admin_interface /bin/sh -c "echo -e \"# FROM COBBLER SNIPPET\nDEVICE=$ADMIN_IF\nIPADDR={{ common.admin_ip }}\nNETMASK={{ common.admin_mask }}\nBOOTPROTO=none\nONBOOT=yes\nUSERCTL=no\n\" | tee /etc/sysconfig/network-scripts/ifcfg-$ADMIN_IF" - -cloud-init-per instance set_gateway /bin/sh -c 'echo GATEWAY="{{ common.gw }}" | tee -a /etc/sysconfig/network' - -cloud-init-per instance udev_persistent_net5 service network start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_remove rm -f /etc/resolv.conf diff --git a/cloud-init-templates/boothook_fuel_6.1_centos.jinja2 b/cloud-init-templates/boothook_fuel_6.1_centos.jinja2 index 5c25c32c..14524eca 100644 --- a/cloud-init-templates/boothook_fuel_6.1_centos.jinja2 +++ b/cloud-init-templates/boothook_fuel_6.1_centos.jinja2 @@ -15,22 +15,6 @@ cloud-init-per instance disable_selinux_on_the_fly setenforce 0 cloud-init-per instance disable_selinux sed -i 's/^SELINUX=.*/SELINUX=disabled/g' /etc/sysconfig/selinux - -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 service network stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -cloud-init-per instance configure_admin_interface /bin/sh -c "echo -e \"# FROM COBBLER SNIPPET\nDEVICE=$ADMIN_IF\nIPADDR={{ common.admin_ip }}\nNETMASK={{ common.admin_mask }}\nBOOTPROTO=none\nONBOOT=yes\nUSERCTL=no\n\" | tee /etc/sysconfig/network-scripts/ifcfg-$ADMIN_IF" - -cloud-init-per instance set_gateway /bin/sh -c 'echo GATEWAY="{{ common.gw }}" | tee -a /etc/sysconfig/network' - -cloud-init-per instance udev_persistent_net5 service network start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_remove rm -f /etc/resolv.conf diff --git a/cloud-init-templates/boothook_fuel_6.1_ubuntu.jinja2 b/cloud-init-templates/boothook_fuel_6.1_ubuntu.jinja2 index 63fe3974..4753f2af 100644 --- a/cloud-init-templates/boothook_fuel_6.1_ubuntu.jinja2 +++ b/cloud-init-templates/boothook_fuel_6.1_ubuntu.jinja2 @@ -12,26 +12,6 @@ function add_str_to_file_if_not_exists { cloud-init-per instance wipe_sources_list_templates /bin/sh -c 'echo | tee /etc/cloud/templates/sources.list.ubuntu.tmpl' -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 /etc/init.d/networking stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -# Check if we do not already have static config (or interface seems unconfigured) -if [ ! -d "/etc/network/interfaces.d" ]; then - mkdir -p /etc/network/interfaces.d - echo 'source /etc/network/interfaces.d/*' > /etc/network/interfaces -fi -if [ ! -e "/etc/network/interfaces.d/ifcfg-$ADMIN_IF" ]; then - echo -e "auto $ADMIN_IF\niface $ADMIN_IF inet static\n\taddress {{ common.admin_ip }}\n\tnetmask {{ common.admin_mask }}\n\tgateway {{ common.gw }}" > /etc/network/interfaces.d/ifcfg-"$ADMIN_IF" -fi - -cloud-init-per instance udev_persistent_net5 /etc/init.d/networking start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_mkdir mkdir -p /etc/resolvconf/resolv.conf.d diff --git a/cloud-init-templates/boothook_fuel_7.0_centos.jinja2 b/cloud-init-templates/boothook_fuel_7.0_centos.jinja2 index a82134f5..339dcfa4 100644 --- a/cloud-init-templates/boothook_fuel_7.0_centos.jinja2 +++ b/cloud-init-templates/boothook_fuel_7.0_centos.jinja2 @@ -15,22 +15,6 @@ cloud-init-per instance disable_selinux_on_the_fly setenforce 0 cloud-init-per instance disable_selinux sed -i 's/^SELINUX=.*/SELINUX=disabled/g' /etc/sysconfig/selinux - -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 service network stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -cloud-init-per instance configure_admin_interface /bin/sh -c "echo -e \"# FROM COBBLER SNIPPET\nDEVICE=$ADMIN_IF\nIPADDR={{ common.admin_ip }}\nNETMASK={{ common.admin_mask }}\nBOOTPROTO=none\nONBOOT=yes\nUSERCTL=no\n\" | tee /etc/sysconfig/network-scripts/ifcfg-$ADMIN_IF" - -cloud-init-per instance set_gateway /bin/sh -c 'echo GATEWAY="{{ common.gw }}" | tee -a /etc/sysconfig/network' - -cloud-init-per instance udev_persistent_net5 service network start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_remove rm -f /etc/resolv.conf diff --git a/cloud-init-templates/boothook_fuel_7.0_ubuntu.jinja2 b/cloud-init-templates/boothook_fuel_7.0_ubuntu.jinja2 index 253be2d6..73d5ea00 100644 --- a/cloud-init-templates/boothook_fuel_7.0_ubuntu.jinja2 +++ b/cloud-init-templates/boothook_fuel_7.0_ubuntu.jinja2 @@ -12,26 +12,6 @@ function add_str_to_file_if_not_exists { cloud-init-per instance wipe_sources_list_templates /bin/sh -c 'echo | tee /etc/cloud/templates/sources.list.ubuntu.tmpl' -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 /etc/init.d/networking stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -# Check if we do not already have static config (or interface seems unconfigured) -if [ ! -d "/etc/network/interfaces.d" ]; then - mkdir -p /etc/network/interfaces.d - echo 'source /etc/network/interfaces.d/*' > /etc/network/interfaces -fi -if [ ! -e "/etc/network/interfaces.d/ifcfg-$ADMIN_IF" ]; then - echo -e "auto $ADMIN_IF\niface $ADMIN_IF inet static\n\taddress {{ common.admin_ip }}\n\tnetmask {{ common.admin_mask }}\n\tgateway {{ common.gw }}" > /etc/network/interfaces.d/ifcfg-"$ADMIN_IF" -fi - -cloud-init-per instance udev_persistent_net5 /etc/init.d/networking start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_mkdir mkdir -p /etc/resolvconf/resolv.conf.d diff --git a/cloud-init-templates/boothook_fuel_8.0_ubuntu.jinja2 b/cloud-init-templates/boothook_fuel_8.0_ubuntu.jinja2 index b526f289..e5eaf9cc 100644 --- a/cloud-init-templates/boothook_fuel_8.0_ubuntu.jinja2 +++ b/cloud-init-templates/boothook_fuel_8.0_ubuntu.jinja2 @@ -12,26 +12,6 @@ function add_str_to_file_if_not_exists { cloud-init-per instance wipe_sources_list_templates /bin/sh -c 'echo | tee /etc/cloud/templates/sources.list.ubuntu.tmpl' -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 /etc/init.d/networking stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -# Check if we do not already have static config (or interface seems unconfigured) -if [ ! -d "/etc/network/interfaces.d" ]; then - mkdir -p /etc/network/interfaces.d - echo 'source /etc/network/interfaces.d/*' > /etc/network/interfaces -fi -if [ ! -e "/etc/network/interfaces.d/ifcfg-$ADMIN_IF" ]; then - echo -e "auto $ADMIN_IF\niface $ADMIN_IF inet static\n\taddress {{ common.admin_ip }}\n\tnetmask {{ common.admin_mask }}\n\tgateway {{ common.gw }}" > /etc/network/interfaces.d/ifcfg-"$ADMIN_IF" -fi - -cloud-init-per instance udev_persistent_net5 /etc/init.d/networking start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_mkdir mkdir -p /etc/resolvconf/resolv.conf.d diff --git a/cloud-init-templates/boothook_fuel_9.0_ubuntu.jinja2 b/cloud-init-templates/boothook_fuel_9.0_ubuntu.jinja2 index 0afefcc3..e5eaf9cc 100644 --- a/cloud-init-templates/boothook_fuel_9.0_ubuntu.jinja2 +++ b/cloud-init-templates/boothook_fuel_9.0_ubuntu.jinja2 @@ -12,26 +12,6 @@ function add_str_to_file_if_not_exists { cloud-init-per instance wipe_sources_list_templates /bin/sh -c 'echo | tee /etc/cloud/templates/sources.list.ubuntu.tmpl' -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 /etc/init.d/networking stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -# Check if we do not already have static config (or interface seems unconfigured) -if [ ! -d "/etc/network/interfaces.d" ]; then - mkdir -p /etc/network/interfaces.d - echo 'source /etc/network/interfaces.d/*' > /etc/network/interfaces -fi -if [ ! -e "/etc/network/interfaces.d/ifcfg-$ADMIN_IF" ]; then - echo -e "auto $ADMIN_IF\niface $ADMIN_IF inet static\n\taddress {{ common.admin_ip }}\n\tnetmask {{ common.admin_mask }}\n\tgateway {{ common.gw }}" > /etc/network/interfaces.d/ifcfg-"$ADMIN_IF" -fi - -cloud-init-per instance udev_persistent_net5 /etc/init.d/networking start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_mkdir mkdir -p /etc/resolvconf/resolv.conf.d @@ -96,4 +76,4 @@ cloud-init-per instance skel_bash cp -f /etc/skel/.bash* /root/ cloud-init-per instance hiera_puppet mkdir -p /etc/puppet /var/lib/hiera cloud-init-per instance touch_puppet touch /var/lib/hiera/common.yaml /etc/puppet/hiera.yaml /var/log/puppet.log -cloud-init-per instance chmod_puppet chmod 600 /var/log/puppet.log \ No newline at end of file +cloud-init-per instance chmod_puppet chmod 600 /var/log/puppet.log diff --git a/cloud-init-templates/boothook_ubuntu.jinja2 b/cloud-init-templates/boothook_ubuntu.jinja2 index 78ff1fc8..42577ad6 100644 --- a/cloud-init-templates/boothook_ubuntu.jinja2 +++ b/cloud-init-templates/boothook_ubuntu.jinja2 @@ -12,26 +12,6 @@ function add_str_to_file_if_not_exists { cloud-init-per instance wipe_sources_list_templates /bin/sh -c 'echo | tee /etc/cloud/templates/sources.list.ubuntu.tmpl' -# configure udev rules - -# udev persistent net -cloud-init-per instance udev_persistent_net1 /etc/init.d/networking stop - -ADMIN_MAC={{ common.admin_mac }} -ADMIN_IF=$(echo {{ common.udevrules }} | sed 's/[,=]/\n/g' | grep "$ADMIN_MAC" | cut -d_ -f2 | head -1) -# Check if we do not already have static config (or interface seems unconfigured) -if [ ! -d "/etc/network/interfaces.d" ]; then - mkdir -p /etc/network/interfaces.d - echo 'source /etc/network/interfaces.d/*' > /etc/network/interfaces -fi -if [ ! -e "/etc/network/interfaces.d/ifcfg-$ADMIN_IF" ]; then - echo -e "auto $ADMIN_IF\niface $ADMIN_IF inet static\n\taddress {{ common.admin_ip }}\n\tnetmask {{ common.admin_mask }}\n\tgateway {{ common.gw }}" > /etc/network/interfaces.d/ifcfg-"$ADMIN_IF" -fi - -cloud-init-per instance udev_persistent_net5 /etc/init.d/networking start - -# end of udev - #FIXME(agordeev): if operator updates dns settings on masternode after the node had been provisioned, # cloud-init will start to generate resolv.conf with non-actual data cloud-init-per instance resolv_conf_mkdir mkdir -p /etc/resolvconf/resolv.conf.d diff --git a/fuel_agent/errors.py b/fuel_agent/errors.py index ab619489..04efa657 100644 --- a/fuel_agent/errors.py +++ b/fuel_agent/errors.py @@ -182,3 +182,7 @@ class WrongOutputContainer(BaseError): class BootstrapFileAlreadyExists(BaseError): pass + + +class UnsupportedNetworkConfiguration(BaseError): + pass diff --git a/fuel_agent/manager.py b/fuel_agent/manager.py index 62f5aab9..e717231d 100644 --- a/fuel_agent/manager.py +++ b/fuel_agent/manager.py @@ -32,6 +32,7 @@ from fuel_agent.utils import hardware as hw from fuel_agent.utils import lvm as lu from fuel_agent.utils import md as mu from fuel_agent.utils import partition as pu +from fuel_agent.utils import provision from fuel_agent.utils import utils opts = [ @@ -922,31 +923,19 @@ class Manager(object): gu.grub2_install(install_devices, chroot=chroot) if CONF.fix_udev_net_rules: - # FIXME(agordeev) There's no convenient way to perfrom NIC - # remapping in Ubuntu, so injecting files prior the first boot - # should work - with open(chroot + '/etc/udev/rules.d/70-persistent-net.rules', - 'wt', encoding='utf-8') as f: - f.write(u'# Generated by fuel-agent during provisioning: ' - u'BEGIN\n') - # pattern is aa:bb:cc:dd:ee:ff_eth0,aa:bb:cc:dd:ee:ff_eth1 - for mapping in self.driver.configdrive_scheme. \ - common.udevrules.split(','): - mac_addr, nic_name = mapping.split('_') - f.write(u'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' - u'ATTR{address}=="%s", ATTR{type}=="1", ' - u'KERNEL=="eth*", NAME="%s"\n' % (mac_addr, - nic_name)) - f.write( - u'# Generated by fuel-agent during provisioning: END\n') - # FIXME(agordeev): Disable net-generator that will add new etries - # to 70-persistent-net.rules - with open(chroot + '/etc/udev/rules.d/' - '75-persistent-net-generator.rules', 'wt', - encoding='utf-8') as f: - f.write(u'# Generated by fuel-agent during provisioning:\n' - u'# DO NOT DELETE. It is needed to disable ' - u'net-generator\n') + provision.udev_nic_naming_rules( + chroot, self.driver.configdrive_scheme.common.udevrules) + + if CONF.prepare_configdrive: + # FIXME(agordeev): Normally, that should be handled out side of + # fuel-agent. Just a temporary fix to avoid dealing with cloud-init + # boothooks. + provision.configure_admin_nic( + chroot=chroot, + iface=self.driver.configdrive_scheme.common.admin_iface_name, + ip=self.driver.configdrive_scheme.common.admin_ip, + netmask=self.driver.configdrive_scheme.common.admin_mask, + gw=self.driver.configdrive_scheme.common.gw) # FIXME(kozhukalov): Prevent nailgun-agent from doing anything. # This ugly hack is to be used together with the command removing diff --git a/fuel_agent/tests/test_manager.py b/fuel_agent/tests/test_manager.py index 17c5d6bf..41d72e97 100644 --- a/fuel_agent/tests/test_manager.py +++ b/fuel_agent/tests/test_manager.py @@ -61,6 +61,7 @@ class TestManager(unittest2.TestCase): mock_lbd.return_value = test_nailgun.LIST_BLOCK_DEVICES_SAMPLE self.mgr = manager.Manager(test_nailgun.PROVISION_SAMPLE_DATA) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -72,7 +73,8 @@ class TestManager(unittest2.TestCase): def test_do_bootloader_grub1_kernel_initrd_guessed(self, mock_umount, mock_mount, mock_utils, mock_gu, mock_open, - mock_bu, mock_hw): + mock_bu, mock_hw, + mock_prov): mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('', '') mock_gu.guess_grub_version.return_value = 1 @@ -99,6 +101,7 @@ class TestManager(unittest2.TestCase): mock_gu.guess_kernel.assert_called_once_with( regexp='fake_kernel_regexp', chroot='/tmp/target') + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -110,7 +113,8 @@ class TestManager(unittest2.TestCase): def test_do_bootloader_grub1_kernel_initrd_set(self, mock_umount, mock_mount, mock_utils, mock_gu, mock_open, - mock_bu, mock_hw): + mock_bu, mock_hw, + mock_prov): mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('', '') mock_gu.guess_grub_version.return_value = 1 @@ -132,6 +136,7 @@ class TestManager(unittest2.TestCase): self.assertFalse(mock_gu.guess_kernel.called) self.assertFalse(mock_bu.override_lvm_config.called) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.objects.bootloader.Grub', autospec=True) @@ -143,7 +148,7 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_rootfs_uuid(self, mock_umount, mock_mount, mock_utils, mock_gu, mock_open, - mock_grub, mock_bu, mock_hw): + mock_grub, mock_bu, mock_hw, mock_prov): def _fake_uuid(*args, **kwargs): if len(args) >= 8 and args[7] == '/dev/mapper/os-root': return ('FAKE_ROOTFS_UUID', None) @@ -179,6 +184,7 @@ class TestManager(unittest2.TestCase): self.assertRaises(errors.WrongPartitionSchemeError, self.mgr.do_bootloader) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -189,7 +195,7 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_grub_version_changes( self, mock_umount, mock_mount, mock_utils, mock_gu, mock_open, - mock_bu, mock_hw): + mock_bu, mock_hw, mock_prov): # actually covers only grub1 related logic mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('fake_UUID\n', None) @@ -199,6 +205,7 @@ class TestManager(unittest2.TestCase): chroot='/tmp/target') self.assertEqual('expected_version', self.mgr.driver.grub.version) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -208,7 +215,8 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'mount_target') @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_grub1(self, mock_umount, mock_mount, mock_utils, - mock_gu, mock_open, mock_bu, mock_hw): + mock_gu, mock_open, mock_bu, mock_hw, + mock_prov): # actually covers only grub1 related logic mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('fake_UUID\n', None) @@ -231,6 +239,7 @@ class TestManager(unittest2.TestCase): self.assertFalse(mock_gu.grub2_cfg.called) self.assertFalse(mock_gu.grub2_install.called) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -240,7 +249,8 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'mount_target') @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_grub2(self, mock_umount, mock_mount, mock_utils, - mock_gu, mock_open, mock_bu, mock_hw): + mock_gu, mock_open, mock_bu, mock_hw, + mock_prov): # actually covers only grub2 related logic mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('fake_UUID\n', None) @@ -258,6 +268,7 @@ class TestManager(unittest2.TestCase): self.assertFalse(mock_gu.grub1_cfg.called) self.assertFalse(mock_gu.grub1_install.called) + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.open', @@ -268,7 +279,7 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_with_multipath( self, mock_umount, mock_mount, mock_utils, mock_gu, mock_open, - mock_bu, mock_hw): + mock_bu, mock_hw, mock_prov): # actually covers only multipath related logic # Lets assume that only /dev/sda device is not-multipath mock_hw.is_multipath_device.side_effect = False, False, True @@ -294,6 +305,7 @@ class TestManager(unittest2.TestCase): update_initramfs=True, lvm_conf_path='/etc/lvm/lvm.conf') + @mock.patch('fuel_agent.manager.provision', autospec=True) @mock.patch('fuel_agent.manager.hw', autospec=True) @mock.patch('fuel_agent.manager.bu', autospec=True) @mock.patch('fuel_agent.manager.gu', create=True) @@ -301,7 +313,7 @@ class TestManager(unittest2.TestCase): @mock.patch.object(manager.Manager, 'mount_target') @mock.patch.object(manager.Manager, 'umount_target') def test_do_bootloader_writes(self, mock_umount, mock_mount, mock_utils, - mock_gu, mock_bu, mock_hw): + mock_gu, mock_bu, mock_hw, mock_prov): # actually covers only write() calls mock_hw.is_multipath_device.return_value = False mock_utils.execute.return_value = ('fake_UUID\n', None) @@ -309,30 +321,10 @@ class TestManager(unittest2.TestCase): file_handle_mock = mock_open.return_value.__enter__.return_value self.mgr.do_bootloader() expected_open_calls = [ - mock.call('/tmp/target/etc/udev/rules.d/70-persistent-net.' - 'rules', 'wt', encoding='utf-8'), - mock.call('/tmp/target/etc/udev/rules.d/75-persistent-net-' - 'generator.rules', 'wt', encoding='utf-8'), mock.call('/tmp/target/etc/nailgun-agent/nodiscover', 'w'), mock.call('/tmp/target/etc/fstab', 'wt', encoding='utf-8')] self.assertEqual(expected_open_calls, mock_open.call_args_list) expected_write_calls = [ - mock.call('# Generated by fuel-agent during provisioning: ' - 'BEGIN\n'), - mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' - 'ATTR{address}=="08:00:27:79:da:80", ATTR{type}=="1"' - ', KERNEL=="eth*", NAME="eth0"\n'), - mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' - 'ATTR{address}=="08:00:27:46:43:60", ATTR{type}=="1"' - ', KERNEL=="eth*", NAME="eth1"\n'), - mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' - 'ATTR{address}=="08:00:27:b1:d7:15", ATTR{type}=="1"' - ', KERNEL=="eth*", NAME="eth2"\n'), - mock.call('# Generated by fuel-agent during provisioning: ' - 'END\n'), - mock.call('# Generated by fuel-agent during provisioning:\n# ' - 'DO NOT DELETE. It is needed to disable ' - 'net-generator\n'), mock.call('UUID=fake_UUID /boot ext2 defaults 0 0\n'), mock.call('UUID=fake_UUID /tmp ext2 defaults 0 0\n'), mock.call( @@ -346,6 +338,14 @@ class TestManager(unittest2.TestCase): mock_mount.assert_called_once_with('/tmp/target') mock_utils.makedirs_if_not_exists.assert_called_once_with( '/tmp/target/etc/nailgun-agent') + mock_prov.udev_nic_naming_rules.assert_called_once_with( + '/tmp/target', self.mgr.driver.configdrive_scheme.common.udevrules) + mock_prov.configure_admin_nic.assert_called_once_with( + chroot='/tmp/target', + iface=self.mgr.driver.configdrive_scheme.common.admin_iface_name, + ip=self.mgr.driver.configdrive_scheme.common.admin_ip, + netmask=self.mgr.driver.configdrive_scheme.common.admin_mask, + gw=self.mgr.driver.configdrive_scheme.common.gw) @mock.patch('fuel_agent.drivers.nailgun.Nailgun.parse_image_meta', return_value={}) diff --git a/fuel_agent/tests/test_provision_utils.py b/fuel_agent/tests/test_provision_utils.py new file mode 100644 index 00000000..ae969148 --- /dev/null +++ b/fuel_agent/tests/test_provision_utils.py @@ -0,0 +1,116 @@ +# Copyright 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import mock +import unittest2 + +from fuel_agent import errors +from fuel_agent.utils import provision + + +class TestProvisionUtils(unittest2.TestCase): + + def test_udev_nic_naming_rules(self): + udevrules = "08:00:27:79:da:80_eth0,08:00:27:46:43:60_eth1,"\ + "08:00:27:b1:d7:15_eth2" + with mock.patch.object(provision, 'open', create=True) as mock_open: + file_handle_mock = mock_open.return_value.__enter__.return_value + provision.udev_nic_naming_rules('/tmp/target', udevrules) + expected_open_calls = [ + mock.call('/tmp/target/etc/udev/rules.d/70-persistent-net.' + 'rules', 'wt', encoding='utf-8'), + mock.call('/tmp/target/etc/udev/rules.d/75-persistent-net-' + 'generator.rules', 'wt', encoding='utf-8')] + self.assertEqual(expected_open_calls, mock_open.call_args_list) + expected_write_calls = [ + mock.call('# Generated by fuel-agent during provisioning: ' + 'BEGIN\n'), + mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + 'ATTR{address}=="08:00:27:79:da:80", ATTR{type}=="1"' + ', KERNEL=="eth*", NAME="eth0"\n'), + mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + 'ATTR{address}=="08:00:27:46:43:60", ATTR{type}=="1"' + ', KERNEL=="eth*", NAME="eth1"\n'), + mock.call('SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + 'ATTR{address}=="08:00:27:b1:d7:15", ATTR{type}=="1"' + ', KERNEL=="eth*", NAME="eth2"\n'), + mock.call('# Generated by fuel-agent during provisioning: ' + 'END\n'), + mock.call('# Generated by fuel-agent during provisioning:\n# ' + 'DO NOT DELETE. It is needed to disable ' + 'net-generator\n'), + ] + self.assertEqual(expected_write_calls, + file_handle_mock.write.call_args_list) + + @mock.patch.object(provision, 'os', autospec=True) + def test_configure_admin_nic_failed(self, mock_os): + mock_os.path.exists.return_value = False + self.assertRaises(errors.UnsupportedNetworkConfiguration, + provision.configure_admin_nic, + 'chroot', 'iface', 'ip', 'netmask', 'gw') + self.assertEqual([mock.call('chroot/etc/network/interfaces'), + mock.call('chroot/etc/sysconfig/network-scripts')], + mock_os.path.exists.call_args_list) + + @mock.patch.object(provision, 'utils', autospec=True) + def test_configure_admin_nic_ubuntu(self, mock_utils): + with mock.patch.object(provision, 'open', create=True) as mock_open: + file_handle_mock = mock_open.return_value.__enter__.return_value + provision.configure_admin_nic_ubuntu( + '/tmp/target', 'IFACE', 'IP', 'NETMASK', 'GW') + expected_open_calls = [ + mock.call('/tmp/target/etc/network/interfaces', + 'wt', encoding='utf-8'), + mock.call('/tmp/target/etc/network/interfaces.d/ifcfg-IFACE', + 'wt', encoding='utf-8')] + self.assertEqual(expected_open_calls, mock_open.call_args_list) + expected_write_calls = [ + mock.call(u'# Generated by fuel-agent during provisioning:\n' + u'source-directory /etc/network/interfaces.d\n'), + mock.call(u'# Generated by fuel-agent during provisioning:\n' + u'auto IFACE\n' + u'iface IFACE inet static\n' + u'\taddress IP\n' + u'\tnetmask NETMASK\n' + u'\tgateway GW\n'), + ] + self.assertEqual(expected_write_calls, + file_handle_mock.write.call_args_list) + + @mock.patch.object(provision, 'utils', autospec=True) + def test_configure_admin_nic_centos(self, mock_utils): + with mock.patch.object(provision, 'open', create=True) as mock_open: + file_handle_mock = mock_open.return_value.__enter__.return_value + provision.configure_admin_nic_centos( + '/tmp/target', 'IFACE', 'IP', 'NETMASK', 'GW') + expected_open_calls = [ + mock.call('/tmp/target/etc/sysconfig/network-scripts/' + 'ifcfg-IFACE', + 'wt', encoding='utf-8'), + mock.call('/tmp/target/etc/sysconfig/network', + 'at', encoding='utf-8')] + self.assertEqual(expected_open_calls, mock_open.call_args_list) + expected_write_calls = [ + mock.call(u'# Generated by fuel-agent during provisioning:\n' + u'DEVICE=IFACE\n' + u'IPADDR=IP\n' + u'NETMASK=NETMASK\n' + u'BOOTPROTO=none\n' + u'ONBOOT=yes\n' + u'USERCTL=no\n'), + mock.call(u'GATEWAY="GW"\n'), + ] + self.assertEqual(expected_write_calls, + file_handle_mock.write.call_args_list) diff --git a/fuel_agent/utils/provision.py b/fuel_agent/utils/provision.py new file mode 100644 index 00000000..2f8cac7c --- /dev/null +++ b/fuel_agent/utils/provision.py @@ -0,0 +1,100 @@ +# Copyright 2016 Mirantis, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from io import open +import os + +from oslo_log import log as logging + +from fuel_agent import errors +from fuel_agent.utils import utils + +LOG = logging.getLogger(__name__) + + +def udev_nic_naming_rules(chroot, udevrules): + """Generates NIC naming rules for udev. + + Expected string formatting: "(macaddrX)_(nicX)" comma separated. + Eg.: "08:00:27:79:da:80_eth0,08:00:27:46:43:60_eth1" + """ + # FIXME(agordeev) There's no convenient way to perfrom NIC + # remapping in Ubuntu, so injecting files prior the first boot + # should work + with open(chroot + '/etc/udev/rules.d/70-persistent-net.rules', + 'wt', encoding='utf-8') as f: + f.write(u'# Generated by fuel-agent during provisioning: ' + u'BEGIN\n') + # pattern is aa:bb:cc:dd:ee:ff_eth0,aa:bb:cc:dd:ee:ff_eth1 + for mapping in udevrules.split(','): + mac_addr, nic_name = mapping.split('_') + f.write(u'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ' + u'ATTR{address}=="%s", ATTR{type}=="1", ' + u'KERNEL=="eth*", NAME="%s"\n' % (mac_addr, + nic_name)) + f.write( + u'# Generated by fuel-agent during provisioning: END\n') + # FIXME(agordeev): Disable net-generator that will add new entries + # to 70-persistent-net.rules + with open(chroot + '/etc/udev/rules.d/' + '75-persistent-net-generator.rules', 'wt', + encoding='utf-8') as f: + f.write(u'# Generated by fuel-agent during provisioning:\n' + u'# DO NOT DELETE. It is needed to disable ' + u'net-generator\n') + + +def configure_admin_nic(chroot, iface, ip, netmask, gw): + debian_conf = '/etc/network/interfaces' + redhat_conf = '/etc/sysconfig/network-scripts' + if os.path.exists(chroot + debian_conf): + configure_admin_nic_ubuntu(chroot, iface, ip, netmask, gw) + elif os.path.exists(chroot + redhat_conf): + configure_admin_nic_centos(chroot, iface, ip, netmask, gw) + else: + raise errors.UnsupportedNetworkConfiguration( + "Can't find suitable configuration files for admin NIC") + + +def configure_admin_nic_ubuntu(chroot, iface, ip, netmask, gw): + ifaces_dir = '/etc/network/interfaces.d' + ifcfg_path = os.path.join(ifaces_dir, 'ifcfg-{0}'.format(iface)) + utils.makedirs_if_not_exists(ifaces_dir) + with open(chroot + '/etc/network/interfaces', 'wt', encoding='utf-8') as f: + f.write(u'# Generated by fuel-agent during provisioning:\n' + u'source-directory /etc/network/interfaces.d\n') + with open(chroot + ifcfg_path, 'wt', encoding='utf-8') as f: + f.write(u'# Generated by fuel-agent during provisioning:\n' + u'auto {iface}\n' + u'iface {iface} inet static\n' + u'\taddress {ip}\n' + u'\tnetmask {netmask}\n' + u'\tgateway {gw}\n'.format(iface=iface, + ip=ip, + netmask=netmask, + gw=gw)) + + +def configure_admin_nic_centos(chroot, iface, ip, netmask, gw): + ifcfg_path = '/etc/sysconfig/network-scripts/ifcfg-{0}'.format(iface) + with open(chroot + ifcfg_path, 'wt', encoding='utf-8') as f: + f.write(u'# Generated by fuel-agent during provisioning:\n' + u'DEVICE={iface}\n' + u'IPADDR={ip}\n' + u'NETMASK={netmask}\n' + u'BOOTPROTO=none\n' + u'ONBOOT=yes\n' + u'USERCTL=no\n'.format(iface=iface, ip=ip, netmask=netmask)) + with open(chroot + '/etc/sysconfig/network', 'at', encoding='utf-8') as f: + f.write(u'GATEWAY="{gw}"\n'.format(gw=gw))