Open Kilda Java Documentation
topology.py
Go to the documentation of this file.
1 # Copyright 2017 Telstra Open Source
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 #
15 
16 import ConfigParser
17 import datetime
18 import logging
19 import json
20 
21 from flask import Flask, flash, redirect, render_template, request, session, abort, url_for, Response, jsonify
22 from flask_login import LoginManager, UserMixin, login_required, login_user, logout_user, current_user
23 import py2neo
24 import pytz
25 from werkzeug import exceptions as http_errors
26 
27 from app import application
28 from . import neo4j_tools
29 
30 logger = logging.getLogger(__name__)
31 
32 config = ConfigParser.RawConfigParser()
33 config.read('topology_engine_rest.ini')
34 
35 neo4j_connect = neo4j_tools.connect(config)
36 
37 DATETIME_FORMAT = '%Y-%m-%dT%H:%M:%S.%fZ'
38 UNIX_EPOCH = datetime.datetime(1970, 1, 1, 0, 0, 0, 0, pytz.utc)
39 
40 
41 @application.route('/api/v1/topology/network')
42 @login_required
44  """
45  2017.03.08 (carmine) - this is now identical to api_v1_topology.
46  :return: the switches and links
47  """
48 
49  query = 'MATCH (n:switch) return n'
50  topology = []
51  for record in neo4j_connect.run(query).data():
52  record = record['n']
53  relations = [
54  rel['dst_switch']
55  for rel in neo4j_connect.match(nodes=[record], r_type='isl')]
56  relations.sort()
57  topology.append({
58  'name': record['name'],
59  'outgoing_relationships': relations
60  })
61 
62  topology.sort(key=lambda x:x['name'])
63  topology = {'nodes': topology}
64 
65  return json.dumps(topology, default=lambda o: o.__dict__, sort_keys=True)
66 
67 
68 class Nodes(object):
69  def toJSON(self):
70  return json.dumps(self, default=lambda o: o.__dict__, sort_keys=False, indent=4)
71 
72 class Edge(object):
73  def toJSON(self):
74  return json.dumps(self, default=lambda o: o.__dict__, sort_keys=False, indent=4)
75 
76 class Link(object):
77  def toJSON(self):
78  return json.dumps(self, default=lambda o: o.__dict__, sort_keys=False, indent=4)
79 
80 
81 @application.route('/api/v1/topology/nodes')
82 @login_required
84  edges = []
85  for record in neo4j_connect.run('MATCH (n:switch) return n').data():
86  source = record['n']
87 
88  for rel in neo4j_connect.match(nodes=[source], r_type='isl'):
89  dest = rel.end_node()
90 
91  s = Link()
92  s.label = source['name']
93  s.id = py2neo.remote(source)._id
94 
95  t = Link()
96  t.label = dest['name']
97  t.id = py2neo.remote(dest)._id
98 
99  edge = Edge()
100  edge.value = "{} to {}".format(s.label, t.label)
101  edge.source = s
102  edge.target = t
103 
104  edges.append(edge)
105 
106  edges.sort(key=lambda x: (x.source['id'], x.target['id']))
107  nodes = Nodes()
108  nodes.edges = edges
109 
110  return nodes.toJSON()
111 
112 
113 @application.route('/api/v1/topology/clear')
114 @login_required
116  """
117  Clear the entire topology
118  :returns the result of api_v1_network() after the delete
119  """
120  query = 'MATCH (n) detach delete n'
121  neo4j_connect.run(query)
122  return api_v1_network()
123 
124 
125 @application.route('/topology/network', methods=['GET'])
126 @login_required
128  return render_template('topologynetwork.html')
129 
130 
131 @application.route('/api/v1/topology/flows')
132 @login_required
134  try:
135  query = "MATCH (a:switch)-[r:flow]->(b:switch) RETURN r"
136  result = neo4j_connect.run(query).data()
137  flows = [format_flow(raw['r']) for raw in result]
138  return json.dumps(flows)
139  except Exception as e:
140  return "error: {}".format(str(e))
141 
142 
143 @application.route('/api/v1/topology/flows/<flow_id>')
144 @login_required
146  query = (
147  "MATCH (a:switch)-[r:flow]->(b:switch)\n"
148  "WHERE r.flowid = {flow_id}\n"
149  "RETURN r")
150  result = neo4j_connect.run(query, flow_id=flow_id).data()
151  if not result:
152  return http_errors.NotFound(
153  'There is no flow with flow_id={}'.format(flow_id))
154  if len(result) < 2:
155  return http_errors.NotFound('Flow data corrupted (too few results)')
156  elif 2 < len(result):
157  return http_errors.NotFound('Flow data corrupted (too many results)')
158 
159  flow_pair = [format_flow(record['r']) for record in result]
160  flow_pair.sort(key=lambda x: is_forward_cookie(x['cookie']))
161  flow_data = dict(zip(['reverse', 'forward'], flow_pair))
162 
163  return jsonify(flow_data)
164 
165 
166 def format_isl(link):
167  """
168  :param link: A valid Link returned from the db
169  :return: A dictionary in the form of org.openkilda.messaging.info.event.IslInfoData
170  """
171  isl = {
172  'clazz': 'org.openkilda.messaging.info.event.IslInfoData',
173  'latency_ns': int(link['latency']),
174  'path': [{'switch_id': link['src_switch'],
175  'port_no': int(link['src_port']),
176  'seq_id': 0,
177  'segment_latency': int(link['latency'])},
178  {'switch_id': link['dst_switch'],
179  'port_no': int(link['dst_port']),
180  'seq_id': 1,
181  'segment_latency': 0}],
182  'speed': link['speed'],
183  'state': get_isl_state(link),
184  'available_bandwidth': link['available_bandwidth'],
185  'time_create': format_db_datetime(link['time_create']),
186  'time_modify': format_db_datetime(link['time_modify'])
187  }
188 
189  # fields that have already been used .. should find easier way to do this..
190  already_used = list(isl.keys())
191  for k,v in link.iteritems():
192  if k not in already_used:
193  isl[k] = v
194 
195  return isl
196 
197 
198 def get_isl_state(link):
199  if link['status'] == 'active':
200  return 'DISCOVERED'
201  elif link['status'] == 'moved':
202  return 'MOVED'
203  else:
204  return 'FAILED'
205 
206 
207 def format_switch(switch):
208  """
209  :param switch: A valid Switch returned from the db
210  :return: A dictionary in the form of org.openkilda.messaging.info.event.SwitchInfoData
211  """
212  return {
213  'clazz': 'org.openkilda.messaging.info.event.SwitchInfoData',
214  'switch_id': switch['name'],
215  'address': switch['address'],
216  'hostname': switch['hostname'],
217  'state': 'ACTIVATED' if switch['state'] == 'active' else 'DEACTIVATED',
218  'description': switch['description']
219  }
220 
221 
222 def format_flow(raw_flow):
223  flow = raw_flow.copy()
224 
225  path = json.loads(raw_flow['flowpath'])
226  path['clazz'] = 'org.openkilda.messaging.info.event.PathInfoData'
227 
228  flow['flowpath'] = path
229 
230  return flow
231 
232 
233 @application.route('/api/v1/topology/links')
234 @login_required
236  """
237  :return: all isl relationships in the database
238  """
239  try:
240  query = "MATCH (a:switch)-[r:isl]->(b:switch) RETURN r"
241  result = neo4j_connect.run(query).data()
242 
243  links = []
244  for link in result:
245  neo4j_connect.pull(link['r'])
246  links.append(format_isl(link['r']))
247 
248  application.logger.info('links found %d', len(result))
249 
250  return jsonify(links)
251  except Exception as e:
252  return "error: {}".format(str(e))
253 
254 
255 @application.route('/api/v1/topology/switches')
256 @login_required
258  """
259  :return: all switches in the database
260  """
261  try:
262  query = "MATCH (n:switch) RETURN n"
263  result = neo4j_connect.run(query).data()
264 
265  switches = []
266  for sw in result:
267  neo4j_connect.pull(sw['n'])
268  switches.append(format_switch(sw['n']))
269 
270  application.logger.info('switches found %d', len(result))
271 
272  return jsonify(switches)
273  except Exception as e:
274  return "error: {}".format(str(e))
275 
276 
277 @application.route('/api/v1/topology/links/bandwidth/<src_switch>/<int:src_port>')
278 @login_required
279 def api_v1_topology_link_bandwidth(src_switch, src_port):
280  query = (
281  "MATCH (a:switch)-[r:isl]->(b:switch) "
282  "WHERE r.src_switch = '{}' AND r.src_port = {} "
283  "RETURN r.available_bandwidth").format(src_switch, int(src_port))
284 
285  return neo4j_connect.run(query).data()[0]['r.available_bandwidth']
286 
287 
288 @application.route('/api/v1/topology/routes/src/<src_switch>/dst/<dst_switch>')
289 @login_required
290 def api_v1_routes_between_nodes(src_switch, dst_switch):
291  depth = request.args.get('depth') or '10'
292  query = (
293  "MATCH p=(src:switch{{name:'{src_switch}'}})-[:isl*..{depth}]->"
294  "(dst:switch{{name:'{dst_switch}'}}) "
295  "WHERE ALL(x IN NODES(p) WHERE SINGLE(y IN NODES(p) WHERE y = x)) "
296  "WITH RELATIONSHIPS(p) as links "
297  "WHERE ALL(l IN links WHERE l.status = 'active') "
298  "RETURN links"
299  ).format(src_switch=src_switch, depth=depth, dst_switch=dst_switch)
300 
301  result = neo4j_connect.run(query).data()
302 
303  paths = []
304  for links in result:
305  current_path = []
306  for isl in links['links']:
307  path_node = build_path_nodes(isl, len(current_path))
308  current_path.extend(path_node)
309 
310  paths.append(build_path_info(current_path))
311 
312  return jsonify(paths)
313 
314 
315 def build_path_info(path):
316  return {
317  'clazz': 'org.openkilda.messaging.info.event.PathInfoData',
318  'path': path
319  }
320 
321 
322 def build_path_nodes(link, seq_id):
323  nodes = []
324  src_node = {
325  'clazz': 'org.openkilda.messaging.info.event.PathNode',
326  'switch_id': link['src_switch'],
327  'port_no': int(link['src_port']),
328  'seq_id': seq_id
329  }
330  nodes.append(src_node)
331 
332  dst_node = {
333  'clazz': 'org.openkilda.messaging.info.event.PathNode',
334  'switch_id': link['dst_switch'],
335  'port_no': int(link['dst_port']),
336  'seq_id': seq_id + 1
337  }
338  nodes.append(dst_node)
339  return nodes
340 
341 
342 # FIXME(surabujin): stolen from topology-engine code, must use some shared
343 # codebase
344 def is_forward_cookie(cookie):
345  return int(cookie) & 0x4000000000000000
346 
347 
349  if not value:
350  return None
351 
352  value = datetime.datetime.strptime(value, DATETIME_FORMAT)
353  value = value.replace(tzinfo=pytz.utc)
354 
355  from_epoch = value - UNIX_EPOCH
356  seconds = from_epoch.total_seconds()
357  return seconds * 1000 + from_epoch.microseconds // 1000
def api_v1_topology_get_flow(flow_id)
Definition: topology.py:145
def api_v1_topology_links()
Definition: topology.py:235
def format_isl(link)
Definition: topology.py:166
def build_path_nodes(link, seq_id)
Definition: topology.py:322
def toJSON(self)
Definition: topology.py:69
def api_v1_topology_nodes()
Definition: topology.py:83
def format_flow(raw_flow)
Definition: topology.py:222
def api_v1_routes_between_nodes(src_switch, dst_switch)
Definition: topology.py:290
def is_forward_cookie(cookie)
Definition: topology.py:344
def api_v1_topo_clear()
Definition: topology.py:115
def get_isl_state(link)
Definition: topology.py:198
def format_db_datetime(value)
Definition: topology.py:348
def format_switch(switch)
Definition: topology.py:207
def api_v1_topology_switches()
Definition: topology.py:257
def api_v1_topology_link_bandwidth(src_switch, src_port)
Definition: topology.py:279
def toJSON(self)
Definition: topology.py:73
def api_v1_topology_flows()
Definition: topology.py:133
def build_path_info(path)
Definition: topology.py:315
def api_v1_network()
Definition: topology.py:43
def topology_network()
Definition: topology.py:127