sts-sponsors team mailing list archive
-
sts-sponsors team
-
Mailing list archive
-
Message #08174
[Merge] ~troyanov/maas:use-netmon-binary into maas:master
Anton Troyanov has proposed merging ~troyanov/maas:use-netmon-binary into maas:master.
Commit message:
feat(netmon)!: replace tcpdump with maas-netmon
New binary `maas-netmon` is introduced for ARP network discovery.
BREAKING CHANGE: Binary doesn't read PCAP format, thus it is not possible to pass in stdin or file as an argument anymore. It is also not possible to ask for a verbose output.
Requested reviews:
MAAS Lander (maas-lander)
MAAS Maintainers (maas-maintainers)
For more details, see:
https://code.launchpad.net/~troyanov/maas/+git/maas/+merge/442457
--
Your team MAAS Committers is subscribed to branch maas:master.
diff --git a/Makefile b/Makefile
index 8caf1e9..68e0262 100644
--- a/Makefile
+++ b/Makefile
@@ -202,7 +202,6 @@ lint-go-fix: lint-go
lint-shell:
@shellcheck -x \
package-files/usr/lib/maas/beacon-monitor \
- package-files/usr/lib/maas/network-monitor \
package-files/usr/lib/maas/unverified-ssh \
snap/hooks/* \
snap/local/tree/bin/* \
diff --git a/debian/extras/99-maas-common-sudoers b/debian/extras/99-maas-common-sudoers
index 412c60b..c00627c 100644
--- a/debian/extras/99-maas-common-sudoers
+++ b/debian/extras/99-maas-common-sudoers
@@ -2,7 +2,6 @@ maas ALL= NOPASSWD: /usr/bin/lshw
maas ALL= NOPASSWD: /sbin/blockdev
# MAAS network monitoring tools.
-maas ALL= NOPASSWD: /usr/lib/maas/network-monitor
maas ALL= NOPASSWD: /usr/lib/maas/beacon-monitor
# Control of the HTTP server: MAAS needs to reconfigure it after editing
diff --git a/debian/maas-common.install b/debian/maas-common.install
index 7caf604..5d6f0f5 100644
--- a/debian/maas-common.install
+++ b/debian/maas-common.install
@@ -4,7 +4,6 @@ package-files/usr/lib/maas/maas-delete-file usr/lib/maas
package-files/usr/lib/maas/maas-write-file usr/lib/maas
# Install network monitoring scripts
-package-files/usr/lib/maas/network-monitor usr/lib/maas
package-files/usr/lib/maas/beacon-monitor usr/lib/maas
# Install unverified-ssh
diff --git a/package-files/usr/lib/maas/network-monitor b/package-files/usr/lib/maas/network-monitor
deleted file mode 100755
index 2b6d362..0000000
--- a/package-files/usr/lib/maas/network-monitor
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/sh -euf
-# Copyright 2016 Canonical Ltd. This software is licensed under the
-# GNU Affero General Public License version 3 (see the file LICENSE).
-
-# Utility script to wrap `tcpdump`, so that this script can be called with
-# `sudo` without allowing MAAS access to read arbitrary network traffic.
-# This script is designed to be as minimal as possible, to prevent arbitrary
-# code execution.
-
-if [ $# -ne 1 ]; then
- echo "Writes ARP traffic (and ARP traffic on tagged VLANs) to stdout" >&2
- echo "using tcpdump's binary PCAP format." >&2
- echo "" >&2
- echo "Usage:" >&2
- echo " $0 <interface>" >&2
- exit 32
-fi
-
-exec "${SNAP:-}/usr/bin/tcpdump" -Z root --interface "$1" --no-promiscuous-mode \
- --packet-buffered --immediate-mode --snapshot-length=64 -n -w - \
- "arp or (vlan and arp)"
diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
index 6889cda..77cd0c3 100644
--- a/snap/snapcraft.yaml
+++ b/snap/snapcraft.yaml
@@ -248,8 +248,10 @@ parts:
- OUT_PREFIX=maas-
build-packages:
- golang-go
+ organize:
+ bin/maas-netmon: usr/sbin/maas-netmon
prime:
- - bin/maas-netmon
+ - usr/sbin/maas-netmon
tree:
plugin: dump
diff --git a/src/provisioningserver/utils/arp.py b/src/provisioningserver/utils/arp.py
index 86172fb..35e701b 100644
--- a/src/provisioningserver/utils/arp.py
+++ b/src/provisioningserver/utils/arp.py
@@ -4,356 +4,15 @@
"""Utilities for working with ARP packets."""
-from collections import namedtuple
-from datetime import datetime
-import json
import os
-import stat
-import struct
import subprocess
import sys
from textwrap import dedent
-from netaddr import EUI, IPAddress
-
from provisioningserver.path import get_path
from provisioningserver.utils import sudo
-from provisioningserver.utils.ethernet import Ethernet, ETHERTYPE
-from provisioningserver.utils.network import bytes_to_int, format_eui
-from provisioningserver.utils.pcap import PCAP, PCAPError
from provisioningserver.utils.script import ActionScriptError
-# The SEEN_AGAIN_THRESHOLD is a time (in seconds) that determines how often
-# to report (IP, MAC) bindings that have been seen again (or "REFRESHED").
-# While it is important for MAAS to know about "NEW" and "MOVED" bindings
-# immediately, "REFRESHED" bindings occur too often to be useful, and
-# are thus throttled by this value.
-SEEN_AGAIN_THRESHOLD = 600
-
-# Definitions for ARP packet used with `struct`.
-ARP_PACKET = "!hhBBh6sL6sL"
-ARPPacket = namedtuple(
- "ARPPacket",
- (
- "hardware_type",
- "protocol",
- "hardware_length",
- "protocol_length",
- "operation",
- "sender_mac",
- "sender_ip",
- "target_mac",
- "target_ip",
- ),
-)
-
-SIZEOF_ARP_PACKET = 28
-
-
-class ARP_OPERATION:
- """Enumeration to represent ARP operation types."""
-
- REQUEST = 1
- REPLY = 2
-
- def __init__(self, operation):
- super().__init__()
- self.operation = operation
-
- def __bytes__(self):
- """Returns the ARP operation in byte format.
-
- The returned value will be padded to two bytes; suitable for placement
- in an ARP packet.
- """
- return bytes.fromhex("%04x" % self.operation)
-
- def __str__(self):
- if self.operation == 1:
- extra = " (request)"
- elif self.operation == 2:
- extra = " (reply)"
- else:
- extra = ""
- return "%d%s" % (self.operation, extra)
-
- def __radd__(self, other):
- """Allows concatenating an ARP_OPERATION with `bytes`."""
- if isinstance(other, bytes):
- return other + bytes(self)
- else:
- raise NotImplementedError(
- "ARP_OPERATION may only be added to `bytes`."
- )
-
-
-class ARP:
- """Representation of an ARP packet."""
-
- def __init__(
- self, pkt_bytes, time=None, src_mac=None, dst_mac=None, vid=None
- ):
- """
- :param pkt_bytes: The input bytes of the ARP packet.
- :type pkt_bytes: bytes
- :param time: Timestamp packet was seen (seconds since epoch)
- :type time: str
- :param src_mac: Source MAC address from Ethernet header.
- :type src_mac: bytes
- :param dst_mac: Destination MAC address from Ethernet header.
- :type dst_mac: bytes
- :param vid: 802.1q VLAN ID (VID), or None if untagged.
- :type vid: int
- :return:
- """
- packet = ARPPacket._make(
- struct.unpack(ARP_PACKET, pkt_bytes[0:SIZEOF_ARP_PACKET])
- )
- self.packet = packet
- self.time = time
- if src_mac is not None:
- self.src_mac = EUI(bytes_to_int(src_mac))
- else:
- self.src_mac = None
- if dst_mac is not None:
- self.dst_mac = EUI(bytes_to_int(dst_mac))
- else:
- self.dst_mac = None
- self.vid = vid
- self.hardware_type = packet.hardware_type
- self.protocol_type = packet.protocol
- self.hardware_length = packet.hardware_length
- self.protocol_length = packet.protocol_length
- self.operation = packet.operation
- self.sender_hardware_bytes = packet.sender_mac
- self.sender_protocol_bytes = packet.sender_ip
- self.target_hardware_bytes = packet.target_mac
- self.target_protocol_bytes = packet.target_ip
-
- @property
- def source_eui(self):
- """Returns a netaddr.EUI representing the source MAC address."""
- return EUI(bytes_to_int(self.sender_hardware_bytes))
-
- @property
- def target_eui(self):
- """Returns a netaddr.EUI representing the target MAC address."""
- return EUI(bytes_to_int(self.target_hardware_bytes))
-
- @property
- def source_ip(self):
- """Returns a netaddr.IPAddress representing the source IP address."""
- return IPAddress(self.sender_protocol_bytes)
-
- @property
- def target_ip(self):
- """Returns a netaddr.IPAddress representing the target IP address."""
- return IPAddress(self.target_protocol_bytes)
-
- def is_valid(self):
- """Only (Ethernet MAC, IPv4) bindings are currently supported. This
- method ensures this ARP packet specifies those types.
- """
- # http://www.iana.org/assignments/arp-parameters/arp-parameters.xhtml
- # Hardware type 1 == Ethernet
- if self.hardware_type != 1:
- return False
- # Protocol type 0x800 == IPv4 (this should match the Ethertype)
- if self.protocol_type != 0x800:
- return False
- if self.hardware_length != 6:
- return False
- if self.protocol_length != 4:
- return False
- return True
-
- def bindings(self):
- """Yields each (MAC, IP) binding found in this ARP packet."""
- if not self.is_valid():
- return
-
- if self.operation == 1:
- # This is an ARP request.
- # We can find a binding in the (source_eui, source_ip)
- source_ip = self.source_ip
- source_eui = self.source_eui
- if int(source_ip) != 0 and int(source_eui) != 0:
- yield (source_ip, self.source_eui)
- elif self.operation == 2:
- # This is an ARP reply.
- # We can find a binding in both the (source_eui, source_ip) and
- # the (target_eui, target_ip).
- source_ip = self.source_ip
- source_eui = self.source_eui
- target_ip = self.target_ip
- target_eui = self.target_eui
- if int(source_ip) != 0 and int(source_eui) != 0:
- yield (source_ip, self.source_eui)
- if int(target_ip) != 0 and int(target_eui) != 0:
- yield (target_ip, self.target_eui)
-
- def write(self, out=sys.stdout):
- """Output text-based details about this ARP packet to the specified
- file or stream.
- :param out: An object with a `write(str)` method.
- """
- if self.time is not None:
- out.write(
- "ARP observed at %s:\n" % (datetime.fromtimestamp(self.time))
- )
- if self.vid is not None:
- out.write(
- f" 802.1q VLAN ID (VID): {self.vid} (0x{self.vid:03x})\n"
- )
- if self.src_mac is not None:
- out.write(
- " Ethernet source: %s\n" % format_eui(self.src_mac)
- )
- if self.dst_mac is not None:
- out.write(
- " Ethernet destination: %s\n" % format_eui(self.dst_mac)
- )
- out.write(" Hardware type: 0x%04x\n" % self.hardware_type)
- out.write(" Protocol type: 0x%04x\n" % self.protocol_type)
- out.write("Hardware address length: %d\n" % self.hardware_length)
- out.write("Protocol address length: %d\n" % self.protocol_length)
- out.write(
- " Operation: %s\n" % (ARP_OPERATION(self.operation))
- )
- out.write(
- "Sender hardware address: %s\n" % (format_eui(self.source_eui))
- )
- out.write("Sender protocol address: %s\n" % self.source_ip)
- out.write(
- "Target hardware address: %s\n" % (format_eui(self.target_eui))
- )
- out.write("Target protocol address: %s\n" % self.target_ip)
- out.write("\n")
-
-
-def update_bindings_and_get_event(bindings, vid, ip, mac, time):
- """Update the specified bindings dictionary and returns a dictionary if the
- information resulted in an update to the bindings. (otherwise, returns
- None.)
-
- If an event is returned, it will be a dictionary with the following fields:
-
- ip - The IP address of the binding.
- mac - The MAC address the IP was bound to.
- previous_mac - (if the IP moved between MACs) The previous MAC that
- was using the IP address.
- time - The time (in seconds since the epoch) the binding was observed.
- event - An event type; either "NEW", "MOVED", or "REFRESHED".
- """
- if (vid, ip) in bindings:
- binding = bindings[(vid, ip)]
- if binding["mac"] != mac:
- # Another MAC claimed ownership of this IP address. Update the
- # MAC and emit a "MOVED" event.
- previous_mac = binding["mac"]
- binding["mac"] = mac
- binding["time"] = time
- return dict(
- ip=str(ip),
- mac=format_eui(mac),
- time=time,
- event="MOVED",
- previous_mac=format_eui(previous_mac),
- vid=vid,
- )
- elif time - binding["time"] >= SEEN_AGAIN_THRESHOLD:
- binding["time"] = time
- return dict(
- ip=str(ip),
- mac=format_eui(mac),
- time=time,
- event="REFRESHED",
- vid=vid,
- )
- else:
- # The IP was found in the bindings dict, but within the
- # SEEN_AGAIN_THRESHOLD. Don't update the record; the time field
- # records the last time we emitted an event for this IP address.
- return None
- else:
- # We haven't seen this IP before, so add a binding for it and
- # emit a "NEW" event.
- bindings[(vid, ip)] = {"mac": mac, "time": time}
- return dict(
- ip=str(ip), mac=format_eui(mac), time=time, event="NEW", vid=vid
- )
-
-
-def update_and_print_bindings(bindings, arp, out=sys.stdout):
- """Update the specified bindings dictionary with the given ARP packet.
-
- Output a JSON object on the specified stream (defaults to stdout) based on
- the results of updating the binding.
- """
- for ip, mac in arp.bindings():
- event = update_bindings_and_get_event(
- bindings, arp.vid, ip, mac, arp.time
- )
- if event is not None:
- out.write("%s\n" % json.dumps(event))
- out.flush()
-
-
-def observe_arp_packets(
- verbose=False, bindings=False, input=sys.stdin.buffer, output=sys.stdout
-):
- """Read stdin and look for tcpdump binary ARP output.
- :param verbose: Output text-based ARP packet details.
- :type verbose: bool
- :param bindings: Track (MAC, IP) bindings, and print new/update bindings.
- :type bindings: bool
- :param input: Stream to read PCAP data from.
- :type input: a file or stream supporting `read(int)`
- :param output: Stream to write JSON data to.
- :type input: a file or stream supporting `write(str)` and `flush()`.
- """
- if bindings:
- bindings = dict()
- else:
- bindings = None
- try:
- pcap = PCAP(input)
- if pcap.global_header.data_link_type != 1:
- # Not an Ethernet interface. Need to exit here, because our
- # assumptions about the link layer header won't be correct.
- return 4
- for header, packet in pcap:
- ethernet = Ethernet(packet, time=header.timestamp_seconds)
- if not ethernet.is_valid():
- # Ignore packets with a truncated Ethernet header.
- continue
- if len(ethernet.payload) < SIZEOF_ARP_PACKET:
- # Ignore truncated ARP packets.
- continue
- if ethernet.ethertype != ETHERTYPE.ARP:
- # Ignore non-ARP packets.
- continue
- arp = ARP(
- ethernet.payload,
- src_mac=ethernet.src_mac,
- dst_mac=ethernet.dst_mac,
- vid=ethernet.vid,
- time=ethernet.time,
- )
- if bindings is not None:
- update_and_print_bindings(bindings, arp, output)
- if verbose:
- arp.write()
- except EOFError:
- # Capture aborted before it could even begin. Note that this does not
- # occur if the end-of-stream occurs normally. (In that case, the
- # program will just exit.)
- return 3
- except PCAPError:
- # Capture aborted due to an I/O error.
- return 2
- return None
-
def add_arguments(parser):
"""Add this command's options to the `ArgumentParser`.
@@ -370,32 +29,15 @@ def add_arguments(parser):
"""
)
parser.add_argument(
- "-v",
- "--verbose",
- action="store_true",
- required=False,
- help="Print verbose packet information.",
- )
- parser.add_argument(
"interface",
type=str,
nargs="?",
help="Ethernet interface from which to capture traffic. Optional if "
"an input file is specified.",
)
- parser.add_argument(
- "-i",
- "--input-file",
- type=str,
- required=False,
- help="File to read PCAP output from. Use - for stdin. Default is to "
- "call `sudo /usr/lib/maas/network-monitor` to get input.",
- )
-def run(
- args, output=sys.stdout, stdin=sys.stdin, stdin_buffer=sys.stdin.buffer
-):
+def run(args, output=sys.stdout):
"""Observe an Ethernet interface and print ARP bindings."""
# First, become a progress group leader, so that signals can be directed
@@ -403,28 +45,12 @@ def run(
os.setpgrp()
network_monitor = None
- if args.input_file is None:
- if args.interface is None:
- raise ActionScriptError("Required argument: interface")
- cmd = [get_path("/usr/lib/maas/network-monitor"), args.interface]
- cmd = sudo(cmd)
- network_monitor = subprocess.Popen(
- cmd, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE
- )
- infile = network_monitor.stdout
- else:
- if args.input_file == "-":
- mode = os.fstat(stdin.fileno()).st_mode
- if not stat.S_ISFIFO(mode):
- raise ActionScriptError("Expected stdin to be a pipe.")
- infile = stdin_buffer
- else:
- infile = open(args.input_file, "rb")
- return_code = observe_arp_packets(
- bindings=True, verbose=args.verbose, input=infile, output=output
- )
- if return_code is not None:
- raise SystemExit(return_code)
+ if args.interface is None:
+ raise ActionScriptError("Required argument: interface")
+
+ cmd = [get_path("/usr/sbin/maas-netmon"), args.interface]
+ cmd = sudo(cmd)
+ network_monitor = subprocess.Popen(cmd, stdout=output)
if network_monitor is not None:
return_code = network_monitor.poll()
if return_code is not None:
diff --git a/src/provisioningserver/utils/tests/test_arp.py b/src/provisioningserver/utils/tests/test_arp.py
index cf26e68..574aa0e 100644
--- a/src/provisioningserver/utils/tests/test_arp.py
+++ b/src/provisioningserver/utils/tests/test_arp.py
@@ -5,565 +5,23 @@
from argparse import ArgumentParser
-from datetime import datetime
import io
-import json
-import subprocess
-from tempfile import NamedTemporaryFile
-from textwrap import dedent
-import time
from unittest.mock import Mock
-from netaddr import EUI, IPAddress
-from testtools.matchers import Equals, HasLength
from testtools.testcase import ExpectedException
from maastesting.factory import factory
from maastesting.matchers import MockCalledOnceWith
from maastesting.testcase import MAASTestCase
from provisioningserver.utils import arp as arp_module
-from provisioningserver.utils.arp import (
- add_arguments,
- ARP,
- ARP_OPERATION,
- run,
- SEEN_AGAIN_THRESHOLD,
- update_and_print_bindings,
- update_bindings_and_get_event,
-)
-from provisioningserver.utils.network import (
- format_eui,
- hex_str_to_bytes,
- ipv4_to_bytes,
-)
+from provisioningserver.utils.arp import add_arguments, run
from provisioningserver.utils.script import ActionScriptError
-def make_arp_packet(
- sender_ip,
- sender_mac,
- target_ip,
- target_mac="00:00:00:00:00:00",
- op=ARP_OPERATION.REQUEST,
- hardware_type="0x0001",
- protocol="0x0800",
- hardware_length="0x06",
- protocol_length="0x04",
-):
- # Concatenate a byte string with the specified values.
- # For ARP format, see: https://tools.ietf.org/html/rfc826
- arp_packet = (
- hex_str_to_bytes(hardware_type)
- + hex_str_to_bytes(protocol)
- + hex_str_to_bytes(hardware_length)
- + hex_str_to_bytes(protocol_length)
- + ARP_OPERATION(op)
- + hex_str_to_bytes(sender_mac)
- + ipv4_to_bytes(sender_ip)
- + hex_str_to_bytes(target_mac)
- + ipv4_to_bytes(target_ip)
- )
- return arp_packet
-
-
-class TestARP(MAASTestCase):
- def test_operation_enum__str(self):
- self.expectThat(
- str(ARP_OPERATION(ARP_OPERATION.REQUEST)), Equals("1 (request)")
- )
- self.expectThat(
- str(ARP_OPERATION(ARP_OPERATION.REPLY)), Equals("2 (reply)")
- )
- self.expectThat(str(ARP_OPERATION(3)), Equals("3"))
-
- def test_operation_enum__bytes(self):
- self.expectThat(
- bytes(ARP_OPERATION(ARP_OPERATION.REQUEST)), Equals(b"\0\x01")
- )
- self.expectThat(
- bytes(ARP_OPERATION(ARP_OPERATION.REPLY)), Equals(b"\0\x02")
- )
-
- def test_operation_enum__radd(self):
- self.expectThat(
- b"\xff" + bytes(ARP_OPERATION(ARP_OPERATION.REPLY)) + b"\xff",
- Equals(b"\xff\0\x02\xff"),
- )
-
- def test_write(self):
- ts = int(time.time())
- expected_time = datetime.fromtimestamp(ts)
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- eth_src = "02:03:04:05:06:07"
- eth_dst = "ff:ff:ff:ff:ff:ff"
- arp_packet = make_arp_packet(
- pkt_sender_ip, pkt_sender_mac, pkt_target_ip, pkt_target_mac
- )
- arp = ARP(
- arp_packet,
- time=ts,
- src_mac=hex_str_to_bytes(eth_src),
- dst_mac=hex_str_to_bytes(eth_dst),
- vid=100,
- )
- out = io.StringIO()
- arp.write(out)
- expected_output = dedent(
- """\
- ARP observed at {expected_time}:
- 802.1q VLAN ID (VID): 100 (0x064)
- Ethernet source: {eth_src}
- Ethernet destination: {eth_dst}
- Hardware type: 0x0001
- Protocol type: 0x0800
- Hardware address length: 6
- Protocol address length: 4
- Operation: 1 (request)
- Sender hardware address: {pkt_sender_mac}
- Sender protocol address: {pkt_sender_ip}
- Target hardware address: {pkt_target_mac}
- Target protocol address: {pkt_target_ip}
- """
- )
- self.assertEqual(
- expected_output.format(**locals()).strip(),
- out.getvalue().strip(),
- )
-
- def test_is_valid__succeeds_for_normal_packet(self):
- arp_packet = make_arp_packet(
- "192.168.0.1", "01:02:03:04:05:06", "192.168.0.2"
- )
- arp = ARP(arp_packet)
- self.assertTrue(arp.is_valid())
-
- def test_is_valid__fails_for_invalid_packets(self):
- arp = ARP(b"\x00" * 28)
- self.assertFalse(arp.is_valid())
- arp = ARP(
- make_arp_packet(
- "192.168.0.1",
- "01:02:03:04:05:06",
- "192.168.0.2",
- hardware_type="0x0000",
- )
- )
- self.assertFalse(arp.is_valid())
- arp = ARP(
- make_arp_packet(
- "192.168.0.1",
- "01:02:03:04:05:06",
- "192.168.0.2",
- protocol="0x0000",
- )
- )
- self.assertFalse(arp.is_valid())
- arp = ARP(
- make_arp_packet(
- "192.168.0.1",
- "01:02:03:04:05:06",
- "192.168.0.2",
- hardware_length="0x00",
- )
- )
- self.assertFalse(arp.is_valid())
- arp = ARP(
- make_arp_packet(
- "192.168.0.1",
- "01:02:03:04:05:06",
- "192.168.0.2",
- protocol_length="0x00",
- )
- )
- self.assertFalse(arp.is_valid())
-
- def test_properties(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- eth_src = "02:03:04:05:06:07"
- eth_dst = "ff:ff:ff:ff:ff:ff"
- arp_packet = make_arp_packet(
- pkt_sender_ip, pkt_sender_mac, pkt_target_ip, pkt_target_mac
- )
- arp = ARP(
- arp_packet,
- src_mac=hex_str_to_bytes(eth_src),
- dst_mac=hex_str_to_bytes(eth_dst),
- )
- self.assertEqual(EUI(pkt_sender_mac), arp.source_eui)
- self.assertEqual(EUI(pkt_target_mac), arp.target_eui)
- self.assertEqual(IPAddress(pkt_sender_ip), arp.source_ip)
- self.assertEqual(IPAddress(pkt_target_ip), arp.target_ip)
-
- def test_bindings__returns_sender_for_request(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REQUEST,
- )
- )
- self.assertCountEqual(
- arp.bindings(), [(IPAddress(pkt_sender_ip), EUI(pkt_sender_mac))]
- )
-
- def test_bindings__returns_sender_and_target_for_reply(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "02:03:04:05:06:07"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REPLY,
- )
- )
- self.assertCountEqual(
- arp.bindings(),
- [
- (IPAddress(pkt_sender_ip), EUI(pkt_sender_mac)),
- (IPAddress(pkt_target_ip), EUI(pkt_target_mac)),
- ],
- )
-
- def test_bindings__skips_null_source_ip_for_request(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "0.0.0.0"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REQUEST,
- )
- )
- self.assertCountEqual(arp.bindings(), [])
-
- def test_bindings__skips_null_source_ip_in_reply(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "0.0.0.0"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "02:03:04:05:06:07"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REPLY,
- )
- )
- self.assertCountEqual(
- arp.bindings(), [(IPAddress(pkt_target_ip), EUI(pkt_target_mac))]
- )
-
- def test_bindings__skips_null_target_ip_in_reply(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "0.0.0.0"
- pkt_target_mac = "02:03:04:05:06:07"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REPLY,
- )
- )
- self.assertCountEqual(
- arp.bindings(), [(IPAddress(pkt_sender_ip), EUI(pkt_sender_mac))]
- )
-
- def test_bindings__skips_null_source_eui_for_request(self):
- pkt_sender_mac = "00:00:00:00:00:00"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REQUEST,
- )
- )
- self.assertCountEqual(arp.bindings(), [])
-
- def test_bindings__skips_null_source_eui_in_reply(self):
- pkt_sender_mac = "00:00:00:00:00:00"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "02:03:04:05:06:07"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REPLY,
- )
- )
- self.assertCountEqual(
- arp.bindings(), [(IPAddress(pkt_target_ip), EUI(pkt_target_mac))]
- )
-
- def test_bindings__skips_null_target_eui_in_reply(self):
- pkt_sender_mac = "01:02:03:04:05:06"
- pkt_sender_ip = "192.168.0.1"
- pkt_target_ip = "192.168.0.2"
- pkt_target_mac = "00:00:00:00:00:00"
- arp = ARP(
- make_arp_packet(
- pkt_sender_ip,
- pkt_sender_mac,
- pkt_target_ip,
- pkt_target_mac,
- op=ARP_OPERATION.REPLY,
- )
- )
- self.assertCountEqual(
- arp.bindings(), [(IPAddress(pkt_sender_ip), EUI(pkt_sender_mac))]
- )
-
-
-class TestUpdateBindingsAndGetEvent(MAASTestCase):
- def test_new_binding(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac = EUI("00:01:02:03:04:05")
- vid = None
- event = update_bindings_and_get_event(bindings, vid, ip, mac, 0)
- self.assertEqual({(vid, ip): {"mac": mac, "time": 0}}, bindings)
- self.assertThat(
- event,
- Equals(
- dict(
- event="NEW",
- ip=str(ip),
- mac=format_eui(mac),
- time=0,
- vid=vid,
- )
- ),
- )
-
- def test_new_bindings_with_vid(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac = EUI("00:01:02:03:04:05")
- vid = None
- event = update_bindings_and_get_event(bindings, vid, ip, mac, 0)
- self.assertEqual({(vid, ip): {"mac": mac, "time": 0}}, bindings)
- self.assertThat(
- event,
- Equals(
- dict(
- event="NEW",
- ip=str(ip),
- mac=format_eui(mac),
- time=0,
- vid=vid,
- )
- ),
- )
- vid = 4095
- event = update_bindings_and_get_event(bindings, vid, ip, mac, 0)
- self.assertThat(
- bindings,
- Equals(
- {
- (None, ip): {"mac": mac, "time": 0},
- (4095, ip): {"mac": mac, "time": 0},
- }
- ),
- )
- self.assertThat(
- event,
- Equals(
- dict(
- event="NEW",
- ip=str(ip),
- mac=format_eui(mac),
- time=0,
- vid=vid,
- )
- ),
- )
-
- def test_refreshed_binding(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac = EUI("00:01:02:03:04:05")
- vid = None
- update_bindings_and_get_event(bindings, vid, ip, mac, 0)
- event = update_bindings_and_get_event(
- bindings, vid, ip, mac, SEEN_AGAIN_THRESHOLD
- )
- self.assertEqual(
- {(vid, ip): {"mac": mac, "time": SEEN_AGAIN_THRESHOLD}},
- bindings,
- )
- self.assertThat(
- event,
- Equals(
- dict(
- event="REFRESHED",
- ip=str(ip),
- mac=format_eui(mac),
- time=SEEN_AGAIN_THRESHOLD,
- vid=vid,
- )
- ),
- )
-
- def test_refreshed_binding_within_threshold_does_not_emit_event(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac = EUI("00:01:02:03:04:05")
- vid = None
- update_bindings_and_get_event(bindings, vid, ip, mac, 0)
- event = update_bindings_and_get_event(bindings, vid, ip, mac, 1)
- self.assertEqual({(vid, ip): {"mac": mac, "time": 0}}, bindings)
- self.assertIsNone(event)
-
- def test_moved_binding(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac1 = EUI("00:01:02:03:04:05")
- mac2 = EUI("02:03:04:05:06:07")
- vid = None
- update_bindings_and_get_event(bindings, vid, ip, mac1, 0)
- event = update_bindings_and_get_event(bindings, vid, ip, mac2, 1)
- self.assertEqual({(vid, ip): {"mac": mac2, "time": 1}}, bindings)
- self.assertThat(
- event,
- Equals(
- dict(
- event="MOVED",
- ip=str(ip),
- mac=format_eui(mac2),
- time=1,
- previous_mac=format_eui(mac1),
- vid=vid,
- )
- ),
- )
-
-
-class FakeARP:
- """Fake ARP packet used for testing the processing of bindings."""
-
- def __init__(self, mock_bindings, time=0, vid=None):
- self.mock_bindings = mock_bindings
- self.time = time
- self.vid = vid
-
- def bindings(self):
- yield from self.mock_bindings
-
-
-class TestUpdateAndPrintBindings(MAASTestCase):
- def test_prints_bindings_in_json_format(self):
- bindings = {}
- ip = IPAddress("192.168.0.1")
- mac1 = EUI("00:01:02:03:04:05")
- mac2 = EUI("02:03:04:05:06:07")
- # Need to test with three bindings so that we ensure we cover JSON
- # output for NEW, MOVED, and REFRESHED. Two packets is sufficient
- # to test all three. (Though it would be three packets in real life,
- # it's better to test it this way, since some packets *do* have two
- # bindings.)
- arp1 = FakeARP([(ip, mac1), (ip, mac2)])
- arp2 = FakeARP([(ip, mac2)], time=SEEN_AGAIN_THRESHOLD)
- out = io.StringIO()
- update_and_print_bindings(bindings, arp1, out)
- update_and_print_bindings(bindings, arp2, out)
- self.assertEqual(
- {(None, ip): {"mac": mac2, "time": SEEN_AGAIN_THRESHOLD}},
- bindings,
- )
- output = io.StringIO(out.getvalue())
- lines = output.readlines()
- self.assertThat(lines, HasLength(3))
- line1 = json.loads(lines[0])
- self.assertThat(
- line1,
- Equals(
- {
- "ip": str(ip),
- "mac": format_eui(mac1),
- "time": 0,
- "event": "NEW",
- "vid": None,
- }
- ),
- )
- line2 = json.loads(lines[1])
- self.assertThat(
- line2,
- Equals(
- {
- "ip": str(ip),
- "mac": format_eui(mac2),
- "previous_mac": format_eui(mac1),
- "time": 0,
- "event": "MOVED",
- "vid": None,
- }
- ),
- )
- line3 = json.loads(lines[2])
- self.assertThat(
- line3,
- Equals(
- {
- "ip": str(ip),
- "mac": format_eui(mac2),
- "time": SEEN_AGAIN_THRESHOLD,
- "event": "REFRESHED",
- "vid": None,
- }
- ),
- )
-
-
-# Test data expected from an input PCAP file.
-test_input = (
- b"\xd4\xc3\xb2\xa1\x02\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00"
- b"@\x00\x00\x00\x01\x00\x00\x00*\xdc\xa0W\x9e+\x03\x00<\x00\x00\x00"
- b"<\x00\x00\x00\x80\xfa[\x0cFN\x00$\xa5\xaf$\x85\x08\x06\x00\x01"
- b"\x08\x00\x06\x04\x00\x01\x00$\xa5\xaf$\x85\xac\x10*\x01\x00\x00\x00\x00"
- b"\x00\x00\xac\x10*m\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"
- b"\x00\x00\x00\x00\x00\x00\x00\x00*\xdc\xa0W\xbb+\x03\x00*\x00\x00\x00"
- b"*\x00\x00\x00\x00$\xa5\xaf$\x85\x80\xfa[\x0cFN\x08\x06\x00\x01"
- b"\x08\x00\x06\x04\x00\x02\x80\xfa[\x0cFN\xac\x10*m\x00$\xa5\xaf"
- b"$\x85\xac\x10*\x01"
-)
-
-
class TestObserveARPCommand(MAASTestCase):
"""Tests for `maas-rack observe-arp`."""
- def test_requires_input_file(self):
+ def test_requires_input_interface(self):
parser = ArgumentParser()
add_arguments(parser)
args = parser.parse_args([])
@@ -579,15 +37,13 @@ class TestObserveARPCommand(MAASTestCase):
popen = self.patch(arp_module.subprocess, "Popen")
popen.return_value.poll = Mock()
popen.return_value.poll.return_value = None
- popen.return_value.stdout = io.BytesIO(test_input)
+ popen.return_value.stdout = io.StringIO("{}")
output = io.StringIO()
run(args, output=output)
self.assertThat(
popen,
MockCalledOnceWith(
- ["sudo", "-n", "/usr/lib/maas/network-monitor", "eth0"],
- stdin=subprocess.DEVNULL,
- stdout=subprocess.PIPE,
+ ["sudo", "-n", "/usr/sbin/maas-netmon", "eth0"], stdout=output
),
)
@@ -598,72 +54,16 @@ class TestObserveARPCommand(MAASTestCase):
popen = self.patch(arp_module.subprocess, "Popen")
popen.return_value.poll = Mock()
popen.return_value.poll.return_value = None
- popen.return_value.stdout = io.BytesIO(test_input)
+ popen.return_value.stdout = io.StringIO("{}")
output = io.StringIO()
run(args, output=output)
self.assertThat(
popen,
MockCalledOnceWith(
- ["sudo", "-n", "/usr/lib/maas/network-monitor", "eth0"],
- stdin=subprocess.DEVNULL,
- stdout=subprocess.PIPE,
+ ["sudo", "-n", "/usr/sbin/maas-netmon", "eth0"], stdout=output
),
)
- def test_checks_for_pipe(self):
- parser = ArgumentParser()
- add_arguments(parser)
- args = parser.parse_args(["--input-file", "-"])
- output = io.StringIO()
- stdin = self.patch(arp_module.sys, "stdin")
- stdin.return_value.fileno = Mock()
- fstat = self.patch(arp_module.os, "fstat")
- fstat.return_value.st_mode = None
- stat = self.patch(arp_module.stat, "S_ISFIFO")
- stat.return_value = False
- with ExpectedException(
- ActionScriptError, "Expected stdin to be a pipe"
- ):
- run(args, output=output)
-
- def test_allows_pipe_input(self):
- parser = ArgumentParser()
- add_arguments(parser)
- args = parser.parse_args(["--input-file", "-"])
- output = io.StringIO()
- stdin = self.patch(arp_module.sys, "stdin")
- stdin.return_value.fileno = Mock()
- fstat = self.patch(arp_module.os, "fstat")
- fstat.return_value.st_mode = None
- stat = self.patch(arp_module.stat, "S_ISFIFO")
- stat.return_value = True
- stdin_buffer = io.BytesIO(test_input)
- run(args, output=output, stdin_buffer=stdin_buffer)
-
- def test_allows_file_input(self):
- with NamedTemporaryFile("wb") as f:
- parser = ArgumentParser()
- add_arguments(parser)
- f.write(test_input)
- f.flush()
- args = parser.parse_args(["--input-file", f.name])
- output = io.StringIO()
- run(args, output=output)
-
- def test_raises_systemexit_observe_arp_return_code(self):
- parser = ArgumentParser()
- add_arguments(parser)
- args = parser.parse_args(["eth0"])
- popen = self.patch(arp_module.subprocess, "Popen")
- popen.return_value.poll = Mock()
- popen.return_value.poll.return_value = None
- popen.return_value.stdout = io.BytesIO(test_input)
- output = io.StringIO()
- observe_arp_packets = self.patch(arp_module, "observe_arp_packets")
- observe_arp_packets.return_value = 37
- with ExpectedException(SystemExit, ".*37.*"):
- run(args, output=output)
-
def test_raises_systemexit_poll_result(self):
parser = ArgumentParser()
add_arguments(parser)
@@ -671,7 +71,7 @@ class TestObserveARPCommand(MAASTestCase):
popen = self.patch(arp_module.subprocess, "Popen")
popen.return_value.poll = Mock()
popen.return_value.poll.return_value = None
- popen.return_value.stdout = io.BytesIO(test_input)
+ popen.return_value.stdout = io.StringIO("{}")
output = io.StringIO()
observe_arp_packets = self.patch(arp_module, "observe_arp_packets")
observe_arp_packets.return_value = None
References