Repository: easybuilders/easybuild-framework

Issue:
Modifications made to easybuild/tools/systemtools.py between 4.5.1 and 4.5.2 break EasyBuild on SLES 15-SP1.
EasyBuild 4.5.2 fails on SLES 15-SP1. It fails to locate a Python command that can import easybuild.main. When I try to import easybuild.main by hand, it produces the error messages:
```
>>> import easybuild.main
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/main.py", line 48, in <module>
    from easybuild.framework.easyblock import build_and_install_one, inject_checksums
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/framework/easyblock.py", line 57, in <module>
    from easybuild.framework.easyconfig import EASYCONFIGS_PKG_SUBDIR
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/framework/easyconfig/__init__.py", line 38, in <module>
    from easybuild.framework.easyconfig.easyconfig import EasyConfig  # noqa
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/framework/easyconfig/easyconfig.py", line 53, in <module>
    from easybuild.framework.easyconfig.constants import EXTERNAL_MODULE_MARKER
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/framework/easyconfig/constants.py", line 52, in <module>
    'OS_VERSION': (get_os_version(), "System version"),
  File "/users/kurtlust/Work/easybuild/lib/python3.6/site-packages/easybuild/tools/systemtools.py", line 782, in get_os_version
    raise EasyBuildError("Don't know how to determine subversions for SLES %s", os_version)
easybuild.tools.build_log.EasyBuildError: "Don't know how to determine subversions for SLES 15-SP1"
```

I suspect changes to the OS detection code that have been made recently change the form of the version string for SLES, breaking the block of code between line 748 and 782 in some way? 

To help in debugging:
* platform.dist returns ('', '', '')
* distro does not exist on our system
* `read_file(ETC_OS_RELEASE)` returns `'NAME="SLES"\nVERSION="15-SP1"\nVERSION_ID="15.1"\nPRETTY_NAME="SUSE Linux Enterprise Server 15 SP1"\nID="sles"\nID_LIKE="suse"\nANSI_COLOR="0;32"\nCPE_NAME="cpe:/o:suse:sles:15:sp1"\n'`. So the code further on sets os_version to `15-SP1`
* get_os_name() is SLES so the block of code starting on line 750 is executed but it does not contain anything for SLES 15, and moreover it is not needed to check the kernel versions to determine the OS version.

In EasyBuild 4.4.2 (which I have used extensively on our system) and EasyBuild 4.5.1 (not really used yet, just installed to determine when EasyBuild got broken on SLES 15) both get_os_name and get_os_version in that file return UNKNOWN. So obviously the code was somehow broken but not in a way that affected the use of EasyBuild (or at least I did not notice it).


Discussion:
@klust Thanks for reporting this issue!

I guess you're hitting this on LUMI?

I just noticed that the same issue pops up on Piz Daint as well (cc @victorusu @lucamar), so we should fix this ASAP and push out a bugfix release a bit more quickly than usual...
Thanks for reporting the issue: I have tried quickly last week to update EasyBuild to the latest version and I also got an error (likely similar as reported by @klust) but I did not have time to investigate it further. Therefore we are still using 4.5.1 at present.
We'll work on reproducing this in our test suite, fixing it, and pushing out a bugfix release ASAP (hopefully still this week).

Resolving patch:
diff --git a/easybuild/tools/systemtools.py b/easybuild/tools/systemtools.py
--- a/easybuild/tools/systemtools.py
+++ b/easybuild/tools/systemtools.py
@@ -742,40 +742,31 @@ def get_os_version():
                 os_version = res.group('version')
 
     if os_version:
-        if get_os_name() in ["suse", "SLES"]:
-
-            # SLES subversions can only be told apart based on kernel version,
-            # see http://wiki.novell.com/index.php/Kernel_versions
-            version_suffixes = {
-                '11': [
-                    ('2.6.27', ''),
-                    ('2.6.32', '_SP1'),
-                    ('3.0.101-63', '_SP4'),
-                    # not 100% correct, since early SP3 had 3.0.76 - 3.0.93, but close enough?
-                    ('3.0.101', '_SP3'),
-                    # SP2 kernel versions range from 3.0.13 - 3.0.101
-                    ('3.0', '_SP2'),
-                ],
-
-                '12': [
-                    ('3.12.28', ''),
-                    ('3.12.49', '_SP1'),
-                ],
-            }
-
+        # older SLES subversions can only be told apart based on kernel version,
+        # see http://wiki.novell.com/index.php/Kernel_versions
+        sles_version_suffixes = {
+            '11': [
+                ('2.6.27', ''),
+                ('2.6.32', '_SP1'),
+                ('3.0.101-63', '_SP4'),
+                # not 100% correct, since early SP3 had 3.0.76 - 3.0.93, but close enough?
+                ('3.0.101', '_SP3'),
+                # SP2 kernel versions range from 3.0.13 - 3.0.101
+                ('3.0', '_SP2'),
+            ],
+
+            '12': [
+                ('3.12.28', ''),
+                ('3.12.49', '_SP1'),
+            ],
+        }
+        if get_os_name() in ['suse', 'SLES'] and os_version in sles_version_suffixes:
             # append suitable suffix to system version
-            if os_version in version_suffixes.keys():
-                kernel_version = platform.uname()[2]
-                known_sp = False
-                for (kver, suff) in version_suffixes[os_version]:
-                    if kernel_version.startswith(kver):
-                        os_version += suff
-                        known_sp = True
-                        break
-                if not known_sp:
-                    suff = '_UNKNOWN_SP'
-            else:
-                raise EasyBuildError("Don't know how to determine subversions for SLES %s", os_version)
+            kernel_version = platform.uname()[2]
+            for (kver, suff) in sles_version_suffixes[os_version]:
+                if kernel_version.startswith(kver):
+                    os_version += suff
+                    break
 
         return os_version
     else:


Test patch:
diff --git a/test/framework/systemtools.py b/test/framework/systemtools.py
--- a/test/framework/systemtools.py
+++ b/test/framework/systemtools.py
@@ -366,10 +366,13 @@ def setUp(self):
         self.orig_is_readable = st.is_readable
         self.orig_read_file = st.read_file
         self.orig_run_cmd = st.run_cmd
+        self.orig_platform_dist = st.platform.dist if hasattr(st.platform, 'dist') else None
         self.orig_platform_uname = st.platform.uname
         self.orig_get_tool_version = st.get_tool_version
         self.orig_sys_version_info = st.sys.version_info
         self.orig_HAVE_ARCHSPEC = st.HAVE_ARCHSPEC
+        self.orig_HAVE_DISTRO = st.HAVE_DISTRO
+        self.orig_ETC_OS_RELEASE = st.ETC_OS_RELEASE
         if hasattr(st, 'archspec_cpu_host'):
             self.orig_archspec_cpu_host = st.archspec_cpu_host
         else:
@@ -383,10 +386,14 @@ def tearDown(self):
         st.get_os_name = self.orig_get_os_name
         st.get_os_type = self.orig_get_os_type
         st.run_cmd = self.orig_run_cmd
+        if self.orig_platform_dist is not None:
+            st.platform.dist = self.orig_platform_dist
         st.platform.uname = self.orig_platform_uname
         st.get_tool_version = self.orig_get_tool_version
         st.sys.version_info = self.orig_sys_version_info
         st.HAVE_ARCHSPEC = self.orig_HAVE_ARCHSPEC
+        st.HAVE_DISTRO = self.orig_HAVE_DISTRO
+        st.ETC_OS_RELEASE = self.orig_ETC_OS_RELEASE
         if self.orig_archspec_cpu_host is not None:
             st.archspec_cpu_host = self.orig_archspec_cpu_host
         super(SystemToolsTest, self).tearDown()
@@ -738,6 +745,21 @@ def test_os_version(self):
         os_version = get_os_version()
         self.assertTrue(isinstance(os_version, string_type) or os_version == UNKNOWN)
 
+        # make sure that bug fixed in https://github.com/easybuilders/easybuild-framework/issues/3952
+        # does not surface again, by mocking what's needed to make get_os_version fall into SLES-specific path
+
+        if hasattr(st.platform, 'dist'):
+            st.platform.dist = lambda: (None, None)
+        st.HAVE_DISTRO = False
+
+        st.get_os_name = lambda: 'SLES'
+        fake_etc_os_release = os.path.join(self.test_prefix, 'os-release')
+        write_file(fake_etc_os_release, 'VERSION="15-SP1"')
+        st.ETC_OS_RELEASE = fake_etc_os_release
+
+        os_version = get_os_version()
+        self.assertEqual(os_version, '15-SP1')
+
     def test_gcc_version_native(self):
         """Test getting gcc version."""
         gcc_version = get_gcc_version()