| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 1 | #!/usr/bin/env python |
| 2 | # -*- coding: utf-8 -*- |
| 3 | # PYTHON_ARGCOMPLETE_OK |
| 4 | |
| 5 | ## |
| 6 | # Copyright 2015 Telefónica Investigación y Desarrollo, S.A.U. |
| 7 | # This file is part of openmano |
| 8 | # All Rights Reserved. |
| 9 | # |
| 10 | # Licensed under the Apache License, Version 2.0 (the "License"); you may |
| 11 | # not use this file except in compliance with the License. You may obtain |
| 12 | # a copy of the License at |
| 13 | # |
| 14 | # http://www.apache.org/licenses/LICENSE-2.0 |
| 15 | # |
| 16 | # Unless required by applicable law or agreed to in writing, software |
| 17 | # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| 18 | # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| 19 | # License for the specific language governing permissions and limitations |
| 20 | # under the License. |
| 21 | # |
| 22 | # For those usages not covered by the Apache License, Version 2.0 please |
| 23 | # contact with: nfvlabs@tid.es |
| 24 | ## |
| 25 | |
| 26 | ''' |
| 27 | This program is useful to interact directly with Openflow Controllers |
| 28 | to clear rules, add and delete rules, list rules, etc. |
| 29 | ''' |
| 30 | |
| 31 | __author__="Gerardo Garcia, Alfonso Tierno, Pablo Montes" |
| 32 | __date__ ="$09-oct-2014 09:09:48$" |
| 33 | |
| 34 | #import time |
| 35 | import os |
| 36 | import sys |
| 37 | import argparse |
| 38 | import argcomplete |
| 39 | import imp |
| 40 | import yaml |
| 41 | import requests |
| 42 | import logging |
| mirabal | 9f65710 | 2017-04-10 20:05:40 +0200 | [diff] [blame] | 43 | import osm_openvim.openflow_conn as openflow_conn |
| 44 | from osm_openvim.openflow_thread import change_db2of, FlowBadFormat |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 45 | |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 46 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 47 | def of_switches(args): |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 48 | try: |
| 49 | c = ofconnector.get_of_switches() |
| 50 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 51 | for s in c: |
| 52 | print " %s %s" % (s[0], s[1]) |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 53 | return 0 |
| 54 | except openflow_conn.OpenflowconnException as e: |
| 55 | print ("OF get switch error {}".format(str(e))) |
| 56 | return -1 |
| 57 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 58 | |
| 59 | def of_list(args): |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 60 | try: |
| 61 | c = ofconnector.get_of_rules(not args.no_translate) |
| 62 | |
| 63 | if args.verbose > 0: |
| 64 | print yaml.safe_dump(c, indent=4, default_flow_style=False) |
| 65 | return 0 |
| 66 | |
| 67 | print " switch priority name ingress_port " \ |
| 68 | "dst_mac vlan_id actions" |
| 69 | for name, rule in c.iteritems(): |
| 70 | action_list = [] |
| 71 | for action in rule["actions"]: |
| 72 | action_list.append(action[0] + "=" + str(action[1])) |
| 73 | if "vlan_id" in rule: |
| 74 | vlan = str(rule["vlan_id"]) |
| 75 | else: |
| 76 | vlan = "any" |
| 77 | print "%s %s %s %s %s %s %s" % \ |
| 78 | (rule["switch"], str(rule["priority"]).ljust(6), name.ljust(40), rule["ingress_port"].ljust(8), |
| 79 | rule.get("dst_mac", "any").ljust(18), vlan.ljust(4), ",".join(action_list)) |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 80 | return 0 |
| 81 | |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 82 | except openflow_conn.OpenflowconnException as e: |
| 83 | print("OF get list error {}".format(str(e))) |
| 84 | return -1 |
| 85 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 86 | |
| 87 | def of_clear(args): |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 88 | try: |
| 89 | if not args.force: |
| 90 | r = raw_input("Clear all Openflow rules (y/N)? ") |
| 91 | if not (len(r) > 0 and r[0].lower() == "y"): |
| 92 | return 0 |
| 93 | c = ofconnector.clear_all_flows() |
| 94 | return 0 |
| 95 | except openflow_conn.OpenflowconnException as e: |
| 96 | print ("OF error {}".format(str(e))) |
| 97 | return -1 |
| 98 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 99 | |
| 100 | def of_port_list(args): |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 101 | try: |
| 102 | c = ofconnector.obtain_port_correspondence() |
| 103 | yaml.safe_dump({"ports": c}, sys.stdout, indent=2, default_flow_style=False) |
| 104 | # def of_dump(args): |
| 105 | # args.verbose = 3 |
| 106 | # args.no_translate=False |
| 107 | # of_list(args) |
| 108 | return len(c) |
| 109 | except openflow_conn.OpenflowconnException as e: |
| 110 | print("OF error {}".format(str(e))) |
| 111 | return -1 |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 112 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 113 | |
| 114 | def of_reinstall(args): |
| 115 | try: |
| 116 | URLrequest = "http://%s:%s/openvim/networks/all/openflow" %(vim_host, vim_admin_port) |
| 117 | print URLrequest |
| 118 | openvim_response = requests.put(URLrequest) |
| 119 | print openvim_response.text |
| 120 | return 0 |
| 121 | except requests.exceptions.RequestException as e: |
| 122 | print " Exception GET at '"+URLrequest+"' " + str(e) |
| 123 | return -1 |
| 124 | |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 125 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 126 | def of_install(args): |
| 127 | line_number=1 |
| 128 | try: |
| 129 | f = open(args.file, "r") |
| 130 | text = f.read() |
| 131 | f.close() |
| 132 | lines=text.split("\n") |
| 133 | heads=lines[0].split() |
| 134 | |
| 135 | for line in lines[1:]: |
| 136 | line_number += 1 |
| 137 | rule={} |
| 138 | items= line.split() |
| 139 | if len(items)==0 or items[0][0]=="#": #empty line or commented |
| 140 | continue |
| 141 | for i in range(0,len(items)): |
| 142 | rule[ heads[i] ] = items[i] |
| 143 | if rule["vlan_id"] == "any": |
| 144 | del rule["vlan_id"] |
| 145 | if rule["dst_mac"] == "any": |
| 146 | del rule["dst_mac"] |
| 147 | if 'priority' in rule and (rule['priority']==None or rule['priority']=="None" ): |
| 148 | del rule['priority'] |
| 149 | try: |
| 150 | change_db2of(rule) |
| 151 | except FlowBadFormat as e: |
| 152 | print "Format error at line %d: %s" % (line_number, str(e)) |
| 153 | continue |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 154 | try: |
| 155 | ofconnector.new_flow(rule) |
| 156 | error = "OK" |
| 157 | except openflow_conn.OpenflowconnException as e: |
| 158 | error = "ERROR: " + str(e) |
| 159 | print "%s %s %s input=%s dst_mac=%s vlan_id=%s %s" % (rule["switch"], |
| 160 | str(rule.get("priority")).ljust(6), |
| 161 | rule["name"].ljust(20), |
| 162 | rule["ingress_port"].ljust(3), |
| 163 | rule.get("dst_mac", "any").ljust(18), |
| 164 | rule.get("vlan_id", "any").ljust(4), error) |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 165 | return 0 |
| 166 | except IOError as e: |
| 167 | print " Error opening file '" + args.file + "': " + e.args[1] |
| 168 | return -1 |
| 169 | except yaml.YAMLError as exc: |
| 170 | error_pos = "" |
| 171 | if hasattr(exc, 'problem_mark'): |
| 172 | mark = exc.problem_mark |
| 173 | error_pos = " at position: (%s:%s)" % (mark.line+1, mark.column+1) |
| 174 | print " Error yaml/json format error at " + error_pos |
| 175 | return -1 |
| 176 | |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 177 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 178 | def of_add(args): |
| 179 | if args.act==None and args.actions==None: |
| 180 | print "openflow add: error: one of the arguments --actions or [--setvlan,--stripvlan],--out is required" |
| 181 | return -1 |
| 182 | elif args.act!=None and args.actions!=None: |
| 183 | print "openflow add: error: Use either --actions option or [--setvlan,--stripvlan],--out options; but not both" |
| 184 | return -1 |
| 185 | |
| 186 | rule={"name":args.name, "priority":args.priority, |
| 187 | "ingress_port": args.inport |
| 188 | } |
| 189 | if args.matchvlan: |
| 190 | rule["vlan_id"] = args.matchvlan |
| 191 | if args.matchmac: |
| 192 | rule["dst_mac"] = args.matchmac |
| 193 | |
| 194 | if args.actions: |
| 195 | rule["actions"] = args.actions |
| 196 | try: |
| 197 | change_db2of(rule) |
| 198 | except FlowBadFormat as e: |
| 199 | print "Format error at --actions: '%s' Expected 'vlan=<None/vlan_id>,out=<egress_port>,...'" % str(e) |
| 200 | return -1 |
| 201 | elif args.act: |
| 202 | rule["actions"]=[] |
| 203 | error_msj = "openflow add: error: --setvlan,--stripvlan options must be followed by an --out option" |
| 204 | previous_option_vlan=False # indicates if the previous option was a set or strip vlan to avoid consecutive ones and to force an out options afterwards |
| 205 | for action in args.act: |
| 206 | if action==None or type(action)==int: |
| 207 | if previous_option_vlan: #consecutive vlan options |
| 208 | print error_msj |
| 209 | return -1 |
| 210 | previous_option_vlan=True |
| 211 | rule["actions"].append( ("vlan", action) ) |
| 212 | else: |
| 213 | previous_option_vlan=False |
| 214 | rule["actions"].append( ("out", action) ) |
| 215 | if previous_option_vlan: |
| 216 | print error_msj |
| 217 | return -1 |
| 218 | #print rule |
| 219 | #return |
| 220 | |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 221 | try: |
| 222 | c = ofconnector.new_flow(rule) |
| 223 | if args.print_id: |
| 224 | print rule["name"] |
| 225 | return 0 |
| 226 | |
| 227 | except openflow_conn.OpenflowconnException as e: |
| 228 | print("OF error {}".format(str(e))) |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 229 | return -1 |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 230 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 231 | |
| 232 | def of_delete(args): |
| 233 | if not args.force: |
| 234 | r = raw_input("Clear rule %s (y/N)? " %(args.name)) |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 235 | if not (len(r) >0 and r[0].lower() == "y"): |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 236 | return 0 |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 237 | try: |
| 238 | ofconnector.del_flow(args.name) |
| 239 | return 0 |
| 240 | except openflow_conn.OpenflowconnException as e: |
| 241 | print("OF error {}".format(str(e))) |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 242 | return -1 |
| mirabal | 6c60065 | 2017-03-16 17:22:57 +0100 | [diff] [blame] | 243 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 244 | |
| 245 | def config(args): |
| 246 | print "OPENVIM_HOST: %s" %(vim_host) |
| 247 | print "OPENVIM_ADMIN_PORT: %s" %(vim_admin_port) |
| 248 | print "OF_CONTROLLER_TYPE: %s" %(of_controller_type) |
| 249 | if of_controller_module or (of_controller_type!="floodlight" and of_controller_type!="opendaylight"): |
| 250 | print "OF_CONTROLLER_MODULE: %s" %(of_controller_module) |
| 251 | print "OF_CONTROLLER_USER: %s" %(of_controller_user) |
| 252 | print "OF_CONTROLLER_PASSWORD: %s" %(of_controller_password) |
| 253 | #print "OF_CONTROLLER_VERSION: %s" %(of_controller_version) |
| 254 | print "OF_CONTROLLER_IP: %s" %(of_controller_ip) |
| 255 | print "OF_CONTROLLER_PORT: %s" %(of_controller_port) |
| 256 | print "OF_CONTROLLER_DPID: %s" %(of_controller_dpid) |
| 257 | return |
| 258 | |
| 259 | version="0.8" |
| 260 | global vim_host |
| 261 | global vim_admin_port |
| 262 | global of_controller_type |
| 263 | global of_controller_user |
| 264 | global of_controller_password |
| 265 | global of_controller_ip |
| 266 | global of_controller_port |
| 267 | global of_controller_dpid |
| 268 | global of_controller_module |
| 269 | global ofconnector |
| 270 | |
| 271 | if __name__=="__main__": |
| 272 | #print "test_ofconnector version", version, "Jul 2015" |
| 273 | #print "(c) Copyright Telefonica" |
| 274 | |
| 275 | vim_host = os.getenv('OPENVIM_HOST',"localhost") |
| 276 | vim_admin_port = os.getenv('OPENVIM_ADMIN_PORT',"8085") |
| 277 | of_controller_type = os.getenv('OF_CONTROLLER_TYPE',"floodlight") |
| 278 | of_controller_user = os.getenv('OF_CONTROLLER_USER',None) |
| 279 | of_controller_password = os.getenv('OF_CONTROLLER_PASSWORD',None) |
| 280 | #of_controller_version = os.getenv('OF_CONTROLLER_VERSION',"0.90") |
| 281 | of_controller_ip = os.getenv('OF_CONTROLLER_IP',"localhost") |
| 282 | of_controller_port = os.getenv('OF_CONTROLLER_PORT',"7070") |
| 283 | of_controller_dpid = os.getenv('OF_CONTROLLER_DPID','00:01:02:03:e4:05:e6:07') |
| 284 | of_controller_module = os.getenv('OF_CONTROLLER_MODULE',None) |
| 285 | |
| 286 | main_parser = argparse.ArgumentParser(description='User program to interact with Openflow controller') |
| 287 | main_parser.add_argument('--version', action='version', version='%(prog)s ' + version ) |
| 288 | |
| 289 | #main_parser = argparse.ArgumentParser() |
| 290 | subparsers = main_parser.add_subparsers(help='commands') |
| 291 | |
| 292 | config_parser = subparsers.add_parser('config', help="prints configuration values") |
| 293 | config_parser.set_defaults(func=config) |
| 294 | |
| 295 | add_parser = subparsers.add_parser('add', help="adds an openflow rule") |
| 296 | add_parser.add_argument('--verbose', '-v', action='count') |
| 297 | add_parser.add_argument("name", action="store", help="name of the rule") |
| 298 | add_parser.add_argument("--inport", required=True, action="store", type=str, help="match rule: ingress-port") |
| 299 | add_parser.add_argument("--actions", action="store", type=str, help="action with the format: vlan=<None/vlan-id>,out=<egress-port>,...") |
| 300 | add_parser.add_argument("--priority", action="store", type=int, help="rule priority") |
| 301 | add_parser.add_argument("--matchmac", action="store", help="match rule: mac address") |
| 302 | add_parser.add_argument("--matchvlan", action="store", type=int, help="match rule: vlan id") |
| 303 | add_parser.add_argument("--stripvlan", action="append_const", dest="act", const=None, help="alternative to --actions. Use before --out to strip vlan") |
| 304 | add_parser.add_argument("--setvlan", action="append", dest="act", type=int, help="alternative to --actions. Use before --out to set vlan") |
| 305 | add_parser.add_argument("--out", action="append", dest="act", type=str, help="alternative to --actions. out=<egress-port> can be used several times") |
| 306 | add_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| tierno | d03ce28 | 2016-12-02 14:40:59 +0100 | [diff] [blame] | 307 | add_parser.add_argument('--print-id', action='store_true', help="print the flow id after added") |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 308 | add_parser.set_defaults(func=of_add) |
| 309 | |
| 310 | delete_parser = subparsers.add_parser('delete', help="delete an openflow rule") |
| 311 | delete_parser.add_argument('--verbose', '-v', action='count') |
| 312 | delete_parser.add_argument("-f", "--force", action="store_true", help="force deletion without asking") |
| 313 | delete_parser.add_argument("name", action="store", help="name of the rule to be deleted") |
| 314 | delete_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 315 | delete_parser.set_defaults(func=of_delete) |
| 316 | |
| 317 | switches_parser = subparsers.add_parser('switches', help="list all switches controlled by the OFC") |
| 318 | switches_parser.add_argument('--verbose', '-v', action='count') |
| 319 | switches_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 320 | switches_parser.set_defaults(func=of_switches) |
| 321 | |
| 322 | list_parser = subparsers.add_parser('list', help="list openflow rules") |
| 323 | list_parser.add_argument('--verbose', '-v', action='count') |
| 324 | list_parser.add_argument("--no-translate", "-n", action="store_true", help="Skip translation from openflow index to switch port name") |
| 325 | list_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 326 | list_parser.set_defaults(func=of_list) |
| 327 | |
| 328 | #dump_parser = subparsers.add_parser('dump', help="dump openflow rules") |
| 329 | #dump_parser.set_defaults(func=of_dump) |
| 330 | |
| 331 | clear_parser = subparsers.add_parser('clear', help="clear all openflow rules") |
| 332 | clear_parser.add_argument("-f", "--force", action="store_true", help="forces deletion without asking") |
| 333 | clear_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 334 | clear_parser.set_defaults(func=of_clear) |
| 335 | |
| 336 | install_parser = subparsers.add_parser('install', help="install openflow rules from file") |
| 337 | install_parser.add_argument("file", action="store", help="file with rules generated using 'openflow list > rules.txt'") |
| 338 | install_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 339 | install_parser.set_defaults(func=of_install) |
| 340 | |
| 341 | reinstall_parser = subparsers.add_parser('reinstall', help="reinstall openflow rules from VIM rules") |
| 342 | reinstall_parser.set_defaults(func=of_reinstall) |
| 343 | reinstall_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 344 | |
| 345 | portlist_parser = subparsers.add_parser('port-list', help="list the physical to openflow port correspondence") |
| 346 | portlist_parser.set_defaults(func=of_port_list) |
| 347 | portlist_parser.add_argument('--debug', '-d', action='store_true', help="show debug information") |
| 348 | |
| 349 | argcomplete.autocomplete(main_parser) |
| 350 | |
| 351 | args = main_parser.parse_args() |
| 352 | module_info=None |
| 353 | try: |
| 354 | if args.func is not config: |
| 355 | params={ "of_ip": of_controller_ip, |
| 356 | "of_port": of_controller_port, |
| 357 | "of_dpid": of_controller_dpid, |
| 358 | "of_user": of_controller_user, |
| 359 | "of_password": of_controller_password, |
| 360 | } |
| 361 | if "debug" in args and args.debug: |
| 362 | streamformat = "%(asctime)s %(name)s %(levelname)s: %(message)s" |
| 363 | logging.basicConfig(format=streamformat, level= logging.DEBUG) |
| 364 | logger = logging.getLogger('vim') |
| 365 | logger.setLevel(logging.DEBUG) |
| 366 | params["of_debug"]="DEBUG" |
| 367 | else: |
| 368 | #logger = logging.getLogger('vim').addHandler(logging.NullHandler()) |
| 369 | #logger.setLevel(logging.CRITICAL) |
| 370 | params["of_debug"]="CRITICAL" |
| 371 | |
| 372 | if of_controller_type=='opendaylight': |
| 373 | module = "ODL" |
| 374 | elif of_controller_module != None: |
| 375 | module = of_controller_module |
| 376 | else: |
| 377 | module = of_controller_type |
| mirabal | 72fcda7 | 2017-05-09 11:01:06 +0200 | [diff] [blame] | 378 | |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 379 | try: |
| mirabal | 72fcda7 | 2017-05-09 11:01:06 +0200 | [diff] [blame] | 380 | pkg = __import__("osm_openvim." + module) |
| 381 | of_conn = getattr(pkg, module) |
| tierno | f7aa8c4 | 2016-09-06 16:43:04 +0200 | [diff] [blame] | 382 | ofconnector = of_conn.OF_conn(params) |
| 383 | except Exception as e: |
| 384 | print "Cannot open the Openflow controller '%s': %s" % (type(e).__name__, str(e)) |
| 385 | result = -1 |
| 386 | exit() |
| 387 | result = args.func(args) |
| 388 | if result == None: |
| 389 | result = 0 |
| 390 | |
| 391 | #for some reason it fails if call exit inside try instance. Need to call exit at the end !? |
| 392 | except (IOError, ImportError) as e: |
| 393 | print "Cannot open openflow controller module '%s'; %s: %s" % (module, type(e).__name__, str(e)) |
| 394 | result = -1 |
| 395 | #except Exception as e: |
| 396 | # print "Cannot open the Openflow controller '%s': %s" % (type(e).__name__, str(e)) |
| 397 | # result = -1 |
| 398 | except requests.exceptions.ConnectionError as e: |
| 399 | print "Cannot connect to server; %s: %s" % (type(e).__name__, str(e)) |
| 400 | result = -2 |
| 401 | except (KeyboardInterrupt): |
| 402 | print 'Exiting openVIM' |
| 403 | result = -3 |
| 404 | except (SystemExit): |
| 405 | result = -4 |
| 406 | |
| 407 | #close open file |
| 408 | if module_info and module_info[0]: |
| 409 | file.close(module_info[0]) |
| 410 | exit(result) |
| 411 | |
| 412 | |
| 413 | |