From cbb93977cd0e427ba83c2f3dff31668901ad4699 Mon Sep 17 00:00:00 2001 From: Douglas Bagnall Date: Wed, 28 Oct 2015 12:20:37 +1300 Subject: samba-tool: add sites subnet subcommands This allows you to add, remove, or shift subnets. Signed-off-by: Douglas Bagnall Signed-off-by: Andrew Bartlett Reviewed-by: Garming Sam Reviewed-by: Andrew Bartlett --- python/samba/netcmd/sites.py | 121 ++++++++++++++++++++- python/samba/subnets.py | 186 +++++++++++++++++++++++++++++++++ python/samba/tests/samba_tool/sites.py | 55 ++++++++++ source4/dsdb/tests/python/sites.py | 76 ++++++++++++++ 4 files changed, 436 insertions(+), 2 deletions(-) create mode 100644 python/samba/subnets.py diff --git a/python/samba/netcmd/sites.py b/python/samba/netcmd/sites.py index 53091a251cf..f0c792d90ee 100644 --- a/python/samba/netcmd/sites.py +++ b/python/samba/netcmd/sites.py @@ -16,7 +16,7 @@ # along with this program. If not, see . # -from samba import sites +from samba import sites, subnets from samba.samdb import SamDB import samba.getopt as options from samba.auth import system_session @@ -102,10 +102,127 @@ class cmd_sites_delete(Command): self.outf.write("Site %s removed!\n" % sitename) +class cmd_sites_subnet_create(Command): + """Create a new subnet.""" + synopsis = "%prog [options]" + takes_args = ["subnetname", "site_of_subnet"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, site_of_subnet, H=None, sambaopts=None, + credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.create_subnet(samdb, samdb.get_config_basedn(), subnetname, + site_of_subnet) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error while creating subnet %s: %s" % + (subnetname, e)) + + self.outf.write("Subnet %s created !\n" % subnetname) + + +class cmd_sites_subnet_delete(Command): + """Delete an existing subnet.""" + + synopsis = "%prog [options]" + + takes_args = ["subnetname"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, H=None, sambaopts=None, credopts=None, + versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.delete_subnet(samdb, samdb.get_config_basedn(), subnetname) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error while removing subnet %s, error: %s" % + (subnetname, e)) + + self.outf.write("Subnet %s removed!\n" % subnetname) + + +class cmd_sites_subnet_set_site(Command): + """Assign a subnet to a site.""" + synopsis = "%prog [options]" + takes_args = ["subnetname", "site_of_subnet"] + + takes_optiongroups = { + "sambaopts": options.SambaOptions, + "versionopts": options.VersionOptions, + "credopts": options.CredentialsOptions, + } + + takes_options = [ + Option("-H", "--URL", help="LDB URL for database or target server", + type=str, metavar="URL", dest="H"), + ] + + def run(self, subnetname, site_of_subnet, H=None, sambaopts=None, + credopts=None, versionopts=None): + lp = sambaopts.get_loadparm() + creds = credopts.get_credentials(lp) + samdb = SamDB(url=H, session_info=system_session(), + credentials=creds, lp=lp) + + samdb.transaction_start() + try: + subnets.set_subnet_site(samdb, samdb.get_config_basedn(), + subnetname, site_of_subnet) + samdb.transaction_commit() + except subnets.SubnetException, e: + samdb.transaction_cancel() + raise CommandError("Error assigning subnet %s to site %s: %s" % + (subnetname, site_of_subnet, e)) + + print >> self.outf, ("Subnet %s shifted to site %s" % + (subnet_name, site_of_subnet)) + + +class cmd_sites_subnet(SuperCommand): + """Subnet management subcommands.""" + subcommands = { + "create": cmd_sites_subnet_create(), + "remove": cmd_sites_subnet_delete(), + "set-site": cmd_sites_subnet_set_site(), + } class cmd_sites(SuperCommand): """Sites management.""" - subcommands = {} subcommands["create"] = cmd_sites_create() subcommands["remove"] = cmd_sites_delete() + subcommands["subnet"] = cmd_sites_subnet() diff --git a/python/samba/subnets.py b/python/samba/subnets.py new file mode 100644 index 00000000000..e859f06e46d --- /dev/null +++ b/python/samba/subnets.py @@ -0,0 +1,186 @@ +# Add/remove subnets to sites. +# +# Copyright (C) Catalyst.Net Ltd 2015 +# Copyright Matthieu Patou 2011 +# +# Catalyst.Net's contribution was written by Douglas Bagnall +# . +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +import ldb +from ldb import FLAG_MOD_ADD, FLAG_MOD_REPLACE, LdbError +from sites import SiteNotFoundException + +class SubnetException(Exception): + """Base element for Subnet errors""" + pass + + +class SubnetNotFound(SubnetException): + """The subnet requested does not exist.""" + pass + + +class SubnetAlreadyExists(SubnetException): + """The subnet being added already exists.""" + pass + + +class SubnetInvalid(SubnetException): + """The subnet CIDR is invalid.""" + pass + + +class SiteNotFound(SubnetException): + """The site to be used for the subnet does not exist.""" + pass + + +def create_subnet(samdb, configDn, subnet_name, site_name): + """Create a subnet and associate it with a site. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: name of the subnet to create (a CIDR range) + :return: None + :raise SubnetAlreadyExists: if the subnet to be created already exists. + :raise SiteNotFound: if the site does not exist. + """ + ret = samdb.search(base=configDn, scope=ldb.SCOPE_SUBTREE, + expression='(&(objectclass=Site)(cn=%s))' % + ldb.binary_encode(site_name)) + if len(ret) != 1: + raise SiteNotFound('A site with the name %s does not exist' % + site_name) + dn_site = ret[0].dn + + if not isinstance(subnet_name, str): + raise SubnetInvalid("%s is not a valid subnet (not a string)" % subnet_name) + + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + m = ldb.Message() + m.dn = dnsubnet + m["objectclass"] = ldb.MessageElement("subnet", FLAG_MOD_ADD, + "objectclass") + m["siteObject"] = ldb.MessageElement(str(dn_site), FLAG_MOD_ADD, + "siteObject") + samdb.add(m) + except ldb.LdbError as (enum, estr): + if enum == ldb.ERR_INVALID_DN_SYNTAX: + raise SubnetInvalid("%s is not a valid subnet: %s" % (subnet_name, estr)) + elif enum == ldb.ERR_ENTRY_ALREADY_EXISTS: + # Subnet collisions are checked by exact match only, not + # overlapping range. This won't stop you creating + # 10.1.1.0/24 when there is already 10.1.0.0/16, or + # prevent you from having numerous IPv6 subnets that refer + # to the same range (e.g 5::0/16, 5::/16, 5:0:0::/16). + raise SubnetAlreadyExists('A subnet with the CIDR %s already exists' + % subnet_name) + else: + raise + + +def delete_subnet(samdb, configDn, subnet_name): + """Delete a subnet. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: Name of the subnet to delete + :return: None + :raise SubnetNotFound: if the subnet to be deleted does not exist. + """ + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + ret = samdb.search(base=dnsubnet, scope=ldb.SCOPE_BASE, + expression="objectClass=subnet") + if len(ret) != 1: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + + samdb.delete(dnsubnet) + + +def set_subnet_site(samdb, configDn, subnet_name, site_name): + """Assign a subnet to a site. + + This dissociates the subnet from its previous site. + + :param samdb: A samdb connection + :param configDn: The DN of the configuration partition + :param subnet_name: Name of the subnet + :param site_name: Name of the site + :return: None + :raise SubnetNotFound: if the subnet does not exist. + :raise SiteNotFound: if the site does not exist. + """ + + dnsubnet = ldb.Dn(samdb, "CN=Subnets,CN=Sites") + if dnsubnet.add_base(configDn) == False: + raise SubnetException("dnsubnet.add_base() failed") + if dnsubnet.add_child("CN=X") == False: + raise SubnetException("dnsubnet.add_child() failed") + dnsubnet.set_component(0, "CN", subnet_name) + + try: + ret = samdb.search(base=dnsubnet, scope=ldb.SCOPE_BASE, + expression="objectClass=subnet") + if len(ret) != 1: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SubnetNotFound('Subnet %s does not exist' % subnet_name) + + dnsite = ldb.Dn(samdb, "CN=Sites") + if dnsite.add_base(configDn) == False: + raise SubnetException("dnsites.add_base() failed") + if dnsite.add_child("CN=X") == False: + raise SubnetException("dnsites.add_child() failed") + dnsite.set_component(0, "CN", site_name) + + dnservers = ldb.Dn(samdb, "CN=Servers") + dnservers.add_base(dnsite) + + try: + ret = samdb.search(base=dnsite, scope=ldb.SCOPE_BASE, + expression="objectClass=site") + if len(ret) != 1: + raise SiteNotFoundException('Site %s does not exist' % site_name) + except LdbError as (enum, estr): + if enum == ldb.ERR_NO_SUCH_OBJECT: + raise SiteNotFoundException('Site %s does not exist' % site_name) + + siteDn = str(ret[0].dn) + + m = ldb.Message() + m.dn = dnsubnet + m["siteObject"] = ldb.MessageElement(siteDn, FLAG_MOD_REPLACE, + "siteObject") + samdb.modify(m) diff --git a/python/samba/tests/samba_tool/sites.py b/python/samba/tests/samba_tool/sites.py index 212df92cacd..81cc66d73b0 100644 --- a/python/samba/tests/samba_tool/sites.py +++ b/python/samba/tests/samba_tool/sites.py @@ -55,3 +55,58 @@ class SitesCmdTestCase(BaseSitesCmdTestCase): # now delete it self.samdb.delete(dnsite, ["tree_delete:0"]) + + +class SitesSubnetCmdTestCase(BaseSitesCmdTestCase): + def setUp(self): + super(SitesSubnetCmdTestCase, self).setUp() + self.sitename = "testsite" + self.sitename2 = "testsite2" + self.samdb.transaction_start() + sites.create_site(self.samdb, self.config_dn, self.sitename) + sites.create_site(self.samdb, self.config_dn, self.sitename2) + self.samdb.transaction_commit() + + def tearDown(self): + self.samdb.transaction_start() + sites.delete_site(self.samdb, self.config_dn, self.sitename) + sites.delete_site(self.samdb, self.config_dn, self.sitename2) + self.samdb.transaction_commit() + super(SitesSubnetCmdTestCase, self).tearDown() + + def test_site_subnet_create(self): + cidrs = (("10.9.8.0/24", self.sitename), + ("50.60.0.0/16", self.sitename2), + ("50.61.0.0/16", self.sitename2), # second subnet on the site + ("50.0.0.0/8", self.sitename), # overlapping subnet, other site + ("50.62.1.2/32", self.sitename), # single IP + ("aaaa:bbbb:cccc:dddd:eeee:ffff:2222:1100/120", + self.sitename2), + ) + + for cidr, sitename in cidrs: + result, out, err = self.runsubcmd("sites", "subnet", "create", + cidr, sitename, + "-H", self.dburl, + self.creds_string) + self.assertCmdSuccess(result) + + ret = self.samdb.search(base=self.config_dn, + scope=ldb.SCOPE_SUBTREE, + expression=('(&(objectclass=subnet)(cn=%s))' + % cidr)) + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 1) + + dnsubnets = ldb.Dn(self.samdb, + "CN=Subnets,CN=Sites,%s" % self.config_dn) + + for cidr, sitename in cidrs: + dnsubnet = ldb.Dn(self.samdb, ("Cn=%s,CN=Subnets,CN=Sites,%s" % + (cidr, self.config_dn))) + + ret = self.samdb.search(base=dnsubnets, scope=ldb.SCOPE_ONELEVEL, + expression='(dn=%s)' % dnsubnet) + self.assertIsNotNone(ret) + self.assertEqual(len(ret), 1) + self.samdb.delete(dnsubnet, ["tree_delete:0"]) diff --git a/source4/dsdb/tests/python/sites.py b/source4/dsdb/tests/python/sites.py index f42e7bf7e3e..6242a9dbda0 100755 --- a/source4/dsdb/tests/python/sites.py +++ b/source4/dsdb/tests/python/sites.py @@ -27,10 +27,12 @@ from samba.tests.subunitrun import TestProgram, SubunitOptions import samba.getopt as options from samba import sites +from samba import subnets from samba.auth import system_session from samba.samdb import SamDB import samba.tests from samba.dcerpc import security +from ldb import SCOPE_SUBTREE parser = optparse.OptionParser(__file__ + " [options] ") sambaopts = options.SambaOptions(parser) @@ -107,5 +109,79 @@ class SimpleSitesTests(SitesBaseTests): "Default-First-Site-Name") +# tests for subnets +class SimpleSubnetTests(SitesBaseTests): + + def setUp(self): + super(SimpleSubnetTests, self).setUp() + self.basedn = self.ldb.get_config_basedn() + self.sitename = "testsite" + self.sitename2 = "testsite2" + self.ldb.transaction_start() + sites.create_site(self.ldb, self.basedn, self.sitename) + sites.create_site(self.ldb, self.basedn, self.sitename2) + self.ldb.transaction_commit() + + def tearDown(self): + self.ldb.transaction_start() + sites.delete_site(self.ldb, self.basedn, self.sitename) + sites.delete_site(self.ldb, self.basedn, self.sitename2) + self.ldb.transaction_commit() + super(SimpleSubnetTests, self).tearDown() + + def test_create_delete(self): + """Create a subnet and delete it again.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.11.12.0/24" + + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + + self.assertRaises(subnets.SubnetAlreadyExists, + subnets.create_subnet, self.ldb, basedn, cidr, + self.sitename) + + subnets.delete_subnet(self.ldb, basedn, cidr) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr) + + def test_create_shift_delete(self): + """Create a subnet, shift it to another site, then delete it.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.11.12.0/24" + + subnets.create_subnet(self.ldb, basedn, cidr, self.sitename) + + subnets.set_subnet_site(self.ldb, basedn, cidr, self.sitename2) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + sites = ret[0]['siteObject'] + self.assertEqual(len(sites), 1) + self.assertEqual(sites[0], + 'CN=testsite2,CN=Sites,%s' % self.ldb.get_config_basedn()) + + self.assertRaises(subnets.SubnetAlreadyExists, + subnets.create_subnet, self.ldb, basedn, cidr, + self.sitename) + + subnets.delete_subnet(self.ldb, basedn, cidr) + + ret = self.ldb.search(base=basedn, scope=SCOPE_SUBTREE, + expression='(&(objectclass=subnet)(cn=%s))' % cidr) + + self.assertEqual(len(ret), 0, 'Failed to delete subnet %s' % cidr) + + def test_delete_subnet_that_does_not_exist(self): + """Ensure we can't delete a site that isn't there.""" + basedn = self.ldb.get_config_basedn() + cidr = "10.15.0.0/16" + + self.assertRaises(subnets.SubnetNotFound, + subnets.delete_subnet, self.ldb, basedn, cidr) + TestProgram(module=__name__, opts=subunitopts) -- cgit v1.2.1