<snip>

updated patches attached
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

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

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 .')
+
+    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 3610239eb261a5161714ddf148d7bc9fb02264cd 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 1384f6b8f9c2cdf5ef0edfce1f120039a8d7d0e0 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 9c77fd62e9a8d8c7147f5ba055f4a9f30ee8e559..505ba4e4090e1ea577b8ab433f27a33f3599e113 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 02afaf51456a46e4db0164842d224ab40683cb97 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 efba55aa75e342f566a40a0d10887e173b8a83fc..2c6226d8af3ee65540f6efd8203cb3296998f111 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 d634f302ee7997577479c0ee856e9e6064212568 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 2c6226d8af3ee65540f6efd8203cb3296998f111..6ef3b72a4782c3f296151db4cc5062bd3160635f 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'),
+        ),
+    )
+
+    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 ecc8fe013851047bec69a3fd0a8c5612a22da443 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: prevent to remove used locations

User should be notified that location is used by IPA server(s) and
deletion should be aborted without --force option.

Referint plugin is configured to remove references of deleted locations.

https://fedorahosted.org/freeipa/ticket/2008
---
 API.txt                            |  3 ++-
 VERSION                            |  4 ++--
 install/updates/25-referint.update |  1 +
 ipalib/plugins/location.py         | 25 +++++++++++++++++++++++++
 4 files changed, 30 insertions(+), 3 deletions(-)

diff --git a/API.txt b/API.txt
index 8ea71797e99111c2844426ac84590dda92b8c1f2..c8f3712cb64a4fe28994e392a4708b6f69836738 100644
--- a/API.txt
+++ b/API.txt
@@ -2772,9 +2772,10 @@ output: Entry('result')
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
 output: PrimaryKey('value')
 command: location_del
-args: 1,2,3
+args: 1,3,3
 arg: DNSNameParam('idnsname+', cli_name='name')
 option: Flag('continue', autofill=True, cli_name='continue', default=False)
+option: Flag('force', autofill=True, default=False)
 option: Str('version?')
 output: Output('result', type=[<type 'dict'>])
 output: Output('summary', type=[<type 'unicode'>, <type 'NoneType'>])
diff --git a/VERSION b/VERSION
index 6ab6dd68a903116280f36e261e1aed962b3b4fa4..336353f6c520dba3c1b725e55231c6f1cb008524 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=173
-# Last change: mbasti - location-show: list servers in the location
+IPA_API_VERSION_MINOR=174
+# Last change: mbasti - server-del: prevent to remove used locations
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 6ef3b72a4782c3f296151db4cc5062bd3160635f..83a5438bfa0f2830004b67d2676c0e1b9c1c1736 100644
--- a/ipalib/plugins/location.py
+++ b/ipalib/plugins/location.py
@@ -14,7 +14,9 @@ from ipalib import (
     Str,
     DNSNameParam,
     output,
+    Flag,
 )
+from ipalib.errors import DependentEntry
 from ipalib.plugable import Registry
 from ipalib.plugins.baseldap import (
     LDAPCreate,
@@ -137,6 +139,29 @@ class location_del(LDAPDelete):
 
     msg_summary = _('Deleted IPA location "%(value)s"')
 
+    takes_options = LDAPDelete.takes_options + (
+        Flag(
+            'force',
+            label=_('Force'),
+            doc=_('force location removal'),
+        ),
+    )
+
+    def pre_callback(self, ldap, dn, *keys, **options):
+        assert isinstance(dn, DN)
+        if not options.get('force'):
+            servers = self.api.Command.server_find(
+                in_location=keys[-1])['result']
+            location_members = u', '.join(
+                server['cn'][0] for server in servers)
+            if location_members:
+                raise DependentEntry(
+                    label=_('IPA Server(s)'),
+                    key=keys[-1],
+                    dependent=location_members
+                )
+        return dn
+
 
 @register()
 class location_mod(LDAPUpdate):
-- 
2.5.5

From f284893cd9888ecbcb30b5fd8b1bad1399baca73 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    | 107 ++++++++++++++++++++++-
 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, 258 insertions(+), 7 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..7417ffc4df70a778b957ce597097762fde90eaa8 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):
@@ -111,3 +119,100 @@ 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()
+
+
+@pytest.mark.tier1
+class TestReferintLocation(XMLRPC_test):
+    def test_location_referint(self, location, server):
+        location.ensure_exists()
+        server.update(
+            dict(ipalocation_location=location.idnsname_obj),
+            expected_updates=dict(
+                ipalocation_location=[location.idnsname_obj],
+            )
+        )
+        command = location.make_delete_command(force=True)
+        command()
+        location.track_delete()
+        # here referint plugins should remove location from servers
+        del server.attrs['ipalocation_location']
+        server.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..45c6d4b93d1bb1ede79c0578cd08e7a4fbdefb91 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,15 +40,17 @@ 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=False):
         """Make function that removes this location using location-del"""
-        return self.make_command('location_del', self.idnsname)
+        return self.make_command('location_del', self.idnsname, force=force)
 
     def make_retrieve_command(self, all=False, raw=False):
         """Make function that retrieves this location using location-show"""
@@ -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