Ejemplo n.º 1
0
    def data(self):
        # We need to set up a repository to hold the "zone-list" file.
        self.hg = LocalMercurialRepository()

        # Create a zone list for testing.
        self.hg.put_file("zone-list", "testdomain1.test.\ntestdomain2.test.\n")

        self.hg.commit("zone file for testing")

        # Configure Jinx API to use the repo.
        jinx_global_settings["DNS_REPOSITORY_URL"] = self.hg.repository_path

        # Now create a very simple dynamic nameserver to use for testing.
        self.bind_dir = bind_dir = tempfile.mkdtemp()

        self.tsig_key = "RFyyAJE2aLyBfVvfUZbLYw=="

        conf_text = """
options
{
    directory "%s";
    allow-transfer { 127.0.0.1; };
};

key localhost {
    algorithm "HMAC-MD5";
    secret "%s";
};

zone "testdomain1.test."
{
    type master;
    file "testdomain1.test.";
    allow-update {
        key localhost;
    };
};

zone "testdomain2.test."
{
    type master;
    file "testdomain2.test.";
    allow-update {
        key localhost;
    };
};
""" % (
            bind_dir,
            self.tsig_key,
        )

        named_conf = open("%s/named.conf" % bind_dir, "w")
        named_conf.write(conf_text)
        named_conf.close()

        self.testdomain1_text = """
$TTL 3600
testdomain1.test.  IN SOA  localhost. hostmaster.testdomain1.test. 2011090103 600 600 1209600 60
                   IN NS   ns1.testdomain1.test.

ns1                IN A    127.0.0.1
baz            300 IN A    1.1.1.1
moof           320 IN A    1.1.1.1
               320 IN A    1.1.1.2
               320 IN A    1.1.1.3
"""

        zone_file = open("%s/testdomain1.test." % bind_dir, "w")
        zone_file.write(self.testdomain1_text)
        zone_file.close()

        self.testdomain1 = dns.zone.from_text(self.testdomain1_text, "testdomain1.test.", relativize=False)

        self.testdomain2_text = self.testdomain1_text.replace("testdomain1", "testdomain2")
        self.testdomain2 = dns.zone.from_text(self.testdomain2_text, "testdomain2.test.", relativize=False)

        zone_file = open("%s/testdomain2.test." % bind_dir, "w")
        zone_file.write(self.testdomain2_text)
        zone_file.close()

        dev_null = open("/dev/null", "w")
        log = open("%s/named.log" % bind_dir, "w")
        self.named = subprocess.Popen(
            ["/usr/sbin/named", "-g", "-p", "53000", "-c", "%s/named.conf" % bind_dir], stdout=dev_null, stderr=log
        )
        # print "logging to %s/named.log" % bind_dir

        jinx_global_settings["DNS_NAMESERVER"] = "localhost"
        jinx_global_settings["DNS_NAMESERVER_PORT"] = "53000"

        tsig_key = open("%s/localhost.key" % bind_dir, "w")
        tsig_key.write(self.tsig_key + "\n")
        tsig_key.close()

        jinx_global_settings["DNS_TSIG_KEY"] = "%s/localhost.key" % bind_dir

        Lock(name="dns", value=1).save()
Ejemplo n.º 2
0
class TestUpdateDnsRecords(JinxTestCase):
    api_call_path = "/jinx/2.0/update_dns_records"

    def data(self):
        # We need to set up a repository to hold the "zone-list" file.
        self.hg = LocalMercurialRepository()

        # Create a zone list for testing.
        self.hg.put_file("zone-list", "testdomain1.test.\ntestdomain2.test.\n")

        self.hg.commit("zone file for testing")

        # Configure Jinx API to use the repo.
        jinx_global_settings["DNS_REPOSITORY_URL"] = self.hg.repository_path

        # Now create a very simple dynamic nameserver to use for testing.
        self.bind_dir = bind_dir = tempfile.mkdtemp()

        self.tsig_key = "RFyyAJE2aLyBfVvfUZbLYw=="

        conf_text = """
options
{
    directory "%s";
    allow-transfer { 127.0.0.1; };
};

key localhost {
    algorithm "HMAC-MD5";
    secret "%s";
};

zone "testdomain1.test."
{
    type master;
    file "testdomain1.test.";
    allow-update {
        key localhost;
    };
};

zone "testdomain2.test."
{
    type master;
    file "testdomain2.test.";
    allow-update {
        key localhost;
    };
};
""" % (
            bind_dir,
            self.tsig_key,
        )

        named_conf = open("%s/named.conf" % bind_dir, "w")
        named_conf.write(conf_text)
        named_conf.close()

        self.testdomain1_text = """
$TTL 3600
testdomain1.test.  IN SOA  localhost. hostmaster.testdomain1.test. 2011090103 600 600 1209600 60
                   IN NS   ns1.testdomain1.test.

ns1                IN A    127.0.0.1
baz            300 IN A    1.1.1.1
moof           320 IN A    1.1.1.1
               320 IN A    1.1.1.2
               320 IN A    1.1.1.3
"""

        zone_file = open("%s/testdomain1.test." % bind_dir, "w")
        zone_file.write(self.testdomain1_text)
        zone_file.close()

        self.testdomain1 = dns.zone.from_text(self.testdomain1_text, "testdomain1.test.", relativize=False)

        self.testdomain2_text = self.testdomain1_text.replace("testdomain1", "testdomain2")
        self.testdomain2 = dns.zone.from_text(self.testdomain2_text, "testdomain2.test.", relativize=False)

        zone_file = open("%s/testdomain2.test." % bind_dir, "w")
        zone_file.write(self.testdomain2_text)
        zone_file.close()

        dev_null = open("/dev/null", "w")
        log = open("%s/named.log" % bind_dir, "w")
        self.named = subprocess.Popen(
            ["/usr/sbin/named", "-g", "-p", "53000", "-c", "%s/named.conf" % bind_dir], stdout=dev_null, stderr=log
        )
        # print "logging to %s/named.log" % bind_dir

        jinx_global_settings["DNS_NAMESERVER"] = "localhost"
        jinx_global_settings["DNS_NAMESERVER_PORT"] = "53000"

        tsig_key = open("%s/localhost.key" % bind_dir, "w")
        tsig_key.write(self.tsig_key + "\n")
        tsig_key.close()

        jinx_global_settings["DNS_TSIG_KEY"] = "%s/localhost.key" % bind_dir

        Lock(name="dns", value=1).save()

    def tearDown(self):
        self.named.terminate()
        self.named.wait()
        del self.named
        del self.hg
        shutil.rmtree(self.bind_dir, ignore_errors=True)

    def _axfr(self, zone):
        axfr = dns.query.xfr("localhost", zone, relativize=False, port=53000)
        return dns.zone.from_xfr(axfr, relativize=False)

    def _check_zone_in_hg(self, zone_name, zone_data, description):
        """Ensure that the version of the zone in hg equals zone_data."""

        # Jinx will have pushed changes into my local repo; bring them into my
        # working dir.
        self.hg.run_hg_command("update")

        hg_zone = dns.zone.from_text(self.hg.get_file("zones/%s" % zone_name), origin=zone_name, relativize=False)

        self.assertEquals(
            hg_zone, zone_data, "when %s, zone %s was not properly updated in hg." % (description, zone_name)
        )

    def _increment_serial(self, zone):
        """Increment the serial number in the zone.
        
        Arguments:
            zone -- a dnspython.zone.Zone object
        """

        soa_rd = zone.find_rdataset(zone.origin, "SOA")
        soa = soa_rd.items[0]
        soa.serial += 1

    def testAddARecord(self):
        response = self.do_api_call([["foo.testdomain1.test.", 300, "IN", "A", ["1.2.3.4"]]], [], [], {}, "description")
        self.assert_response_code(response, 200, response.data)

        # Construct a version of the zone with the "foo" record added
        rd = dns.rdataset.from_text_list("IN", "A", 300, ["1.2.3.4"])
        self.testdomain1.replace_rdataset("foo.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        # Now see if the "foo" A record and only this record was added to the zone on the nameserver.
        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "A record was not added to zone")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "adding A record")

    def testAddCNAMERecord(self):
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "IN", "CNAME", ["bar.testdomain1.test."]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 200, response.data)

        # Construct a version of the zone with the "foo" record added
        rd = dns.rdataset.from_text_list("IN", "CNAME", 300, ["bar.testdomain1.test."])
        self.testdomain1.replace_rdataset("foo.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        # Now see if the "foo" A record and only this record was added to the zone on the nameserver.
        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "CNAME record was not added to zone")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "adding CNAME record")

    def testAddRoundRobin(self):
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "IN", "A", ["1.1.1.2", "1.1.1.3", "1.1.1.4"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 200, response.data)

        # Construct a version of the zone with the "foo" rrset added
        rd = dns.rdataset.from_text_list("IN", "A", 300, ["1.1.1.2", "1.1.1.3", "1.1.1.4"])
        self.testdomain1.replace_rdataset("foo.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        # Now see if the "foo" A record and only this record was added to the zone on the nameserver.
        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "A record round-robin was not added to zone")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "adding A record round-robin")

    def testAddBadZone(self):
        response = self.do_api_call(
            [["foo.testdomain3.test.", 300, "IN", "A", ["1.1.1.2", "1.1.1.3", "1.1.1.4"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 400)

    def testRelativeDomainName(self):
        response = self.do_api_call([["foo", 300, "IN", "A", ["1.1.1.2"]]], [], [], {}, "description")
        self.assert_response_code(response, 400)

    def testBadRdType(self):
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "IN", "APPLE", ["1.1.1.2"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 400)

    def testBadRdClass(self):
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "INCHWORM", "A", ["1.1.1.2"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 400)

    def testBadRecordData(self):
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "IN", "A", ["bad IP address"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 400)

    def testBadRecordFormat(self):
        response = self.do_api_call([["foo.testdomain1.test.", 300, "IN"]], [], [], {}, "description")
        self.assert_response_code(response, 400)

    def testBadRecordName(self):
        response = self.do_api_call(
            [["fo*@#@o.testdomain1.test.", 300, "IN", "A", ["1.1.1.2"]]], [], [], {}, "description"
        )
        self.assert_response_code(response, 400)

    def testAddComments(self):
        response = self.do_api_call(
            [], [], [], {"foo.testdomain1.test.": "foo comment", "bar.testdomain1.test.": "bar comment"}, "description"
        )
        self.assert_response_code(response, 200, "Couldn't add record comments: %s" % response.data)

        foo = DNSRecord.objects.get(name="foo.testdomain1.test.")
        self.assertEquals(foo.comment, "foo comment")

        bar = DNSRecord.objects.get(name="bar.testdomain1.test.")
        self.assertEquals(bar.comment, "bar comment")

    def testAddCommentBadZone(self):
        response = self.do_api_call([], [], [], {"foo.testdomain3.test.": "foo comment"}, "description")
        self.assert_response_code(response, 400)

    def testModifyComment(self):
        DNSRecord(name="foo.testdomain1.test.", comment="old comment").save()

        response = self.do_api_call([], [], [], {"foo.testdomain1.test.": "new comment"}, "description")
        self.assert_response_code(response, 200, "Couldn't modify record comment: %s" % response.data)

        foo = DNSRecord.objects.get(name="foo.testdomain1.test.")
        self.assertEquals(foo.comment, "new comment")

    def testDeleteRecordComment(self):
        DNSRecord(name="foo.testdomain1.test.", comment="existing comment").save()

        response = self.do_api_call([], [], [], {"foo.testdomain1.test.": None}, "description")
        self.assert_response_code(response, 200, "Couldn't delete record comment: %s" % response.data)

        foo = DNSRecord.objects.get(name="foo.testdomain1.test.")
        self.assertEquals(foo.comment, None)

    def testAddExistingRecord(self):
        response = self.do_api_call([["baz.testdomain1.test.", 300, "IN", "A", ["1.2.3.4"]]], [], [], {}, "description")
        self.assert_response_code(response, 409)

    def testDeleteRecord(self):
        response = self.do_api_call([], [["baz.testdomain1.test.", 300, "IN", "A", ["1.1.1.1"]]], [], {}, "description")
        self.assert_response_code(response, 200)

        # Now construct a zone with the baz record deleted and make sure it matches the nameserver's zone.
        self.testdomain1.delete_rdataset("baz.testdomain1.test.", "A")
        self._increment_serial(self.testdomain1)

        # Now see if the "foo" A record and only this record was added to the zone on the nameserver.
        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "Baz record was not deleted from zone.")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "deleting a record")

    def testDeleteRecordBadData(self):
        response = self.do_api_call([], [["baz.testdomain1.test.", 300, "IN", "A", ["1.1.1.2"]]], [], {}, "description")
        self.assert_response_code(response, 409, "Should get an error when deleting a record with non-matching data")

    def testModifyRecord1(self):
        response = self.do_api_call(
            [],
            [],
            [["baz.testdomain1.test.", [300, "IN", "A", ["1.1.1.1"]], [300, "IN", "A", ["1.1.1.2"]]]],
            {},
            "description",
        )
        self.assert_response_code(response, 200, "Couldn't modify record: %s" % response.data)

        rd = dns.rdataset.from_text_list("IN", "A", 300, ["1.1.1.2"])
        self.testdomain1.replace_rdataset("baz.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "Baz record was not modified.")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "modifying A record value")

    def testModifyRecord2(self):
        response = self.do_api_call(
            [],
            [],
            [["baz.testdomain1.test.", [300, "IN", "A", ["1.1.1.1"]], [60, "IN", "A", ["1.1.1.1"]]]],
            {},
            "description",
        )
        self.assert_response_code(response, 200, "Couldn't modify record: %s" % response.data)

        rd = dns.rdataset.from_text_list("IN", "A", 60, ["1.1.1.1"])
        self.testdomain1.replace_rdataset("baz.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "Baz record was not modified.")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "modifying A record TTL")

    def testModifyRoundRobin(self):
        # Change an entry in a round-robin
        response = self.do_api_call(
            [],
            [],
            [
                [
                    "moof.testdomain1.test.",
                    [320, "IN", "A", ["1.1.1.1", "1.1.1.2", "1.1.1.3"]],
                    [320, "IN", "A", ["1.1.1.1", "1.1.1.2", "1.1.1.4"]],
                ]
            ],
            {},
            "description",
        )
        self.assert_response_code(response, 200, "Couldn't modify round-robin: %s" % response.data)

        rd = dns.rdataset.from_text_list("IN", "A", 320, ["1.1.1.1", "1.1.1.2", "1.1.1.4"])
        self.testdomain1.replace_rdataset("moof.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "round-robin was not modified.")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "modifying a record in a round-robin")

    def testModifyRecordBadData(self):
        response = self.do_api_call(
            [],
            [],
            [["baz.testdomain1.test.", [300, "IN", "A", ["1.1.1.2"]], [300, "IN", "A", ["1.1.1.3"]]]],
            {},
            "description",
        )
        self.assert_response_code(response, 409)

    def testModifyRecordBadTTL(self):
        response = self.do_api_call(
            [],
            [],
            [["baz.testdomain1.test.", [500, "IN", "A", ["1.1.1.1"]], [300, "IN", "A", ["1.1.1.3"]]]],
            {},
            "description",
        )
        self.assert_response_code(response, 409)

    def testAddRecordsToMultipleZones(self):
        response = self.do_api_call(
            [
                ["foo.testdomain1.test.", 300, "IN", "A", ["1.2.3.4"]],
                ["bar.testdomain2.test.", 500, "IN", "A", ["1.3.4.5"]],
            ],
            [],
            [],
            {},
            "description",
        )
        self.assert_response_code(response, 200, "Failed to add records to two zones: %s" % response.data)

        # Check testdomain1.test.
        rd = dns.rdataset.from_text_list("IN", "A", 300, ["1.2.3.4"])
        self.testdomain1.replace_rdataset("foo.testdomain1.test.", rd)
        self._increment_serial(self.testdomain1)

        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "A record was not added to testdomain1")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "adding A record")

        # Check testdomain2.test.
        rd = dns.rdataset.from_text_list("IN", "A", 500, ["1.3.4.5"])
        self.testdomain2.replace_rdataset("bar.testdomain2.test.", rd)
        self._increment_serial(self.testdomain2)

        new_zone = self._axfr("testdomain2.test.")
        self.assertEquals(new_zone, self.testdomain2, "A record was not added to testdomain2")

        self._check_zone_in_hg("testdomain2.test.", new_zone, "adding A record")

    def testMassiveUpdate(self):
        # add, modify, and delete records and comments, in multiple zones, all at the same time

        DNSRecord(name="a.testdomain1.test.", comment="old a comment").save()
        DNSRecord(name="b.testdomain2.test.", comment="old b comment").save()

        response = self.do_api_call(
            [
                ["foo.testdomain1.test.", 300, "IN", "A", ["1.1.1.1"]],
                ["bar.testdomain2.test.", 400, "IN", "A", ["1.1.1.1", "2.2.2.2", "3.3.3.3"]],
            ],
            [
                ["baz.testdomain1.test.", 300, "IN", "A", ["1.1.1.1"]],
                ["moof.testdomain2.test.", 320, "IN", "A", ["1.1.1.1", "1.1.1.2", "1.1.1.3"]],
            ],
            [
                ["baz.testdomain2.test.", [300, "IN", "A", ["1.1.1.1"]], [320, "IN", "A", ["2.2.2.2"]]],
                [
                    "moof.testdomain1.test.",
                    [320, "IN", "A", ["1.1.1.1", "1.1.1.2", "1.1.1.3"]],
                    [330, "IN", "A", ["9.9.9.9", "8.8.8.8", "1.1.1.1"]],
                ],
            ],
            {
                "a.testdomain1.test.": "new a comment",
                "b.testdomain2.test.": None,
                "c.testdomain1.test.": "new c comment",
            },
            "description",
        )
        self.assert_response_code(response, 200, "Unable to perform massive update: %s" % response.data)
        self.assertEquals(response.data, 2)

        # Check testdomain1.test.

        # add foo
        rd = dns.rdataset.from_text_list("IN", "A", 300, ["1.1.1.1"])
        self.testdomain1.replace_rdataset("foo.testdomain1.test.", rd)

        # remove baz
        self.testdomain1.delete_rdataset("baz.testdomain1.test.", "A")

        # modify moof
        rd = dns.rdataset.from_text_list("IN", "A", 330, ["9.9.9.9", "8.8.8.8", "1.1.1.1"])
        self.testdomain1.replace_rdataset("moof.testdomain1.test.", rd)

        self._increment_serial(self.testdomain1)

        new_zone = self._axfr("testdomain1.test.")
        self.assertEquals(new_zone, self.testdomain1, "Massive update did not match for testdomain1")

        self._check_zone_in_hg("testdomain1.test.", new_zone, "performing massive update")

        # Check testdomain2.test.

        # add bar
        rd = dns.rdataset.from_text_list("IN", "A", 400, ["1.1.1.1", "2.2.2.2", "3.3.3.3"])
        self.testdomain2.replace_rdataset("bar.testdomain2.test.", rd)

        # remove moof
        self.testdomain2.delete_rdataset("moof.testdomain2.test.", "A")

        # modify baz
        rd = dns.rdataset.from_text_list("IN", "A", 320, ["2.2.2.2"])
        self.testdomain2.replace_rdataset("baz.testdomain2.test.", rd)

        self._increment_serial(self.testdomain2)

        new_zone = self._axfr("testdomain2.test.")
        self.assertEquals(new_zone, self.testdomain2, "Massive update did not match for testdomain2")

        self._check_zone_in_hg("testdomain2.test.", new_zone, "performing massive update")

        # Now check comments
        a = DNSRecord.objects.get(name="a.testdomain1.test.")
        self.assertEquals(a.comment, "new a comment")

        b = DNSRecord.objects.get(name="b.testdomain2.test.")
        self.assertEquals(b.comment, None)

        c = DNSRecord.objects.get(name="c.testdomain1.test.")
        self.assertEquals(c.comment, "new c comment")

    def testHgComment(self):
        # make a change and ensure that the hg commit comment is what I pass
        response = self.do_api_call(
            [["foo.testdomain1.test.", 300, "IN", "A", ["1.2.3.4"]]], [], [], {}, "test description text"
        )
        self.assert_response_code(response, 200, response.data)

        # Pull in the changes pushed by Jinx.
        self.hg.run_hg_command("update")

        history = self.hg.log()

        # Note: the "unittest: " part comes from the name of the user that made
        # the update_dns_records call.  In this unit test framework, that
        # username is passed in do_api_call by passing
        # REMOTE_USER="******" to the API call.
        self.assertEquals(history[0]["description"], "unittest: test description text")