├── ovsdb_db ├── .fsm.db.~lock~ ├── commands ├── fsm.db └── fsm.ovsschema ├── src ├── monitors │ └── TODO ├── .DS_Store ├── __pycache__ │ └── ovsdb_client.cpython-37.pyc ├── device.py ├── fsm_FF.py ├── fsm_LB.py ├── fred_poc.py ├── variables.py ├── update_ovsdb.py ├── transition.py ├── agent.py ├── monitor.py ├── state.py ├── machine.py └── ovsdb_client.py ├── images ├── fred-gear.png └── controller_fred_switch.png ├── packages ├── pyasn1-0.4.4.tar.gz ├── pysnmp-4.4.6.tar.gz └── pycryptodomex-3.7.0.tar.gz ├── config ├── fsm_name_table.yaml ├── fsm_state_table.yaml ├── fsm_transition_table.yaml └── fsm_monitor_table.yaml ├── README.md ├── install.sh └── LICENSE /ovsdb_db/.fsm.db.~lock~: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/monitors/TODO: -------------------------------------------------------------------------------- 1 | # ADD PROGRAMMABLE MONITORS - SNMP MIB POLL, etc 2 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/src/.DS_Store -------------------------------------------------------------------------------- /images/fred-gear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/images/fred-gear.png -------------------------------------------------------------------------------- /packages/pyasn1-0.4.4.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/packages/pyasn1-0.4.4.tar.gz -------------------------------------------------------------------------------- /packages/pysnmp-4.4.6.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/packages/pysnmp-4.4.6.tar.gz -------------------------------------------------------------------------------- /images/controller_fred_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/images/controller_fred_switch.png -------------------------------------------------------------------------------- /packages/pycryptodomex-3.7.0.tar.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/packages/pycryptodomex-3.7.0.tar.gz -------------------------------------------------------------------------------- /src/__pycache__/ovsdb_client.cpython-37.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/comcast/Fred-Framework/master/src/__pycache__/ovsdb_client.cpython-37.pyc -------------------------------------------------------------------------------- /config/fsm_name_table.yaml: -------------------------------------------------------------------------------- 1 | - fsm_name: fsm_FF 2 | default_state: state_WAN1 3 | enable: false 4 | status: ready 5 | - fsm_name: fsm_FFLB 6 | default_state: state_WAN1 7 | enable: true 8 | status: ready 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fred-Framework 2 |

3 | 4 |

5 | 6 | ## Sequence diagram - Interactions within Fred's machinery 7 | ![Image of Fred](/images/controller_fred_switch.png) 8 | -------------------------------------------------------------------------------- /src/device.py: -------------------------------------------------------------------------------- 1 | from agent import Agent 2 | 3 | # Device (switch) has an agent 4 | 5 | class Device: 6 | 7 | def __init__(self): 8 | self.agent = Agent.create_agent(self) 9 | 10 | def start_agent(self): 11 | self.agent.launch() 12 | -------------------------------------------------------------------------------- /src/fsm_FF.py: -------------------------------------------------------------------------------- 1 | class fsm_FF(Machine): 2 | fsm_name = "fsm_FF" 3 | 4 | def load_default_state(self): 5 | pass 6 | 7 | def state_transition(self): 8 | pass 9 | 10 | def monitor_current_state(self): 11 | pass 12 | 13 | def machine_clean_up(self): 14 | pass 15 | -------------------------------------------------------------------------------- /src/fsm_LB.py: -------------------------------------------------------------------------------- 1 | class fsm_LB(Machine): 2 | fsm_name = "fsm_LB" 3 | fsm_default_state = "state_WAN1" 4 | 5 | def load_default_state(self): 6 | pass 7 | 8 | def state_transition(self): 9 | pass 10 | 11 | def monitor_current_state(self): 12 | pass 13 | 14 | def machine_clean_up(self): 15 | pass 16 | -------------------------------------------------------------------------------- /src/fred_poc.py: -------------------------------------------------------------------------------- 1 | from device import Device 2 | # The external driver 3 | 4 | # For any device, before spinning off the agent, define its states, default_state, 5 | # But the default state can be / should be obtained from the scema??? 6 | # POC just need to start the agent, the details of the state will be pulled from the OVSdb database 7 | 8 | # When a new device object is created, an agent for the device is automatically created. 9 | switch1 = Device() 10 | # Start the agent 11 | switch1.start_agent() 12 | 13 | #switch2 = Device() -------------------------------------------------------------------------------- /src/variables.py: -------------------------------------------------------------------------------- 1 | __FSM_NAME_TABLE__ = "fsm_name_table" 2 | __FSM_STATE_TABLE__ = "fsm_state_table" 3 | __FSM_MONITOR_TABLE__ = "fsm_monitor_table" 4 | __FSM_TRANSITION_TABLE__ = "fsm_transition_table" 5 | 6 | __FSM_TABLE_LIST__ = [__FSM_NAME_TABLE__, __FSM_STATE_TABLE__, __FSM_MONITOR_TABLE__, __FSM_TRANSITION_TABLE__] 7 | 8 | __OVSDB_IP__ = '127.0.0.1' 9 | __OVSDB_PORT__ = 6640 10 | 11 | __STATE_MONITOR_TIME__ = 30 12 | 13 | __USERNAME__ = 'admin-vhd' 14 | __PASSWORD__ = 'admin-vhd' 15 | __SWITCH_IP_PORT__ = '127.0.0.1:443' 16 | 17 | -------------------------------------------------------------------------------- /config/fsm_state_table.yaml: -------------------------------------------------------------------------------- 1 | - fsm_name: fsm_FF 2 | state_name: state_WAN1 3 | state_config: TODO RULES 4 | on_state_config: TODO 5 | off_state_config: TODO 6 | 7 | - fsm_name: fsm_FF 8 | state_name: state_WAN2 9 | state_config: TODO RULES 10 | on_state_config: TODO 11 | off_state_config: TODO 12 | 13 | - fsm_name: fsm_FFLB 14 | state_name: state_WAN1 15 | state_config: TODO RULES 16 | on_state_config: TODO 17 | off_state_config: TODO 18 | 19 | - fsm_name: fsm_FFLB 20 | state_name: state_WAN2 21 | state_config: TODO RULES 22 | on_state_config: TODO 23 | off_state_config: TODO 24 | -------------------------------------------------------------------------------- /ovsdb_db/commands: -------------------------------------------------------------------------------- 1 | ovsdb-tool create ./fsm.db ./fsm.ovsschema 2 | ovsdb-server --pidfile --detach --log-file --remote punix:/var/run/openvswitch/db.sock ./fsm.db 3 | ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640 4 | ovsdb-client dump tcp:172.17.0.3:6640 5 | 6 | 7 | ovsdb-client transact tcp:172.17.0.2:6640 '["FSM",{"op":"select", "table":"fsm_monitor_table", "where":[]}]' 8 | 9 | python update_ovsdb.py -q '["FSM",{"op":"update", "table":"fsm_monitor_table", "where":[["fsm_name", "==", "fsm_FF"],["monitor_name", "==", "monitor_WAN1"]], "row":{"desired_mib_value": 6} }]' 10 | 11 | sudo ovs-appctl -t ovsdb-server exit 12 | -------------------------------------------------------------------------------- /src/update_ovsdb.py: -------------------------------------------------------------------------------- 1 | from ovsdb_client import OVSDBConnection 2 | import variables as names 3 | import sys 4 | import argparse 5 | 6 | 7 | def main(argv): 8 | 9 | ap = argparse.ArgumentParser() 10 | ap.add_argument("-t", "--table_name", type=str, required=False, help="FSM table name to be updated") 11 | ap.add_argument("-q", "--update_query", type=str, required=True, help="Update query to run on the FSM table") 12 | args = ap.parse_args() 13 | 14 | ovsdb_client = OVSDBConnection(names.__OVSDB_IP__, names.__OVSDB_PORT__) 15 | ovsdb_client.update_fsm_table(args.update_query) 16 | 17 | 18 | 19 | if __name__ == "__main__": 20 | sys.exit(main(sys.argv)) -------------------------------------------------------------------------------- /config/fsm_transition_table.yaml: -------------------------------------------------------------------------------- 1 | - fsm_name: fsm_FF 2 | transition_name: WAN1_to_WAN2 3 | start_state: state_WAN1 4 | end_state: state_WAN2 5 | transition_logic: monitor_WAN1 6 | 7 | - fsm_name: fsm_FF 8 | transition_name: WAN2_to_WAN1 9 | start_state: state_WAN2 10 | end_state: state_WAN1 11 | transition_logic: monitor_WAN2 12 | 13 | - fsm_name: fsm_FFLB 14 | transition_name: WAN1_to_WAN2 15 | start_state: state_WAN1 16 | end_state: state_WAN2 17 | transition_logic: monitor_WAN1 18 | 19 | - fsm_name: fsm_FFLB 20 | transition_name: WAN2_to_WAN1 21 | start_state: state_WAN2 22 | end_state: state_WAN1 23 | transition_logic: monitor_WAN2 24 | 25 | - fsm_name: fsm_FFLB 26 | transition_name: 8020_to_WAN1 27 | start_state: state_8020 28 | end_state: state_WAN1 29 | transition_logic: monitor_WAN1 30 | -------------------------------------------------------------------------------- /ovsdb_db/fsm.db: -------------------------------------------------------------------------------- 1 | OVSDB JSON 803 efc0404ec8225d288767d13dc3ba4007b8da7446 2 | {"name":"FSM","tables":{"fsm_transition_table":{"columns":{"transition_logic":{"type":"string"},"transition_name":{"type":"string"},"start_state":{"type":"string"},"end_state":{"type":"string"},"fsm_name":{"type":"string"}}},"fsm_state_table":{"columns":{"on_state_config":{"type":"string"},"state_name":{"type":"string"},"state_config":{"type":"string"},"fsm_name":{"type":"string"},"off_state_config":{"type":"string"}}},"fsm_monitor_table":{"columns":{"fsm_name":{"type":"string"},"monitor_name":{"type":"string"},"run_period":{"type":"integer"},"upper_bound":{"type":"boolean"},"snmp_mib":{"type":"string"},"desired_mib_value":{"type":"integer"}}},"fsm_name_table":{"columns":{"fsm_name":{"type":"string"},"status":{"type":"string"},"default_state":{"type":"string"},"enable":{"type":"boolean"}}}}} 3 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | 2 | ############## SETUP PACKAGES ############### 3 | cd packages/ 4 | tar -xzvf pycryptodomex-3.7.0.tar.gz 5 | tar -xzvf pyasn1-0.4.4.tar.gz 6 | tar -xzvf pysnmp-4.4.6.tar.gz 7 | 8 | cd pyasn1-0.4.4 9 | python setup.py install 10 | cd ../pycryptodomex-3.7.0 11 | python setup.py install 12 | cd ../pysnmp-4.4.6 13 | python setup.py install 14 | cd ../ 15 | rm -rf pyasn1-0.4.4 16 | rm -rf pycryptodomex-3.7.0 17 | rm -rf pysnmp-4.4.6 18 | cd ../ 19 | 20 | ############## SETUP OVSDB-SERVER ############### 21 | cd ovsdb_db/ 22 | rm ./fsm.db 23 | ovsdb-tool create ./fsm.db ./fsm.ovsschema 24 | mkdir /var/run/openvswitch 25 | touch /var/run/openvswitch/ovsdb-server.pid 26 | mkdir /var/log/openvswitch 27 | touch /var/log/openvswitch/ovsdb-server.log 28 | ovs-appctl -t ovsdb-server exit 29 | ovsdb-server --pidfile --detach --log-file --remote punix:/var/run/openvswitch/db.sock ./fsm.db 30 | ovs-appctl -t ovsdb-server ovsdb-server/add-remote ptcp:6640 31 | ovsdb-client dump tcp:127.0.0.1:6640 32 | 33 | -------------------------------------------------------------------------------- /config/fsm_monitor_table.yaml: -------------------------------------------------------------------------------- 1 | # run_period: Time interval to run health monitors 2 | # snmp_mib: The SNMP MIB on the device to monitor 3 | # desired_mib_value: Expected value of the counters obtained from the device 4 | # upper_bound: True: desired value is maximum allowed || true: desired value is minimum allowed 5 | 6 | - fsm_name: fsm_FF 7 | monitor_name: monitor_WAN1 8 | run_period: 1 9 | snmp_mib: 1.3.6.1.2.1.25.3.2.1.1.7 10 | desired_mib_value: 7 11 | upper_bound: true 12 | 13 | - fsm_name: fsm_FF 14 | monitor_name: monitor_WAN2 15 | run_period: 1 16 | snmp_mib: 1.3.6.1.2.1.25.3.2.1.1.7 17 | desired_mib_value: 7 18 | upper_bound: true 19 | 20 | - fsm_name: fsm_FFLB 21 | monitor_name: monitor_WAN1 22 | run_period: 1 23 | snmp_mib: 1.3.6.1.2.1.25.3.2.1.1.7 24 | desired_mib_value: 6 25 | upper_bound: true 26 | 27 | - fsm_name: fsm_FFLB 28 | monitor_name: monitor_WAN2 29 | run_period: 1 30 | snmp_mib: 1.3.6.1.2.1.25.3.2.1.1.7 31 | desired_mib_value: 7 32 | upper_bound: true 33 | 34 | - fsm_name: fsm_FFLB 35 | monitor_name: monitor_8020 36 | run_period: 1 37 | snmp_mib: 1.3.6.1.2.1.25.3.2.1.1.7 38 | desired_mib_value: 7 39 | upper_bound: true 40 | -------------------------------------------------------------------------------- /src/transition.py: -------------------------------------------------------------------------------- 1 | class Transition(): 2 | 3 | def __init__(self, state, machine, transition): 4 | # Every transition belongs to some machine and some state 5 | self.fsm = machine 6 | self.start_state = state 7 | 8 | # Transition specific parameters 9 | self.transition_name = transition["transition_name"] 10 | # Transition logic is a boolean combination of one or more health monitors (monitor_WAN1+!monitor_WAN2) 11 | self.logic_list = transition["transition_logic"].split("+") 12 | self.end_state = self.fsm.get_state_object(transition["end_state"]) 13 | 14 | #print "Transition", self.fsm.name, self.start_state.name, self.transition_name, str(self.logic_list), "\n" 15 | 16 | def check_transition_logic(self, logic_list): 17 | for logic in logic_list: 18 | status = True 19 | if logic.startswith('!'): 20 | # Convert !monitor_WAN1 to monitor_WAN1 with status=False 21 | logic = logic[1:] 22 | status = False 23 | result = self.fsm.do_health_check(logic, status) 24 | # A True in result indicates that a transition is required 25 | return result -------------------------------------------------------------------------------- /src/agent.py: -------------------------------------------------------------------------------- 1 | import json 2 | from ovsdb_client import OVSDBConnection 3 | from machine import Machine 4 | import variables as names 5 | import logging 6 | # Singleton class: A device can have only one agent 7 | # Agent is owner of different states that are part of machines in device 8 | # Agent is also an owner of actions (methods) that can be applied to different states. 9 | 10 | # OWNS MACHINES 11 | 12 | class Agent: 13 | __agent = None 14 | _name = "FRED" 15 | 16 | @staticmethod 17 | def create_agent(device): 18 | if Agent.__agent is None: 19 | return Agent(device) 20 | return Agent.__agent 21 | 22 | def __init__(self, dev): 23 | if Agent.__agent is not None: 24 | raise Exception("Singleton Class: Agent can be created only with createAgent() method") 25 | else: 26 | Agent.__agent = self 27 | 28 | # Agent is a part of some device, dev 29 | self.device = dev 30 | 31 | # Agent can contain more than one FSM 32 | self.list_of_fsm = [] 33 | 34 | # Agent owns the ovsdb client (OVSdb server runs independently inside switch) 35 | self.ovsdb_client = OVSDBConnection(names.__OVSDB_IP__, names.__OVSDB_PORT__) 36 | 37 | def launch(self): 38 | # Reset and fill the tables in OVSdb using the fsm.ovsschema 39 | self.ovsdb_client.init_fsm_tables() 40 | 41 | # Get FSMs for this device from database (This can be done by reading YAML files too) 42 | kvargs = {"table_name": names.__FSM_NAME_TABLE__, "where": [], 43 | "callback": self.get_fsm_table_callback} 44 | self.ovsdb_client.get_fsm_table(**kvargs) 45 | 46 | # TODO This callback does not look good here 47 | # Callback to wait and receive the transaction output 48 | def get_fsm_table_callback(self, message): 49 | machines = message["result"][0]["rows"] 50 | for machine in machines: 51 | # Initialize an object for every fsm 52 | fsm = Machine(self, machine) 53 | # Add the new FSM to the FSMs owned by this agent 54 | self.list_of_fsm.append(fsm) 55 | # Signal the OVSdb client that this callback will not persist 56 | return False 57 | 58 | def enable_fsm(self, fsm_name): 59 | pass 60 | 61 | def report_to_controller(self): 62 | pass 63 | -------------------------------------------------------------------------------- /ovsdb_db/fsm.ovsschema: -------------------------------------------------------------------------------- 1 | { 2 | "name": "FSM", 3 | "tables": { 4 | "fsm_name_table": { 5 | "columns": { 6 | "fsm_name": { 7 | "type": "string" 8 | }, 9 | "default_state": { 10 | "type": "string" 11 | }, 12 | "enable": { 13 | "type": "boolean" 14 | }, 15 | "status": { 16 | "type": "string" 17 | } 18 | } 19 | }, 20 | 21 | "fsm_state_table": { 22 | "columns": { 23 | "fsm_name": { 24 | "type": "string" 25 | }, 26 | "state_name": { 27 | "type": "string" 28 | }, 29 | "state_config": { 30 | "type": "string" 31 | }, 32 | "on_state_config": { 33 | "type": "string" 34 | }, 35 | "off_state_config": { 36 | "type": "string" 37 | } 38 | } 39 | }, 40 | 41 | "fsm_transition_table": { 42 | "columns": { 43 | "fsm_name": { 44 | "type": "string" 45 | }, 46 | "transition_name": { 47 | "type": "string" 48 | }, 49 | "start_state": { 50 | "type": "string" 51 | }, 52 | "end_state": { 53 | "type": "string" 54 | }, 55 | "transition_logic": { 56 | "type": "string" 57 | } 58 | } 59 | }, 60 | 61 | "fsm_monitor_table": { 62 | "columns": { 63 | "fsm_name": { 64 | "type": "string" 65 | }, 66 | "monitor_name": { 67 | "type": "string" 68 | }, 69 | "run_period": { 70 | "type": "integer" 71 | }, 72 | "snmp_mib": { 73 | "type": "string" 74 | }, 75 | "desired_mib_value": { 76 | "type": "integer" 77 | }, 78 | "upper_bound": { 79 | "type": "boolean" 80 | } 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/monitor.py: -------------------------------------------------------------------------------- 1 | from pysnmp.entity.rfc3413.oneliner import cmdgen 2 | 3 | class Monitor: 4 | 5 | def __init__(self, machine, monitor): 6 | # Every monitor belong to some machine 7 | self.fsm = machine 8 | self.ovsdb_client = machine.ovsdb_client 9 | # Monitor specific parameters 10 | self.monitor_name = monitor["monitor_name"] 11 | self.run_period = monitor["run_period"] 12 | self.monitor_code = monitor["snmp_mib"] 13 | self.monitor_val = monitor["desired_mib_value"] 14 | self.upper_bound = monitor["upper_bound"] 15 | 16 | self.cmdGen = cmdgen.CommandGenerator() 17 | 18 | def update_monitor(self, monitor): 19 | self.monitor_name = monitor["monitor_name"] 20 | self.run_period = monitor["run_period"] 21 | self.monitor_code = monitor["snmp_mib"] 22 | self.monitor_val = monitor["desired_mib_value"] 23 | self.upper_bound = monitor["upper_bound"] 24 | 25 | # status: check based on TL's monitor: true/false 26 | # IMPORTANT: return value TRUE indicates that the monitor is 27 | def do_health_check(self, status): 28 | device_state = self.execute_monitor_code() 29 | 30 | # If the counters are greater than the maximum allowed 31 | if self.upper_bound and device_state > self.monitor_val: 32 | return True 33 | # If the counters are lesser than the minimum allowed 34 | elif not self.upper_bound and device_state < self.monitor_val: 35 | return True 36 | 37 | # Otherwise all is well - monitor returns false indicating no need to state change 38 | return False 39 | 40 | def execute_monitor_code(self): 41 | # TODO USE SWITCH's APIs to execute the monitor code 42 | # May be use callback to handle the output based on the need 43 | # output = device.run(self.monitor_code) 44 | #self.ovsdb_client.run_command_on_device(self.monitor_code) 45 | print "monitoring" 46 | return 7 47 | 48 | errorIndication, errorStatus, errorIndex, varBinds = self.cmdGen.getCmd( 49 | cmdgen.CommunityData('public'), 50 | cmdgen.UdpTransportTarget(('127.0.0.1', 161)), 51 | self.monitor_code 52 | ) 53 | if errorIndication: 54 | print(errorIndication) 55 | elif errorStatus: 56 | print('%s at %s' % (errorStatus.prettyPrint(), 57 | errorIndex and varBinds[int(errorIndex) - 1][0] or '?')) 58 | else: 59 | for varbind in varBinds: 60 | print varbind[0], " = ", varbind[1] 61 | mibValue = varbind[1] 62 | 63 | return mibValue 64 | 65 | -------------------------------------------------------------------------------- /src/state.py: -------------------------------------------------------------------------------- 1 | import variables as names 2 | from transition import Transition 3 | import time 4 | 5 | # OWNS TRANSITIONS 6 | 7 | class State(): 8 | 9 | def __init__(self, machine, state): 10 | # Every state belongs to a machine 11 | self.fsm = machine 12 | self.ovsdb_client = machine.ovsdb_client 13 | 14 | # State specific parameters 15 | self.name = state["state_name"] 16 | self.on_config = state["on_state_config"] 17 | self.off_config = state["off_state_config"] 18 | self.state_config = state["state_config"] 19 | 20 | # State transition happens when the transition logic (TL) is met 21 | # One start state can have multiple different transition based on the TL 22 | self.transitions_ready = False 23 | self.list_of_transitions = [] 24 | 25 | def get_transitions(self): 26 | where_clause = [["fsm_name", "==", self.fsm.name],["start_state", "==", self.name]] 27 | kvargs = {"table_name": names.__FSM_TRANSITION_TABLE__, "where": where_clause, 28 | "callback": self.get_transition_table_callback} 29 | self.ovsdb_client.get_fsm_table(**kvargs) 30 | 31 | def get_transition_table_callback(self, message): 32 | transitions = message["result"][0]["rows"] 33 | for transition in transitions: 34 | self.list_of_transitions.append(Transition(self, self.fsm, transition)) 35 | self.transitions_ready = True 36 | return False 37 | 38 | def run(self): 39 | # apply on_state config #TODO what is off_state_config?? 40 | self.apply_on_config() 41 | self.transitions_ready = False 42 | self.get_transitions() # Get any updated transitions every time -- needed? (impacts thread's performance) 43 | while not self.transitions_ready: 44 | continue # do nothing until transaction logic is ready 45 | # This will run only when the state is set to actve by the FSM 46 | print "\n","State THREAD READY", self.name, self.fsm.name, "\n" 47 | 48 | # Start monitoring state. Move to end_state if transition logic criterion is met. 49 | transition = self.list_of_transitions[0] 50 | while not transition.check_transition_logic(transition.logic_list): 51 | #self.transitions_ready = False 52 | #self.get_transitions() # Get any updated transitions every time -- needed? (impacts thread's performance) 53 | # Performance hack: Trigger the transaction with OVSdb before the state thread goes to sleep - new transactions will be ready when state thread becomes active 54 | time.sleep(names.__STATE_MONITOR_TIME__) # Monitor the state transition status periodically 55 | #while not self.transitions_ready: 56 | # continue # do nothing until transaction logic is ready 57 | 58 | new_state = transition.end_state # Move to the end_state when the transition logic is met 59 | self.fsm.transition_to_new_state(new_state) 60 | print "\n","State THREAD DYING", self.name, self.fsm.name, "\n" 61 | # End of thread's life 62 | 63 | def apply_on_config(self): 64 | # TODO Call device's API to push configuration into switch's config table 65 | pass 66 | 67 | def apply_off_config(self): 68 | pass 69 | -------------------------------------------------------------------------------- /src/machine.py: -------------------------------------------------------------------------------- 1 | import variables as names 2 | import logging 3 | import threading, time 4 | from state import State 5 | from monitor import Monitor 6 | 7 | # OWNS STATES, MONITORS 8 | # TODO Why maintain list of monitors when they are updated on runtime everytime? 9 | class Machine: 10 | 11 | def __init__(self, agent, machine): 12 | # A machine objects belongs to an agent 13 | self.agent = agent 14 | self.ovsdb_client = agent.ovsdb_client 15 | 16 | # Machine specific parameters 17 | self.name = machine["fsm_name"] 18 | self.default_state = machine["default_state"] 19 | self.status = machine["enable"] 20 | self.state_thread = None 21 | self.monitor_ready = False 22 | self.list_of_monitors = {} 23 | # If FSM's status is enabled, initialize states and monitors 24 | if self.status: 25 | # Every machine has a list of possible states 26 | self.list_of_states = [] 27 | self.get_all_states() 28 | # Every machine owns the monitors that run within 29 | #self.list_of_monitors = {} 30 | #self.get_all_monitors() 31 | 32 | def get_all_states(self): 33 | where_clause = [["fsm_name", "==", self.name]] 34 | kvargs = {"table_name": names.__FSM_STATE_TABLE__, "where": where_clause, 35 | "callback": self.get_states_table_callback} 36 | self.ovsdb_client.get_fsm_table(**kvargs) 37 | 38 | def get_states_table_callback(self, message): 39 | states = message["result"][0]["rows"] 40 | for state in states: 41 | new_state = State(self, state) 42 | self.list_of_states.append(new_state) 43 | # Check if the state is the default state and start it 44 | if new_state.name == self.default_state: 45 | self.state_thread = threading.Thread(target=new_state.run) 46 | self.state_thread.start() 47 | #logging.debug(state["state_name"]) 48 | return False 49 | 50 | def get_monitor(self, monitor_name): 51 | where_clause = [["fsm_name", "==", self.name],["monitor_name", "==", monitor_name]] 52 | kvargs = {"table_name": names.__FSM_MONITOR_TABLE__, "where": where_clause, 53 | "callback": self.get_monitor_table_callback} 54 | self.ovsdb_client.get_fsm_table(**kvargs) 55 | 56 | def get_monitor_table_callback(self, message): 57 | # TODO add error check here if ovsdb returns nothing 58 | monitors = message["result"][0]["rows"] 59 | 60 | # Ideally I expect only one monitor to be returned by FSM_NAME and MONITOR_NAME index key combination 61 | if len(monitors) != 1: 62 | logging.error("Undesired number of monitors %d returned", len(monitors)) 63 | 64 | for monitor in monitors: 65 | if not monitor["monitor_name"] in self.list_of_monitors: 66 | self.list_of_monitors[monitor["monitor_name"]] = Monitor(self, monitor) 67 | else: # The monitor exist -> update the monitor 68 | old_monitor = self.list_of_monitors.get(monitor["monitor_name"]) 69 | old_monitor.update_monitor(monitor) 70 | 71 | self.monitor_ready = True 72 | return False 73 | 74 | def do_health_check(self, monitor_name, status): 75 | self.monitor_ready = False 76 | self.get_monitor(monitor_name) # this is done every healthcheck interval for every FSM as the monitors can change real-time 77 | while not self.monitor_ready: 78 | continue # do nothing until new monitors are ready 79 | 80 | monitor = self.list_of_monitors.get(monitor_name) 81 | if not monitor: 82 | logging.error("Monitor %s not found!! Check the FSM_MONITOR_TABLE", monitor_name) 83 | return 84 | # Run the heath check for the monitor 85 | return monitor.do_health_check(status) 86 | 87 | def transition_to_new_state(self, new_state): 88 | try: 89 | time.sleep(1) 90 | self.state_thread = threading.Thread(target=new_state.run) 91 | self.state_thread.start() 92 | logging.debug("Starting fsm [%s] state: [%s] state: [%s]", self.name, 93 | str(self.state_thread.is_alive()), new_state.name) 94 | except Exception as e: 95 | logging.exception("exception [%s] while handling fsm [%s] state: [%s] state: [%s]", e.message, self.name, str(self.state_thread.is_alive()), new_state.name) 96 | pass 97 | 98 | def get_state_object(self, state_name): 99 | state = None 100 | for state in self.list_of_states: 101 | if state.name == state_name: 102 | found = True 103 | break 104 | if not found: 105 | logging.error("Error! State not found!! Check the FSM_STATE_TABLE") 106 | return None 107 | return state 108 | 109 | # Every FSM has a default state 110 | def get_default_state(self): 111 | # Get schema for state from OVSdb schema 112 | pass 113 | 114 | # Every FSM has a default state 115 | def add_state(self): 116 | # Get schema for state from OVSdb schema 117 | pass 118 | 119 | def start_machine(self): 120 | pass 121 | 122 | def enable(self): 123 | pass 124 | 125 | def disable(self): 126 | pass 127 | 128 | def set_default_state(self): 129 | pass 130 | 131 | def load_default_state(self): 132 | pass 133 | 134 | def state_transition(self): 135 | pass 136 | 137 | def monitor_current_state(self): 138 | pass 139 | 140 | def machine_clean_up(self): 141 | pass 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/ovsdb_client.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | import logging 4 | import os 5 | import socket 6 | import threading 7 | import yaml 8 | import variables as names 9 | from jsonrpclib import Server 10 | import ssl 11 | 12 | logging.basicConfig(level=logging.DEBUG) 13 | 14 | """ 15 | a simple client to talk to ovsdb over json rpc 16 | """ 17 | 18 | delete_json = \ 19 | [ 20 | "FSM", 21 | { 22 | "op":"delete", 23 | "table":"", 24 | "where": [] 25 | } 26 | ] 27 | 28 | insert_json = \ 29 | [ 30 | "FSM", 31 | { 32 | "op":"insert", 33 | "table":"", 34 | "row":{} 35 | } 36 | ] 37 | 38 | select_json = \ 39 | [ 40 | "FSM", 41 | { 42 | "op":"select", 43 | "table":"", 44 | "where":[] 45 | } 46 | ] 47 | 48 | """ 49 | Value inside where clause: 50 | [ 51 | "", 52 | "==", 53 | [ 54 | "", 55 | "" 56 | ] 57 | ] 58 | """ 59 | 60 | 61 | def default_echo_handler(message, ovsconn): 62 | logging.debug("responding to echo") 63 | ovsconn.send({"result": message.get("params", None), "error": None, "id": message['id']}) 64 | 65 | def default_message_handler(message, ovsconn): 66 | logging.debug("default handler called for method %s", message['method']) 67 | ovsconn.responses.append(message) 68 | 69 | class OVSDBConnection(threading.Thread): 70 | """Connects to an ovsdb server that has manager set using 71 | ovs-vsctl set-manager ptcp:5000 72 | clients can make calls and register a callback for results, callbacks 73 | are linked based on the message ids. 74 | clients can also register methods which they are interested in by 75 | providing a callback. 76 | """ 77 | 78 | def __init__(self, IP, PORT, **handlers): 79 | super(OVSDBConnection, self).__init__() 80 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 81 | self.socket.connect((IP, PORT)) 82 | self.responses = [] 83 | self.callbacks = {} 84 | self.callback_id = 0 85 | self.read_on = True 86 | self.handlers = handlers or {"echo": default_echo_handler} 87 | #self.daemon = True TODO Run thread in BG. 88 | self.start() 89 | 90 | def send(self, message, callback=None): 91 | if callback: 92 | self.callback_id += 1 93 | message['id'] = self.callback_id 94 | self.callbacks[self.callback_id] = callback 95 | self.socket.send(json.dumps(message)) 96 | 97 | def response(self, id): 98 | return [x for x in self.responses if x['id'] == id] 99 | 100 | def set_handler(self, method_name, handler): 101 | self.handlers[method_name] = handler 102 | 103 | def _on_remote_message(self, message): 104 | try: 105 | json_m = json.loads(message, 106 | object_pairs_hook=collections.OrderedDict) 107 | #check first to see if the message is for a method and we have a 108 | # handler for it 109 | handler_method = json_m.get('method', None) 110 | # handler method is returned as "echo" for default case. 111 | if handler_method: 112 | self.handlers.get(handler_method, default_message_handler)( 113 | json_m, self) 114 | elif json_m.get("result", None) and json_m['id'] in self.callbacks: 115 | id = json_m['id'] 116 | #check if this is a result of an earlier call we made and that 117 | # we have a callback registered 118 | if not self.callbacks[id](json_m): 119 | # if callback is to be persisted, callback should return 120 | # something 121 | self.callbacks.pop(id) 122 | else: 123 | #add it for sync clients 124 | default_message_handler(message, self) 125 | except Exception as e: 126 | logging.exception("exception [%s] while handling message [%s]", e.message, message) 127 | #self.read_on = False 128 | 129 | def __echo_response(message, self): 130 | self.send({"result": message.get("params", None), 131 | "error": None, "id": message['id']}) 132 | 133 | def run(self): 134 | 135 | chunks = [] 136 | lc = rc = 0 137 | while self.read_on: 138 | response = self.socket.recv(4096) 139 | if response: 140 | response = response.decode('utf8') 141 | message_mark = 0 142 | for i, c in enumerate(response): 143 | #todo fix the curlies in quotes 144 | if c == '{': 145 | lc += 1 146 | elif c == '}': 147 | rc += 1 148 | 149 | if rc > lc: 150 | raise Exception("json string not valid") 151 | 152 | elif lc == rc and lc is not 0: 153 | chunks.append(response[message_mark:i + 1]) 154 | message = "".join(chunks) 155 | self._on_remote_message(message) 156 | lc = rc = 0 157 | message_mark = i + 1 158 | chunks = [] 159 | 160 | chunks.append(response[message_mark:]) 161 | 162 | def stop(self, force=False): 163 | self.read_on = False 164 | if force: 165 | self.socket.close() 166 | 167 | def fsm_table_prune(self, fsm_table_name): 168 | delete_json[1]["table"] = fsm_table_name 169 | delete_query = {"method": "transact", "params": delete_json} 170 | #print (json.dumps(delete_query, indent=4)) 171 | self.send(delete_query, self.res) 172 | 173 | def fsm_table_setup(self, fsm_table_name): 174 | path = ''.join([os.getcwd(), "/../config/", fsm_table_name, ".yaml"]) 175 | file_path = os.path.abspath(path) 176 | with open(file_path, 'r') as stream: 177 | rows = yaml.safe_load(stream) 178 | for row in rows: 179 | insert_json[1]["table"] = fsm_table_name 180 | insert_json[1]["row"] = row 181 | insert_query = {"method": "transact", "params": insert_json} 182 | #print json.dumps(insert_query, indent=4) 183 | self.send(insert_query, self.res) 184 | 185 | def get_fsm_table(self, **kwargs): 186 | transact_query = {"method": "transact", "params": select_json} 187 | select_json[1]["table"] = kwargs["table_name"] 188 | select_json[1]["where"] = kwargs["where"] 189 | self.send(transact_query, kwargs["callback"]) 190 | 191 | def init_fsm_tables(self): 192 | for table in names.__FSM_TABLE_LIST__: 193 | self.fsm_table_prune(table) 194 | self.fsm_table_setup(table) 195 | 196 | def update_fsm_table(self, update_json): 197 | update_query = {"method": "transact", "params": json.loads(update_json)} 198 | self.send(update_query, self.update_callback) 199 | 200 | def update_callback(self, message): 201 | logging.debug("Updated the table and now exiting") 202 | self.read_on = False 203 | 204 | def run_command_on_device(self, command): 205 | # TODO This is a temporary workaround for certificate_verify_failed error for HTTPS 206 | if not os.environ.get('PYTHONHTTPSVERIFY', '') and getattr(ssl, '_create_unverified_context', None): 207 | ssl._create_default_https_context = ssl._create_unverified_context 208 | 209 | # switch = Server("https://admin:admin@10.0.0.254:443/command-api") 210 | server = "https://" + names.__USERNAME__ + ":" + names.__PASSWORD__ \ 211 | + "@" + names.__SWITCH_IP_PORT__ + "/command-api" 212 | switch = Server(server) 213 | # response = switch.runCmds(1, ["show hostname"]) 214 | response = switch.runCmds(1, command) 215 | logging.debug("Response: " + response) 216 | 217 | def res(self, message): 218 | #print "ovsdb-server response ", json.dumps(message, indent=4) 219 | return False -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------