From 2431e18e14bcffb83e2ff380f320a653e251aef8 Mon Sep 17 00:00:00 2001 From: Fred Barnes Date: Tue, 19 Nov 2019 13:11:06 +0000 Subject: [PATCH] Allow service address PTR to point at shared name This adds an option --map_to_shared_name option for service addition and update that causes the reverse PTR DNS record to point back to the FQDN of the shared-name (within a resource-group). If the 'sa_aliases' flag is set for the shared-name, an address alias from the shared-name to the service address is created (and removed on service address deletion). Change-Id: Icdf078985321a5d035f3d5ac41a45b3f1e4d2393 Addresses-Issue: Jira/AQUILON-6374 Tested-by: Aquilon Template Build testing and verification Reviewed-by: Tomasz Kotarba --- etc/input.xml | 2 + .../worker/commands/add_service_address.py | 80 ++++++++++-- .../worker/commands/del_service_address.py | 37 +++++- .../worker/commands/update_service_address.py | 42 +++++- tests/broker/orderedsuite.py | 6 +- .../test_add_service_address_sn_aliases.py | 121 ++++++++++++++++++ .../test_del_service_address_sn_aliases.py | 59 +++++++++ 7 files changed, 324 insertions(+), 23 deletions(-) create mode 100644 tests/broker/test_add_service_address_sn_aliases.py create mode 100644 tests/broker/test_del_service_address_sn_aliases.py diff --git a/etc/input.xml b/etc/input.xml index 3636d0a9f..41138f0aa 100755 --- a/etc/input.xml +++ b/etc/input.xml @@ -6425,6 +6425,7 @@ + @@ -6507,6 +6508,7 @@ + diff --git a/lib/aquilon/worker/commands/add_service_address.py b/lib/aquilon/worker/commands/add_service_address.py index 5f44f4981..00f2dbf32 100755 --- a/lib/aquilon/worker/commands/add_service_address.py +++ b/lib/aquilon/worker/commands/add_service_address.py @@ -17,18 +17,33 @@ # limitations under the License. """Contains the logic for `aq add service address`.""" -from aquilon.exceptions_ import ArgumentError from aquilon.aqdb.column_types import AqStr -from aquilon.aqdb.model import ServiceAddress, Host, Fqdn, DnsDomain, Bunker +from aquilon.aqdb.model import ( + BundleResource, + Bunker, + DnsDomain, + Fqdn, + Host, + ResourceGroup, + ServiceAddress, + SharedServiceName, +) +from aquilon.exceptions_ import ArgumentError from aquilon.utils import validate_nlist_key from aquilon.worker.broker import BrokerCommand -from aquilon.worker.dbwrappers.dns import grab_address -from aquilon.worker.dbwrappers.interface import get_interfaces, generate_ip +from aquilon.worker.dbwrappers.change_management import ChangeManagement +from aquilon.worker.dbwrappers.dns import ( + add_address_alias, + grab_address, +) +from aquilon.worker.dbwrappers.interface import ( + generate_ip, + get_interfaces, +) from aquilon.worker.dbwrappers.location import get_default_dns_domain from aquilon.worker.dbwrappers.resources import get_resource_holder from aquilon.worker.dbwrappers.search import search_next from aquilon.worker.processes import DSDBRunner -from aquilon.worker.dbwrappers.change_management import ChangeManagement class CommandAddServiceAddress(BrokerCommand): @@ -36,10 +51,11 @@ class CommandAddServiceAddress(BrokerCommand): required_parameters = ["name"] - def render(self, session, logger, plenaries, service_address, shortname, prefix, - dns_domain, ip, ipfromtype, name, interfaces, hostname, cluster, metacluster, - resourcegroup, network_environment, map_to_primary, shared, - comments, user, justification, reason, exporter, + def render(self, session, logger, plenaries, service_address, shortname, + prefix, dns_domain, ip, ipfromtype, name, interfaces, + hostname, cluster, metacluster, resourcegroup, + network_environment, map_to_primary, map_to_shared_name, + shared, comments, user, justification, reason, exporter, default_dns_domain_from, **kwargs): """Extend the superclass method to render this command. @@ -64,6 +80,8 @@ def render(self, session, logger, plenaries, service_address, shortname, prefix, :param network_environment: a network environment (default: internal) :param map_to_primary: True if the reverse PTR should point to the primary name + :param map_to_shared_name: True if the reverse PTR should point to + a shared-name within the same resourcegroup :param shared: allow the address to be used multiple times :param comments: a string with comments :param user: a string with the principal / user who invoked the command @@ -159,6 +177,16 @@ def render(self, session, logger, plenaries, service_address, shortname, prefix, ip = generate_ip(session, logger, None, net_location_set, ip=ip, ipfromtype=ipfromtype) + # if in a resource-group, look for a sibling SharedServiceName resource + sibling_ssn = None + if (isinstance(holder, BundleResource) and + isinstance(holder.resourcegroup, ResourceGroup)): + for res in holder.resources: + if isinstance(res, SharedServiceName): + # this one + sibling_ssn = res + break + # TODO: add allow_multi=True dbdns_rec, newly_created = grab_address(session, service_address, ip, network_environment, @@ -167,11 +195,27 @@ def render(self, session, logger, plenaries, service_address, shortname, prefix, require_grn=False) ip = dbdns_rec.ip - if map_to_primary: - if not isinstance(toplevel_holder, Host): + if map_to_primary and map_to_shared_name: + raise ArgumentError("Cannot use --map_to_primary and " + "--map_to_shared_name together") + elif map_to_shared_name: + # if the holder is a resource-group that has a SharedServiceName + # resource, then set the PTR record as the SharedServiceName's FQDN + if sibling_ssn: + dbdns_rec.reverse_ptr = sibling_ssn.fqdn + else: + raise ArgumentError("--map_to_shared_name specified, but no " + "shared service name in {0:l}". + format(holder)) + elif map_to_primary: + if isinstance(toplevel_holder, Host): + dbdns_rec.reverse_ptr = \ + toplevel_holder.hardware_entity.primary_name.fqdn + else: raise ArgumentError("The --map_to_primary option works only " - "for host-based service addresses.") - dbdns_rec.reverse_ptr = toplevel_holder.hardware_entity.primary_name.fqdn + "for host-based service addresses or " + "within a resource-group where a " + "SharedServiceName resource exists.") dbifaces = [] if interfaces: @@ -193,6 +237,16 @@ def render(self, session, logger, plenaries, service_address, shortname, prefix, session.flush() + # if we have a sibling SharedServiceName where service-address + # aliases is set, add a new address-alias pointing at the IP + if sibling_ssn and sibling_ssn.sa_aliases: + add_address_alias(session, logger, config=self.config, + dbsrcfqdn=sibling_ssn.fqdn, + dbtargetfqdn=dbdns_rec.fqdn, + ttl=None, grn=None, eon_id=None, + comments=None, exporter=exporter, + flush_session=True) + plenaries.add(holder.holder_object) plenaries.add(dbsrv) diff --git a/lib/aquilon/worker/commands/del_service_address.py b/lib/aquilon/worker/commands/del_service_address.py index 72ac6304d..0e7b0efa4 100755 --- a/lib/aquilon/worker/commands/del_service_address.py +++ b/lib/aquilon/worker/commands/del_service_address.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python # -*- cpy-indent-level: 4; indent-tabs-mode: nil -*- # ex: set expandtab softtabstop=4 shiftwidth=4: # -# Copyright (C) 2012,2013,2014,2015,2016,2017,2018 Contributor +# Copyright (C) 2012-2019 Contributor # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,14 +16,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +from aquilon.aqdb.model import ( + AddressAlias, + BundleResource, + ResourceGroup, + ServiceAddress, + SharedServiceName, +) from aquilon.exceptions_ import ArgumentError -from aquilon.aqdb.model import ServiceAddress from aquilon.worker.broker import BrokerCommand +from aquilon.worker.dbwrappers.change_management import ChangeManagement from aquilon.worker.dbwrappers.dns import delete_dns_record from aquilon.worker.dbwrappers.resources import get_resource_holder from aquilon.worker.dbwrappers.service_instance import check_no_provided_service from aquilon.worker.processes import DSDBRunner -from aquilon.worker.dbwrappers.change_management import ChangeManagement class CommandDelServiceAddress(BrokerCommand): @@ -59,6 +66,30 @@ def render(self, session, logger, plenaries, name, hostname, cluster, metacluste holder.resources.remove(dbsrv) if not dbdns_rec.service_addresses: + # if we're in a resource-group and a shared-service-name exists + # that has sa_aliases set, and there'a an alias pointing at + # ourselves, remove it. + + sibling_ssn = None + if (isinstance(holder, BundleResource) and + isinstance(holder.resourcegroup, ResourceGroup)): + for res in holder.resources: + if isinstance(res, SharedServiceName): + # this one + sibling_ssn = res + break + + if sibling_ssn and sibling_ssn.sa_aliases: + # look for one match against this target only + for rr in sibling_ssn.fqdn.dns_records: + if not isinstance(rr, AddressAlias): + continue + if rr.target != dbdns_rec.fqdn: + continue + + delete_dns_record(rr, exporter=exporter) + break + delete_dns_record(dbdns_rec, exporter=exporter) session.flush() diff --git a/lib/aquilon/worker/commands/update_service_address.py b/lib/aquilon/worker/commands/update_service_address.py index cbcaf9bdd..20797ee8e 100755 --- a/lib/aquilon/worker/commands/update_service_address.py +++ b/lib/aquilon/worker/commands/update_service_address.py @@ -1,7 +1,8 @@ +#!/usr/bin/env python # -*- cpy-indent-level: 4; indent-tabs-mode: nil -*- # ex: set expandtab softtabstop=4 shiftwidth=4: # -# Copyright (C) 2015,2016,2017 Contributor +# Copyright (C) 2015-2017,2019 Contributor # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,9 +16,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from aquilon.exceptions_ import ArgumentError -from aquilon.aqdb.model import ServiceAddress, Host, NetworkEnvironment +from aquilon.aqdb.model import ( + BundleResource, + Host, + NetworkEnvironment, + ResourceGroup, + ServiceAddress, + SharedServiceName, +) from aquilon.aqdb.model.network import get_net_id_from_ip +from aquilon.exceptions_ import ArgumentError from aquilon.worker.broker import BrokerCommand from aquilon.worker.dbwrappers.dns import update_address from aquilon.worker.dbwrappers.interface import get_interfaces @@ -31,8 +39,9 @@ class CommandUpdateServiceAddress(BrokerCommand): required_parameters = ["name"] - def render(self, session, logger, plenaries, ip, name, interfaces, hostname, cluster, - metacluster, resourcegroup, network_environment, map_to_primary, + def render(self, session, logger, plenaries, ip, name, interfaces, + hostname, cluster, metacluster, resourcegroup, + network_environment, map_to_primary, map_to_shared_name, comments, user, justification, reason, **arguments): holder = get_resource_holder(session, logger, hostname, cluster, metacluster, resourcegroup, compel=True) @@ -73,6 +82,10 @@ def render(self, session, logger, plenaries, ip, name, interfaces, hostname, clu if comments is not None: dbsrv.comments = comments + if map_to_primary and map_to_shared_name: + raise ArgumentError("Cannot use --map_to_primary and " + "--map_to_shared_name together") + if map_to_primary is not None: if not isinstance(toplevel_holder, Host): raise ArgumentError("The --map_to_primary option works only " @@ -82,6 +95,25 @@ def render(self, session, logger, plenaries, ip, name, interfaces, hostname, clu else: dbsrv.dns_record.reverse_ptr = None + if map_to_shared_name: + # if the holder is a resource-group that has a SharedServiceName + # resource, then set the PTR record as the SharedServiceName's FQDN + + sibling_ssn = None + if (isinstance(holder, BundleResource) and + isinstance(holder.resourcegroup, ResourceGroup)): + for res in holder.resources: + if isinstance(res, SharedServiceName): + # this one + sibling_ssn = res + break + + if sibling_ssn: + dbsrv.dns_record.reverse_ptr = sibling_ssn.fqdn + else: + raise ArgumentError("--map_to_shared_name specified, but no " + "shared service name") + session.flush() with plenaries.get_key(): diff --git a/tests/broker/orderedsuite.py b/tests/broker/orderedsuite.py index 80f917cdf..a3262dad0 100755 --- a/tests/broker/orderedsuite.py +++ b/tests/broker/orderedsuite.py @@ -93,6 +93,7 @@ from .test_add_sandbox import TestAddSandbox from .test_add_service import TestAddService from .test_add_service_address import TestAddServiceAddress +from .test_add_service_address_sn_aliases import TestAddServiceAddressSNAliases from .test_add_share import TestAddShare from .test_add_shared_service_name import TestAddSharedServiceName from .test_add_srv_record import TestAddSrvRecord @@ -204,6 +205,7 @@ from .test_del_sandbox import TestDelSandbox from .test_del_service import TestDelService from .test_del_service_address import TestDelServiceAddress +from .test_del_service_address_sn_aliases import TestDelServiceAddressSNAliases from .test_del_share import TestDelShare from .test_del_shared_service_name import TestDelSharedServiceName from .test_del_srv_record import TestDelSrvRecord @@ -406,7 +408,7 @@ class BrokerTestSuite(unittest.TestSuite): TestAddResourceGroup, TestAddShare, TestAddFilesystem, TestAddApplication, TestAddIntervention, TestAddHostlink, TestAddRebootSchedule, TestAddRebootIntervention, - TestAddSharedServiceName, + TestAddSharedServiceName, TestAddServiceAddressSNAliases, TestFlush, TestMakeAquilon, TestMakeCluster, TestCluster, TestAddAllowedPersonality, @@ -504,7 +506,7 @@ class BrokerTestSuite(unittest.TestSuite): TestUnbindFeature, TestDel10GigHardware, TestDelVirtualHardware, TestUnbindCluster, TestUncluster, - TestDelSharedServiceName, + TestDelServiceAddressSNAliases, TestDelSharedServiceName, TestDelShare, TestDelFilesystem, TestDelHostlink, TestDelRebootIntervention, TestDelRebootSchedule, TestDelIntervention, TestDelApplication, diff --git a/tests/broker/test_add_service_address_sn_aliases.py b/tests/broker/test_add_service_address_sn_aliases.py new file mode 100644 index 000000000..5f2fc3e75 --- /dev/null +++ b/tests/broker/test_add_service_address_sn_aliases.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python +# -*- cpy-indent-level: 4; indent-tabs-mode: nil -*- +# ex: set expandtab softtabstop=4 shiftwidth=4: +# +# Copyright (C) 2019 Contributor +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module for testing adding service addresses mapped back to shared names + with address alias creation.""" + +import unittest + +if __name__ == "__main__": + import utils + utils.import_depends() + +from brokertest import TestBrokerCommand + + +class TestAddServiceAddressSNAliases(TestBrokerCommand): + + def test_000_no_sn_map_ptr(self): + # ensure we cannot map-to-shared-name if none exists + ip = self.net['np_bucket2_vip'].usable[2] + command = ['add_service_address', '--cluster=utvcs1', + '--name=utvcs1sa1', + '--service_address=utvcs1sa1.aqd-unittest.ms.com', + '--ip', ip, '--map_to_shared_name'] + err = self.badrequesttest(command) + self.matchoutput(err, '--map_to_shared_name specified, ' + 'but no shared service name in', command) + + def test_000_no_map_to_primary_and_shared_name(self): + # ensure we cannot use both --map_to_primary and --map_to_shared_name + # options. + ip = self.net['np_bucket2_vip'].usable[2] + command = ['add_service_address', '--cluster=utvcs1', + '--name=utvcs1sa1', + '--service_address=utvcs1sa1.aqd-unittest.ms.com', + '--ip', ip, '--map_to_primary', '--map_to_shared_name'] + err = self.badrequesttest(command) + self.matchoutput(err, 'Cannot use --map_to_primary and ' + '--map_to_shared_name together', command) + + def test_005_add_empty_resourcegroup(self): + command = ['add_resourcegroup', '--cluster=utvcs1', + '--resourcegroup=utvcs1ifset3'] + self.successtest(command) + + def test_010_no_sn_map_ptr(self): + # ensure we cannot map-to-shared-name if none in a resourcegroup + ip = self.net['np_bucket2_vip'].usable[2] + command = ['add_service_address', '--resourcegroup=utvcs1ifset3', + '--name=utvcs1sa1', + '--service_address=utvcs1sa1.aqd-unittest.ms.com', + '--ip', ip, '--map_to_shared_name'] + err = self.badrequesttest(command) + self.matchoutput(err, '--map_to_shared_name specified, ' + 'but no shared service name in', command) + + def test_015_del_empty_resourcegroup(self): + command = ['del_resourcegroup', '--cluster=utvcs1', + '--resourcegroup=utvcs1ifset3'] + self.successtest(command) + + def test_020_mapped_to_shared_name_aa(self): + # create a service address mapped to the shared-name with + # address alias creation + ip = self.net['np_bucket2_vip'].usable[2] + service_addr = 'utvcs1sa1.aqd-unittest.ms.com' + self.dsdb_expect_add(service_addr, ip) + command = ['add_service_address', '--resourcegroup=utvcs1ifset', + '--name=utvcs1sa1', '--service_address', service_addr, + '--ip', ip, '--map_to_shared_name'] + self.successtest(command) + self.dsdb_verify() + + def test_020_mapped_to_shared_name_noaa(self): + # create a service address mapped to the shared-name without + # address alias creation + ip = self.net['np_bucket2_vip'].usable[3] + service_addr = 'utvcs1sa2.aqd-unittest.ms.com' + self.dsdb_expect_add(service_addr, ip) + command = ['add_service_address', '--resourcegroup=utvcs1ifset2', + '--name=utvcs1sa2', '--service_address', service_addr, + '--ip', ip, '--map_to_shared_name'] + self.successtest(command) + self.dsdb_verify() + + def test_025_utvcs1pn1_addr_alias(self): + command = ['search_dns', '--fqdn=utvcs1pn1.aqd-unittest.ms.com', + '--fullinfo'] + out = self.commandtest(command) + self.matchoutput(out, 'Address Alias: utvcs1pn1.aqd-unittest.ms.com', + command) + self.matchoutput(out, 'Target: utvcs1sa1.aqd-unittest.ms.com', + command) + + def test_025_utvcs1pn2_noaddr_alias(self): + command = ['search_dns', '--fqdn=utvcs1pn2.aqd-unittest.ms.com', + '--fullinfo'] + out = self.commandtest(command) + self.matchclean(out, 'Address Alias: utvcs1pn2.aqd-unittest.ms.com', + command) + self.matchclean(out, 'Target: utvcs1sa2.aqd-unittest.ms.com', command) + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase( + TestAddServiceAddressSNAliases) + unittest.TextTestRunner(verbosity=2).run(suite) diff --git a/tests/broker/test_del_service_address_sn_aliases.py b/tests/broker/test_del_service_address_sn_aliases.py new file mode 100644 index 000000000..8e005b114 --- /dev/null +++ b/tests/broker/test_del_service_address_sn_aliases.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python +# -*- cpy-indent-level: 4; indent-tabs-mode: nil -*- +# ex: set expandtab softtabstop=4 shiftwidth=4: +# +# Copyright (C) 2019 Contributor +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module for testing removal of service addresses mapped back to shared + names with address alias creation.""" + +import unittest + +if __name__ == "__main__": + import utils + utils.import_depends() + +from brokertest import TestBrokerCommand + + +class TestDelServiceAddressSNAliases(TestBrokerCommand): + + def test_010_remove_sa_aa(self): + ip = self.net['np_bucket2_vip'].usable[2] + self.dsdb_expect_delete(ip) + command = ['del_service_address', '--resourcegroup=utvcs1ifset', + '--name=utvcs1sa1'] + self.successtest(command) + self.dsdb_verify() + + def test_010_remove_sa_noaa(self): + ip = self.net['np_bucket2_vip'].usable[3] + self.dsdb_expect_delete(ip) + command = ['del_service_address', '--resourcegroup=utvcs1ifset2', + '--name=utvcs1sa2'] + self.successtest(command) + self.dsdb_verify() + + def test_020_check_no_utvcs1sa1_alias(self): + command = ['search_dns', '--fqdn=utvcs1pn1.aqd-unittest.ms.com', + '--fullinfo'] + out = self.commandtest(command) + self.matchclean(out, 'Address Alias: utvcs1pn1.aqd-unittest.ms.com', + command) + + +if __name__ == '__main__': + suite = unittest.TestLoader().loadTestsFromTestCase( + TestDelServiceAddressSNAliases) + unittest.TextTestRunner(verbosity=2).run(suite)