On 02.06.2016 15:03, Jan Cholasta wrote:
On 2.6.2016 14:39, Petr Spacek wrote:
On 2.6.2016 14:20, Jan Cholasta wrote:
On 2.6.2016 14:06, Petr Spacek wrote:
On 1.6.2016 18:00, Martin Basti wrote:
<snip>

updated patches attached

freeipa-mbasti-0473.6-DNS-Locations-Always-create-DNS-related-privileges.patch


From 549379a36281d80818fca4ec929d499efafda044 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 4 May 2016 17:33:52 +0200
Subject: [PATCH 1/4] DNS Locations: Always create DNS related privileges

DNS privileges are important for handling DNS locations which can be
created without DNS servers in IPA topology. We will also need this
privileges presented for future feature 'External DNS support'

https://fedorahosted.org/freeipa/ticket/2008
---
 install/share/delegation.ldif        | 16 ++++++++++++++++
 install/share/dns.ldif               | 16 ----------------
 install/updates/37-locations.update  |  0
 install/updates/40-delegation.update | 16 ++++++++++++++++
 4 files changed, 32 insertions(+), 16 deletions(-)
 create mode 100644 install/updates/37-locations.update

diff --git a/install/share/delegation.ldif b/install/share/delegation.ldif
index
067b4d26a8be8f4d1b699c15b027ed7f260ddb5b..064078306560528842fa76176152ac594db077c8
100644
--- a/install/share/delegation.ldif
+++ b/install/share/delegation.ldif
@@ -80,6 +80,22 @@ objectClass: nestedgroup
 cn: Delegation Administrator
 description: Role administration

+dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: nestedgroup
+cn: DNS Administrators
+description: DNS Administrators
+
+dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: nestedgroup
+cn: DNS Servers
+description: DNS Servers
+
 dn: cn=Service Administrators,cn=privileges,cn=pbac,$SUFFIX
 changetype: add
 objectClass: top
diff --git a/install/share/dns.ldif b/install/share/dns.ldif
index
bd5cc57f90ed66066699af06a74e1426cc8f9a59..6cee478674af191350cf24e0aef74c5e418f392e
100644
--- a/install/share/dns.ldif
+++ b/install/share/dns.ldif
@@ -12,19 +12,3 @@ aci: (targetattr = "*")(version 3.0; acl "Allow read
access"; allow (read,search
aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl
"Remove DNS entries from a zone";allow (delete) userattr =
"parent[1].managedby#GROUPDN";)
aci: (targetattr = "a6record || aaaarecord || afsdbrecord || aplrecord || arecord || certrecord || cn || cnamerecord || dhcidrecord || dlvrecord || dnamerecord || dnsclass || dnsttl || dsrecord || hinforecord || hiprecord
|| idnsallowdynupdate || idnsallowquery || idnsallowsyncptr ||
idnsallowtransfer || idnsforwarders || idnsforwardpolicy || idnsname || idnssecinlinesigning || idnssoaexpire || idnssoaminimum || idnssoamname ||
idnssoarefresh || idnssoaretry || idnssoarname || idnssoaserial ||
idnsupdatepolicy || idnszoneactive || ipseckeyrecord || keyrecord ||
kxrecord || locrecord || mdrecord || minforecord || mxrecord || naptrrecord || nsecrecord || nsec3paramrecord || nsrecord || nxtrecord || ptrrecord ||
rprecord || rrsigrecord || sigrecord || spfrecord || srvrecord ||
sshfprecord || tlsarecord || txtrecord || unknownrecord ")(target =
"ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update DNS entries in
a zone";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)
-
-dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
-changetype: add
-objectClass: top
-objectClass: groupofnames
-objectClass: nestedgroup
-cn: DNS Administrators
-description: DNS Administrators
-
-dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
-changetype: add
-objectClass: top
-objectClass: groupofnames
-objectClass: nestedgroup
-cn: DNS Servers
-description: DNS Servers
diff --git a/install/updates/37-locations.update
b/install/updates/37-locations.update
new file mode 100644
index
0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391

diff --git a/install/updates/40-delegation.update
b/install/updates/40-delegation.update
index
f0431b92d707b17607fe873efbfe2fcccd3efce1..259cbdbdab9eef69e29dba117db36a9e3e0c5f66
100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -274,3 +274,19 @@ default:objectClass: groupofnames
 default:objectClass: top
 default:cn: Vault Administrators
 default:description: Vault Administrators
+
+
+# Locations - always create DNS related privileges
+dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: nestedgroup
+default:cn: DNS Administrators
+default:description: DNS Administrators
+
+dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: nestedgroup
+default:cn: DNS Servers
+default:description: DNS Servers
-- 2.5.5


freeipa-mbasti-0474.6-DNS-Locations-add-new-attributes-and-objectclasses.patch


From 4363fd4823efcf173f9cc6b56769771bf7867170 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 12 May 2016 10:53:37 +0200
Subject: [PATCH 2/4] DNS Locations: add new attributes and objectclasses

http://www.freeipa.org/page/V4/DNS_Location_Mechanism

https://fedorahosted.org/freeipa/ticket/2008
---
 install/share/60ipadns.ldif | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif
index
71b99d4d03c34591dc83a5706d300727f3f77f30..5bfed905566bdbfe4e011e218c328701ce854943
100644
--- a/install/share/60ipadns.ldif
+++ b/install/share/60ipadns.ldif
@@ -71,6 +71,8 @@ attributeTypes: ( 2.16.840.1.113730.3.8.5.26 NAME
'idnsSecKeySep' DESC 'DNSKEY S
attributeTypes: ( 2.16.840.1.113730.3.8.5.27 NAME 'idnsSecAlgorithm' DESC 'DNSKEY algorithm: string used as mnemonic' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26
SINGLE-VALUE X-ORIGIN 'IPA v4.1' )
attributeTypes: ( 2.16.840.1.113730.3.8.5.28 NAME 'idnsSecKeyRef' DESC
'PKCS#11 URI of the key' EQUALITY caseExactMatch SINGLE-VALUE SYNTAX
1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.1' )
attributeTypes: ( 2.16.840.1.113730.3.8.11.74 NAME 'ipaDNSVersion' DESC 'IPA DNS data version' EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'IPA v4.3' )
+attributeTypes: ( 2.16.840.1.113730.3.8.5.32 NAME 'ipaLocation' DESC
'Reference to IPA location' EQUALITY distinguishedNameMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
+attributeTypes: ( 2.16.840.1.113730.3.8.5.33 NAME 'ipaLocationWeight' DESC
'Weight for the server in IPA location' EQUALITY integerMatch SYNTAX
1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
objectClasses: ( 2.16.840.1.113730.3.8.6.0 NAME 'idnsRecord' DESC 'dns
Record, usually a host' SUP top STRUCTURAL MUST idnsName MAY ( cn $
idnsAllowDynUpdate $ dNSTTL $ dNSClass $ aRecord $ aAAARecord $ a6Record $ nSRecord $ cNAMERecord $ pTRRecord $ sRVRecord $ tXTRecord $ mXRecord $ mDRecord $ hInfoRecord $ mInfoRecord $ aFSDBRecord $ SigRecord $ KeyRecord $ LocRecord $ nXTRecord $ nAPTRRecord $ kXRecord $ certRecord $ dNameRecord
$ dSRecord $ sSHFPRecord $ rRSIGRecord $ nSECRecord $ DLVRecord $
TLSARecord $ UnknownRecord $ RPRecord $ APLRecord $ IPSECKEYRecord $
DHCIDRecord $ HIPRecord $ SPFRecord ) )
objectClasses: ( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord STRUCTURAL MUST ( idnsZoneActive $ idnsSOAmName $
idnsSOArName $ idnsSOAserial $ idnsSOArefresh $ idnsSOAretry $
idnsSOAexpire $ idnsSOAminimum ) MAY ( idnsUpdatePolicy $ idnsAllowQuery $ idnsAllowTransfer $ idnsAllowSyncPTR $ idnsForwardPolicy $ idnsForwarders $
idnsSecInlineSigning $ nSEC3PARAMRecord ) )
objectClasses: ( 2.16.840.1.113730.3.8.6.2 NAME 'idnsConfigObject' DESC
'DNS global config options' STRUCTURAL MAY ( idnsForwardPolicy $
idnsForwarders $ idnsAllowSyncPTR $ idnsZoneRefresh $ idnsPersistentSearch ) )
@@ -78,3 +80,5 @@ objectClasses: ( 2.16.840.1.113730.3.8.12.18 NAME
'ipaDNSZone' SUP top AUXILIARY
objectClasses: ( 2.16.840.1.113730.3.8.6.3 NAME 'idnsForwardZone' DESC 'Forward Zone class' SUP top STRUCTURAL MUST ( idnsName $ idnsZoneActive )
MAY ( idnsForwarders $ idnsForwardPolicy ) )
objectClasses: ( 2.16.840.1.113730.3.8.6.4 NAME 'idnsSecKey' DESC 'DNSSEC
key metadata' STRUCTURAL MUST ( idnsSecKeyRef $ idnsSecKeyCreated $
idnsSecAlgorithm ) MAY ( idnsSecKeyPublish $ idnsSecKeyActivate $
idnsSecKeyInactive $ idnsSecKeyDelete $ idnsSecKeyZone $ idnsSecKeyRevoke $
idnsSecKeySep $ cn ) X-ORIGIN 'IPA v4.1' )
objectClasses: ( 2.16.840.1.113730.3.8.12.36 NAME 'ipaDNSContainer' DESC 'IPA DNS container' AUXILIARY MUST ( ipaDNSVersion ) X-ORIGIN 'IPA v4.3' ) +objectClasses: ( 2.16.840.1.113730.3.8.6.7 NAME 'ipaLocationObject' DESC 'Object for storing IPA server location' STRUCTURAL MUST ( idnsName ) MAY (
description ) X-ORIGIN 'IPA v4.4' )
+objectClasses: ( 2.16.840.1.113730.3.8.6.8 NAME 'ipaLocationMember' DESC
'Member object of IPA location' AUXILIARY MAY ( ipaLocation $
ipaLocationWeight ) X-ORIGIN 'IPA v4.4' )
-- 2.5.5


freeipa-mbasti-0475.6-DNS-Locations-location-commands.patch


From c353f0ecbb0e97d9ff28e38ddea27168e69f9ac5 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 12 May 2016 10:54:20 +0200
Subject: [PATCH 3/4] DNS Locations: location-* commands

http://www.freeipa.org/page/V4/DNS_Location_Mechanism

https://fedorahosted.org/freeipa/ticket/2008
---
 ACI.txt                               |   8 ++
 API.txt                               |  59 ++++++++++++++
 VERSION                               |   4 +-
 install/share/bootstrap-template.ldif |   6 ++
 install/updates/37-locations.update   |   4 +
 install/updates/Makefile.am           |   1 +
 ipalib/constants.py                   |   1 +
 ipalib/plugins/location.py            | 149
++++++++++++++++++++++++++++++++++
 8 files changed, 230 insertions(+), 2 deletions(-)
 create mode 100644 ipalib/plugins/location.py

diff --git a/ACI.txt b/ACI.txt
index
cea814a0ceb7aea48b709236f0f88677e851ac92..2226eccc74ec6d25c1f6fcc93f3e1c7d636b8146
100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -158,6 +158,14 @@ dn: cn=IPA.EXAMPLE,cn=kerberos,dc=ipa,dc=example
aci: (targetattr = "createtimestamp || entryusn || krbdefaultencsalttypes || krbmaxrenewableage || krbmaxticketlife || krbsupportedencsalttypes ||
modifytimestamp || objectclass")(targetfilter =
"(objectclass=krbticketpolicyaux)")(version 3.0;acl "permission:System: Read Default Kerberos Ticket Policy";allow (compare,read,search) groupdn =
"ldap:///cn=System: Read Default Kerberos Ticket
Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
aci: (targetattr = "krbmaxrenewableage || krbmaxticketlife")(targetfilter = "(objectclass=krbticketpolicyaux)")(version 3.0;acl "permission:System: Read User Kerberos Ticket Policy";allow (compare,read,search) groupdn =
"ldap:///cn=System: Read User Kerberos Ticket
Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl
"permission:System: Add IPA Locations";allow (add) groupdn =
"ldap:///cn=System: Add IPA
Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "description")(targetfilter =
"(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Modify IPA Locations";allow (write) groupdn = "ldap:///cn=System: Modify
IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "createtimestamp || description || entryusn || idnsname
|| modifytimestamp || objectclass")(targetfilter =
"(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Read IPA Locations";allow (compare,read,search) groupdn = "ldap:///cn=System:
Read IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl
"permission:System: Remove IPA Locations";allow (delete) groupdn =
"ldap:///cn=System: Remove IPA
Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ng,cn=alt,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipanisnetgroup)")(version 3.0;acl
"permission:System: Add Netgroups";allow (add) groupdn =
"ldap:///cn=System: Add Netgroups,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ng,cn=alt,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index
3ad250e74f48ef3c54494ba6bd2d398a7c5d1b69..0568a6573236ca25c7b2353832f949c95b353758
100644
--- a/API.txt
+++ b/API.txt
@@ -2759,6 +2759,65 @@ option: Str('version?')
 output: Entry('result')
output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: location_add
+args: 1,6,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: location_del
+args: 1,2,3
+arg: DNSNameParam('idnsname+', cli_name='name')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: ListOfPrimaryKeys('value')
+command: location_find
+args: 1,8,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?', autofill=False)
+option: DNSNameParam('idnsname?', autofill=False, cli_name='name')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Int('sizelimit?', autofill=False)
+option: Int('timelimit?', autofill=False)
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
+command: location_mod
+args: 1,8,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: location_show
+args: 1,4,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: migrate_ds
 args: 2,20,4
 arg: Str('ldapuri', cli_name='ldap_uri')
diff --git a/VERSION b/VERSION
index
45fdb09788dbc6496272da786bb6d6afa45bf118..03908580e3008b5011588588ad41083310d24095
100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=170
-# Last change: mbasti - *-find: do not search for members by default
+IPA_API_VERSION_MINOR=171
+# Last change: mbasti - location-* commands
diff --git a/install/share/bootstrap-template.ldif
b/install/share/bootstrap-template.ldif
index
628a8e2e0f5483b9f6f565b0c7d11eb000a5912d..83be4399508a905f8eae7e2f59140a6b4051b661
100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -119,6 +119,12 @@ objectClass: nsContainer
 objectClass: top
 cn: etc

+dn: cn=locations,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: locations
+
 dn: cn=sysaccounts,cn=etc,$SUFFIX
 changetype: add
 objectClass: nsContainer
diff --git a/install/updates/37-locations.update
b/install/updates/37-locations.update
index
e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cf47e6d6296af830a76aad2c9b9f5a6ea5d9f3a1
100644
--- a/install/updates/37-locations.update
+++ b/install/updates/37-locations.update
@@ -0,0 +1,4 @@
+dn: cn=locations,cn=etc,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: locations
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index
3edc21473d676bd282e9ea2b88769c097fb8a63a..737a8bbbd1a4915a6aefec2d273b90bb3ca31710
100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -28,6 +28,7 @@ app_DATA =                \
     25-referint.update        \
     30-provisioning.update        \
     30-s4u2proxy.update        \
+    37-locations.update        \
     40-delegation.update        \
     40-realm_domains.update        \
     40-replication.update        \
diff --git a/ipalib/constants.py b/ipalib/constants.py
index
021f18cd366b821427bdbfcc5e354d2047ef39b1..d1c9ccf68d01ef1dc032559ca8a353eede7a0e09
100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -121,6 +121,7 @@ DEFAULT_CONFIG = (
('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))), ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn',
'etc'))),
     ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
+    ('container_locations', DN(('cn', 'locations'), ('cn', 'etc'))),

     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/location.py b/ipalib/plugins/location.py
new file mode 100644
index
0000000000000000000000000000000000000000..efba55aa75e342f566a40a0d10887e173b8a83fc

--- /dev/null
+++ b/ipalib/plugins/location.py
@@ -0,0 +1,149 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from __future__ import absolute_import
+
+from ipalib import (
+    _,
+    ngettext,
+    api,
+    Str,
+    DNSNameParam
+)
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import (
+    LDAPCreate,
+    LDAPSearch,
+    LDAPRetrieve,
+    LDAPDelete,
+    LDAPObject,
+    LDAPUpdate,
+)
+from ipapython.dnsutil import DNSName
+
+__doc__ = _("""
+IPA locations
+""") + _("""
+Manipulate DNS locations
+""") + _("""
+EXAMPLES:
+""") + _("""
+  Find all locations:
+    ipa location-find
+""") + _("""
+  Show specific location:
+    ipa location-show location
+""") + _("""
+  Add location:
+    ipa location-add location --description 'My location'
+""") + _("""
+  Delete location:
+    ipa location-del location
+""")
+
+register = Registry()
+
+
+@register()
+class location(LDAPObject):
+    """
+    IPA locations
+    """
+    container_dn = api.env.container_locations
+    object_name = _('location')
+    object_name_plural = _('locations')
+    object_class = ['ipaLocationObject']
+    search_attributes = ['idnsName']
+    default_attributes = [
+        'idnsname', 'description'
+    ]
+    label = _('IPA Locations')
+    label_singular = _('IPA Location')
+
+    permission_filter_objectclasses = ['ipaLocationObject']
+    managed_permissions = {
+        'System: Read IPA Locations': {
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'idnsname', 'description',
+            },
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Add IPA Locations': {
+            'ipapermright': {'add'},
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Remove IPA Locations': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Modify IPA Locations': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'description',
+            },
+            'default_privileges': {'DNS Administrators'},
+        },
+    }
+
+    takes_params = (
+        DNSNameParam(
+            'idnsname',
+            cli_name='name',
+            primary_key=True,
+            label=_('Location name'),
+            doc=_('IPA location name'),
+ # dns name must be relative, we will put it into middle of
+            # location domain name for location records
+            only_relative=True,
+        ),
+        Str(
+            'description?',
+            label=_('Description'),
+            doc=_('IPA Location description'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        loc = keys[-1]
+        assert isinstance(loc, DNSName)
+        loc_a = loc.ToASCII()
+
+        return super(location, self).get_dn(loc_a, **options)
+
+
+@register()
+class location_add(LDAPCreate):
+    __doc__ = _('Add a new IPA location.')
+
+    msg_summary = _('Added IPA location "%(value)s"')
+
+
+@register()
+class location_del(LDAPDelete):
+    __doc__ = _('Delete an IPA location.')
+
+    msg_summary = _('Deleted IPA location "%(value)s"')
+
+
+@register()
+class location_mod(LDAPUpdate):
+    __doc__ = _('Modify information about an IPA location .')

Typo - redundant ' ' at the end.


Conditional NACK, warnings mentioned in
http://www.freeipa.org/page/V4/DNS_Location_Mechanism#CLI
are not there.

I'm open to changing this to ACK if you open a separate ticket for this
omission so we do not forget to add them later on.

+1

Done


Patch 480:

1) The code in location_show.execute() looks like it could be moved to
location_show.post_callback()

I had to add it to execute because I modifies result entry not just entry_attrs


2) Before calling super().output_for_cli(), pop 'servers' from result, so that
it is not displayed with --all.


Done

Patch 481:

1) Could we rename --force to --nonempty (or something better)? I would like to reserve --force for "ignore NotFound when deleting the entry", which is not
the case here.

IMHO option is unnecessary. Just delete the location (and unset location from
all member servers). The design does not contain --force anyway :-)

OK, that's even better :-)

Done

Updated patches attached
From dd7636715a6a44a62d96b48f107f0611d3ca624a Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 4 May 2016 17:33:52 +0200
Subject: [PATCH 1/4] DNS Locations: Always create DNS related privileges

DNS privileges are important for handling DNS locations which can be
created without DNS servers in IPA topology. We will also need this
privileges presented for future feature 'External DNS support'

https://fedorahosted.org/freeipa/ticket/2008
---
 install/share/delegation.ldif        | 16 ++++++++++++++++
 install/share/dns.ldif               | 16 ----------------
 install/updates/37-locations.update  |  0
 install/updates/40-delegation.update | 16 ++++++++++++++++
 4 files changed, 32 insertions(+), 16 deletions(-)
 create mode 100644 install/updates/37-locations.update

diff --git a/install/share/delegation.ldif b/install/share/delegation.ldif
index 067b4d26a8be8f4d1b699c15b027ed7f260ddb5b..064078306560528842fa76176152ac594db077c8 100644
--- a/install/share/delegation.ldif
+++ b/install/share/delegation.ldif
@@ -80,6 +80,22 @@ objectClass: nestedgroup
 cn: Delegation Administrator
 description: Role administration
 
+dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: nestedgroup
+cn: DNS Administrators
+description: DNS Administrators
+
+dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
+changetype: add
+objectClass: top
+objectClass: groupofnames
+objectClass: nestedgroup
+cn: DNS Servers
+description: DNS Servers
+
 dn: cn=Service Administrators,cn=privileges,cn=pbac,$SUFFIX
 changetype: add
 objectClass: top
diff --git a/install/share/dns.ldif b/install/share/dns.ldif
index bd5cc57f90ed66066699af06a74e1426cc8f9a59..6cee478674af191350cf24e0aef74c5e418f392e 100644
--- a/install/share/dns.ldif
+++ b/install/share/dns.ldif
@@ -12,19 +12,3 @@ aci: (targetattr = "*")(version 3.0; acl "Allow read access"; allow (read,search
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Add DNS entries in a zone";allow (add) userattr = "parent[1].managedby#GROUPDN";)
 aci: (target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Remove DNS entries from a zone";allow (delete) userattr = "parent[1].managedby#GROUPDN";)
 aci: (targetattr = "a6record || aaaarecord || afsdbrecord || aplrecord || arecord || certrecord || cn || cnamerecord || dhcidrecord || dlvrecord || dnamerecord || dnsclass || dnsttl || dsrecord || hinforecord || hiprecord || idnsallowdynupdate || idnsallowquery || idnsallowsyncptr || idnsallowtransfer || idnsforwarders || idnsforwardpolicy || idnsname || idnssecinlinesigning || idnssoaexpire || idnssoaminimum || idnssoamname || idnssoarefresh || idnssoaretry || idnssoarname || idnssoaserial || idnsupdatepolicy || idnszoneactive || ipseckeyrecord || keyrecord || kxrecord || locrecord || mdrecord || minforecord || mxrecord || naptrrecord || nsecrecord || nsec3paramrecord || nsrecord || nxtrecord || ptrrecord || rprecord || rrsigrecord || sigrecord || spfrecord || srvrecord || sshfprecord || tlsarecord || txtrecord || unknownrecord ")(target = "ldap:///idnsname=*,cn=dns,$SUFFIX";)(version 3.0;acl "Update DNS entries in a zone";allow (write) userattr = "parent[0,1].managedby#GROUPDN";)
-
-dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
-changetype: add
-objectClass: top
-objectClass: groupofnames
-objectClass: nestedgroup
-cn: DNS Administrators
-description: DNS Administrators
-
-dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
-changetype: add
-objectClass: top
-objectClass: groupofnames
-objectClass: nestedgroup
-cn: DNS Servers
-description: DNS Servers
diff --git a/install/updates/37-locations.update b/install/updates/37-locations.update
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/install/updates/40-delegation.update b/install/updates/40-delegation.update
index f0431b92d707b17607fe873efbfe2fcccd3efce1..259cbdbdab9eef69e29dba117db36a9e3e0c5f66 100644
--- a/install/updates/40-delegation.update
+++ b/install/updates/40-delegation.update
@@ -274,3 +274,19 @@ default:objectClass: groupofnames
 default:objectClass: top
 default:cn: Vault Administrators
 default:description: Vault Administrators
+
+
+# Locations - always create DNS related privileges
+dn: cn=DNS Administrators,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: nestedgroup
+default:cn: DNS Administrators
+default:description: DNS Administrators
+
+dn: cn=DNS Servers,cn=privileges,cn=pbac,$SUFFIX
+default:objectClass: top
+default:objectClass: groupofnames
+default:objectClass: nestedgroup
+default:cn: DNS Servers
+default:description: DNS Servers
-- 
2.5.5

From 5ae3785faea9d02214de651f6a7b21da422b436e Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 12 May 2016 10:53:37 +0200
Subject: [PATCH 2/4] DNS Locations: add new attributes and objectclasses

http://www.freeipa.org/page/V4/DNS_Location_Mechanism

https://fedorahosted.org/freeipa/ticket/2008
---
 install/share/60ipadns.ldif | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/install/share/60ipadns.ldif b/install/share/60ipadns.ldif
index 71b99d4d03c34591dc83a5706d300727f3f77f30..5bfed905566bdbfe4e011e218c328701ce854943 100644
--- a/install/share/60ipadns.ldif
+++ b/install/share/60ipadns.ldif
@@ -71,6 +71,8 @@ attributeTypes: ( 2.16.840.1.113730.3.8.5.26 NAME 'idnsSecKeySep' DESC 'DNSKEY S
 attributeTypes: ( 2.16.840.1.113730.3.8.5.27 NAME 'idnsSecAlgorithm' DESC 'DNSKEY algorithm: string used as mnemonic' EQUALITY caseIgnoreIA5Match SUBSTR caseIgnoreIA5SubstringsMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.26 SINGLE-VALUE X-ORIGIN 'IPA v4.1' )
 attributeTypes: ( 2.16.840.1.113730.3.8.5.28 NAME 'idnsSecKeyRef' DESC 'PKCS#11 URI of the key' EQUALITY caseExactMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.15 X-ORIGIN 'IPA v4.1' )
 attributeTypes: ( 2.16.840.1.113730.3.8.11.74 NAME 'ipaDNSVersion' DESC 'IPA DNS data version' EQUALITY integerMatch ORDERING integerOrderingMatch SINGLE-VALUE SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 X-ORIGIN 'IPA v4.3' )
+attributeTypes: ( 2.16.840.1.113730.3.8.5.32 NAME 'ipaLocation' DESC 'Reference to IPA location' EQUALITY distinguishedNameMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.12 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
+attributeTypes: ( 2.16.840.1.113730.3.8.5.33 NAME 'ipaLocationWeight' DESC 'Weight for the server in IPA location' EQUALITY integerMatch SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE X-ORIGIN 'IPA v4.4' )
 objectClasses: ( 2.16.840.1.113730.3.8.6.0 NAME 'idnsRecord' DESC 'dns Record, usually a host' SUP top STRUCTURAL MUST idnsName MAY ( cn $ idnsAllowDynUpdate $ dNSTTL $ dNSClass $ aRecord $ aAAARecord $ a6Record $ nSRecord $ cNAMERecord $ pTRRecord $ sRVRecord $ tXTRecord $ mXRecord $ mDRecord $ hInfoRecord $ mInfoRecord $ aFSDBRecord $ SigRecord $ KeyRecord $ LocRecord $ nXTRecord $ nAPTRRecord $ kXRecord $ certRecord $ dNameRecord $ dSRecord $ sSHFPRecord $ rRSIGRecord $ nSECRecord $ DLVRecord $ TLSARecord $ UnknownRecord $ RPRecord $ APLRecord $ IPSECKEYRecord $ DHCIDRecord $ HIPRecord $ SPFRecord ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.1 NAME 'idnsZone' DESC 'Zone class' SUP idnsRecord STRUCTURAL MUST ( idnsZoneActive $ idnsSOAmName $ idnsSOArName $ idnsSOAserial $ idnsSOArefresh $ idnsSOAretry $ idnsSOAexpire $ idnsSOAminimum ) MAY ( idnsUpdatePolicy $ idnsAllowQuery $ idnsAllowTransfer $ idnsAllowSyncPTR $ idnsForwardPolicy $ idnsForwarders $ idnsSecInlineSigning $ nSEC3PARAMRecord ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.2 NAME 'idnsConfigObject' DESC 'DNS global config options' STRUCTURAL MAY ( idnsForwardPolicy $ idnsForwarders $ idnsAllowSyncPTR $ idnsZoneRefresh $ idnsPersistentSearch ) )
@@ -78,3 +80,5 @@ objectClasses: ( 2.16.840.1.113730.3.8.12.18 NAME 'ipaDNSZone' SUP top AUXILIARY
 objectClasses: ( 2.16.840.1.113730.3.8.6.3 NAME 'idnsForwardZone' DESC 'Forward Zone class' SUP top STRUCTURAL MUST ( idnsName $ idnsZoneActive ) MAY ( idnsForwarders $ idnsForwardPolicy ) )
 objectClasses: ( 2.16.840.1.113730.3.8.6.4 NAME 'idnsSecKey' DESC 'DNSSEC key metadata' STRUCTURAL MUST ( idnsSecKeyRef $ idnsSecKeyCreated $ idnsSecAlgorithm ) MAY ( idnsSecKeyPublish $ idnsSecKeyActivate $ idnsSecKeyInactive $ idnsSecKeyDelete $ idnsSecKeyZone $ idnsSecKeyRevoke $ idnsSecKeySep $ cn ) X-ORIGIN 'IPA v4.1' )
 objectClasses: ( 2.16.840.1.113730.3.8.12.36 NAME 'ipaDNSContainer' DESC 'IPA DNS container' AUXILIARY MUST ( ipaDNSVersion ) X-ORIGIN 'IPA v4.3' )
+objectClasses: ( 2.16.840.1.113730.3.8.6.7 NAME 'ipaLocationObject' DESC 'Object for storing IPA server location' STRUCTURAL MUST ( idnsName ) MAY ( description ) X-ORIGIN 'IPA v4.4' )
+objectClasses: ( 2.16.840.1.113730.3.8.6.8 NAME 'ipaLocationMember' DESC 'Member object of IPA location' AUXILIARY MAY ( ipaLocation $ ipaLocationWeight ) X-ORIGIN 'IPA v4.4' )
-- 
2.5.5

From a6dbd57f9a1d8a972f2a07033ebd539f7ae28c1d Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 12 May 2016 10:54:20 +0200
Subject: [PATCH 3/4] DNS Locations: location-* commands

http://www.freeipa.org/page/V4/DNS_Location_Mechanism

https://fedorahosted.org/freeipa/ticket/2008
---
 ACI.txt                               |   8 ++
 API.txt                               |  59 ++++++++++++++
 VERSION                               |   4 +-
 install/share/bootstrap-template.ldif |   6 ++
 install/updates/37-locations.update   |   4 +
 install/updates/Makefile.am           |   1 +
 ipalib/constants.py                   |   1 +
 ipalib/plugins/location.py            | 149 ++++++++++++++++++++++++++++++++++
 8 files changed, 230 insertions(+), 2 deletions(-)
 create mode 100644 ipalib/plugins/location.py

diff --git a/ACI.txt b/ACI.txt
index cea814a0ceb7aea48b709236f0f88677e851ac92..2226eccc74ec6d25c1f6fcc93f3e1c7d636b8146 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -158,6 +158,14 @@ dn: cn=IPA.EXAMPLE,cn=kerberos,dc=ipa,dc=example
 aci: (targetattr = "createtimestamp || entryusn || krbdefaultencsalttypes || krbmaxrenewableage || krbmaxticketlife || krbsupportedencsalttypes || modifytimestamp || objectclass")(targetfilter = "(objectclass=krbticketpolicyaux)")(version 3.0;acl "permission:System: Read Default Kerberos Ticket Policy";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Default Kerberos Ticket Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=users,cn=accounts,dc=ipa,dc=example
 aci: (targetattr = "krbmaxrenewableage || krbmaxticketlife")(targetfilter = "(objectclass=krbticketpolicyaux)")(version 3.0;acl "permission:System: Read User Kerberos Ticket Policy";allow (compare,read,search) groupdn = "ldap:///cn=System: Read User Kerberos Ticket Policy,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Add IPA Locations";allow (add) groupdn = "ldap:///cn=System: Add IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "description")(targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Modify IPA Locations";allow (write) groupdn = "ldap:///cn=System: Modify IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "createtimestamp || description || entryusn || idnsname || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Read IPA Locations";allow (compare,read,search) groupdn = "ldap:///cn=System: Read IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=locations,cn=etc,dc=ipa,dc=example
+aci: (targetfilter = "(objectclass=ipaLocationObject)")(version 3.0;acl "permission:System: Remove IPA Locations";allow (delete) groupdn = "ldap:///cn=System: Remove IPA Locations,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ng,cn=alt,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipanisnetgroup)")(version 3.0;acl "permission:System: Add Netgroups";allow (add) groupdn = "ldap:///cn=System: Add Netgroups,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=ng,cn=alt,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index 3ad250e74f48ef3c54494ba6bd2d398a7c5d1b69..0568a6573236ca25c7b2353832f949c95b353758 100644
--- a/API.txt
+++ b/API.txt
@@ -2759,6 +2759,65 @@ option: Str('version?')
 output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
+command: location_add
+args: 1,6,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?')
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: location_del
+args: 1,2,3
+arg: DNSNameParam('idnsname+', cli_name='name')
+option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Str('version?')
+output: Output('result', type=[<type 'dict'>])
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: ListOfPrimaryKeys('value')
+command: location_find
+args: 1,8,4
+arg: Str('criteria?')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('description?', autofill=False)
+option: DNSNameParam('idnsname?', autofill=False, cli_name='name')
+option: Flag('pkey_only?', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Int('sizelimit?', autofill=False)
+option: Int('timelimit?', autofill=False)
+option: Str('version?')
+output: Output('count', type=[<type 'int'>])
+output: ListOfEntries('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: Output('truncated', type=[<type 'bool'>])
+command: location_mod
+args: 1,8,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: Str('description?', autofill=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
+command: location_show
+args: 1,4,3
+arg: DNSNameParam('idnsname', cli_name='name')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: migrate_ds
 args: 2,20,4
 arg: Str('ldapuri', cli_name='ldap_uri')
diff --git a/VERSION b/VERSION
index 45fdb09788dbc6496272da786bb6d6afa45bf118..03908580e3008b5011588588ad41083310d24095 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=170
-# Last change: mbasti - *-find: do not search for members by default
+IPA_API_VERSION_MINOR=171
+# Last change: mbasti - location-* commands
diff --git a/install/share/bootstrap-template.ldif b/install/share/bootstrap-template.ldif
index 628a8e2e0f5483b9f6f565b0c7d11eb000a5912d..83be4399508a905f8eae7e2f59140a6b4051b661 100644
--- a/install/share/bootstrap-template.ldif
+++ b/install/share/bootstrap-template.ldif
@@ -119,6 +119,12 @@ objectClass: nsContainer
 objectClass: top
 cn: etc
 
+dn: cn=locations,cn=etc,$SUFFIX
+changetype: add
+objectClass: nsContainer
+objectClass: top
+cn: locations
+
 dn: cn=sysaccounts,cn=etc,$SUFFIX
 changetype: add
 objectClass: nsContainer
diff --git a/install/updates/37-locations.update b/install/updates/37-locations.update
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..cf47e6d6296af830a76aad2c9b9f5a6ea5d9f3a1 100644
--- a/install/updates/37-locations.update
+++ b/install/updates/37-locations.update
@@ -0,0 +1,4 @@
+dn: cn=locations,cn=etc,$SUFFIX
+default: objectClass: nsContainer
+default: objectClass: top
+default: cn: locations
diff --git a/install/updates/Makefile.am b/install/updates/Makefile.am
index 3edc21473d676bd282e9ea2b88769c097fb8a63a..737a8bbbd1a4915a6aefec2d273b90bb3ca31710 100644
--- a/install/updates/Makefile.am
+++ b/install/updates/Makefile.am
@@ -28,6 +28,7 @@ app_DATA =				\
 	25-referint.update		\
 	30-provisioning.update		\
 	30-s4u2proxy.update		\
+	37-locations.update		\
 	40-delegation.update		\
 	40-realm_domains.update		\
 	40-replication.update		\
diff --git a/ipalib/constants.py b/ipalib/constants.py
index 021f18cd366b821427bdbfcc5e354d2047ef39b1..d1c9ccf68d01ef1dc032559ca8a353eede7a0e09 100644
--- a/ipalib/constants.py
+++ b/ipalib/constants.py
@@ -121,6 +121,7 @@ DEFAULT_CONFIG = (
     ('container_certprofile', DN(('cn', 'certprofiles'), ('cn', 'ca'))),
     ('container_topology', DN(('cn', 'topology'), ('cn', 'ipa'), ('cn', 'etc'))),
     ('container_caacl', DN(('cn', 'caacls'), ('cn', 'ca'))),
+    ('container_locations', DN(('cn', 'locations'), ('cn', 'etc'))),
 
     # Ports, hosts, and URIs:
     ('xmlrpc_uri', 'http://localhost:8888/ipa/xml'),
diff --git a/ipalib/plugins/location.py b/ipalib/plugins/location.py
new file mode 100644
index 0000000000000000000000000000000000000000..d4940e7fb6222618803ab5b7748ff2ca5f7c20f4
--- /dev/null
+++ b/ipalib/plugins/location.py
@@ -0,0 +1,149 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from __future__ import absolute_import
+
+from ipalib import (
+    _,
+    ngettext,
+    api,
+    Str,
+    DNSNameParam
+)
+from ipalib.plugable import Registry
+from ipalib.plugins.baseldap import (
+    LDAPCreate,
+    LDAPSearch,
+    LDAPRetrieve,
+    LDAPDelete,
+    LDAPObject,
+    LDAPUpdate,
+)
+from ipapython.dnsutil import DNSName
+
+__doc__ = _("""
+IPA locations
+""") + _("""
+Manipulate DNS locations
+""") + _("""
+EXAMPLES:
+""") + _("""
+  Find all locations:
+    ipa location-find
+""") + _("""
+  Show specific location:
+    ipa location-show location
+""") + _("""
+  Add location:
+    ipa location-add location --description 'My location'
+""") + _("""
+  Delete location:
+    ipa location-del location
+""")
+
+register = Registry()
+
+
+@register()
+class location(LDAPObject):
+    """
+    IPA locations
+    """
+    container_dn = api.env.container_locations
+    object_name = _('location')
+    object_name_plural = _('locations')
+    object_class = ['ipaLocationObject']
+    search_attributes = ['idnsName']
+    default_attributes = [
+        'idnsname', 'description'
+    ]
+    label = _('IPA Locations')
+    label_singular = _('IPA Location')
+
+    permission_filter_objectclasses = ['ipaLocationObject']
+    managed_permissions = {
+        'System: Read IPA Locations': {
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'idnsname', 'description',
+            },
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Add IPA Locations': {
+            'ipapermright': {'add'},
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Remove IPA Locations': {
+            'ipapermright': {'delete'},
+            'default_privileges': {'DNS Administrators'},
+        },
+        'System: Modify IPA Locations': {
+            'ipapermright': {'write'},
+            'ipapermdefaultattr': {
+                'description',
+            },
+            'default_privileges': {'DNS Administrators'},
+        },
+    }
+
+    takes_params = (
+        DNSNameParam(
+            'idnsname',
+            cli_name='name',
+            primary_key=True,
+            label=_('Location name'),
+            doc=_('IPA location name'),
+            # dns name must be relative, we will put it into middle of
+            # location domain name for location records
+            only_relative=True,
+        ),
+        Str(
+            'description?',
+            label=_('Description'),
+            doc=_('IPA Location description'),
+        ),
+    )
+
+    def get_dn(self, *keys, **options):
+        loc = keys[-1]
+        assert isinstance(loc, DNSName)
+        loc_a = loc.ToASCII()
+
+        return super(location, self).get_dn(loc_a, **options)
+
+
+@register()
+class location_add(LDAPCreate):
+    __doc__ = _('Add a new IPA location.')
+
+    msg_summary = _('Added IPA location "%(value)s"')
+
+
+@register()
+class location_del(LDAPDelete):
+    __doc__ = _('Delete an IPA location.')
+
+    msg_summary = _('Deleted IPA location "%(value)s"')
+
+
+@register()
+class location_mod(LDAPUpdate):
+    __doc__ = _('Modify information about an IPA location.')
+
+    msg_summary = _('Modified IPA location "%(value)s"')
+
+
+@register()
+class location_find(LDAPSearch):
+    __doc__ = _('Search for IPA locations.')
+
+    msg_summary = ngettext(
+        '%(count)d IPA location matched',
+        '%(count)d IPA locations matched', 0
+    )
+
+
+@register()
+class location_show(LDAPRetrieve):
+    __doc__ = _('Display information about an IPA location.')
-- 
2.5.5

From 86fe2aa3b7b494d1169349e8036b9c8bc3c6e7a3 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Thu, 5 May 2016 16:07:20 +0200
Subject: [PATCH 4/4] DNS Locations: API tests

Tests for location-* commands

https://fedorahosted.org/freeipa/ticket/2008
---
 ipatests/test_xmlrpc/test_location_plugin.py    | 113 ++++++++++++++++++++++
 ipatests/test_xmlrpc/tracker/location_plugin.py | 119 ++++++++++++++++++++++++
 2 files changed, 232 insertions(+)
 create mode 100644 ipatests/test_xmlrpc/test_location_plugin.py
 create mode 100644 ipatests/test_xmlrpc/tracker/location_plugin.py

diff --git a/ipatests/test_xmlrpc/test_location_plugin.py b/ipatests/test_xmlrpc/test_location_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..1ca3eac7c72e0662034cb67039e1d0925bd1acca
--- /dev/null
+++ b/ipatests/test_xmlrpc/test_location_plugin.py
@@ -0,0 +1,113 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+from __future__ import absolute_import
+
+import pytest
+
+from ipalib import errors
+from ipatests.test_xmlrpc.tracker.location_plugin import LocationTracker
+from ipatests.test_xmlrpc.xmlrpc_test import (
+    XMLRPC_test,
+    raises_exact,
+)
+
+
+@pytest.fixture(scope='class', params=[u'location1', u'sk\xfa\u0161ka.idna'])
+def location(request):
+    tracker = LocationTracker(request.param)
+    return tracker.make_fixture(request)
+
+
+@pytest.fixture(scope='class')
+def location_invalid(request):
+    tracker = LocationTracker(u'invalid..location')
+    return tracker
+
+
+@pytest.fixture(scope='class')
+def location_absolute(request):
+    tracker = LocationTracker(u'invalid.absolute.')
+    return tracker.make_fixture(request)
+
+
+@pytest.mark.tier1
+class TestNonexistentIPALocation(XMLRPC_test):
+    def test_retrieve_nonexistent(self, location):
+        location.ensure_missing()
+        command = location.make_retrieve_command()
+        with raises_exact(errors.NotFound(
+                reason=u'%s: location not found' % location.idnsname)):
+            command()
+
+    def test_update_nonexistent(self, location):
+        location.ensure_missing()
+        command = location.make_update_command(updates=dict(
+            description=u'Nope'))
+        with raises_exact(errors.NotFound(
+                reason=u'%s: location not found' % location.idnsname)):
+            command()
+
+    def test_delete_nonexistent(self, location):
+        location.ensure_missing()
+        command = location.make_delete_command()
+        with raises_exact(errors.NotFound(
+                reason=u'%s: location not found' % location.idnsname)):
+            command()
+
+@pytest.mark.tier1
+class TestInvalidIPALocations(XMLRPC_test):
+    def test_invalid_name(self, location_invalid):
+        command = location_invalid.make_create_command()
+        with raises_exact(errors.ConversionError(
+                name=u'name',
+                error=u"empty DNS label")):
+            command()
+
+    def test_invalid_absolute(self, location_absolute):
+        command = location_absolute.make_create_command()
+        with raises_exact(errors.ValidationError(
+                name=u'name', error=u'must be relative')):
+            command()
+
+
+@pytest.mark.tier1
+class TestCRUD(XMLRPC_test):
+    def test_create_duplicate(self, location):
+        location.ensure_exists()
+        command = location.make_create_command(force=True)
+        with raises_exact(errors.DuplicateEntry(
+                message=u'location with name "%s" already exists' %
+                        location.idnsname)):
+            command()
+
+    def test_retrieve_simple(self, location):
+        location.retrieve()
+
+    def test_retrieve_all(self, location):
+        location.retrieve(all=True)
+
+    def test_search_simple(self, location):
+        location.find()
+
+    def test_search_all(self, location):
+        location.find(all=True)
+
+    def test_update_simple(self, location):
+        location.update(dict(
+                description=u'Updated description',
+            ),
+            expected_updates=dict(
+                description=[u'Updated description'],
+            ))
+        location.retrieve()
+
+    def test_try_rename(self, location):
+        location.ensure_exists()
+        command = location.make_update_command(
+            updates=dict(setattr=u'idnsname=changed'))
+        with raises_exact(errors.NotAllowedOnRDN()):
+            command()
+
+    def test_delete_location(self, location):
+        location.delete()
diff --git a/ipatests/test_xmlrpc/tracker/location_plugin.py b/ipatests/test_xmlrpc/tracker/location_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7af09768d0b4e7258ec78e4be533c72bf32e35f
--- /dev/null
+++ b/ipatests/test_xmlrpc/tracker/location_plugin.py
@@ -0,0 +1,119 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+from __future__ import absolute_import
+
+from ipapython.dn import DN
+from ipapython.dnsutil import DNSName
+from ipatests.util import assert_deepequal
+from ipatests.test_xmlrpc.tracker.base import Tracker
+
+
+class LocationTracker(Tracker):
+    """Tracker for IPA Location tests"""
+    retrieve_keys = {'idnsname', 'description', 'dn'}
+    retrieve_all_keys = retrieve_keys | {'objectclass'}
+    create_keys = retrieve_keys | {'objectclass'}
+    find_keys = retrieve_keys
+    find_all_keys = retrieve_all_keys
+    update_keys = {'idnsname', 'description'}
+
+    def __init__(self, name, description=u"Location description"):
+        super(LocationTracker, self).__init__(default_version=None)
+        # ugly hack to allow testing invalid inputs
+        try:
+            self.idnsname_obj = DNSName(name)
+        except Exception:
+            self.idnsname_obj = DNSName(u"placeholder-for-invalid-value")
+
+        self.idnsname = name
+        self.description = description
+        self.dn = DN(
+            ('idnsname', self.idnsname_obj.ToASCII()),
+            'cn=locations',
+            'cn=etc', self.api.env.basedn
+        )
+
+    def make_create_command(self, force=None):
+        """Make function that creates this location using location-add"""
+        return self.make_command(
+            'location_add', self.idnsname, description=self.description,
+        )
+
+    def make_delete_command(self):
+        """Make function that removes this location using location-del"""
+        return self.make_command('location_del', self.idnsname)
+
+    def make_retrieve_command(self, all=False, raw=False):
+        """Make function that retrieves this location using location-show"""
+        return self.make_command(
+            'location_show', self.idnsname, all=all, raw=raw
+        )
+
+    def make_find_command(self, *args, **kwargs):
+        """Make function that finds locations using location-find"""
+        return self.make_command('location_find', *args, **kwargs)
+
+    def make_update_command(self, updates):
+        """Make function that modifies the location using location-mod"""
+        return self.make_command('location_mod', self.idnsname, **updates)
+
+    def track_create(self):
+        """Update expected state for location creation"""
+
+        self.attrs = dict(
+            dn=self.dn,
+            idnsname=[self.idnsname_obj],
+            description=[self.description],
+            objectclass=[u'top', u'ipaLocationObject'],
+        )
+        self.exists = True
+
+    def check_create(self, result):
+        """Check `location-add` command result"""
+        assert_deepequal(dict(
+            value=self.idnsname_obj,
+            summary=u'Added IPA location "{loc}"'.format(loc=self.idnsname),
+            result=self.filter_attrs(self.create_keys)
+        ), result)
+
+    def check_delete(self, result):
+        """Check `location-del` command result"""
+        assert_deepequal(dict(
+            value=[self.idnsname_obj],
+            summary=u'Deleted IPA location "{loc}"'.format(loc=self.idnsname),
+            result=dict(failed=[]),
+        ), result)
+
+    def check_retrieve(self, result, all=False, raw=False):
+        """Check `location-show` command result"""
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+        assert_deepequal(dict(
+            value=self.idnsname_obj,
+            summary=None,
+            result=expected,
+        ), result)
+
+    def check_find(self, result, all=False, raw=False):
+        """Check `location-find` command result"""
+        if all:
+            expected = self.filter_attrs(self.find_all_keys)
+        else:
+            expected = self.filter_attrs(self.find_keys)
+        assert_deepequal(dict(
+            count=1,
+            truncated=False,
+            summary=u'1 IPA location matched',
+            result=[expected],
+        ), result)
+
+    def check_update(self, result, extra_keys=()):
+        """Check `location-update` command result"""
+        assert_deepequal(dict(
+            value=self.idnsname_obj,
+            summary=u'Modified IPA location "{loc}"'.format(loc=self.idnsname),
+            result=self.filter_attrs(self.update_keys | set(extra_keys))
+        ), result)
-- 
2.5.5

From 7eef7e33409ce67bc29bdd82489cac1a01677e50 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Fri, 13 May 2016 16:19:51 +0200
Subject: [PATCH 1/5] Allow to use non-Str attributes as keys for members

Locations use DNSNameParam as pkey_value, but implementation of searches
for members was able to use only Str param. This commit allows to use
other param classes for search.

Required for: https://fedorahosted.org/freeipa/ticket/2008
---
 ipalib/plugins/baseldap.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/ipalib/plugins/baseldap.py b/ipalib/plugins/baseldap.py
index bbd8ba146ead81857bbc4c2aee550b855b846be5..62b726da1a0baef9fc9fa4bb386a2101ca1f10b2 100644
--- a/ipalib/plugins/baseldap.py
+++ b/ipalib/plugins/baseldap.py
@@ -1890,9 +1890,10 @@ class LDAPSearch(BaseLDAPCommand, crud.Search):
                 ldap_object=ldap_obj.object_name_plural
             )
             name = '%s%s' % (relationship[1], to_cli(ldap_obj_name))
-            yield Str(
-                '%s*' % name, cli_name='%ss' % name, doc=doc,
-                label=ldap_obj.object_name
+            yield ldap_obj.primary_key.clone_rename(
+                '%s' % name, cli_name='%ss' % name, doc=doc,
+                label=ldap_obj.object_name, multivalue=True, query=True,
+                required=False, primary_key=False
             )
             doc = self.member_param_excl_doc % dict(
                 searched_object=self.obj.object_name_plural,
@@ -1900,9 +1901,10 @@ class LDAPSearch(BaseLDAPCommand, crud.Search):
                 ldap_object=ldap_obj.object_name_plural
             )
             name = '%s%s' % (relationship[2], to_cli(ldap_obj_name))
-            yield Str(
-                '%s*' % name, cli_name='%ss' % name, doc=doc,
-                label=ldap_obj.object_name
+            yield ldap_obj.primary_key.clone_rename(
+                '%s' % name, cli_name='%ss' % name, doc=doc,
+                label=ldap_obj.object_name, multivalue=True, query=True,
+                required=False, primary_key=False
             )
 
     def get_options(self):
-- 
2.5.5

From 1f2704950d8b22b2228255fd24889c50f582b285 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Wed, 11 May 2016 18:27:37 +0200
Subject: [PATCH 2/5] DNS Locations: extend server-* command with locations

Server find, server show, server mod should work with IPA locations.

https://fedorahosted.org/freeipa/ticket/2008
---
 API.txt                    |  20 ++++++++-
 VERSION                    |   4 +-
 ipalib/plugins/location.py |   2 +-
 ipalib/plugins/server.py   | 104 ++++++++++++++++++++++++++++++++++++++++++---
 4 files changed, 121 insertions(+), 9 deletions(-)

diff --git a/API.txt b/API.txt
index 0568a6573236ca25c7b2353832f949c95b353758..ed7245854801526e7edcbecf8e71112c0a201f10 100644
--- a/API.txt
+++ b/API.txt
@@ -3930,14 +3930,16 @@ output: Output('result', type=[<type 'dict'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: ListOfPrimaryKeys('value')
 command: server_find
-args: 1,12,4
+args: 1,14,4
 arg: Str('criteria?')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Str('cn?', autofill=False, cli_name='name')
+option: DNSNameParam('in_location*', cli_name='in_locations')
 option: Int('ipamaxdomainlevel?', autofill=False, cli_name='maxlevel')
 option: Int('ipamindomainlevel?', autofill=False, cli_name='minlevel')
 option: Flag('no_members', autofill=True, default=True)
 option: Str('no_topologysuffix*', cli_name='no_topologysuffixes')
+option: DNSNameParam('not_in_location*', cli_name='not_in_locations')
 option: Flag('pkey_only?', autofill=True, default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Int('sizelimit?', autofill=False)
@@ -3948,6 +3950,22 @@ output: Output('count', type=[<type 'int'>])
 output: ListOfEntries('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: Output('truncated', type=[<type 'bool'>])
+command: server_mod
+args: 1,10,3
+arg: Str('cn', cli_name='name')
+option: Str('addattr*', cli_name='addattr')
+option: Flag('all', autofill=True, cli_name='all', default=False)
+option: Str('delattr*', cli_name='delattr')
+option: DNSNameParam('ipalocation_location?', autofill=False, cli_name='location')
+option: Int('ipalocationweight?', autofill=False, cli_name='location_weight')
+option: Flag('no_members', autofill=True, default=False)
+option: Flag('raw', autofill=True, cli_name='raw', default=False)
+option: Flag('rights', autofill=True, default=False)
+option: Str('setattr*', cli_name='setattr')
+option: Str('version?')
+output: Entry('result')
+output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
+output: PrimaryKey('value')
 command: server_show
 args: 1,5,3
 arg: Str('cn', cli_name='name')
diff --git a/VERSION b/VERSION
index 03908580e3008b5011588588ad41083310d24095..c801194505e7a7076d0e8dea42078027a582758a 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=171
-# Last change: mbasti - location-* commands
+IPA_API_VERSION_MINOR=172
+# Last change: mbasti - server-mod: locations added
diff --git a/ipalib/plugins/location.py b/ipalib/plugins/location.py
index d4940e7fb6222618803ab5b7748ff2ca5f7c20f4..232496728534d788aa6b9a98753522b26a0f6daf 100644
--- a/ipalib/plugins/location.py
+++ b/ipalib/plugins/location.py
@@ -106,7 +106,7 @@ class location(LDAPObject):
     )
 
     def get_dn(self, *keys, **options):
-        loc = keys[-1]
+        loc = keys[0]
         assert isinstance(loc, DNSName)
         loc_a = loc.ToASCII()
 
diff --git a/ipalib/plugins/server.py b/ipalib/plugins/server.py
index 6faaf8ec5a98501ccdcb3dbc5983ce4a27baacba..3192a588b487c217d78343675fc89e467d8021a9 100644
--- a/ipalib/plugins/server.py
+++ b/ipalib/plugins/server.py
@@ -6,16 +6,21 @@ import dbus
 import dbus.mainloop.glib
 
 from ipalib import api, crud, errors, messages
-from ipalib import Int, Str
+from ipalib import Int, Str, DNSNameParam
 from ipalib.plugable import Registry
 from .baseldap import (
     LDAPSearch,
     LDAPRetrieve,
     LDAPDelete,
-    LDAPObject)
+    LDAPObject,
+    LDAPUpdate,
+)
 from ipalib.request import context
 from ipalib import _, ngettext
 from ipalib import output
+from ipapython.dn import DN
+from ipapython.dnsutil import DNSName
+
 
 __doc__ = _("""
 IPA servers
@@ -43,18 +48,21 @@ class server(LDAPObject):
     object_name = _('server')
     object_name_plural = _('servers')
     object_class = ['top']
+    possible_objectclasses = ['ipaLocationMember']
     search_attributes = ['cn']
     default_attributes = [
         'cn', 'iparepltopomanagedsuffix', 'ipamindomainlevel',
-        'ipamaxdomainlevel'
+        'ipamaxdomainlevel', 'ipalocation', 'ipalocationweight'
     ]
     label = _('IPA Servers')
     label_singular = _('IPA Server')
     attribute_members = {
         'iparepltopomanagedsuffix': ['topologysuffix'],
+        'ipalocation': ['location'],
     }
     relationships = {
         'iparepltopomanagedsuffix': ('Managed', '', 'no_'),
+        'ipalocation': ('IPA', 'in_', 'not_in_'),
     }
     takes_params = (
         Str(
@@ -87,6 +95,23 @@ class server(LDAPObject):
             doc=_('Maximum domain level'),
             flags={'no_create', 'no_update'},
         ),
+        DNSNameParam(
+            'ipalocation_location?',
+            cli_name='location',
+            label=_('Location'),
+            doc=_('Server location'),
+            only_relative=True,
+            flags={'no_search'},
+        ),
+        Int(
+            'ipalocationweight?',
+            cli_name='location_weight',
+            label=_('Location weight'),
+            doc=_('Location weight for server'),
+            minvalue=0,
+            maxvalue=65535,
+            flags={'no_search'},
+        )
     )
 
     def _get_suffixes(self):
@@ -105,6 +130,67 @@ class server(LDAPObject):
                 suffixes.get(m, m) for m in entry['iparepltopomanagedsuffix']
             ]
 
+    def normalize_location(self, kw, **options):
+        """
+        Return the DN of location
+        """
+        if 'ipalocation_location' in kw:
+            location = kw.pop('ipalocation_location')
+            kw['ipalocation'] = (
+                [self.api.Object.location.get_dn(location)]
+                if location is not None else location
+            )
+
+    def convert_location(self, entry_attrs, **options):
+        """
+        Return a location name from DN
+        """
+        if options.get('raw'):
+            return
+
+        converted_locations = [
+            DNSName(location_dn['idnsname']) for
+            location_dn in entry_attrs.pop('ipalocation', [])
+        ]
+
+        if converted_locations:
+            entry_attrs['ipalocation_location'] = converted_locations
+
+
+@register()
+class server_mod(LDAPUpdate):
+    __doc__ = _('Modify information about an IPA server.')
+
+    msg_summary = _('Modified IPA server "%(value)s"')
+
+    def args_options_2_entry(self, *args, **options):
+        kw = super(server_mod, self).args_options_2_entry(
+            *args, **options)
+        self.obj.normalize_location(kw, **options)
+        return kw
+
+    def pre_callback(self, ldap, dn, entry_attrs, attrs_list, *keys, **options):
+        assert isinstance(dn, DN)
+
+        if entry_attrs.get('ipalocation'):
+            if not ldap.entry_exists(entry_attrs['ipalocation'][0]):
+                self.api.Object.location.handle_not_found(
+                    options['ipalocation_location'])
+
+        if 'ipalocation' or 'ipalocationweight' in entry_attrs:
+            server_entry = ldap.get_entry(dn, ['objectclass'])
+
+            # we need to extend object with ipaLocationMember objectclass
+            entry_attrs['objectclass'] = (
+                server_entry['objectclass'] + ['ipalocationmember']
+            )
+
+        return dn
+
+    def post_callback(self, ldap, dn, entry_attrs, *keys, **options):
+        assert isinstance(dn, DN)
+        self.obj.convert_location(entry_attrs, **options)
+        return dn
 
 @register()
 class server_find(LDAPSearch):
@@ -114,7 +200,13 @@ class server_find(LDAPSearch):
         '%(count)d IPA server matched',
         '%(count)d IPA servers matched', 0
     )
-    member_attributes = ['iparepltopomanagedsuffix']
+    member_attributes = ['iparepltopomanagedsuffix', 'ipalocation']
+
+    def args_options_2_entry(self, *args, **options):
+        kw = super(server_find, self).args_options_2_entry(
+            *args, **options)
+        self.obj.normalize_location(kw, **options)
+        return kw
 
     def get_options(self):
         for option in super(server_find, self).get_options():
@@ -173,6 +265,8 @@ class server_find(LDAPSearch):
             for entry in entries:
                 self.obj._apply_suffixes(entry, suffixes)
 
+        for entry in entries:
+            self.obj.convert_location(entry, **options)
         return truncated
 
 
@@ -184,7 +278,7 @@ class server_show(LDAPRetrieve):
         if not options.get('raw', False):
             suffixes = self.obj._get_suffixes()
             self.obj._apply_suffixes(entry, suffixes)
-
+        self.obj.convert_location(entry, **options)
         return dn
 
 
-- 
2.5.5

From d9f2fb95981182063225a9634c11d6d7b240908e Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Fri, 13 May 2016 17:08:43 +0200
Subject: [PATCH 3/5] DNS Location: location-show: return list of servers in
 location

location-show returns list of servers curently assigned to the location

https://fedorahosted.org/freeipa/ticket/2008
---
 ACI.txt                    |  2 ++
 API.txt                    |  3 +-
 VERSION                    |  4 +--
 ipalib/plugins/location.py | 77 ++++++++++++++++++++++++++++++++++++++++++++--
 ipalib/plugins/server.py   | 16 ++++++++++
 5 files changed, 97 insertions(+), 5 deletions(-)

diff --git a/ACI.txt b/ACI.txt
index 2226eccc74ec6d25c1f6fcc93f3e1c7d636b8146..a09495e5a445d7c3bc51f0b082c538b80bbb425a 100644
--- a/ACI.txt
+++ b/ACI.txt
@@ -226,6 +226,8 @@ dn: cn=usermap,cn=selinux,dc=ipa,dc=example
 aci: (targetattr = "accesstime || cn || createtimestamp || description || entryusn || hostcategory || ipaenabledflag || ipaselinuxuser || ipauniqueid || member || memberhost || memberuser || modifytimestamp || objectclass || seealso || usercategory")(targetfilter = "(objectclass=ipaselinuxusermap)")(version 3.0;acl "permission:System: Read SELinux User Maps";allow (compare,read,search) userdn = "ldap:///all";;)
 dn: cn=usermap,cn=selinux,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipaselinuxusermap)")(version 3.0;acl "permission:System: Remove SELinux User Maps";allow (delete) groupdn = "ldap:///cn=System: Remove SELinux User Maps,cn=permissions,cn=pbac,dc=ipa,dc=example";)
+dn: cn=masters,cn=ipa,cn=etc,dc=ipa,dc=example
+aci: (targetattr = "cn || createtimestamp || entryusn || ipalocation || ipalocationweight || modifytimestamp || objectclass")(targetfilter = "(objectclass=ipaLocationMember)")(version 3.0;acl "permission:System: Read Locations of IPA Servers";allow (compare,read,search) groupdn = "ldap:///cn=System: Read Locations of IPA Servers,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=services,cn=accounts,dc=ipa,dc=example
 aci: (targetfilter = "(objectclass=ipaservice)")(version 3.0;acl "permission:System: Add Services";allow (add) groupdn = "ldap:///cn=System: Add Services,cn=permissions,cn=pbac,dc=ipa,dc=example";)
 dn: cn=services,cn=accounts,dc=ipa,dc=example
diff --git a/API.txt b/API.txt
index ed7245854801526e7edcbecf8e71112c0a201f10..8ea71797e99111c2844426ac84590dda92b8c1f2 100644
--- a/API.txt
+++ b/API.txt
@@ -2809,13 +2809,14 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: location_show
-args: 1,4,3
+args: 1,4,4
 arg: DNSNameParam('idnsname', cli_name='name')
 option: Flag('all', autofill=True, cli_name='all', default=False)
 option: Flag('raw', autofill=True, cli_name='raw', default=False)
 option: Flag('rights', autofill=True, default=False)
 option: Str('version?')
 output: Entry('result')
+output: Output('servers', type=[<type 'dict'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: migrate_ds
diff --git a/VERSION b/VERSION
index c801194505e7a7076d0e8dea42078027a582758a..6ab6dd68a903116280f36e261e1aed962b3b4fa4 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=172
-# Last change: mbasti - server-mod: locations added
+IPA_API_VERSION_MINOR=173
+# Last change: mbasti - location-show: list servers in the location
diff --git a/ipalib/plugins/location.py b/ipalib/plugins/location.py
index 232496728534d788aa6b9a98753522b26a0f6daf..58c4e055cd65719bf465974035005fbda1435832 100644
--- a/ipalib/plugins/location.py
+++ b/ipalib/plugins/location.py
@@ -2,14 +2,18 @@
 # Copyright (C) 2016  FreeIPA Contributors see COPYING for license
 #
 
-from __future__ import absolute_import
+from __future__ import (
+    absolute_import,
+    division,
+)
 
 from ipalib import (
     _,
     ngettext,
     api,
     Str,
-    DNSNameParam
+    DNSNameParam,
+    output,
 )
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import (
@@ -20,6 +24,7 @@ from ipalib.plugins.baseldap import (
     LDAPObject,
     LDAPUpdate,
 )
+from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 
 __doc__ = _("""
@@ -103,6 +108,12 @@ class location(LDAPObject):
             label=_('Description'),
             doc=_('IPA Location description'),
         ),
+        Str(
+            'servers_server*',
+            label=_('Servers'),
+            doc=_('Servers that belongs to the IPA location'),
+            flags={'virtual_attribute', 'no_create', 'no_update', 'no_search'},
+        ),
     )
 
     def get_dn(self, *keys, **options):
@@ -147,3 +158,65 @@ class location_find(LDAPSearch):
 @register()
 class location_show(LDAPRetrieve):
     __doc__ = _('Display information about an IPA location.')
+
+    has_output = LDAPRetrieve.has_output + (
+        output.Output(
+            'servers',
+            type=dict,
+            doc=_('Servers in location'),
+            flags={'no_display'},  # we use customized print to CLI
+        ),
+    )
+
+    def execute(self, *keys, **options):
+        result = super(location_show, self).execute(*keys, **options)
+
+        servers_additional_info = {}
+        if not options.get('raw'):
+            servers_name = []
+            weight_sum = 0
+
+            servers = self.api.Command.server_find(
+                in_location=keys[0], no_members=False)['result']
+            for server in servers:
+                servers_name.append(server['cn'][0])
+                weight = int(server.get('ipalocationweight', [100])[0])
+                weight_sum += weight
+                servers_additional_info[server['cn'][0]] = {
+                    'cn': server['cn'],
+                    'ipalocationweight': server.get(
+                        'ipalocationweight', [u'100']),
+                }
+
+            for server in servers_additional_info.values():
+                server['location_relative_weight'] = [
+                    u'{:.1f}%'.format(
+                        int(server['ipalocationweight'][0])*100.0/weight_sum)
+                ]
+            if servers_name:
+                result['result']['servers_server'] = servers_name
+        result['servers'] = servers_additional_info
+
+        return result
+
+    def output_for_cli(self, textui, result, *keys, **options):
+        super(location_show, self).output_for_cli(
+            textui, result, *keys, **options)
+
+        servers = result.get('servers', {})
+        if servers:
+            textui.print_indented(_("Servers details:"), indent=1)
+
+        first = True
+        for hostname, details in servers.items():
+            if first:
+                first = False
+            else:
+                textui.print_line("")
+
+            for param in self.api.Command.server_find.output_params():
+                if param.name in details:
+                    textui.print_indented(
+                        u"{}: {}".format(
+                            param.label, u', '.join(details[param.name])),
+                        indent=2)
diff --git a/ipalib/plugins/server.py b/ipalib/plugins/server.py
index 3192a588b487c217d78343675fc89e467d8021a9..511f8135b14edb8d6af7fc6a0a04ccd8a110a00b 100644
--- a/ipalib/plugins/server.py
+++ b/ipalib/plugins/server.py
@@ -64,6 +64,16 @@ class server(LDAPObject):
         'iparepltopomanagedsuffix': ('Managed', '', 'no_'),
         'ipalocation': ('IPA', 'in_', 'not_in_'),
     }
+    permission_filter_objectclasses = ['ipaLocationMember']
+    managed_permissions = {
+        'System: Read Locations of IPA Servers': {
+            'ipapermright': {'read', 'search', 'compare'},
+            'ipapermdefaultattr': {
+                'objectclass', 'cn', 'ipalocation', 'ipalocationweight',
+            },
+            'default_privileges': {'DNS Administrators'},
+        },
+    }
     takes_params = (
         Str(
             'cn',
@@ -111,6 +121,12 @@ class server(LDAPObject):
             minvalue=0,
             maxvalue=65535,
             flags={'no_search'},
+        ),
+        Str(
+            'location_relative_weight',
+            label=_('Location relative weight'),
+            doc=_('Location relative weight for server (counts per location)'),
+            flags={'virtual_attribute','no_create', 'no_update', 'no_search'},
         )
     )
 
-- 
2.5.5

From 4d9490b82dd417e896bed8330e9eb0caacc5c137 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Fri, 13 May 2016 18:39:47 +0200
Subject: [PATCH 4/5] DNS Locations: when removing location remove it from
 servers first

Locations should be removed from server by using server-mod during
location-del (future patches will handle DNS records in server-mod)

Referint plugin is configured to remove references of deleted locations.

https://fedorahosted.org/freeipa/ticket/2008
---
 install/updates/25-referint.update | 1 +
 ipalib/plugins/location.py         | 8 ++++++++
 2 files changed, 9 insertions(+)

diff --git a/install/updates/25-referint.update b/install/updates/25-referint.update
index 3f78ee9755823fb3d5838d3069f4506c57a69d05..b887ede9c98f100709d24aae26b75d501f581016 100644
--- a/install/updates/25-referint.update
+++ b/install/updates/25-referint.update
@@ -19,3 +19,4 @@ add: referint-membership-attr: ipaassignedidview
 add: referint-membership-attr: ipaallowedtarget
 add: referint-membership-attr: ipamemberca
 add: referint-membership-attr: ipamembercertprofile
+add: referint-membership-attr: ipalocation
diff --git a/ipalib/plugins/location.py b/ipalib/plugins/location.py
index 58c4e055cd65719bf465974035005fbda1435832..3bf97fab3b3f7972b4e55499a2fd6d85a23f8f67 100644
--- a/ipalib/plugins/location.py
+++ b/ipalib/plugins/location.py
@@ -137,6 +137,14 @@ class location_del(LDAPDelete):
 
     msg_summary = _('Deleted IPA location "%(value)s"')
 
+    def pre_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+        servers = self.api.Command.server_find(
+            in_location=keys[-1])['result']
+        for server in servers:
+            self.api.Command.server_mod(server['cn'][0], location=None)
+        return dn
+
 
 @register()
 class location_mod(LDAPUpdate):
-- 
2.5.5

From 3f40a54308e94d65846a78c7581cb3eff4e925e4 Mon Sep 17 00:00:00 2001
From: Martin Basti <mba...@redhat.com>
Date: Tue, 17 May 2016 13:08:59 +0200
Subject: [PATCH 5/5] DNS Locations: extend tests with server-* commands

https://fedorahosted.org/freeipa/ticket/2008
---
 ipatests/test_xmlrpc/test_location_plugin.py    |  91 +++++++++++++++++++-
 ipatests/test_xmlrpc/tracker/base.py            |   4 +
 ipatests/test_xmlrpc/tracker/location_plugin.py |  44 ++++++++--
 ipatests/test_xmlrpc/tracker/server_plugin.py   | 110 ++++++++++++++++++++++++
 4 files changed, 241 insertions(+), 8 deletions(-)
 create mode 100644 ipatests/test_xmlrpc/tracker/server_plugin.py

diff --git a/ipatests/test_xmlrpc/test_location_plugin.py b/ipatests/test_xmlrpc/test_location_plugin.py
index 1ca3eac7c72e0662034cb67039e1d0925bd1acca..97e97a2bc9ec910b65c3fc5b551e226fc52e53b8 100644
--- a/ipatests/test_xmlrpc/test_location_plugin.py
+++ b/ipatests/test_xmlrpc/test_location_plugin.py
@@ -5,12 +5,14 @@ from __future__ import absolute_import
 
 import pytest
 
-from ipalib import errors
+from ipalib import errors, api
 from ipatests.test_xmlrpc.tracker.location_plugin import LocationTracker
+from ipatests.test_xmlrpc.tracker.server_plugin import ServerTracker
 from ipatests.test_xmlrpc.xmlrpc_test import (
     XMLRPC_test,
     raises_exact,
 )
+from ipapython.dnsutil import DNSName
 
 
 @pytest.fixture(scope='class', params=[u'location1', u'sk\xfa\u0161ka.idna'])
@@ -31,6 +33,12 @@ def location_absolute(request):
     return tracker.make_fixture(request)
 
 
+@pytest.fixture(scope='class')
+def server(request):
+    tracker = ServerTracker(api.env.host)
+    return tracker
+
+
 @pytest.mark.tier1
 class TestNonexistentIPALocation(XMLRPC_test):
     def test_retrieve_nonexistent(self, location):
@@ -75,7 +83,7 @@ class TestInvalidIPALocations(XMLRPC_test):
 class TestCRUD(XMLRPC_test):
     def test_create_duplicate(self, location):
         location.ensure_exists()
-        command = location.make_create_command(force=True)
+        command = location.make_create_command()
         with raises_exact(errors.DuplicateEntry(
                 message=u'location with name "%s" already exists' %
                         location.idnsname)):
@@ -111,3 +119,82 @@ class TestCRUD(XMLRPC_test):
 
     def test_delete_location(self, location):
         location.delete()
+
+
+@pytest.mark.tier1
+class TestLocationsServer(XMLRPC_test):
+
+    def test_add_nonexistent_location_to_server(self, server):
+        nonexistent_loc = DNSName(u'nonexistent-location')
+        command = server.make_update_command(
+            updates=dict(
+                ipalocation_location=nonexistent_loc,
+            )
+        )
+        with raises_exact(errors.NotFound(
+                reason=u"{location}: location not found".format(
+                    location=nonexistent_loc
+                ))):
+            command()
+
+    def test_add_location_to_server(self, location, server):
+        location.ensure_exists()
+        server.update(
+            dict(ipalocation_location=location.idnsname_obj),
+            expected_updates=dict(
+                ipalocation_location=[location.idnsname_obj],
+            )
+        )
+        location.add_server_to_location(server.server_name)
+        location.retrieve()
+
+    def test_retrieve(self, server):
+        server.retrieve()
+
+    def test_retrieve_all(self, server):
+        server.retrieve(all=True)
+
+    def test_search_server_with_location(self, location, server):
+        command = server.make_find_command(
+            server.server_name, in_location=location.idnsname_obj)
+        result = command()
+        server.check_find(result)
+
+    def test_search_server_with_location_with_all(self, location, server):
+        command = server.make_find_command(
+            server.server_name, in_location=location.idnsname_obj, all=True)
+        result = command()
+        server.check_find(result, all=True)
+
+    def test_search_server_without_location(self, location, server):
+        command = server.make_find_command(
+            server.server_name, not_in_location=location.idnsname_obj)
+        result = command()
+        server.check_find_nomatch(result)
+
+    def test_add_location_to_server_custom_weight(self, location, server):
+        location.ensure_exists()
+        server.update(
+            dict(
+                ipalocation_location=location.idnsname_obj,
+                ipalocationweight=200,
+            ),
+            expected_updates=dict(
+                ipalocation_location=[location.idnsname_obj],
+                ipalocationweight=[u'200'],
+            )
+        )
+        # remove invalid data from the previous test
+        location.remove_server_from_location(server.server_name)
+
+        location.add_server_to_location(server.server_name, weight=200)
+        location.retrieve()
+
+    def test_remove_location_from_server(self, location, server):
+        server.update(dict(ipalocation_location=None))
+        location.remove_server_from_location(server.server_name)
+        location.retrieve()
+
+    def test_remove_location_weight_from_server(self, location, server):
+        server.update(dict(ipalocationweight=None))
+        location.retrieve()
diff --git a/ipatests/test_xmlrpc/tracker/base.py b/ipatests/test_xmlrpc/tracker/base.py
index acd382dd3f3ccf337e4d924e296aa57f5c07fad5..6a0af510f52aa1d7ccd94450c0848149d9abab48 100644
--- a/ipatests/test_xmlrpc/tracker/base.py
+++ b/ipatests/test_xmlrpc/tracker/base.py
@@ -281,6 +281,10 @@ class Tracker(object):
         result = command()
         self.attrs.update(updates)
         self.attrs.update(expected_updates)
+        for key, value in self.attrs.items():
+            if value is None:
+                del self.attrs[key]
+
         self.check_update(result, extra_keys=set(updates.keys()) |
                                              set(expected_updates.keys()))
 
diff --git a/ipatests/test_xmlrpc/tracker/location_plugin.py b/ipatests/test_xmlrpc/tracker/location_plugin.py
index c7af09768d0b4e7258ec78e4be533c72bf32e35f..5e9713c1e3ba7a8ef6534c3a29b70f353725e374 100644
--- a/ipatests/test_xmlrpc/tracker/location_plugin.py
+++ b/ipatests/test_xmlrpc/tracker/location_plugin.py
@@ -3,19 +3,25 @@
 #
 from __future__ import absolute_import
 
+import six
+
 from ipapython.dn import DN
 from ipapython.dnsutil import DNSName
 from ipatests.util import assert_deepequal
 from ipatests.test_xmlrpc.tracker.base import Tracker
 
 
+if six.PY3:
+    unicode = str
+
+
 class LocationTracker(Tracker):
     """Tracker for IPA Location tests"""
-    retrieve_keys = {'idnsname', 'description', 'dn'}
+    retrieve_keys = {'idnsname', 'description', 'dn', 'servers_server'}
     retrieve_all_keys = retrieve_keys | {'objectclass'}
-    create_keys = retrieve_keys | {'objectclass'}
-    find_keys = retrieve_keys
-    find_all_keys = retrieve_all_keys
+    create_keys = {'idnsname', 'description', 'dn', 'objectclass'}
+    find_keys = {'idnsname', 'description', 'dn',}
+    find_all_keys = find_keys | {'objectclass'}
     update_keys = {'idnsname', 'description'}
 
     def __init__(self, name, description=u"Location description"):
@@ -34,13 +40,15 @@ class LocationTracker(Tracker):
             'cn=etc', self.api.env.basedn
         )
 
+        self.servers = {}
+
     def make_create_command(self, force=None):
         """Make function that creates this location using location-add"""
         return self.make_command(
             'location_add', self.idnsname, description=self.description,
         )
 
-    def make_delete_command(self):
+    def make_delete_command(self, force=None):
         """Make function that removes this location using location-del"""
         return self.make_command('location_del', self.idnsname)
 
@@ -65,7 +73,7 @@ class LocationTracker(Tracker):
             dn=self.dn,
             idnsname=[self.idnsname_obj],
             description=[self.description],
-            objectclass=[u'top', u'ipaLocationObject'],
+            objectclass=[u'ipaLocationObject'],
         )
         self.exists = True
 
@@ -95,6 +103,7 @@ class LocationTracker(Tracker):
             value=self.idnsname_obj,
             summary=None,
             result=expected,
+            servers=self.servers,
         ), result)
 
     def check_find(self, result, all=False, raw=False):
@@ -117,3 +126,26 @@ class LocationTracker(Tracker):
             summary=u'Modified IPA location "{loc}"'.format(loc=self.idnsname),
             result=self.filter_attrs(self.update_keys | set(extra_keys))
         ), result)
+
+    def add_server_to_location(
+            self, server_name, weight=100, relative_weight=u"100.0%"):
+        self.attrs.setdefault('servers_server', []).append(server_name)
+        self.servers[server_name] = {
+            'cn': [server_name],
+            'ipalocationweight': [unicode(weight)],
+            'location_relative_weight': [relative_weight]
+        }
+
+    def remove_server_from_location(self, server_name):
+        if 'servers_server' in self.attrs:
+            try:
+                self.attrs['servers_server'].remove(server_name)
+            except ValueError:
+                pass
+            else:
+                if not self.attrs['servers_server']:
+                    del self.attrs['servers_server']
+        try:
+            del self.servers[server_name]
+        except KeyError:
+            pass
diff --git a/ipatests/test_xmlrpc/tracker/server_plugin.py b/ipatests/test_xmlrpc/tracker/server_plugin.py
new file mode 100644
index 0000000000000000000000000000000000000000..42e63d78f251623fc5088d79a9b0da439c52113b
--- /dev/null
+++ b/ipatests/test_xmlrpc/tracker/server_plugin.py
@@ -0,0 +1,110 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+from __future__ import absolute_import
+
+from ipapython.dn import DN
+from ipatests.util import assert_deepequal
+from ipatests.test_xmlrpc.tracker.base import Tracker
+
+
+class ServerTracker(Tracker):
+    """Tracker for IPA Location tests"""
+    retrieve_keys = {
+        'cn', 'dn', 'ipamaxdomainlevel', 'ipamindomainlevel',
+        'iparepltopomanagedsuffix_topologysuffix', 'ipalocation_location',
+        'ipalocationweight',
+    }
+    retrieve_all_keys = retrieve_keys | {'objectclass'}
+    create_keys = retrieve_keys | {'objectclass'}
+    find_keys = {
+        'cn', 'dn', 'ipamaxdomainlevel', 'ipamindomainlevel',
+        'ipalocationweight',
+    }
+    find_all_keys = retrieve_all_keys
+    update_keys = {
+        'cn', 'ipamaxdomainlevel', 'ipamindomainlevel',
+        'ipalocation_location', 'ipalocationweight',
+    }
+
+    def __init__(self, name):
+        super(ServerTracker, self).__init__(default_version=None)
+        self.server_name = name
+        self.dn = DN(
+            ('cn', self.server_name),
+            'cn=masters,cn=ipa,cn=etc',
+            self.api.env.basedn
+        )
+        self.exists = True  # we cannot add server manually using server-add
+        self.attrs = dict(
+            dn=self.dn,
+            cn=[self.server_name],
+            iparepltopomanagedsuffix_topologysuffix=[u'domain', u'ca'],
+            objectclass=[
+                u"ipalocationmember",
+                u"ipaReplTopoManagedServer",
+                u"top",
+                u"ipaConfigObject",
+                u"nsContainer",
+                u"ipaSupportedDomainLevelConfig"
+            ],
+            ipamaxdomainlevel=[u"1"],
+            ipamindomainlevel=[u"0"],
+        )
+        self.exists = True
+
+    def make_retrieve_command(self, all=False, raw=False):
+        """Make function that retrieves this server using server-show"""
+        return self.make_command(
+            'server_show', self.name, all=all, raw=raw
+        )
+
+    def make_find_command(self, *args, **kwargs):
+        """Make function that finds servers using server-find"""
+        return self.make_command('server_find', *args, **kwargs)
+
+    def make_update_command(self, updates):
+        """Make function that modifies the server using server-mod"""
+        return self.make_command('server_mod', self.name, **updates)
+
+    def check_retrieve(self, result, all=False, raw=False):
+        """Check `server-show` command result"""
+        if all:
+            expected = self.filter_attrs(self.retrieve_all_keys)
+        else:
+            expected = self.filter_attrs(self.retrieve_keys)
+        assert_deepequal(dict(
+            value=self.server_name,
+            summary=None,
+            result=expected,
+        ), result)
+
+    def check_find(self, result, all=False, raw=False):
+        """Check `server-find` command result"""
+        if all:
+            expected = self.filter_attrs(self.find_all_keys)
+        else:
+            expected = self.filter_attrs(self.find_keys)
+        assert_deepequal(dict(
+            count=1,
+            truncated=False,
+            summary=u'1 IPA server matched',
+            result=[expected],
+        ), result)
+
+    def check_find_nomatch(self, result):
+        """ Check 'server-find' command result when no match is expected """
+        assert_deepequal(dict(
+            count=0,
+            truncated=False,
+            summary=u'0 IPA servers matched',
+            result=[],
+        ), result)
+
+    def check_update(self, result, extra_keys=()):
+        """Check `server-update` command result"""
+        assert_deepequal(dict(
+            value=self.server_name,
+            summary=u'Modified IPA server "{server}"'.format(server=self.name),
+            result=self.filter_attrs(self.update_keys | set(extra_keys))
+        ), result)
-- 
2.5.5

-- 
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