On 03.06.2016 08:53, Petr Spacek wrote:
On 2.6.2016 17:53, Martin Basti wrote:
<snip>
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.
I forgot to add, this will be in next batch of patches (you may see that there
are not marked DNS servers in output of location show), I do not see reason to
open ticket when the current one is not finished.

+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
I had to add top object class to the plugin and tests to make tests pass.
Patch is attached.

CondACK: Fix this before pushing somehow.


Updated and heavily rebased patches attached.
From 0ba65ac9702d04fdccab7809af51c166e82e3379 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 486f84785aba85edfd8ea3cfb8cc6968e51d4c8a 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 6dad45daf8803ccf923795992690a98ed9486fe4 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 +
 ipaserver/plugins/location.py         | 149 ++++++++++++++++++++++++++++++++++
 8 files changed, 230 insertions(+), 2 deletions(-)
 create mode 100644 ipaserver/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 44bf64d17c726916e8bee33118d4dd453a3d8b65..bfdb9043d081c684523b0527afea6fbd28b611aa 100644
--- a/API.txt
+++ b/API.txt
@@ -2787,6 +2787,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 9d2e234a8536361b3d405c9b171a0587937abcda..de7ad35f94a7b008abddcd5899148b23fe9fc2a8 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: ipalib: introduce API schema plugins
+IPA_API_VERSION_MINOR=174
+# 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 58f9b94ebe129e707ca3e804ea0119272576007d..a2cbfdbcda07429478f97c62cc6896890f2f7979 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/ipaserver/plugins/location.py b/ipaserver/plugins/location.py
new file mode 100644
index 0000000000000000000000000000000000000000..7c0aab17435a10edd85aeb6d4911a2be1f983951
--- /dev/null
+++ b/ipaserver/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 ipaserver.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 = ['top', '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 ce240f7636542b5c080df482b7a986d8363b3c0d 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 d3efa0885f8ee340dc0401327943f1d1fa176f82 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
---
 ipaserver/plugins/baseldap.py | 14 ++++++++------
 1 file changed, 8 insertions(+), 6 deletions(-)

diff --git a/ipaserver/plugins/baseldap.py b/ipaserver/plugins/baseldap.py
index bbd8ba146ead81857bbc4c2aee550b855b846be5..62b726da1a0baef9fc9fa4bb386a2101ca1f10b2 100644
--- a/ipaserver/plugins/baseldap.py
+++ b/ipaserver/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 b5d357a4c6b10b53196e913621552271e065a80b 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 +-
 ipaserver/plugins/location.py |   2 +-
 ipaserver/plugins/server.py   | 104 ++++++++++++++++++++++++++++++++++++++++--
 4 files changed, 121 insertions(+), 9 deletions(-)

diff --git a/API.txt b/API.txt
index bfdb9043d081c684523b0527afea6fbd28b611aa..991275079ee276070ac10c2161fe791865529e2c 100644
--- a/API.txt
+++ b/API.txt
@@ -4006,14 +4006,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)
@@ -4024,6 +4026,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 de7ad35f94a7b008abddcd5899148b23fe9fc2a8..70bd7c91cad76d146c62292091ae1a9cba0506ff 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=174
-# Last change: mbasti - location-* commands
+IPA_API_VERSION_MINOR=175
+# Last change: mbasti - server-mod: locations added
diff --git a/ipaserver/plugins/location.py b/ipaserver/plugins/location.py
index 7c0aab17435a10edd85aeb6d4911a2be1f983951..35435f451d92282c87e41ca8720d8900f24111b7 100644
--- a/ipaserver/plugins/location.py
+++ b/ipaserver/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/ipaserver/plugins/server.py b/ipaserver/plugins/server.py
index 6faaf8ec5a98501ccdcb3dbc5983ce4a27baacba..3192a588b487c217d78343675fc89e467d8021a9 100644
--- a/ipaserver/plugins/server.py
+++ b/ipaserver/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 b504e2c475713dd3d413194ff7acd4ab19fdaf20 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 ++--
 ipaclient/plugins/location.py | 35 +++++++++++++++++++++++++++
 ipaserver/plugins/location.py | 55 +++++++++++++++++++++++++++++++++++++++++--
 ipaserver/plugins/server.py   | 16 +++++++++++++
 6 files changed, 110 insertions(+), 5 deletions(-)
 create mode 100644 ipaclient/plugins/location.py

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 991275079ee276070ac10c2161fe791865529e2c..f17093022d54d5cd0ccbf1863f6def0589bbf8c9 100644
--- a/API.txt
+++ b/API.txt
@@ -2837,13 +2837,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 70bd7c91cad76d146c62292091ae1a9cba0506ff..4ada7467a646b4b7162fd56248399a65f8600663 100644
--- a/VERSION
+++ b/VERSION
@@ -90,5 +90,5 @@ IPA_DATA_VERSION=20100614120000
 #                                                      #
 ########################################################
 IPA_API_VERSION_MAJOR=2
-IPA_API_VERSION_MINOR=175
-# Last change: mbasti - server-mod: locations added
+IPA_API_VERSION_MINOR=176
+# Last change: mbasti - location-show: list servers in the location
diff --git a/ipaclient/plugins/location.py b/ipaclient/plugins/location.py
new file mode 100644
index 0000000000000000000000000000000000000000..b3b6026c095e309d020687099c1f62661cbaa1cc
--- /dev/null
+++ b/ipaclient/plugins/location.py
@@ -0,0 +1,35 @@
+#
+# Copyright (C) 2016  FreeIPA Contributors see COPYING for license
+#
+
+from ipaclient.frontend import MethodOverride
+from ipalib import _
+from ipalib.plugable import Registry
+
+
+register = Registry()
+
+
+@register(override=True)
+class location_show(MethodOverride):
+    def output_for_cli(self, textui, output, *keys, **options):
+        rv = super(location_show, self).output_for_cli(
+            textui, output, *keys, **options)
+
+        servers = output.get('servers', {})
+        first = True
+        for hostname, details in servers.items():
+            if first:
+                textui.print_indented(_("Servers details:"), indent=1)
+                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)
+
+        return rv
diff --git a/ipaserver/plugins/location.py b/ipaserver/plugins/location.py
index 35435f451d92282c87e41ca8720d8900f24111b7..1edda8e25211b29d1caabaa9ef0ba48e063fd088 100644
--- a/ipaserver/plugins/location.py
+++ b/ipaserver/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 ipaserver.plugins.baseldap import (
@@ -20,6 +24,7 @@ from ipaserver.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,43 @@ 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
diff --git a/ipaserver/plugins/server.py b/ipaserver/plugins/server.py
index 3192a588b487c217d78343675fc89e467d8021a9..511f8135b14edb8d6af7fc6a0a04ccd8a110a00b 100644
--- a/ipaserver/plugins/server.py
+++ b/ipaserver/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 2fa7baa537bbf11bd2ba134396270e2abde6e02d 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 +
 ipaserver/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/ipaserver/plugins/location.py b/ipaserver/plugins/location.py
index 1edda8e25211b29d1caabaa9ef0ba48e063fd088..32306648c06ed1cf2cfb77400680f87b8547b0b6 100644
--- a/ipaserver/plugins/location.py
+++ b/ipaserver/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 f97720a0cb2411234e3fca5dc4a23d90269fcdfb 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 |  42 +++++++--
 ipatests/test_xmlrpc/tracker/server_plugin.py   | 110 ++++++++++++++++++++++++
 4 files changed, 240 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..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..8901b7e0015529ff612aa924647e33db84e42e2c 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)
 
@@ -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