URL: https://github.com/freeipa/freeipa/pull/1148
Author: tiran
 Title: #1148: 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/1148/head:pr1148
git checkout pr1148
From 595b134c8938e13d2e8f5fcf5905dfab6dd9b45d Mon Sep 17 00:00:00 2001
From: Christian Heimes <chei...@redhat.com>
Date: Wed, 11 Oct 2017 12:09:30 +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                                   |   6 +-
 Makefile.am                                  |   6 +-
 configure.ac                                 |   8 --
 freeipa.spec.in                              |   1 +
 ignore_import_errors.py                      |   7 +-
 ipalib/config.py                             |  11 +--
 ipalib/setup.py                              |   1 +
 ipalib/util.py                               |   8 +-
 ipaplatform/Makefile.am                      |  11 +++
 ipaplatform/__init__.py                      |  24 +++++
 ipaplatform/_importhook.py                   | 125 +++++++++++++++++++++++++++
 ipaplatform/base/constants.py                |   2 +
 ipaplatform/base/paths.py                    |   3 +-
 ipaplatform/base/services.py                 |   8 +-
 ipaplatform/base/tasks.py                    |   5 ++
 ipaplatform/constants.py                     |   8 ++
 ipaplatform/fallback.py.in                   |   1 +
 ipaplatform/paths.py                         |   8 ++
 ipaplatform/services.py                      |   8 ++
 ipaplatform/setup.py                         |   1 +
 ipaplatform/tasks.py                         |   8 ++
 ipapython/certdb.py                          |  28 ++----
 ipapython/config.py                          |  13 +--
 ipapython/ipautil.py                         |   2 +-
 ipapython/setup.py                           |   1 +
 ipasetup.py.in                               |  13 ++-
 ipatests/test_ipaplatform/__init__.py        |   0
 ipatests/test_ipaplatform/test_importhook.py |  36 ++++++++
 pylint_plugins.py                            |  42 +++++++++
 pylintrc                                     |   6 +-
 pypi/Makefile.am                             |   1 -
 pypi/ipaplatform/Makefile.am                 |   3 -
 pypi/ipaplatform/README.txt                  |   2 -
 pypi/ipaplatform/ipaplatform/__init__.py     |   5 --
 pypi/ipaplatform/setup.cfg                   |   6 --
 pypi/ipaplatform/setup.py                    |  26 ------
 pypi/test_placeholder.py                     |   4 +-
 37 files changed, 334 insertions(+), 114 deletions(-)
 create mode 100644 ipaplatform/__init__.py
 create mode 100644 ipaplatform/_importhook.py
 create mode 100644 ipaplatform/constants.py
 create mode 100644 ipaplatform/fallback.py.in
 create mode 100644 ipaplatform/paths.py
 create mode 100644 ipaplatform/services.py
 create mode 100644 ipaplatform/tasks.py
 create mode 100644 ipatests/test_ipaplatform/__init__.py
 create mode 100644 ipatests/test_ipaplatform/test_importhook.py
 delete mode 100644 pypi/ipaplatform/Makefile.am
 delete mode 100644 pypi/ipaplatform/README.txt
 delete mode 100644 pypi/ipaplatform/ipaplatform/__init__.py
 delete mode 100644 pypi/ipaplatform/setup.cfg
 delete mode 100755 pypi/ipaplatform/setup.py

diff --git a/.gitignore b/.gitignore
index 8f4c2aa7a9..2f90ab0069 100644
--- a/.gitignore
+++ b/.gitignore
@@ -108,11 +108,7 @@ freeipa2-dev-doc
 /client/ipa-join
 /client/ipa-rmkeytab
 
+/ipaplatform/fallback.py
 /ipapython/version.py
 /ipapython/.DEFAULT_PLUGINS
 
-/ipaplatform/__init__.py
-/ipaplatform/constants.py
-/ipaplatform/paths.py
-/ipaplatform/services.py
-/ipaplatform/tasks.py
diff --git a/Makefile.am b/Makefile.am
index 02e53f550c..2e37c5b9b6 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -8,10 +8,10 @@ if WITH_IPATESTS
     IPATESTS_SUBDIRS = ipatests
 endif
 
-IPACLIENT_SUBDIRS = ipaclient ipalib ipapython
-IPA_PLACEHOLDERS = freeipa ipa ipaplatform ipaserver ipatests
+IPACLIENT_SUBDIRS = ipaclient ipalib ipaplatform ipapython
+IPA_PLACEHOLDERS = freeipa ipa ipaserver ipatests
 SUBDIRS = asn1 util client contrib po pypi \
-	$(IPACLIENT_SUBDIRS) ipaplatform $(IPATESTS_SUBDIRS) $(SERVER_SUBDIRS)
+	$(IPACLIENT_SUBDIRS) $(IPATESTS_SUBDIRS) $(SERVER_SUBDIRS)
 
 MOSTLYCLEANFILES = ipasetup.pyc ipasetup.pyo \
 		   ignore_import_errors.pyc ignore_import_errors.pyo \
diff --git a/configure.ac b/configure.ac
index f098eb1dac..699fd64cba 100644
--- a/configure.ac
+++ b/configure.ac
@@ -520,13 +520,6 @@ 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
@@ -594,7 +587,6 @@ AC_CONFIG_FILES([
     pypi/Makefile
     pypi/freeipa/Makefile
     pypi/ipa/Makefile
-    pypi/ipaplatform/Makefile
     pypi/ipaserver/Makefile
     pypi/ipatests/Makefile
     po/Makefile.in
diff --git a/freeipa.spec.in b/freeipa.spec.in
index 8b7f179da4..b8bafacdbf 100644
--- a/freeipa.spec.in
+++ b/freeipa.spec.in
@@ -1610,6 +1610,7 @@ fi
 %{python_sitelib}/ipapython-*.egg-info
 %{python_sitelib}/ipalib-*.egg-info
 %{python_sitelib}/ipaplatform-*.egg-info
+%{python_sitelib}/ipaplatform-*-nspkg.pth
 
 
 %files common -f %{gettext_domain}.lang
diff --git a/ignore_import_errors.py b/ignore_import_errors.py
index 4ee6ee98bc..7f3ee50d39 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/ipalib/config.py b/ipalib/config.py
index 151c4b4a8f..c714c137fc 100644
--- a/ipalib/config.py
+++ b/ipalib/config.py
@@ -28,6 +28,7 @@
 
 For the per-request thread-local information, see `ipalib.request`.
 """
+from __future__ import absolute_import
 
 import os
 from os import path
@@ -39,6 +40,7 @@
 from six.moves.configparser import RawConfigParser, ParsingError
 # pylint: enable=import-error
 
+from ipaplatform.tasks import tasks
 from ipapython.dn import DN
 from ipalib.base import check_name
 from ipalib.constants import (
@@ -47,12 +49,6 @@
     TLS_VERSIONS
 )
 from ipalib import errors
-try:
-    # pylint: disable=ipa-forbidden-import
-    from ipaplatform.tasks import tasks
-    # pylint: enable=ipa-forbidden-import
-except ImportError:
-    tasks = None
 
 if six.PY3:
     unicode = str
@@ -453,8 +449,7 @@ def _bootstrap(self, **overrides):
         self.home = os.environ.get('HOME', None)
 
         # Set fips_mode only if ipaplatform module was loaded
-        if tasks is not None:
-            self.fips_mode = tasks.is_fips_enabled()
+        self.fips_mode = tasks.is_fips_enabled()
 
         # Merge in overrides:
         self._merge(**overrides)
diff --git a/ipalib/setup.py b/ipalib/setup.py
index cdbd61c012..722fcebd31 100644
--- a/ipalib/setup.py
+++ b/ipalib/setup.py
@@ -37,6 +37,7 @@
             "ipalib.install",
         ],
         install_requires=[
+            "ipaplatform",
             "ipapython",
             "netaddr",
             "pyasn1",
diff --git a/ipalib/util.py b/ipalib/util.py
index 91d6e469a5..3601fd0f9f 100644
--- a/ipalib/util.py
+++ b/ipalib/util.py
@@ -55,10 +55,7 @@
     TLS_VERSIONS, TLS_VERSION_MINIMAL, TLS_HIGH_CIPHERS
 )
 from ipalib.text import _
-# pylint: disable=ipa-forbidden-import
-from ipalib.install import sysrestore
 from ipaplatform.paths import paths
-# pylint: enable=ipa-forbidden-import
 from ipapython.ssh import SSHPublicKey
 from ipapython.dn import DN, RDN
 from ipapython.dnsutil import DNSName
@@ -1078,8 +1075,9 @@ def check_client_configuration():
     """
     Check if IPA client is configured on the system.
     """
-    fstore = sysrestore.FileStore(paths.IPA_CLIENT_SYSRESTORE)
-    if not fstore.has_files() and not os.path.exists(paths.IPA_DEFAULT_CONF):
+    if (not os.path.isfile(paths.IPA_DEFAULT_CONF) or
+            not os.path.isdir(paths.IPA_CLIENT_SYSRESTORE) or
+            not os.listdir(paths.IPA_CLIENT_SYSRESTORE)):
         raise ScriptError('IPA client is not configured on this system')
 
 
diff --git a/ipaplatform/Makefile.am b/ipaplatform/Makefile.am
index 8be72b25da..61b5f53516 100644
--- a/ipaplatform/Makefile.am
+++ b/ipaplatform/Makefile.am
@@ -1 +1,12 @@
 include $(top_srcdir)/Makefile.python.am
+
+EXTRA_DIST = fallback.py.in
+
+all-local: fallback.py
+dist-hook: fallback.py
+install-exec-local: fallback.py
+
+fallback.py: fallback.py.in $(top_builddir)/$(CONFIG_STATUS)
+	$(AM_V_GEN)sed						\
+		-e 's|@IPAPLATFORM[@]|$(IPAPLATFORM)|g'		\
+		$< > $@
diff --git a/ipaplatform/__init__.py b/ipaplatform/__init__.py
new file mode 100644
index 0000000000..4ce1057239
--- /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 0000000000..23c034891b
--- /dev/null
+++ b/ipaplatform/_importhook.py
@@ -0,0 +1,125 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+"""Meta import hook for ipaplatform.
+
+Known Linux distros with /etc/os-release
+----------------------------------------
+
+- alpine
+- centos (like rhel, fedora)
+- debian
+- fedora
+- rhel
+- ubuntu (like debian)
+"""
+
+import importlib
+import sys
+import warnings
+
+import ipaplatform
+try:
+    from ipaplatform.fallback import FALLBACK
+except ImportError:
+    FALLBACK = None
+
+
+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'
+    }
+    bsd_family = ('freebsd', 'openbsd', 'netbsd', 'dragonfly', 'gnukfreebsd')
+
+    def __init__(self):
+        self.platform_ids = self._get_platform_ids()
+        self.platform = self._get_platform(self.platform_ids)
+
+    def _get_platform_ids(self):
+        if sys.platform.startswith('linux'):
+            # Linux, get distribution from /etc/os-release
+            try:
+                platforms = self._read_osrelease()
+            except OSError as e:
+                warnings.warn("Failed to read /etc/os-release: {}".format(e))
+                platforms = []
+        elif sys.platform == 'win32':
+            # Windows 32 or 64bit platform
+            platforms = ['win32']
+        elif sys.platform == 'darwin':
+            # macOS
+            platforms = ['macos']
+        elif sys.platform.startswith(self.bsd_family):
+            # BSD family, look for e.g. ['freebsd10', 'freebsd']
+            platforms = [sys.platform, sys.platform.rstrip('0123456789')]
+        else:
+            raise ValueError(sys.platform)
+        # Allow official packages to provide a fallback
+        if FALLBACK is not None and FALLBACK not in platforms:
+            platforms.append(FALLBACK)
+        return platforms
+
+    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(
+                        v.strip() for v in value.split(' ') if v.strip()
+                    )
+        return platforms
+
+    def _get_platform(self, platform_ids):
+        for platform in platform_ids:
+            try:
+                importlib.import_module('ipaplatform.{}'.format(platform))
+            except ImportError:
+                pass
+            else:
+                return platform
+        raise ImportError('No ipaplatform available for "{}"'.format(
+                          ', '.join(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__.update(platform_mod.__dict__)
+            for key in list(base_mod.__dict__):
+                if not hasattr(platform_mod, key):
+                    delattr(base_mod, key)
+        else:
+            sys.modules[fullname] = platform_mod
+        return platform_mod
+
+
+metaimporter = IpaMetaImporter()
+sys.meta_path.insert(0, metaimporter)
+
+fixup_module = metaimporter.load_module
+ipaplatform.NAME = metaimporter.platform
diff --git a/ipaplatform/base/constants.py b/ipaplatform/base/constants.py
index 6592c63d97..f97f299704 100644
--- a/ipaplatform/base/constants.py
+++ b/ipaplatform/base/constants.py
@@ -37,3 +37,5 @@ class BaseConstantsNamespace(object):
         'httpd_dbus_sssd': 'on',
     }
     SSSD_USER = "sssd"
+
+constants = BaseConstantsNamespace()
diff --git a/ipaplatform/base/paths.py b/ipaplatform/base/paths.py
index 2d96dc2efa..c27adfe711 100644
--- a/ipaplatform/base/paths.py
+++ b/ipaplatform/base/paths.py
@@ -356,5 +356,6 @@ class BasePathNamespace(object):
     GSSPROXY_CONF = '/etc/gssproxy/10-ipa.conf'
     KRB5CC_HTTPD = '/tmp/krb5cc-httpd'
     IF_INET6 = '/proc/net/if_inet6'
+    AUTHCONFIG = None
 
-path_namespace = BasePathNamespace
+paths = BasePathNamespace()
diff --git a/ipaplatform/base/services.py b/ipaplatform/base/services.py
index fca6298fc0..712052112f 100644
--- a/ipaplatform/base/services.py
+++ b/ipaplatform/base/services.py
@@ -505,8 +505,12 @@ def remove(self):
 
 # Objects below are expected to be exported by platform module
 
-service = None
-knownservices = None
+def base_service_class_factory(name, api=None):
+    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 dc3cacc237..bea49ca61d 100644
--- a/ipaplatform/base/tasks.py
+++ b/ipaplatform/base/tasks.py
@@ -204,6 +204,9 @@ def configure_httpd_service_ipa_conf(self):
         """Configure httpd service to work with IPA"""
         raise NotImplementedError()
 
+    def configure_http_gssproxy_conf(self, ipauser):
+        raise NotImplementedError()
+
     def remove_httpd_service_ipa_conf(self):
         """Remove configuration of httpd service of IPA"""
         raise NotImplementedError()
@@ -219,3 +222,5 @@ def add_user_to_group(self, user, group):
             logger.debug('Done adding user to group')
         except ipautil.CalledProcessError as e:
             logger.debug('Failed to add user to group: %s', e)
+
+tasks = BaseTaskNamespace()
diff --git a/ipaplatform/constants.py b/ipaplatform/constants.py
new file mode 100644
index 0000000000..cc43cfb1df
--- /dev/null
+++ b/ipaplatform/constants.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.constants.
+"""
+import ipaplatform._importhook
+
+ipaplatform._importhook.fixup_module('ipaplatform.constants')
diff --git a/ipaplatform/fallback.py.in b/ipaplatform/fallback.py.in
new file mode 100644
index 0000000000..d0436ba0d7
--- /dev/null
+++ b/ipaplatform/fallback.py.in
@@ -0,0 +1 @@
+FALLBACK = '@IPAPLATFORM@'
diff --git a/ipaplatform/paths.py b/ipaplatform/paths.py
new file mode 100644
index 0000000000..2fcb477d42
--- /dev/null
+++ b/ipaplatform/paths.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.paths.
+"""
+import ipaplatform._importhook
+
+ipaplatform._importhook.fixup_module('ipaplatform.paths')
diff --git a/ipaplatform/services.py b/ipaplatform/services.py
new file mode 100644
index 0000000000..0d40f64430
--- /dev/null
+++ b/ipaplatform/services.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.services.
+"""
+import ipaplatform._importhook
+
+ipaplatform._importhook.fixup_module('ipaplatform.services')
diff --git a/ipaplatform/setup.py b/ipaplatform/setup.py
index 501e2bc568..1098ab6f10 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 0000000000..23c7859094
--- /dev/null
+++ b/ipaplatform/tasks.py
@@ -0,0 +1,8 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+"""IpaMetaImporter replaces this module with ipaplatform.$NAME.tasks.
+"""
+import ipaplatform._importhook
+
+ipaplatform._importhook.fixup_module('ipaplatform.tasks')
diff --git a/ipapython/certdb.py b/ipapython/certdb.py
index 92da7829ac..14e6adf69f 100644
--- a/ipapython/certdb.py
+++ b/ipapython/certdb.py
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
+from __future__ import absolute_import
 
 import collections
 import logging
@@ -30,24 +31,12 @@
 
 import cryptography.x509
 
+from ipaplatform.paths import paths
 from ipapython.dn import DN
 from ipapython.kerberos import Principal
 from ipapython import ipautil
 from ipalib import x509     # pylint: disable=ipa-forbidden-import
 
-try:
-    # pylint: disable=import-error,ipa-forbidden-import
-    from ipaplatform.paths import paths
-    # pylint: enable=import-error,ipa-forbidden-import
-except ImportError:
-    CERTUTIL = '/usr/bin/certutil'
-    PK12UTIL = '/usr/bin/pk12util'
-    OPENSSL = '/usr/bin/openssl'
-else:
-    CERTUTIL = paths.CERTUTIL
-    PK12UTIL = paths.PK12UTIL
-    OPENSSL = paths.OPENSSL
-
 
 logger = logging.getLogger(__name__)
 
@@ -188,7 +177,8 @@ def verify_kdc_cert_validity(kdc_cert, ca_certs, realm):
 
         try:
             ipautil.run(
-                [OPENSSL, 'verify', '-CAfile', ca_file.name, kdc_file.name],
+                [paths.OPENSSL, 'verify', '-CAfile', ca_file.name,
+                 kdc_file.name],
                 capture_output=True)
         except ipautil.CalledProcessError as e:
             raise ValueError(e.output)
@@ -244,7 +234,7 @@ def __exit__(self, type, value, tb):
         self.close()
 
     def run_certutil(self, args, stdin=None, **kwargs):
-        new_args = [CERTUTIL, "-d", self.secdir]
+        new_args = [paths.CERTUTIL, "-d", self.secdir]
         new_args = new_args + args
         new_args.extend(['-f', self.pwd_file])
         return ipautil.run(new_args, stdin, **kwargs)
@@ -367,7 +357,7 @@ def get_trust_chain(self, nickname):
         return root_nicknames
 
     def export_pkcs12(self, nickname, pkcs12_filename, pkcs12_passwd=None):
-        args = [PK12UTIL, "-d", self.secdir,
+        args = [paths.PK12UTIL, "-d", self.secdir,
                 "-o", pkcs12_filename,
                 "-n", nickname,
                 "-k", self.pwd_file]
@@ -391,7 +381,7 @@ def export_pkcs12(self, nickname, pkcs12_filename, pkcs12_passwd=None):
                 pkcs12_password_file.close()
 
     def import_pkcs12(self, pkcs12_filename, pkcs12_passwd=None):
-        args = [PK12UTIL, "-d", self.secdir,
+        args = [paths.PK12UTIL, "-d", self.secdir,
                 "-i", pkcs12_filename,
                 "-k", self.pwd_file, '-v']
         pkcs12_password_file = None
@@ -501,7 +491,7 @@ def import_files(self, files, import_keys=False, key_password=None,
                                 (key_file, filename))
 
                         args = [
-                            OPENSSL, 'pkcs8',
+                            paths.OPENSSL, 'pkcs8',
                             '-topk8',
                             '-passout', 'file:' + self.pwd_file,
                         ]
@@ -588,7 +578,7 @@ def import_files(self, files, import_keys=False, key_password=None,
                 out_password = ipautil.ipa_generate_password()
                 out_pwdfile = ipautil.write_tmp_file(out_password)
                 args = [
-                    OPENSSL, 'pkcs12',
+                    paths.OPENSSL, 'pkcs12',
                     '-export',
                     '-in', in_file.name,
                     '-out', out_file.name,
diff --git a/ipapython/config.py b/ipapython/config.py
index 8393e0d5d5..7e2b2324c9 100644
--- a/ipapython/config.py
+++ b/ipapython/config.py
@@ -16,6 +16,7 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
+from __future__ import absolute_import
 
 # pylint: disable=deprecated-module
 from optparse import (
@@ -32,17 +33,9 @@
 from six.moves.urllib.parse import urlsplit
 # pylint: enable=import-error
 
+from ipaplatform.paths import paths
 from ipapython.dn import DN
 
-try:
-    # pylint: disable=ipa-forbidden-import
-    from ipaplatform.paths import paths
-    # pylint: enable=ipa-forbidden-import
-except ImportError:
-    IPA_DEFAULT_CONF = '/etc/ipa/default.conf'
-else:
-    IPA_DEFAULT_CONF = paths.IPA_DEFAULT_CONF
-
 
 class IPAConfigError(Exception):
     def __init__(self, msg=''):
@@ -181,7 +174,7 @@ def get_domain(self):
 
 def __parse_config(discover_server = True):
     p = SafeConfigParser()
-    p.read(IPA_DEFAULT_CONF)
+    p.read(paths.IPA_DEFAULT_CONF)
 
     try:
         if not config.default_realm:
diff --git a/ipapython/ipautil.py b/ipapython/ipautil.py
index c5c5e9e213..462010f227 100644
--- a/ipapython/ipautil.py
+++ b/ipapython/ipautil.py
@@ -29,7 +29,7 @@
 import os
 import sys
 import copy
-import stat
+import stat  # pylint: disable=bad-python3-import
 import shutil
 import socket
 import re
diff --git a/ipapython/setup.py b/ipapython/setup.py
index 4f71530391..b982577d91 100755
--- a/ipapython/setup.py
+++ b/ipapython/setup.py
@@ -42,6 +42,7 @@
             "dnspython",
             "gssapi",
             # "ipalib",  # circular dependency
+            "ipaplatform",
             "netaddr",
             "netifaces",
             "six",
diff --git a/ipasetup.py.in b/ipasetup.py.in
index 2862ae234c..e8e80a4a85 100644
--- a/ipasetup.py.in
+++ b/ipasetup.py.in
@@ -25,22 +25,27 @@ class build_py(setuptools_build_py):
     """
     def initialize_options(self):
         setuptools_build_py.initialize_options(self)
-        self.skip_package = None
+        self.skip_modules = ()
 
     def finalize_options(self):
         setuptools_build_py.finalize_options(self)
         omit = os.environ.get('IPA_OMIT_INSTALL', '0')
         if omit == '1':
             distname = self.distribution.metadata.name
-            self.skip_package = '{}.install'.format(distname)
+            self.skip_modules = (
+                # *.install.* subpackages
+                '{}.install'.format(distname),
+                # platform fallback override module
+                'ipaplatform.fallback',
+            )
             log.warn("bdist_wheel: Ignore package: %s",
-                     self.skip_package)
+                     ', '.join(self.skip_modules))
 
     def build_module(self, module, module_file, package):
         if isinstance(package, str):
             package = package.split('.')
         name = '.'.join(list(package) + [module])
-        if self.skip_package and name.startswith(self.skip_package):
+        if self.skip_modules and name.startswith(self.skip_modules):
             # remove file in case it has been copied to build/lib before
             outfile = self.get_module_outfile(self.build_lib, package, module)
             try:
diff --git a/ipatests/test_ipaplatform/__init__.py b/ipatests/test_ipaplatform/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/ipatests/test_ipaplatform/test_importhook.py b/ipatests/test_ipaplatform/test_importhook.py
new file mode 100644
index 0000000000..a824b0a6a2
--- /dev/null
+++ b/ipatests/test_ipaplatform/test_importhook.py
@@ -0,0 +1,36 @@
+#
+# Copyright (C) 2017  FreeIPA Contributors see COPYING for license
+#
+import sys
+
+import pytest
+
+import ipaplatform
+import ipaplatform.constants
+import ipaplatform.paths
+import ipaplatform.services
+import ipaplatform.tasks
+from ipaplatform._importhook import metaimporter
+from ipaplatform.fallback import FALLBACK
+
+
+@pytest.mark.skipif(not sys.platform.startswith('linux'),
+                    reason='test requires linux')
+def test_os_release():
+    platforms = metaimporter._get_platform_ids()
+    assert FALLBACK in platforms
+
+@pytest.mark.parametrize('mod,name', [
+    (ipaplatform.constants, 'ipaplatform.constants'),
+    (ipaplatform.paths, 'ipaplatform.paths'),
+    (ipaplatform.services, 'ipaplatform.services'),
+    (ipaplatform.tasks, 'ipaplatform.tasks'),
+])
+def test_importhook(mod, name):
+    assert name in metaimporter.modules
+    prefix, suffix = name.split('.')
+    assert prefix == 'ipaplatform'
+    override = '.'.join((prefix, metaimporter.platform, suffix))
+    assert mod.__name__ == override
+    # dicts are equal, modules may not be identical
+    assert mod.__dict__ == sys.modules[override].__dict__
diff --git a/pylint_plugins.py b/pylint_plugins.py
index e5c999861e..0c8678be0e 100644
--- a/pylint_plugins.py
+++ b/pylint_plugins.py
@@ -269,6 +269,48 @@ def pytest_config_transform():
 register_module_extender(MANAGER, 'pytest', pytest_config_transform)
 
 
+def ipaplatform_constants_transform():
+    return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
+    from ipaplatform.base.constants import constants
+    __all__ = ('constants',)
+    '''))
+
+
+def ipaplatform_paths_transform():
+    return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
+    from ipaplatform.base.paths import paths
+    __all__ = ('paths',)
+    '''))
+
+
+def ipaplatform_services_transform():
+    return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
+    from ipaplatform.base.services import knownservices
+    from ipaplatform.base.services import timedate_services
+    from ipaplatform.base.services import service
+    from ipaplatform.base.services import wellknownservices
+    from ipaplatform.base.services import wellknownports
+    __all__ = ('knownservices', 'timedate_services', 'service',
+               'wellknownservices', 'wellknownports')
+    '''))
+
+
+def ipaplatform_tasks_transform():
+    return AstroidBuilder(MANAGER).string_build(textwrap.dedent('''
+    from ipaplatform.base.tasks import tasks
+    __all__ = ('tasks',)
+    '''))
+
+register_module_extender(MANAGER, 'ipaplatform.constants',
+                         ipaplatform_constants_transform)
+register_module_extender(MANAGER, 'ipaplatform.paths',
+                         ipaplatform_paths_transform)
+register_module_extender(MANAGER, 'ipaplatform.services',
+                         ipaplatform_services_transform)
+register_module_extender(MANAGER, 'ipaplatform.tasks',
+                         ipaplatform_tasks_transform)
+
+
 class IPAChecker(BaseChecker):
     __implements__ = IAstroidChecker
 
diff --git a/pylintrc b/pylintrc
index 462b96cb00..8cd7c870bf 100644
--- a/pylintrc
+++ b/pylintrc
@@ -116,9 +116,9 @@ dummy-variables-rgx=_.+
 [IPA]
 forbidden-imports=
     client/:ipaserver,
-    ipaclient/:ipaclient.install:ipalib.install:ipaplatform:ipaserver,
+    ipaclient/:ipaclient.install:ipalib.install:ipaserver,
     ipaclient/install/:ipaserver,
-    ipalib/:ipaclient.install:ipalib.install:ipaplatform:ipaserver,
+    ipalib/:ipaclient.install:ipalib.install:ipaserver,
     ipalib/install/:ipaserver,
     ipaplatform/:ipaclient:ipalib:ipaserver,
-    ipapython/:ipaclient:ipalib:ipaplatform:ipaserver
+    ipapython/:ipaclient:ipalib:ipaserver
diff --git a/pypi/Makefile.am b/pypi/Makefile.am
index 5d8be9c1f3..bcbe1ea41f 100644
--- a/pypi/Makefile.am
+++ b/pypi/Makefile.am
@@ -7,7 +7,6 @@ NULL =
 SUBDIRS =			\
 	freeipa			\
 	ipa				\
-	ipaplatform		\
 	ipaserver		\
 	ipatests		\
 	$(NULL)
diff --git a/pypi/ipaplatform/Makefile.am b/pypi/ipaplatform/Makefile.am
deleted file mode 100644
index 15d86ce0c4..0000000000
--- a/pypi/ipaplatform/Makefile.am
+++ /dev/null
@@ -1,3 +0,0 @@
-include $(top_srcdir)/Makefile.python.am
-
-pkginstall = false
diff --git a/pypi/ipaplatform/README.txt b/pypi/ipaplatform/README.txt
deleted file mode 100644
index 15064b0b0a..0000000000
--- a/pypi/ipaplatform/README.txt
+++ /dev/null
@@ -1,2 +0,0 @@
-This is a dummy package for FreeIPA's ipaplatform.
-
diff --git a/pypi/ipaplatform/ipaplatform/__init__.py b/pypi/ipaplatform/ipaplatform/__init__.py
deleted file mode 100644
index 3b12c8c74e..0000000000
--- a/pypi/ipaplatform/ipaplatform/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-#
-# Copyright (C) 2017 FreeIPA Contributors see COPYING for license
-#
-
-raise ImportError("ipaplatform is not yet supported as PyPI package.")
diff --git a/pypi/ipaplatform/setup.cfg b/pypi/ipaplatform/setup.cfg
deleted file mode 100644
index 62f65c719e..0000000000
--- a/pypi/ipaplatform/setup.cfg
+++ /dev/null
@@ -1,6 +0,0 @@
-[bdist_wheel]
-universal = 1
-
-[aliases]
-packages = clean --all egg_info bdist_wheel
-release = packages register upload
diff --git a/pypi/ipaplatform/setup.py b/pypi/ipaplatform/setup.py
deleted file mode 100755
index f0fca2c708..0000000000
--- a/pypi/ipaplatform/setup.py
+++ /dev/null
@@ -1,26 +0,0 @@
-#
-# Copyright (C) 2017 FreeIPA Contributors see COPYING for license
-#
-"""Dummy package for FreeIPA
-
-ipaplatform is not yet available as PyPI package.
-"""
-
-from os.path import abspath, dirname
-import sys
-
-if __name__ == '__main__':
-    # include ../../ for ipasetup.py
-    sys.path.append(dirname(dirname(dirname(abspath(__file__)))))
-    from ipasetup import ipasetup  # noqa: E402
-
-    ipasetup(
-        name='ipaplatform',
-        doc = __doc__,
-        packages=[
-            "ipaplatform",
-        ],
-        install_requires=[
-            "ipaclient",
-        ]
-    )
diff --git a/pypi/test_placeholder.py b/pypi/test_placeholder.py
index d17b23af43..74002f577f 100644
--- a/pypi/test_placeholder.py
+++ b/pypi/test_placeholder.py
@@ -9,13 +9,14 @@
 
 @pytest.mark.parametrize("modname", [
     # placeholder packages raise ImportError
-    'ipaplatform',
     'ipaserver',
     'ipatests',
     # PyPI packages do not have install subpackage
     'ipaclient.install',
     'ipalib.install',
     'ipapython.install',
+    # fallback module should not be shipped in wheels
+    'ipaplatform.fallback',
 ])
 def test_fail_import(modname):
     try:
@@ -29,6 +30,7 @@ def test_fail_import(modname):
 @pytest.mark.parametrize("modname", [
     'ipaclient',
     'ipalib',
+    'ipaplatform',
     'ipapython',
 ])
 def test_import(modname):
_______________________________________________
FreeIPA-devel mailing list -- freeipa-devel@lists.fedorahosted.org
To unsubscribe send an email to freeipa-devel-le...@lists.fedorahosted.org

Reply via email to