On 02/22/2013 11:16 AM, Petr Viktorin wrote:
https://fedorahosted.org/freeipa/ticket/2886

This changes the DNA magic value to -1, and the corresponding IPA's
parameters (gidnumber, uidnumber) to be optional (instead of autofill).

Since the old clients still say "999" when they mean "pick one I don't
care", we need to detect them and change 999 to -1. For that there's a
new capability, optional_uid_params.


Behavior summary:

With --uid 999:
   old client, old server: sends 999, creates random UID
   old client, new server: sends 999, creates random UID
   new client, old server: incompatible
   new client, new server: sends 999, creates UID 999

Without --uid:
   old client, old server: sends 999, creates random UID
   old client, new server: sends 999, creates random UID
   new client, old server: incompatible
   new client, new server: doesn't send UID, creates random UID

Upgrade should work fine.

I didn't test winsync as I don't have a Windows machine.


The patch is here

--
PetrĀ³
From f5f7c2936f94ba332f4e8493ab9b785d5449eddb Mon Sep 17 00:00:00 2001
From: Petr Viktorin <pvikt...@redhat.com>
Date: Tue, 8 Jan 2013 04:10:35 -0500
Subject: [PATCH] Change DNA magic value to -1 to make UID 999 usable

Change user-add's uid & gid parameters from autofill to optional.
Change the DNA magic value to -1.

For old clients, which will still send 999 when they want DNA
assignment, translate the 999 to -1. This is done via a new
capability, optional_uid_params.

Tests included

https://fedorahosted.org/freeipa/ticket/2886
---
 API.txt                                            |   13 ++--
 VERSION                                            |    2 +-
 daemons/ipa-sam/ipa_sam.c                          |    2 +-
 .../ipa-winsync/ipa-winsync-conf.ldif              |    4 +-
 install/share/default-smb-group.ldif               |    2 +-
 install/share/dna.ldif                             |    2 +-
 install/updates/20-dna.update                      |   10 +++
 ipalib/capabilities.py                             |    6 ++
 ipalib/plugins/baseldap.py                         |    2 +
 ipalib/plugins/group.py                            |    5 +-
 ipalib/plugins/user.py                             |   34 +++++---
 tests/test_install/1_add.update                    |    4 +-
 tests/test_xmlrpc/test_user_plugin.py              |   86 ++++++++++++++++++++
 13 files changed, 144 insertions(+), 28 deletions(-)

diff --git a/API.txt b/API.txt
index a43fce596a6e01ba1fc709242ede0303994a255d..1664f61493093ca2438536455e98b792572a953a 100644
--- a/API.txt
+++ b/API.txt
@@ -3416,7 +3416,7 @@ option: Str('cn', attribute=True, autofill=True, cli_name='cn', multivalue=False
 option: Str('displayname', attribute=True, autofill=True, cli_name='displayname', multivalue=False, required=False)
 option: Str('facsimiletelephonenumber', attribute=True, cli_name='fax', multivalue=True, required=False)
 option: Str('gecos', attribute=True, autofill=True, cli_name='gecos', multivalue=False, required=False)
-option: Int('gidnumber', attribute=True, autofill=True, cli_name='gidnumber', default=999, minvalue=1, multivalue=False, required=True)
+option: Int('gidnumber', attribute=True, cli_name='gidnumber', minvalue=1, multivalue=False, required=False)
 option: Str('givenname', attribute=True, cli_name='first', multivalue=False, required=True)
 option: Str('homedirectory', attribute=True, cli_name='homedir', multivalue=False, required=False)
 option: Str('initials', attribute=True, autofill=True, cli_name='initials', multivalue=False, required=False)
@@ -3440,7 +3440,7 @@ option: Str('st', attribute=True, cli_name='state', multivalue=False, required=F
 option: Str('street', attribute=True, cli_name='street', multivalue=False, required=False)
 option: Str('telephonenumber', attribute=True, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, cli_name='title', multivalue=False, required=False)
-option: Int('uidnumber', attribute=True, autofill=True, cli_name='uid', default=999, minvalue=1, multivalue=False, required=True)
+option: Int('uidnumber', attribute=True, cli_name='uid', minvalue=1, multivalue=False, required=False)
 option: Password('userpassword', attribute=True, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -3477,7 +3477,7 @@ option: Str('cn', attribute=True, autofill=False, cli_name='cn', multivalue=Fals
 option: Str('displayname', attribute=True, autofill=False, cli_name='displayname', multivalue=False, query=True, required=False)
 option: Str('facsimiletelephonenumber', attribute=True, autofill=False, cli_name='fax', multivalue=True, query=True, required=False)
 option: Str('gecos', attribute=True, autofill=False, cli_name='gecos', multivalue=False, query=True, required=False)
-option: Int('gidnumber', attribute=True, autofill=False, cli_name='gidnumber', default=999, minvalue=1, multivalue=False, query=True, required=False)
+option: Int('gidnumber', attribute=True, autofill=False, cli_name='gidnumber', minvalue=1, multivalue=False, query=True, required=False)
 option: Str('givenname', attribute=True, autofill=False, cli_name='first', multivalue=False, query=True, required=False)
 option: Str('homedirectory', attribute=True, autofill=False, cli_name='homedir', multivalue=False, query=True, required=False)
 option: Str('in_group*', cli_name='in_groups', csv=True)
@@ -3511,7 +3511,7 @@ option: Str('telephonenumber', attribute=True, autofill=False, cli_name='phone',
 option: Int('timelimit?', autofill=False, minvalue=0)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, query=True, required=False)
 option: Str('uid', attribute=True, autofill=False, cli_name='login', maxlength=255, multivalue=False, pattern='^[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?$', primary_key=True, query=True, required=False)
-option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', default=999, minvalue=1, multivalue=False, query=True, required=False)
+option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, query=True, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, query=True, required=False)
 option: Str('version?', exclude='webui')
 option: Flag('whoami', autofill=True, default=False)
@@ -3530,7 +3530,7 @@ option: Str('delattr*', cli_name='delattr', exclude='webui')
 option: Str('displayname', attribute=True, autofill=False, cli_name='displayname', multivalue=False, required=False)
 option: Str('facsimiletelephonenumber', attribute=True, autofill=False, cli_name='fax', multivalue=True, required=False)
 option: Str('gecos', attribute=True, autofill=False, cli_name='gecos', multivalue=False, required=False)
-option: Int('gidnumber', attribute=True, autofill=False, cli_name='gidnumber', default=999, minvalue=1, multivalue=False, required=False)
+option: Int('gidnumber', attribute=True, autofill=False, cli_name='gidnumber', minvalue=1, multivalue=False, required=False)
 option: Str('givenname', attribute=True, autofill=False, cli_name='first', multivalue=False, required=False)
 option: Str('homedirectory', attribute=True, autofill=False, cli_name='homedir', multivalue=False, required=False)
 option: Str('initials', attribute=True, autofill=False, cli_name='initials', multivalue=False, required=False)
@@ -3554,7 +3554,7 @@ option: Str('st', attribute=True, autofill=False, cli_name='state', multivalue=F
 option: Str('street', attribute=True, autofill=False, cli_name='street', multivalue=False, required=False)
 option: Str('telephonenumber', attribute=True, autofill=False, cli_name='phone', multivalue=True, required=False)
 option: Str('title', attribute=True, autofill=False, cli_name='title', multivalue=False, required=False)
-option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', default=999, minvalue=1, multivalue=False, required=False)
+option: Int('uidnumber', attribute=True, autofill=False, cli_name='uid', minvalue=1, multivalue=False, required=False)
 option: Password('userpassword', attribute=True, autofill=False, cli_name='password', exclude='webui', multivalue=False, required=False)
 option: Str('version?', exclude='webui')
 output: Entry('result', <type 'dict'>, Gettext('A dictionary representing an LDAP entry', domain='ipa', localedir=None))
@@ -3588,3 +3588,4 @@ output: Output('result', <type 'bool'>, None)
 output: Output('summary', (<type 'unicode'>, <type 'NoneType'>), None)
 output: Output('value', <type 'unicode'>, None)
 capability: messages 2.52
+capability: optional_uid_params 2.54
diff --git a/VERSION b/VERSION
index 26696e1f5f2ea6cbc7d042833980435f2084761e..5aabba5932a3462368bab903c3c7a0773305042c 100644
--- a/VERSION
+++ b/VERSION
@@ -89,4 +89,4 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=53
+IPA_API_VERSION_MINOR=54
diff --git a/daemons/ipa-sam/ipa_sam.c b/daemons/ipa-sam/ipa_sam.c
index 0d4a27cf6def737412f6ab3ca48df8d1339c15b6..9ba4ba3917ba8235a5a21e459370119ca16b3f4b 100644
--- a/daemons/ipa-sam/ipa_sam.c
+++ b/daemons/ipa-sam/ipa_sam.c
@@ -101,7 +101,7 @@ bool secrets_store(const char *key, const void *data, size_t size); /* available
 
 #define IPA_KEYTAB_SET_OID "2.16.840.1.113730.3.8.10.1"
 #define IPA_KEYTAB_SET_OID_OLD "2.16.840.1.113730.3.8.3.1"
-#define IPA_MAGIC_ID_STR "999"
+#define IPA_MAGIC_ID_STR "-1"
 
 #define LDAP_ATTRIBUTE_CN "cn"
 #define LDAP_ATTRIBUTE_UID "uid"
diff --git a/daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif b/daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif
index b646c2b10db1eabda747d587a0d176b6afae63e7..08b43277f20bf44a22977d35b2560a010cebd4db 100644
--- a/daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif
+++ b/daemons/ipa-slapi-plugins/ipa-winsync/ipa-winsync-conf.ldif
@@ -24,5 +24,5 @@ ipaWinSyncDefaultGroupAttr: ipaDefaultPrimaryGroup
 ipaWinSyncDefaultGroupFilter: (gidNumber=*)(objectclass=posixGroup)(objectclass=groupOfNames)
 ipaWinSyncAcctDisable: both
 ipaWinSyncForceSync: true
-ipaWinSyncUserAttr: uidNumber 999
-ipaWinSyncUserAttr: gidNumber 999
+ipaWinSyncUserAttr: uidNumber -1
+ipaWinSyncUserAttr: gidNumber -1
diff --git a/install/share/default-smb-group.ldif b/install/share/default-smb-group.ldif
index abcc8a945a8187529044beeb73262b5434070b48..3d2e2a04cebaea63a6ae8527d8b30f8d19acd476 100644
--- a/install/share/default-smb-group.ldif
+++ b/install/share/default-smb-group.ldif
@@ -2,7 +2,7 @@ dn: cn=Default SMB Group,cn=groups,cn=accounts,$SUFFIX
 changetype: add
 cn: Default SMB Group
 description: Fallback group for primary group RID, do not add users to this group
-gidnumber: 999
+gidnumber: -1
 objectclass: top
 objectclass: ipaobject
 objectclass: posixgroup
diff --git a/install/share/dna.ldif b/install/share/dna.ldif
index ee927fcc5ba0aa5b49cf79964359e9dffe89ee5b..86be44ccfaf65d2ea09c51a499271b95ed7fdbc3 100644
--- a/install/share/dna.ldif
+++ b/install/share/dna.ldif
@@ -9,7 +9,7 @@ dnaType: uidNumber
 dnaType: gidNumber
 dnaNextValue: eval($IDSTART)
 dnaMaxValue: eval($IDMAX)
-dnaMagicRegen: 999
+dnaMagicRegen: -1
 dnaFilter: (|(objectClass=posixAccount)(objectClass=posixGroup)(objectClass=ipaIDobject))
 dnaScope: $SUFFIX
 dnaThreshold: 500
diff --git a/install/updates/20-dna.update b/install/updates/20-dna.update
index b83a3703dc9a7d1d78e7ffa5e72431d68045e9a9..04047dd12787e589953e4f938a03d868de3ae93e 100644
--- a/install/updates/20-dna.update
+++ b/install/updates/20-dna.update
@@ -1,3 +1,13 @@
 # Enable the DNA plugin
 dn: cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
 only:nsslapd-pluginEnabled: on
+
+# Change the magic value to -1
+dn: cn=Posix IDs,cn=Distributed Numeric Assignment Plugin,cn=plugins,cn=config
+only:dnaMagicRegen: -1
+
+dn: cn=ipa-winsync,cn=plugins,cn=config
+remove:ipaWinSyncUserAttr: uidNumber 999
+remove:ipaWinSyncUserAttr: gidNumber 999
+add:ipaWinSyncUserAttr: uidNumber -1
+add:ipaWinSyncUserAttr: gidNumber -1
diff --git a/ipalib/capabilities.py b/ipalib/capabilities.py
index 751b93e2b7060ef58380f7de648dc715a1f44385..6fcc436d5e00a58430c76aac6b3ebaf976ea23a8 100644
--- a/ipalib/capabilities.py
+++ b/ipalib/capabilities.py
@@ -35,6 +35,12 @@ capabilities = dict(
     # http://freeipa.org/page/V3/Messages
     messages=u'2.52',
 
+    # optional_uid_params: Before this version, UID & GID parameter defaults
+    # were 999, which meant "assign dynamically", so was not possible to get
+    # a user with UID=999. With the capability, these parameters are optional
+    # and 999 really means 999.
+    # https://fedorahosted.org/freeipa/ticket/2886
+    optional_uid_params=u'2.54'
 )
 
 
diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index 85e2bec36b011fd4539dfadc8d97535bb157bca7..29ea33e94ff78c243cb19e6bcbc0f1b173506924 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -36,6 +36,8 @@ from ipalib.text import _
 from ipalib.util import json_serialize, validate_hostname
 from ipapython.dn import DN, RDN
 
+DNA_MAGIC = -1
+
 global_output_params = (
     Flag('has_password',
         label=_('Password'),
diff --git a/ipalib/plugins/group.py b/ipalib/plugins/group.py
index 06e80931a0d77beb93b08cdf2637e3c750c1bafa..e27acc6b04d6a9443a2a383db2476da9c60d3771 100644
--- a/ipalib/plugins/group.py
+++ b/ipalib/plugins/group.py
@@ -21,6 +21,7 @@
 from ipalib import api
 from ipalib import Int, Str
 from ipalib.plugins.baseldap import *
+from ipalib.plugins import baseldap
 from ipalib import _, ngettext
 if api.env.in_server and api.env.context in ['lite', 'server']:
     try:
@@ -202,7 +203,7 @@ class group_add(LDAPCreate):
         elif not options['nonposix']:
             entry_attrs['objectclass'].append('posixgroup')
             if not 'gidnumber' in options:
-                entry_attrs['gidnumber'] = 999
+                entry_attrs['gidnumber'] = baseldap.DNA_MAGIC
         return dn
 
 
@@ -281,7 +282,7 @@ class group_mod(LDAPUpdate):
                 old_entry_attrs['objectclass'].append('posixgroup')
                 entry_attrs['objectclass'] = old_entry_attrs['objectclass']
                 if not 'gidnumber' in options:
-                    entry_attrs['gidnumber'] = 999
+                    entry_attrs['gidnumber'] = baseldap.DNA_MAGIC
 
         if options['external']:
             if is_protected_group:
diff --git a/ipalib/plugins/user.py b/ipalib/plugins/user.py
index 80bdc39e256b293ccef397c899327b7aa96df465..8c52fec52b55ced20bb7d1aec5f143fa59e7298e 100644
--- a/ipalib/plugins/user.py
+++ b/ipalib/plugins/user.py
@@ -18,23 +18,25 @@
 # 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 time import gmtime, strftime, strptime
+from time import gmtime, strftime
 import string
+import posixpath
+import os
 
 from ipalib import api, errors
-from ipalib import Flag, Int, Password, Str, Bool, Bytes
+from ipalib import Flag, Int, Password, Str, Bool
 from ipalib.plugins.baseldap import *
+from ipalib.plugins import baseldap
 from ipalib.request import context
 from ipalib import _, ngettext
 from ipalib import output
 from ipapython.ipautil import ipa_generate_password
 from ipapython.ipavalidate import Email
-import posixpath
+from ipalib.capabilities import client_has_capability
 from ipalib.util import (normalize_sshpubkey, validate_sshpubkey,
     convert_sshpubkey_post)
 if api.env.in_server and api.env.context in ['lite', 'server']:
     from ipaserver.plugins.ldap2 import ldap2
-    import os
 
 __doc__ = _("""
 Users
@@ -81,7 +83,6 @@ EXAMPLES:
 
 
 NO_UPG_MAGIC = '__no_upg__'
-DNA_MAGIC = 999
 
 user_output_params = (
     Flag('has_keytab',
@@ -300,20 +301,16 @@ class user(LDAPObject):
             label=_('Random password'),
             flags=('no_create', 'no_update', 'no_search', 'virtual_attribute'),
         ),
-        Int('uidnumber',
+        Int('uidnumber?',
             cli_name='uid',
             label=_('UID'),
             doc=_('User ID Number (system will assign one if not provided)'),
-            autofill=True,
-            default=DNA_MAGIC,
             minvalue=1,
         ),
-        Int('gidnumber',
+        Int('gidnumber?',
             label=_('GID'),
             doc=_('Group ID Number'),
             minvalue=1,
-            default=DNA_MAGIC,
-            autofill=True,
         ),
         Str('street?',
             cli_name='street',
@@ -468,6 +465,19 @@ class user_add(LDAPCreate):
             entry_attrs.setdefault('description', [])
             entry_attrs['description'].append(NO_UPG_MAGIC)
 
+        entry_attrs.setdefault('uidnumber', baseldap.DNA_MAGIC)
+
+        if not client_has_capability(
+                options['version'], 'optional_uid_params'):
+            # https://fedorahosted.org/freeipa/ticket/2886
+            # Old clients say 999 (OLD_DNA_MAGIC) when they really mean
+            # "assign a value dynamically".
+            OLD_DNA_MAGIC = 999
+            if entry_attrs.get('uidnumber') == OLD_DNA_MAGIC:
+                entry_attrs['uidnumber'] = baseldap.DNA_MAGIC
+            if entry_attrs.get('gidnumber') == OLD_DNA_MAGIC:
+                entry_attrs['gidnumber'] = baseldap.DNA_MAGIC
+
         validate_nsaccountlock(entry_attrs)
         config = ldap.get_ipa_config()[1]
         if 'ipamaxusernamelength' in config:
@@ -493,7 +503,7 @@ class user_add(LDAPCreate):
                                   api.env.basedn))
         entry_attrs.setdefault('krbprincipalname', '%s@%s' % (entry_attrs['uid'], api.env.realm))
 
-        if entry_attrs.get('gidnumber', DNA_MAGIC) == DNA_MAGIC:
+        if entry_attrs.get('gidnumber') is None:
             # gidNumber wasn't specified explicity, find out what it should be
             if not options.get('noprivate', False) and ldap.has_upg():
                 # User Private Groups - uidNumber == gidNumber
diff --git a/tests/test_install/1_add.update b/tests/test_install/1_add.update
index ecb419513c39185c72dfb367e02d8cf9c91bed2e..2543a71f202918173c07f4d41caeb87823405480 100644
--- a/tests/test_install/1_add.update
+++ b/tests/test_install/1_add.update
@@ -16,7 +16,7 @@ add:homedirectory: /home/tuser
 add:loginshell: /bin/bash
 add:sn: User
 add:uid: tuser
-add:uidnumber: 999
-add:gidnumber: 999
+add:uidnumber: -1
+add:gidnumber: -1
 add:cn: Test User
 
diff --git a/tests/test_xmlrpc/test_user_plugin.py b/tests/test_xmlrpc/test_user_plugin.py
index a61db23d501c4f25e860a7cc1dabfe3b01ddffcf..7e992224c66bb4d780f295b9ff815f5b9851ef70 100644
--- a/tests/test_xmlrpc/test_user_plugin.py
+++ b/tests/test_xmlrpc/test_user_plugin.py
@@ -1748,4 +1748,90 @@ class test_user(Declarative):
                 ),
             ),
         ),
+
+        dict(
+            desc='Create "%s" with UID 999' % user1,
+            command=(
+                'user_add', [user1], dict(
+                    givenname=u'Test', sn=u'User1', uidnumber=999)
+            ),
+            expected=dict(
+                value=user1,
+                summary=u'Added user "%s"' % user1,
+                result=dict(
+                    gecos=[u'Test User1'],
+                    givenname=[u'Test'],
+                    homedirectory=[u'/home/tuser1'],
+                    krbprincipalname=[u'tuser1@' + api.env.realm],
+                    loginshell=[u'/bin/sh'],
+                    objectclass=objectclasses.user,
+                    sn=[u'User1'],
+                    uid=[user1],
+                    uidnumber=[u'999'],
+                    gidnumber=[u'999'],
+                    displayname=[u'Test User1'],
+                    cn=[u'Test User1'],
+                    mail=[u'%s@%s' % (user1, api.env.domain)],
+                    initials=[u'TU'],
+                    ipauniqueid=[fuzzy_uuid],
+                    krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+                                              ('cn','kerberos'),api.env.basedn)],
+                    mepmanagedentry=[get_group_dn(user1)],
+                    memberof_group=[u'ipausers'],
+                    has_keytab=False,
+                    has_password=False,
+                    dn=get_user_dn(user1),
+                ),
+            ),
+            extra_check = upg_check,
+        ),
+
+        dict(
+            desc='Delete "%s"' % user1,
+            command=('user_del', [user1], {}),
+            expected=dict(
+                result=dict(failed=u''),
+                summary=u'Deleted user "%s"' % user1,
+                value=user1,
+            ),
+        ),
+
+        dict(
+            desc='Create "%s" with old DNA_MAGIC uid 999' % user1,
+            command=(
+                'user_add', [user1], dict(
+                    givenname=u'Test', sn=u'User1', uidnumber=999,
+                    version=u'2.49')
+            ),
+            expected=dict(
+                value=user1,
+                summary=u'Added user "%s"' % user1,
+                result=dict(
+                    gecos=[u'Test User1'],
+                    givenname=[u'Test'],
+                    homedirectory=[u'/home/tuser1'],
+                    krbprincipalname=[u'tuser1@' + api.env.realm],
+                    loginshell=[u'/bin/sh'],
+                    objectclass=objectclasses.user,
+                    sn=[u'User1'],
+                    uid=[user1],
+                    uidnumber=[lambda v: int(v) != 999],
+                    gidnumber=[lambda v: int(v) != 999],
+                    displayname=[u'Test User1'],
+                    cn=[u'Test User1'],
+                    mail=[u'%s@%s' % (user1, api.env.domain)],
+                    initials=[u'TU'],
+                    ipauniqueid=[fuzzy_uuid],
+                    krbpwdpolicyreference=[DN(('cn','global_policy'),('cn',api.env.realm),
+                                              ('cn','kerberos'),api.env.basedn)],
+                    mepmanagedentry=[get_group_dn(user1)],
+                    memberof_group=[u'ipausers'],
+                    has_keytab=False,
+                    has_password=False,
+                    dn=get_user_dn(user1),
+                ),
+            ),
+            extra_check = upg_check,
+        ),
+
     ]
-- 
1.7.7.6

_______________________________________________
Freeipa-devel mailing list
Freeipa-devel@redhat.com
https://www.redhat.com/mailman/listinfo/freeipa-devel

Reply via email to