URL: https://github.com/freeipa/freeipa/pull/492
Author: HonzaCholasta
 Title: #492: [WIP] config: remove meaningless defaults
Action: synchronized

To pull the PR as Git branch:
git remote add ghfreeipa https://github.com/freeipa/freeipa
git fetch ghfreeipa pull/492/head:pr492
git checkout pr492
From f054783ed7f632f6f676fe2d3fec1c486163e956 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 23 Feb 2017 09:44:04 +0000
Subject: [PATCH 1/6] user, migration: use LDAPClient for ad-hoc LDAP
 connections

Use LDAPClient instead of ldap2 for ad-hoc remote LDAP connections in the
user_status and migrate-ds plugins.
---
 ipaserver/plugins/migration.py | 15 +++++----------
 ipaserver/plugins/user.py      | 11 ++++-------
 2 files changed, 9 insertions(+), 17 deletions(-)

diff --git a/ipaserver/plugins/migration.py b/ipaserver/plugins/migration.py
index 72abd14..e8d102a 100644
--- a/ipaserver/plugins/migration.py
+++ b/ipaserver/plugins/migration.py
@@ -28,13 +28,9 @@
 from ipalib.cli import to_cli
 from ipalib.plugable import Registry
 from .user import NO_UPG_MAGIC
-if api.env.in_server and api.env.context in ['lite', 'server']:
-    try:
-        from ipaserver.plugins.ldap2 import ldap2
-    except Exception as e:
-        raise e
 from ipalib import _
 from ipapython.dn import DN
+from ipapython.ipaldap import LDAPClient
 from ipapython.ipautil import write_tmp_file
 from ipapython.kerberos import Principal
 import datetime
@@ -885,8 +881,6 @@ def execute(self, ldapuri, bindpw, **options):
             return dict(result={}, failed={}, enabled=False, compat=True)
 
         # connect to DS
-        ds_ldap = ldap2(self.api, ldap_uri=ldapuri)
-
         cacert = None
         if options.get('cacertfile') is not None:
             # store CA cert into file
@@ -894,12 +888,13 @@ def execute(self, ldapuri, bindpw, **options):
             cacert = tmp_ca_cert_f.name
 
             # start TLS connection
-            ds_ldap.connect(bind_dn=options['binddn'], bind_pw=bindpw,
-                            cacert=cacert)
+            ds_ldap = LDAPClient(ldapuri, cacert=cacert)
+            ds_ldap.simple_bind(options['binddn'], bindpw)
 
             tmp_ca_cert_f.close()
         else:
-            ds_ldap.connect(bind_dn=options['binddn'], bind_pw=bindpw)
+            ds_ldap = LDAPClient(ldapuri, cacert=cacert)
+            ds_ldap.simple_bind(options['binddn'], bindpw)
 
         # check whether the compat plugin is enabled
         if not options.get('compat'):
diff --git a/ipaserver/plugins/user.py b/ipaserver/plugins/user.py
index 88171cf..4c4b7d3 100644
--- a/ipaserver/plugins/user.py
+++ b/ipaserver/plugins/user.py
@@ -21,7 +21,6 @@
 import time
 from time import gmtime, strftime
 import posixpath
-import os
 
 import six
 
@@ -62,12 +61,10 @@
 from ipalib import output
 from ipaplatform.paths import paths
 from ipapython.dn import DN
+from ipapython.ipaldap import LDAPClient
 from ipapython.ipautil import ipa_generate_password, TMP_PWD_ENTROPY_BITS
 from ipalib.capabilities import client_has_capability
 
-if api.env.in_server:
-    from ipaserver.plugins.ldap2 import ldap2
-
 if six.PY3:
     unicode = str
 
@@ -1115,9 +1112,9 @@ def execute(self, *keys, **options):
             if host == api.env.host:
                 other_ldap = self.obj.backend
             else:
-                other_ldap = ldap2(self.api, ldap_uri='ldap://%s' % host)
                 try:
-                    other_ldap.connect(ccache=os.environ['KRB5CCNAME'])
+                    other_ldap = LDAPClient(ldap_uri='ldap://%s' % host)
+                    other_ldap.gssapi_bind()
                 except Exception as e:
                     self.error("user_status: Connecting to %s failed with %s" % (host, str(e)))
                     newresult = {'dn': dn}
@@ -1162,7 +1159,7 @@ def execute(self, *keys, **options):
                 count += 1
 
             if host != api.env.host:
-                other_ldap.disconnect()
+                other_ldap.close()
 
         return dict(result=entries,
                     count=count,

From 6ea9124b9bec0abd30236082d692f12e2b2fec1f Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 23 Feb 2017 09:52:51 +0000
Subject: [PATCH 2/6] {ca,kra}instance: drop redundant URI argument from ad-hoc
 ldap2 connections

Use the default URI from api.env.ldap_uri, as it is the same as the URI
provided using the argument.
---
 ipaserver/install/cainstance.py  | 19 +++++--------------
 ipaserver/install/krainstance.py |  4 +---
 2 files changed, 6 insertions(+), 17 deletions(-)

diff --git a/ipaserver/install/cainstance.py b/ipaserver/install/cainstance.py
index 6e3f995..e464263 100644
--- a/ipaserver/install/cainstance.py
+++ b/ipaserver/install/cainstance.py
@@ -706,9 +706,7 @@ def __create_ca_agent(self):
         cert = x509.load_certificate(cert_data, x509.DER)
 
         # connect to CA database
-        server_id = installutils.realm_to_serverid(api.env.realm)
-        dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
-        conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
+        conn = ldap2.ldap2(api)
         conn.connect(autobind=True)
 
         # create ipara user with ipaCert certificate
@@ -1341,14 +1339,12 @@ def __update_entry_from_cert(make_filter, make_entry, dercert):
     base_dn = DN(('o', 'ipaca'))
 
     attempts = 0
-    server_id = installutils.realm_to_serverid(api.env.realm)
-    dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
     updated = False
 
     while attempts < 10:
         conn = None
         try:
-            conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
+            conn = ldap2.ldap2(api)
             conn.connect(autobind=True)
 
             db_filter = make_filter(dercert)
@@ -1378,7 +1374,7 @@ def __update_entry_from_cert(make_filter, make_entry, dercert):
         except errors.NetworkError:
             syslog.syslog(
                 syslog.LOG_ERR,
-                'Connection to %s failed, sleeping 30s' % dogtag_uri)
+                'Connection to %s failed, sleeping 30s' % api.env.ldap_uri)
             time.sleep(30)
             attempts += 1
         except Exception as e:
@@ -1468,10 +1464,7 @@ def ensure_entry(dn, **attrs):
     otherwise add the entry and return ``True``.
 
     """
-    server_id = installutils.realm_to_serverid(api.env.realm)
-    dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
-
-    conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
+    conn = ldap2.ldap2(api)
     if not conn.isconnected():
         conn.connect(autobind=True)
 
@@ -1558,9 +1551,7 @@ def __get_profile_config(profile_id):
         '/usr/share/ipa/profiles/{}.cfg'.format(profile_id), sub_dict)
 
 def import_included_profiles():
-    server_id = installutils.realm_to_serverid(api.env.realm)
-    dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
-    conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
+    conn = ldap2.ldap2(api)
     if not conn.isconnected():
         conn.connect(autobind=True)
 
diff --git a/ipaserver/install/krainstance.py b/ipaserver/install/krainstance.py
index d7ab6fd..8272702 100644
--- a/ipaserver/install/krainstance.py
+++ b/ipaserver/install/krainstance.py
@@ -302,9 +302,7 @@ def __create_kra_agent(self):
         cert = x509.load_certificate(cert_data, x509.DER)
 
         # connect to KRA database
-        server_id = installutils.realm_to_serverid(api.env.realm)
-        dogtag_uri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % server_id
-        conn = ldap2.ldap2(api, ldap_uri=dogtag_uri)
+        conn = ldap2.ldap2(api)
         conn.connect(autobind=True)
 
         # create ipakra user with ipaCert certificate

From cb3108d87ea407683809545f76c921156709070d Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 23 Feb 2017 10:14:41 +0000
Subject: [PATCH 3/6] test_ldap: drop redundant URI argument

Use the default URI from api.env.ldap_uri, as it is the same as the URI
provided using the argument.
---
 ipatests/test_ipaserver/test_ldap.py | 23 +++++++++++------------
 1 file changed, 11 insertions(+), 12 deletions(-)

diff --git a/ipatests/test_ipaserver/test_ldap.py b/ipatests/test_ipaserver/test_ldap.py
index 3a0b4b2..3975e60 100644
--- a/ipatests/test_ipaserver/test_ldap.py
+++ b/ipatests/test_ipaserver/test_ldap.py
@@ -35,7 +35,7 @@
 import six
 
 from ipaplatform.paths import paths
-from ipaserver.plugins.ldap2 import ldap2
+from ipaserver.plugins.ldap2 import ldap2, AUTOBIND_DISABLED
 from ipalib import api, x509, create_api, errors
 from ipapython import ipautil
 from ipapython.dn import DN
@@ -52,7 +52,7 @@ class test_ldap(object):
 
     def setup(self):
         self.conn = None
-        self.ldapuri = 'ldap://%s' % ipautil.format_netloc(api.env.host)
+        self.ldapuri = api.env.ldap_uri
         nss.nss_init_nodb()
         self.dn = DN(('krbprincipalname','ldap/%s@%s' % (api.env.host, api.env.realm)),
                      ('cn','services'),('cn','accounts'),api.env.basedn)
@@ -65,8 +65,8 @@ def test_anonymous(self):
         """
         Test an anonymous LDAP bind using ldap2
         """
-        self.conn = ldap2(api, ldap_uri=self.ldapuri)
-        self.conn.connect()
+        self.conn = ldap2(api)
+        self.conn.connect(autobind=AUTOBIND_DISABLED)
         dn = api.env.basedn
         entry_attrs = self.conn.get_entry(dn, ['associateddomain'])
         domain = entry_attrs.single_value['associateddomain']
@@ -76,8 +76,8 @@ def test_GSSAPI(self):
         """
         Test a GSSAPI LDAP bind using ldap2
         """
-        self.conn = ldap2(api, ldap_uri=self.ldapuri)
-        self.conn.connect()
+        self.conn = ldap2(api)
+        self.conn.connect(autobind=AUTOBIND_DISABLED)
         entry_attrs = self.conn.get_entry(self.dn, ['usercertificate'])
         cert = entry_attrs.get('usercertificate')
         cert = cert[0]
@@ -95,7 +95,7 @@ def test_simple(self):
             fp.close()
         else:
             raise nose.SkipTest("No directory manager password in %s" % pwfile)
-        self.conn = ldap2(api, ldap_uri=self.ldapuri)
+        self.conn = ldap2(api)
         self.conn.connect(bind_dn=DN(('cn', 'directory manager')), bind_pw=dm_password)
         entry_attrs = self.conn.get_entry(self.dn, ['usercertificate'])
         cert = entry_attrs.get('usercertificate')
@@ -135,8 +135,7 @@ def test_autobind(self):
         """
         Test an autobind LDAP bind using ldap2
         """
-        ldapuri = 'ldapi://%%2fvar%%2frun%%2fslapd-%s.socket' % api.env.realm.replace('.','-')
-        self.conn = ldap2(api, ldap_uri=ldapuri)
+        self.conn = ldap2(api)
         try:
             self.conn.connect(autobind=True)
         except errors.ACIError:
@@ -159,9 +158,9 @@ class test_LDAPEntry(object):
     dn2 = DN(('cn', cn2[0]))
 
     def setup(self):
-        self.ldapuri = 'ldap://%s' % ipautil.format_netloc(api.env.host)
-        self.conn = ldap2(api, ldap_uri=self.ldapuri)
-        self.conn.connect()
+        self.ldapuri = api.env.ldap_uri
+        self.conn = ldap2(api)
+        self.conn.connect(autobind=AUTOBIND_DISABLED)
 
         self.entry = self.conn.make_entry(self.dn1, cn=self.cn1)
 

From 75a7c9241a7b2df5c3340071f62c2054f133136b Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Thu, 23 Feb 2017 10:15:52 +0000
Subject: [PATCH 4/6] ldap2: remove unused URI argument from ldap2 constructor

---
 ipapython/ipaldap.py       | 19 ++++++++++---------
 ipaserver/plugins/ldap2.py | 11 ++++++-----
 2 files changed, 16 insertions(+), 14 deletions(-)

diff --git a/ipapython/ipaldap.py b/ipapython/ipaldap.py
index 82d45b9..3897b5b 100644
--- a/ipapython/ipaldap.py
+++ b/ipapython/ipaldap.py
@@ -700,7 +700,16 @@ def __init__(self, ldap_uri, start_tls=False, force_schema_updates=False,
             If true, attributes are decoded to Python types according to their
             syntax.
         """
-        self.ldap_uri = ldap_uri
+        if ldap_uri is not None:
+            self.ldap_uri = ldap_uri
+            self.host = 'localhost'
+            self.port = None
+            url_data = urlparse(ldap_uri)
+            self._protocol = url_data.scheme
+            if self._protocol in ('ldap', 'ldaps'):
+                self.host = url_data.hostname
+                self.port = url_data.port
+
         self._start_tls = start_tls
         self._force_schema_updates = force_schema_updates
         self._no_schema = no_schema
@@ -708,14 +717,6 @@ def __init__(self, ldap_uri, start_tls=False, force_schema_updates=False,
         self._cacert = cacert
         self._sasl_nocanon = sasl_nocanon
 
-        self.host = 'localhost'
-        self.port = None
-        url_data = urlparse(ldap_uri)
-        self._protocol = url_data.scheme
-        if self._protocol in ('ldap', 'ldaps'):
-            self.host = url_data.hostname
-            self.port = url_data.port
-
         self.log = log_mgr.get_logger(self)
         self._has_schema = False
         self._schema = None
diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py
index e671ecb..3cff47b 100644
--- a/ipaserver/plugins/ldap2.py
+++ b/ipaserver/plugins/ldap2.py
@@ -67,20 +67,21 @@ class ldap2(CrudBackend, LDAPClient):
     LDAP Backend Take 2.
     """
 
-    def __init__(self, api, ldap_uri=None):
-        if ldap_uri is None:
-            ldap_uri = api.env.ldap_uri
-
+    def __init__(self, api):
         force_schema_updates = api.env.context in ('installer', 'updates')
 
         CrudBackend.__init__(self, api)
-        LDAPClient.__init__(self, ldap_uri,
+        LDAPClient.__init__(self, None,
                             force_schema_updates=force_schema_updates)
 
         self.__time_limit = float(LDAPClient.time_limit)
         self.__size_limit = int(LDAPClient.size_limit)
 
     @property
+    def ldap_uri(self):
+        return self.api.env.ldap_uri
+
+    @property
     def time_limit(self):
         if self.__time_limit is None:
             return float(self.get_ipa_config().single_value.get(

From cf6b7fd5571c79a4aaf3d0f4daa4079659fff1c0 Mon Sep 17 00:00:00 2001
From: Petr Spacek <pspa...@redhat.com>
Date: Tue, 10 May 2016 14:20:15 +0200
Subject: [PATCH 5/6] ipalib.constants: Remove default domain, realm, basedn,
 xmlrpc_uri, ldap_uri

Domain, realm, basedn, xmlrpc_uri, ldap_uri do not have any reasonable default.
This patch removes hardcoded default so the so the code which depends
on these values blows up early and does not do crazy stuff
with default values instead of real ones.

This should help to uncover issues caused by improper ipalib
initialization.
---
 ipalib/constants.py | 16 +++++++++++-----
 makeaci             |  3 +++
 makeapi             |  6 ++++++
 3 files changed, 20 insertions(+), 5 deletions(-)

diff --git a/ipalib/constants.py b/ipalib/constants.py
index e64324f..0269959 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -67,9 +67,12 @@
     ('version', VERSION),
 
     # Domain, realm, basedn:
-    ('domain', 'example.com'),
-    ('realm', 'EXAMPLE.COM'),
-    ('basedn', DN(('dc', 'example'), ('dc', 'com'))),
+    # Following values do not have any reasonable default.
+    # Do not initialize them so the code which depends on them blows up early
+    # and does not do crazy stuff with default values instead of real ones.
+    # ('domain', 'example.com'),
+    # ('realm', 'EXAMPLE.COM'),
+    # ('basedn', DN(('dc', 'example'), ('dc', 'com'))),
 
     # LDAP containers:
     ('container_accounts', DN(('cn', 'accounts'))),
@@ -124,9 +127,12 @@
     ('container_sysaccounts', DN(('cn', 'sysaccounts'), ('cn', 'etc'))),
 
     # Ports, hosts, and URIs:
-    ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
+    # Following values do not have any reasonable default.
+    # Do not initialize them so the code which depends on them blows up early
+    # and does not do crazy stuff with default values instead of real ones.
+    # ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
     # jsonrpc_uri is set in Env._finalize_core()
-    ('ldap_uri', 'ldap://localhost:389'),
+    # ('ldap_uri', 'ldap://localhost:389'),
 
     ('rpc_protocol', 'jsonrpc'),
 
diff --git a/makeaci b/makeaci
index 98b199c..813ecc7 100755
--- a/makeaci
+++ b/makeaci
@@ -98,6 +98,9 @@ def main(options):
         plugins_on_demand=False,
         basedn=DN('dc=ipa,dc=example'),
         realm='IPA.EXAMPLE',
+        domain="example.com",
+        xmlrpc_uri="http://localhost:8888/ipa/xml";,
+        ldap_uri="ldap://localhost:389";,
     )
 
     from ipaserver.install.plugins import update_managed_permissions
diff --git a/makeapi b/makeapi
index d0a7295..729b922 100755
--- a/makeapi
+++ b/makeapi
@@ -40,6 +40,7 @@ from ipalib.parameters import Param
 from ipalib.output import Output
 from ipalib.text import Gettext, NGettext, ConcatenatedLazyText
 from ipalib.capabilities import capabilities
+from ipapython.dn import DN
 
 API_FILE='API.txt'
 
@@ -513,6 +514,11 @@ def main():
         enable_ra=True,
         mode='developer',
         plugins_on_demand=False,
+        basedn=DN(('dc', 'example'), ('dc', 'com')),
+        realm="EXAMPLE.COM",
+        domain="example.com",
+        xmlrpc_uri="http://localhost:8888/ipa/xml";,
+        ldap_uri="ldap://localhost:389";,
     )
 
     api.bootstrap(**cfg)

From f9549515beb003dd2b48417736690e1e9aa23445 Mon Sep 17 00:00:00 2001
From: Jan Cholasta <jchol...@redhat.com>
Date: Tue, 21 Feb 2017 13:24:51 +0000
Subject: [PATCH 6/6] config: provide defaults for `xmlrpc_uri`, `ldap_uri` and
 `basedn`

Derive the default value of `xmlrpc_uri` and `ldap_uri` from `server`.
Derive the default value of `basedn` from `domain`.
---
 ipalib/config.py    | 14 ++++++++++++++
 ipalib/constants.py |  5 ++---
 makeaci             |  2 --
 makeapi             |  4 ----
 pylint_plugins.py   |  4 +++-
 5 files changed, 19 insertions(+), 10 deletions(-)

diff --git a/ipalib/config.py b/ipalib/config.py
index 388ffe8..28ed5ea 100644
--- a/ipalib/config.py
+++ b/ipalib/config.py
@@ -565,6 +565,20 @@ def _finalize_core(self, **defaults):
         if 'log' not in self:
             self.log = self._join('logdir', '%s.log' % self.context)
 
+        if 'basedn' not in self and 'domain' in self:
+            self.basedn = DN(*(('dc', dc) for dc in self.domain.split('.')))
+
+        # Derive xmlrpc_uri from server
+        # (Note that this is done before deriving jsonrpc_uri from xmlrpc_uri
+        # and server from jsonrpc_uri so that when only server or xmlrpc_uri
+        # is specified, all 3 keys have a value.)
+        if 'xmlrpc_uri' not in self and 'server' in self:
+            self.xmlrpc_uri = 'https://{}/ipa/xml'.format(self.server)
+
+        # Derive ldap_uri from server
+        if 'ldap_uri' not in self and 'server' in self:
+            self.ldap_uri = 'ldap://{}'.format(self.server)
+
         # Derive jsonrpc_uri from xmlrpc_uri
         if 'jsonrpc_uri' not in self:
             if 'xmlrpc_uri' in self:
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 0269959..71b5aa4 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -130,8 +130,9 @@
     # Following values do not have any reasonable default.
     # Do not initialize them so the code which depends on them blows up early
     # and does not do crazy stuff with default values instead of real ones.
+    # ('server', 'localhost'),
     # ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
-    # jsonrpc_uri is set in Env._finalize_core()
+    # ('jsonrpc_uri', 'http://localhost:8888/ipa/json'),
     # ('ldap_uri', 'ldap://localhost:389'),
 
     ('rpc_protocol', 'jsonrpc'),
@@ -237,8 +238,6 @@
     ('in_server', object),  # Whether or not running in-server (bool)
     ('logdir', object),  # Directory containing log files
     ('log', object),  # Path to context specific log file
-    ('jsonrpc_uri', object),  # derived from xmlrpc_uri in Env._finalize_core()
-    ('server', object),  # derived from jsonrpc_uri in Env._finalize_core()
 
 )
 
diff --git a/makeaci b/makeaci
index 813ecc7..57622fe 100755
--- a/makeaci
+++ b/makeaci
@@ -99,8 +99,6 @@ def main(options):
         basedn=DN('dc=ipa,dc=example'),
         realm='IPA.EXAMPLE',
         domain="example.com",
-        xmlrpc_uri="http://localhost:8888/ipa/xml";,
-        ldap_uri="ldap://localhost:389";,
     )
 
     from ipaserver.install.plugins import update_managed_permissions
diff --git a/makeapi b/makeapi
index 729b922..2b1d154 100755
--- a/makeapi
+++ b/makeapi
@@ -40,7 +40,6 @@ from ipalib.parameters import Param
 from ipalib.output import Output
 from ipalib.text import Gettext, NGettext, ConcatenatedLazyText
 from ipalib.capabilities import capabilities
-from ipapython.dn import DN
 
 API_FILE='API.txt'
 
@@ -514,11 +513,8 @@ def main():
         enable_ra=True,
         mode='developer',
         plugins_on_demand=False,
-        basedn=DN(('dc', 'example'), ('dc', 'com')),
         realm="EXAMPLE.COM",
         domain="example.com",
-        xmlrpc_uri="http://localhost:8888/ipa/xml";,
-        ldap_uri="ldap://localhost:389";,
     )
 
     api.bootstrap(**cfg)
diff --git a/pylint_plugins.py b/pylint_plugins.py
index 45adf71..5d7da73 100644
--- a/pylint_plugins.py
+++ b/pylint_plugins.py
@@ -94,7 +94,9 @@ def fake_class(name_or_class_obj, members=()):
         'xmlrpc_uri',
         'validate_api',
         'startup_traceback',
-        'verbose'
+        'verbose',
+        'server',
+        {'domain': dir(str)},
     ] + LOGGING_ATTRS,
     'ipalib.errors.ACIError': [
         'info',
-- 
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