← Back to team overview

curtin-dev team mailing list archive

[Merge] ~alexsander-souza/curtin:sles_support into curtin:master

 

Alexsander de Souza has proposed merging ~alexsander-souza/curtin:sles_support into curtin:master.

Commit message:
add SLES support

Requested reviews:
  curtin developers (curtin-dev)

For more details, see:
https://code.launchpad.net/~alexsander-souza/curtin/+git/curtin/+merge/435914
-- 
Your team curtin developers is requested to review the proposed merge of ~alexsander-souza/curtin:sles_support into curtin:master.
diff --git a/curtin/block/deps.py b/curtin/block/deps.py
index 5cb23e0..ec5fe83 100644
--- a/curtin/block/deps.py
+++ b/curtin/block/deps.py
@@ -94,6 +94,25 @@ def detect_required_packages_mapping(osfamily=DISTROS.debian):
             'zfs': [],
             'zpool': [],
         },
+        DISTROS.suse: {
+            'bcache': ['bcache-tools'],
+            'btrfs': ['btrfsprogs'],
+            'dm_crypt': ['cryptsetup'],
+            'ext2': ['e2fsprogs'],
+            'ext3': ['e2fsprogs'],
+            'ext4': ['e2fsprogs'],
+            'jfs': ['jfsutils'],
+            'iscsi': [],
+            'lvm_partition': ['lvm2'],
+            'lvm_volgroup': ['lvm2'],
+            'ntfs': [],
+            'raid': ['mdadm'],
+            'reiserfs': [],
+            'xfs': ['xfsprogs'],
+            'zfsroot': [],
+            'zfs': [],
+            'zpool': [],
+        },
     }
     if osfamily not in distro_mapping:
         raise ValueError('No block package mapping for distro: %s' % osfamily)
diff --git a/curtin/commands/curthooks.py b/curtin/commands/curthooks.py
index 03a53b1..69833a2 100644
--- a/curtin/commands/curthooks.py
+++ b/curtin/commands/curthooks.py
@@ -1038,6 +1038,7 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
     DEFAULT_MULTIPATH_PACKAGES = {
         DISTROS.debian: ['multipath-tools-boot'],
         DISTROS.redhat: ['device-mapper-multipath'],
+        DISTROS.suse: ['multipath-tools'],
     }
     if osfamily not in DEFAULT_MULTIPATH_PACKAGES:
         raise ValueError(
@@ -1154,7 +1155,7 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
             grub_cfg = os.path.sep.join(
                 [target, '/etc/default/grub.d/50-curtin-multipath.cfg'])
             omode = 'w'
-        elif osfamily == DISTROS.redhat:
+        elif osfamily in [DISTROS.redhat, DISTROS.suse]:
             grub_cfg = os.path.sep.join([target, '/etc/default/grub'])
             omode = 'a'
         else:
@@ -1199,7 +1200,7 @@ def detect_and_handle_multipath(cfg, target, osfamily=DISTROS.debian):
         # Initrams needs to be updated to include /etc/multipath.cfg
         # and /etc/multipath/bindings files.
         update_initramfs(target, all_kernels=True)
-    elif osfamily == DISTROS.redhat:
+    elif osfamily in [DISTROS.redhat, DISTROS.suse]:
         # Write out initramfs/dracut config for multipath
         dracut_conf_multipath = os.path.sep.join(
             [target, '/etc/dracut.conf.d/10-curtin-multipath.conf'])
@@ -1308,6 +1309,12 @@ def install_missing_packages(cfg, target, osfamily=DISTROS.debian):
             # SecureBoot support
             if distro.has_pkg_available("shim-signed"):
                 uefi_pkgs.append("shim-signed")
+        elif osfamily == DISTROS.suse:
+            uefi_pkgs.extend(['grub2', 'grub2-branding-SLE'])
+            arch = distro.get_architecture()
+            if arch == 'amd64':
+                arch = 'x86_64'
+            uefi_pkgs.append('grub2-%s-efi' % arch)
         else:
             raise ValueError('Unknown grub2 package list for distro: %s' %
                              osfamily)
@@ -1753,6 +1760,14 @@ def builtin_curthooks(cfg, target, state):
             description="setting up swap"):
         add_swap(cfg, target, state.get('fstab'))
 
+    if osfamily == DISTROS.suse:
+        # set cloud-init maas datasource for SuSE images
+        if cfg.get('cloudconfig'):
+            handle_cloudconfig(
+                cfg['cloudconfig'],
+                base_dir=paths.target_path(target,
+                                           'etc/cloud/cloud.cfg.d'))
+
     if osfamily == DISTROS.redhat:
         # set cloud-init maas datasource for centos images
         if cfg.get('cloudconfig'):
diff --git a/curtin/commands/install_grub.py b/curtin/commands/install_grub.py
index 79b6695..38bf71a 100644
--- a/curtin/commands/install_grub.py
+++ b/curtin/commands/install_grub.py
@@ -250,7 +250,7 @@ def get_grub_install_command(uefi, distroinfo, target):
         # prefer grub-multi-install if present
         if uefi and os.path.exists(target_path(target, GRUB_MULTI_INSTALL)):
             grub_install_cmd = GRUB_MULTI_INSTALL
-    elif distroinfo.family == distro.DISTROS.redhat:
+    elif distroinfo.family in [distro.DISTROS.redhat, distro.DISTROS.suse]:
         grub_install_cmd = 'grub2-install'
 
     LOG.debug('Using grub install command: %s', grub_install_cmd)
@@ -289,6 +289,23 @@ def gen_uefi_install_commands(grub_name, grub_target, grub_cmd, update_nvram,
                               '/boot/efi/EFI/%s/grub.cfg' % bootid])
         else:
             post_cmds.append(['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'])
+    elif distroinfo.family == distro.DISTROS.suse:
+        bootid = 'suse'
+        grub_cfg = '/boot/grub2/grub.cfg'
+        loader = find_efi_loader(target, bootid)
+        if loader:
+            grub_cmd = None
+            grub_cfg = '/boot/efi/EFI/%s/grub.cfg' % bootid
+            if update_nvram:
+                efi_disk, efi_part_num = get_efi_disk_part(devices)
+                # Add entry to the EFI boot menu
+                install_cmds.append(['efibootmgr', '--create',
+                                     '--write-signature', '--label', bootid,
+                                     '--disk', efi_disk,
+                                     '--part', efi_part_num,
+                                     '--loader',
+                                     efi_loader_esp_path(loader)])
+        post_cmds.append(['grub2-mkconfig', '-o', grub_cfg])
     else:
         raise ValueError("Unsupported os family for grub "
                          "install: %s" % distroinfo.family)
@@ -315,6 +332,8 @@ def gen_install_commands(grub_name, grub_cmd, distroinfo, devices,
     if distroinfo.family == distro.DISTROS.debian:
         install_cmds.append(['dpkg-reconfigure', grub_name])
         install_cmds.append(['update-grub'])
+    elif distroinfo.family == distro.DISTROS.suse:
+        post_cmds.append(['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'])
     elif distroinfo.family == distro.DISTROS.redhat:
         if rhel_ver in ["7", "8", "9"]:
             post_cmds.append(
diff --git a/curtin/distro.py b/curtin/distro.py
index 618af47..4266278 100644
--- a/curtin/distro.py
+++ b/curtin/distro.py
@@ -379,6 +379,26 @@ def rpm_get_dist_id(target=None):
     return dist.rstrip()
 
 
+def run_zypper_command(mode, args=None, opts=None, env=None, target=None,
+                       execute=True, allow_daemons=False):
+    defopts = ['--non-interactive', '--non-interactive-include-reboot-patches',
+               '--quiet']
+
+    if args is None:
+        args = []
+
+    if opts is None:
+        opts = []
+
+    cmd = ['zypper']
+    cmd += defopts + opts + [mode] + args
+    if not execute:
+        return env, cmd
+
+    with ChrootableTarget(target, allow_daemons=allow_daemons) as inchroot:
+        return inchroot.subp(cmd, env=env)
+
+
 def system_upgrade(opts=None, target=None, env=None, allow_daemons=False,
                    osfamily=None):
     LOG.debug("Upgrading system in %s", target)
@@ -391,6 +411,8 @@ def system_upgrade(opts=None, target=None, env=None, allow_daemons=False,
                          'subcommands': ('dist-upgrade', 'autoremove')},
         DISTROS.redhat: {'function': run_yum_command,
                          'subcommands': ('upgrade',)},
+        DISTROS.suse: {'function': run_zypper_command,
+                       'subcommands': ('refresh', 'update', 'purge-kernels',)},
     }
     if osfamily not in distro_cfg:
         raise ValueError('Distro "%s" does not have system_upgrade support',
@@ -414,6 +436,7 @@ def install_packages(pkglist, osfamily=None, opts=None, target=None, env=None,
     installer_map = {
         DISTROS.debian: run_apt_command,
         DISTROS.redhat: run_yum_command,
+        DISTROS.suse: run_zypper_command,
     }
 
     install_cmd = installer_map.get(osfamily)
@@ -429,10 +452,15 @@ def has_pkg_available(pkg, target=None, osfamily=None):
     if not osfamily:
         osfamily = get_osfamily(target=target)
 
-    if osfamily not in [DISTROS.debian, DISTROS.redhat]:
+    if osfamily not in [DISTROS.debian, DISTROS.redhat, DISTROS.suse]:
         raise ValueError('has_pkg_available: unsupported distro family: %s',
                          osfamily)
 
+    if osfamily == DISTROS.suse:
+        out, _ = subp(['zypper', '--quiet', 'search',
+                       '--match-exact', pkg], capture=True, target=target)
+        return 'No matching items found.' not in out
+
     if osfamily == DISTROS.debian:
         out, _ = subp(['apt-cache', 'pkgnames'], capture=True, target=target)
         for item in out.splitlines():
@@ -603,7 +631,7 @@ def get_architecture(target=None, osfamily=None):
     if osfamily == DISTROS.debian:
         return dpkg_get_architecture(target=target)
 
-    if osfamily == DISTROS.redhat:
+    if osfamily in [DISTROS.redhat, DISTROS.suse]:
         return rpm_get_architecture(target=target)
 
     raise ValueError("Unhandled osfamily=%s" % osfamily)
diff --git a/curtin/net/deps.py b/curtin/net/deps.py
index b78654d..e019a9e 100644
--- a/curtin/net/deps.py
+++ b/curtin/net/deps.py
@@ -75,6 +75,14 @@ def detect_required_packages_mapping(osfamily=DISTROS.debian):
             'openvswitch': ['openvswitch-switch'],
             'vlan': [],
             'vlans': []},
+        DISTROS.suse: {
+            'bond': [],
+            'bonds': [],
+            'bridge': ['bridge-utils'],
+            'bridges': ['bridge-utils'],
+            'openvswitch': ['openvswitch-switch'],
+            'vlan': ['vlan'],
+            'vlans': ['vlan']},
     }
     if osfamily not in distro_mapping:
         raise ValueError('No net package mapping for distro: %s' % osfamily)
diff --git a/tests/unittests/test_commands_install_grub.py b/tests/unittests/test_commands_install_grub.py
index 24f9476..ab52505 100644
--- a/tests/unittests/test_commands_install_grub.py
+++ b/tests/unittests/test_commands_install_grub.py
@@ -830,6 +830,121 @@ class TestGenUefiInstallCommands(CiTestCase):
                 grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
                 devices, self.target))
 
+    def test_suse_install(self):
+        self.m_os_release.return_value = {'ID': 'suse'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        grub_name = 'grub2-efi-x64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub2-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_install = [
+            ['efibootmgr', '-v'],
+            [grub_cmd, '--target=%s' % grub_target,
+             '--efi-directory=/boot/efi',
+             '--bootloader-id=suse', '--recheck'],
+        ]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/grub2/grub.cfg'],
+            ['efibootmgr', '-v']
+        ]
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
+    def test_suse_install_existing(self):
+        # simulate existing bootloaders already installed in target system
+        # by touching the files grub would have installed, including shim
+        def _enable_loaders(bootid):
+            efi_path = 'boot/efi/EFI'
+            target_efi_path = os.path.join(self.target, efi_path)
+            loaders = [
+                os.path.join(target_efi_path, bootid, 'shimx64.efi'),
+                os.path.join(target_efi_path, 'BOOT', 'BOOTX64.EFI'),
+                os.path.join(target_efi_path, bootid, 'grubx64.efi'),
+            ]
+            for loader in loaders:
+                util.ensure_dir(os.path.dirname(loader))
+                with open(loader, 'w+') as fh:
+                    fh.write('\n')
+
+        self.m_os_release.return_value = {'ID': 'suse'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        _enable_loaders("suse")
+        grub_name = 'grub2-efi-x64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub2-install'
+        update_nvram = True
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_loader = '/EFI/suse/shimx64.efi'
+        expected_install = [
+            ['efibootmgr', '-v'],
+            ['efibootmgr', '--create', '--write-signature',
+             '--label', 'suse', '--disk', disk, '--part', part,
+             '--loader', expected_loader],
+        ]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/efi/EFI/suse/grub.cfg'],
+            ['efibootmgr', '-v']
+        ]
+
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
+    def test_suse_install_existing_no_nvram(self):
+        # verify grub install command is not executed if update_nvram is False
+        # on suse.
+        def _enable_loaders(bootid):
+            efi_path = 'boot/efi/EFI'
+            target_efi_path = os.path.join(self.target, efi_path)
+            loaders = [
+                os.path.join(target_efi_path, bootid, 'shimx64.efi'),
+                os.path.join(target_efi_path, 'BOOT', 'BOOTX64.EFI'),
+                os.path.join(target_efi_path, bootid, 'grubx64.efi'),
+            ]
+            for loader in loaders:
+                util.write_file(loader, content="")
+
+        self.m_os_release.return_value = {'ID': 'suse'}
+        distroinfo = install_grub.distro.get_distroinfo()
+        bootid = distroinfo.variant
+        _enable_loaders(bootid)
+        grub_name = 'grub2-efi-x64'
+        grub_target = 'x86_64-efi'
+        grub_cmd = 'grub2-install'
+        update_nvram = False
+        devices = ['/dev/disk-a-part1']
+        disk = '/dev/disk-a'
+        part = '1'
+        self.m_get_disk_part.return_value = (disk, part)
+
+        expected_install = [
+            ['efibootmgr', '-v'],
+        ]
+        expected_post = [
+            ['grub2-mkconfig', '-o', '/boot/efi/EFI/%s/grub.cfg' % bootid],
+            ['efibootmgr', '-v']
+        ]
+
+        self.assertEqual(
+            (expected_install, expected_post),
+            install_grub.gen_uefi_install_commands(
+                grub_name, grub_target, grub_cmd, update_nvram, distroinfo,
+                devices, self.target))
+
 
 class TestGenInstallCommands(CiTestCase):
 
diff --git a/tests/unittests/test_curthooks.py b/tests/unittests/test_curthooks.py
index a224819..c13434c 100644
--- a/tests/unittests/test_curthooks.py
+++ b/tests/unittests/test_curthooks.py
@@ -479,6 +479,22 @@ class TestInstallMissingPkgs(CiTestCase):
                 expected_pkgs, target=target, osfamily=self.distro_family)
 
     @patch.object(events, 'ReportEventStack')
+    def test_install_packages_on_uefi_amd64_sles(self, mock_events):
+        arch = 'amd64'
+        self.mock_arch.return_value = arch
+        self.mock_machine.return_value = 'x86_64'
+        expected_pkgs = ['efibootmgr', 'grub2', 'grub2-branding-SLE',
+                         'grub2-x86_64-efi']
+        self.mock_uefi.return_value = True
+        self.mock_haspkg.return_value = True
+        target = "not-a-real-target"
+        cfg = {}
+        curthooks.install_missing_packages(
+            cfg, target=target, osfamily=distro.DISTROS.suse)
+        self.mock_install_packages.assert_called_with(
+                expected_pkgs, target=target, osfamily=distro.DISTROS.suse)
+
+    @patch.object(events, 'ReportEventStack')
     def test_install_packages_on_uefi_amd64_centos(self, mock_events):
         arch = 'amd64'
         self.mock_arch.return_value = arch
diff --git a/tests/unittests/test_distro.py b/tests/unittests/test_distro.py
index 7532126..06c25db 100644
--- a/tests/unittests/test_distro.py
+++ b/tests/unittests/test_distro.py
@@ -370,6 +370,34 @@ class TestYumInstall(CiTestCase):
         m_subp.assert_has_calls(expected_calls)
 
 
+class TestZypperInstall(CiTestCase):
+
+    @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    @mock.patch('curtin.util.subp')
+    def test_zypper_install(self, m_subp):
+        pkglist = ['foobar', 'wark']
+        target = 'mytarget'
+        expected_calls = [
+            mock.call(['zypper', '--non-interactive',
+                       '--non-interactive-include-reboot-patches',
+                       '--quiet', 'install'] + pkglist, env=None,
+                      target=paths.target_path(target))
+        ]
+
+        m_subp.reset_mock()
+        self.assertFalse(m_subp.called)
+        distro.run_zypper_command('install', pkglist, target=target)
+        m_subp.assert_has_calls(expected_calls)
+
+        # call yum_install through install_packages; expect the same calls
+        # so clear m_subp's call stack.
+        m_subp.reset_mock()
+        self.assertFalse(m_subp.called)
+        osfamily = distro.DISTROS.suse
+        distro.install_packages(pkglist, osfamily=osfamily, target=target)
+        m_subp.assert_has_calls(expected_calls)
+
+
 class TestSystemUpgrade(CiTestCase):
 
     @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
@@ -469,6 +497,35 @@ class TestSystemUpgrade(CiTestCase):
         m_apt_update.assert_has_calls(apt_update_calls)
         m_subp.assert_has_calls(expected_calls)
 
+    @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    @mock.patch('curtin.util.subp')
+    def test_system_upgrade_suse(self, m_subp):
+        """system_upgrade osfamily=suse
+        calls run_zypper_command mode=upgrade"""
+        osfamily = distro.DISTROS.suse
+        target = 'mytarget'
+        expected_calls = [
+            mock.call(['zypper', '--non-interactive',
+                       '--non-interactive-include-reboot-patches', '--quiet',
+                       'refresh'], env=None,
+                      target='/work/curtin/mytarget'),
+            mock.call(['zypper', '--non-interactive',
+                       '--non-interactive-include-reboot-patches', '--quiet',
+                       'update'], env=None,
+                      target='/work/curtin/mytarget'),
+            mock.call(['zypper', '--non-interactive',
+                       '--non-interactive-include-reboot-patches', '--quiet',
+                       'purge-kernels'], env=None,
+                      target='/work/curtin/mytarget'),
+        ]
+
+        # call system_upgrade via osfamily; note that we expect the same calls
+        # but to prevent a false positive we clear m_subp's call stack.
+        m_subp.reset_mock()
+        self.assertFalse(m_subp.called)
+        distro.system_upgrade(target=target, osfamily=osfamily)
+        m_subp.assert_has_calls(expected_calls)
+
 
 class TestHasPkgAvailable(CiTestCase):
 
@@ -519,6 +576,18 @@ class TestHasPkgAvailable(CiTestCase):
         self.assertEqual(pkg == self.package, result)
         m_subp.assert_has_calls([mock.call('list', opts=['--cacheonly'])])
 
+    @mock.patch.object(util.ChrootableTarget, "__enter__", new=lambda a: a)
+    @mock.patch('curtin.distro.subp')
+    def test_has_pkg_available_suse_returns_false_not_avail(self, m_subp):
+        osfamily = distro.DISTROS.suse
+        m_subp.return_value = ('No matching items found.', '')
+        result = distro.has_pkg_available(self.package, self.target, osfamily)
+        self.assertEqual(False, result)
+        m_subp.assert_has_calls([mock.call(['zypper', '--quiet', 'search',
+                                            '--match-exact', self.package],
+                                capture=True,
+                                target=self.target)])
+
 
 class TestGetArchitecture(CiTestCase):
 
@@ -563,4 +632,13 @@ class TestGetArchitecture(CiTestCase):
                          self.m_rpm_get_arch.call_args_list)
         self.assertEqual(0, self.m_dpkg_get_arch.call_count)
 
+    def test_suse_osfamily_calls_rpm_get_arch(self):
+        osfamily = distro.DISTROS.suse
+        expected_result = self.m_rpm_get_arch.return_value
+        result = distro.get_architecture(target=self.target, osfamily=osfamily)
+        self.assertEqual(expected_result, result)
+        self.assertEqual([mock.call(target=self.target)],
+                         self.m_rpm_get_arch.call_args_list)
+        self.assertEqual(0, self.m_dpkg_get_arch.call_count)
+
 # vi: ts=4 expandtab syntax=python

Follow ups