samba-tool: add sites subnet subcommands
authorDouglas Bagnall <douglas.bagnall@catalyst.net.nz>
Tue, 27 Oct 2015 23:20:37 +0000 (12:20 +1300)
committerAndrew Bartlett <abartlet@samba.org>
Thu, 24 Dec 2015 03:09:29 +0000 (04:09 +0100)
This allows you to add, remove, or shift subnets.

Signed-off-by: Douglas Bagnall <douglas.bagnall@catalyst.net.nz>
Signed-off-by: Andrew Bartlett <abartlet@samba.org>
Reviewed-by: Garming Sam <garming@catalyst.net.nz>
Reviewed-by: Andrew Bartlett <abartlet@samba.org>
python/samba/netcmd/sites.py
python/samba/subnets.py [new file with mode: 0644]
python/samba/tests/samba_tool/sites.py
source4/dsdb/tests/python/sites.py

index 53091a251cf5ae631b78909d018328392b8d4a61..f0c792d90eeb0216f431acef273fd05e3dc4a771 100644 (file)
@@ -16,7 +16,7 @@
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 #
 
-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 <subnet> <site-of-subnet> [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 <subnet> [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 <subnet> <site-of-subnet> [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 (file)
index 0000000..e859f06
--- /dev/null
@@ -0,0 +1,186 @@
+# Add/remove subnets to sites.
+#
+# Copyright (C) Catalyst.Net Ltd 2015
+# Copyright Matthieu Patou <mat@matws.net> 2011
+#
+# Catalyst.Net's contribution was written by Douglas Bagnall
+# <douglas.bagnall@catalyst.net.nz>.
+#
+# 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 <http://www.gnu.org/licenses/>.
+#
+
+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)
index 212df92cacd9a0ce2235bd3e1e50be2fa6125ced..81cc66d73b080428d6fa5740e87f6f8dc5167174 100644 (file)
@@ -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"])
index f42e7bf7e3ecd8c658e98d00d91c88c5b45d1bff..6242a9dbda0518a3c57809608a3a85126f718429 100755 (executable)
@@ -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] <host>")
 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)