blob: e7101300ec254d24bf6edb9b27a49ca35d5ffec1 [file] [log] [blame]
Srikanth Vavilapalli1725e492014-12-01 17:50:52 -08001#!/usr/bin/python
2#
3# Copyright (c) 2010,2013 Big Switch Networks, Inc.
4#
5# Licensed under the Eclipse Public License, Version 1.0 (the
6# "License"); you may not use this file except in compliance with the
7# License. You may obtain a copy of the License at
8#
9# http://www.eclipse.org/legal/epl-v10.html
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
14# implied. See the License for the specific language governing
15# permissions and limitations under the License.
16#
17
18import subprocess
19import os, atexit
20import sys, traceback # traceback.print_exc()
21from optparse import OptionParser
22from types import StringType
23import datetime
24import json
25import traceback
26import re
27import time
28import urllib2
29import httplib # provides error processing for isinstance
30import socket
31
32from prettyprint import PrettyPrinter
33from storeclient import StoreClient
34from climodelinfo import CliModelInfo
35from vendor import VendorDB
36
37class ParamException(Exception):
38 def __init__(self, value):
39 self.value = value
40 def __str__(self):
41 return repr(self.value)
42
43TIMESTAMP_HELP = \
44 ('Enter an integer timestamp in milliseconds since the epoch or formatted\n' +
45 'as one of the following options in your local timezone:\n'
46 '"YYYY-MM-DD HH:MM:SS" YYYY-MM-DDTHH:MM:SS\n' +
47 '"YYYY-MM-DD HH:MM:SS+TTT" YYYY-MM-DDTHH:MM:SS+TTT\n' +
48 'YYYY-MM-DD MM-DD\n' +
49 'HH:MM now')
50
51
52class Validate():
53
54 # initialize in init_obj_type_info
55 obj_type_info_dict = {} # see init_obj_type_info_dict
56 obj_types = []
57 obj_keys = {}
58
59 known_controllers = ["127.0.0.1:8000"]
60 controller = None
61 # LOOK! manage this separately until we can for sure talk to eth0 addr instead of 127.0.0.1
62 controller_for_prompt = "127.0.0.1:8000"
63 cluster = "default"
64
65 run = True
66
67 # Cached options for last completion
68 last_line = None
69 last_options = None
70
71 # Are we running in batch mode, i.e. "cat commands | cli.py"
72 batch = False
73
74 # used for parsing as an arg
75 local_name_pattern = "localhost://([A-Za-z0-9_:@\-\.\/]+$)"
76
77 # contained objects
78 pp = None
79 store = None
80 model_handler = None
81
82 #
83 # --------------------------------------------------------------------------------
84 # init_obj_type_info_dict
85 #
86 # Builds the init_obj_type_info_dict, which is a dictionary indexed by
87 # the current mode, which also results in a dictionary, then indexed
88 # by a either a 'field' or 'field_ordering' key.
89 #
90 # When init_obj_type_info_dict is indexed by the table/model name, the
91 # value is a dictionary. That dictionary has several keys:
92 # - 'fields'
93 # - 'field_orderings'
94 #
95 # The 'fields' indexed result returns a dictionary, each of which
96 # is indexed by the names of the fields within the table/model. further
97 # indicies describe details about that field within the table:
98 # - 'formatter' desscribes the prettyPrinter function for that field
99 # - 'primary_key' identifes this field as the key for the table/model.
100 # - 'verbose_name' identfies an additonal name for the field, inteneded
101 # to be more descriptive
102 # - 'has_rest_model' True/False
103 # - 'json_serialize_string' True/False
104 # - 'help_text' String providing more clues about the intent of this field
105 #
106 # The 'field_ordering' dictionary associated with the name of the table/model
107 # is used to order the output.
108 #
109 # Also populates the self.obj_keys[], which is a dictionary mapping the table/model
110 # name to the name of the storage key (table/model's column) for that table.
111 #
112 def init_obj_type_info_dict(self):
113 self.cli_model_info = CliModelInfo()
114 self.obj_type_info_dict = self.cli_model_info.get_complete_obj_type_info_dict()
115 self.obj_types = [k for (k,v) in self.obj_type_info_dict.items()
116 if 'has_rest_model' in v]
117 for (k,d) in self.obj_type_info_dict.items():
118 candidate_keys = [f for f in d['fields'].keys()
119 if d['fields'][f].get('primary_key', False)]
120 if len(candidate_keys) > 0:
121 self.obj_keys[k] = candidate_keys[0]
122
123
124 #
125 # --------------------------------------------------------------------------------
126 # make_unit_formatter
127 #
128 def make_unit_formatter(self, units):
129 return lambda x,y: '%s %s' % (x, units)
130
131 #
132 # --------------------------------------------------------------------------------
133 # init_obj_type_info_dict
134 #
135 # Builds the init_obj_type_info_dict, which is a dictionary indexed by
136 # the current mode, which also results in a dictionary, then indexed
137 # by a either a 'field' or 'field_ordering' key.
138 #
139 # When init_obj_type_info_dict is indexed by the table/model name, the
140 # value is a dictionary. That dictionary has several keys:
141 # - 'fields'
142 # - 'field_orderings'
143 #
144 # The 'fields' indexed result returns a dictionary, each of which
145 # is indexed by the names of the fields within the table/model. further
146 # indicies describe details about that field within the table:
147 # - 'formatter' desscribes the prettyPrinter function for that field
148 # - 'primary_key' identifes this field as the key for the table/model.
149 # - 'verbose_name' identfies an additonal name for the field, inteneded
150 # to be more descriptive
151 # - 'has_rest_model' True/False
152 # - 'json_serialize_string' True/False
153 # - 'help_text' String providing more clues about the intent of this field
154 #
155 # The 'field_ordering' dictionary associated with the name of the table/model
156 # is used to order the output.
157 #
158 # Also populates the self.obj_keys[], which is a dictionary mapping the table/model
159 # name to the name of the storage key (table/model's column) for that table.
160 #
161 def init_obj_type_info_dict(self):
162 self.cli_model_info = CliModelInfo()
163 self.obj_type_info_dict = self.cli_model_info.get_complete_obj_type_info_dict()
164 self.obj_types = [k for (k,v) in self.obj_type_info_dict.items()
165 if 'has_rest_model' in v]
166 for (k,d) in self.obj_type_info_dict.items():
167 candidate_keys = [f for f in d['fields'].keys()
168 if d['fields'][f].get('primary_key', False)]
169 if len(candidate_keys) > 0:
170 self.obj_keys[k] = candidate_keys[0]
171
172 #
173 # --------------------------------------------------------------------------------
174 # init
175 #
176 def init(self):
177
178 self.vendordb = VendorDB()
179 self.vendordb.init()
180
181 parser = OptionParser()
182 parser.add_option("-c", "--controller", dest="controller",
183 help="set default controller to CONTROLLER",
184 metavar="CONTROLLER", default=self.controller)
185 (options, args) = parser.parse_args()
186 self.controller = options.controller
187 if not self.controller:
188 self.controller = "127.0.0.1:8000"
189
190 if not sys.stdin.isatty():
191 self.batch = True
192
193 self.init_obj_type_info_dict()
194
195 self.pp = PrettyPrinter(self.obj_type_info_dict)
196 self.store = StoreClient()
197 self.store.set_controller(self.controller)
198
199
200 #
201 # --------------------------------------------------------------------------------
202 # parse_optional_parameters
203 #
204 def parse_optional_parameters(self, params, words):
205 parsed = {}
206 i = 0
207 while i < len(words):
208 word = words[i]
209 possible = [x for x in params if x.startswith(word)]
210 if len(possible) == 0:
211 raise ParamException('unknown option: %s' % word)
212 elif len(possible) > 1:
213 raise ParamException('ambiguous option: %s\n%s' %
214 (word, "\n".join(possible)))
215 else:
216 param_name = possible[0]
217 param = params[param_name]
218 if (param['type'] == 'flag'):
219 parsed[param_name] = True
220 else:
221 if i+1 < len(words):
222 argument = words[i+1]
223 if (param['type'] == 'string'):
224 parsed[param_name] = argument
225 elif (param['type'] == 'int'):
226 try:
227 parsed[param_name] = int(argument)
228 except ValueError:
229 raise ParamException('option %s requires ' +
230 'integer argument'
231 % word)
232 elif (param['type'] == 'enum'):
233 arg_possible = [x
234 for x in param['values']
235 if x.startswith(argument)]
236 if (len(arg_possible) == 0):
237 raise ParamException('option %s value must be in (%s)' %
238 (word,", ".join(param['values'])))
239 elif (len(arg_possible) > 1):
240 raise ParamException('ambiguous option %s value:\n%s' %
241 (word, "\n".join(arg_possible)))
242 else:
243 parsed[param_name] = arg_possible[0]
244 i += 1
245 else:
246 raise ParamException('option %s requires an argument'
247 % word)
248 i += 1
249 return parsed
250
251 #
252 # --------------------------------------------------------------------------------
253 # rest_error_to_dict
254 # Turn an exception into an error dictionary, which can later be printed.
255 # using rest_error_dict_to_message().
256 #
257 def rest_error_to_dict(self, e, detail=None):
258 errors = None
259 # try to identifify validation requests
260 if isinstance(e, httplib.BadStatusLine):
261 errors = {'connection_error' : 'REST API server %s: '
262 'server not running' %
263 self.controller}
264 return errors
265
266 elif isinstance(e, urllib2.HTTPError):
267 code = e.code
268 error_returned = e.readline()
269
270 if code == 404:
271 if detail:
272 errors = {'not_found_error' : 'Not Found: %s' % detail}
273 else:
274 errors = {'not_found_error' : 'Not Found: %s' % error_returned}
275 elif code == 500 or code == 403:
276 errors = {'connection_error' : 'REST API server %s unable to connect: '
277 'Cassandra possibly not running' %
278 self.controller}
279 elif code == 400:
280 try:
281 errors = json.loads(error_returned)
282 except:
283 # if the error can't be converted into a dictionary, then imbed the complete
284 # errors as the value for a specific error key.
285 errors = {'error_result_error': "Can't convert returned error: %s" % error_returned}
286 pass
287 else:
288 errors = {'unknown_error': 'HttpError %s' % error_returned}
289 else:
290 errors = {'unknown_error': "Need error managmenet for error %s" % type(e)}
291
292 return errors
293
294 #
295 # --------------------------------------------------------------------------------
296 # rest_error_dict_to_message
297 # Turn an rest_error_dict returned from rest_error_to_dict
298 # into an error message which can ge printed. Code assumes multiple errors
299 # won't occur; if a 'field_error' exists, for example, a 'model_error' won't
300 # also be posted in the error
301 #
302 def rest_error_dict_to_message(self, rest_error_dict):
303 error_msg = ""
304 if 'field_errors' in rest_error_dict:
305 for (k,v) in rest_error_dict['field_errors'].items():
306 error_msg += "Syntax error: field %s: %s" % (k,v)
307 # traceback.print_stack(), to find out why the error occured
308 elif 'model_error' in rest_error_dict:
309 error_msg += "Error: %s" % rest_error_dict['model_error']
310 elif 'not_found_error' in rest_error_dict:
311 error_msg += "Error: %s" % rest_error_dict['not_found_error']
312 elif 'connection_error' in rest_error_dict:
313 error_msg += rest_error_dict['connection_error']
314 elif 'error_result_error' in rest_error_dict:
315 error_msg += rest_error_dict['error_result_error']
316 elif 'unknown_error' in rest_error_dict:
317 error_msg += rest_error_dict['unknown_error']
318 else:
319 error_msg = "REST API server on controller-node %s " % self.controller
320 error_msg += "had %s error:\n" % rest_error_dict['error_type']
321 error_msg += rest_error_dict['description']
322 return error_msg
323
324 #
325 # --------------------------------------------------------------------------------
326 # method_from_name
327 #
328 def method_from_name(self, name):
329 return getattr(self, "validate_" + name.replace("-","_"), None)
330
331 #
332 # --------------------------------------------------------------------------------
333 # validate_port
334 #
335 # - validate port foreign key (to switch)
336 #
337 def validate_port(self):
338 error = None
339 print "validate_port"
340
341 # collect known switches
342 switch_ids = None
343 switch_key = self.obj_keys["switch"]
344 try:
345 switch_table = self.store.get_table_from_store("switch")
346 switch_ids = [x[switch_key] for x in switch_table]
347 except Exception, e:
348 error = self.rest_error_to_dict(e)
349 print self.rest_error_dict_to_message(error)
350 pass
351
352 if error:
353 print "Unable to collect switch, no switch dpid validation for port table"
354 error = None
355
356 # collect known ports
357 port_table = None
358 port_key = self.obj_keys["port"]
359 try:
360 port_table = self.store.get_table_from_store("port")
361 except Exception, e:
362 error = self.rest_error_to_dict(e)
363 print self.rest_error_dict_to_message(error)
364 pass
365
366 if error:
367 print "Unable to collect ports"
368 return
369
370 for port in port_table:
371 if not port_key in port:
372 print "No port id in row"
373 else:
374 port_id = port[port_key]
375 if not 'switch' in port:
376 print 'port %s No switch in port (foreign key)' % port_id
377
378 #
379 # --------------------------------------------------------------------------------
380 # validate_switch
381 #
382 # - validate switch foreign key (to controller)
383 #
384 def validate_switch(self):
385 print "validate_switch"
386 error = None
387
388 # collect known controllers
389 controller_ids = None
390 controller_key = self.obj_keys["controller-node"]
391 try:
392 controller_table = self.store.get_table_from_store("controller-node")
393 controller_ids = [x[contoller_key] for x in controller_table if controller_key in controller_table]
394 except Exception, e:
395 error = self.rest_error_to_dict(e)
396 print self.rest_error_dict_to_message(error)
397 pass
398
399 if error:
400 print "Unable to collect controller, no controller validation for switches"
401 error = None
402 if len(controller_ids) == 0:
403 print "Unable to collect any controller ids"
404
405 # collect known ports
406 switch_table = None
407 switch_key = self.obj_keys["switch"]
408 try:
409 switch_table = self.store.get_table_from_store("switch")
410 except Exception, e:
411 error = self.rest_error_to_dict(e)
412 print self.rest_error_dict_to_message(error)
413 pass
414
415 if error:
416 print "Unable to collect switches"
417 return
418
419 if len(switch_table) == 0:
420 print "switch table empty"
421
422 for switch in switch_table:
423 if not switch_key in switch:
424 print "No switch id in row"
425 else:
426 switch_id = switch[switch_key]
427 if not 'switch' in switch:
428 print 'switch %s No controller foreign key' % switch_id
429 else:
430 controller = switch['controller']
431 if not controller in controller_ids:
432 print "switch %s missing controller (foreign key) %s " % (switch_id, controller)
433
434 #
435 # --------------------------------------------------------------------------------
436 # validate_host_vns_interface
437 #
438 # - validate host-vns-interface foreigb key (to vns-interface)
439 # - validate host-vns-interface foreigb key (to host)
440 # - crack the id into three fragments, and validate each of the
441 # fragments references the expected componont (ie: matches the foreign key)
442 #
443 def validate_host_vns_interface(self):
444 print "host_vns_interface"
445
446 error = None
447 # collect host's
448 host_ids = None
449 host_key = self.obj_keys["host"]
450 try:
451 host_table = self.store.get_table_from_store("host")
452 host_ids = [x[host_key] for x in host_table]
453 except Exception, e:
454 error = self.rest_error_to_dict(e)
455 print self.rest_error_dict_to_message(error)
456 pass
457
458 if error:
459 print "Unable to collect hosts, no host-vns-interface host validation"
460 error = None
461
462 # collect vns-interfaces
463 vns_interface_ids = None
464 vns_interface_key = self.obj_keys["vns-interface"]
465 try:
466 vns_interface_table = self.store.get_table_from_store("vns-interface")
467 vns_interface_ids = [x[vns_interface_key] for x in vns_interface_table]
468 except Exception, e:
469 error = self.rest_error_to_dict(e)
470 print self.rest_error_dict_to_message(error)
471 pass
472
473 if error:
474 print "Unable to collect vns-interfaces, no host-vns-interface validation for vns-interfaces"
475 error = None
476
477 # collect host-vns-interface
478 host_vns_interface_ids = None
479 host_vns_interface_key = self.obj_keys["host-vns-interface"]
480 try:
481 host_vns_interface_table = self.store.get_table_from_store("host-vns-interface")
482 host_vns_interface_ids = [x[host_vns_interface_key] for x in host_vns_interface_table]
483 except Exception, e:
484 error = self.rest_error_to_dict(e)
485 print self.rest_error_dict_to_message(error)
486 pass
487
488 if error:
489 print "Unable to collect host-vns-interface"
490 return
491
492 if len(host_vns_interface_table) == 0:
493 print "host_vns_interface_table empty"
494
495 host_vns_interface_id = self.obj_keys['host-vns-interface']
496 for host_vns_interface in host_vns_interface_table:
497 if not host_vns_interface_id in host_vns_interface:
498 print "host_vns_interface no primary key"
499 this_host_vns_interface_id = host_vns_interface[host_vns_interface_id]
500 if not 'host' in host_vns_interface:
501 print "host_vns_interface %s no host foreign key" % this_host_vns_interface_id
502 else:
503 host_foreign_key = host_vns_interface['host']
504 if not host_foreign_key in host_ids:
505 print "host_vns_interface %s foreign key %s references missing host" % \
506 (this_host_vns_interface_id, host_foreign_key)
507
508 if not 'interface' in host_vns_interface:
509 print "host_vns_interface %s no vns-interface foreign key %s" % \
510 (this_host_vns_interface_id, foreign_key)
511 else:
512 interface_foreign_key = host_vns_interface['interface']
513 if not interface_foreign_key in vns_interface_ids:
514 print "host_vns_interface %s foreign key %s" % \
515 (this_host_vns_interface, interface_foreign_key)
516
517 parts = this_host_vns_interface_id.split("|")
518 if len(parts) != 3:
519 print "host_vns_interface_id %d needs to be three fields split by '|'"
520 else:
521 if parts[0] != host_foreign_key:
522 print "host_vns_interface %s related host foreign key %s isn't part of id" % \
523 (this_host_vns_interface, host_foreign_key)
524 # the interface_foreign_key should have two parts.
525 interface_parts = interface_foreign_key.split('|')
526 if len(interface_parts) != 2:
527 print "host_vns_interface %s related vns-interface foreign key %s " \
528 "needs to be two words split by '|'" % \
529 (this_host_vns_interface_id, interface_foreign_key)
530 elif interface_parts[0] != parts[1]:
531 print "host_vns_interface %s related vns_interface foreign key %s " \
532 "doesn't match host id part %s" % \
533 (this_host_vns_interface, interface_part[0], parts[1])
534 elif interface_parts[1] != parts[2]:
535 print "host_vns_interface %s related vns_interface foreign key %s " \
536 "doesn't match interface long name part %s" % \
537 (this_host_vns_interface, interface_part[1], parts[2])
538
539 #
540 # --------------------------------------------------------------------------------
541 # validate_vns_interface
542 #
543 def validate_vns_interface(self):
544 print "vns-interface"
545
546 # --------------------------------------------------------------------------------
547 # validate_vns_interface_rule
548 #
549 # - id exists,
550 # - each row has a foreign key
551 # - each foreign key exists
552 # - the id, which is a concatenation of vns name and row id, has the correct foreign key
553 #
554 def validate_vns_interface_rule(self):
555 print "vns_interface_rule"
556
557 error = None
558 # collect known vns's
559 try:
560 vns_table = self.store.get_table_from_store("vns-definition")
561 except Exception, e:
562 error = self.rest_error_to_dict(e)
563 pass
564 if error:
565 print "Unable to collect vns-definition"
566 return
567
568 # collect known switches
569 switch_ids = None
570 switch_key = self.obj_keys["switch"]
571 try:
572 switch_table = self.store.get_table_from_store("switch")
573 switch_ids = [x[switch_key] for x in switch_table]
574 except Exception, e:
575 error = self.rest_error_to_dict(e)
576 print self.rest_error_dict_to_message(error)
577 pass
578
579 if error:
580 print "Unable to collect switch, no switch dpid validation for vns rules"
581 error = None
582
583 try:
584 vns_interface_rules_table = self.store.get_table_from_store("vns-interface-rule")
585 except Exception, e:
586 error = self.rest_error_to_dict(e)
587 pass
588 if error:
589 print "Unable to collect vns-interface-rule"
590 return
591 vns_id = self.obj_keys["vns-interface-rule"]
592
593 for rule in vns_interface_rules_table:
594 if not vns_id in rule:
595 print "rule has missing rule id"
596 rule_id = rule[vns_id]
597 parts = rule_id.split("|")
598 if len(parts) < 2:
599 print "rule %s has invalid id" % rule_id
600 vns_part = parts[0]
601 if not 'vns' in rule:
602 print "rule %s has missing vns foreign key" % rule_id
603 else:
604 if rule['vns'] != vns_part:
605 print "rule %s has inconsistent vns foreign key: %s" % (rule_id, rule['vns'])
606 if 'ports' in rule and not 'switch' in rule:
607 print "rule %s has a ports field populated but no switch" % rule_id
608 elif 'switch' in rule and not 'ports' in rule:
609 print "rule %s has a switch field populated but no switch" % rule_id
610 if 'switch' in rule and not rule['switch'] in switch_ids:
611 print "rule %s has an unknown switch dpid %s" % (rule_id, rule['switch'])
612
613
614 #
615 # --------------------------------------------------------------------------------
616 # validate
617 #
618 def validate(self):
619 print "store validation"
620
621 tables = self.obj_type_info_dict.keys()
622 for table in tables:
623 method = self.method_from_name(table)
624 if method:
625 method()
626
627
628#
629# --------------------------------------------------------------------------------
630# Initialization crazyness to make it work across platforms. Many platforms don't include
631# GNU readline (e.g. mac os x) and we need to compensate for this
632
633import sys
634try:
635 import readline
636except ImportError:
637 try:
638 import pyreadline as readline
639 except ImportError:
640 print "Can't find any readline equivalent - aborting."
641else:
642 import rlcompleter
643 if(sys.platform == 'darwin'):
644 # needed for Mac, please fix Apple
645 readline.parse_and_bind ("bind ^I rl_complete")
646 else:
647 readline.parse_and_bind("tab: complete")
648 readline.parse_and_bind("?: possible-completions")
649
650
651#
652# --------------------------------------------------------------------------------
653# Run the shell
654
655def main():
656 # Uncomment the next two lines to enable remote debugging with PyDev
657 #import pydevd
658 #pydevd.settrace()
659 validate = Validate()
660 validate.init()
661 validate.validate()
662
663if __name__ == '__main__':
664 main()