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)