URL: https://github.com/freeipa/freeipa/pull/252
Author: tiran
 Title: #252: Use namespace-aware meta importer for ipaplatform
Action: opened

PR body:
"""
Instead of symlinks and build-time configuration the ipaplatform module
is now able to auto-detect platforms on import time. The meta importer
uses the platform 'ID' from /etc/os-releases. It falls back to 'ID_LIKE'
on platforms like CentOS, which has ID=centos and ID_LIKE="rhel fedora".

The meta importer is able to handle namespace packages and the
ipaplatform package has been turned into a namespace package in order to
support external platform specifications.

https://fedorahosted.org/freeipa/ticket/6474

Signed-off-by: Christian Heimes <chei...@redhat.com>
"""

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/252/head:pr252
git checkout pr252
From dba131b9346d085f0e99da170948d0140fb5d7be Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Fri, 28 Oct 2016 09:23:48 +0200
Subject: [PATCH] Use namespace-aware meta importer for ipaplatform

Instead of symlinks and build-time configuration the ipaplatform module
is now able to auto-detect platforms on import time. The meta importer
uses the platform 'ID' from /etc/os-releases. It falls back to 'ID_LIKE'
on platforms like CentOS, which has ID=centos and ID_LIKE="rhel fedora".

The meta importer is able to handle namespace packages and the
ipaplatform package has been turned into a namespace package in order to
support external platform specifications.

https://fedorahosted.org/freeipa/ticket/6474

Signed-off-by: Christian Heimes <chei...@redhat.com>
---
 .gitignore                    |  5 ---
 configure.ac                  | 38 -----------------
 ignore_import_errors.py       |  7 +++-
 ipaplatform/__init__.py       | 24 +++++++++++
 ipaplatform/_importhook.py    | 96 +++++++++++++++++++++++++++++++++++++++++++
 ipaplatform/base/constants.py |  2 +
 ipaplatform/base/paths.py     |  2 +-
 ipaplatform/base/services.py  |  8 +++-
 ipaplatform/base/tasks.py     |  2 +
 ipaplatform/constants.py      | 10 +++++
 ipaplatform/paths.py          | 10 +++++
 ipaplatform/services.py       | 11 +++++
 ipaplatform/setup.py          |  1 +
 ipaplatform/tasks.py          | 10 +++++
 14 files changed, 179 insertions(+), 47 deletions(-)
 create mode 100644 ipaplatform/__init__.py
 create mode 100644 ipaplatform/_importhook.py
 create mode 100644 ipaplatform/constants.py
 create mode 100644 ipaplatform/paths.py
 create mode 100644 ipaplatform/services.py
 create mode 100644 ipaplatform/tasks.py

diff --git a/.gitignore b/.gitignore
index 2bacc85..428049b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -83,8 +83,3 @@ freeipa2-dev-doc
 
 /ipapython/version.py
 
-/ipaplatform/__init__.py
-/ipaplatform/constants.py
-/ipaplatform/paths.py
-/ipaplatform/services.py
-/ipaplatform/tasks.py
diff --git a/configure.ac b/configure.ac
index b042455..a121956 100644
--- a/configure.ac
+++ b/configure.ac
@@ -319,35 +319,6 @@ if test "x$MSGATTRIB" = "xno"; then
 fi
 
 dnl ---------------------------------------------------------------------------
-dnl IPA platform
-dnl ---------------------------------------------------------------------------
-AC_ARG_WITH([ipaplatform],
-	    [AC_HELP_STRING([--with-ipaplatform],
-			    [IPA platform module to use])],
-	    [IPAPLATFORM=${withval}],
-	    [IPAPLATFORM=""])
-AC_MSG_CHECKING([supported IPA platform])
-
-if test "x${IPAPLATFORM}" == "x"; then
-	if test -r "/etc/os-release"; then
-		IPAPLATFORM=$(. /etc/os-release; echo "$ID")
-	else
-		AC_MSG_ERROR([unable to read /etc/os-release])
-	fi
-	if test "x${IPAPLATFORM}" == "x"; then
-		AC_MSG_ERROR([unable to find ID variable in /etc/os-release])
-	fi
-fi
-
-if test ! -d "${srcdir}/ipaplatform/${IPAPLATFORM}"; then
-	AC_MSG_ERROR([IPA platform ${IPAPLATFORM} is not supported])
-fi
-
-AC_SUBST([IPAPLATFORM])
-AC_MSG_RESULT([${IPAPLATFORM}])
-
-
-dnl ---------------------------------------------------------------------------
 dnl Version information from VERSION.m4 and command line
 dnl ---------------------------------------------------------------------------
 dnl Are we in source tree?
@@ -468,15 +439,6 @@ AC_SUBST(CFLAGS)
 AC_SUBST(CPPFLAGS)
 AC_SUBST(LDFLAGS)
 
-
-# Files
-AC_CONFIG_LINKS([ipaplatform/__init__.py:ipaplatform/$IPAPLATFORM/__init__.py
-                 ipaplatform/constants.py:ipaplatform/$IPAPLATFORM/constants.py
-                 ipaplatform/paths.py:ipaplatform/$IPAPLATFORM/paths.py
-                 ipaplatform/services.py:ipaplatform/$IPAPLATFORM/services.py
-                 ipaplatform/tasks.py:ipaplatform/$IPAPLATFORM/tasks.py
-		])
-
 AC_CONFIG_FILES([
     Makefile
     asn1/Makefile
diff --git a/ignore_import_errors.py b/ignore_import_errors.py
index 4ee6ee9..7f3ee50 100644
--- a/ignore_import_errors.py
+++ b/ignore_import_errors.py
@@ -6,13 +6,18 @@
 ImportError ignoring import hook.
 """
 
-from __future__ import print_function
+from __future__ import absolute_import, print_function
 
 import imp
 import inspect
 import os.path
 import sys
 
+# Load ipaplatform's meta importer before IgnoreImporter is registered as
+# meta importer.
+import ipaplatform.paths  # pylint: disable=unused-import
+
+
 DIRNAME = os.path.dirname(os.path.abspath(__file__))
 
 
diff --git a/ipaplatform/__init__.py b/ipaplatform/__init__.py
new file mode 100644
index 0000000..4ce1057
--- /dev/null
+++ b/ipaplatform/__init__.py
@@ -0,0 +1,24 @@
+# Copyright (C) 2016  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+"""ipaplatform namespace package
+
+In the presence of a namespace package, any code in this module will be
+ignore.
+"""
+__import__('pkg_resources').declare_namespace(__name__)
+
+NAME = None  # initialized by IpaMetaImporter
diff --git a/ipaplatform/_importhook.py b/ipaplatform/_importhook.py
new file mode 100644
index 0000000..29d5fe2
--- /dev/null
+++ b/ipaplatform/_importhook.py
@@ -0,0 +1,96 @@
+# Copyright (C) 2016  Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+#
+
+import importlib
+import sys
+
+import ipaplatform
+
+
+class IpaMetaImporter(object):
+    """Meta import hook and platform detector.
+
+    The meta import hook uses /etc/os-release to auto-detects the best
+    matching ipaplatform provider. It is compatible with external namespace
+    packages, too.
+    """
+    modules = {
+        'ipaplatform.constants',
+        'ipaplatform.paths',
+        'ipaplatform.services',
+        'ipaplatform.tasks'
+    }
+
+    def __init__(self):
+        self.platform_ids = self._read_osrelease()
+        self.platform = self._get_platform()
+        # fix modules that have been loaded
+        for module in self.modules:
+            if module in sys.modules:
+                self.load_module(module)
+
+    def _read_osrelease(self, filename='/etc/os-release'):
+        platforms = []
+        with open(filename) as f:
+            for line in f:
+                key, value = line.rstrip('\n').split('=', 1)
+                if value.startswith(('"', "'")):
+                    value = value[1:-1]
+                if key == 'ID':
+                    platforms.insert(0, value)
+                # fallback to base distro, centos has ID_LIKE="rhel fedora"
+                if key == 'ID_LIKE':
+                    platforms.extend(value.split(' '))
+        return platforms
+
+    def _get_platform(self):
+        for platform in self.platform_ids:
+            try:
+                importlib.import_module('ipaplatform.{}'.format(platform))
+            except ImportError:
+                pass
+            else:
+                return platform
+        raise ImportError('No ipaplatform available for "{}"'.format(
+                          ', '.join(self.platform_ids)))
+
+    def find_module(self, fullname, path=None):
+        """Meta importer hook"""
+        if fullname in self.modules:
+            return self
+        return None
+
+    def load_module(self, fullname):
+        """Meta importer hook"""
+        suffix = fullname.split('.', 1)[1]
+        alias = 'ipaplatform.{}.{}'.format(self.platform, suffix)
+        platform_mod = importlib.import_module(alias)
+        base_mod = sys.modules.get(fullname)
+        if base_mod is not None:
+            # module has been imported before, update its __dict__
+            base_mod.__dict__.clear()
+            base_mod.__dict__.update(platform_mod.__dict__)
+        else:
+            sys.modules[fullname] = platform_mod
+        return platform_mod
+
+
+metaimporter = IpaMetaImporter()
+sys.meta_path.insert(0, metaimporter)
+
+if ipaplatform.NAME is None:
+    ipaplatform.NAME = metaimporter.platform
diff --git a/ipaplatform/base/constants.py b/ipaplatform/base/constants.py
index 3e1c4c6..44704c4 100644
--- a/ipaplatform/base/constants.py
+++ b/ipaplatform/base/constants.py
@@ -26,3 +26,5 @@ class BaseConstantsNamespace(object):
     # nfsd init variable used to enable kerberized NFS
     SECURE_NFS_VAR = "SECURE_NFS"
     SSSD_USER = "sssd"
+
+constants = BaseConstantsNamespace()
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index bbf6b53..bf924f8 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -368,4 +368,4 @@ def USER_CACHE_PATH(self):
             )
         )
 
-path_namespace = BasePathNamespace
+paths = BasePathNamespace()
diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py
index 750d979..8d51da3 100644
--- a/ipaplatform/base/services.py
+++ b/ipaplatform/base/services.py
@@ -483,8 +483,12 @@ def remove(self):
 
 # Objects below are expected to be exported by platform module
 
-service = None
-knownservices = None
+def base_service_class_factory(name):
+    raise NotImplementedError
+
+
+service = base_service_class_factory
+knownservices = KnownServices({})
 
 # System may support more time&date services. FreeIPA supports ntpd only, other
 # services will be disabled during IPA installation
diff --git a/ipaplatform/base/tasks.py b/ipaplatform/base/tasks.py
index 1e687b6..533834b 100644
--- a/ipaplatform/base/tasks.py
+++ b/ipaplatform/base/tasks.py
@@ -249,3 +249,5 @@ def configure_httpd_service_ipa_conf(self):
     def remove_httpd_service_ipa_conf(self):
         """Remove configuration of httpd service of IPA"""
         raise NotImplementedError()
+
+tasks = BaseTaskNamespace()
diff --git a/ipaplatform/constants.py b/ipaplatform/constants.py
new file mode 100644
index 0000000..42a940d
--- /dev/null
+++ b/ipaplatform/constants.py
@@ -0,0 +1,10 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.constants.
+"""
+# flake8: noqa
+# pylint: disable=unused-import
+
+from .base.constants import constants
+from . import _importhook
diff --git a/ipaplatform/paths.py b/ipaplatform/paths.py
new file mode 100644
index 0000000..90da40a
--- /dev/null
+++ b/ipaplatform/paths.py
@@ -0,0 +1,10 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+"""This module will be shadowed by IpaMetaImporter
+"""
+# flake8: noqa
+# pylint: disable=unused-import
+
+from .base.paths import paths
+from . import _importhook
diff --git a/ipaplatform/services.py b/ipaplatform/services.py
new file mode 100644
index 0000000..9e27db2
--- /dev/null
+++ b/ipaplatform/services.py
@@ -0,0 +1,11 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.services.
+"""
+# flake8: noqa
+# pylint: disable=unused-import
+
+from .base.services import wellknownservices, wellknownports
+from .base.services import service, knownservices, timedate_services
+from . import _importhook
diff --git a/ipaplatform/setup.py b/ipaplatform/setup.py
index 97311de..402320d 100644
--- a/ipaplatform/setup.py
+++ b/ipaplatform/setup.py
@@ -32,6 +32,7 @@
         name="ipaplatform",
         doc=__doc__,
         package_dir={'ipaplatform': ''},
+        namespace_packages=['ipaplatform'],
         packages=[
             "ipaplatform",
             "ipaplatform.base",
diff --git a/ipaplatform/tasks.py b/ipaplatform/tasks.py
new file mode 100644
index 0000000..3283a77
--- /dev/null
+++ b/ipaplatform/tasks.py
@@ -0,0 +1,10 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.tasks.
+"""
+# flake8: noqa
+# pylint: disable=unused-import
+
+from .base.tasks import tasks
+from . import _importhook
-- 
Manage your subscription for the Freeipa-devel mailing list:
https://www.redhat.com/mailman/listinfo/freeipa-devel
Contribute to FreeIPA: http://www.freeipa.org/page/Contribute/Code

Reply via email to