mirror of
https://github.com/xcat2/xcat-core.git
synced 2026-10-07 10:06:39 +00:00
2abcec617b
The previous commit deleted the scenarios nothing ran. This one covers the
other half of the same review: places where spec.md states a behaviour and
nothing on the wire ever checked it.
Eight scenarios, each with the fixture work it needs:
S-36/37/38 next-server follows a node's own tftpserver, then its
xcatmaster, then the subnet -- three nodes, three sources,
three different answers, which is the only arrangement in
which a server that ignores the node attributes can be told
apart from one that honours them
S-08 a node whose mac attribute names two ports is reserved on
both, at the two addresses its two hostnames resolve to
S-33/34 a diskless node is told its iSCSI target: option 17 for a
normal client, the ISAN vendor space for one announcing that
vendor class
S-12 an architecture whose loader is not on disk is served an
address and no boot file, with a second architecture whose
loader is still there as the control
S-14 a URL names the port the web server actually listens on
S-05 a dynamic range written as a CIDR block serves out of it
S-59 a withdrawn reservation stops being handed out
S-54 a client that speaks BOOTP and not DHCP is still served
Three of these needed the tool to grow: BOOTP has no option 53, so a
BOOTREQUEST step type and a BOOTREPLY message type; option 119 is a name
list that may be compressed against itself, so RFC 3397 decoding, with a
fall back to hex on anything malformed rather than a misreading to assert
against.
RELEASE exposed a state-machine bug on the way. A step that expects no
reply left the client where it was, which is right for a DISCOVER nobody
answered and wrong for a RELEASE, which is never answered and whose whole
point is giving the lease up. A scenario could not boot a node, release
and boot it again -- which is exactly how S-02 has to be asserted, since
a session lasts one scenario and a second boot cannot reference the first
across that boundary.
Every new conf is reachable: three cases0 entries drive the eight fixture
subcommands, so none of them is dead on arrival.
153 lines
6.2 KiB
Python
153 lines
6.2 KiB
Python
import os
|
|
import unittest
|
|
|
|
import helper
|
|
|
|
from dhcptest_lib import config, machine
|
|
from dhcptest_lib.model import Scenario, Step
|
|
|
|
|
|
def step(name, type, expect=None, params=None, assertions=()):
|
|
return Step(name=name, type=type, expect=expect, params=params or {},
|
|
assertions=assertions)
|
|
|
|
|
|
def scenario(*steps):
|
|
return Scenario(name="s", steps=steps)
|
|
|
|
|
|
class Table(unittest.TestCase):
|
|
|
|
def test_known_step_types_covers_passive_and_protocol_types(self):
|
|
types = machine.known_step_types()
|
|
for expected in ("discover", "request", "renew", "rebind", "release",
|
|
"decline", "inform", "noop", "sleep"):
|
|
self.assertIn(expected, types)
|
|
|
|
def test_inform_is_legal_from_any_state(self):
|
|
self.assertIsNotNone(machine.transition_for(machine.INIT, "inform"))
|
|
self.assertIsNotNone(machine.transition_for(machine.BOUND, "inform"))
|
|
|
|
def test_discover_is_not_legal_once_bound(self):
|
|
self.assertIsNone(machine.transition_for(machine.BOUND, "discover"))
|
|
|
|
def test_a_selecting_request_expects_ack_or_nak(self):
|
|
transition = machine.transition_for(machine.SELECTING, "request")
|
|
self.assertEqual(transition.expects, frozenset(["ack", "nak"]))
|
|
self.assertEqual(transition.default_expect, "ack")
|
|
self.assertEqual(transition.next_state, machine.BOUND)
|
|
|
|
def test_renew_is_unicast_and_rebind_is_not(self):
|
|
self.assertTrue(machine.transition_for(machine.BOUND, "renew").unicast)
|
|
self.assertFalse(machine.transition_for(machine.BOUND, "rebind").unicast)
|
|
|
|
def test_a_bootrequest_is_a_single_exchange_from_init(self):
|
|
transition = machine.transition_for(machine.INIT, "bootrequest")
|
|
self.assertEqual(transition.expects, frozenset(["bootreply"]))
|
|
self.assertEqual(transition.default_expect, "bootreply")
|
|
# No handshake follows a BOOTREPLY, so the session stays where it was
|
|
# and a second BOOTREQUEST in the same scenario is still legal.
|
|
self.assertEqual(transition.next_state, machine.INIT)
|
|
|
|
|
|
class Validation(unittest.TestCase):
|
|
|
|
def test_a_clean_lease_scenario_has_no_problems(self):
|
|
self.assertEqual(machine.validate_scenario(scenario(
|
|
step("d", "discover"),
|
|
step("r", "request", params={"requested_address": "$offer.address",
|
|
"server_id": "$offer.server_id"}),
|
|
)), [])
|
|
|
|
def test_a_request_without_a_discover_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(step("r", "request")))
|
|
self.assertEqual(len(problems), 1)
|
|
self.assertIn("needs requested_address=", problems[0])
|
|
|
|
def test_a_renew_before_bound_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(step("x", "renew")))
|
|
self.assertIn("not legal in state INIT", problems[0])
|
|
|
|
def test_a_reference_to_a_later_step_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(
|
|
step("d", "discover", params={"ciaddr": "$ack.address"}),
|
|
))
|
|
self.assertIn("used before anything binds $ack", problems[0])
|
|
|
|
def test_a_misspelt_reply_field_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(
|
|
step("d", "discover"),
|
|
step("r", "request", params={"requested_address": "$offer.addres"}),
|
|
))
|
|
self.assertIn("not a field of a DHCP reply", problems[0])
|
|
|
|
def test_an_impossible_expectation_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(
|
|
step("d", "discover", expect="ack"),
|
|
))
|
|
self.assertIn("is not a possible reply", problems[0])
|
|
|
|
def test_expect_none_leaves_the_state_alone_and_binds_nothing(self):
|
|
problems = machine.validate_scenario(scenario(
|
|
step("silent", "discover", expect="none"),
|
|
step("again", "discover"),
|
|
))
|
|
self.assertEqual(problems, [])
|
|
|
|
def test_a_release_gives_the_lease_up_even_though_nothing_answers_it(self):
|
|
# RELEASE is never answered, so its expect is "none" -- but unlike a
|
|
# DISCOVER nobody replied to, the step did its work, and the client is
|
|
# back in INIT and free to discover again.
|
|
self.assertEqual(machine.validate_scenario(scenario(
|
|
step("d", "discover"),
|
|
step("r", "request", params={"requested_address": "$offer.address",
|
|
"server_id": "$offer.server_id"}),
|
|
step("bye", "release"),
|
|
step("again", "discover"),
|
|
)), [])
|
|
|
|
def test_a_duplicate_step_name_is_rejected(self):
|
|
problems = machine.validate_scenario(scenario(
|
|
step("d", "discover"), step("d", "discover", expect="none"),
|
|
))
|
|
self.assertIn("duplicate step name", problems[0])
|
|
|
|
def test_expected_types_follows_expect(self):
|
|
self.assertEqual(
|
|
machine.expected_types(step("d", "discover"), machine.INIT),
|
|
frozenset(["offer"]))
|
|
self.assertEqual(
|
|
machine.expected_types(step("d", "discover", expect="none"),
|
|
machine.INIT),
|
|
frozenset())
|
|
self.assertEqual(
|
|
machine.expected_types(step("r", "request", expect="any"),
|
|
machine.SELECTING),
|
|
frozenset(["ack", "nak"]))
|
|
|
|
def test_required_variables_gathers_every_set_name(self):
|
|
names = machine.required_variables([scenario(
|
|
step("d", "discover", params={"mac": "%(who)s"},
|
|
assertions=(),),
|
|
)])
|
|
self.assertEqual(names, {"who"})
|
|
|
|
|
|
class ShippedFiles(unittest.TestCase):
|
|
"""conf/ must stay valid: this is what `dhcptest validate` runs in CI."""
|
|
|
|
def test_every_shipped_scenario_validates(self):
|
|
for name in sorted(os.listdir(helper.CONF)):
|
|
if not name.endswith(".conf"):
|
|
continue
|
|
scenarios, _ = config.load([os.path.join(helper.CONF, name)],
|
|
raw=True)
|
|
for one in scenarios:
|
|
problems = machine.validate_scenario(one)
|
|
self.assertEqual(problems, [],
|
|
"%s/%s: %s" % (name, one.name, problems))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|