blob: 127ec955780f609be8375542c1180bce0fbb52c9 [file] [log] [blame]
Pavlin Radoslavovb94817b2014-05-22 21:12:47 -07001#! /usr/bin/env python
2# -*- Mode: python; py-indent-offset: 4; tab-width: 8; indent-tabs-mode: t; -*-
3
4"""
5onoscli : ONOS-specific Command Line Interface
6
7Usage:
8 # Running the CLI in interactive mode:
9 $ ./onoscli
10
11 # Running multiple CLI commands in batch mode
12 $ cat commands.txt | ./onoscli
13
14 # Running a single command from the system command-line
15 $ ./onoscli -c show switch all
16
17 # Run the following command for additional help
18 $ ./onoscli -h
19"""
20
21#
22# INSTALLATION NOTES: MUST install Python cmd2 module. E.g., on Ubuntu:
23# sudo apt-get install python-cmd2
24# On older Ubuntu installations (e.g., Ubuntu-12.04), install also:
25# sudo apt-get install python-pyparsing
26#
27
28#
29# ADDING A NEW CLI COMMAND:
30# 1. Add the appropriate Command entry (or entries) inside array
31# OnosCli.init_commands.
32# See the comments for init_commands for details.
33# 2. Add the appropriate callback (inside class OnosCli) for the CLI command
34#
35#
36
37import sys
38import argparse
39import json
40from optparse import make_option
41import urllib2
42from urllib2 import URLError, HTTPError
43import cmd2
44from cmd2 import Cmd
45from cmd2 import options
46
47
48class Command():
49 "Command node. A hierarchy of nodes are organized in a command tree."
50
51 def __init__(self, name, help, callback=None, add_parser_args=None):
52 """name: a string with the full command name
53 help: the help string for the command
54 callback: the method to be called if the command is executed
55 add_parser_args: the parser arguments to add to the command: a dictionary of argparse arguments"""
56 # Normalize the name by removing extra spaces
57 self.split_name = name.split() # List of the words in the name
58 self.name = ' '.join(self.split_name) # Normalized name
59 self.last_subname = self.split_name[-1] # Last word in the name
60 self.parent_name = ' '.join(self.split_name[:-1]) # Name of parent command
61 self.help = help # THe help string
62 self.callback = callback # The command callback
63 self.add_parser_args = add_parser_args # Parser arguments to add
64 self.parent = None # The parent Command
65 self.children = [] # The children Command entries
66
67
68class OnosCli(Cmd):
69 "The main ONOS CLI class"
70
71 # Setup generic CLI fields
72 intro = "Welcome to the ONOS CLI. Type help or ? to list commands.\n"
73 prompt = "(onos) "
74 settable = Cmd.settable + "prompt CLI prompt"
75
76 # Setup ONOS-specific fields
77 onos_ip = "127.0.0.1"
78 settable = settable + "onos_ip ONOS IP address"
79 onos_port = 8080
80 settable = settable + "onos_port ONOS REST port number"
81 output_format = "json" # Valid values: json, text
82 settable = settable + "output_format The output format: `text` or `json`"
83
84 # Collection of commands sorted by the level in the CLI command hierarchy
85 commands = []
86 # Commands, parsers and subparsers keyed by the command name
87 commands_dict = {}
88 parsers_dict = {}
89 subparsers_dict = {}
90
91 def __init__(self):
92 Cmd.__init__(self)
93
94 #
95 # An array of the ONOS-specific CLI commands.
96 # Each entry is a Command instance, and must have at least
97 # two arguments:
98 # * Command name as typed on the CLI. E.g.:
99 # "show intent"
100 # * Command help description. E.g.:
101 # "Show intents"
102 #
103 # Executable commands should have a third Command argument, which is
104 # the name of the method to be called when the command is executed.
105 # The method will be called with the (argparse) parsed arguments
106 # for that command.
107 #
108 # If an executable command takes arguments, those should be described
109 # in the Command's fourth argument. It is a list of pairs:
110 # [
111 # ("--argument-name1", dict(...)),
112 # ("--argument-name2", dict(...)),
113 # ...
114 # ]
115 # where the first entry in the pair is the argument name, and the
116 # second entry in the pair is a dictionary with argparse-specific
117 # description of the argument.
118 #
119 init_commands = [
120 Command("delete", "Delete command"),
121 #
122 Command("delete intent",
123 """Delete high-level intents
124 Usage:
125 delete intent --intent-id INTENT_ID Delete a high-level intent
126 delete intent --all Delete all high-level intents
127 Arguments:
128 --intent-id INTENT_ID The Intent ID (an integer)
129 --all Delete all high-level intents""",
130 self.delete_intent,
131 [
132 ("--intent-id", dict(required=False, type=int)),
133 ("--all", dict(required=False, action='store_true'))
134 ]
135 ),
136 #
137 Command("set", "Set command"),
138 #
139 Command("set intent",
140 """Set a high-level intent
141 Usage:
142 set intent <ARGS>
143 Arguments:
144 --intent-id INTENT_ID The Intent ID (an integer) (REQUIRED)
145 --src-dpid SRC_DPID Source Switch DPID (REQUIRED)
146 --src-port SRC_PORT Source Switch Port (REQUIRED)
147 --dst-dpid DST_DPID Destination Switch DPID (REQUIRED)
148 --dst-port DST_PORT Destination Switch Port (REQUIRED)
149 --match-src-mac MATCH_SRC_MAC Matching Source MAC Address (REQUIRED)
150 --match-dst-mac MATCH_DST_MAC Matching Destination MAC Address (REQUIRED)""",
151 self.set_intent,
152 [
153 ("--intent-id", dict(required=True, type=int)),
154 ("--src-dpid", dict(required=True)),
155 ("--src-port", dict(required=True, type=int)),
156 ("--dst-dpid", dict(required=True)),
157 ("--dst-port", dict(required=True, type=int)),
158 ("--match-src-mac", dict(required=True)),
159 ("--match-dst-mac", dict(required=True))
160 ]),
161 #
162 Command("show", "Show command"),
163 #
164 Command("show device", "Show devices"),
165 #
166 Command("show device all", "Show all devices", self.show_device_all),
167 #
168 Command("show intent", "Show intents"),
169 #
170 Command("show intent high",
171 """Show all high-level intents
172 show intent high --intent-id INTENT_ID Show a high-level intent""",
173 self.show_intent_high,
174 [
175 ("--intent-id", dict(required=False, type=int))
176 ]
177 ),
178 #
179 Command("show intent low",
180 """Show all low-level intents
181 show intent low --intent-id INTENT_ID Show a low-level intent""",
182 self.show_intent_low,
183 [
184 ("--intent-id", dict(required=False))
185 ]
186 ),
187 #
188 Command("show link", "Show links"),
189 #
190 Command("show link all", "Show all links", self.show_link_all),
191 #
192 Command("show path", "Show a path"),
193 #
194 Command("show path shortest",
195 """Show a shortest path
196 Usage:
197 show path shortest --src-dpid SRC_DPID --dst-dpid DST_DPID
198 Arguments:
199 --src-dpid SRC_DPID Source Switch DPID
200 --dst-dpid DST_DPID Destination Switch DPID""",
201 self.show_path_shortest,
202 [
203 ("--src-dpid", dict(required=True)),
204 ("--dst-dpid", dict(required=True))
205 ]),
206 #
207 Command("show switch", "Show switches"),
208 #
209 Command("show switch all", "Show all switches", self.show_switch_all)
210 ]
211
212 # Sort the commands by the level in the CLI command hierarchy
213 self.commands = sorted(init_commands, key = lambda c: len(c.name.split()))
214
215 # Create a dictionary with all commands: name -> Command
216 for c in self.commands:
217 self.commands_dict[c.name] = c
218
219 # Create a tree with all commands
220 for c in self.commands:
221 if c.parent_name:
222 pc = self.commands_dict[c.parent_name]
223 pc.children.append(c)
224 c.parent = pc
225
226 # Create the parsers and the sub-parsers
227 for c in self.commands:
228 # Add a parser
229 parser = None
230 if c.parent is None:
231 # Add a top-level parser
232 parser = argparse.ArgumentParser(description=c.help,
233 prog=c.name,
234 add_help=False)
235 else:
236 # Add a parser from the parent's subparser
237 parent_subparser = self.subparsers_dict[c.parent_name]
238 parser = parent_subparser.add_parser(c.last_subname,
239 help=c.help,
240 add_help=False)
241 self.parsers_dict[c.name] = parser
242 # Add a sub-parser
243 if c.children:
244 subparser = parser.add_subparsers(help=c.help)
245 self.subparsers_dict[c.name] = subparser
246 # Setup the callback
247 if c.callback is not None:
248 parser.set_defaults(func=c.callback)
249 # Init the argument parser
250 if c.add_parser_args is not None:
251 for a in c.add_parser_args:
252 (p1, p2) = a
253 parser.add_argument(p1, **p2)
254
255 def delete_intent(self, args):
256 "CLI command callback: delete intent"
257
258 url = ""
259 if args.all:
260 # Delete all intents
261 url = "http://%s:%s/wm/onos/intent/high" % (self.onos_ip, self.onos_port)
262 else:
263 if args.intent_id is None:
264 print "*** Unknown syntax:"
265 self.help_delete()
266 return;
267 # Delete an intent
268 url = "http://%s:%s/wm/onos/intent/high/%s" % (self.onos_ip, self.onos_port, args.intent_id)
269
270 result = delete_json(url)
271 # NOTE: No need to print the response
272 # if len(result) != 0:
273 # self.print_json_result(result)
274
275 def set_intent(self, args):
276 "CLI command callback: set intent"
277
278 intents = []
279 oper = {}
280 # Create the POST payload
281 oper['intentId'] = args.intent_id
282 oper['intentType'] = 'CONSTRAINED_SHORTEST_PATH' # XXX: Hardcoded
283 oper['staticPath'] = False # XXX: Hardcoded
284 oper['srcSwitchDpid'] = args.src_dpid
285 oper['srcSwitchPort'] = args.src_port
286 oper['dstSwitchDpid'] = args.dst_dpid
287 oper['dstSwitchPort'] = args.dst_port
288 oper['matchSrcMac'] = args.match_src_mac
289 oper['matchDstMac'] = args.match_dst_mac
290 intents.append(oper)
291
292 url = "http://%s:%s/wm/onos/intent/high" % (self.onos_ip, self.onos_port)
293 result = post_json(url, intents)
294 # NOTE: No need to print the response
295 # if len(result) != 0:
296 # self.print_json_result(result)
297
298 def show_device_all(self, args):
299 "CLI command callback: show device all"
300
301 url = "http://%s:%s/wm/onos/topology/devices/json" % (self.onos_ip, self.onos_port)
302 result = get_json(url)
303 self.print_json_result(result)
304
305 def show_intent_high(self, args):
306 "CLI command callback: show intent high"
307
308 if args.intent_id is None:
309 # Show all intents
310 url = "http://%s:%s/wm/onos/intent/high" % (self.onos_ip, self.onos_port)
311 else:
312 # Show a single intent
313 url = "http://%s:%s/wm/onos/intent/high/%s" % (self.onos_ip, self.onos_port, args.intent_id)
314
315 result = get_json(url)
316 self.print_json_result(result)
317
318 def show_intent_low(self, args):
319 "CLI command callback: show intent low"
320
321 if args.intent_id is None:
322 # Show all intents
323 url = "http://%s:%s/wm/onos/intent/low" % (self.onos_ip, self.onos_port)
324 else:
325 # Show a single intent
326 url = "http://%s:%s/wm/onos/intent/low/%s" % (self.onos_ip, self.onos_port, args.intent_id)
327
328 result = get_json(url)
329 self.print_json_result(result)
330
331 def show_link_all(self, args):
332 "CLI command callback: show link all"
333
334 url = "http://%s:%s/wm/onos/topology/links/json" % (self.onos_ip, self.onos_port)
335 result = get_json(url)
336 #
337 if (self.output_format == "json"):
338 self.print_json_result(result)
339 else:
340 # NOTE: The code below is for demo purpose only how to
341 # decode and print the links in text format. It will be
342 # reimplemented in the future.
343 links = result
344 print "# src_dpid src_port -> dst_dpid dst_port"
345 for v in sorted(links, key=lambda x: x['src-switch']):
346 if v.has_key('dst-switch'):
347 dst_dpid = str(v['dst-switch'])
348 if v.has_key('src-switch'):
349 src_dpid = str(v['src-switch'])
350 if v.has_key('src-port'):
351 src_port = str(v['src-port'])
352 if v.has_key('dst-port'):
353 dst_port = str(v['dst-port'])
354 self.print_result("%s %s -> %s %s" % (src_dpid, src_port, dst_dpid, dst_port))
355
356 def show_path_shortest(self, args):
357 "CLI command callback: show path shortest"
358
359 url = "http://%s:%s/wm/onos/intent/path/switch/%s/shortest-path/%s" % (self.onos_ip, self.onos_port, args.src_dpid, args.dst_dpid)
360 result = get_json(url)
361 #
362 self.print_json_result(result)
363
364 def show_switch_all(self, args):
365 "CLI command callback: show switch all"
366
367 url = "http://%s:%s/wm/onos/topology/switches/json" % (self.onos_ip, self.onos_port)
368 result = get_json(url)
369 #
370 self.print_json_result(result)
371
372 #
373 # Implement "delete" top-level command
374 #
375 def do_delete(self, arg):
376 "Top-level 'delete' command"
377 self.impl_do_command('delete', arg)
378 def complete_delete(self, text, line, begidx, endidx):
379 "Completion of top-level 'delete' command"
380 return self.impl_complete_command('delete', text, line, begidx, endidx)
381 def help_delete(self):
382 "Help for top-level 'delete' command"
383 self.impl_help_command('delete')
384
385 #
386 # Implement "set" top-level command
387 #
388 def do_set(self, arg):
389 "Top-level 'set' command"
390 self.impl_do_command('set', arg)
391 def complete_set(self, text, line, begidx, endidx):
392 "Completion of top-level 'set' command"
393 return self.impl_complete_command('set', text, line, begidx, endidx)
394 def help_set(self):
395 "Help for top-level 'set' command"
396 self.impl_help_command('set')
397
398 #
399 # Implement "show" top-level command
400 #
401 def do_show(self, arg):
402 "Top-level 'show' command"
403 self.impl_do_command('show', arg)
404 def complete_show(self, text, line, begidx, endidx):
405 "Completion of top-level 'show' command"
406 return self.impl_complete_command('show', text, line, begidx, endidx)
407 def help_show(self):
408 "Help for top-level 'show' command"
409 self.impl_help_command('show')
410
411 #
412 # Implement the "do_something" top-level command execution
413 #
414 def impl_do_command(self, root_name, arg):
415 "Implementation of top-level 'do_something' command execution"
416 parser = self.parsers_dict[root_name]
417 parsed_args = parser.parse_args(arg.split())
418 parsed_args.func(parsed_args)
419
420 #
421 # Implement the "complete_something" top-level command completion
422 #
423 def impl_complete_command(self, root_name, text, line, begidx, endidx):
424 "Implementation of top-level 'complete_something' command completion"
425 root_command = self.commands_dict[root_name]
426 subtree_commands = self.collect_subtree_commands(root_command)
427
428 #
429 # Loop through the commands and add their portion
430 # of the sub-name to the list of completions.
431 #
432 # NOTE: We add a command only if it has a callback.
433 #
434 completions = []
435 for c in subtree_commands:
436 if c.callback is None:
437 continue
438 name = c.split_name[len(root_command.split_name):]
439 completions.append(' '.join(name))
440
441 mline = line.partition(" ")[2]
442 offs = len(mline) - len(text)
443 return [s[offs:] for s in completions if s.startswith(mline)]
444
445 #
446 # Implement the "help_something" top-level command help
447 #
448 def impl_help_command(self, root_name):
449 "Implementation of top-level 'help_something' command help"
450 root_command = self.commands_dict[root_name]
451 subtree_commands = self.collect_subtree_commands(root_command)
452
453 #
454 # Loop through the commands and print the help for each command.
455 # NOTE: We add a command only if it has a callback.
456 #
457 print "Help for the `%s` command:" % (root_name)
458 for c in subtree_commands:
459 if c.callback is None:
460 continue
461 print " {0:30}{1:30}".format(c.name, c.help)
462 # if c.init_arg_parser is not None:
463 # parser = self.parsers_dict[c.name]
464 # parser.print_help()
465
466 #
467 # Traverse (breadth-first) a subtree and return all nodes except the
468 # root node.
469 #
470 def collect_subtree_commands(self, root_command):
471 """Collect a subtree of commands.
472 Traverses (breadth-first) a subtree of commands and returns
473 all nodes except the root node."""
474
475 commands = []
476 subtree_commands = []
477 commands.append(root_command)
478 # Use breadth-first to traverse the subtree
479 while commands:
480 pc = commands.pop(0)
481 for c in pc.children:
482 commands.append(c)
483 subtree_commands.append(c)
484 return subtree_commands
485
486 def log_debug(self, msg):
487 """Log debug information.
488 msg: the message to log
489 Use the following CLI commands to enable/disable debugging:
490 paramset debug true
491 paramset debug false
492 """
493 if self.debug:
494 print "%s" % (msg)
495
496 def print_json_result(self, json_result):
497 """Print JSON result."""
498 if len(json_result) == 0:
499 return
500 result = json.dumps(json_result, indent=4)
501 self.print_result(result)
502
503 def print_result(self, result):
504 """Print parsed result."""
505 print "%s" % (result)
506
507 #
508 # Implementation of the "paramshow" CLI command.
509 #
510 # NOTE: The do_paramshow implementation below is copied from
511 # the cmd2.do_show() implementation
512 #
513 @options([make_option('-l', '--long', action="store_true",
514 help="describe function of parameter")])
515 def do_paramshow(self, arg, opts):
516 '''Shows value of a parameter.'''
517 param = arg.strip().lower()
518 result = {}
519 maxlen = 0
520 for p in self.settable:
521 if (not param) or p.startswith(param):
522 result[p] = '%s: %s' % (p, str(getattr(self, p)))
523 maxlen = max(maxlen, len(result[p]))
524 if result:
525 for p in sorted(result):
526 if opts.long:
527 self.poutput('%s # %s' % (result[p].ljust(maxlen), self.settable[p]))
528 else:
529 self.poutput(result[p])
530 else:
531 raise NotImplementedError("Parameter '%s' not supported (type 'show' for list of parameters)." % param)
532
533 #
534 # Implementation of the "paramset" CLI command.
535 #
536 #
537 # NOTE: The do_paramset implementation below is copied from
538 # the cmd2.do_set() implementation (with minor modifications).
539 #
540 def do_paramset(self, arg):
541 '''
542 Sets a cmd2 parameter. Accepts abbreviated parameter names so long
543 as there is no ambiguity. Call without arguments for a list of
544 settable parameters with their values.'''
545
546 class NotSettableError(Exception):
547 pass
548
549 try:
550 statement, paramName, val = arg.parsed.raw.split(None, 2)
551 val = val.strip()
552 paramName = paramName.strip().lower()
553 if paramName not in self.settable:
554 hits = [p for p in self.settable if p.startswith(paramName)]
555 if len(hits) == 1:
556 paramName = hits[0]
557 else:
558 return self.do_paramshow(paramName)
559 currentVal = getattr(self, paramName)
560 if (val[0] == val[-1]) and val[0] in ("'", '"'):
561 val = val[1:-1]
562 else:
563 val = cmd2.cast(currentVal, val)
564 setattr(self, paramName, val)
565 self.stdout.write('%s - was: %s\nnow: %s\n' % (paramName, currentVal, val))
566 if currentVal != val:
567 try:
568 onchange_hook = getattr(self, '_onchange_%s' % paramName)
569 onchange_hook(old=currentVal, new=val)
570 except AttributeError:
571 pass
572 except (ValueError, AttributeError, NotSettableError) as exc:
573 self.do_paramshow(arg)
574
575
576def get_json(url):
577 """Make a REST GET call and return the JSON result
578 url: the URL to call"""
579
580 parsed_result = ""
581 try:
582 response = urllib2.urlopen(url)
583 result = response.read()
584 response.close()
585 parsed_result = json.loads(result)
586 except HTTPError as exc:
587 print "ERROR:"
588 print " REST GET URL: %s" % url
589 # NOTE: exc.fp contains the object with the response payload
590 error_payload = json.loads(exc.fp.read())
591 print " REST Error Code: %s" % (error_payload['code'])
592 print " REST Error Summary: %s" % (error_payload['summary'])
593 print " REST Error Description: %s" % (error_payload['formattedDescription'])
594 print " HTTP Error Code: %s" % exc.code
595 print " HTTP Error Reason: %s" % exc.reason
596 except URLError as exc:
597 print "ERROR:"
598 print " REST GET URL: %s" % url
599 print " URL Error Reason: %s" % exc.reason
600 return parsed_result
601
602def post_json(url, data):
603 """Make a REST POST call and return the JSON result
604 url: the URL to call
605 data: the data to POST"""
606
607 parsed_result = ""
608 data_json = json.dumps(data)
609 try:
610 request = urllib2.Request(url, data_json)
611 request.add_header("Content-Type", "application/json")
612 response = urllib2.urlopen(request)
613 result = response.read()
614 response.close()
615 if len(result) != 0:
616 parsed_result = json.loads(result)
617 except HTTPError as exc:
618 print "ERROR:"
619 print " REST POST URL: %s" % url
620 # NOTE: exc.fp contains the object with the response payload
621 error_payload = json.loads(exc.fp.read())
622 print " REST Error Code: %s" % (error_payload['code'])
623 print " REST Error Summary: %s" % (error_payload['summary'])
624 print " REST Error Description: %s" % (error_payload['formattedDescription'])
625 print " HTTP Error Code: %s" % exc.code
626 print " HTTP Error Reason: %s" % exc.reason
627 except URLError as exc:
628 print "ERROR:"
629 print " REST POST URL: %s" % url
630 print " URL Error Reason: %s" % exc.reason
631 return parsed_result
632
633def delete_json(url):
634 """Make a REST DELETE call and return the JSON result
635 url: the URL to call"""
636
637 parsed_result = ""
638 try:
639 request = urllib2.Request(url)
640 request.get_method = lambda: 'DELETE'
641 response = urllib2.urlopen(request)
642 result = response.read()
643 response.close()
644 if len(result) != 0:
645 parsed_result = json.loads(result)
646 except HTTPError as exc:
647 print "ERROR:"
648 print " REST DELETE URL: %s" % url
649 # NOTE: exc.fp contains the object with the response payload
650 error_payload = json.loads(exc.fp.read())
651 print " REST Error Code: %s" % (error_payload['code'])
652 print " REST Error Summary: %s" % (error_payload['summary'])
653 print " REST Error Description: %s" % (error_payload['formattedDescription'])
654 print " HTTP Error Code: %s" % exc.code
655 print " HTTP Error Reason: %s" % exc.reason
656 except URLError as exc:
657 print "ERROR:"
658 print " REST DELETE URL: %s" % url
659 print " URL Error Reason: %s" % exc.reason
660 return parsed_result
661
662if __name__ == '__main__':
663 onosCli = OnosCli()
664
665 # Setup the parser
666 parser = argparse.ArgumentParser()
667 parser.add_argument('-c', '--command', nargs=argparse.REMAINDER,
668 help="Run arguments to the end of the line as a CLI command")
669 parser.add_argument('--onos-ip',
670 help="Set the ONOS IP address (for REST calls)")
671 parser.add_argument('--onos-port',
672 help="Set the ONOS port number (for REST calls)")
673 parser.add_argument('-t', '--test', nargs='+',
674 help="Test against transcript(s) in FILE (wildcards OK)")
675
676 # Parse the arguments
677 parsed_args = parser.parse_args()
678 if parsed_args.onos_ip:
679 onosCli.onos_ip = parsed_args.onos_ip
680 if parsed_args.onos_port:
681 onosCli.onos_port = parsed_args.onos_port
682 #
683 # NOTE: We have to reset the command-line options so the Cmd2 parser
684 # doesn't process them again.
685 #
686 sys.argv = [sys.argv[0]]
687
688 # Run the CLI as appropriate
689 if parsed_args.test:
690 # Run CLI Transcript Tests
691 onosCli.runTranscriptTests(parsed_args.test)
692 elif parsed_args.command:
693 # Run arguments as a CLI command
694 command_line = ' '.join(parsed_args.command)
695 onosCli.onecmd(command_line)
696 else:
697 # Run interactive CLI
698 onosCli.cmdloop()