Skip to content

Commit 074ea25

Browse files
authored
Merge pull request #197 from MichaelWasher/merge_refactor_1
Refactor Chewie for Better SoC
2 parents e567c0a + 4535c26 commit 074ea25

File tree

8 files changed

+354
-225
lines changed

8 files changed

+354
-225
lines changed

.pylintrc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
[MASTER]
22
jobs=4
33

4+
[MESSAGES CONTROL]
5+
disable=no-absolute-import
6+
7+
48
[TYPECHECK]
59
generated-members=socket.*

chewie/chewie.py

Lines changed: 110 additions & 192 deletions
Large diffs are not rendered by default.

chewie/managed_port.py

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
"""This module is used to represent a single 802.1x Port"""
2+
from chewie.utils import get_logger, EapQueueMessage, get_random_id
3+
from chewie.mac_address import MacAddress
4+
from chewie.event import EventPortStatusChange
5+
from chewie.message_parser import IdentityMessage
6+
from chewie.eap import Eap
7+
8+
9+
class ManagedPort:
10+
"""This class is used to represent a single 802.1x Port"""
11+
DEFAULT_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD = 20
12+
DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL = 60
13+
PAE_GROUP_ADDRESS = MacAddress.from_string("01:80:C2:00:00:03")
14+
15+
def __init__(self, port_id, log_prefix, timer_scheduler, eap_output_messages,
16+
radius_output_messages):
17+
self.port_id = port_id
18+
self.logger = get_logger(log_prefix)
19+
self.supplicant_output_messages = eap_output_messages
20+
self.radius_output_messages = radius_output_messages
21+
22+
self.state_machines = {} # mac : state_machine
23+
self.current_preemtive_eapol_id = None
24+
self.port_status = False # port_id: status (true=up, false=down)
25+
self.identity_job = None # timerJob
26+
self.session_job = None # timerJob
27+
self.timer_scheduler = timer_scheduler
28+
29+
@property
30+
def status(self):
31+
"""
32+
Returns the current status of the port.
33+
True is up
34+
False is down
35+
"""
36+
return self.port_status
37+
38+
@status.setter
39+
def status(self, value):
40+
"""
41+
Send status of a port at port_id
42+
Args:
43+
port_id ():
44+
status ():
45+
"""
46+
self.port_status = value
47+
48+
# Trigger Subscribers
49+
for _, state_machine in self.state_machines.items():
50+
event = EventPortStatusChange(value)
51+
state_machine.event(event)
52+
53+
if not value:
54+
self.state_machines.clear()
55+
56+
@property
57+
def clients(self):
58+
"""Returns a list of all managed clients that are attached to this port"""
59+
return [(self.port_id, mac) for mac in self.state_machines.items()]
60+
61+
def stop_identity_requests(self):
62+
"""Stop sending Preemptive Identitity Requests"""
63+
if self.identity_job:
64+
self.identity_job.cancel()
65+
66+
self.current_preemtive_eapol_id = None
67+
68+
def start_identity_requests(self):
69+
"""Start Sending Preemptive Identity Requests"""
70+
self.identity_job = self.timer_scheduler.call_later(
71+
self.DEFAULT_PORT_UP_IDENTITY_REQUEST_WAIT_PERIOD,
72+
self.send_preemptive_identity_request)
73+
74+
def send_preemptive_identity_request(self):
75+
"""
76+
If there is no active (in progress, or in state success(2)) supplicant send out the
77+
preemptive identity request message.
78+
"""
79+
if not self.port_status:
80+
self.logger.debug(
81+
'cant send output on port %s is down', self.port_id)
82+
return
83+
84+
self.logger.debug("Sending Identity Request on port %s", self.port_id)
85+
# schedule next request.
86+
self.identity_job = self.timer_scheduler.call_later(
87+
self.DEFAULT_PREEMPTIVE_IDENTITY_REQUEST_INTERVAL,
88+
self.send_preemptive_identity_request)
89+
90+
self._send_identity_request()
91+
92+
def _send_identity_request(self):
93+
"""
94+
Message (EAP Identity Request) that notifies supplicant that port is using 802.1X
95+
Args:
96+
port_id (str):
97+
98+
"""
99+
_id = get_random_id()
100+
self.current_preemtive_eapol_id = _id
101+
data = IdentityMessage(self.PAE_GROUP_ADDRESS, _id, Eap.REQUEST, "")
102+
self.supplicant_output_messages.put_nowait(
103+
EapQueueMessage(data, self.PAE_GROUP_ADDRESS, MacAddress.from_string(self.port_id)))
104+
return _id
105+
106+
def start_port_session(self, period, src_mac):
107+
"""Start a port session"""
108+
self.session_job = self.timer_scheduler.call_later(
109+
period,
110+
self._reauth_port, src_mac)
111+
112+
def _reauth_port(self, src_mac):
113+
"""
114+
Send an Identity Request to src_mac, on port_id.
115+
prompting the supplicant to re authenticate.
116+
Args:
117+
src_mac (MacAddress):
118+
port_id (str):
119+
"""
120+
state_machine = self.state_machines.get(str(src_mac), None)
121+
122+
if state_machine and state_machine.is_success():
123+
self.logger.info(
124+
'reauthenticating src_mac: %s on port: %s', src_mac, self.port_id)
125+
self.start_identity_requests()
126+
127+
elif state_machine is None:
128+
self.logger.debug('not reauthing. state machine on port: %s, mac: %s is none',
129+
self.port_id,
130+
src_mac)
131+
else:
132+
self.logger.debug("not reauthing, authentication is not in success(2) (state: %s)'",
133+
state_machine.state)

chewie/radius_lifecycle.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def __init__(self, radius_secret, server_id, logger):
3333
self.packet_id_to_request_authenticator = {}
3434

3535
def process_outbound(self, radius_output_bits):
36-
"""Placeholder method extracted from Chewie.send_radius_messages()"""
36+
"""Placeholder method extracted from Chewie._send_radius_messages()"""
3737
radius_payload = radius_output_bits.message
3838
src_mac = radius_output_bits.src_mac
3939
username = radius_output_bits.identity
@@ -71,7 +71,7 @@ def build_event_radius_message_received(self, radius):
7171
return EventRadiusMessageReceived(radius, state, radius.attributes.to_dict())
7272

7373
def process_outbound_mab_request(self, radius_output_bits):
74-
"""Placeholder method extracted from Chewie.send_radius_messages()"""
74+
"""Placeholder method extracted from Chewie._send_radius_messages()"""
7575
src_mac = radius_output_bits.src_mac
7676
port_id = radius_output_bits.port_mac
7777
self.logger.info("Sending MAB to RADIUS: %s", src_mac)

chewie/utils.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Utility Functions"""
22
import logging
33
from collections import namedtuple # pytype: disable=pyi-error
4+
import random
45

56

67
def get_logger(logname):
@@ -20,6 +21,10 @@ def wrapped(self, *args, **kwargs):
2021
return wrapped
2122

2223

24+
def get_random_id(): # pylint: disable=missing-docstring
25+
return random.randint(0, 200)
26+
27+
2328
class MessageParseError(Exception):
2429
"""Error for when parsing cannot be successfully completed."""
2530
pass

test/unit/test_chewie.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from chewie.state_machines.mab_state_machine import MacAuthenticationBypassStateMachine
1515
from eventlet.queue import Queue
1616

17+
from chewie.managed_port import ManagedPort
1718
from helpers import FakeTimerScheduler
1819

1920
FROM_SUPPLICANT = Queue()
@@ -30,7 +31,7 @@
3031
def patch_things(func):
3132
"""decorator to mock patch socket operations and random number generators"""
3233

33-
@patch('chewie.chewie.get_random_id', get_random_id_helper)
34+
@patch('chewie.managed_port.get_random_id', get_random_id_helper)
3435
@patch('chewie.chewie.EapSocket', FakeEapSocket)
3536
@patch('chewie.chewie.RadiusSocket', FakeRadiusSocket)
3637
@patch('chewie.chewie.MabSocket', FakeMabSocket)
@@ -296,7 +297,6 @@ def test_get_state_machine(self):
296297
state_machine = self.chewie.get_state_machine('12:34:56:78:9a:bc',
297298
# pylint: disable=invalid-name
298299
'00:00:00:00:00:01')
299-
300300
self.assertEqual(len(self.chewie.state_machines), 1)
301301

302302
self.assertIs(state_machine, self.chewie.get_state_machine('12:34:56:78:9a:bc',
@@ -314,6 +314,7 @@ def test_get_state_machine(self):
314314
# port 2 has 1 mac
315315
self.assertEqual(len(self.chewie.state_machines['00:00:00:00:00:02']), 1)
316316

317+
# TODO Stop Test from touching internal get_state_machine_from_radius_packet
317318
def test_get_state_machine_by_packet_id(self):
318319
"""Tests Chewie.get_state_machine_by_packet_id()"""
319320
self.chewie.radius_lifecycle.packet_id_to_mac[56] = {'src_mac': '12:34:56:78:9a:bc',
@@ -322,10 +323,10 @@ def test_get_state_machine_by_packet_id(self):
322323
# pylint: disable=invalid-name
323324
'00:00:00:00:00:01')
324325

325-
self.assertIs(self.chewie.get_state_machine_from_radius_packet_id(56),
326+
self.assertIs(self.chewie._get_state_machine_from_radius_packet_id(56),
326327
state_machine)
327328
with self.assertRaises(KeyError):
328-
self.chewie.get_state_machine_from_radius_packet_id(20)
329+
self.chewie._get_state_machine_from_radius_packet_id(20)
329330

330331
@patch_things
331332
@setup_generators(sup_replies_success, radius_replies_success)
@@ -343,9 +344,6 @@ def test_success_dot1x(self):
343344
'00:00:00:00:00:01').state,
344345
FullEAPStateMachine.SUCCESS2)
345346

346-
347-
348-
349347
@patch_things
350348
@setup_generators(sup_replies_success, radius_replies_success)
351349
def test_chewie_identity_response_dot1x(self):
@@ -403,7 +401,7 @@ def test_port_status_changes(self):
403401
# This will keep adding jobs forever.
404402
self.assertEqual(len(self.fake_scheduler.jobs), 1)
405403
self.assertEqual(self.fake_scheduler.jobs[0].function.__name__,
406-
Chewie.send_preemptive_identity_request_if_no_active_on_port.__name__)
404+
ManagedPort.send_preemptive_identity_request.__name__)
407405

408406

409407
@patch_things
@@ -545,3 +543,16 @@ def test_mab_failure_auth(self):
545543
self.chewie.get_state_machine('02:42:ac:17:00:6f',
546544
'00:00:00:00:00:01').state,
547545
MacAuthenticationBypassStateMachine.AAA_FAILURE)
546+
547+
@patch_things
548+
@setup_generators(sup_replies_success, radius_replies_success)
549+
def test_smoke_test_clients(self):
550+
"""Test success api"""
551+
FROM_SUPPLICANT.put_nowait(bytes.fromhex("0000000000010242ac17006f888e01010000"))
552+
553+
pool = eventlet.GreenPool()
554+
pool.spawn(self.chewie.run)
555+
556+
eventlet.sleep(1)
557+
self.assertIsNotNone(self.chewie.clients)
558+
self.assertEqual(len(self.chewie.clients), 1)

test/unit/test_chewie_mocks.py

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from chewie.event import EventMessageReceived
99
from chewie.utils import EapQueueMessage
1010

11+
# pylint: disable=protected-access
1112

1213
def return_if(expected, return_value):
1314
"""allows us to do expect-this-return-that style mocking"""
@@ -21,8 +22,10 @@ def inner_function(*args):
2122
return inner_function
2223

2324

24-
FakeLogger = namedtuple('FakeLogger', ('name',)) # pylint: disable=invalid-name
25-
FakeEapMessage = namedtuple('FakeEapMessage', ('src_mac',)) # pylint: disable=invalid-name
25+
FakeLogger = namedtuple('FakeLogger', ('name',)
26+
) # pylint: disable=invalid-name
27+
FakeEapMessage = namedtuple(
28+
'FakeEapMessage', ('src_mac',)) # pylint: disable=invalid-name
2629

2730

2831
class ChewieWithMocksTestCase(unittest.TestCase):
@@ -41,14 +44,16 @@ def setUp(self):
4144
def test_eap_packet_in_goes_to_new_state_machine(self, state_machine,
4245
ethernet_parse): # pylint: disable=invalid-name
4346
"""test EAP packet creates a new state machine and is sent on"""
44-
self.chewie.eap_socket = Mock(**{'receive.return_value': 'message from socket'})
47+
self.chewie._eap_socket = Mock(
48+
**{'receive.return_value': 'message from socket'})
4549
ethernet_parse.side_effect = return_if(
4650
('message from socket',),
4751
(FakeEapMessage('fake src mac'), 'fake dst mac')
4852
)
49-
self.chewie.receive_eap_messages()
53+
self.chewie._receive_eap_messages()
5054
state_machine().event.assert_called_with(
51-
EventMessageReceived(FakeEapMessage('fake src mac'), 'fake dst mac')
55+
EventMessageReceived(FakeEapMessage(
56+
'fake src mac'), 'fake dst mac')
5257
)
5358

5459
@patch("chewie.chewie.Chewie.running", Mock(side_effect=[True, False]))
@@ -57,34 +62,35 @@ def test_eap_packet_in_goes_to_new_state_machine(self, state_machine,
5762
def test_eap_output_packet_gets_packed_and_sent(self,
5863
ethernet_pack): # pylint: disable=invalid-name
5964
"""test EAP packet creates a new state machine and is sent on"""
60-
self.chewie.eap_socket = Mock()
65+
self.chewie._eap_socket = Mock()
6166
ethernet_pack.return_value = "packed ethernet"
6267
self.chewie.eap_output_messages.put_nowait(
6368
EapQueueMessage("output eap message", "src mac", "port mac"))
64-
self.chewie.send_eap_messages()
65-
self.chewie.eap_socket.send.assert_called_with("packed ethernet")
69+
self.chewie._send_eap_messages()
70+
self.chewie._eap_socket.send.assert_called_with("packed ethernet")
6671

6772
@patch("chewie.chewie.Chewie.running", Mock(side_effect=[True, False]))
6873
@patch("chewie.chewie.MessageParser.radius_parse")
69-
@patch("chewie.chewie.Chewie.get_state_machine_from_radius_packet_id")
74+
@patch("chewie.chewie.Chewie._get_state_machine_from_radius_packet_id")
7075
@patch("chewie.chewie.sleep", Mock())
7176
def test_radius_packet_in_goes_to_state_machine(self, state_machine,
7277
radius_parse): # pylint: disable=invalid-name
7378
"""test radius packet goes to a state machine"""
7479
# note that the state machine has to exist already - if not then we blow up
7580
fake_radius = namedtuple('Radius', ('packet_id',))('fake packet id')
76-
self.chewie.radius_socket = Mock(**{'receive.return_value': 'message from socket'})
81+
self.chewie._radius_socket = Mock(
82+
**{'receive.return_value': 'message from socket'})
7783
self.chewie.radius_lifecycle = Mock(**{'build_event_radius_message_received.side_effect':
78-
return_if(
79-
(fake_radius,),
80-
'fake event'
81-
)})
84+
return_if(
85+
(fake_radius,),
86+
'fake event'
87+
)})
8288
radius_parse.side_effect = return_if(
8389
('message from socket', 'SECRET', self.chewie.radius_lifecycle),
8490
fake_radius
8591
)
8692
# not checking args as we can't mock the callback
87-
self.chewie.receive_radius_messages()
93+
self.chewie._receive_radius_messages()
8894
state_machine().event.assert_called_with(
8995
'fake event'
9096
)
@@ -93,13 +99,14 @@ def test_radius_packet_in_goes_to_state_machine(self, state_machine,
9399
@patch("chewie.chewie.sleep", Mock())
94100
def test_radius_output_packet_gets_packed_and_sent(self): # pylint: disable=invalid-name
95101
"""test EAP packet creates a new state machine and is sent on"""
96-
self.chewie.radius_socket = Mock()
102+
self.chewie._radius_socket = Mock()
97103

98-
self.chewie.radius_output_messages.put_nowait('fake radius output bits')
104+
self.chewie.radius_output_messages.put_nowait(
105+
'fake radius output bits')
99106
self.chewie.radius_lifecycle = Mock(**{'process_outbound.side_effect':
100-
return_if(
101-
('fake radius output bits',),
102-
'packed radius'
103-
)})
104-
self.chewie.send_radius_messages()
105-
self.chewie.radius_socket.send.assert_called_with("packed radius")
107+
return_if(
108+
('fake radius output bits',),
109+
'packed radius'
110+
)})
111+
self.chewie._send_radius_messages()
112+
self.chewie._radius_socket.send.assert_called_with("packed radius")

0 commit comments

Comments
 (0)