Open Kilda Java Documentation
flow_tool.py
Go to the documentation of this file.
1 #!/usr/bin/python
2 # Copyright 2017 Telstra Open Source
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 # http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 #
16 
17 
18 from bottle import run, get, response, request, post, error, install
19 import ctypes
20 import multiprocessing
21 import os
22 import scapy.all as s
23 import socket
24 import logging
25 import json
26 from logging.config import dictConfig
27 from functools import wraps
28 
29 logger = logging.getLogger()
30 
31 
32 def log_to_logger(fn):
33  '''
34  Wrap a Bottle request so that a log line is emitted after it's handled.
35  (This decorator can be extended to take the desired logger as a param.)
36  '''
37  @wraps(fn)
38  def _log_to_logger(*args, **kwargs):
39  actual_response = fn(*args, **kwargs)
40  logger.info('%s %s %s %s' % (request.remote_addr,
41  request.method,
42  request.url,
43  response.status))
44  return actual_response
45  return _log_to_logger
46 
47 install(log_to_logger)
48 
49 number_of_packets = 1000
50 expected_delta = 500
51 of_ctl = "ovs-ofctl -O openflow13"
52 
53 
55  def _hatch(__):
56  def _hatchet():
57  for _ in pars:
58  if request.query.get(_) is None:
59  response.status = 500
60  return "%s: %s must be specified\n" % (request.path, _)
61  return __(dict([(_, request.query.get(_)) for _ in pars]))
62  return _hatchet
63  return _hatch
64 
65 
66 def respond(status, ok_message, fail_message):
67  if status:
68  response.status = 200
69  return ok_message
70  response.status = 503
71  return fail_message
72 
73 
74 @error(404)
75 def not_found(error):
76  return "Thank you, Mario! but our princess is in another castle!\n"
77 
78 
79 @post('/set_link_state')
80 @required_parameters("switch", "port", "newstate")
82  iface = "%s-eth%s" % (p['switch'], p['port'])
83  newstate = iface, p['newstate']
84  result = os.system("ifconfig %s %s" % newstate)
85  return respond(result == 0,
86  "Successfully put link %s in state %s\n" % newstate,
87  "Failed to put link %s in state %s\n" % newstate)
88 
89 
90 @get('/checkflowtraffic')
91 @required_parameters("srcswitch", "dstswitch", "srcport", "dstport", "srcvlan",
92  "dstvlan")
94  def traffic_sender(linkid, vlanid):
95  payload = s.Ether()/s.Dot1Q(vlan=int(vlanid))/s.IP()/s.ICMP()
96  s.sendp(payload, iface=linkid, count=number_of_packets)
97 
98  def traffic_listener(traffic_goes_through, vlanid, link):
99  # NOTE: sniff() takes optional filter argument which is supposed to
100  # contain BPF string. This filter is then supposed to be applied to
101  # captured packets in a manner similar to other traffic capture tools.
102  # However in case sniff() fails to use filtering it apparently just
103  # returns any packet instead of failing. It appears that running
104  # scapy in a container with insufficient (i.e. any other set than full
105  # set) privileges results exactly in this behavior. lfilter argument
106  # apparently makes things even worse since sniff appears to loose
107  # packets when lfilter is used.
108  # That is why an approach with a delta of packets and sniff timeout
109  # is used now. It appears to be the most reliable way to test traffic
110  # through flow.
111  result = s.sniff(timeout=5, iface=link)
112  received = sum(1 for _ in result if _.haslayer(s.ICMP))
113  if number_of_packets - received < expected_delta:
114  traffic_goes_through.value = True
115 
116  traffic_goes_through = multiprocessing.Value(ctypes.c_bool, False)
117  sender = multiprocessing.Process(
118  target=traffic_sender,
119  args=("%s-eth%s" % (p['srcswitch'], p['srcport']), p['srcvlan']))
120  checker = multiprocessing.Process(
121  target=traffic_listener,
122  args=(traffic_goes_through, p['dstvlan'],
123  "%s-eth%s" % (p['dstswitch'], p['dstport'])))
124  checker.start(), sender.start(), sender.join(5), checker.join(7)
125 
126  return respond(traffic_goes_through.value,
127  "Traffic seems to go through\n",
128  "Traffic does not seem to go through\n")
129 
130 
131 @post("/knockoutswitch")
132 @required_parameters("switch")
134  result = os.system("ovs-vsctl del-controller %s" % p['switch'])
135  return respond(result == 0,
136  "Switch %s is successfully knocked out\n" % p['switch'],
137  "Failed to knock out switch %s\n" % p['switch'])
138 
139 
140 @post("/reviveswitch")
141 @required_parameters("switch", "controller")
143  params = p['controller'].split(":", 3)
144  ip = socket.gethostbyname(params[1])
145  controller = params[0] + ":" + ip + ":" + params[2]
146  result = os.system("ovs-vsctl set-controller %s %s" %
147  (p['switch'], controller))
148  return respond(result == 0,
149  "Switch %s is successfully revived\n" % p['switch'],
150  "Failed to revive switch %s\n" % p['switch'])
151 
152 
153 @post("/cutlink")
154 @required_parameters("switch", "port")
155 def cut_link(p):
156  sppair = (p['switch'], p['port'])
157  result = os.system("ovs-ofctl add-flow %s priority=65500,in_port=%s,"
158  "action=drop -O openflow13" % sppair)
159  return respond(result == 0,
160  "Link to switch %s port %s is successfully cut\n" % sppair,
161  "Failed to cut link to switch %s port %s\n" % sppair)
162 
163 
164 @post("/restorelink")
165 @required_parameters("switch", "port")
167  sppair = (p['switch'], p['port'])
168  result = os.system("ovs-ofctl del-flows %s -O openflow13 \"priority=65500"
169  ",in_port=%s\" --strict" % (p['switch'], p['port']))
170  return respond(result == 0,
171  "Link to switch %s port %s is restored\n" % sppair,
172  "Failed to restore link to switch %s port %s\n" % sppair)
173 
174 
175 def port_mod(switch, port, action):
176  return os.system("%s mod-port %s %s %s" % (of_ctl, switch, port, action))
177 
178 
179 @post("/port/down")
180 @required_parameters("switch", "port")
181 def port_down(p):
182  result = port_mod(p['switch'], p['port'], 'down')
183  return respond(result == 0,
184  "Switch %s port %s down\n" % (p['switch'], p['port']),
185  "Fail switch %s port %s down\n" % (p['switch'], p['port']))
186 
187 
188 @post("/port/up")
189 @required_parameters("switch", "port")
190 def port_up(p):
191  result = port_mod(p['switch'], p['port'], 'up')
192  return respond(result == 0,
193  "Switch %s port %s up\n" % (p['switch'], p['port']),
194  "Fail switch %s port %s up\n" % (p['switch'], p['port']))
195 
196 
197 @post("/send_malformed_packet")
199  # This packet create isl between de:ad:be:ef:00:00:00:02 and
200  # de:ad:be:ef:00:00:00:02
201 
202  data = '\x02\x07\x04\xbe\xef\x00\x00\x00\x02\x04\x03\x02\x00\x01\x06\x02' \
203  '\x00x\xfe\x0c\x00&\xe1\x00\xde\xad\xbe\xef\x00\x00\x00\x02\xfe' \
204  '\x0c\x00&\xe1\x01\x00\x00\x01_\xb6\x8c\xacG\xfe\x08\x00&\xe1\x02' \
205  '\x00\x00\x00\x00\x00\x00'
206 
207  payload = (s.Ether(dst="00:26:e1:ff:ff:ff") /
208  s.IP(dst="192.168.0.255") /
209  s.UDP(dport=61231, sport=61231) /
210  data)
211 
212  try:
213  s.sendp(payload, iface="00000001-eth1")
214  return "ok"
215  except Exception as ex:
216  response.status = 500
217  return "can't send malformed packet {}".format(ex)
218 
219 
220 def main():
221  with open("/app/log.json", "r") as fd:
222  logging.config.dictConfig(json.load(fd))
223 
224  run(host='0.0.0.0', port=17191, debug=True)
def get(section, option)
Definition: config.py:43
def respond(status, ok_message, fail_message)
Definition: flow_tool.py:66
def port_mod(switch, port, action)
Definition: flow_tool.py:175
def required_parameters(pars)
Definition: flow_tool.py:54
def not_found(error)
Definition: flow_tool.py:75