diff --git a/control_plane/controller/controller/__init__.py b/control_plane/controller/controller/__init__.py index 06b5ee4..b8f31b5 100644 --- a/control_plane/controller/controller/__init__.py +++ b/control_plane/controller/controller/__init__.py @@ -18,13 +18,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Controller +# Implementation of a SDN Controller # # @author Carmine Scarpitta # - -## -# -# This module contains all public symbols from the library -# +''' +This package contains an implementation of a SDN Controller. +''' diff --git a/control_plane/controller/controller/cli/__init__.py b/control_plane/controller/controller/cli/__init__.py index 3122ccd..3576324 100644 --- a/control_plane/controller/controller/cli/__init__.py +++ b/control_plane/controller/controller/cli/__init__.py @@ -18,7 +18,11 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Implementation of a CLI for the SRv6 Controller +# Implementation of a CLI for the SDN Controller # # @author Carmine Scarpitta # + +''' +This package provides an implementation of a CLI for the SDN Controller. +''' diff --git a/control_plane/controller/controller/cli/cli.py b/control_plane/controller/controller/cli/cli.py index 78753c5..be1fbd4 100644 --- a/control_plane/controller/controller/cli/cli.py +++ b/control_plane/controller/controller/cli/cli.py @@ -24,7 +24,9 @@ # -"""Implementation of a CLI for the SRv6 Controller""" +''' +Implementation of a CLI for the SRv6 Controller. +''' # This comment avoids the annoying warning "Too many lines in module" # of pylint. Maybe we should split this module in the future. @@ -52,64 +54,97 @@ from pkg_resources import resource_filename # Controller dependencies -from controller import arangodb_driver +from controller.db_utils.arangodb import arangodb_driver from controller import srv6_usid from controller.cli import srv6_cli, srv6pm_cli, topo_cli -from controller.init_db import init_srv6_usid_db +from controller.db_utils.arangodb.init_db import init_srv6_usid_db # Folder containing this script BASE_PATH = os.path.dirname(os.path.realpath(__file__)) # Logger reference +logging.basicConfig(level=logging.NOTSET) logger = logging.getLogger(__name__) # Configure logging level for urllib3 logging.getLogger('urllib3').setLevel(logging.WARNING) -# import utils -# import srv6_controller -# import ti_extraction -# import srv6_pm - # Default path to the .env file DEFAULT_ENV_FILE_PATH = resource_filename(__name__, '../config/controller.env') # Default value for debug mode DEFAULT_DEBUG = False +# Path where to save the history file +# We save it to in the same folder of this script +HISTORY_FILE_PATH = os.path.join(os.path.dirname(os.path.realpath(__file__)), + '.controller_history') +# Maximum length (in lines) of the history file +# If the history file exceeds this limit, it is truncated +HISTORY_FILE_LENGTH = 1000 # Set line delimiters, required for the auto-completion feature readline.set_completer_delims(' \t\n') class CustomCmd(Cmd): - """This class extends the python class Cmd and implements a handler - for CTRL+C and CTRL+D""" + ''' + This class extends the python class Cmd and implements a handler + for CTRL+C and CTRL+D + ''' - histfile = os.path.join(os.path.dirname(os.path.realpath(__file__)), - '.controller_history') - histfile_size = 1000 + # History file + histfile = os.path.join(HISTORY_FILE_PATH) + # History size + histfile_size = HISTORY_FILE_LENGTH def preloop(self): + ''' + Hook method offered by the Cmd library, executed once when cmdloop() + is called. + ''' + # If history persistency is enabled, readline library has been + # imported and the history file already exists... if readline and os.path.exists(self.histfile): + # ...read the history from the history file readline.read_history_file(self.histfile) def postloop(self): + ''' + Hook method offered by the Cmd library executed once when cmdloop() is + about to return. In this method we write the history to the history + file. + ''' + # If history persistency is enabled and readline library has been + # imported if readline: + # Set the number of lines to save in the history file + # If the history file exceeds the length limit, the history file + # is truncated readline.set_history_length(self.histfile_size) + # Write the history to the history file readline.write_history_file(self.histfile) def cmdloop(self, intro=None): - """ Command loop""" + ''' + CLI loop that accepts commands from the user and dispatch them to the + methods. + :param intro: Intro string to be issued before the first prompt. + :type intro: str, optional + ''' # pylint: disable=no-self-use - + # + # Loop implementing the interpreter for the commands while True: try: + # Start CLI loop that accepts commands from the user and + # dispatch them to the methods super(CustomCmd, self).cmdloop(intro=intro) break except KeyboardInterrupt: - print("^C") + # Handle CTRL+C + print('^C') except Exception as err: # pylint: disable=broad-except # When an exception is raised, we log the traceback # and keep the CLI open and ready to receive next comands @@ -120,78 +155,114 @@ def cmdloop(self, intro=None): print() def emptyline(self): - """Avoid to execute the last command if empty line is entered""" - + ''' + Method called when an empty line is entered in response to the prompt. + By default, it repeats the last nonempty command entered. To avoid the + execution of the last nonempty command, we override this method and + leave it blank. + ''' # pylint: disable=no-self-use def default(self, line): - """Default behavior""" - + ''' + Method called on an input line when the command prefix is not + recognized. + + :param line: The command. + :type line: str + :return: True if the command is "quit", False if the command is + unrecognized. + :rtype: bool + ''' + # If the user entered "x" or "q", exit if line in ['x', 'q']: return self.do_exit(line) - - print("Unrecognized command: {}".format(line)) + # Command unrecognized + print('Unrecognized command: {}'.format(line)) return False def do_exit(self, args): - """New line on exit""" - + ''' + Go new line on exit. + + :param args: The arguments passed to the command. + :type args: str + :return: True. + :rtype: bool + ''' # pylint: disable=unused-argument, no-self-use - - print() # New line + # + # Print new line and return + print() return True def help_exit(self): - """Help message for exit callback""" - + ''' + Help message for exit callback. + ''' # pylint: disable=no-self-use - + # + # Print the help message print('exit the application. Shorthand: x q Ctrl-D.') + # Register handler for the "exit" command do_EOF = do_exit + # Register help for the "exit" command help_EOF = help_exit class ControllerCLITopology(CustomCmd): - """Topology subsection""" + ''' + Topology subsection. + Subsection of the CLI containing several topology related functions. + ''' - prompt = "controller(topology)> " + # Prompt string + prompt = 'controller(topology)> ' def do_show_nodes(self, args): - """Show nodes""" - + ''' + Retrieve the nodes from ArangoDB and print the list of the available + nodes. + + :param args: The argument passed to this command. + :type args: str + :return: False in order to leave the CLI subsection open. + :rtype: bool + ''' # pylint: disable=no-self-use, unused-argument - - try: - # args = (srv6_cli - # .parse_arguments_print_nodes( - # prog='print_nodes', args=args.split(' '))) - pass - except SystemExit: - return False # This workaround avoid exit in case of errors - # pylint: disable=no-self-use # - # ArangoDB params + # Extract the ArangoDB params from the environment variables arango_url = os.getenv('ARANGO_URL') arango_user = os.getenv('ARANGO_USER') arango_password = os.getenv('ARANGO_PASSWORD') # Connect to ArangoDB client = arangodb_driver.connect_arango( url=arango_url) # TODO keep arango connection open - # Connect to the db + # Connect to the "srv6_usid" db database = arangodb_driver.connect_srv6_usid_db( client=client, username=arango_user, password=arango_password ) + # Extract the nodes configuration stored in the "srv6_usid" database nodes_dict = arangodb_driver.get_nodes_config(database) + # Print the available nodes srv6_cli.print_nodes(nodes_dict=nodes_dict) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_load_nodes_config(self, args): - '''Load node configuration to database''' + ''' + Read the nodes configuration from a YAML file and load it to a Arango + database. + + :param args: The argument passed to this command. + :type args: str + :return: False in order to leave the CLI subsection open. + :rtype: bool + ''' # pylint: disable=no-self-use # # Parse arguments @@ -201,34 +272,55 @@ def do_load_nodes_config(self, args): args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors - # ArangoDB params + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Extract the ArangoDB params from the environment variables arango_url = os.getenv('ARANGO_URL') arango_user = os.getenv('ARANGO_USER') arango_password = os.getenv('ARANGO_PASSWORD') # Connect to ArangoDB client = arangodb_driver.connect_arango( url=arango_url) # TODO keep arango connection open - # Connect to the db + # Connect to the "srv6_usid" db database = arangodb_driver.connect_srv6_usid_db( client=client, username=arango_user, password=arango_password ) + # Push the nodes configuration to the database arangodb_driver.insert_nodes_config(database, srv6_usid.read_nodes( args.nodes_file)[0]) + # Return False in order to keep the CLI subsection open after the + # command execution + return False def do_extract(self, args): - """Extract the network topology""" - + ''' + Extract the network topology from a set of nodes running the ISIS + protocol. + + :param args: The argument passed to this command. + :type args: str + :return: False in order to leave the CLI subsection open. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = (topo_cli .parse_arguments_topology_information_extraction_isis( prog='extract', args=args.split(' '))) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Extract the topology topo_cli.topology_information_extraction_isis( routers=args.routers.split(','), period=args.period, @@ -241,23 +333,34 @@ def do_extract(self, args): topo_graph=args.topo_graph, verbose=args.verbose ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_load_on_arango(self, args): - """Read nodes and edges YAML files and upload the topology - on ArangoDB""" - + ''' + Read nodes and edges YAML files and upload the topology on ArangoDB. + + :param args: The argument passed to this command. + :type args: str + :return: False in order to leave the CLI subsection open. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = topo_cli.parse_arguments_load_topo_on_arango( prog='load_on_arango', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Load the topology on Arango database topo_cli.load_topo_on_arango( arango_url=args.arango_url, arango_user=args.arango_user, @@ -266,23 +369,36 @@ def do_load_on_arango(self, args): edges_yaml=args.edges_yaml, verbose=args.verbose ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_extract_and_load_on_arango(self, args): - """Extract the network topology from a set of nodes running ISIS - and upload it on ArangoDB""" - + ''' + Extract the network topology from a set of nodes running ISIS + and upload it on ArangoDB. + + :param args: The argument passed to this command. + :type args: str + :return: False in order to leave the CLI subsection open. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: arg = (topo_cli .parse_arguments_extract_topo_from_isis_and_load_on_arango( prog='extract_and_load_on_arango', args=args.split(' ')) ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Extract the topology from a set of nodes running the ISIS protocol + # and load the extracted topology on a Arango database topo_cli.extract_topo_from_isis_and_load_on_arango( isis_nodes=arg.isis_nodes.split(','), isisd_pwd=arg.isisd_pwd, @@ -296,15 +412,28 @@ def do_extract_and_load_on_arango(self, args): period=arg.period, verbose=arg.verbose ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def complete_show_nodes(self, text, line, start_idx, end_idx): - """Auto-completion for show_nodes command""" - + ''' + Auto-completion for show_nodes command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -315,15 +444,28 @@ def complete_show_nodes(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_print_nodes(text, prev_text) def complete_load_nodes_config(self, text, line, start_idx, end_idx): - """Auto-completion for load_nodes_config command""" - + ''' + Auto-completion for load_nodes_config command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -334,15 +476,28 @@ def complete_load_nodes_config(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_load_nodes_config(text, prev_text) def complete_extract(self, text, line, start_idx, end_idx): - """Auto-completion for extract command""" - + ''' + Auto-completion for extract command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -353,16 +508,29 @@ def complete_extract(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return topo_cli.complete_topology_information_extraction_isis( text, prev_text) def complete_load_on_arango(self, text, line, start_idx, end_idx): - """Auto-completion for load_on_arango command""" - + ''' + Auto-completion for load_on_arango command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -373,16 +541,29 @@ def complete_load_on_arango(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return topo_cli.complete_load_topo_on_arango(text, prev_text) def complete_extract_and_load_on_arango(self, text, line, start_idx, end_idx): - """Auto-completion for extract_and_load_on_arango command""" - + ''' + Auto-completion for extract_and_load_on_arango command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -393,57 +574,67 @@ def complete_extract_and_load_on_arango(self, text, prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return (topo_cli .complete_extract_topo_from_isis_and_load_on_arango( text, prev_text)) def help_show_nodes(self): - """Show help usage for show_nodes config command""" - # + ''' + Show help usage for show_nodes config command. + ''' # pylint: disable=no-self-use # + # Print the help usage srv6_cli.parse_arguments_print_nodes( prog='show_nodes', args=['--help'] ) def help_load_nodes_config(self): - """Show help usage for load_nodes_config nodes command""" - # + ''' + Show help usage for load_nodes_config command. + ''' # pylint: disable=no-self-use # + # Print the help usage srv6_cli.parse_arguments_load_nodes_config( prog='load_nodes_config', args=['--help'] ) def help_extract(self): - """Show help usage for extract command""" - + ''' + Show help usage for extract command. + ''' # pylint: disable=no-self-use - + # + # Print the help usage topo_cli.parse_arguments_topology_information_extraction_isis( prog='extract', args=['--help'] ) def help_load_on_arango(self): - """Show help usage for load_topo_on_arango""" - + ''' + Show help usage for load_topo_on_arango. + ''' # pylint: disable=no-self-use - + # + # Print the help usage topo_cli.parse_arguments_load_topo_on_arango( prog='load_on_arango', args=['--help'] ) def help_extract_and_load_on_arango(self): - """Show help usage for extract_and_load_on_arango""" - + ''' + Show help usage for extract_and_load_on_arango. + ''' # pylint: disable=no-self-use - + # + # Print the help usage topo_cli.parse_arguments_extract_topo_from_isis_and_load_on_arango( prog='extract_and_load_on_arango', args=['--help'] @@ -451,22 +642,40 @@ def help_extract_and_load_on_arango(self): class ControllerCLISRv6PMConfiguration(CustomCmd): - """srv6pm->Configuration subsection""" + ''' + srv6pm->Configuration subsection. + Subsection of the CLI containing several functions for SRv6 Performance + Monitoring configuration. + ''' - prompt = "controller(srv6pm-configuration)> " + # Prompt string + prompt = 'controller(srv6pm-configuration)> ' def do_set(self, args): - """Set configuation""" - + ''' + Set the configuation for an experiment. Before running an experiment + you need to set the configuration. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6pm_cli.parse_arguments_set_configuration( prog='start', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Set the configuration srv6pm_cli.set_configuration( sender=args.sender_ip, reflector=args.reflector_ip, @@ -479,37 +688,62 @@ def do_set(self, args): number_of_color=args.number_of_color, pm_driver=args.pm_driver ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_reset(self, args): - """Clear configuration""" - + ''' + Clear the configuration and reset the nodes. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6pm_cli.parse_arguments_reset_configuration( prog='start', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Clear the configuration srv6pm_cli.reset_configuration( sender=args.sender_ip, reflector=args.reflector_ip, sender_port=args.sender_port, reflector_port=args.reflector_port, ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def complete_set(self, text, line, start_idx, end_idx): - """Auto-completion for set command""" - + ''' + Auto-completion for set command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -525,10 +759,23 @@ def complete_set(self, text, line, start_idx, end_idx): return srv6pm_cli.complete_set_configuration(text, prev_text) def complete_reset(self, text, line, start_idx, end_idx): - """Auto-completion for reset command""" - + ''' + Auto-completion for reset command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -544,20 +791,24 @@ def complete_reset(self, text, line, start_idx, end_idx): return srv6pm_cli.complete_reset_configuration(text, prev_text) def help_set(self): - """Show help usagte for set operation""" - + ''' + Show help usagte for set operation. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6pm_cli.parse_arguments_set_configuration( prog='start', args=['--help'] ) def help_reset(self): - """Show help usage for reset operation""" - + ''' + Show help usage for reset operation. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6pm_cli.parse_arguments_reset_configuration( prog='start', args=['--help'] @@ -565,22 +816,32 @@ def help_reset(self): class ControllerCLISRv6PMExperiment(CustomCmd): - """srv6pm->experiment subsection""" + ''' + srv6pm->experiment subsection. + Subsection of the CLI containing several functions for SRv6 Performance + Monitoring experiments. + ''' - prompt = "controller(srv6pm-experiment)> " + # Prompt string + prompt = 'controller(srv6pm-experiment)> ' def do_start(self, args): - """Start an experiment""" - + '''Start an experiment''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6pm_cli.parse_arguments_start_experiment( prog='start', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Start an experiment srv6pm_cli.start_experiment( sender=args.sender_ip, reflector=args.reflector_ip, @@ -590,11 +851,6 @@ def do_start(self, args): refl_send_dest=args.refl_send_dest, send_refl_sidlist=args.send_refl_sidlist, refl_send_sidlist=args.refl_send_sidlist, - # Interfaces moved to set_configuration - # send_in_interfaces=args.send_in_interfaces, - # refl_in_interfaces=args.refl_in_interfaces, - # send_out_interfaces=args.send_out_interfaces, - # refl_out_interfaces=args.refl_out_interfaces, measurement_protocol=args.measurement_protocol, measurement_type=args.measurement_type, authentication_mode=args.authentication_mode, @@ -608,22 +864,34 @@ def do_start(self, args): refl_send_localseg=args.refl_send_localseg, force=args.force ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_show(self, args): - """Show results of a running experiment""" - + ''' + Show results of a running experiment. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6pm_cli.parse_arguments_get_experiment_results( prog='show', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Get results srv6pm_cli.get_experiment_results( sender=args.sender_ip, reflector=args.reflector_ip, @@ -632,22 +900,33 @@ def do_show(self, args): send_refl_sidlist=args.send_refl_sidlist, refl_send_sidlist=args.refl_send_sidlist ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_stop(self, args): - """Stop a running experiment""" - + ''' + Stop a running experiment. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6pm_cli.parse_arguments_stop_experiment( prog='stop', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False srv6pm_cli.stop_experiment( sender=args.sender_ip, reflector=args.reflector_ip, @@ -660,15 +939,28 @@ def do_stop(self, args): send_refl_localseg=args.send_refl_localseg, refl_send_localseg=args.refl_send_localseg ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def complete_start(self, text, line, start_idx, end_idx): - """Auto-completion for start command""" - + ''' + Auto-completion for start command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -679,15 +971,28 @@ def complete_start(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6pm_cli.complete_start_experiment(text, prev_text) def complete_show(self, text, line, start_idx, end_idx): - """Auto-completion for show command""" - + ''' + Auto-completion for show command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -698,15 +1003,28 @@ def complete_show(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6pm_cli.complete_get_experiment_results(text, prev_text) def complete_stop(self, text, line, start_idx, end_idx): - """Auto-completion for stop command""" - + ''' + Auto-completion for stop command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -717,35 +1035,41 @@ def complete_stop(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6pm_cli.complete_stop_experiment(text, prev_text) def help_start(self): - """Show help usage for start operation""" - + ''' + Show help usage for start operation. + ''' # pylint: disable=no-self-use - + # + # Print the help usage srv6pm_cli.parse_arguments_start_experiment( prog='start', args=['--help'] ) def help_show(self): - """Show help usasge for show operation""" - + ''' + Show help usasge for show operation. + ''' # pylint: disable=no-self-use - + # + # Print the help usage srv6pm_cli.parse_arguments_get_experiment_results( prog='show', args=['--help'] ) def help_stop(self): - """Show help usage for stop operation""" - + ''' + Show help usage for stop operation. + ''' # pylint: disable=no-self-use - + # + # Print the help usage srv6pm_cli.parse_arguments_stop_experiment( prog='stop', args=['--help'] @@ -753,44 +1077,79 @@ def help_stop(self): class ControllerCLISRv6PM(CustomCmd): - """srv6pm subcommmand""" + ''' + srv6pm subcommmand. + Subsection of the CLI containing several functions for SRv6 Performance + Monitoring. + ''' - prompt = "controller(srv6pm)> " + # Prompt string + prompt = 'controller(srv6pm)> ' def do_experiment(self, args): - """Enter srv6pm-experiment subsection""" - + ''' + Enter srv6pm-experiment subsection. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use, unused-argument - + # + # Start SRv6 PM Experiment command loop sub_cmd = ControllerCLISRv6PMExperiment() sub_cmd.cmdloop() def do_configuration(self, args): - """Enter srv6pm-configuration subsection""" - + ''' + Enter srv6pm-configuration subsection. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use, unused-argument - + # + # Start SRv6 PM Experiment command loop sub_cmd = ControllerCLISRv6PMConfiguration() sub_cmd.cmdloop() class ControllerCLISRv6(CustomCmd): - """srv6 subsection""" + ''' + srv6 subsection. + Subsection of the CLI containing several functions for SRv6. + ''' - prompt = "controller(srv6)> " + # Prompt string + prompt = 'controller(srv6)> ' def do_path(self, args): - """Handle a SRv6 path""" - + ''' + Handle a SRv6 path. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6_cli.parse_arguments_srv6_path( prog='path', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Handle the SRv6 path srv6_cli.handle_srv6_path( operation=args.op, grpc_address=args.grpc_ip, @@ -804,22 +1163,34 @@ def do_path(self, args): bsid_addr=args.bsid_addr, fwd_engine=args.fwd_engine ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_behavior(self, args): - """Handle a SRv6 behavior""" - + ''' + Handle a SRv6 behavior. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6_cli.parse_arguments_srv6_behavior( prog='behavior', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Handle SRv6 behavior srv6_cli.handle_srv6_behavior( operation=args.op, grpc_address=args.grpc_ip, @@ -835,22 +1206,34 @@ def do_behavior(self, args): metric=args.metric, fwd_engine=args.fwd_engine ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_unitunnel(self, args): - """Handle a SRv6 unidirectional tunnel""" - + ''' + Handle a SRv6 unidirectional tunnel. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6_cli.parse_arguments_srv6_unitunnel( prog='unitunnel', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Handle SRv6 unidirectional tunnel srv6_cli.handle_srv6_unitunnel( operation=args.op, ingress_ip=args.ingress_grpc_ip, @@ -863,22 +1246,34 @@ def do_unitunnel(self, args): bsid_addr=args.bsid_addr, fwd_engine=args.fwd_engine ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_biditunnel(self, args): - """Handle a SRv6 bidirectional tunnel""" - + ''' + Handle a SRv6 bidirectional tunnel. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Parse the arguments try: args = srv6_cli.parse_arguments_srv6_biditunnel( prog='biditunnel', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Handle SRv6 bidirectional tunnel srv6_cli.handle_srv6_biditunnel( operation=args.op, node_l_ip=args.l_grpc_ip, @@ -894,30 +1289,41 @@ def do_biditunnel(self, args): bsid_addr=args.bsid_addr, fwd_engine=args.fwd_engine ) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def do_usid_policy(self, args): - """Handle a SRv6 uSID policy""" - + ''' + Handle a SRv6 uSID policy. + + :param args: The arguments passed to the command. + :type args: str + :return: False. + :rtype: bool + ''' # pylint: disable=no-self-use, unused-argument - + # + # Parse the arguments try: args = srv6_cli.parse_arguments_srv6_usid_policy( prog='usid_policy', args=args.split(' ') ) except SystemExit: - return False # This workaround avoid exit in case of errors - # ArangoDB params + # In case of errors during the parsing, SystemExit will be raised + # and the process will be terminated + # In order to avoid the process to be terminated, we handle this + # exception and we return "False" to leave the CLI subsection open + return False + # Extract the ArangoDB params from the environment variables arango_url = os.getenv('ARANGO_URL') arango_user = os.getenv('ARANGO_USER') arango_password = os.getenv('ARANGO_PASSWORD') # Connect to ArangoDB client = arangodb_driver.connect_arango( url=arango_url) # TODO keep arango connection open - # Connect to the db + # Connect to the "srv6_usid" db database = arangodb_driver.connect_srv6_usid_db( client=client, username=arango_user, @@ -947,15 +1353,28 @@ def do_usid_policy(self, args): ) # Print nodes available srv6_cli.print_nodes(nodes_dict=nodes_dict) - # Return False in order to keep the CLI subsection open - # after the command execution + # Return False in order to keep the CLI subsection open after the + # command execution return False def complete_path(self, text, line, start_idx, end_idx): - """Auto-completion for path command""" - + ''' + Auto-completion for path command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -966,15 +1385,28 @@ def complete_path(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_srv6_path(text, prev_text) def complete_behavior(self, text, line, start_idx, end_idx): - """Auto-completion for behavior command""" - + ''' + Auto-completion for behavior command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -985,15 +1417,28 @@ def complete_behavior(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_srv6_behavior(text, prev_text) def complete_unitunnel(self, text, line, start_idx, end_idx): - """Auto-completion for unitunnel command""" - + ''' + Auto-completion for unitunnel command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -1004,15 +1449,28 @@ def complete_unitunnel(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_srv6_unitunnel(text, prev_text) def complete_biditunnel(self, text, line, start_idx, end_idx): - """Auto-completion for biditunnel command""" - + ''' + Auto-completion for biditunnel command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -1023,15 +1481,28 @@ def complete_biditunnel(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_srv6_biditunnel(text, prev_text) def complete_usid_policy(self, text, line, start_idx, end_idx): - """Auto-completion for usid_policy command""" - + ''' + Auto-completion for usid_policy command. + + :param text: The string prefix we are attempting to match: all + returned matches begin with it. + :type text: str + :param line: The current input line with leading whitespace removed. + :type line: str + :param start_idx: The beginning index of the prefix text. + :type start_idx: int + :param end_idx: The ending index of the prefix text. + :type end_idx: int + :return: A list containing all the possible matches. + :rtype: list + ''' # pylint: disable=no-self-use, unused-argument - + # # Get the previous argument in the command # Depending on the previous argument, it is possible to # complete specific params, such as the paths @@ -1042,55 +1513,65 @@ def complete_usid_policy(self, text, line, start_idx, end_idx): prev_text = None if len(args) > 1: prev_text = args[-2] # [-2] because last element is always '' - # Call auto-completion function and return a list of - # possible arguments + # Call auto-completion function and return a list of possible + # arguments return srv6_cli.complete_usid_policy(text, prev_text) def help_path(self): - """Show help usage for path command""" - + ''' + Show help usage for path command. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6_cli.parse_arguments_srv6_path( prog='path', args=['--help'] ) def help_behavior(self): - """Show help usage for behavior command""" - + ''' + Show help usage for behavior command. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6_cli.parse_arguments_srv6_behavior( prog='behavior', args=['--help'] ) def help_unitunnel(self): - """Show help usage for unitunnel command""" - + ''' + Show help usage for unitunnel command. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6_cli.parse_arguments_srv6_unitunnel( prog='unitunnel', args=['--help'] ) def help_biditunnel(self): - """Show help usage for biditunnel command""" - + ''' + Show help usage for biditunnel command. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6_cli.parse_arguments_srv6_biditunnel( prog='biditunnel', args=['-help'] ) def help_usid_policy(self): - """Show help usage for usid_policy command""" - + ''' + Show help usage for usid_policy command. + ''' # pylint: disable=no-self-use - + # + # Show the help usage srv6_cli.parse_arguments_usid_policy( prog='usid_policy', args=['--help'] @@ -1098,58 +1579,95 @@ def help_usid_policy(self): class ControllerCLI(CustomCmd): - """Controller CLI entry point""" + ''' + Controller CLI entry point. + ''' + # Prompt string prompt = 'controller> ' - intro = "Welcome! Type ? to list commands" + # Intro message + intro = 'Welcome! Type ? to list commands' def do_exit(self, args): - """Exit from the CLI""" - + ''' + Exit from the CLI. + + :param args: The arguments passed to the command. + :type args: str + :return: True. + :rtype: bool + ''' # pylint: disable=no-self-use, unused-argument - - print("Bye") + # + # Print bye message and return + print('Bye') return True def do_srv6(self, args): - """Enter srv6 subsection""" + ''' + Enter srv6 subsection. + :param args: The arguments passed to the command. + :type args: str + ''' # pylint: disable=no-self-use, unused-argument - + # + # Start the SRv6 command loop sub_cmd = ControllerCLISRv6() sub_cmd.cmdloop() def do_srv6pm(self, args): - """Enter srv6pm subsection""" + ''' + Enter srv6pm subsection. + :param args: The arguments passed to the command. + :type args: str + ''' # pylint: disable=no-self-use, unused-argument - + # + # Start the SRv6 PM command loop sub_cmd = ControllerCLISRv6PM() sub_cmd.cmdloop() def do_topology(self, args): - """Enter topology subsection""" + ''' + Enter topology subsection. + :param args: The arguments passed to the command. + :type args: str + ''' # pylint: disable=no-self-use, unused-argument - + # + # Start the Topology command loop sub_cmd = ControllerCLITopology() sub_cmd.cmdloop() def default(self, line): - """Default behavior""" - + ''' + Default behavior. + + :param line: The command. + :type line: str + :return: True if the command is "quit", False if the command is + unrecognized. + :rtype: bool + ''' + # If the user entered "x" or "q", exit if line in ['x', 'q']: return self.do_exit(line) - - print("Unrecognized command: {}".format(line)) + # Command unrecognized + print('Unrecognized command: {}'.format(line)) return False + # Register handler for the "exit" command do_EOF = do_exit # Class representing the configuration class Config: - """Class implementing configuration for the Controller""" + ''' + Class implementing configuration for the Controller. + ''' # ArangoDB username arango_user = None @@ -1164,8 +1682,14 @@ class Config: # Load configuration from .env file def load_config(self, env_file): - """Load configuration from a .env file""" - + ''' + Load configuration from a .env file + + :param env_file: The path and the name of the .env file containing the + nodes configuration. + :type env_file: str + ''' + # Load configuration from .env file logger.info('*** Loading configuration from %s', env_file) # Path to the .env file env_path = Path(env_file) @@ -1197,18 +1721,25 @@ def load_config(self, env_file): self.debug = None def validate_config(self): - """Validate current configuration""" + ''' + Validate current configuration. + :return: True if the configuraation is valid, False otherwise. + :rtype: bool + ''' # pylint: disable=no-self-use - + # + # Validate configuration + # TODO logger.info('*** Validating configuration') success = True # Return result return success def print_config(self): - """Pretty print current configuration""" - + ''' + Pretty print current configuration. + ''' print() print('****************** CONFIGURATION ******************') print() @@ -1223,13 +1754,16 @@ def print_config(self): print() def import_dependencies(self): - """Import dependencies""" + ''' + Import dependencies. + ''' # Parse options def parse_arguments(): - """Command-line arguments parser""" - + ''' + Command-line arguments parser. + ''' # Get parser parser = ArgumentParser( description='gRPC Southbound APIs for SRv6 Controller' @@ -1249,8 +1783,9 @@ def parse_arguments(): def __main(): - """Entry point for this module""" - + ''' + Entry point for this module. + ''' # Parse command-line arguments args = parse_arguments() # Path to the .env file containing the parameters for the node manager' diff --git a/control_plane/controller/controller/cli/srv6_cli.py b/control_plane/controller/controller/cli/srv6_cli.py index 1b9aec3..5180972 100644 --- a/control_plane/controller/controller/cli/srv6_cli.py +++ b/control_plane/controller/controller/cli/srv6_cli.py @@ -18,14 +18,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Implementation of SRv6 Controller +# Collection of SRv6 utilities for the Controller CLI # # @author Carmine Scarpitta # -"""SRv6 utilities for Controller CLI""" +''' +SRv6 utilities for Controller CLI. +''' +# General imports +import logging import sys from argparse import ArgumentParser @@ -33,32 +37,84 @@ from controller import srv6_utils, srv6_usid, utils from controller.cli import utils as cli_utils +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + # Default CA certificate path DEFAULT_CERTIFICATE = 'cert_server.pem' -def handle_srv6_usid_policy( - operation, - nodes_dict, - lr_destination, - rl_destination, - nodes_lr=None, - nodes_rl=None, - table=-1, - metric=-1, - _id=None, - l_grpc_ip=None, - l_grpc_port=None, - l_fwd_engine=None, - r_grpc_ip=None, - r_grpc_port=None, - r_fwd_engine=None, - decap_sid=None, - locator=None): - """Handle a SRv6 uSID policy""" - +def handle_srv6_usid_policy(operation, nodes_dict, lr_destination, + rl_destination, nodes_lr=None, nodes_rl=None, + table=-1, metric=-1, _id=None, l_grpc_ip=None, + l_grpc_port=None, l_fwd_engine=None, + r_grpc_ip=None, r_grpc_port=None, + r_fwd_engine=None, decap_sid=None, locator=None): + ''' + Handle a SRv6 uSID policy. + + :param operation: The operation to be performed on the uSID policy + (i.e. add, get, change, del). + :type operation: str + :param nodes_dict: Dict containing the nodes configuration. + :type nodes_dict: dict + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str + :param nodes_lr: Waypoints of the SRv6 route for the right to left path. + :type nodes_lr: list + :param nodes_rl: Waypoints of the SRv6 route for the right to leftpath. + :type nodes_rl: list + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string + :param l_grpc_ip: gRPC IP address of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_ip: str, optional + :param l_grpc_port: gRPC port of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_port: str, optional + :param l_fwd_engine: forwarding engine of the left node, required if the + left node is expressed numerically in the nodes list. + :type l_fwd_engine: str, optional + :param r_grpc_ip: gRPC IP address of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_ip: str, optional + :param r_grpc_port: gRPC port of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_port: str, optional + :param r_fwd_engine: Forwarding engine of the right node, required if the + right node is expressed numerically in the nodes + list. + :type r_fwd_engine: str, optional + :param decap_sid: uSID used for the decap behavior (End.DT6). + :type decap_sid: str, optional + :param locator: Locator prefix (e.g. 'fcbb:bbbb::'). + :type locator: str, optional + :raises NodeNotFoundError: Node name not found in the mapping file. + :raises InvalidConfigurationError: The mapping file is not a valid + YAML file. + :raises TooManySegmentsError: segments arg contains more than 6 segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. + :raises InvalidSIDError: SID is wrong for one or more segments. + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + :raises controller.utils.PolicyNotFoundError: Policy not found. + ''' # pylint: disable=too-many-arguments - + # + # Handle the SRv6 uSID policy + logger.debug('Trying to handle the SRv6 uSID policy') res = srv6_usid.handle_srv6_usid_policy( operation=operation, nodes_dict=nodes_dict, @@ -78,27 +134,61 @@ def handle_srv6_usid_policy( decap_sid=decap_sid, locator=locator ) - if res is not None: - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) - - -def handle_srv6_path( - operation, - grpc_address, - grpc_port, - destination, - segments="", - device='', - encapmode="encap", - table=-1, - metric=-1, - bsid_addr='', - fwd_engine='Linux'): - """Handle a SRv6 path""" + # Convert the status code to a human-readable textual description and + # print the description + logger.info('handle_srv6_usid_policy returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) - # pylint: disable=too-many-arguments +def handle_srv6_path(operation, grpc_address, grpc_port, destination, + segments="", device='', encapmode="encap", table=-1, + metric=-1, bsid_addr='', fwd_engine='Linux'): + ''' + Handle a SRv6 path on a node. + + :param operation: The operation to be performed on the SRv6 path + (i.e. add, get, change, del). + :type operation: str + :param grpc_address: gRPC IP address of the node. + :type grpc_address: str + :param grpc_port: gRPC port of the node. + :type grpc_port: int + :param destination: The destination prefix of the SRv6 path. + It can be a IP address or a subnet. + :type destination: str + :param segments: The SID list to be applied to the packets going to + the destination (not required for "get" and "del" + operations). + :type segments: list, optional + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param encapmode: The encap mode to use for the path, i.e. "inline" or + "encap" (default: encap). + :type encapmode: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional + :param fwd_engine: Forwarding engine for the SRv6 route (default: Linux). + :type fwd_engine: str, optional + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + ''' + # pylint: disable=too-many-arguments + # + # Handle the SRv6 uSID path + logger.debug('Trying to handle the SRv6 path') + # Establish a gRPC Channel to the node + logger.debug('Trying to establish a connection to the node %s on ' + 'port %s', grpc_address, grpc_port) with utils.get_grpc_session(grpc_address, grpc_port) as channel: + # Handle SRv6 path res = srv6_utils.handle_srv6_path( operation=operation, channel=channel, @@ -111,27 +201,61 @@ def handle_srv6_path( bsid_addr=bsid_addr, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) - - -def handle_srv6_behavior( - operation, - grpc_address, - grpc_port, - segment, - action='', - device='', - table=-1, - nexthop="", - lookup_table=-1, - interface="", - segments="", - metric=-1, - fwd_engine='Linux'): - """Handle a SRv6 behavior""" + # Convert the status code to a human-readable textual description and + # print the description + logger.info('handle_srv6_path returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) - # pylint: disable=too-many-arguments +def handle_srv6_behavior(operation, grpc_address, grpc_port, segment, + action='', device='', table=-1, nexthop="", + lookup_table=-1, interface="", segments="", + metric=-1, fwd_engine='Linux'): + ''' + Handle a SRv6 behavior on a node. + + :param operation: The operation to be performed on the SRv6 path + (i.e. add, get, change, del). + :type operation: str + :param grpc_address: gRPC IP address of the node. + :type grpc_address: str + :param grpc_port: gRPC port of the node. + :type grpc_port: int + :param segment: The local segment of the SRv6 behavior. It can be a IP + address or a subnet. + :type segment: str + :param action: The SRv6 action associated to the behavior (e.g. End or + End.DT6), (not required for "get" and "change"). + :type action: str, optional + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param nexthop: The nexthop of cross-connect behaviors (e.g. End.DX4 + or End.DX6). + :type nexthop: str, optional + :param lookup_table: The lookup table for the decap behaviors (e.g. + End.DT4 or End.DT6). + :type lookup_table: int, optional + :param interface: The outgoing interface for the End.DX2 behavior. + :type interface: str, optional + :param segments: The SID list to be applied for the End.B6 behavior. + :type segments: list, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param fwd_engine: Forwarding engine for the SRv6 route (default: Linux). + :type fwd_engine: str, optional + ''' + # pylint: disable=too-many-arguments + # + # Handle the SRv6 uSID behavior + logger.debug('Trying to handle the SRv6 behavior') + # Establish a gRPC Channel to the node + logger.debug('Trying to establish a connection to the node %s on ' + 'port %s', grpc_address, grpc_port) with utils.get_grpc_session(grpc_address, grpc_port) as channel: res = srv6_utils.handle_srv6_behavior( operation=operation, @@ -147,20 +271,57 @@ def handle_srv6_behavior( metric=metric, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description and + # print the description + logger.info('handle_srv6_behavior returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) def handle_srv6_unitunnel(operation, ingress_ip, ingress_port, egress_ip, egress_port, destination, segments, localseg=None, bsid_addr='', fwd_engine='Linux'): - """Handle a SRv6 unidirectional tunnel""" - + ''' + Handle a SRv6 unidirectional tunnel. + + :param ingress_ip: gRPC IP address of the ingress node. + :type ingress_ip: str + :param ingress_port: gRPC port of the ingress node. + :type ingress_port: int + :param egress_ip: gRPC IP address of the egress node. + :type egress_ip: str + :param egress_port: gRPC port of the egress node. + :type egress_port: int + :param destination: The destination prefix of the SRv6 tunnel. + It can be a IP address or a subnet. + :type destination: str + :param segments: The SID list to be applied to the packets going to + the destination + :type segments: list + :param localseg: The local segment to be associated to the End.DT6 + seg6local function on the egress node. If the argument + 'localseg' isn't passed in, the End.DT6 function + is not created. + :type localseg: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional + :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. + :type fwd_engine: str, optional + ''' # pylint: disable=too-many-arguments - + # + # Handle the SRv6 unidirectional tunnel + logger.debug('Trying to handle the SRv6 unidirectional tunnel') + # Establish a gRPC Channel to the ingress and egress nodes + logger.debug('Trying to establish a connection to the ingress node %s on ' + 'port %s', ingress_ip, ingress_port) + logger.debug('Trying to establish a connection to the egress node %s on ' + 'port %s', egress_ip, egress_port) with utils.get_grpc_session(ingress_ip, ingress_port) as ingress_channel, \ utils.get_grpc_session(egress_ip, egress_port) as egress_channel: if operation == 'add': + # Create the unidirectional tunnel on the nodes res = srv6_utils.create_uni_srv6_tunnel( ingress_channel=ingress_channel, egress_channel=egress_channel, @@ -170,8 +331,12 @@ def handle_srv6_unitunnel(operation, ingress_ip, ingress_port, bsid_addr=bsid_addr, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description + # and print the description + logger.info('handle_srv6_behavior returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) elif operation == 'del': + # Remove the unidirectional tunnel from the nodes res = srv6_utils.destroy_uni_srv6_tunnel( ingress_channel=ingress_channel, egress_channel=egress_channel, @@ -180,9 +345,12 @@ def handle_srv6_unitunnel(operation, ingress_ip, ingress_port, bsid_addr=bsid_addr, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description + # and print the description + logger.info('handle_srv6_behavior returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) else: - print('Invalid operation %s' % operation) + logger.error('Invalid operation %s' % operation) def handle_srv6_biditunnel(operation, node_l_ip, node_l_port, @@ -190,13 +358,58 @@ def handle_srv6_biditunnel(operation, node_l_ip, node_l_port, sidlist_lr, sidlist_rl, dest_lr, dest_rl, localseg_lr=None, localseg_rl=None, bsid_addr='', fwd_engine='Linux'): - """Handle SRv6 bidirectional tunnel""" - + ''' + Handle SRv6 bidirectional tunnel. + + :param node_l_ip: gRPC IP address of the left node. + :type node_l_ip: str + :param node_l_port: gRPC port of the left node. + :type node_l_port: int + :param node_r_ip: gRPC IP address of the right node. + :type node_r_ip: str + :param node_l_port: gRPC port of the right node. + :type node_l_port: int + :param sidlist_lr: The SID list to be applied to the packets going on the + left to right path + :type sidlist_lr: list + :param sidlist_rl: The SID list to be applied to the packets going on the + right to left path + :type sidlist_rl: list + :param dest_lr: The destination prefix of the SRv6 left to right path. + It can be a IP address or a subnet. + :type dest_lr: str + :param dest_rl: The destination prefix of the SRv6 right to left path. + It can be a IP address or a subnet. + :type dest_rl: str + :param localseg_lr: The local segment to be associated to the End.DT6 + seg6local function for the left to right path. If the + argument 'localseg_lr' isn't passed in, the End.DT6 + function is not created. + :type localseg_lr: str, optional + :param localseg_rl: The local segment to be associated to the End.DT6 + seg6local function for the right to left path. If the + argument 'localseg_lr' isn't passed in, the End.DT6 + function is not created. + :type localseg_rl: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional + :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. + :type fwd_engine: str, optional + ''' # pylint: disable=too-many-arguments,too-many-locals - + # + # Handle the SRv6 bidirectional tunnel + logger.debug('Trying to handle the SRv6 bidirectional tunnel') + # Establish a gRPC Channel to the left and right nodes + logger.debug('Trying to establish a connection to the ingress node %s on ' + 'port %s', node_l_ip, node_l_port) + logger.debug('Trying to establish a connection to the egress node %s on ' + 'port %s', node_r_ip, node_r_port) with utils.get_grpc_session(node_l_ip, node_l_port) as node_l_channel, \ utils.get_grpc_session(node_r_ip, node_r_port) as node_r_channel: if operation == 'add': + # Create the unidirectional tunnel on the nodes res = srv6_utils.create_srv6_tunnel( node_l_channel=node_l_channel, node_r_channel=node_r_channel, @@ -209,8 +422,12 @@ def handle_srv6_biditunnel(operation, node_l_ip, node_l_port, bsid_addr=bsid_addr, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description + # and print the description + logger.info('handle_srv6_behavior returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) elif operation == 'del': + # Remove the unidirectional tunnel from the nodes res = srv6_utils.destroy_srv6_tunnel( node_l_channel=node_l_channel, node_r_channel=node_r_channel, @@ -221,21 +438,26 @@ def handle_srv6_biditunnel(operation, node_l_ip, node_l_port, bsid_addr=bsid_addr, fwd_engine=fwd_engine ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description + # and print the description + logger.info('handle_srv6_behavior returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) else: - print('Invalid operation %s' % operation) + logger.error('Invalid operation %s' % operation) def args_srv6_usid_policy(): ''' - Command-line arguments for the srv6_usid_policy command + Command-line arguments for the srv6_usid_policy command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by the argparse library - - is_path, a boolean flag indicating whether the argument is a path or not - ''' + - is_path, a boolean flag indicating whether the argument is a path or not. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--secure'], @@ -329,8 +551,17 @@ def args_srv6_usid_policy(): # Parse options def parse_arguments_srv6_usid_policy(prog=sys.argv[0], args=None): - """Command-line arguments parser for srv6_usid_policy function""" - + ''' + Command-line arguments parser for srv6_usid_policy function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='gRPC Southbound APIs for SRv6 Controller' @@ -346,9 +577,18 @@ def parse_arguments_srv6_usid_policy(prog=sys.argv[0], args=None): # TAB-completion for SRv6 uSID policy def complete_srv6_usid_policy(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_usid_policy args = args_srv6_usid_policy() # Paths auto-completion @@ -378,14 +618,16 @@ def complete_srv6_usid_policy(text, prev_text): def args_srv6_path(): ''' - Command-line arguments for the srv6_path command + Command-line arguments for the srv6_path command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by the argparse library - - is_path, a boolean flag indicating whether the argument is a path or not - ''' + - is_path, a boolean flag indicating whether the argument is a path or not. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--grpc-ip'], @@ -452,8 +694,17 @@ def args_srv6_path(): # Parse options def parse_arguments_srv6_path(prog=sys.argv[0], args=None): - """Command-line arguments parser for srv6_path function""" - + ''' + Command-line arguments parser for srv6_path function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='gRPC Southbound APIs for SRv6 Controller' @@ -469,9 +720,18 @@ def parse_arguments_srv6_path(prog=sys.argv[0], args=None): # TAB-completion for SRv6 Path def complete_srv6_path(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_path args = args_srv6_path() # Paths auto-completion @@ -501,13 +761,15 @@ def complete_srv6_path(text, prev_text): def args_srv6_behavior(): ''' - Command-line arguments for the srv6_behavior command + Command-line arguments for the srv6_behavior command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--grpc-ip'], @@ -580,8 +842,17 @@ def args_srv6_behavior(): # Parse options def parse_arguments_srv6_behavior(prog=sys.argv[0], args=None): - """Command-line arguments parser for srv6_behavior function""" - + ''' + Command-line arguments parser for srv6_behavior function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='gRPC Southbound APIs for SRv6 Controller' @@ -597,9 +868,18 @@ def parse_arguments_srv6_behavior(prog=sys.argv[0], args=None): # TAB-completion for SRv6 behavior def complete_srv6_behavior(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_behavior args = args_srv6_behavior() # Paths auto-completion @@ -629,13 +909,15 @@ def complete_srv6_behavior(text, prev_text): def args_srv6_unitunnel(): ''' - Command-line arguments for the srv6_unitunnel command + Command-line arguments for the srv6_unitunnel command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--op'], @@ -696,8 +978,17 @@ def args_srv6_unitunnel(): # Parse options def parse_arguments_srv6_unitunnel(prog=sys.argv[0], args=None): - """Command-line arguments parser for srv6_unitunnel function""" - + ''' + Command-line arguments parser for srv6_unitunnel function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='gRPC Southbound APIs for SRv6 Controller' @@ -713,9 +1004,18 @@ def parse_arguments_srv6_unitunnel(prog=sys.argv[0], args=None): # TAB-completion for SRv6 unidirectional tunnel def complete_srv6_unitunnel(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_unitunnel args = args_srv6_unitunnel() # Paths auto-completion @@ -745,13 +1045,15 @@ def complete_srv6_unitunnel(text, prev_text): def args_srv6_biditunnel(): ''' - Command-line arguments for the srv6_biditunnel command + Command-line arguments for the srv6_biditunnel command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--op'], @@ -824,8 +1126,17 @@ def args_srv6_biditunnel(): # Parse options def parse_arguments_srv6_biditunnel(prog=sys.argv[0], args=None): - """Command-line arguments parser for srv6_biditunnel function""" - + ''' + Command-line arguments parser for srv6_biditunnel function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='gRPC Southbound APIs for SRv6 Controller' @@ -841,9 +1152,18 @@ def parse_arguments_srv6_biditunnel(prog=sys.argv[0], args=None): # TAB-completion for SRv6 bi-directional tunnel def complete_srv6_biditunnel(text, prev_text=None): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_biditunnel args = args_srv6_biditunnel() # Paths auto-completion @@ -880,7 +1200,6 @@ def args_load_nodes_config(): the argparse library - is_path, a boolean flag indicating whether the argument is a path or not ''' - return [ { 'args': ['--nodes-file'], @@ -895,8 +1214,17 @@ def args_load_nodes_config(): # Parse options def parse_arguments_load_nodes_config(prog=sys.argv[0], args=None): - """Command-line arguments parser for load_nodes_config function""" - + ''' + Command-line arguments parser for load_nodes_config function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='Load nodes configuration to the database' @@ -912,9 +1240,18 @@ def parse_arguments_load_nodes_config(prog=sys.argv[0], args=None): # TAB-completion for SRv6 uSID def complete_load_nodes_config(text, prev_text=None): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_biditunnel args = args_load_nodes_config() # Paths auto-completion @@ -944,22 +1281,33 @@ def complete_load_nodes_config(text, prev_text=None): def args_print_nodes(): ''' - Command-line arguments for the print_nodes command + Command-line arguments for the print_nodes command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by the argparse library - - is_path, a boolean flag indicating whether the argument is a path or not - ''' + - is_path, a boolean flag indicating whether the argument is a path or not. + :return: The list of the arguments. + :rtype: list + ''' return [ ] # Parse options def parse_arguments_print_nodes(prog=sys.argv[0], args=None): - """Command-line arguments parser for print_nodes function""" - + ''' + Command-line arguments parser for print_nodes function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='Show the list of the available devices' @@ -975,9 +1323,18 @@ def parse_arguments_print_nodes(prog=sys.argv[0], args=None): # TAB-completion for SRv6 uSID def complete_print_nodes(text, prev_text=None): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for srv6_biditunnel args = args_print_nodes() # Paths auto-completion @@ -1005,11 +1362,22 @@ def complete_print_nodes(text, prev_text=None): return args -def print_node_to_addr_mapping(nodes_filename): - '''Print mapping node to IP address''' - srv6_usid.print_node_to_addr_mapping(nodes_filename) +def print_nodes_from_config_file(nodes_filename): + ''' + This function reads a YAML file containing the nodes configuration and + print the available nodes. + + :param nodes_filename: The file containing the nodes configuration. + :type nodes_filename: str + ''' + srv6_usid.print_nodes_from_config_file(nodes_filename) def print_nodes(nodes_dict): - '''Print nodes''' + ''' + Print nodes. + + :param nodes_dict: Dict containing the nodes + :type nodes_dict: dict + ''' srv6_usid.print_nodes(nodes_dict=nodes_dict) diff --git a/control_plane/controller/controller/cli/srv6pm_cli.py b/control_plane/controller/controller/cli/srv6pm_cli.py index 8ef6061..e48bbdd 100644 --- a/control_plane/controller/controller/cli/srv6pm_cli.py +++ b/control_plane/controller/controller/cli/srv6pm_cli.py @@ -18,14 +18,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Implementation of a CLI for the SRv6 Controller +# Collection of SRv6 Performance Measurement utilities for the Controller CLI # # @author Carmine Scarpitta # -"""SRv6 PM utilities for Controller CLI""" +''' +SRv6 PM utilities for Controller CLI. +''' +# General imports +import logging import sys from argparse import ArgumentParser @@ -33,6 +37,10 @@ from controller import srv6_pm, utils from controller.cli import utils as cli_utils +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + # Default CA certificate path DEFAULT_CERTIFICATE = 'cert_server.pem' @@ -41,12 +49,43 @@ def set_configuration(sender, reflector, sender_port, reflector_port, send_udp_port, refl_udp_port, interval_duration, delay_margin, number_of_color, pm_driver): - """Configure a node""" - + ''' + Configure a node for a SRv6 Performance Measurement experiment. + + :param sender: The IP address of the gRPC server on the sender. + :type sender: str + :param sender_port: The port of the gRPC server on the sender. + :type sender_port: int + :param reflector: The IP address of the gRPC server on the reflector. + :type reflector: str + :param reflector_port: The port of the gRPC server on the reflector. + :type reflector_port: int + :param send_udp_port: The destination UDP port used by the sender + :type send_udp_port: int + :param refl_udp_port: The destination UDP port used by the reflector + :type refl_udp_port: int + :param interval_duration: The duration of the interval + :type interval_duration: int + :param delay_margin: The delay margin + :type delay_margin: int + :param number_of_color: The number of the color + :type number_of_color: int + :param pm_driver: The driver to use for the experiments (i.e. eBPF or + IPSet). + :type pm_driver: str + ''' # pylint: disable=too-many-arguments - + # + # Establish a gRPC Channel to the sender and a gRPC Channel to the + # reflector + logger.debug('Trying to establish a connection to the sender %s on ' + 'port %s', sender, sender_port) + logger.debug('Trying to establish a connection to the reflector %s on ' + 'port %s', reflector, reflector_port) with utils.get_grpc_session(sender, sender_port) as sender_channel, \ utils.get_grpc_session(reflector, reflector_port) as refl_channel: + # Send the set_configuration request + logger.debug('Trying to set the configuration') res = srv6_pm.set_configuration( sender_channel=sender_channel, reflector_channel=refl_channel, @@ -57,39 +96,128 @@ def set_configuration(sender, reflector, number_of_color=number_of_color, pm_driver=pm_driver ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description and + # print the description + logger.info('Set configuration returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) def reset_configuration(sender, reflector, sender_port, reflector_port): - """Clear node configuration""" - + ''' + Reset the configuration for a SRv6 Performance Measurement experiment. + + :param sender: The IP address of the gRPC server on the sender. + :type sender: str + :param sender_port: The port of the gRPC server on the sender. + :type sender_port: int + :param reflector: The IP address of the gRPC server on the reflector. + :type reflector: str + :param reflector_port: The port of the gRPC server on the reflector. + :type reflector_port: int + ''' + # Establish a gRPC Channel to the sender and a gRPC Channel to the + # reflector + logger.debug('Trying to establish a connection to the sender %s on ' + 'port %s', sender, sender_port) + logger.debug('Trying to establish a connection to the reflector %s on ' + 'port %s', reflector, reflector_port) with utils.get_grpc_session(sender, sender_port) as sender_channel, \ utils.get_grpc_session(reflector, reflector_port) as refl_channel: + # Send the reset_configuration request + logger.debug('Trying to reset the configuration') res = srv6_pm.reset_configuration( sender_channel=sender_channel, reflector_channel=refl_channel ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description and + # print the description + logger.info('Reset configuration returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) def start_experiment(sender, reflector, sender_port, reflector_port, send_refl_dest, refl_send_dest, send_refl_sidlist, refl_send_sidlist, - # send_in_interfaces, refl_in_interfaces, - # send_out_interfaces, refl_out_interfaces, measurement_protocol, measurement_type, authentication_mode, authentication_key, timestamp_format, delay_measurement_mode, padding_mbz, loss_measurement_mode, measure_id=None, send_refl_localseg=None, refl_send_localseg=None, force=False): - """Start an experiment""" - + ''' + Start an experiment. + + :param sender: The IP address of the gRPC server on the sender. + :type sender: str + :param sender_port: The port of the gRPC server on the sender. + :type sender_port: int + :param reflector: The IP address of the gRPC server on the reflector. + :type reflector: str + :param reflector_port: The port of the gRPC server on the reflector. + :type reflector_port: int + :param send_refl_dest: The destination of the SRv6 path + sender->reflector + :type send_refl_dest: str + :param refl_send_dest: The destination of the SRv6 path + reflector->sender + :type refl_send_dest: str + :param send_refl_sidlist: The SID list to be used for the path + sender->reflector + :type send_refl_sidlist: list + :param refl_send_sidlist: The SID list to be used for the path + reflector->sender + :type refl_send_sidlist: list + :param measurement_protocol: The measurement protocol (i.e. TWAMP + or STAMP) + :type measurement_protocol: str + :param measurement_type: The measurement type (i.e. delay or loss) + :type measurement_type: str + :param authentication_mode: The authentication mode (i.e. HMAC_SHA_256) + :type authentication_mode: str + :param authentication_key: The authentication key + :type authentication_key: str + :param timestamp_format: The Timestamp Format (i.e. PTPv2 or NTP) + :type timestamp_format: str + :param delay_measurement_mode: Delay measurement mode (i.e. one-way, + two-way or loopback mode) + :type delay_measurement_mode: str + :param padding_mbz: The padding size + :type padding_mbz: int + :param loss_measurement_mode: The loss measurement mode (i.e. Inferred + or Direct mode) + :type loss_measurement_mode: str + :param measure_id: Identifier for the experiment (default is None). + automatically generated. + :type measure_id: int, optional + :param send_refl_localseg: The local segment associated to the End.DT6 + (decap) function for the path + sender->reflector (default is None). + If the argument 'send_localseg' isn't passed + in, the seg6local End.DT6 route is not created. + :type send_refl_localseg: str, optional + :param refl_send_localseg: The local segment associated to the End.DT6 + (decap) function for the path + reflector->sender (default is None). + If the argument 'send_localseg' isn't passed + in, the seg6local End.DT6 route is not created. + :type refl_send_localseg: str, optional + :param force: If set, force the controller to start an experiment if a + SRv6 path for the destination already exists. The old SRv6 + path is replaced with the new one (default is False). + :type force: bool, optional + ''' # pylint: disable=too-many-arguments, too-many-locals - + # + # Establish a gRPC Channel to the sender and a gRPC Channel to the + # reflector + logger.debug('Trying to establish a connection to the sender %s on ' + 'port %s', sender, sender_port) + logger.debug('Trying to establish a connection to the reflector %s on ' + 'port %s', reflector, reflector_port) with utils.get_grpc_session(sender, sender_port) as sender_channel, \ utils.get_grpc_session(reflector, reflector_port) as refl_channel: + # Send the start_experiment request res = srv6_pm.start_experiment( sender_channel=sender_channel, reflector_channel=refl_channel, @@ -97,11 +225,6 @@ def start_experiment(sender, reflector, refl_send_dest=refl_send_dest, send_refl_sidlist=send_refl_sidlist.split(','), refl_send_sidlist=refl_send_sidlist.split(','), - # Interfaces moved to set_configuration - # send_in_interfaces=send_in_interfaces, - # refl_in_interfaces=refl_in_interfaces, - # send_out_interfaces=send_out_interfaces, - # refl_out_interfaces=refl_out_interfaces, measurement_protocol=measurement_protocol, measurement_type=measurement_type, authentication_mode=authentication_mode, @@ -115,37 +238,109 @@ def start_experiment(sender, reflector, refl_send_localseg=refl_send_localseg, force=force ) - print('%s\n\n' % utils.STATUS_CODE_TO_DESC[res]) + # Convert the status code to a human-readable textual description and + # print the description + logger.info('start_experiment returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) def get_experiment_results(sender, reflector, sender_port, reflector_port, send_refl_sidlist, refl_send_sidlist): - """Get the results of a running experiment""" - + ''' + Get the results of a running experiment. + + :param sender: The IP address of the gRPC server on the sender. + :type sender: str + :param sender_port: The port of the gRPC server on the sender. + :type sender_port: int + :param reflector: The IP address of the gRPC server on the reflector. + :type reflector: str + :param reflector_port: The port of the gRPC server on the reflector. + :type reflector_port: int + :param send_refl_sidlist: The SID list to be used for the path + sender->reflector + :type send_refl_sidlist: list + :param refl_send_sidlist: The SID list to be used for the path + reflector->sender + :type refl_send_sidlist: list + :raises controller.utils.NoMeasurementDataAvailableError: If an error + occurred while + retrieving the + results. + ''' # pylint: disable=too-many-arguments - + # + # Establish a gRPC Channel to the sender and a gRPC Channel to the + # reflector + logger.debug('Trying to establish a connection to the sender %s on ' + 'port %s', sender, sender_port) + logger.debug('Trying to establish a connection to the reflector %s on ' + 'port %s', reflector, reflector_port) with utils.get_grpc_session(sender, sender_port) as sender_channel, \ utils.get_grpc_session(reflector, reflector_port) as refl_channel: - print(srv6_pm.get_experiment_results( - sender_channel=sender_channel, - reflector_channel=refl_channel, - send_refl_sidlist=send_refl_sidlist.split(','), - refl_send_sidlist=refl_send_sidlist.split(',') - )) + # Get and print the experiment results + logger.info('start_experiment returned:\n\n%s', + srv6_pm.get_experiment_results( + sender_channel=sender_channel, + reflector_channel=refl_channel, + send_refl_sidlist=send_refl_sidlist.split(','), + refl_send_sidlist=refl_send_sidlist.split(',') + )) def stop_experiment(sender, reflector, sender_port, reflector_port, send_refl_dest, refl_send_dest, send_refl_sidlist, refl_send_sidlist, send_refl_localseg=None, refl_send_localseg=None): - """Stop a running experiment""" - + ''' + Stop a running experiment. + + :param sender: The IP address of the gRPC server on the sender. + :type sender: str + :param sender_port: The port of the gRPC server on the sender. + :type sender_port: int + :param reflector: The IP address of the gRPC server on the reflector. + :type reflector: str + :param reflector_port: The port of the gRPC server on the reflector. + :type reflector_port: int + :param send_refl_dest: The destination of the SRv6 path + sender->reflector + :type send_refl_dest: str + :param refl_send_dest: The destination of the SRv6 path + reflector->sender + :type refl_send_dest: str + :param send_refl_sidlist: The SID list used for the path + sender->reflector + :type send_refl_sidlist: list + :param refl_send_sidlist: The SID list used for the path + reflector->sender + :type refl_send_sidlist: list + :param send_refl_localseg: The local segment associated to the End.DT6 + (decap) function for the path sender->reflector + (default is None). If the argument + 'send_localseg' isn't passed in, the seg6local + End.DT6 route is not removed. + :type send_refl_localseg: str, optional + :param refl_send_localseg: The local segment associated to the End.DT6 + (decap) function for the path reflector->sender + (default is None). If the argument + 'send_localseg' isn't passed in, the seg6local + End.DT6 route is not removed. + :type refl_send_localseg: str, optional + ''' # pylint: disable=too-many-arguments - + # + # Establish a gRPC Channel to the sender and a gRPC Channel to the + # reflector + logger.debug('Trying to establish a connection to the sender %s on ' + 'port %s', sender, sender_port) + logger.debug('Trying to establish a connection to the reflector %s on ' + 'port %s', reflector, reflector_port) with utils.get_grpc_session(sender, sender_port) as sender_channel, \ utils.get_grpc_session(reflector, reflector_port) as refl_channel: - srv6_pm.stop_experiment( + # Send the stop_experiment request + res = srv6_pm.stop_experiment( sender_channel=sender_channel, reflector_channel=refl_channel, send_refl_dest=send_refl_dest, @@ -155,17 +350,23 @@ def stop_experiment(sender, reflector, send_refl_localseg=send_refl_localseg, refl_send_localseg=refl_send_localseg ) + # Convert the status code to a human-readable textual description and + # print the description + logger.info('stop_experiment returned %s - %s\n\n', res, + utils.STATUS_CODE_TO_DESC[res]) def args_set_configuration(): ''' - Command-line arguments for the set_configuration command + Command-line arguments for the set_configuration command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--sender-ip'], @@ -245,8 +446,17 @@ def args_set_configuration(): # Parse options def parse_arguments_set_configuration(prog=sys.argv[0], args=None): - """Command-line arguments parser for set_configuration function""" - + ''' + Command-line arguments parser for set_configuration function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -262,9 +472,18 @@ def parse_arguments_set_configuration(prog=sys.argv[0], args=None): # TAB-completion for set_configuration def complete_set_configuration(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get the arguments for set_configuration args = args_set_configuration() # Paths auto-completion @@ -294,13 +513,15 @@ def complete_set_configuration(text, prev_text): def args_reset_configuration(): ''' - Command-line arguments for the reset_configuration command + Command-line arguments for the reset_configuration command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--sender-ip'], @@ -340,8 +561,17 @@ def args_reset_configuration(): # Parse options def parse_arguments_reset_configuration(prog=sys.argv[0], args=None): - """Command-line arguments parser for reset_configuration function""" - + ''' + Command-line arguments parser for reset_configuration function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -357,9 +587,18 @@ def parse_arguments_reset_configuration(prog=sys.argv[0], args=None): # TAB-completion for reset_configuration def complete_reset_configuration(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get the arguments for reset_configuration args = args_reset_configuration() # Paths auto-completion @@ -389,13 +628,15 @@ def complete_reset_configuration(text, prev_text): def args_start_experiment(): ''' - Command-line arguments for the start_experiment command + Command-line arguments for the start_experiment command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--sender-ip'], @@ -503,8 +744,17 @@ def args_start_experiment(): # Parse options def parse_arguments_start_experiment(prog=sys.argv[0], args=None): - """Command-line arguments parser for start_experiment function""" - + ''' + Command-line arguments parser for start_experiment function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -520,9 +770,18 @@ def parse_arguments_start_experiment(prog=sys.argv[0], args=None): # TAB-completion for start_experiment def complete_start_experiment(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get the arguments for start_experiment args = args_start_experiment() # Paths auto-completion @@ -552,13 +811,15 @@ def complete_start_experiment(text, prev_text): def args_get_experiment_results(): ''' - Command-line arguments for the get_experiment_results command + Command-line arguments for the get_experiment_results command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--sender-ip'], @@ -606,8 +867,17 @@ def args_get_experiment_results(): # Parse options def parse_arguments_get_experiment_results(prog=sys.argv[0], args=None): - """Command-line arguments parser for get_experiments_results function""" - + ''' + Command-line arguments parser for get_experiments_results function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -623,9 +893,18 @@ def parse_arguments_get_experiment_results(prog=sys.argv[0], args=None): # TAB-completion for get_experiment_results def complete_get_experiment_results(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get the arguments for get_experiment_results args = args_get_experiment_results() # Paths auto-completion @@ -656,13 +935,15 @@ def complete_get_experiment_results(text, prev_text): def args_stop_experiment(): ''' - Command-line arguments for the stop_experiment command + Command-line arguments for the stop_experiment command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--sender-ip'], @@ -726,8 +1007,17 @@ def args_stop_experiment(): # Parse options def parse_arguments_stop_experiment(prog=sys.argv[0], args=None): - """Command-line arguments parser for stop_experiment function""" - + ''' + Command-line arguments parser for stop_experiment function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -743,9 +1033,18 @@ def parse_arguments_stop_experiment(prog=sys.argv[0], args=None): # TAB-completion for stop_experiment def complete_stop_experiment(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get the arguments for stop_experiment args = args_stop_experiment() # Paths auto-completion diff --git a/control_plane/controller/controller/cli/topo_cli.py b/control_plane/controller/controller/cli/topo_cli.py index ec7b822..4cfc475 100644 --- a/control_plane/controller/controller/cli/topo_cli.py +++ b/control_plane/controller/controller/cli/topo_cli.py @@ -18,23 +18,30 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Implementation of a CLI for the SRv6 Controller +# Collection of topology utilities for the Controller CLI # # @author Carmine Scarpitta # -"""ArangoDB utilities for Controller CLI""" +''' +ArangoDB utilities for Controller CLI. +''' # General imports +import logging import os import sys from argparse import ArgumentParser # Controller dependencies -from controller import arangodb_utils +from controller.db_utils.arangodb import arangodb_utils from controller.cli import utils as cli_utils +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + # Interval between two consecutive extractions (in seconds) DEFAULT_TOPO_EXTRACTION_PERIOD = 0 @@ -43,11 +50,48 @@ def extract_topo_from_isis(isis_nodes, isisd_pwd, nodes_yaml, edges_yaml, addrs_yaml=None, hosts_yaml=None, verbose=False): - """Extract the network topology from a set of nodes running - ISIS protocol""" - + ''' + Extract the network topology from a set of nodes running ISIS protocol. + The extracted topology can be exported to a YAML file (two separate YAML + files for nodes and edges). Optionally, you can enrich the extracted + topology with IP addresses and other hosts by creating your own addresses + and hosts YAML files. + + :param isis_nodes: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type isis_nodes: list + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param nodes_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML file + (default: None). + :type nodes_yaml: str, optional + :param edges_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML file + (default: None). + :type edges_yaml: str, optional + :param addrs_yaml: The path and the name of the YAML file containing the + addresses of the nodes. If this argument is not passed, + the addresses are not added to the exported topology. + :type addrs_yaml: str, optional + :param hosts_yaml: The path and the name of the YAML file containing the + hosts. If this argument is not passed, the hosts are + not added to the exported topology. + :type hosts_yaml: str, optional + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + :raises controller.db_utils.arangodb.arangodb_utils \\ + .TopologyInformationExtractionError: Error while attempting to + extract the topology. + ''' # pylint: disable=too-many-arguments - + # + # Extract the topology + logger.debug('Trying to extract the topology') arangodb_utils.extract_topo_from_isis( isis_nodes=isis_nodes.split(','), isisd_pwd=isisd_pwd, @@ -57,25 +101,45 @@ def extract_topo_from_isis(isis_nodes, isisd_pwd, hosts_yaml=hosts_yaml, verbose=verbose ) + logger.info('Topology extraction completed successfully') def load_topo_on_arango(arango_url, arango_user, arango_password, nodes_yaml, edges_yaml, verbose=False): - """Load a network topology on a Arango database""" - + ''' + Load a network topology on a Arango database. + + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param arango_user: The username used to access the ArangoDB. + :type arango_user: str + :param arango_password: The password used to access the ArangoDB. + :type arango_password: str + :param nodes_yaml: Set of nodes. + :type nodes_yaml: set + :param edges_yaml: Set of edges. + :type edges_yaml: set + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + ''' # pylint: disable=too-many-arguments - + # # Init database + logger.debug('Initializing database') nodes_collection, edges_collection = arangodb_utils.initialize_db( arango_url=arango_url, arango_user=arango_user, arango_password=arango_password ) # Read nodes YAML + logger.debug('Loading nodes YAML') nodes = arangodb_utils.load_yaml_dump(nodes_yaml) # Read edges YAML + logger.debug('Loading edges YAML') edges = arangodb_utils.load_yaml_dump(edges_yaml) # Load nodes and edges on ArangoDB + logger.debug('Loading topology on ArangoDB') arangodb_utils.load_topo_on_arango( arango_url=arango_url, user=arango_user, @@ -86,6 +150,7 @@ def load_topo_on_arango(arango_url, arango_user, arango_password, edges_collection=edges_collection, verbose=verbose ) + logger.info('Topology loaded on ArangoDB successfully') def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, @@ -95,11 +160,55 @@ def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, nodes_yaml=None, edges_yaml=None, addrs_yaml=None, hosts_yaml=None, period=0, verbose=False): - """Extract the topology from a set of nodes running ISIS protocol - and load it on a Arango database""" - + ''' + Extract the network topology from a set of nodes running ISIS protocol + and upload it on a database. The extracted topology can be exported to a + YAML file (two separate YAML files for nodes and edges). Optionally, you + can enrich the extracted topology with IP addresses and other hosts by + creating your own addresses and hosts YAML files. + + :param isis_nodes: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type isis_nodes: list + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param arango_user: The username used to access the ArangoDB. + :type arango_user: str + :param arango_password: The password used to access the ArangoDB. + :type arango_password: str + :param nodes_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML file + (default: None). + :type nodes_yaml: str, optional + :param edges_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML file + (default: None). + :type edges_yaml: str, optional + :param addrs_yaml: The path and the name of the YAML file containing the + addresses of the nodes. If this argument is not passed, + the addresses are not added to the exported topology. + :type addrs_yaml: str, optional + :param hosts_yaml: The path and the name of the YAML file containing the + hosts. If this argument is not passed, the hosts are + not added to the exported topology. + :type hosts_yaml: str, optional + :param period: The interval between two consecutive extractions. If this + arguments is equals to 0, this function performs a single + extraction and then returns (default: 0). + :type period: int, optional + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + ''' # pylint: disable=too-many-arguments - + # + # Extract the topology and load it on ArangoDB + logger.debug('Extracting topology and loading on ArangoDB') arangodb_utils.extract_topo_from_isis_and_load_on_arango( isis_nodes=isis_nodes, isisd_pwd=isisd_pwd, @@ -113,6 +222,7 @@ def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, period=period, verbose=verbose ) + logger.info('Topology extracted and loaded on ArangoDB') def topology_information_extraction_isis(routers, period, isisd_pwd, @@ -121,10 +231,60 @@ def topology_information_extraction_isis(routers, period, isisd_pwd, edges_file_yaml=None, addrs_yaml=None, hosts_yaml=None, topo_graph=None, verbose=False): - """Run periodical topology extraction""" - + ''' + Run periodical topology extraction. + + :param routers: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type routers: list + :param period: The interval between two consecutive extractions. If this + arguments is equals to 0, this function performs a single + extraction and then returns (default: 0). + :type period: int, optional + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param arango_user: The username used to access the ArangoDB. + :type arango_user: str + :param arango_password: The password used to access the ArangoDB. + :type arango_password: str + :param topo_file_json: The path and the name of the output JSON file + containing the topology. If this parameter is not + provided, the topology is not exported to a JSON + file (default: None). + :type topo_file_json: str, optional + :param nodes_file_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML + file (default: None). + :type nodes_file_yaml: str, optional + :param edges_file_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML + file (default: None). + :type edges_file_yaml: str, optional + :param addrs_yaml: The path and the name of the YAML file containing the + addresses of the nodes. If this argument is not passed, + the addresses are not added to the exported topology. + :type addrs_yaml: str, optional + :param hosts_yaml: The path and the name of the YAML file containing the + hosts. If this argument is not passed, the hosts are + not added to the exported topology. + :type hosts_yaml: str, optional + :param topo_graph: The path and the name of the output SVG file (image). + If this parameter is not provided, the topology is not + exported as image (default: None). + :type topo_graph: str, optional + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + ''' # pylint: disable=too-many-arguments, unused-argument - + # + # Extract the topology and load it on ArangoDB + logger.debug('Extracting topology and loading on ArangoDB') arangodb_utils.extract_topo_from_isis_and_load_on_arango( isis_nodes=routers, isisd_pwd=isisd_pwd, @@ -135,17 +295,20 @@ def topology_information_extraction_isis(routers, period, isisd_pwd, period=period, verbose=verbose ) + logger.info('Topology extracted and loaded on ArangoDB') def args_extract_topo_from_isis(): ''' - Command-line arguments for the extract_topo_from_isis command + Command-line arguments for the extract_topo_from_isis command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--isis-nodes'], @@ -187,8 +350,17 @@ def args_extract_topo_from_isis(): # Parse options def parse_arguments_extract_topo_from_isis(prog=sys.argv[0], args=None): - """Command-line arguments parser for topolgy extraction function""" - + ''' + Command-line arguments parser for topolgy extraction function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -204,9 +376,18 @@ def parse_arguments_extract_topo_from_isis(prog=sys.argv[0], args=None): # TAB-completion for extract_topo_from_isis def complete_extract_topo_from_isis(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments from extract_topo_from_isis args = args_extract_topo_from_isis() # Paths auto-completion @@ -238,13 +419,15 @@ def complete_extract_topo_from_isis(text, prev_text): def args_load_topo_on_arango(): ''' - Command-line arguments for the load_topo_on_arango command + Command-line arguments for the load_topo_on_arango command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--arango-url'], @@ -283,8 +466,17 @@ def args_load_topo_on_arango(): # Parse options def parse_arguments_load_topo_on_arango(prog=sys.argv[0], args=None): - """Command-line arguments parser for load on Arango function""" - + ''' + Command-line arguments parser for load on Arango function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -300,9 +492,18 @@ def parse_arguments_load_topo_on_arango(prog=sys.argv[0], args=None): # TAB-completion for load_topo_on_arango def complete_load_topo_on_arango(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for load_topo_on_arango args = args_load_topo_on_arango() # Paths auto-completion @@ -332,13 +533,15 @@ def complete_load_topo_on_arango(text, prev_text): def args_extract_topo_from_isis_and_load_on_arango(): ''' - Command-line arguments for the extract_topo_from_isis_and_load_on_arango + Command-line arguments for the extract_topo_from_isis_and_load_on_arango. command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--isis-nodes'], @@ -401,9 +604,17 @@ def args_extract_topo_from_isis_and_load_on_arango(): # Parse options def parse_arguments_extract_topo_from_isis_and_load_on_arango( prog=sys.argv[0], args=None): - """Command-line arguments parser for - extract_topo_from_isis_and_load_on_arango function""" - + '''Command-line arguments parser for + extract_topo_from_isis_and_load_on_arango function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -419,9 +630,18 @@ def parse_arguments_extract_topo_from_isis_and_load_on_arango( # TAB-completion for extract_topo_from_isis_and_load_on_arango def complete_extract_topo_from_isis_and_load_on_arango(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for extract_topo_from_isis_and_load_on_arango args = args_extract_topo_from_isis_and_load_on_arango() # Paths auto-completion @@ -452,13 +672,15 @@ def complete_extract_topo_from_isis_and_load_on_arango(text, prev_text): def args_topology_information_extraction_isis(): ''' - Command-line arguments for the topology_information_extraction_isis + Command-line arguments for the topology_information_extraction_isis. command. Arguments are represented as a dicts. Each dict has two items: - args, a list of names for the argument - kwargs, a dict containing the attributes for the argument required by - the argparse library - ''' + the argparse library. + :return: The list of the arguments. + :rtype: list + ''' return [ { 'args': ['--routers'], @@ -516,9 +738,18 @@ def args_topology_information_extraction_isis(): # Parse options def parse_arguments_topology_information_extraction_isis( prog=sys.argv[0], args=None): - """Command-line arguments parser for topology - information extraction function""" - + ''' + Command-line arguments parser for topology information extraction + function. + + :param prog: The name of the program (default: sys.argv[0]) + :type prog: str, optional + :param args: List of strings to parse. If None, the list is taken from + sys.argv (default: None). + :type args: list, optional + :return: Return the namespace populated with the argument strings. + :rtype: argparse.Namespace + ''' # Get parser parser = ArgumentParser( prog=prog, description='' @@ -534,9 +765,18 @@ def parse_arguments_topology_information_extraction_isis( # TAB-completion for topology_information_extraction_isis def complete_topology_information_extraction_isis(text, prev_text): - """This function receives a string as argument and returns - a list of parameters candidate for the auto-completion of the string""" - + ''' + This function receives a string as argument and returns + a list of parameters candidate for the auto-completion of the string. + + :param text: The text to be auto-completed. + :type text: str + :param prev_text: The argument that comes before the text to be + auto-completed. + :type prev_text: str + :return: A list containing the possible words for the auto-completion. + :rtype: list + ''' # Get arguments for topology_information_extraction_isis args = args_topology_information_extraction_isis() # Paths auto-completion diff --git a/control_plane/controller/controller/cli/utils.py b/control_plane/controller/controller/cli/utils.py index 712db81..576dce7 100644 --- a/control_plane/controller/controller/cli/utils.py +++ b/control_plane/controller/controller/cli/utils.py @@ -24,7 +24,9 @@ # -"""Utilities functions used by Controller CLI""" +''' +Utilities functions used by Controller CLI. +''' # General imports import glob as gb @@ -36,9 +38,15 @@ def complete_path(path): - """Take a partial 'path' as argument and return a - list of path names that match the 'path'""" - + ''' + Take a partial 'path' as argument and return a list of path names that + match the 'path'. + + :param path: The partial path. + :type path: str + :return: A list of path names that match the 'path'. + :rtype: list + ''' if op.isdir(path): return gb.glob(op.join(path, '*')) return gb.glob(path + '*') diff --git a/control_plane/controller/controller/config/controller.env b/control_plane/controller/controller/config/controller.env index e40628f..4059107 100644 --- a/control_plane/controller/controller/config/controller.env +++ b/control_plane/controller/controller/config/controller.env @@ -32,6 +32,9 @@ # Must debug logs be enabled? (optional, default: True) # export DEBUG=True +# Must persistency be enabled? (optional, default: False) +export ENABLE_PERSISTENCY=True + ############################################################################## diff --git a/control_plane/controller/controller/controller_cli.py b/control_plane/controller/controller/controller_cli.py index 7951741..a63683b 100644 --- a/control_plane/controller/controller/controller_cli.py +++ b/control_plane/controller/controller/controller_cli.py @@ -18,14 +18,17 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Controller +# Implementation of SDN Controller # # @author Carmine Scarpitta # -"""Entry point for controller CLI""" +''' +Entry point for controller CLI. +''' +# Controller dependencies from controller.cli import cli diff --git a/control_plane/controller/controller/db_utils/__init__.py b/control_plane/controller/controller/db_utils/__init__.py new file mode 100644 index 0000000..a14ffe4 --- /dev/null +++ b/control_plane/controller/controller/db_utils/__init__.py @@ -0,0 +1,30 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Collection of database utilities +# +# @author Carmine Scarpitta +# + +''' +This package provides several utilities that allow a SDN Controller to +interact with a database. This enables the support for the Controller +persistency. +''' diff --git a/control_plane/controller/controller/db_utils/arangodb/__init__.py b/control_plane/controller/controller/db_utils/arangodb/__init__.py new file mode 100644 index 0000000..39b1770 --- /dev/null +++ b/control_plane/controller/controller/db_utils/arangodb/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Collection of ArangoDB database utilities +# +# @author Carmine Scarpitta +# + +''' +This package provides a collection of utilities for interacting with a +ArangoDB database. +''' diff --git a/control_plane/controller/controller/arangodb_driver.py b/control_plane/controller/controller/db_utils/arangodb/arangodb_driver.py similarity index 99% rename from control_plane/controller/controller/arangodb_driver.py rename to control_plane/controller/controller/db_utils/arangodb/arangodb_driver.py index 970998e..f14af0c 100644 --- a/control_plane/controller/controller/arangodb_driver.py +++ b/control_plane/controller/controller/db_utils/arangodb/arangodb_driver.py @@ -25,7 +25,7 @@ ''' -ArangoDB driver +Python driver for ArangoDB. ''' # pylint: disable=too-many-arguments @@ -37,7 +37,7 @@ class NodesConfigNotLoadedError(Exception): ''' - NodesConfigNotLoadedError + NodesConfigNotLoadedError. ''' @@ -47,7 +47,7 @@ def connect_arango(url): :param url: ArangoDB URL or list of URLs. :type url: str - :return: ArangoDB client + :return: ArangoDB client. :rtype: arango.client.ArangoClient ''' return ArangoClient(hosts=url) @@ -57,6 +57,8 @@ def connect_db(client, db_name, username, password): ''' Connect to a Arango database. + :param client: ArangoDB client. + :type client: arango.client.ArangoClient :param db_name: Database name. :type db_name: str :param username: Username for basic authentication. diff --git a/control_plane/controller/controller/arangodb_utils.py b/control_plane/controller/controller/db_utils/arangodb/arangodb_utils.py similarity index 50% rename from control_plane/controller/controller/arangodb_utils.py rename to control_plane/controller/controller/db_utils/arangodb/arangodb_utils.py index 2849d4a..1227854 100644 --- a/control_plane/controller/controller/arangodb_utils.py +++ b/control_plane/controller/controller/db_utils/arangodb/arangodb_utils.py @@ -25,7 +25,8 @@ ''' -ArangoDB utilities +This module provides several utilities that allow the Controller to interact +with ArangoDB. ''' # General imports @@ -33,11 +34,12 @@ import logging import time +# pyaml dependencies from pyaml import yaml # Import topology extraction utility functions -from controller.ti_extraction import (connect_and_extract_topology_isis, - dump_topo_yaml) +from controller.ti_extraction.ti_extraction_isis import connect_and_extract_topology_isis +from controller.ti_extraction.ti_extraction_utils import dump_topo_yaml # DB update modules from db_update import arango_db @@ -48,18 +50,39 @@ logger = logging.getLogger(__name__) +class TopologyInformationExtractionError(Exception): + ''' + An error occurred while attempting to extract the network topology. + ''' + + def save_yaml_dump(obj, filename): ''' - Export an object to a YAML file + Export an object to a YAML file. + + :param obj: The object to export. + :type obj: list or dict + :param filename: The path and the name of the output file. + :type filename: str + :return: True. + :rtype: bool ''' - # Save file + # Export the object to a YAML file with open(filename, 'w') as outfile: yaml.dump(obj, outfile) + # Done, return + return True def load_yaml_dump(filename): ''' - Load a YAML file and return a dict representation + Load a YAML file and return a list or dict representation. + + :param filename: The path and the name of the input file. + :type filename: str + :return: A list or dict containing the information extracted from the + file. + :rtype: list or dict ''' # Load YAML file with open(filename, 'r') as infile: @@ -68,15 +91,25 @@ def load_yaml_dump(filename): def fill_ip_addresses(nodes, addresses_yaml): ''' - Add addresses to a nodes dict + Read the IP addresses of the nodes from a YAML file and add the addresses + to a nodes list. The matching between the addresses and the nodes is based + on the nodes key which acts as node identifier. + + :param nodes: List containing the nodes. Each node is represented as dict. + :type nodes: list + :param addresses_yaml: The path and name of the input YAML file. + :type addresses_yaml: str + :return: The list of the nodes enriched with the IP addresses read from the + YAML file. + :rtype: list ''' - # Read IP addresses information from a YAML file and - # add addresses to the nodes - logger.info('*** Filling nodes YAML file with IP addresses') - # Open hosts file + # Read IP addresses information from a YAML file and add addresses to the + # nodes + logger.debug('*** Filling nodes YAML file with IP addresses') + # Open addresses file with open(addresses_yaml, 'r') as infile: addresses = yaml.safe_load(infile.read()) - # Parse addresses + # Parse addresses and build mapping node to address node_to_addr = dict() for addr in addresses: node_to_addr[addr['node']] = addr['ip_address'] @@ -91,17 +124,28 @@ def fill_ip_addresses(nodes, addresses_yaml): def add_hosts(nodes, edges, hosts_yaml): ''' - Add hosts to a topology + Read the hosts from a YAML file and add them to a topology. Topology is + expressed through its nodes and edges. + + :param nodes: List containing the nodes. Each node is represented as dict. + :type nodes: list + :param edges: List containing the edges. Each edge is represented as dict. + :type edges: list + :param hosts_yaml: The path and name of the input YAML file. + :type hosts_yaml: str + :return: Tuple containing the list of nodes and edges enriched with the + hosts contained in the YAML file. + :rtype: tuple ''' - # Read hosts information from a YAML file and - # add hosts to the nodes and edges lists - logger.info('*** Adding hosts to the topology') + # Read hosts information from a YAML file and add hosts to the nodes and + # edges lists + logger.debug('*** Adding hosts to the topology') # Open hosts file with open(hosts_yaml, 'r') as infile: hosts = yaml.safe_load(infile.read()) # Add hosts and links for host in hosts: - # Add host + # Add host to the nodes list nodes.append({ '_key': host['name'], 'type': 'host', @@ -127,17 +171,28 @@ def add_hosts(nodes, edges, hosts_yaml): '_from': 'nodes/%s' % host['name'], 'type': 'edge' }) + # Return the updated nodes and edges lists logger.info('*** Nodes YAML updated\n') logger.info('*** Edges YAML updated\n') - # Return the updated nodes and edges lists return nodes, edges def initialize_db(arango_url, arango_user, arango_password, verbose=False): ''' - Initialize database + Initialize database. + + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param arango_user: The username used to access the ArangoDB. + :type arango_user: str + :param arango_password: The password used to access the ArangoDB. + :type arango_password: str + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + :return: A tuple containing the nodes collection and the edges collection. + :rtype: tuple ''' - # # pylint: disable=unused-argument # # Wrapper function @@ -149,13 +204,48 @@ def initialize_db(arango_url, arango_user, arango_password, verbose=False): def extract_topo_from_isis(isis_nodes, isisd_pwd, - nodes_yaml, edges_yaml, + nodes_yaml=None, edges_yaml=None, addrs_yaml=None, hosts_yaml=None, verbose=False): ''' - Extract the network topology - from a set of nodes running ISIS protocol + Extract the network topology from a set of nodes running ISIS protocol. + The extracted topology can be exported to a YAML file (two separate YAML + files for nodes and edges). Optionally, you can enrich the extracted + topology with IP addresses and other hosts by creating your own addresses + and hosts YAML files. + + :param isis_nodes: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type isis_nodes: list + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param nodes_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML file + (default: None). + :type nodes_yaml: str, optional + :param edges_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML file + (default: None). + :type edges_yaml: str, optional + :param addrs_yaml: The path and the name of the YAML file containing the + addresses of the nodes. If this argument is not passed, + the addresses are not added to the exported topology. + :type addrs_yaml: str, optional + :param hosts_yaml: The path and the name of the YAML file containing the + hosts. If this argument is not passed, the hosts are + not added to the exported topology. + :type hosts_yaml: str, optional + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + :return: True. + :rtype: bool + :raises controller.db_utils.arangodb.arangodb_utils \\ + .TopologyInformationExtractionError: Error while attempting to + extract the topology. ''' - # # pylint: disable=too-many-arguments # # Param isis_nodes: list of ip-port @@ -169,7 +259,7 @@ def extract_topo_from_isis(isis_nodes, isisd_pwd, ) if nodes is None or edges is None or node_to_systemid is None: logger.error('Cannot extract topology') - return + raise TopologyInformationExtractionError # Export the topology in YAML format nodes, edges = dump_topo_yaml( nodes=nodes, @@ -189,6 +279,8 @@ def extract_topo_from_isis(isis_nodes, isisd_pwd, # Save edges YAML file if edges_yaml is not None: save_yaml_dump(edges, edges_yaml) + # Done, return + return True def load_topo_on_arango(arango_url, user, password, @@ -196,9 +288,28 @@ def load_topo_on_arango(arango_url, user, password, nodes_collection, edges_collection, verbose=False): ''' - Load a network topology on a database + Load a network topology on a database. + + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param user: The username used to access the ArangoDB. + :type user: str + :param password: The password used to access the ArangoDB. + :type password: str + :param nodes: Set of nodes. + :type nodes: set + :param edges: Set of edges. + :type edges: set + :param nodes_collection: Collection of nodes. + :type nodes_collection: arango.collection.StandardCollection + :param edges_collection: Collection of edges. + :type edges_collection: arango.collection.StandardCollection + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + :return: True. + :rtype: bool ''' - # # Current Arango arguments are not used, # so we can skip the check # pylint: disable=unused-argument, too-many-arguments @@ -210,6 +321,8 @@ def load_topo_on_arango(arango_url, user, password, nodes_dict=nodes, edges_dict=edges ) + # Done, return + return True def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, @@ -221,9 +334,51 @@ def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, period=0, verbose=False): ''' Extract the network topology from a set of nodes running ISIS protocol - and upload it on a database + and upload it on a database. The extracted topology can be exported to a + YAML file (two separate YAML files for nodes and edges). Optionally, you + can enrich the extracted topology with IP addresses and other hosts by + creating your own addresses and hosts YAML files. + + :param isis_nodes: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type isis_nodes: list + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param arango_url: The URL of the ArangoDB. + :type arango_url: str + :param arango_user: The username used to access the ArangoDB. + :type arango_user: str + :param arango_password: The password used to access the ArangoDB. + :type arango_password: str + :param nodes_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML file + (default: None). + :type nodes_yaml: str, optional + :param edges_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML file + (default: None). + :type edges_yaml: str, optional + :param addrs_yaml: The path and the name of the YAML file containing the + addresses of the nodes. If this argument is not passed, + the addresses are not added to the exported topology. + :type addrs_yaml: str, optional + :param hosts_yaml: The path and the name of the YAML file containing the + hosts. If this argument is not passed, the hosts are + not added to the exported topology. + :type hosts_yaml: str, optional + :param period: The interval between two consecutive extractions. If this + arguments is equals to 0, this function performs a single + extraction and then returns (default: 0). + :type period: int, optional + :param verbose: Define whether to enable the verbose mode or not + (default: False). + :type verbose: bool, optional + :return: True. + :rtype: bool ''' - # # pylint: disable=too-many-arguments, too-many-locals # # Param isis_nodes: list of ip-port @@ -292,3 +447,5 @@ def extract_topo_from_isis_and_load_on_arango(isis_nodes, isisd_pwd, break # Wait 'period' seconds between two extractions time.sleep(period) + # Done, return + return True diff --git a/control_plane/controller/controller/db_utils/arangodb/init_db.py b/control_plane/controller/controller/db_utils/arangodb/init_db.py new file mode 100644 index 0000000..ee77a76 --- /dev/null +++ b/control_plane/controller/controller/db_utils/arangodb/init_db.py @@ -0,0 +1,97 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Utilities for the initialization of ArangoDB +# +# @author Carmine Scarpitta +# + + +''' +This module provides a collection of utilities used to initialize a Arango +database +''' + +# General imports +import logging +import os + +# Controller depedencies +from controller.db_utils.arangodb import arangodb_driver + + +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + + +# Global variables definition +# +# +# ArangoDB params +ARANGO_URL = os.getenv('ARANGO_URL', 'http://localhost:8529') +ARANGO_USER = os.getenv('ARANGO_USER', 'root') +ARANGO_PASSWORD = os.getenv('ARANGO_PASSWORD', '12345678') + + +def init_srv6_usid_db(arango_url=ARANGO_URL, arango_user=ARANGO_USER, + arango_password=ARANGO_PASSWORD): + ''' + Initialize uSID database and uSID policies collection. + + :param arango_url: The URL of the ArangoDB. If this argument is not + provided, the value assigned to the environment + variable ARANGO_URL will be used as URL. + :type arango_url: str, optional + :param arango_user: The username used to access the ArangoDB. If this + argument is not provided, the value assigned to the + environment variable ARANGO_USER will be used as + username. + :type arango_user: str, optional + :param arango_password: The password used to access the ArangoDB. If this + argument is not provided, the value assigned to the + environment variable ARANGO_PASSWORD will be used as + password. + :type arango_password: str, optional + :return: True. + :rtype: bool + ''' + logger.debug('*** Initializing SRv6 uSID database') + # Connect to ArangoDB + client = arangodb_driver.connect_arango(url=arango_url) + # Initialize SRv6 uSID database + arangodb_driver.init_srv6_usid_db( + client=client, + arango_username=arango_user, + arango_password=arango_password + ) + # Initialize uSID policies collection + arangodb_driver.init_usid_policies_collection( + client=client, + arango_username=arango_user, + arango_password=arango_password + ) + logger.info('*** SRv6 uSID database initialized successfully') + return True + + +# Entry point for this module +if __name__ == '__main__': + init_srv6_usid_db() diff --git a/control_plane/controller/controller/init_db.py b/control_plane/controller/controller/init_db.py deleted file mode 100644 index 5931ba0..0000000 --- a/control_plane/controller/controller/init_db.py +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/python - -########################################################################## -# Copyright (C) 2020 Carmine Scarpitta -# (Consortium GARR and University of Rome "Tor Vergata") -# www.garr.it - www.uniroma2.it/netgroup -# -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Utilities for the initialization of ArangoDB -# -# @author Carmine Scarpitta -# - - -''' -This module provides a collection of utilities used to initialize a Arango -database -''' - -# General imports -import os - -# Controller depedencies -from controller import arangodb_driver - - -# ArangoDB params -ARANGO_URL = os.getenv('ARANGO_URL', 'http://localhost:8529') -ARANGO_USER = os.getenv('ARANGO_USER', 'root') -ARANGO_PASSWORD = os.getenv('ARANGO_PASSWORD', '12345678') - - -def init_srv6_usid_db(): - ''' - Initialize uSID database and uSID policies collection - ''' - # Connect to ArangoDB - client = arangodb_driver.connect_arango(url=ARANGO_URL) - # Initialize SRv6 uSID database - arangodb_driver.init_srv6_usid_db( - client=client, - arango_username=ARANGO_USER, - arango_password=ARANGO_PASSWORD - ) - # Initialize uSID policies collection - arangodb_driver.init_usid_policies_collection( - client=client, - arango_username=ARANGO_USER, - arango_password=ARANGO_PASSWORD - ) - - -# Entry point for this module -if __name__ == '__main__': - init_srv6_usid_db() diff --git a/control_plane/controller/controller/srv6_pm.py b/control_plane/controller/controller/srv6_pm.py index aa391a9..6d1f0e1 100644 --- a/control_plane/controller/controller/srv6_pm.py +++ b/control_plane/controller/controller/srv6_pm.py @@ -18,21 +18,22 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Control-Plane functionalities used for SRv6 PM +# Control-Plane functionalities used for SRv6 Performance Monitoring # # @author Carmine Scarpitta # ''' -This module implements control-plane functionalities for SRv6 PM +This module implements control-plane functionalities for SRv6 Performance +Monitoring. ''' # pylint: disable=too-many-lines +# General imports import json import logging -# General imports import os import sys import time @@ -52,47 +53,46 @@ # Controller dependencies from controller import srv6_utils, utils -# Configuration parameters +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + +# Read configuration parameters from the environment variables # -# Kafka support +# Kafka support (default: disabled) ENABLE_KAFKA_INTEGRATION = os.getenv('ENABLE_KAFKA_INTEGRATION', 'false') ENABLE_KAFKA_INTEGRATION = ENABLE_KAFKA_INTEGRATION.lower() == 'true' -# gRPC sserver +# gRPC server (default: disabled) +# In the standard operation mode, the SDN Controller acts as gRPC client +# sending requests to the gRPC servers executed on the nodes. In some project, +# we need a gRPC server executed on the Controller and ready to accepts +# commands sent by the nodes ENABLE_GRPC_SERVER = os.getenv('ENABLE_GRPC_SERVER', 'false') ENABLE_GRPC_SERVER = ENABLE_GRPC_SERVER.lower() == 'true' -# Kafka server -KAFKA_SERVERS = os.getenv('KAFKA_SERVERS', 'kafka:9092') +# Comma-separated Kafka servers (default: "kafka:9092") +KAFKA_SERVERS = os.getenv('KAFKA_SERVERS', 'kafka:9092').split(',') # Kafka depedencies -try: - if ENABLE_KAFKA_INTEGRATION: +if ENABLE_KAFKA_INTEGRATION: + try: from kafka import KafkaProducer from kafka.errors import KafkaError -except ImportError: - print('ENABLE_KAFKA_INTEGRATION is set in the configuration.') - print('kafka-python is required to run') - print('kafka-python not found.') - sys.exit(-2) + except ImportError: + logger.fatal('ENABLE_KAFKA_INTEGRATION is set in the configuration.') + logger.fatal('kafka-python is required to run') + logger.fatal('kafka-python not found.') + sys.exit(-2) # Global variables definition # # -# Logger reference -logging.basicConfig(level=logging.NOTSET) -logger = logging.getLogger(__name__) # Default parameters for SRv6 controller # # Default IP address of the gRPC server DEFAULT_GRPC_SERVER_IP = '::' # Default port of the gRPC server DEFAULT_GRPC_SERVER_PORT = 12345 -# Default port of the gRPC client -DEFAULT_GRPC_CLIENT_PORT = 12345 -# Define whether to use SSL or not for the gRPC client -DEFAULT_CLIENT_SECURE = False -# SSL certificate of the root CA -DEFAULT_CLIENT_CERTIFICATE = 'client_cert.pem' # Define whether to use SSL or not for the gRPC server DEFAULT_SERVER_SECURE = False # SSL certificate of the gRPC server @@ -100,26 +100,61 @@ # SSL key of the gRPC server DEFAULT_SERVER_KEY = 'server_cert.pem' -# Topic for TWAMP data +# Kafka topic for TWAMP data TOPIC_TWAMP = 'twamp' -# Topic for iperf data +# Kafka topic for iperf data TOPIC_IPERF = 'iperf' -# Kafka servers -if KAFKA_SERVERS is not None: - KAFKA_SERVERS = KAFKA_SERVERS.split(',') - def publish_to_kafka(bootstrap_servers, topic, measure_id, interval, timestamp, fw_color, rv_color, sender_seq_num, reflector_seq_num, sender_tx_counter, sender_rx_counter, reflector_tx_counter, reflector_rx_counter): ''' - Publish the measurement data to Kafka + Publish the measurement data to a Kafka topic. + + :param bootstrap_servers: Kafka servers ("host[:port]" string (or list of + "host[:port]" strings). + :type bootstrap_servers: str or list + :param topic: Kafka topic. + :type topic: str + :param measure_id: An identifier for the measure. + :type measure_id: int + :param interval: The duration of the interval. + :type interval: int + :param timestamp: The timestamp of the measurement. + :type timestamp: str + :param fw_color: Color for the forward path. + :type fw_color: int + :param rv_color: Color for the reverse path. + :type rv_color: int + :param sender_seq_num: Sequence number of the sender (for the forward + path). + :type sender_seq_num: int + :param reflector_seq_num: Sequence number of the reflector (for the + reverse path). + :type reflector_seq_num: int + :param sender_tx_counter: Transmission counter of the sender (for the + forward path). + :type sender_tx_counter: int + :param sender_rx_counter: Reception counter of the sender (for the + reverse path) + :type sender_rx_counter: int + :param reflector_tx_counter: Transmission counter of the reflector (for the + reverse path). + :type reflector_tx_counter: int + :param reflector_rx_counter: Reception counter of the reflector (for the + forward path). + :type reflector_rx_counter: int + :return: Resolves to RecordMetadata. + :rtype: kafka.FutureRecordMetadata + :raises KafkaTimeoutError: If unable to fetch topic metadata, or unable to + obtain memory buffer prior to configured + max_block_ms. ''' - # # pylint: disable=too-many-arguments, too-many-locals # + # Init producer and result producer = None result = None try: @@ -132,14 +167,18 @@ def publish_to_kafka(bootstrap_servers, topic, measure_id, interval, # Publish measurement data to the provided topic result = producer.send( topic=topic, - value={'measure_id': measure_id, 'interval': interval, - 'timestamp': timestamp, 'fw_color': fw_color, - 'rv_color': rv_color, 'sender_seq_num': sender_seq_num, - 'reflector_seq_num': reflector_seq_num, - 'sender_tx_counter': sender_tx_counter, - 'sender_rx_counter': sender_rx_counter, - 'reflector_tx_counter': reflector_tx_counter, - 'reflector_rx_counter': reflector_rx_counter} + value={ + 'measure_id': measure_id, + 'interval': interval, + 'timestamp': timestamp, + 'fw_color': fw_color, + 'rv_color': rv_color, + 'sender_seq_num': sender_seq_num, + 'reflector_seq_num': reflector_seq_num, + 'sender_tx_counter': sender_tx_counter, + 'sender_rx_counter': sender_rx_counter, + 'reflector_tx_counter': reflector_tx_counter, + 'reflector_rx_counter': reflector_rx_counter} ) except KafkaError as err: logger.error('Cannot publish data to Kafka: %s', err) @@ -156,11 +195,45 @@ def publish_iperf_data_to_kafka(bootstrap_servers, topic, _from, measure_id, transfer_dim, bitrate, bitrate_dim, retr, cwnd, cwnd_dim): ''' - Publish IPERF data to Kafka + Publish IPERF data to a Kafka topic. + + :param bootstrap_servers: Kafka servers ("host[:port]" string (or list of + "host[:port]" strings). + :type bootstrap_servers: str or list + :param topic: Kafka topic. + :type topic: str + :param _from: A string representing the originator of the iperf data + (e.g "client") + :type _from: str + :param measure_id: An identifier for the measure. + :type measure_id: int + :param generator_id: Generator ID. + :type generator_id: int + :param interval: The duration of the interval. + :type interval: int + :param transfer: Transferred data (value). + :type transfer: int + :param transfer_dim: Transferred data (unit of measurement). + :type transfer_dim: str + :param bitrate: Bitrate (value). + :type bitrate: int + :param bitrate_dim: Bitrate (unit of measurement). + :type bitrate_dim: str + :param retr: Number of TCP segments retransmitted. + :type retr: int + :param cwnd: Congestion window (value) + :type cwnd: int + :param cwnd_dim: Congestion window (unit of measurement). + :type cwnd_dim: str + :return: Resolves to RecordMetadata. + :rtype: kafka.FutureRecordMetadata + :raises KafkaTimeoutError: If unable to fetch topic metadata, or unable to + obtain memory buffer prior to configured + max_block_ms. ''' - # # pylint: disable=too-many-arguments # + # Init producer and result producer = None result = None try: @@ -183,8 +256,7 @@ def publish_iperf_data_to_kafka(bootstrap_servers, topic, _from, measure_id, 'bitrate_dim': bitrate_dim, 'retr': retr, 'cwnd': cwnd, - 'cwnd_dim': cwnd_dim - } + 'cwnd_dim': cwnd_dim} ) except KafkaError as err: logger.error('Cannot publish data to Kafka: %s', err) @@ -197,74 +269,104 @@ def publish_iperf_data_to_kafka(bootstrap_servers, topic, _from, measure_id, def start_experiment_sender(channel, sidlist, rev_sidlist, - # in_interfaces, out_interfaces, measurement_protocol, measurement_type, authentication_mode, authentication_key, timestamp_format, delay_measurement_mode, padding_mbz, loss_measurement_mode): ''' - RPC used to start an experiment on the sender + RPC used to start an experiment on the sender. + + :param channel: A gRPC channel to the sender. + :type channel: class: `grpc._channel.Channel` + :param sidlist: The SID list of the path to be tested with the experiment. + :type sidlist: list + :param rev_sidlist: The SID list of the reverse path to be tested with the + experiment. + :type rev_sidlist: list + :param measurement_protocol: The measurement protocol (i.e. TWAMP or + STAMP) + :type measurement_protocol: str + :param measurement_type: The measurement type (i.e. delay or loss) + :type measurement_type: str + :param authentication_mode: The authentication mode (i.e. HMAC_SHA_256) + :type authentication_mode: str + :param authentication_key: The authentication key + :type authentication_key: str + :param timestamp_format: The Timestamp Format (i.e. PTPv2 or NTP) + :type timestamp_format: str + :param delay_measurement_mode: Delay measurement mode (i.e. one-way, + two-way or loopback mode) + :type delay_measurement_mode: str + :param padding_mbz: The padding size + :type padding_mbz: int + :param loss_measurement_mode: The loss measurement mode (i.e. Inferred + or Direct mode) + :type loss_measurement_mode: str + :raises controller.utils.InvalidArgumentError: If you provided an invalid + argument is invalid. ''' - # # pylint: disable=too-many-arguments, too-many-return-statements # + # ######################################################################## # Convert string args to int # # Measurement Protocol - try: - if isinstance(measurement_protocol, str): + if isinstance(measurement_protocol, str): + try: measurement_protocol = \ srv6pmCommons_pb2.MeasurementProtocol.Value( measurement_protocol) - except ValueError: - logger.error('Invalid Measurement protocol: %s', measurement_protocol) - return None + except ValueError: + logger.error('Invalid Measurement protocol: %s', + measurement_protocol) + raise utils.InvalidArgumentError # Measurement Type - try: - if isinstance(measurement_type, str): + if isinstance(measurement_type, str): + try: measurement_type = \ srv6pmCommons_pb2.MeasurementType.Value(measurement_type) - except ValueError: - logger.error('Invalid Measurement Type: %s', measurement_type) - return None + except ValueError: + logger.error('Invalid Measurement Type: %s', measurement_type) + raise utils.InvalidArgumentError # Authentication Mode - try: - if isinstance(authentication_mode, str): + if isinstance(authentication_mode, str): + try: authentication_mode = \ srv6pmCommons_pb2.AuthenticationMode.Value(authentication_mode) - except ValueError: - logger.error('Invalid Authentication Mode: %s', authentication_mode) - return None + except ValueError: + logger.error('Invalid Authentication Mode: %s', + authentication_mode) + raise utils.InvalidArgumentError # Timestamp Format - try: - if isinstance(timestamp_format, str): + if isinstance(timestamp_format, str): + try: timestamp_format = \ srv6pmCommons_pb2.TimestampFormat.Value(timestamp_format) - except ValueError: - logger.error('Invalid Timestamp Format: %s', timestamp_format) - return None + except ValueError: + logger.error('Invalid Timestamp Format: %s', timestamp_format) + raise utils.InvalidArgumentError # Delay Measurement Mode - try: - if isinstance(delay_measurement_mode, str): + if isinstance(delay_measurement_mode, str): + try: delay_measurement_mode = \ srv6pmCommons_pb2.MeasurementDelayMode.Value( delay_measurement_mode) - except ValueError: - logger.error('Invalid Delay Measurement Mode: %s', - delay_measurement_mode) - return None + except ValueError: + logger.error('Invalid Delay Measurement Mode: %s', + delay_measurement_mode) + raise utils.InvalidArgumentError # Loss Measurement Mode - try: - if isinstance(loss_measurement_mode, str): + if isinstance(loss_measurement_mode, str): + try: loss_measurement_mode = \ srv6pmCommons_pb2.MeasurementLossMode.Value( loss_measurement_mode) - except ValueError: - logger.error('Invalid Loss Measurement Mode: %s', - loss_measurement_mode) - return None - # + except ValueError: + logger.error('Invalid Loss Measurement Mode: %s', + loss_measurement_mode) + raise utils.InvalidArgumentError + # ######################################################################## # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) # Create the request @@ -273,14 +375,10 @@ def start_experiment_sender(channel, sidlist, rev_sidlist, request.sdlist = '/'.join(sidlist) # Set the reverse SID list request.sdlistreverse = '/'.join(rev_sidlist) - # Set the incoming interfaces - # request.in_interfaces.extend(in_interfaces) - # # Set the outgoing interfaces - # request.out_interfaces.extend(out_interfaces) - # + # ######################################################################## # Set the sender options # - # Set the measureemnt protocol + # Set the measurement protocol (request.sender_options.measurement_protocol) = \ measurement_protocol # pylint: disable=no-member # Set the authentication mode @@ -304,14 +402,19 @@ def start_experiment_sender(channel, sidlist, rev_sidlist, # Set the measurement loss mode request.sender_options.measurement_loss_mode = \ loss_measurement_mode # pylint: disable=no-member - # + # ######################################################################## # Start the experiment on the sender and return the response return stub.startExperimentSender(request=request) def stop_experiment_sender(channel, sidlist): ''' - RPC used to stop an experiment on the sender + RPC used to stop an experiment on the sender. + + :param channel: A gRPC channel to the sender. + :type channel: class: `grpc._channel.Channel` + :param sidlist: The SID list of the path under test. + :type sidlist: list ''' # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) @@ -325,7 +428,12 @@ def stop_experiment_sender(channel, sidlist): def retrieve_experiment_results_sender(channel, sidlist): ''' - RPC used to get the results of a running experiment + RPC used to get the results of a running experiment. + + :param channel: A gRPC channel to the sender. + :type channel: class: `grpc._channel.Channel` + :param sidlist: The SID list of the path under test. + :type sidlist: list ''' # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) @@ -341,21 +449,39 @@ def set_node_configuration(channel, send_udp_port, refl_udp_port, interval_duration, delay_margin, number_of_color, pm_driver): ''' - RPC used to set the configuration on a sender node + RPC used to set the configuration on a node (sender or reflector). + + :param channel: A gRPC channel to the node. + :type channel: class: `grpc._channel.Channel` + :param send_udp_port: UDP port of the sender. + :type send_udp_port: int + :param send_udp_port: UDP port of the reflector. + :type send_udp_port: int + :param interval_duration: The duration of the interval. + :type interval_duration: int + :param delay_margin: The delay margin. + :type delay_margin: int + :param number_of_color: The number of the color. + :type number_of_color: int + :param pm_driver: The driver to use for the experiments (i.e. eBPF or + IPSet). + :type pm_driver: str + :raises controller.utils.InvalidArgumentError: If you provided an invalid + argument is invalid. ''' - # # pylint: disable=too-many-arguments # + # ######################################################################## # Convert string args to int # # PM Driver - try: - if isinstance(pm_driver, str): + if isinstance(pm_driver, str): + try: pm_driver = srv6pmCommons_pb2.PMDriver.Value(pm_driver) - except ValueError: - logger.error('Invalid PM Driver: %s', pm_driver) - return None - # + except ValueError: + logger.error('Invalid PM Driver: %s', pm_driver) + raise utils.InvalidArgumentError + # ######################################################################## # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) # Create the request message @@ -364,7 +490,7 @@ def set_node_configuration(channel, send_udp_port, refl_udp_port, request.ss_udp_port = int(send_udp_port) # Set the destination UDP port of the reflector request.refl_udp_port = int(refl_udp_port) - # + # ######################################################################## # Set the color options # # Set the interval duration @@ -376,16 +502,20 @@ def set_node_configuration(channel, send_udp_port, refl_udp_port, # Set the number of color request.color_options.numbers_of_color = \ int(number_of_color) # pylint: disable=no-member - # + # ######################################################################## # Set driver request.pm_driver = pm_driver + # ######################################################################## # Start the experiment on the reflector and return the response return stub.setConfiguration(request=request) def reset_node_configuration(channel): ''' - RPC used to clear the configuration on a sender node + RPC used to clear the configuration on a node (sender or reflector). + + :param channel: A gRPC channel to the node. + :type channel: class: `grpc._channel.Channel` ''' # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) @@ -396,53 +526,77 @@ def reset_node_configuration(channel): def start_experiment_reflector(channel, sidlist, rev_sidlist, - # in_interfaces, out_interfaces, measurement_protocol, measurement_type, authentication_mode, authentication_key, loss_measurement_mode): ''' - RPC used to start an experiment on the reflector + RPC used to start an experiment on the reflector. + + :param channel: A gRPC channel to the reflector. + :type channel: class: `grpc._channel.Channel` + :param sidlist: The SID list of the path to be tested with the experiment. + :type sidlist: list + :param rev_sidlist: The SID list of the reverse path to be tested with the + experiment. + :type rev_sidlist: list + :param measurement_protocol: The measurement protocol (i.e. TWAMP or + STAMP) + :type measurement_protocol: str + :param measurement_type: The measurement type (i.e. delay or loss) + :type measurement_type: str + :param authentication_mode: The authentication mode (i.e. HMAC_SHA_256) + :type authentication_mode: str + :param authentication_key: The authentication key + :type authentication_key: str + :param loss_measurement_mode: The loss measurement mode (i.e. Inferred + or Direct mode) + :type loss_measurement_mode: str + :raises controller.utils.InvalidArgumentError: If you provided an invalid + argument is invalid. ''' # pylint: disable=too-many-arguments # + # ######################################################################## # Convert string args to int # # Measurement Protocol - try: - if isinstance(measurement_protocol, str): + if isinstance(measurement_protocol, str): + try: measurement_protocol = \ srv6pmCommons_pb2.MeasurementProtocol.Value( measurement_protocol) - except ValueError: - logger.error('Invalid Measurement protocol: %s', measurement_protocol) - return None + except ValueError: + logger.error('Invalid Measurement protocol: %s', + measurement_protocol) + raise utils.InvalidArgumentError # Measurement Type - try: - if isinstance(measurement_type, str): + if isinstance(measurement_type, str): + try: measurement_type = \ srv6pmCommons_pb2.MeasurementType.Value(measurement_type) - except ValueError: - logger.error('Invalid Measurement Type: %s', measurement_type) - return None + except ValueError: + logger.error('Invalid Measurement Type: %s', measurement_type) + raise utils.InvalidArgumentError # Authentication Mode - try: - if isinstance(authentication_mode, str): + if isinstance(authentication_mode, str): + try: authentication_mode = \ srv6pmCommons_pb2.AuthenticationMode.Value(authentication_mode) - except ValueError: - logger.error('Invalid Authentication Mode: %s', authentication_mode) - return None + except ValueError: + logger.error('Invalid Authentication Mode: %s', + authentication_mode) + raise utils.InvalidArgumentError # Loss Measurement Mode - try: - if isinstance(loss_measurement_mode, str): + if isinstance(loss_measurement_mode, str): + try: loss_measurement_mode = \ srv6pmCommons_pb2.MeasurementLossMode.Value( loss_measurement_mode) - except ValueError: - logger.error('Invalid Loss Measurement Mode: %s', - loss_measurement_mode) - return None - # + except ValueError: + logger.error('Invalid Loss Measurement Mode: %s', + loss_measurement_mode) + raise utils.InvalidArgumentError + # ######################################################################## # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) # Create the request message @@ -451,11 +605,7 @@ def start_experiment_reflector(channel, sidlist, rev_sidlist, request.sdlist = '/'.join(sidlist) # Set the reverse SID list request.sdlistreverse = '/'.join(rev_sidlist) - # Set the incoming interfaces - # request.in_interfaces.extend(in_interfaces) - # # Set the outgoing interfaces - # request.out_interfaces.extend(out_interfaces) - # + # ######################################################################## # Set the reflector options # # Set the measurement protocol @@ -473,13 +623,19 @@ def start_experiment_reflector(channel, sidlist, rev_sidlist, # Set the measurement loss mode request.reflector_options.measurement_loss_mode = \ loss_measurement_mode # pylint: disable=no-member + # ######################################################################## # Start the experiment on the reflector and return the response return stub.startExperimentReflector(request=request) def stop_experiment_reflector(channel, sidlist): ''' - RPC used to stop an experiment on the reflector + RPC used to stop an experiment on the reflector. + + :param channel: A gRPC channel to the reflector. + :type channel: class: `grpc._channel.Channel` + :param sidlist: The SID list of the path under test. + :type sidlist: list ''' # Get the reference of the stub stub = srv6pmService_pb2_grpc.SRv6PMStub(channel) @@ -493,8 +649,6 @@ def stop_experiment_reflector(channel, sidlist): def __start_measurement(measure_id, sender_channel, reflector_channel, send_refl_sidlist, refl_send_sidlist, - # send_in_interfaces, refl_in_interfaces, - # send_out_interfaces, refl_out_interfaces, measurement_protocol, measurement_type, authentication_mode, authentication_key, timestamp_format, delay_measurement_mode, @@ -534,7 +688,6 @@ def __start_measurement(measure_id, sender_channel, reflector_channel, or Direct mode) :type loss_measurement_mode: str ''' - # # pylint: disable=too-many-arguments, unused-argument # print("\n************** Start Measurement **************\n") @@ -543,8 +696,6 @@ def __start_measurement(measure_id, sender_channel, reflector_channel, channel=reflector_channel, sidlist=send_refl_sidlist, rev_sidlist=refl_send_sidlist, - # in_interfaces=refl_in_interfaces, - # out_interfaces=refl_out_interfaces, measurement_protocol=measurement_protocol, measurement_type=measurement_type, authentication_mode=authentication_mode, @@ -558,8 +709,6 @@ def __start_measurement(measure_id, sender_channel, reflector_channel, failure_msg='Error in start_experiment_reflector()' ) # Check for errors - if refl_res is None: - return commons_pb2.STATUS_INTERNAL_ERROR if refl_res.status != commons_pb2.STATUS_SUCCESS: return refl_res.status # Start the experiment on the sender @@ -567,8 +716,6 @@ def __start_measurement(measure_id, sender_channel, reflector_channel, channel=sender_channel, sidlist=send_refl_sidlist, rev_sidlist=refl_send_sidlist, - # in_interfaces=send_in_interfaces, - # out_interfaces=send_out_interfaces, measurement_protocol=measurement_protocol, measurement_type=measurement_type, authentication_mode=authentication_mode, @@ -585,8 +732,6 @@ def __start_measurement(measure_id, sender_channel, reflector_channel, failure_msg='Error in start_experiment_sender()' ) # Check for errors - if sender_res is None: - return commons_pb2.STATUS_INTERNAL_ERROR if sender_res.status != commons_pb2.STATUS_SUCCESS: return sender_res.status # Success @@ -606,6 +751,10 @@ def __get_measurement_results(sender_channel, reflector_channel, :type send_refl_sidlist: list :param refl_send_sidlist: The SID list used for reflector->sender path :type refl_send_sidlist: list + :raises controller.utils.NoMeasurementDataAvailableError: If an error + occurred while + retrieving the + results. ''' # pylint: disable=unused-argument # @@ -623,23 +772,24 @@ def __get_measurement_results(sender_channel, reflector_channel, failure_msg='Error in retrieve_experiment_results_sender()' ) # Collect the results - res = None - if sender_res.status == commons_pb2.STATUS_SUCCESS: - res = list() - for data in sender_res.measurement_data: - res.append({ - 'measure_id': data.meas_id, - 'interval': data.interval, - 'timestamp': data.timestamp, - 'fw_color': data.fwColor, - 'rv_color': data.rvColor, - 'sender_seq_num': data.ssSeqNum, - 'reflector_seq_num': data.rfSeqNum, - 'sender_tx_counter': data.ssTxCounter, - 'sender_rx_counter': data.ssRxCounter, - 'reflector_tx_counter': data.rfTxCounter, - 'reflector_rx_counter': data.rfRxCounter, - }) + if sender_res.status != commons_pb2.STATUS_SUCCESS: + logger.warning('No measurement data available') + raise utils.NoMeasurementDataAvailableError + res = list() + for data in sender_res.measurement_data: + res.append({ + 'measure_id': data.meas_id, + 'interval': data.interval, + 'timestamp': data.timestamp, + 'fw_color': data.fwColor, + 'rv_color': data.rvColor, + 'sender_seq_num': data.ssSeqNum, + 'reflector_seq_num': data.rfSeqNum, + 'sender_tx_counter': data.ssTxCounter, + 'sender_rx_counter': data.ssRxCounter, + 'reflector_tx_counter': data.rfTxCounter, + 'reflector_rx_counter': data.rfRxCounter, + }) # Return the results return res @@ -660,7 +810,6 @@ def __stop_measurement(sender_channel, reflector_channel, reflector->sender :type refl_send_sidlist: list ''' - # print("\n************** Stop Measurement **************\n") # Stop the experiment on the sender sender_res = stop_experiment_sender( @@ -699,7 +848,7 @@ def set_configuration(sender_channel, reflector_channel, interval_duration, delay_margin, number_of_color, pm_driver): ''' - Set the configuration + Set the configuration for a SRv6 Performance Measurement experiment. :param sender_channel: The gRPC Channel to the sender :type sender_channel: class: `grpc._channel.Channel` @@ -715,8 +864,10 @@ def set_configuration(sender_channel, reflector_channel, :type delay_margin: int :param number_of_color: The number of the color :type number_of_color: int + :param pm_driver: The driver to use for the experiments (i.e. eBPF or + IPSet). + :type pm_driver: str ''' - # # pylint: disable=too-many-arguments # # Set configuration on the sender @@ -751,24 +902,13 @@ def set_configuration(sender_channel, reflector_channel, def reset_configuration(sender_channel, reflector_channel): ''' - Reset the configuration + Reset the configuration for a SRv6 Performance Measurement experiment. :param sender_channel: The gRPC Channel to the sender node :type sender_channel: class: `grpc._channel.Channel` :param reflector_channel: The gRPC Channel to the reflector node :type reflector_channel: class: `grpc._channel.Channel` - :param send_dst_udp_port: The destination UDP port used by the sender - :type send_dst_udp_port: int - :param refl_dst_udp_port: The destination UDP port used by the reflector - :type refl_dst_udp_port: int - :param interval_duration: The duration of the interval - :type interval_duration: int - :param delay_margin: The delay margin - :type delay_margin: int - :param number_of_color: The number of the color - :type number_of_color: int ''' - # # Reset configuration on the sender res = reset_node_configuration( channel=sender_channel @@ -789,8 +929,6 @@ def reset_configuration(sender_channel, reflector_channel): def start_experiment(sender_channel, reflector_channel, send_refl_dest, refl_send_dest, send_refl_sidlist, refl_send_sidlist, - # send_in_interfaces, refl_in_interfaces, - # send_out_interfaces, refl_out_interfaces, measurement_protocol, measurement_type, authentication_mode, authentication_key, timestamp_format, delay_measurement_mode, @@ -855,7 +993,6 @@ def start_experiment(sender_channel, reflector_channel, send_refl_dest, path is replaced with the new one (default is False). :type force: bool, optional ''' - # # pylint: disable=too-many-arguments, too-many-locals # # If the force flag is set and SRv6 path already exists, remove @@ -894,10 +1031,6 @@ def start_experiment(sender_channel, reflector_channel, send_refl_dest, reflector_channel=reflector_channel, send_refl_sidlist=send_refl_sidlist, refl_send_sidlist=refl_send_sidlist, - # send_in_interfaces=send_in_interfaces, - # send_out_interfaces=send_out_interfaces, - # refl_in_interfaces=refl_in_interfaces, - # refl_out_interfaces=refl_out_interfaces, measurement_protocol=measurement_protocol, measurement_type=measurement_type, authentication_mode=authentication_mode, @@ -932,8 +1065,11 @@ def get_experiment_results(sender_channel, reflector_channel, :type refl_send_sidlist: list :param kafka_servers: IP:port of Kafka server :type kafka_servers: str + :raises controller.utils.NoMeasurementDataAvailableError: If an error + occurred while + retrieving the + results. ''' - # # pylint: disable=too-many-arguments, too-many-locals # # Get the results @@ -943,9 +1079,6 @@ def get_experiment_results(sender_channel, reflector_channel, send_refl_sidlist=send_refl_sidlist, refl_send_sidlist=refl_send_sidlist ) - if results is None: - print('No measurement data available') - return None # Publish results to Kafka if ENABLE_KAFKA_INTEGRATION: for res in results: @@ -960,7 +1093,7 @@ def get_experiment_results(sender_channel, reflector_channel, sender_rx_counter = res['sender_rx_counter'] reflector_tx_counter = res['reflector_tx_counter'] reflector_rx_counter = res['reflector_rx_counter'] - # Publish data to Kafka + # Publish data to Kafka topic "TOPIC_TWAMP" publish_to_kafka( bootstrap_servers=kafka_servers, topic=TOPIC_TWAMP, @@ -1015,7 +1148,6 @@ def stop_experiment(sender_channel, reflector_channel, send_refl_dest, End.DT6 route is not removed. :type refl_send_localseg: str, optional ''' - # # pylint: disable=too-many-arguments # # Stop the experiment @@ -1044,164 +1176,175 @@ def stop_experiment(sender_channel, reflector_channel, send_refl_dest, return commons_pb2.STATUS_SUCCESS -if ENABLE_GRPC_SERVER: - class _SRv6PMService( - srv6pmServiceController_pb2_grpc.SRv6PMControllerServicer): +class _SRv6PMService( + srv6pmServiceController_pb2_grpc.SRv6PMControllerServicer): + ''' + Private class implementing methods exposed by the gRPC server + ''' + + def __init__(self, kafka_servers=KAFKA_SERVERS): + self.kafka_servers = kafka_servers + + def SendMeasurementData(self, request, context): ''' - Private class implementing methods exposed by the gRPC server + This RPC is invoked by a node to send measurement data to the + controller. ''' + # pylint: disable=too-many-locals + # + # Request received + logger.debug('Measurement data received: %s', request) + # Extract data from the request + for data in request.measurement_data: + measure_id = data.meas_id + interval = data.interval + timestamp = data.timestamp + fw_color = data.fwColor + rv_color = data.rvColor + sender_seq_num = data.ssSeqNum + reflector_seq_num = data.rfSeqNum + sender_tx_counter = data.ssTxCounter + sender_rx_counter = data.ssRxCounter + reflector_tx_counter = data.rfTxCounter + reflector_rx_counter = data.rfRxCounter + # Publish data to Kafka topic "TOPIC_TWAMP" + if ENABLE_KAFKA_INTEGRATION: + publish_to_kafka( + bootstrap_servers=self.kafka_servers, + topic=TOPIC_TWAMP, + measure_id=measure_id, + interval=interval, + timestamp=timestamp, + fw_color=fw_color, + rv_color=rv_color, + sender_seq_num=sender_seq_num, + reflector_seq_num=reflector_seq_num, + sender_tx_counter=sender_tx_counter, + sender_rx_counter=sender_rx_counter, + reflector_tx_counter=reflector_tx_counter, + reflector_rx_counter=reflector_rx_counter + ) + # Done, send "OK" to the node + status = commons_pb2.StatusCode.Value('STATUS_SUCCESS') + return srv6pmServiceController_pb2.SendMeasurementDataResponse( + status=status) - def __init__(self, kafka_servers=KAFKA_SERVERS): - self.kafka_servers = kafka_servers - - def SendMeasurementData(self, request, context): - ''' - RPC used to send measurement data to the controller - ''' - # pylint: disable=too-many-locals - # - logger.debug('Measurement data received: %s', request) - # Extract data from the request - for data in request.measurement_data: - measure_id = data.meas_id - interval = data.interval - timestamp = data.timestamp - fw_color = data.fwColor - rv_color = data.rvColor - sender_seq_num = data.ssSeqNum - reflector_seq_num = data.rfSeqNum - sender_tx_counter = data.ssTxCounter - sender_rx_counter = data.ssRxCounter - reflector_tx_counter = data.rfTxCounter - reflector_rx_counter = data.rfRxCounter - # Publish data to Kafka - if ENABLE_KAFKA_INTEGRATION: - publish_to_kafka( - bootstrap_servers=self.kafka_servers, - topic=TOPIC_TWAMP, - measure_id=measure_id, - interval=interval, - timestamp=timestamp, - fw_color=fw_color, - rv_color=rv_color, - sender_seq_num=sender_seq_num, - reflector_seq_num=reflector_seq_num, - sender_tx_counter=sender_tx_counter, - sender_rx_counter=sender_rx_counter, - reflector_tx_counter=reflector_tx_counter, - reflector_rx_counter=reflector_rx_counter - ) - status = commons_pb2.StatusCode.Value('STATUS_SUCCESS') - return srv6pmServiceController_pb2.SendMeasurementDataResponse( - status=status) - - def SendIperfData(self, request, context): - ''' - RPC used to send iperf data to the controller - ''' - # - # pylint: disable=too-many-locals - # - logger.debug('Iperf data received: %s', request) - # Extract data from the request - for data in request.iperf_data: - # From client/server - _from = data._from # pylint: disable=protected-access - # Measure ID - measure_id = data.measure_id - # Generator ID - generator_id = data.generator_id - # Interval - interval = data.interval.val - # Transfer - transfer = data.transfer.val - transfer_dim = data.transfer.dim - # Bitrate - bitrate = data.bitrate.val - bitrate_dim = data.bitrate.dim - # Retr - retr = data.retr.val - # Cwnd - cwnd = data.cwnd.val - cwnd_dim = data.cwnd.dim - # Publish data to Kafka - if ENABLE_KAFKA_INTEGRATION: - publish_iperf_data_to_kafka( - bootstrap_servers=self.kafka_servers, - topic=TOPIC_IPERF, - _from=_from, - measure_id=measure_id, - generator_id=generator_id, - interval=interval, - transfer=transfer, - transfer_dim=transfer_dim, - bitrate=bitrate, - bitrate_dim=bitrate_dim, - retr=retr, - cwnd=cwnd, - cwnd_dim=cwnd_dim, - ) - status = commons_pb2.StatusCode.Value('STATUS_SUCCESS') - return srv6pmServiceController_pb2.SendIperfDataResponse( - status=status) - - -if ENABLE_GRPC_SERVER: - def __start_grpc_server(grpc_ip=DEFAULT_GRPC_SERVER_IP, - grpc_port=DEFAULT_GRPC_SERVER_PORT, - secure=DEFAULT_SERVER_SECURE, - key=DEFAULT_SERVER_KEY, - certificate=DEFAULT_SERVER_CERTIFICATE): + def SendIperfData(self, request, context): ''' - Start gRPC on the controller - - :param grpc_ip: The IP address of the gRPC server - :type grpc_ip: str - :param grpc_port: the port of the gRPC server - :type grpc_port: int - :param secure: define whether to use SSL or not for the gRPC server - (default is False) - :type secure: bool - :param certificate: The path of the server certificate required - for the SSL (default is None) - :type certificate: str - :param key: the path of the server key required for the SSL - (default is None) - :type key: str + This RPC is invoked by a node to send iperf data to the + controller. ''' + # pylint: disable=too-many-locals # - # pylint: disable=too-many-arguments - # - # Setup gRPC server - # - # Create the server and add the handler - grpc_server = grpc.server(futures.ThreadPoolExecutor()) - (srv6pmServiceController_pb2_grpc - .add_SRv6PMControllerServicer_to_server(_SRv6PMService(), - grpc_server)) - # If secure mode is enabled, we need to create a secure endpoint - if secure: - # Read key and certificate - with open(key) as key_file: - key = key_file.read() - with open(certificate) as certificate_file: - certificate = certificate_file.read() - # Create server SSL credentials - grpc_server_credentials = grpc.ssl_server_credentials( - ((key, certificate,),) - ) - # Create a secure endpoint - grpc_server.add_secure_port( - '[%s]:%s' % (grpc_ip, grpc_port), - grpc_server_credentials - ) - else: - # Create an insecure endpoint - grpc_server.add_insecure_port( - '[%s]:%s' % (grpc_ip, grpc_port) - ) - # Start the loop for gRPC - logger.info('Listening gRPC') - grpc_server.start() - while True: - time.sleep(5) + # Request received + logger.debug('Iperf data received: %s', request) + # Extract data from the request + for data in request.iperf_data: + # From client/server + _from = data._from # pylint: disable=protected-access + # Measure ID + measure_id = data.measure_id + # Generator ID + generator_id = data.generator_id + # Interval + interval = data.interval.val + # Transfer + transfer = data.transfer.val + transfer_dim = data.transfer.dim + # Bitrate + bitrate = data.bitrate.val + bitrate_dim = data.bitrate.dim + # Retr + retr = data.retr.val + # Cwnd + cwnd = data.cwnd.val + cwnd_dim = data.cwnd.dim + # Publish data to Kafka topic "TOPIC_IPERF" + if ENABLE_KAFKA_INTEGRATION: + publish_iperf_data_to_kafka( + bootstrap_servers=self.kafka_servers, + topic=TOPIC_IPERF, + _from=_from, + measure_id=measure_id, + generator_id=generator_id, + interval=interval, + transfer=transfer, + transfer_dim=transfer_dim, + bitrate=bitrate, + bitrate_dim=bitrate_dim, + retr=retr, + cwnd=cwnd, + cwnd_dim=cwnd_dim, + ) + # Done, send "OK" to the node + status = commons_pb2.StatusCode.Value('STATUS_SUCCESS') + return srv6pmServiceController_pb2.SendIperfDataResponse( + status=status) + + +def __start_grpc_server(grpc_ip=DEFAULT_GRPC_SERVER_IP, + grpc_port=DEFAULT_GRPC_SERVER_PORT, + secure=DEFAULT_SERVER_SECURE, + key=DEFAULT_SERVER_KEY, + certificate=DEFAULT_SERVER_CERTIFICATE): + ''' + Start gRPC server on the controller. + + :param grpc_ip: The IP address of the gRPC server. + :type grpc_ip: str + :param grpc_port: The port of the gRPC server. + :type grpc_port: int + :param secure: define whether to use SSL or not for the gRPC server + (default is False). + :type secure: bool + :param certificate: The path of the server certificate required + for the SSL (default is None). + :type certificate: str + :param key: the path of the server key required for the SSL + (default is None). + :type key: str + :raises controller.utils.InvalidArgumentError: If gRPC server is disabled + in the configuration. + ''' + # pylint: disable=too-many-arguments + # + # To start a gRPC server on the Controller, ENABLE_GRPC_SERVER must be + # True + if not ENABLE_GRPC_SERVER: + logger.error('gRPC server is disabled. Check your configuration.') + raise utils.InvalidArgumentError + # ######################################################################## + # Setup gRPC server + # + # Create the server and add the handler + grpc_server = grpc.server(futures.ThreadPoolExecutor()) + (srv6pmServiceController_pb2_grpc + .add_SRv6PMControllerServicer_to_server(_SRv6PMService(), + grpc_server)) + # If secure mode is enabled, we need to create a secure endpoint + if secure: + # Read key and certificate + with open(key) as key_file: + key = key_file.read() + with open(certificate) as certificate_file: + certificate = certificate_file.read() + # Create server SSL credentials + grpc_server_credentials = grpc.ssl_server_credentials( + ((key, certificate,),) + ) + # Create a secure endpoint + grpc_server.add_secure_port( + '[%s]:%s' % (grpc_ip, grpc_port), + grpc_server_credentials + ) + else: + # Create an insecure endpoint + grpc_server.add_insecure_port( + '[%s]:%s' % (grpc_ip, grpc_port) + ) + # ########################################################################### + # Start the loop for gRPC + logger.info('Listening gRPC') + grpc_server.start() + while True: + time.sleep(5) diff --git a/control_plane/controller/controller/srv6_usid.py b/control_plane/controller/controller/srv6_usid.py index 45abfae..a974d0e 100644 --- a/control_plane/controller/controller/srv6_usid.py +++ b/control_plane/controller/controller/srv6_usid.py @@ -35,6 +35,7 @@ import os from ipaddress import IPv6Address +# pyaml dependencies from pyaml import yaml # Proto dependencies @@ -42,21 +43,29 @@ # Controller dependencies from controller import srv6_utils from controller import utils + +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + +# Optional imports: +# arangodb_driver - only required to read/write the topology from/to +# a ArangoDB database try: - from controller import arangodb_driver + from controller.db_utils.arangodb import arangodb_driver except ImportError: - print('ArangoDB modules not installed') + logger.warning('ArangoDB modules not installed') + # Global variables definition # # -# Logger reference -logging.basicConfig(level=logging.NOTSET) -logger = logging.getLogger(__name__) # Default number of bits for the SID Locator DEFAULT_LOCATOR_BITS = 32 # Default number of bits for the uSID identifier DEFAULT_USID_ID_BITS = 16 +# Supported forwarding engines +SUPPORTED_FWD_ENGINES = ('Linux', 'VPP', 'P4') class InvalidConfigurationError(srv6_utils.SRv6Exception): @@ -93,57 +102,24 @@ def print_nodes(nodes_dict): ''' Print the nodes. - :param nodes_dict: Dict containing the nodes + :param nodes_dict: Dict containing the nodes. :type nodes_dict: dict ''' print(list(nodes_dict.keys())) -def print_node_to_addr_mapping(nodes_filename): +def print_nodes_from_config_file(nodes_filename): ''' - This function reads a YAML file containing the mapping - of node names to IP addresses and pretty print it + This function reads a YAML file containing the nodes configuration and + print the available nodes. - :param node_to_addr_filename: Name of the YAML file containing the - mapping of node names to IP addresses + :param node_to_addr_filename: Name of the YAML file containing the mapping + of node names to IP addresses. :type node_to_addr_filename: str ''' - # Read the mapping from the file - with open(nodes_filename, 'r') as nodes_file: - nodes = yaml.safe_load(nodes_file) - # Validate the IP addresses - for addr in [node['grpc_ip'] for node in nodes['nodes'].values()]: - if not utils.validate_ipv6_address(addr): - logger.error('Invalid IPv6 address %s in %s', - addr, nodes_filename) - raise InvalidConfigurationError - # Validate the SIDs - for sid in [node['uN'] for node in nodes['nodes'].values()]: - if not utils.validate_ipv6_address(sid): - logger.error('Invalid SID %s in %s', - sid, nodes_filename) - raise InvalidConfigurationError - # Validate the forwarding engine - for fwd_engine in [node['fwd_engine'] for node in nodes['nodes'].values()]: - if fwd_engine not in ['Linux', 'VPP', 'P4']: - logger.error('Invalid forwarding engine %s in %s', - fwd_engine, nodes_filename) - raise InvalidConfigurationError - # Get the #bits of the locator - locator_bits = nodes.get('locator_bits') - # Validate #bits for the SID Locator - if locator_bits is not None and \ - (int(locator_bits) < 0 or int(locator_bits) > 128): - raise InvalidConfigurationError - # Get the #bits of the uSID identifier - usid_id_bits = nodes.get('usid_id_bits') - # Validate #bits for the uSID ID - if usid_id_bits is not None and \ - (int(usid_id_bits) < 0 or int(usid_id_bits) > 128): - raise InvalidConfigurationError - if locator_bits is not None and usid_id_bits is not None and \ - int(usid_id_bits) + int(locator_bits) > 128: - raise InvalidConfigurationError + # Read the nodes from the configuration file + nodes = read_nodes(nodes_filename) + # Print the nodes available print('\nList of available devices:') pprint.PrettyPrinter(indent=4).pprint(list(nodes['nodes'].keys())) print() @@ -151,45 +127,51 @@ def print_node_to_addr_mapping(nodes_filename): def read_nodes(nodes_filename): ''' - Convert a list of node names into a list of IP addresses. + This function reads a YAML file containing the nodes configuration and + return the available nodes. - :param nodes_filename: Name of the YAML file containing the - IP addresses + :param nodes_filename: Name of the YAML file containing the nodes + configuration. :type nodes_filename: str - :return: Tuple (List of IP addresses, Locator bits, uSID ID bits) + :return: Tuple (List of IP addresses, Locator bits, uSID ID bits). :rtype: tuple - :raises NodeNotFoundError: Node name not found in the mapping file - :raises InvalidConfigurationError: The mapping file is not a valid - YAML file + :raises NodeNotFoundError: Node name not found in the mapping file. + :raises InvalidConfigurationError: The mapping file is not a valid YAML + file. ''' # Read the mapping from the file with open(nodes_filename, 'r') as nodes_file: nodes = yaml.safe_load(nodes_file) - # Validate the IP addresses - for addr in [node['grpc_ip'] for node in nodes['nodes'].values()]: - if not utils.validate_ipv6_address(addr): - logger.error('Invalid IPv6 address %s in %s', - addr, nodes_filename) + # Validation checks + # Iterate on the nodes + for node in nodes['nodes'].values(): + # Validate the IP address + if not utils.validate_ipv6_address(node['grpc_ip']): + logger.error('Invalid IPv6 address %s in %s', node['grpc_ip'], + nodes_filename) raise InvalidConfigurationError - # Validate the SIDs - for sid in [node['uN'] for node in nodes['nodes'].values()]: - if not utils.validate_ipv6_address(sid): - logger.error('Invalid SID %s in %s', - sid, nodes_filename) + # Validate the SID + if not utils.validate_ipv6_address(node['uN']): + logger.error('Invalid SID %s in %s', node['uN'], nodes_filename) raise InvalidConfigurationError - # Validate the forwarding engine - for fwd_engine in [node['fwd_engine'] for node in nodes['nodes'].values()]: - if fwd_engine not in ['Linux', 'VPP', 'P4']: + # Validate the forwarding engine + if node['fwd_engine'] not in SUPPORTED_FWD_ENGINES: logger.error('Invalid forwarding engine %s in %s', - fwd_engine, nodes_filename) + node['fwd_engine'], nodes_filename) raise InvalidConfigurationError + # Validation checks passed + # # Get the #bits of the locator + # This parameter is optional and may be omitted in the nodes configuration + # file locator_bits = nodes.get('locator_bits') # Validate #bits for the SID Locator if locator_bits is not None and \ (int(locator_bits) < 0 or int(locator_bits) > 128): raise InvalidConfigurationError # Get the #bits of the uSID identifier + # This parameter is optional and may be omitted in the nodes configuration + # file usid_id_bits = nodes.get('usid_id_bits') # Validate #bits for the uSID ID if usid_id_bits is not None and \ @@ -202,46 +184,78 @@ def read_nodes(nodes_filename): for node in nodes['nodes'].values(): nodes['nodes'][node['name']]['grpc_ip'] = node['grpc_ip'].lower() nodes['nodes'][node['name']]['uN'] = node['uN'].lower() - # Return the nodes list + # Return the nodes list, the #bits for the locator and the #bits for the + # uSID identifier return nodes['nodes'], locator_bits, usid_id_bits +def get_locator_mask(locator_bits): + ''' + Return the locator mask. + + :param locator_bits: The number of bits of the locator. + :type locator_bits: int + :return: The locator mask. + :rtype: str + ''' + # It is computed with a binary manipulation + # We start from the IPv6 address 111...11111 (all "1"), then we put to + # zero the bits of the non-locator part + # The remaining part is the locator, which is converted to an IPv6Address + locator_mask = str(IPv6Address(int('1' * 128, 2) ^ + int('1' * (128 - locator_bits), 2))) + # Done, return the locator mask + return locator_mask + + +def get_usid_id_mask(locator_bits, usid_id_bits): + ''' + Return the uSID identifier mask. + + :param locator_bits: The number of bits of the locator. + :type locator_bits: int + :param usid_id_bits: The number of bits of the uSID identifier. + :type usid_id_bits: int + :return: The locator mask. + :rtype: str + ''' + # It is computed with a binary manipulation + # We start from the IPv6 address 00...000111...11111 (#usid_id_bits of "1" + # in the less significant part of the address, the remaining bits set to + # "0"), then we perform a shift operation to move the "1" to the position + # corresponding to the uSID identifier part + usid_id_mask = str(IPv6Address(int('1' * usid_id_bits, 2) << + (128 - locator_bits - usid_id_bits))) + # Done, return + return usid_id_mask + + def segments_to_micro_segment(locator, segments, locator_bits=DEFAULT_LOCATOR_BITS, usid_id_bits=DEFAULT_USID_ID_BITS): ''' - Convert a SID list (with #segments <= 6) into a uSID. + Convert a SID list into a uSID. The uSID must have enough space to encode + all the segments that you want to insert. If not, an exception is raised. - :param locator: The SID Locator of the segments. - All the segments must use the same SID Locator. + :param locator: The SID Locator of the segments. All the segments must use + the same SID Locator. :type locator: str - :param segments: The SID List to be compressed + :param segments: The SID List to be compressed. :type segments: list - :param locator_bits: Number of bits of the locator part of the SIDs + :param locator_bits: Number of bits of the locator part of the SIDs. :type locator_bits: int - :param usid_id_bits: Number of bits of the uSID identifiers + :param usid_id_bits: Number of bits of the uSID identifiers. :type usid_id_bits: int - :return: The uSID containing all the segments + :return: The uSID containing all the segments. :rtype: str - :raises TooManySegmentsError: segments arg contains too many segments - :raises SIDLocatorError: SID Locator is wrong for one or more segments - :raises InvalidSIDError: SID is wrong for one or more segments + :raises TooManySegmentsError: segments arg contains too many segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. + :raises InvalidSIDError: SID is wrong for one or more segments. ''' # Locator mask, used to extract the locator from the SIDs - # - # It is computed with a binary manipulation - # We start from the IPv6 address 111...11111, then we put to zero - # the bits of the non-locator part - # The remaining part is the locator, which is converted to an IPv6Address - locator_mask = str(IPv6Address(int('1' * 128, 2) ^ - int('1' * (128 - locator_bits), 2))) + locator_mask = get_locator_mask(locator_bits) # uSID identifier mask - # - # It is computed with a binary manipulation - # We start from the IPv6 address 111...11111, then we perform a shift - # operation - usid_id_mask = str(IPv6Address(int('1' * usid_id_bits, 2) << - (128 - locator_bits - usid_id_bits))) + usid_id_mask = get_usid_id_mask(locator_bits, usid_id_bits) # Enforce case-sensitivity locator = locator.lower() _segments = list() @@ -249,39 +263,64 @@ def segments_to_micro_segment(locator, segments, _segments.append(segment.lower()) segments = _segments # Validation check - # We need to verify if there is space in the uSID for all the segments + # We need to verify if there is enough space in the uSID for all the + # segments + # The space available to store the segments is the non-locator part of an + # IPv6 address; each segment is encoded with usid_id_bits + # Therefore, the maximum number of segments that we are able to encode in + # one uSID is computed as follows: + # + # | 128 - locator_bits | + # | ------------------ | + # |_ usid_id_bits _| + # if len(segments) > math.floor((128 - locator_bits) / usid_id_bits): logger.error('Too many segments') raise TooManySegmentsError + # Build the uSID, encoded as an integer + # # uSIDs always start with the SID Locator usid_int = int(IPv6Address(locator)) - # Offset of the uSIDs + # Offset of the uSID identifiers, used to put the uSID identifiers to the + # right position in the uSID offset = 0 # Iterate on the segments for segment in segments: # Split the segment in... # ...segment locator + # The segment locator is obtained as an "and" between the segment and + # the locator mask that we have computed previously segment_locator = \ str(IPv6Address(int(IPv6Address(locator_mask)) & int(IPv6Address(segment)))) + # We already know the locator, because it is passed as argument to + # this function; we only need to check that all the segments have + # the correct segment locator + # If we found a segment with a different locator, we raise an + # exception if locator != segment_locator: # All the segments must have the same Locator logger.error('Wrong locator for the SID %s', ''.join(segment)) raise SIDLocatorError # ...and uSID identifier + # The uSID identifier is obtained as an "and" between the segment and + # the uSID identifier mask that we have computed previously usid_id = \ str(IPv6Address(int(IPv6Address(usid_id_mask)) & int(IPv6Address(segment)))) # Other bits should be equal to zero + # If not, the segment is invalid and we raise an exception if int(IPv6Address(segment)) & ( 0b1 * (128 - locator_bits - usid_id_bits)) != 0: # The SID is invalid logger.error('SID %s is invalid. Final bits should be zero', segment) raise InvalidSIDError - # And append to the uSID + # Finally, append to the uSID identifier to the uSID, after shifting + # it to the right position in the uSID usid_int += int(IPv6Address(usid_id)) >> offset - # Increase offset + # Increase offset to take into account the uSID identifier that we + # added to the uSID offset += usid_id_bits # Get a string representation of the uSID usid = str(IPv6Address(usid_int)) @@ -291,40 +330,42 @@ def segments_to_micro_segment(locator, segments, def get_sid_locator(sid_list, locator_bits=DEFAULT_LOCATOR_BITS): ''' - Get the SID Locator (i.e. the first 32 bits) from a SID List. + Get the SID Locator from a SID List. By default, SID Locator part is 32 + bits long. You can change this behavior by setting the locator_bits + argument of this function. :param sid_list: SID List :type sid_list: list :param locator_bits: Number of bits of the locator part of the SIDs - :type locator_bits: int + (default: 32). + :type locator_bits: int, optional :return: SID Locator :rtype: str - :raises SIDLocatorError: SID Locator is wrong for one or more segments + :raises SIDLocatorError: SID Locator is wrong for one or more segments. ''' # Locator mask, used to extract the locator from the SIDs - # - # It is computed with a binary manipulation - # We start from the IPv6 address 111...11111, then we put to zero - # the bits of the non-locator part - # The remaining part is the locator, which is converted to an IPv6Address - locator_mask = str(IPv6Address(int('1' * 128, 2) ^ - int('1' * (128 - locator_bits), 2))) + locator_mask = get_locator_mask(locator_bits) # Enforce case-sensitivity _sid_list = list() for segment in sid_list: _sid_list.append(segment.lower()) sid_list = _sid_list - # Locator + # Build the locator locator = '' # Iterate on the SID list for segment in sid_list: - # Split the segment in... - # ...segment locator + # The segment locator is obtained as an "and" between the segment and + # the locator mask that we have computed previously segment_locator = \ str(IPv6Address(int(IPv6Address(locator_mask)) & int(IPv6Address(segment)))) + # We need to check that all the segments have the same segment locator + # If we found a segment with a different locator, we raise an + # exception if locator == '': - # Store the segment + # We don't have a locator yet because this is the first segment + # that we are processing + # Store the locator locator = segment_locator elif locator != segment_locator: # All the segments must have the same Locator @@ -338,24 +379,39 @@ def sidlist_to_usidlist(sid_list, udt_sids=None, locator_bits=DEFAULT_LOCATOR_BITS, usid_id_bits=DEFAULT_USID_ID_BITS): ''' - Convert a SID List into a uSID List. + Convert a SID List into a uSID List. SID List may contain any number of + segments. The number of the uSIDs returned by this function (i.e. the + length of the uSID list) depends on the length of the SID List. - :param sid_list: SID List to be converted + :param sid_list: SID List to be converted. :type sid_list: list + :param udt_sids: List of uDT SIDs. + :type udt_sids: list :param locator_bits: Number of bits of the locator part of the SIDs + (default: 32). :type locator_bits: int - :param usid_id_bits: Number of bits of the uSID identifiers + :param usid_id_bits: Number of bits of the uSID identifiers (default: 16). :type usid_id_bits: int - :return: uSID List containing + :return: uSID List containing. :rtype: list - :raises TooManySegmentsError: segments arg contains too many segments - :raises SIDLocatorError: SID Locator is wrong for one or more segments + :raises TooManySegmentsError: segments arg contains too many segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. ''' + # If udt_sids argument is not passed to this function, we initialize it to + # an empty list if udt_sids is None: udt_sids = list() - # Size of the group of SIDs to be compressed in one uSID - # The size depends on the locator bits and uSID ID bits - # Last slot should be always leaved free + # How many SIDs are we able to store in one uSID? + # The size of the group of SIDs to be compressed in one uSID depends on + # the locator bits and uSID ID bits + # The space available to store the segments is the non-locator part of an + # IPv6 address; each segment is encoded with usid_id_bits + # Last slot should be always free, so: + # + # | 128 - locator_bits | + # | ------------------- | - 1 + # |_ usid_id_bits _| + # sid_group_size = math.floor((128 - locator_bits) / usid_id_bits) - 1 # Get the locator locator = get_sid_locator(sid_list=sid_list, locator_bits=locator_bits) @@ -365,12 +421,19 @@ def sidlist_to_usidlist(sid_list, udt_sids=None, while len(sid_list) + len(udt_sids) > 0: # Extract the SIDs to be encoded in one uSID sids_group = sid_list[:sid_group_size] - # uDT list should not be broken into different SIDs + # uDT list cannot be broken into different SIDs + # All the uDT SIDs must put in the same uSID if len(sid_list) + len(udt_sids) <= sid_group_size: + # If there is enough space to store the uDT SIDs, we append them + # to group of SIDs to be encoded in the current uSID + # Else, we don't add them to the current uSID and we wait for a + # uSID that has more free space sids_group += udt_sids + # Since we have processed all the uDT SIDs, we empty the list udt_sids = [] - # Segments are encoded in groups of X - # Take the first X SIDs, build the uSID and add it to the uSID list + # Segments are encoded in groups of "sid_group_size" + # Take the first "sid_group_size" SIDs, build the uSID and add it to + # the uSID list usid_list.append( segments_to_micro_segment( locator=locator, @@ -379,44 +442,59 @@ def sidlist_to_usidlist(sid_list, udt_sids=None, usid_id_bits=usid_id_bits ) ) - # Advance SID list + # Advance SID list: drop the processed SIDs sid_list = sid_list[sid_group_size:] # Return the uSID list return usid_list -def nodes_to_micro_segments(nodes, node_addrs_filename): +def nodes_to_micro_segments(nodes, nodes_config_filename): ''' - Convert a list of nodes into a list of micro segments (uSID List) + Convert a list of nodes into a list of micro segments (uSID List). - :param nodes: List of node names + :param nodes: List of node names. :type node: list - :param node_to_addr_filename: Name of the YAML file containing the - mapping of node names to IP addresses - :type node_to_addr_filename: str - :return: uSID List + :param nodes_config_filename: Name of the YAML file containing the + configuration of the nodes. + :type nodes_config_filename: str + :return: uSID List. :rtype: list - :raises NodeNotFoundError: Node name not found in the mapping file - :raises InvalidConfigurationError: The mapping file is not a valid - YAML file - :raises TooManySegmentsError: segments arg contains more than 6 segments - :raises SIDLocatorError: SID Locator is wrong for one or more segments + :raises NodeNotFoundError: Node name not found in the mapping file. + :raises InvalidConfigurationError: The mapping file is not a valid YAML + file. + :raises TooManySegmentsError: segments arg contains more than 6 segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. ''' - - # Convert the list of nodes into a list of IP addresses (SID list) - # Translation is based on a file containing the mapping - # of node names to IP addresses - nodes_info, locator_bits, usid_id_bits = read_nodes(node_addrs_filename) + # First, convert the list of nodes into a list of IP addresses (SID list) + # Translation is based on a YAML file containing the configuration of the + # nodes + # Read the nodes configuration and extract a dict containing the + # attributes of the nodes, the number of bits of the locator part and the + # number of bits of the uSID identifier part + nodes_info, locator_bits, usid_id_bits = read_nodes(nodes_config_filename) + # We need to convert the list of node names passed as argument into a SID + # list; then the SID list will be converted to a uSID list + # + # Inizialize the SID list sid_list = list() + # Iterate on the nodes that we want include in the SID list for node in nodes: if node not in nodes_info: + # If the node does not figure in the configuration file, we don't + # know its SID and we cannot continue + # Raise an exception raise NodeNotFoundError + # Extract the SID of the node and add it to the SID list sid_list.append(nodes_info[node]['uN']) + # If "locator_bits" is not specified in the configuration file, we use the + # default value (i.e. 32 bits) if locator_bits is None: locator_bits = DEFAULT_LOCATOR_BITS + # If "usid_id_bits" is not specified in the configuration file, we use the + # default value (i.e. 32 bits) if usid_id_bits is None: usid_id_bits = DEFAULT_USID_ID_BITS - # Compress the SID list into a uSID list + # Now we are ready to convert the SID list into a uSID list usid_list = sidlist_to_usidlist( sid_list=sid_list, locator_bits=locator_bits, @@ -426,38 +504,47 @@ def nodes_to_micro_segments(nodes, node_addrs_filename): return usid_list -def validate_usid_id(usid_id): +def validate_usid_id(usid_id, usid_id_bits=DEFAULT_USID_ID_BITS): ''' - Validate a uSID identifier. A valid uSID id should be an integer in the - range (0, 0xffff). + Validate a uSID identifier. :param usid_id: uSID idenfier to validate. :type usid_id: str + :param usid_id_bits: The number of bits used to represent a uSID + identifier (default: 16). :return: True if the uSID identifier is valid. :rtype: bool ''' + # A valid uSID id should be an integer in the range + # (0, 2 ^ usid_id_bits - 1) + min_usid_id = 0 + max_usid_id = (2 ** usid_id_bits) - 1 try: - # A valid uSID id should be an integer in the range (0, 0xffff) - return int(usid_id, 16) >= 0x0 and int(usid_id, 16) <= 0xffff + # Check if the uSID identifier falls into the range + return usid_id >= min_usid_id and usid_id <= max_usid_id except ValueError: - # The uSID id is invalid + # The uSID id argument is invalid return False return True -def usid_id_to_usid(usid_id, locator): +def usid_id_to_usid(usid_id, locator, locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): ''' - Convert a uSID identifier into a SID. + Convert a uSID identifier into a uSID. - :param usid_id: uSID idenfier to convert. + :param usid_id: uSID identifier to convert. :type usid_id: str - :param locator: Locator part to be used for the SID. + :param locator: Locator part to be used for the uSID. :type locator: str - :return: Generated SID. + :return: Generated uSID. :rtype: str ''' + # Compute the offset for the uSID identifier + offset = 128 - locator_bits - usid_id_bits + # Build and return the uSID return str(IPv6Address(int(IPv6Address(locator)) + - (int(usid_id, 16) << 80))) + (int(usid_id, 16) << offset))) def encode_endpoint_node(node, grpc_ip, grpc_port, fwd_engine, locator, @@ -474,7 +561,7 @@ def encode_endpoint_node(node, grpc_ip, grpc_port, fwd_engine, locator, :param grpc_port: Port number of the gRPC server. :type grpc_port: int :param udt: uDT SID of the node, used for the decap operation. If not - provided, the uDT SID is not added to the SID list. + provided, no uDT SID will be associated to the node. :type udt: str, optional :param fwd_engine: Forwarding engine to be used (e.g. Linux or VPP). :type fwd_engine: str @@ -503,12 +590,13 @@ def encode_endpoint_node(node, grpc_ip, grpc_port, fwd_engine, locator, raise InvalidConfigurationError # Validate forwarding engine if fwd_engine is None: - logger.error('grpcfwd_engine_ip is mandatory for node %s', node) + logger.error('fwd_engine is mandatory for node %s', node) raise InvalidConfigurationError # Validate locator if locator is None: logger.error('locator is mandatory for node %s', node) raise InvalidConfigurationError + # All checks passed # # Compute uN SID starting from the provided node identifier # Node identifier can be expressed as SID (an IPv6 address) or a @@ -540,7 +628,7 @@ def encode_intermediate_node(node, locator): ''' Get a dict-representation of a node (intermediate node of the path), starting from gRPC IP and port, uDT sid, forwarding engine and locator. - For the intermediate nodes, we don't need uDT, forwarding engine. + For the intermediate nodes, we don't need uDT, forwarding engine, gRPC IP and gRPC address. :param node: Node identifier. This could be a name, a SID (IPv6 address) @@ -565,6 +653,7 @@ def encode_intermediate_node(node, locator): if locator is None: logger.error('locator is mandatory for node %s', node) raise InvalidConfigurationError + # All checks passed # # Compute uN SID starting from the provided node identifier # Node identifier can be expressed as SID (an IPv6 address) or a @@ -591,17 +680,17 @@ def encode_intermediate_node(node, locator): return None -def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, - l_fwd_engine=None, r_grpc_ip=None, r_grpc_port=None, - r_fwd_engine=None, decap_sid=None, locator=None): +def fill_nodes_config(nodes_config, nodes, l_grpc_ip=None, l_grpc_port=None, + l_fwd_engine=None, r_grpc_ip=None, r_grpc_port=None, + r_fwd_engine=None, decap_sid=None, locator=None): ''' - Fill 'nodes_info' dict with the nodes containined in the 'nodes' list. + Fill 'nodes_config' dict with the nodes containined in the 'nodes' list. - :param nodes_info: Dict containined the nodes information where to add the - nodes. - :type nodes_info: dict - :param nodes: List of nodes. Each node can be expressed as SID (IPv6 - address), a uSID identifier (integer) or a name. + :param nodes_config: Dict containing the nodes information to which the + nodes must be added to. + :type nodes_config: dict + :param nodes: List of nodes to add. Each node can be expressed as a SID + (IPv6 address), a uSID identifier (integer) or a name. :type nodes: list :param l_grpc_ip: gRPC address of the left node in the path. :type l_grpc_ip: str, optional @@ -609,7 +698,7 @@ def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, the path. :type l_grpc_port: str, optional :param l_fwd_engine: Forwarding engine to be used on the left node of - the path (e.g. Linux or VPP). + the path (e.g. Linux or VPP). :type l_fwd_engine: str, optional :param r_grpc_ip: gRPC address of the right node in the path. :type r_grpc_ip: str, optional @@ -617,7 +706,7 @@ def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, the path. :type r_grpc_port: str, optional :param r_fwd_engine: Forwarding engine to be used on the right node of - the path (e.g. Linux or VPP). + the path (e.g. Linux or VPP). :type r_fwd_engine: str, optional :param decap_sid: Decap SID. This could be a SID (IPv6 address) or a uSID identifier (an integer). @@ -643,8 +732,9 @@ def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, udt = decap_sid # Encode left node # - # A node could be expressed as an integer, an IPv6 address (SID) - # or a name + # A node could be expressed as an integer (uSID identifier), an IPv6 + # address (SID) or a name + # Process the node and get a dict representation node = encode_endpoint_node( node=nodes[0], grpc_ip=l_grpc_ip, @@ -654,13 +744,19 @@ def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, locator=locator ) # If we received a node info dict, we add it to the - # nodes info dictionary + # nodes config dictionary if node is not None: - nodes_info[nodes[0]] = node + nodes_config[nodes[0]] = node + else: + # Node is expressed as name; in this case we expect that it is already + # in the configuration + if node not in nodes_config: + raise utils.InvalidArgumentError # Encode right node # # A node could be expressed as an integer, an IPv6 address (SID) # or a name + # Process the node and get a dict representation node = encode_endpoint_node( node=nodes[-1], grpc_ip=r_grpc_ip, @@ -670,44 +766,777 @@ def fill_nodes_info(nodes_info, nodes, l_grpc_ip=None, l_grpc_port=None, locator=locator ) # If we received a node info dict, we add it to the - # nodes info dictionary + # nodes config dictionary if node is not None: - nodes_info[nodes[-1]] = node + nodes_config[nodes[-1]] = node + else: + # Node is expressed as name; in this case we expect that it is already + # in the configuration + if node not in nodes_config: + raise utils.InvalidArgumentError # Encode intermediate nodes # For the intermediate nodes, we don't need forwarding engine, # uDT, gRPC IP and port for node_name in nodes[1:-1]: - # Encode the node + # Process the node and get a dict representation node = encode_intermediate_node( node=node_name, locator=locator ) # If we received a node info dict, we add it to the - # nodes info dictionary + # nodes config dictionary if node is not None: - nodes_info[node_name] = node + nodes_config[node_name] = node + else: + # Node is expressed as name; in this case we expect that it is + # already in the configuration + if node not in nodes_config: + raise utils.InvalidArgumentError + + +def generate_bsid_addr(destination): + ''' + This function generates a BSID address for VPP forwarding engine. + + :param destination: The destination of the path related to the BSID + address. + :type destination: str + :return: The BSID address. + :rtype: str + ''' + # TODO: this function should be improved because some addresses could + # generate conflicts (e.g. fcff:1:: and fcff::1 return the same BSID + # address) + # + # Start from an empty string + bsid_addr = '' + # Iterate on the chars composing the destination address + for char in destination: + # Remove "0" and ":" chars from the BSID address + if char not in ('0', ':'): + bsid_addr += char + # If the last byte of the resulting BSID address is 0, the address must be + # terminated with "::" in order to be a valid IPv6 address; in this case, + # we set add_colon flag and then we add the "::" at the end of the address + add_colon = False + if len(bsid_addr) <= 28: + add_colon = True + # Separate the chars composing the BSID address in groups of 4 + bsid_addr = [(bsid_addr[i:i + 4]) + for i in range(0, len(bsid_addr), 4)] + # Join the groups in a single string by using a ":" to separate them + bsid_addr = ':'.join(bsid_addr) + # Add "::" if required, as established previously + if add_colon: + bsid_addr += '::' + # This results in a valid IPv6 address + return bsid_addr + + +def add_del_srv6_usid_policy_ingress(operation, grpc_ip, grpc_port, node_name, + fwd_engine, egress_udt, destination, + segments, table, metric, + locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): + ''' + Handle a SRv6 Policy using uSIDs on the ingress node. + + :param operation: The operation to be performed on the uSID policy + (i.e. add, get, change, del). + :type operation: str + :param grpc_ip: gRPC IP address of the left node, required if the left + node is expressed numerically in the nodes list. + :type grpc_ip: str + :param grpc_port: gRPC port of the left node, required if the left + node is expressed numerically in the nodes list. + :type grpc_port: str + :param node_name: The name of the ingress node. + :type node_name: str + :param fwd_engine: forwarding engine of the left node, required if the + left node is expressed numerically in the nodes list. + :type fwd_engine: str + :param egress_udt: uDT SID of the egress node. + :type egress_udt: str + :param destination: Destination of the SRv6 route. + :type destination: str + :param segments: Waypoints of the SRv6 route. + :type segments: list + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param locator_bits: Number of bits of the locator part of the SIDs + (default: 32). + :type locator_bits: int + :param usid_id_bits: Number of bits of the uSID identifiers (default: 16). + :type usid_id_bits: int + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). + :rtype: int + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + ''' + # Establish a gRPC channel to the ingress node + with utils.get_grpc_session(grpc_ip, grpc_port) as channel: + # The intermediate nodes can use Linux, VPP or P4 as + # forwarding engine; for the encap nodes, currently only + # Linux and VPP are suppoted + if fwd_engine not in ['Linux', 'VPP']: + logger.error( + 'Encap operation is not supported for ' + '%s with fwd engine %s', node_name, fwd_engine) + return commons_pb2.STATUS_INTERNAL_ERROR + # VPP policies and steering rules require a BSID address + # BSID address can be any IPv6 address + bsid_addr = (generate_bsid_addr(destination) + if fwd_engine == 'VPP' else '') + + udt_sids = list() + # Locator mask + locator_mask = get_locator_mask(locator_bits) + # uDT mask + udt_mask_1 = str( + IPv6Address(int('1' * usid_id_bits, 2) << + (128 - locator_bits - usid_id_bits))) + udt_mask_2 = str( + IPv6Address(int('1' * usid_id_bits, 2) << + (128 - locator_bits - 2 * usid_id_bits))) + # Build uDT sid list + locator_int = int(IPv6Address(egress_udt)) & \ + int(IPv6Address(locator_mask)) + udt_mask_1_int = int(IPv6Address(egress_udt)) & \ + int(IPv6Address(udt_mask_1)) + udt_mask_2_int = int(IPv6Address(egress_udt)) & \ + int(IPv6Address(udt_mask_2)) + udt_sids += [str(IPv6Address(locator_int + + udt_mask_1_int))] + udt_sids += [str(IPv6Address(locator_int + + (udt_mask_2_int << + usid_id_bits)))] + # We need to convert the SID list into a uSID list + # before creating the SRv6 policy + usid_list = sidlist_to_usidlist( + sid_list=segments[1:][:-1], + udt_sids=[segments[1:][-1]] + udt_sids, + locator_bits=locator_bits, + usid_id_bits=usid_id_bits + ) + # Handle a SRv6 path + response = srv6_utils.handle_srv6_path( + operation=operation, + channel=channel, + destination=destination, + segments=usid_list, + encapmode='encap.red', + table=table, + metric=metric, + bsid_addr=bsid_addr, + fwd_engine=fwd_engine + ) + # Done, return the response + return response + + +def get_srv6_usid_policy(lr_destination=None, rl_destination=None, + nodes_lr=None, nodes_rl=None, table=-1, metric=-1, + _id=None): + ''' + Search for SRv6 uSID policies saved to the database that match the + provided arguments. + + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str, optional + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str, optional + :param nodes_lr: Waypoints of the SRv6 route for the left to right path. + :type nodes_lr: list, optional + :param nodes_rl: Waypoints of the SRv6 route for the right to left path. + :type nodes_rl: list, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string, optional + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). + :rtype: int + :raises NodeNotFoundError: Node name not found in the mapping file. + :raises InvalidConfigurationError: The mapping file is not a valid + YAML file. + :raises TooManySegmentsError: segments arg contains more than 6 segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. + :raises InvalidSIDError: SID is wrong for one or more segments. + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + :raises controller.utils.PolicyNotFoundError: Policy not found. + ''' + # Extract the ArangoDB params from the environment variables + arango_url = os.getenv('ARANGO_URL') + arango_user = os.getenv('ARANGO_USER') + arango_password = os.getenv('ARANGO_PASSWORD') + # Controller persistency must be enabled to support the "get" + # operation + if os.getenv('ENABLE_PERSISTENCY') not in ['True', 'true']: + logger.error('Error in get(): Persistency is disabled') + raise utils.InvalidArgumentError + # Connect to ArangoDB + client = arangodb_driver.connect_arango( + url=arango_url) # TODO keep arango connection open + # Connect to the "srv6_usid" db + database = arangodb_driver.connect_srv6_usid_db( + client=client, + username=arango_user, + password=arango_password + ) + # Get the policy from the db + policies = arangodb_driver.find_usid_policy( + database=database, + key=_id, + lr_dst=lr_destination, + rl_dst=rl_destination, + lr_nodes=nodes_lr, + rl_nodes=nodes_rl, + table=table if table != -1 else None, + metric=metric if metric != -1 else None + ) + # Done, return + return list(policies) -def handle_srv6_usid_policy(operation, nodes_dict=None, +def add_del_srv6_usid_policy_fill_nodes_config( + operation, nodes_config=None, + lr_destination=None, + rl_destination=None, + nodes_lr=None, + nodes_rl=None, table=-1, + metric=-1, + _id=None, l_grpc_ip=None, + l_grpc_port=None, + l_fwd_engine=None, + r_grpc_ip=None, + r_grpc_port=None, + r_fwd_engine=None, + decap_sid=None, locator=None, + locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): + ''' + Add or delete SRv6 uSID Policy. + + :param operation: The operation to be performed on the uSID policy + (i.e. add, get, change, del). + :type operation: str + :param nodes_config: Dict containing the nodes configuration. + :type nodes_config: dict + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str + :param nodes_lr: Waypoints of the SRv6 route for the left to right path. + :type nodes_lr: list + :param nodes_rl: Waypoints of the SRv6 route for the right to left path. + :type nodes_rl: list + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string + :param l_grpc_ip: gRPC IP address of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_ip: str, optional + :param l_grpc_port: gRPC port of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_port: str, optional + :param l_fwd_engine: forwarding engine of the left node, required if the + left node is expressed numerically in the nodes list. + :type l_fwd_engine: str, optional + :param r_grpc_ip: gRPC IP address of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_ip: str, optional + :param r_grpc_port: gRPC port of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_port: str, optional + :param r_fwd_engine: Forwarding engine of the right node, required if the + right node is expressed numerically in the nodes + list. + :type r_fwd_engine: str, optional + :param decap_sid: uSID used for the decap behavior (End.DT6). + :type decap_sid: str, optional + :param locator: Locator prefix (e.g. 'fcbb:bbbb::'). + :type locator: str, optional + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). + :rtype: int + :raises NodeNotFoundError: Node name not found in the mapping file. + :raises InvalidConfigurationError: The mapping file is not a valid + YAML file. + :raises TooManySegmentsError: segments arg contains more than 6 segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. + :raises InvalidSIDError: SID is wrong for one or more segments. + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + :raises controller.utils.PolicyNotFoundError: Policy not found. + ''' + # The SID list may contain one or more nodes that are not in the nodes + # configuration dict; if one of these nodes is used as endpoint of the + # SRv6 path, the configuration must be specified by setting the arguments + # l_grpc_ip, l_grpc_port, l_fwd_engine, r_grpc_ip, r_grpc_port and + # r_fwd_engine + # + # We need to add the configuration of these nodes to the "nodes_config" + # dict + # + # Add nodes list for the left-to-right path to the 'nodes_config' dict + if nodes_lr is not None: + fill_nodes_config( + nodes_info=nodes_config, + nodes=nodes_lr, + l_grpc_ip=l_grpc_ip, + l_grpc_port=l_grpc_port, + l_fwd_engine=l_fwd_engine, + r_grpc_ip=r_grpc_ip, + r_grpc_port=r_grpc_port, + r_fwd_engine=r_fwd_engine, + decap_sid=decap_sid, + locator=locator + ) + # Add nodes list for the right-to-left path to the 'nodes_config' dict + if nodes_rl is not None: + fill_nodes_config( + nodes_info=nodes_config, + nodes=nodes_rl, + l_grpc_ip=r_grpc_ip, + l_grpc_port=r_grpc_port, + l_fwd_engine=r_fwd_engine, + r_grpc_ip=l_grpc_ip, + r_grpc_port=l_grpc_port, + r_fwd_engine=l_fwd_engine, + decap_sid=decap_sid, + locator=locator + ) + # Now all the nodes of our SRv6 path are contained in the nodes + # configuration + # We are ready to add/delete the policy + # + # We build a list of the policies to be added or removed + # + # For the "add" operation, "lr_destination", "rl_destination" and + # "nodes_lr" are passed as argument + # "nodes_rl" is an optional argument; if not provided, "nodes_rl" + # will be the reverse of "nodes_lr" (symmetric SRv6 path) + if operation == 'add': + policies = [{ + 'lr_dst': lr_destination, + 'rl_dst': rl_destination, + 'lr_nodes': nodes_lr, + 'rl_nodes': nodes_rl + }] + # For the "del" operation, we perform a lookup in the database to get + # the policies matching the provided arguments + if operation == 'del': + policies = get_srv6_usid_policy( + lr_destination=lr_destination, + rl_destination=rl_destination, + nodes_lr=nodes_lr, + nodes_rl=nodes_rl, + table=table if table != -1 else None, + metric=metric if metric != -1 else None, + _id=_id + ) + for policy in policies: + # For the policies stored to the db, the same rules apply as + # described above and reported here + # + # The SID list may contain one or more nodes that are not in the + # nodes configuration dict; if one of these nodes is used as + # endpoint of the SRv6 path, the configuration must be specified + # by setting the arguments l_grpc_ip, l_grpc_port, l_fwd_engine, + # r_grpc_ip, r_grpc_port and r_fwd_engine + # + # Add nodes list for the left-to-right path to the + # 'nodes_config' dict + if policy.get('lr_nodes') is not None: + fill_nodes_config( + nodes_info=nodes_config, + nodes=policy.get('lr_nodes'), + l_grpc_ip=policy.get('l_grpc_ip'), + l_grpc_port=policy.get('l_grpc_port'), + l_fwd_engine=policy.get('l_fwd_engine'), + r_grpc_ip=policy.get('r_grpc_ip'), + r_grpc_port=policy.get('r_grpc_port'), + r_fwd_engine=policy.get('r_fwd_engine'), + decap_sid=policy.get('decap_sid'), + locator=policy.get('locator') + ) + # Add nodes list for the right-to-left path to the + # 'nodes_info' dict + if policy.get('rl_nodes') is not None: + fill_nodes_config( + nodes_info=nodes_config, + nodes=policy.get('rl_nodes'), + l_grpc_ip=policy.get('r_grpc_ip'), + l_grpc_port=policy.get('r_grpc_port'), + l_fwd_engine=policy.get('r_fwd_engine'), + r_grpc_ip=policy.get('l_grpc_ip'), + r_grpc_port=policy.get('l_grpc_port'), + r_fwd_engine=policy.get('l_fwd_engine'), + decap_sid=policy.get('decap_sid'), + locator=policy.get('locator') + ) + # Check if you have at least one policy to add/delete + # If not, raise an exception + if len(policies) == 0: + logger.error('Policy not found') + raise utils.PolicyNotFoundError + + +def add_del_srv6_usid_policy(operation, policies, nodes_config=None, + lr_destination=None, rl_destination=None, + nodes_lr=None, nodes_rl=None, table=-1, + metric=-1, _id=None, l_grpc_ip=None, + l_grpc_port=None, l_fwd_engine=None, + r_grpc_ip=None, r_grpc_port=None, + r_fwd_engine=None, decap_sid=None, locator=None, + locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): + ''' + Add or remove a SRv6 uSID policy. + + :param operation: The operation to be performed on the uSID policy + (i.e. add, get, change, del). + :type operation: str + :param policies: The policies to add or delete. + :type policies: list + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str + :param nodes_lr: Waypoints of the SRv6 route for the left to right path. + :type nodes_lr: list + :param nodes_rl: Waypoints of the SRv6 route for the right to left path. + :type nodes_rl: list + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param persistency: Define if enable the policy persistency or not. + Persistency requires to enable and configure ArangoDB + (default: True). + :type persistency: bool, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string + :param l_grpc_ip: gRPC IP address of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_ip: str, optional + :param l_grpc_port: gRPC port of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_port: str, optional + :param l_fwd_engine: forwarding engine of the left node, required if the + left node is expressed numerically in the nodes list. + :type l_fwd_engine: str, optional + :param r_grpc_ip: gRPC IP address of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_ip: str, optional + :param r_grpc_port: gRPC port of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_port: str, optional + :param r_fwd_engine: Forwarding engine of the right node, required if the + right node is expressed numerically in the nodes + list. + :type r_fwd_engine: str, optional + :param decap_sid: uSID used for the decap behavior (End.DT6). + :type decap_sid: str, optional + :param locator: Locator prefix (e.g. 'fcbb:bbbb::'). + :type locator: str, optional + :param locator_bits: Number of bits of the locator part of the SIDs + (default: 32). + :type locator_bits: int + :param usid_id_bits: Number of bits of the uSID identifiers (default: 16). + :type usid_id_bits: int + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). + :rtype: int + ''' + # Prepare response + response = commons_pb2.STATUS_SUCCESS + # Fill nodes configuration + add_del_srv6_usid_policy_fill_nodes_config( + operation=operation, + nodes_config=nodes_config, + lr_destination=lr_destination, + rl_destination=rl_destination, + nodes_lr=nodes_lr, + nodes_rl=nodes_rl, + table=table, + metric=metric, + _id=_id, + l_grpc_ip=l_grpc_ip, + l_grpc_port=l_grpc_port, + l_fwd_engine=l_fwd_engine, + r_grpc_ip=r_grpc_ip, + r_grpc_port=r_grpc_port, + r_fwd_engine=r_fwd_engine, + decap_sid=decap_sid, + locator=locator, + locator_bits=locator_bits, + usid_id_bits=usid_id_bits + ) + # #################################################################### + # Iterate on the policies + for policy in policies: + # Extract the attributes + lr_destination = policy.get('lr_dst') + rl_destination = policy.get('rl_dst') + nodes_lr = policy.get('lr_nodes') + nodes_rl = policy.get('rl_nodes') + _id = policy.get('_key') + # If right to left nodes list is not provided, we use the reverse + # left to right SID list (symmetric path) + if nodes_rl is None: + nodes_rl = nodes_lr[::-1] + # The two SID lists must have the same endpoints + if nodes_lr[0] != nodes_rl[-1] or nodes_rl[0] != nodes_lr[-1]: + logger.error('Bad tunnel endpoints') + raise utils.InvalidArgumentError + # Create the SRv6 Policy + try: + # Extract the configuration of the nodes + # + # Ingress node + ingress_node = nodes_config[nodes_lr[0]] + # Intermediate nodes + intermediate_nodes_lr = list() + for node in nodes_lr[1:-1]: + intermediate_nodes_lr.append(nodes_config[node]) + intermediate_nodes_rl = list() + for node in nodes_rl[1:-1]: + intermediate_nodes_rl.append(nodes_config[node]) + # Egress node + egress_node = nodes_config[nodes_lr[-1]] + # Build the SID lists, made of the uN SIDs extracted from the + # nodes configuration + # + # SID list for the left-to-right path + segments_lr = list() + for node in nodes_lr: + segments_lr.append(nodes_config[node]['uN']) + # SID list for the right-to-left path + segments_rl = list() + for node in nodes_rl: + segments_rl.append(nodes_config[node]['uN']) + # Add the uSID policy in the ingress node + add_del_srv6_usid_policy_ingress( + operation=operation, + grpc_ip=ingress_node['grpc_ip'], + grpc_port=ingress_node['grpc_port'], + node_name=ingress_node['name'], + fwd_engine=ingress_node['fwd_engine'], + egress_udt=egress_node['uDT'], + destination=lr_destination, + segments=nodes_lr, + table=table, + metric=metric, + locator_bits=locator_bits, + usid_id_bits=usid_id_bits + ) + # Check for errors + if response != commons_pb2.STATUS_SUCCESS: + return response + # Add the uSID policy in the egress node + add_del_srv6_usid_policy_ingress( + operation=operation, + grpc_ip=egress_node['grpc_ip'], + grpc_port=egress_node['grpc_port'], + node_name=egress_node['name'], + fwd_engine=egress_node['fwd_engine'], + egress_udt=ingress_node['uDT'], + destination=rl_destination, + segments=nodes_rl, + table=table, + metric=metric, + locator_bits=locator_bits, + usid_id_bits=usid_id_bits + ) + # Check for errors + if response != commons_pb2.STATUS_SUCCESS: + return response + # Persist uSID policy to database + handle_srv6_usid_policy_persistency() + except (InvalidConfigurationError, NodeNotFoundError, + TooManySegmentsError, SIDLocatorError, InvalidSIDError): + return commons_pb2.STATUS_INTERNAL_ERROR + # Return the response + return response + + +def handle_srv6_usid_policy_persistency(operation, lr_destination=None, + rl_destination=None, nodes_lr=None, + nodes_rl=None, table=-1, metric=-1, + _id=None, l_grpc_ip=None, + l_grpc_port=None, l_fwd_engine=None, + r_grpc_ip=None, r_grpc_port=None, + r_fwd_engine=None, decap_sid=None, + locator=None, + locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): + ''' + Save or remove a policy from the database. + + :param operation: The operation to be performed on the uSID policy + (i.e. add, get, change, del). + :type operation: str + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str + :param nodes_lr: Waypoints of the SRv6 route for the left to right path. + :type nodes_lr: list + :param nodes_rl: Waypoints of the SRv6 route for the right to left path. + :type nodes_rl: list + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param persistency: Define if enable the policy persistency or not. + Persistency requires to enable and configure ArangoDB + (default: True). + :type persistency: bool, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string + :param l_grpc_ip: gRPC IP address of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_ip: str, optional + :param l_grpc_port: gRPC port of the left node, required if the left + node is expressed numerically in the nodes list. + :type l_grpc_port: str, optional + :param l_fwd_engine: forwarding engine of the left node, required if the + left node is expressed numerically in the nodes list. + :type l_fwd_engine: str, optional + :param r_grpc_ip: gRPC IP address of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_ip: str, optional + :param r_grpc_port: gRPC port of the right node, required if the right + node is expressed numerically in the nodes list. + :type r_grpc_port: str, optional + :param r_fwd_engine: Forwarding engine of the right node, required if the + right node is expressed numerically in the nodes + list. + :type r_fwd_engine: str, optional + :param decap_sid: uSID used for the decap behavior (End.DT6). + :type decap_sid: str, optional + :param locator: Locator prefix (e.g. 'fcbb:bbbb::'). + :type locator: str, optional + :param locator_bits: Number of bits of the locator part of the SIDs + (default: 32). + :type locator_bits: int + :param usid_id_bits: Number of bits of the uSID identifiers (default: 16). + :type usid_id_bits: int + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). + :rtype: int + ''' + # ######################################################################## + # Extract the ArangoDB params from the environment variables + arango_url = os.getenv('ARANGO_URL') + arango_user = os.getenv('ARANGO_USER') + arango_password = os.getenv('ARANGO_PASSWORD') + # Connect to ArangoDB + client = arangodb_driver.connect_arango( + url=arango_url) # TODO keep arango connection open + # Connect to the "srv6_usid" db + database = arangodb_driver.connect_srv6_usid_db( + client=client, + username=arango_user, + password=arango_password + ) + # Perform the operation + if operation == 'add': + # Save the policy to the db + arangodb_driver.insert_usid_policy( + database=database, + lr_dst=lr_destination, + rl_dst=rl_destination, + lr_nodes=nodes_lr, + rl_nodes=nodes_rl, + table=table if table != -1 else None, + metric=metric if metric != -1 else None, + l_grpc_ip=l_grpc_ip, + l_grpc_port=l_grpc_port, + l_fwd_engine=l_fwd_engine, + r_grpc_ip=r_grpc_ip, + r_grpc_port=r_grpc_port, + r_fwd_engine=r_fwd_engine, + decap_sid=decap_sid, + locator=locator + ) + elif operation == 'del': + # Remove the policy from the db + arangodb_driver.delete_usid_policy( + database=database, + key=_id, + lr_dst=lr_destination, + rl_dst=rl_destination, + lr_nodes=nodes_lr, + rl_nodes=nodes_rl, + table=table if table != -1 else None, + metric=metric if metric != -1 else None + ) + else: + logger.error('Unsupported operation: %s', operation) + + +def handle_srv6_usid_policy(operation, nodes_config=None, lr_destination=None, rl_destination=None, nodes_lr=None, nodes_rl=None, table=-1, metric=-1, persistency=True, _id=None, l_grpc_ip=None, l_grpc_port=None, l_fwd_engine=None, r_grpc_ip=None, r_grpc_port=None, - r_fwd_engine=None, decap_sid=None, locator=None): + r_fwd_engine=None, decap_sid=None, locator=None, + locator_bits=DEFAULT_LOCATOR_BITS, + usid_id_bits=DEFAULT_USID_ID_BITS): ''' - Handle a SRv6 Policy using uSIDs + Handle a SRv6 Policy using uSIDs. :param operation: The operation to be performed on the uSID policy - (i.e. add, get, change, del) + (i.e. add, get, change, del). :type operation: str - :param nodes_dict: Dict containing the nodes configuration. - :type nodes_dict: dict - :param destination: Destination of the SRv6 route - :type destination: str - :param nodes: Waypoints of the SRv6 route - :type nodes: list + :param nodes_config: Dict containing the nodes configuration. + :type nodes_config: dict + :param lr_destination: Destination of the SRv6 route for the left to right + path. + :type lr_destination: str + :param rl_destination: Destination of the SRv6 route for the right to left + path. + :type rl_destination: str + :param nodes_lr: Waypoints of the SRv6 route for the left to right path. + :type nodes_lr: list + :param nodes_rl: Waypoints of the SRv6 route for the right to left path. + :type nodes_rl: list :param device: Device of the SRv6 route. If not provided, the device is selected automatically by the node. :type device: str, optional @@ -717,6 +1546,13 @@ def handle_srv6_usid_policy(operation, nodes_dict=None, :param metric: Metric for the SRv6 route. If not provided, the default metric will be used. :type metric: int, optional + :param persistency: Define if enable the policy persistency or not. + Persistency requires to enable and configure ArangoDB + (default: True). + :type persistency: bool, optional + :param _id: The identifier assigned to a policy, used to get or delete + a policy by id. + :type _id: string :param l_grpc_ip: gRPC IP address of the left node, required if the left node is expressed numerically in the nodes list. :type l_grpc_ip: str, optional @@ -740,515 +1576,83 @@ def handle_srv6_usid_policy(operation, nodes_dict=None, :type decap_sid: str, optional :param locator: Locator prefix (e.g. 'fcbb:bbbb::'). :type locator: str, optional - :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS) + :param locator_bits: Number of bits of the locator part of the SIDs + (default: 32). + :type locator_bits: int + :param usid_id_bits: Number of bits of the uSID identifiers (default: 16). + :type usid_id_bits: int + :return: Status Code of the operation (e.g. 0 for STATUS_SUCCESS). :rtype: int - :raises NodeNotFoundError: Node name not found in the mapping file + :raises NodeNotFoundError: Node name not found in the mapping file. :raises InvalidConfigurationError: The mapping file is not a valid - YAML file - :raises TooManySegmentsError: segments arg contains more than 6 segments - :raises SIDLocatorError: SID Locator is wrong for one or more segments - :raises InvalidSIDError: SID is wrong for one or more segments + YAML file. + :raises TooManySegmentsError: segments arg contains more than 6 segments. + :raises SIDLocatorError: SID Locator is wrong for one or more segments. + :raises InvalidSIDError: SID is wrong for one or more segments. + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + :raises controller.utils.PolicyNotFoundError: Policy not found. ''' # pylint: disable=too-many-locals, too-many-arguments # pylint: disable=too-many-return-statements, too-many-branches # pylint: disable=too-many-statements # - # ArangoDB params - arango_url = os.getenv('ARANGO_URL') - arango_user = os.getenv('ARANGO_USER') - arango_password = os.getenv('ARANGO_PASSWORD') - # + # ######################################################################## # Validate arguments if lr_destination is None: + # "lr_destination" is mandatory for the add operation if operation in ['add']: logger.error('"lr_destination" argument is mandatory for %s ' 'operation', operation) - return None + raise utils.InvalidArgumentError if rl_destination is None: + # "rl_destination" is mandatory for the add operation if operation in ['add']: logger.error('"rl_destination" argument is mandatory for %s ' 'operation', operation) - return None + raise utils.InvalidArgumentError if nodes_lr is None: + # "nodes_lr" is mandatory for the add operation if operation in ['add']: logger.error('"nodes_lr" argument is mandatory for %s ' 'operation', operation) - return None + raise utils.InvalidArgumentError if nodes_rl is None: + # "nodes_rl" is optional; if not provided, "nodes_rl" is set to the + # reverse of "nodes_rl" (forward and reverse paths are symmetric) pass - if nodes_dict is None: + if nodes_config is None: + # "nodes_config" is required for "add" and "del" operations if operation in ['add', 'del']: - logger.error('"nodes_filename" argument is mandatory for %s ' + logger.error('"nodes_config" argument is mandatory for %s ' 'operation', operation) - return None + raise utils.InvalidArgumentError + # ######################################################################## + # Perform the operation if operation == 'change': + # TODO: Change operation not yet implemented logger.error('Operation not yet implemented: %s', operation) - return None + raise utils.InvalidArgumentError if operation == 'get': - if not persistency: - logger.error('Error in get(): Persistency is disabled') - return None - # Connect to ArangoDB - client = arangodb_driver.connect_arango( - url=arango_url) # TODO keep arango connection open - # Connect to the db - database = arangodb_driver.connect_srv6_usid_db( - client=client, - username=arango_user, - password=arango_password - ) - # Get the policy from the db - policies = arangodb_driver.find_usid_policy( - database=database, - key=_id, - lr_dst=lr_destination, - rl_dst=rl_destination, - lr_nodes=nodes_lr, - rl_nodes=nodes_rl, - table=table if table != -1 else None, - metric=metric if metric != -1 else None + # Get SRv6 uSID policy + policies = get_srv6_usid_policy( + lr_destination=lr_destination, + rl_destination=rl_destination, + nodes_lr=nodes_lr, + nodes_rl=nodes_rl, + table=table, + metric=metric, + _id=_id ) # Print policies print('\n\n*** uSID policies:') - pprint.PrettyPrinter(indent=4).pprint(list(policies)) + pprint.PrettyPrinter(indent=4).pprint(policies) print('\n\n') - return 0 + # Return the status code + return commons_pb2.STATUS_SUCCESS if operation in ['add', 'del']: - # - # In order to perform this translation, a file containing the - # mapping of node names to IPv6 addresses is required - # - # Read nodes from YAML file - nodes_info = nodes_dict - locator_bits = DEFAULT_LOCATOR_BITS # TODO configurable locator bits - usid_id_bits = DEFAULT_USID_ID_BITS # TODO configurable uSID id bits - # Add nodes list for the left-to-right path to the 'nodes_info' dict - if nodes_lr is not None: - fill_nodes_info( - nodes_info=nodes_info, - nodes=nodes_lr, - l_grpc_ip=l_grpc_ip, - l_grpc_port=l_grpc_port, - l_fwd_engine=l_fwd_engine, - r_grpc_ip=r_grpc_ip, - r_grpc_port=r_grpc_port, - r_fwd_engine=r_fwd_engine, - decap_sid=decap_sid, - locator=locator - ) - # Add nodes list for the right-to-left path to the 'nodes_info' dict - if nodes_rl is not None: - fill_nodes_info( - nodes_info=nodes_info, - nodes=nodes_rl, - l_grpc_ip=r_grpc_ip, - l_grpc_port=r_grpc_port, - l_fwd_engine=r_fwd_engine, - r_grpc_ip=l_grpc_ip, - r_grpc_port=l_grpc_port, - r_fwd_engine=l_fwd_engine, - decap_sid=decap_sid, - locator=locator - ) - # Add - if operation == 'add': - policies = [{ - 'lr_dst': lr_destination, - 'rl_dst': rl_destination, - 'lr_nodes': nodes_lr, - 'rl_nodes': nodes_rl - }] - if operation == 'del': - # - # Connect to ArangoDB - client = arangodb_driver.connect_arango( - url=arango_url) # TODO keep arango connection open - # Connect to the db - database = arangodb_driver.connect_srv6_usid_db( - client=client, - username=arango_user, - password=arango_password - ) - # Get the policy from the db - policies = arangodb_driver.find_usid_policy( - database=database, - key=_id, - lr_dst=lr_destination, - rl_dst=rl_destination, - lr_nodes=nodes_lr, - rl_nodes=nodes_rl, - table=table if table != -1 else None, - metric=metric if metric != -1 else None - ) - - policies = list(policies) - for policy in policies: - # Add nodes list for the left-to-right path to the - # 'nodes_info' dict - if policy.get('lr_nodes') is not None: - fill_nodes_info( - nodes_info=nodes_info, - nodes=policy.get('lr_nodes'), - l_grpc_ip=policy.get('l_grpc_ip'), - l_grpc_port=policy.get('l_grpc_port'), - l_fwd_engine=policy.get('l_fwd_engine'), - r_grpc_ip=policy.get('r_grpc_ip'), - r_grpc_port=policy.get('r_grpc_port'), - r_fwd_engine=policy.get('r_fwd_engine'), - decap_sid=policy.get('decap_sid'), - locator=policy.get('locator') - ) - # Add nodes list for the right-to-left path to the - # 'nodes_info' dict - if policy.get('rl_nodes') is not None: - fill_nodes_info( - nodes_info=nodes_info, - nodes=policy.get('rl_nodes'), - l_grpc_ip=policy.get('r_grpc_ip'), - l_grpc_port=policy.get('r_grpc_port'), - l_fwd_engine=policy.get('r_fwd_engine'), - r_grpc_ip=policy.get('l_grpc_ip'), - r_grpc_port=policy.get('l_grpc_port'), - r_fwd_engine=policy.get('l_fwd_engine'), - decap_sid=policy.get('decap_sid'), - locator=policy.get('locator') - ) - if len(policies) == 0: - logger.error('Policy not found') - return None - # Iterate on the policies - for policy in policies: - lr_destination = policy.get('lr_dst') - rl_destination = policy.get('rl_dst') - nodes_lr = policy.get('lr_nodes') - nodes_rl = policy.get('rl_nodes') - _id = policy.get('_key') - # - # If right to left nodes list is not provided, we use the reverse - # left to right SID list (symmetric path) - if nodes_rl is None: - nodes_rl = nodes_lr[::-1] - # The two SID lists must have the same endpoints - if nodes_lr[0] != nodes_rl[-1] or nodes_rl[0] != nodes_lr[-1]: - logger.error('Bad tunnel endpoints') - return None - # Create the SRv6 Policy - try: - # # Prefix length for local segment - # prefix_len = locator_bits + usid_id_bits - # Ingress node - ingress_node = nodes_info[nodes_lr[0]] - # Intermediate nodes - intermediate_nodes_lr = list() - for node in nodes_lr[1:-1]: - intermediate_nodes_lr.append(nodes_info[node]) - intermediate_nodes_rl = list() - for node in nodes_rl[1:-1]: - intermediate_nodes_rl.append(nodes_info[node]) - # Egress node - egress_node = nodes_info[nodes_lr[-1]] - # Extract the segments - segments_lr = list() - for node in nodes_lr: - segments_lr.append(nodes_info[node]['uN']) - segments_rl = list() - for node in nodes_rl: - segments_rl.append(nodes_info[node]['uN']) - - # Ingress node - with utils.get_grpc_session(ingress_node['grpc_ip'], - ingress_node['grpc_port'] - ) as channel: - # Currently ony Linux and VPP are suppoted for the encap - if ingress_node['fwd_engine'] not in ['Linux', 'VPP']: - logger.error( - 'Encap operation is not supported for ' - '%s with fwd engine %s', - ingress_node['name'], - ingress_node['fwd_engine']) - return commons_pb2.STATUS_INTERNAL_ERROR - # VPP requires a BSID address - bsid_addr = '' - if ingress_node['fwd_engine'] == 'VPP': - for char in lr_destination: - if char not in ('0', ':'): - bsid_addr += char - add_colon = False - if len(bsid_addr) <= 28: - add_colon = True - bsid_addr = [(bsid_addr[i:i + 4]) - for i in range(0, len(bsid_addr), 4)] - bsid_addr = ':'.join(bsid_addr) - if add_colon: - bsid_addr += '::' - - udt_sids = list() - # Locator mask - locator_mask = str(IPv6Address( - int('1' * 128, 2) ^ - int('1' * (128 - locator_bits), 2))) - # uDT mask - udt_mask_1 = str( - IPv6Address(int('1' * usid_id_bits, 2) << - (128 - locator_bits - usid_id_bits))) - udt_mask_2 = str( - IPv6Address(int('1' * usid_id_bits, 2) << - (128 - locator_bits - 2 * usid_id_bits))) - # Build uDT sid list - locator_int = int(IPv6Address(egress_node['uDT'])) & \ - int(IPv6Address(locator_mask)) - udt_mask_1_int = int(IPv6Address(egress_node['uDT'])) & \ - int(IPv6Address(udt_mask_1)) - udt_mask_2_int = int(IPv6Address(egress_node['uDT'])) & \ - int(IPv6Address(udt_mask_2)) - udt_sids += [str(IPv6Address(locator_int + - udt_mask_1_int))] - udt_sids += [str(IPv6Address(locator_int + - (udt_mask_2_int << - usid_id_bits)))] - # We need to convert the SID list into a uSID list - # before creating the SRv6 policy - usid_list = sidlist_to_usidlist( - sid_list=segments_lr[1:][:-1], - udt_sids=[segments_lr[1:][-1]] + udt_sids, - locator_bits=locator_bits, - usid_id_bits=usid_id_bits - ) - # Handle a SRv6 path - response = srv6_utils.handle_srv6_path( - operation=operation, - channel=channel, - destination=lr_destination, - segments=usid_list, - encapmode='encap.red', - table=table, - metric=metric, - bsid_addr=bsid_addr, - fwd_engine=ingress_node['fwd_engine'] - ) - if response != commons_pb2.STATUS_SUCCESS: - # Error - return response - # # Create the uN behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (ingress_node['uN'], prefix_len), - # action='uN', - # fwd_engine=ingress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # # Create the End behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (ingress_node['uN'], 64), - # action='End', - # fwd_engine=ingress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # # Create the decap behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (ingress_node['uDT'], 64), - # action='End.DT6', - # lookup_table=254, - # fwd_engine=ingress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # # Intermediate nodes - # for node in intermediate_nodes: - # with utils.get_grpc_session(node['grpc_ip'], - # node['grpc_port'] - # ) as channel: - # # Create the uN behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (node['uN'], prefix_len), - # action='uN', - # fwd_engine=node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # Egress node - with utils.get_grpc_session(egress_node['grpc_ip'], - egress_node['grpc_port'] - ) as channel: - # Currently ony Linux and VPP are suppoted for the encap - if egress_node['fwd_engine'] not in ['Linux', 'VPP']: - logger.error( - 'Encap operation is not supported for ' - '%s with fwd engine %s', - egress_node['name'], egress_node['fwd_engine']) - return commons_pb2.STATUS_INTERNAL_ERROR - # VPP requires a BSID address - bsid_addr = '' - if egress_node['fwd_engine'] == 'VPP': - for char in lr_destination: - if char not in ('0', ':'): - bsid_addr += char - add_colon = False - if len(bsid_addr) <= 28: - add_colon = True - bsid_addr = [(bsid_addr[i:i + 4]) - for i in range(0, len(bsid_addr), 4)] - bsid_addr = ':'.join(bsid_addr) - if add_colon: - bsid_addr += '::' - # # Create the uN behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (egress_node['uN'], prefix_len), - # action='uN', - # fwd_engine=egress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # # Create the End behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (egress_node['uN'], 64), - # action='End', - # fwd_engine=egress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - # # Create the decap behavior - # response = handle_srv6_behavior( - # operation=operation, - # channel=channel, - # segment='%s/%s' % (egress_node['uDT'], 64), - # action='End.DT6', - # lookup_table=254, - # fwd_engine=egress_node['fwd_engine'] - # ) - # if response != commons_pb2.STATUS_SUCCESS: - # # Error - # return response - udt_sids = list() - # Locator mask - locator_mask = str(IPv6Address( - int('1' * 128, 2) ^ - int('1' * (128 - locator_bits), 2))) - # uDT mask - udt_mask_1 = str(IPv6Address(int('1' * usid_id_bits, 2) << - (128 - locator_bits - - usid_id_bits))) - udt_mask_2 = str(IPv6Address(int('1' * usid_id_bits, 2) << - (128 - locator_bits - - 2 * usid_id_bits))) - # Build uDT sid list - locator_int = int( - IPv6Address( - ingress_node['uDT'])) & int( - IPv6Address(locator_mask)) - udt_mask_1_int = int( - IPv6Address( - ingress_node['uDT'])) & int( - IPv6Address(udt_mask_1)) - udt_mask_2_int = int( - IPv6Address( - ingress_node['uDT'])) & int( - IPv6Address(udt_mask_2)) - udt_sids += [str(IPv6Address(locator_int + - udt_mask_1_int))] - udt_sids += [str(IPv6Address(locator_int + - (udt_mask_2_int << - usid_id_bits)))] - # We need to convert the SID list into a uSID list - # before creating the SRv6 policy - usid_list = sidlist_to_usidlist( - sid_list=segments_rl[1:][:-1], - udt_sids=[segments_rl[1:][-1]] + udt_sids, - locator_bits=locator_bits, - usid_id_bits=usid_id_bits - ) - # Handle a SRv6 path - response = srv6_utils.handle_srv6_path( - operation=operation, - channel=channel, - destination=rl_destination, - segments=usid_list, - encapmode='encap.red', - table=table, - metric=metric, - bsid_addr=bsid_addr, - fwd_engine=egress_node['fwd_engine'] - ) - if response != commons_pb2.STATUS_SUCCESS: - # Error - return response - # Persist uSID policy to database - if persistency: - if operation == 'add': - # Connect to ArangoDB - client = arangodb_driver.connect_arango( - url=arango_url) # TODO keep arango connection open - # Connect to the db - database = arangodb_driver.connect_srv6_usid_db( - client=client, - username=arango_user, - password=arango_password - ) - # Save the policy to the db - arangodb_driver.insert_usid_policy( - database=database, - lr_dst=lr_destination, - rl_dst=rl_destination, - lr_nodes=nodes_lr, - rl_nodes=nodes_rl, - table=table if table != -1 else None, - metric=metric if metric != -1 else None, - l_grpc_ip=l_grpc_ip, - l_grpc_port=l_grpc_port, - l_fwd_engine=l_fwd_engine, - r_grpc_ip=r_grpc_ip, - r_grpc_port=r_grpc_port, - r_fwd_engine=r_fwd_engine, - decap_sid=decap_sid, - locator=locator - ) - elif operation == 'del': - # TODO keep arango connection open - # Connect to ArangoDB - client = arangodb_driver.connect_arango( - url=arango_url) - # Connect to the db - database = arangodb_driver.connect_srv6_usid_db( - client=client, - username=arango_user, - password=arango_password - ) - # Save the policy to the db - arangodb_driver.delete_usid_policy( - database=database, - key=_id, - lr_dst=lr_destination, - rl_dst=rl_destination, - lr_nodes=nodes_lr, - rl_nodes=nodes_rl, - table=table if table != -1 else None, - metric=metric if metric != -1 else None - ) - else: - logger.error('Unsupported operation: %s', operation) - except (InvalidConfigurationError, NodeNotFoundError, - TooManySegmentsError, SIDLocatorError, InvalidSIDError): - return commons_pb2.STATUS_INTERNAL_ERROR - # Return the response - return response + # Add/delete the SRv6 uSID policy + return add_del_srv6_usid_policy() + # Operation unknown logger.error('Unsupported operation: %s', operation) - return None + raise utils.InvalidArgumentError diff --git a/control_plane/controller/controller/srv6_utils.py b/control_plane/controller/controller/srv6_utils.py index 4bbe9c3..4903d49 100644 --- a/control_plane/controller/controller/srv6_utils.py +++ b/control_plane/controller/controller/srv6_utils.py @@ -18,19 +18,18 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Implementation of SRv6 Controller +# SRv6 utilities for SRv6 SDN Controller # # @author Carmine Scarpitta # ''' -Control-Plane functionalities for SRv6 Manager +This module provides a collection of SRv6 utilities for SRv6 SDN Controller. ''' # General imports import logging - import grpc from six import text_type @@ -41,6 +40,7 @@ import srv6_manager_pb2_grpc from controller import utils + # Global variables definition # # @@ -56,18 +56,27 @@ # Parser for gRPC errors def parse_grpc_error(err): ''' - Parse a gRPC error - ''' + Parse a gRPC error. + :param err: The error to parse. + :type err: grpc.RpcError + :return: A status code corresponding to the gRPC error. + :rtype: int + ''' + # Extract the gRPC status code status_code = err.code() + # Extract the error description details = err.details() logger.error('gRPC client reported an error: %s, %s', status_code, details) if grpc.StatusCode.UNAVAILABLE == status_code: + # gRPC server is not available code = commons_pb2.STATUS_GRPC_SERVICE_UNAVAILABLE elif grpc.StatusCode.UNAUTHENTICATED == status_code: + # Authentication problem code = commons_pb2.STATUS_GRPC_UNAUTHORIZED else: + # Generic gRPC error code = commons_pb2.STATUS_INTERNAL_ERROR # Return an error message return code @@ -77,11 +86,45 @@ def handle_srv6_path(operation, channel, destination, segments=None, device='', encapmode="encap", table=-1, metric=-1, bsid_addr='', fwd_engine='Linux'): ''' - Handle a SRv6 Path - ''' + Handle a SRv6 path on a node. + :param operation: The operation to be performed on the SRv6 path + (i.e. add, get, change, del). + :type operation: str + :param channel: The gRPC Channel to the node. + :type channel: class: `grpc._channel.Channel` + :param destination: The destination prefix of the SRv6 path. + It can be a IP address or a subnet. + :type destination: str + :param segments: The SID list to be applied to the packets going to + the destination (not required for "get" and "del" + operations). + :type segments: list, optional + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param encapmode: The encap mode to use for the path, i.e. "inline" or + "encap" (default: encap). + :type encapmode: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional + :param fwd_engine: Forwarding engine for the SRv6 route (default: Linux). + :type fwd_engine: str, optional + :return: The status code of the operation. + :rtype: int + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. + ''' # pylint: disable=too-many-locals, too-many-arguments, too-many-branches - + # + # If segments argument is not provided, we initialize it to an empty list if segments is None: segments = [] # Create request message @@ -106,11 +149,25 @@ def handle_srv6_path(operation, channel, destination, segments=None, path.metric = int(metric) # Set the BSID address (required for VPP) path.bsid_addr = str(bsid_addr) + # Forwarding engine (Linux or VPP) + try: + path_request.fwd_engine = srv6_manager_pb2.FwdEngine.Value(fwd_engine) + except ValueError: + logger.error('Invalid forwarding engine: %s', fwd_engine) + raise utils.InvalidArgumentError # Handle SRv6 policy for VPP + # A SRv6 path in VPP consists of: + # - a SRv6 policy + # - a rule to steer packets sent to a destination through the SRv6 + # policy. + # The steering rule matches the corresponding SRv6 policy through a + # Binding SID (BSID) + # Therefore, VPP requires some extra configuration with respect to Linux if fwd_engine == 'VPP': + # VPP requires BSID address if bsid_addr == '': logger.error('"bsid_addr" argument is mandatory for VPP') - return None + raise utils.InvalidArgumentError # Handle SRv6 policy res = handle_srv6_policy( operation=operation, @@ -121,15 +178,11 @@ def handle_srv6_path(operation, channel, destination, segments=None, metric=metric, fwd_engine=fwd_engine ) + # Check for errors if res != commons_pb2.STATUS_SUCCESS: logger.error('Cannot create SRv6 policy: error %s', res) - return None - # Forwarding engine (Linux or VPP) - try: - path_request.fwd_engine = srv6_manager_pb2.FwdEngine.Value(fwd_engine) - except ValueError: - logger.error('Invalid forwarding engine: %s', fwd_engine) - return None + return res + # The following steps are common for Linux and VPP try: # Get the reference of the stub stub = srv6_manager_pb2_grpc.SRv6ManagerStub(channel) @@ -138,9 +191,10 @@ def handle_srv6_path(operation, channel, destination, segments=None, if operation == 'add': # Set encapmode path.encapmode = text_type(encapmode) + # At least one segment is required for add operation if len(segments) == 0: logger.error('*** Missing segments for seg6 route') - return commons_pb2.STATUS_INTERNAL_ERROR + raise utils.InvalidArgumentError # Iterate on the segments and build the SID list for segment in segments: # Append the segment to the SID list @@ -164,6 +218,10 @@ def handle_srv6_path(operation, channel, destination, segments=None, elif operation == 'del': # Remove the SRv6 path response = stub.Remove(request) + else: + # The operation is unknown + logger.error('Invalid operation: %s', operation) + raise utils.InvalidArgumentError # Get the status code of the gRPC operation response = response.status except grpc.RpcError as err: @@ -177,11 +235,35 @@ def handle_srv6_path(operation, channel, destination, segments=None, def handle_srv6_policy(operation, channel, bsid_addr, segments=None, table=-1, metric=-1, fwd_engine='Linux'): ''' - Handle a SRv6 Path + Handle a SRv6 policy on a node. + + :param operation: The operation to be performed on the SRv6 policy + (i.e. add, get, change, del). + :type operation: str + :param channel: The gRPC Channel to the node. + :type channel: class: `grpc._channel.Channel` + :param bsid_addr: The Binding SID to be used for the policy. + :type bsid_addr: str + :param segments: The SID list to be applied to the packets going to + the destination (not required for "get" and "del" + operations). + :type segments: list, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param fwd_engine: Forwarding engine for the SRv6 route (default: Linux). + :type fwd_engine: str, optional + :return: The status code of the operation. + :rtype: int + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. ''' - # pylint: disable=too-many-locals, too-many-arguments - + # + # If segments argument is not provided, we initialize it to an empty list if segments is None: segments = [] # Create request message @@ -206,25 +288,26 @@ def handle_srv6_policy(operation, channel, bsid_addr, segments=None, fwd_engine) except ValueError: logger.error('Invalid forwarding engine: %s', fwd_engine) - return None + raise utils.InvalidArgumentError try: # Get the reference of the stub stub = srv6_manager_pb2_grpc.SRv6ManagerStub(channel) # Fill the request depending on the operation # and send the request if operation == 'add': + # At least one segment is required for add operation if len(segments) == 0: logger.error('*** Missing segments for seg6 route') - return commons_pb2.STATUS_INTERNAL_ERROR + raise utils.InvalidArgumentError # Iterate on the segments and build the SID list for segment in segments: # Append the segment to the SID list srv6_segment = policy.sr_path.add() srv6_segment.segment = text_type(segment) - # Create the SRv6 path + # Create the SRv6 policy response = stub.Create(request) elif operation == 'get': - # Get the SRv6 path + # Get the SRv6 policy response = stub.Get(request) elif operation == 'change': # Iterate on the segments and build the SID list @@ -232,11 +315,15 @@ def handle_srv6_policy(operation, channel, bsid_addr, segments=None, # Append the segment to the SID list srv6_segment = policy.sr_path.add() srv6_segment.segment = text_type(segment) - # Update the SRv6 path + # Update the SRv6 policy response = stub.Update(request) elif operation == 'del': - # Remove the SRv6 path + # Remove the SRv6 policy response = stub.Remove(request) + else: + # The operation is unknown + logger.error('Invalid operation: %s', operation) + raise utils.InvalidArgumentError # Get the status code of the gRPC operation response = response.status except grpc.RpcError as err: @@ -252,10 +339,48 @@ def handle_srv6_behavior(operation, channel, segment, action='', device='', interface="", segments=None, metric=-1, fwd_engine='Linux'): ''' - Handle a SRv6 behavior + Handle a SRv6 behavior on a node. + + :param operation: The operation to be performed on the SRv6 path + (i.e. add, get, change, del). + :type operation: str + :param channel: The gRPC Channel to the node. + :type channel: class: `grpc._channel.Channel` + :param segment: The local segment of the SRv6 behavior. It can be a IP + address or a subnet. + :type segment: str + :param action: The SRv6 action associated to the behavior (e.g. End or + End.DT6), (not required for "get" and "change"). + :type action: str, optional + :param device: Device of the SRv6 route. If not provided, the device + is selected automatically by the node. + :type device: str, optional + :param table: Routing table containing the SRv6 route. If not provided, + the main table (i.e. table 254) will be used. + :type table: int, optional + :param nexthop: The nexthop of cross-connect behaviors (e.g. End.DX4 + or End.DX6). + :type nexthop: str, optional + :param lookup_table: The lookup table for the decap behaviors (e.g. + End.DT4 or End.DT6). + :type lookup_table: int, optional + :param interface: The outgoing interface for the End.DX2 behavior. + :type interface: str, optional + :param segments: The SID list to be applied for the End.B6 behavior. + :type segments: list, optional + :param metric: Metric for the SRv6 route. If not provided, the default + metric will be used. + :type metric: int, optional + :param fwd_engine: Forwarding engine for the SRv6 route (default: Linux). + :type fwd_engine: str, optional + :return: The status code of the operation. + :rtype: int + :raises controller.utils.InvalidArgumentError: You provided an invalid + argument. ''' # pylint: disable=too-many-arguments, too-many-locals # + # If segments argument is not provided, we initialize it to an empty list if segments is None: segments = [] # Create request message @@ -289,16 +414,17 @@ def handle_srv6_behavior(operation, channel, segment, action='', device='', fwd_engine) except ValueError: logger.error('Invalid forwarding engine: %s', fwd_engine) - return None + raise utils.InvalidArgumentError try: # Get the reference of the stub stub = srv6_manager_pb2_grpc.SRv6ManagerStub(channel) # Fill the request depending on the operation # and send the request if operation == 'add': + # The argument "action" is mandatory for the "add" operation if action == '': logger.error('*** Missing action for seg6local route') - return commons_pb2.STATUS_INTERNAL_ERROR + raise utils.InvalidArgumentError # Set the action for the seg6local route behavior.action = text_type(action) # Set the nexthop for the L3 cross-connect actions @@ -345,8 +471,9 @@ def handle_srv6_behavior(operation, channel, segment, action='', device='', # Remove the SRv6 behavior response = stub.Remove(request) else: + # The operation is unknown logger.error('Invalid operation: %s', operation) - return None + raise utils.InvalidArgumentError # Get the status code of the gRPC operation response = response.status except grpc.RpcError as err: @@ -367,14 +494,14 @@ def create_uni_srv6_tunnel(ingress_channel, egress_channel, destination, segments, localseg=None, bsid_addr='', fwd_engine='Linux'): ''' - Create a unidirectional SRv6 tunnel from to + Create a unidirectional SRv6 tunnel from to . :param ingress_channel: The gRPC Channel to the ingress node :type ingress_channel: class: `grpc._channel.Channel` :param egress_channel: The gRPC Channel to the egress node :type egress_channel: class: `grpc._channel.Channel` :param destination: The destination prefix of the SRv6 path. - It can be a IP address or a subnet. + It can be a IP address or a subnet. :type destination: str :param segments: The SID list to be applied to the packets going to the destination @@ -384,8 +511,13 @@ def create_uni_srv6_tunnel(ingress_channel, egress_channel, 'localseg' isn't passed in, the End.DT6 function is not created. :type localseg: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. :type fwd_engine: str, optional + :return: The status code of the operation. + :rtype: int ''' # pylint: disable=too-many-arguments # @@ -451,16 +583,16 @@ def create_srv6_tunnel(node_l_channel, node_r_channel, Create a bidirectional SRv6 tunnel between and . :param node_l_channel: The gRPC Channel to the left endpoint (node_l) - of the SRv6 tunnel + of the SRv6 tunnel. :type node_l_channel: class: `grpc._channel.Channel` :param node_r_channel: The gRPC Channel to the right endpoint (node_r) - of the SRv6 tunnel + of the SRv6 tunnel. :type node_r_channel: class: `grpc._channel.Channel` :param sidlist_lr: The SID list to be installed on the packets going - from to + from to . :type sidlist_lr: list :param sidlist_rl: The SID list to be installed on the packets going - from to + from to . :type sidlist_rl: list :param dest_lr: The destination prefix of the SRv6 path from to . It can be a IP address or a subnet. @@ -478,8 +610,13 @@ def create_srv6_tunnel(node_l_channel, node_r_channel, to . If the argument 'localseg_rl' isn't passed in, the End.DT6 function is not created. :type localseg_rl: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. :type fwd_engine: str, optional + :return: The status code of the operation. + :rtype: int ''' # pylint: disable=too-many-arguments # @@ -519,9 +656,9 @@ def destroy_uni_srv6_tunnel(ingress_channel, egress_channel, destination, ''' Destroy a unidirectional SRv6 tunnel from to . - :param ingress_channel: The gRPC Channel to the ingress node + :param ingress_channel: The gRPC Channel to the ingress node. :type ingress_channel: class: `grpc._channel.Channel` - :param egress_channel: The gRPC Channel to the egress node + :param egress_channel: The gRPC Channel to the egress node. :type egress_channel: class: `grpc._channel.Channel` :param destination: The destination prefix of the SRv6 path. It can be a IP address or a subnet. @@ -530,11 +667,16 @@ def destroy_uni_srv6_tunnel(ingress_channel, egress_channel, destination, function on the egress node. If the argument 'localseg' isn't passed in, the End.DT6 function is not removed. :type localseg: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. :type fwd_engine: str, optional :param ignore_errors: Whether to ignore "No such process" errors or not - (default is False) + (default is False). :type ignore_errors: bool, optional + :return: The status code of the operation. + :rtype: int ''' # pylint: disable=too-many-arguments # @@ -605,14 +747,14 @@ def destroy_srv6_tunnel(node_l_channel, node_r_channel, Destroy a bidirectional SRv6 tunnel between and . :param node_l_channel: The gRPC channel to the left endpoint of the - SRv6 tunnel (node_l) + SRv6 tunnel (node_l). :type node_l_channel: class: `grpc._channel.Channel` :param node_r_channel: The gRPC channel to the right endpoint of the - SRv6 tunnel (node_r) + SRv6 tunnel (node_r). :type node_r_channel: class: `grpc._channel.Channel` - :param node_l: The IP address of the left endpoint of the SRv6 tunnel + :param node_l: The IP address of the left endpoint of the SRv6 tunnel. :type node_l: str - :param node_r: The IP address of the right endpoint of the SRv6 tunnel + :param node_r: The IP address of the right endpoint of the SRv6 tunnel. :type node_r: str :param dest_lr: The destination prefix of the SRv6 path from to . It can be a IP address or a subnet. @@ -630,11 +772,16 @@ def destroy_srv6_tunnel(node_l_channel, node_r_channel, If the argument 'localseg_r' isn't passed in, the End.DT6 function is not removed. :type localseg_rl: str, optional + :param bsid_addr: The Binding SID to be used for the route (only required + for VPP). + :type bsid_addr: str, optional :param fwd_engine: Forwarding engine for the SRv6 route. Default: Linux. :type fwd_engine: str, optional :param ignore_errors: Whether to ignore "No such process" errors or not - (default is False) + (default is False). :type ignore_errors: bool, optional + :return: The status code of the operation. + :rtype: int ''' # pylint: disable=too-many-arguments # diff --git a/control_plane/controller/controller/ti_extraction.py b/control_plane/controller/controller/ti_extraction.py deleted file mode 100644 index df71b63..0000000 --- a/control_plane/controller/controller/ti_extraction.py +++ /dev/null @@ -1,580 +0,0 @@ -#!/usr/bin/python - -########################################################################## -# Copyright (C) 2020 Carmine Scarpitta -# (Consortium GARR and University of Rome "Tor Vergata") -# www.garr.it - www.uniroma2.it/netgroup -# -# -# Licensed under the Apache License, Version 2.0 (the 'License'); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# Topology information extraction -# -# @author Carmine Scarpitta -# - - -''' -Topology Information Extraction utilities -''' - -import errno -import json -import logging -import os -import re -import socket -import sys -import telnetlib -import time -from argparse import ArgumentParser - -# Logger reference -logging.basicConfig(level=logging.NOTSET) -logger = logging.getLogger(__name__) - -# Optional imports: -# NetworkX - only required to export the topology in JSON format -# and to draw the topology -# pyaml - only required to export the topology in YAML format -# pygraphviz - only required to export the topology to an image file -try: - import networkx as nx - from networkx.drawing.nx_agraph import write_dot - from networkx.readwrite import json_graph -except ImportError: - logger.warning('networkx library is not installed') -try: - from pyaml import yaml -except ImportError: - logger.warning('pyaml library is not installed') -try: - import pygraphviz # pylint: disable=unused-import # noqa: F401 -except ImportError: - logger.warning('pygraphviz library is not installed') - - -# Global variables definition -# -# -# Default topology file -DEFAULT_TOPOLOGY_FILE = '/tmp/topology.json' -# Default nodes file -DEFAULT_NODES_YAML_FILE = '/tmp/nodes.yaml' -# Default edges file -DEFAULT_EDGES_YAML_FILE = '/tmp/edges.yaml' -# Interval between two consecutive extractions (in seconds) -DEFAULT_TOPO_EXTRACTION_PERIOD = 0 -# In our experiment we use 'zebra' as default password -DEFAULT_ISISD_PASSWORD = 'zebra' -# Dot file used to draw topology graph -DOT_FILE_TOPO_GRAPH = '/tmp/topology.dot' -# Define whether the verbose mode is enabled or not by default -DEFAULT_VERBOSE = False - - -# Utility function to dump relevant information of the topology -def dump_topo_json(graph, topo_file): - ''' - Dump the graph to a JSON file - ''' - # - # This function depends on the NetworkX library, which is a - # optional dependency for this script - # - # Check if the NetworkX library has been imported - if 'networkx' not in sys.modules: - logger.critical('NetworkX library required by dump_topo_json() ' - 'has not been imported. Is it installed?') - return - # Export NetworkX object into a json file - # Json dump of the topology - # - # Get json topology - json_topology = json_graph.node_link_data(graph) - # Convert links - json_topology['links'] = [{ - 'source': link['source'], - 'target': link['target'], - 'type': link.get('type') - } for link in json_topology['links']] - # Convert nodes - json_topology['nodes'] = [{ - 'id': node['id'], - 'ip_address': None, - 'type': node.get('type'), - 'ext_reachability': node.get('ext_reachability') - } for node in json_topology['nodes']] - # Dump the topology - logger.info('*** Exporting topology to %s', topo_file) - with open(topo_file, 'w') as outfile: - json.dump(json_topology, outfile, sort_keys=True, indent=2) - logger.info('Topology exported\n') - - -def dump_topo_yaml(nodes, edges, node_to_systemid, - nodes_file_yaml=None, edges_file_yaml=None): - ''' - Dump the provided set of nodes and edges to a dict representation. - Optionally, nodes and edges are exported as YAML file - ''' - # - # This function depends on the pyaml library, which is a - # optional dependency for this script - # - # Check if the pyaml library has been imported - if 'pyaml' not in sys.modules: - logger.critical('pyaml library required by dump_topo_yaml() ' - 'has not been imported. Is it installed?') - return None, None - # Export nodes in YAML format - nodes_yaml = [{ - '_key': node, - 'type': 'router', - 'ip_address': None, - 'ext_reachability': node_to_systemid[node] - } for node in nodes] - # Write nodes to file - if nodes_file_yaml is not None: - logger.info('*** Exporting topology nodes to %s', nodes_file_yaml) - with open(nodes_file_yaml, 'w') as outfile: - yaml.dump(nodes_yaml, outfile) - # Export edges in YAML format - # Character '/' is not accepted in key strign in arango, using - # '-' instead - edges_yaml = [{ - '_key': '%s-dir1' % edge[2].replace('/', '-'), - '_from': 'nodes/%s' % edge[0], - '_to': 'nodes/%s' % edge[1], - 'type': 'core' - } for edge in edges] + [{ - '_key': '%s-dir2' % edge[2].replace('/', '-'), - '_from': 'nodes/%s' % edge[1], - '_to': 'nodes/%s' % edge[0], - 'type': 'core' - } for edge in edges] - # Write edges to file - if edges_file_yaml is not None: - logger.info('*** Exporting topology edges to %s', edges_file_yaml) - with open(edges_file_yaml, 'w') as outfile: - yaml.dump(edges_yaml, outfile) - logger.info('Topology exported\n') - return nodes_yaml, edges_yaml - - -def connect_telnet(router, port): - ''' - Establish a telnet connection to a router on a given port - ''' - # - # Establish a telnet connection to the router - try: - # Init telnet - telnet_conn = telnetlib.Telnet(router, port, 3) - # Connection established - return telnet_conn - except socket.timeout: - # Timeout expired - logging.error('Error: cannot establish a connection ' - 'to %s on port %s\n', str(router), str(port)) - except socket.error as err: - # Socket error - if err.errno != errno.EINTR: - logging.error('Error: cannot establish a connection ' - 'to %s on port %s\n', str(router), str(port)) - return None - - -# Build NetworkX Topology graph -def build_topo_graph(nodes, edges): - ''' - Convert nodes and edges to a NetworkX graph - ''' - # - # This function depends on the NetworkX library, which is a - # optional dependency for this script - # - # Check if the NetworkX library has been imported - if 'networkx' not in sys.modules: - logger.critical('NetworkX library required by build_topo_graph() ' - 'has not been imported. Is it installed?') - return None - logger.info('*** Building topology graph') - # Topology graph - graph = nx.Graph() - # Add nodes to the graph - for node in nodes: - graph.add_node(node) - # Add edges to the graph - for edge in edges: - graph.add_edge(edge[0], edge[1]) - # Return the networkx graph - logger.info('Graph builded successfully\n') - return graph - - -# Utility function to export the network graph as an image file -def draw_topo(graph, svg_topo_file, dot_topo_file=DOT_FILE_TOPO_GRAPH): - ''' - Export the NetworkX graph to a SVG image - ''' - # - # This function depends on the NetworkX library, which is a - # optional dependency for this script - # - # Check if the NetworkX library has been imported - if 'networkx' not in sys.modules: - logger.critical('NetworkX library required by draw_topo() ' - 'has not been imported. Is it installed?') - return - if 'pygraphviz' not in sys.modules: - logger.critical('pygraphviz library required by dump_topo_yaml() ' - 'has not been imported. Is it installed?') - return - # Create dot topology file, an intermediate representation - # of the topology used to export as an image - logger.info('*** Saving topology graph image to %s', svg_topo_file) - write_dot(graph, dot_topo_file) - os.system('dot -Tsvg %s -o %s' % (dot_topo_file, svg_topo_file)) - logger.info('Topology exported\n') - - -def connect_and_extract_topology_isis(ips_ports, - isisd_pwd=DEFAULT_ISISD_PASSWORD, - verbose=DEFAULT_VERBOSE): - ''' - Establish a telnet connection to isisd process running on a router - and extract the network topology from the router - ''' - # - # pylint: disable=too-many-branches, too-many-locals, too-many-statements - # ISIS password - password = isisd_pwd - # Let's parse the input - routers = [] - ports = [] - # First create the chunk - for ip_port in ips_ports: - # Then parse the chunk - data = ip_port.split("-") - routers.append(data[0]) - ports.append(data[1]) - # Connect to a router and extract the topology - for router, port in zip(routers, ports): - print("\n********* Connecting to %s-%s *********" % (router, port)) - # Init telnet and try to establish a connection to the router - try: - telnet_conn = telnetlib.Telnet(router, port) - except socket.error: - print("Error: cannot establish a connection to " + - str(router) + " on port " + str(port) + "\n") - continue - # - # Extract router hostnames - # - # Insert isisd password - if password: - telnet_conn.read_until(b"Password: ") - telnet_conn.write(password.encode('ascii') + b"\r\n") - try: - # terminal length set to 0 to not have interruptions - telnet_conn.write(b"terminal length 0" + b"\r\n") - # Get routing info from isisd database - telnet_conn.write(b"show isis hostname" + b"\r\n") - # Close - telnet_conn.write(b"q" + b"\r\n") - # Get results - hostname_details = telnet_conn.read_all().decode('ascii') - except BrokenPipeError: - logger.error('Broken pipe. Is the password correct?') - continue - finally: - # Close telnet - telnet_conn.close() - # - # Extract router database - # - # Init telnet and try to establish a connection to the router - try: - telnet_conn = telnetlib.Telnet(router, port) - except socket.error: - print("Error: cannot establish a connection to " + - str(router) + " on port " + str(port) + "\n") - continue - # Insert isisd password - if password: - telnet_conn.read_until(b"Password: ") - telnet_conn.write(password.encode('ascii') + b"\r\n") - # terminal length set to 0 to not have interruptions - telnet_conn.write(b"terminal length 0" + b"\r\n") - # Get routing info from isisd database - telnet_conn.write(b"show isis database detail" + b"\r\n") - # Close - telnet_conn.write(b"q" + b"\r\n") - # Get results - database_details = telnet_conn.read_all().decode('ascii') - # Close telnet - telnet_conn.close() - # Set of System IDs - system_ids = set() - # Set of hostnames - hostnames = set() - # Mapping System ID to hostname - system_id_to_hostname = dict() - # Mapping hostname to System ID - hostname_to_system_id = dict() - # Process hostnames - for line in hostname_details.splitlines(): - # Get System ID and hostname - match = re.search('(\\d+.\\d+.\\d+)\\s+(\\S+)', line) - if match: - # Extract System ID - system_id = match.group(1) - # Extract hostname - hostname = match.group(2) - # Update System IDs - system_ids.add(system_id) - # Update hostnames - hostnames.add(hostname) - # Update mappings - system_id_to_hostname[system_id] = hostname - hostname_to_system_id[hostname] = system_id - # Mapping hostname to reachability - reachability_info = dict() - # Process isis database - hostname = None - # IPv6 subnet addresses of edges - ipv6_reachability = dict() - for line in database_details.splitlines(): - # Get hostname - match = re.search('Hostname: (\\S+)', line) - if match: - # Extract hostname - hostname = match.group(1) - # Update reachability info dict - reachability_info[hostname] = set() - # Get extended reachability - match = re.search( - 'Extended Reachability: (\\d+.\\d+.\\d+).\\d+', line) - if match: - # Extract extended reachability info - reachability = match.group(1) - # Update reachability info dict - if reachability != hostname_to_system_id[hostname]: - reachability_info[hostname].add(reachability) - # IPv6 Reachability: fcf0:0:6:8::/64 (Metric: 10) - match = re.search('IPv6 Reachability: (.+/\\d{1,3})', line) - if match: - ip_addr = match.group(1) - if ip_addr not in ipv6_reachability: - # Update IPv6 reachability dict - ipv6_reachability[ip_addr] = list() - # add hostname to hosts list of the ip address in the ipv6 - # reachability dict - ipv6_reachability[ip_addr].append(hostname) - # Build the topology graph - # - # Nodes - nodes = hostnames - # Edges - _edges = set() - # Edges with subnet IP address - edges = set() - for hostname, system_ids in reachability_info.items(): - for system_id in system_ids: - _edges.add((hostname, system_id_to_hostname[system_id])) - for ip_addr in ipv6_reachability: - # Edge link is bidirectional in this case - # Only take IP addresses of links between 2 nodes - if len(ipv6_reachability[ip_addr]) == 2: - (node1, node2) = ipv6_reachability[ip_addr] - edges.add((node1, node2, ip_addr)) - _edges.remove((node1, node2)) - _edges.remove((node2, node1)) - for (node1, node2) in _edges.copy(): - edges.add((node1, node2, None)) - _edges.remove((node1, node2)) - _edges.remove((node2, node1)) - # Print nodes and edges - if verbose: - print('Topology extraction completed\n') - print("Nodes:", nodes) - print("Edges:", edges) - print("***************************************") - # Return topology information - return nodes, edges, hostname_to_system_id - # No router available to extract the topology - return None, None, None - - -def topology_information_extraction_isis(routers, period, isisd_pwd, - topo_file_json=None, - nodes_file_yaml=None, - edges_file_yaml=None, - topo_graph=None, - verbose=DEFAULT_VERBOSE): - ''' - Run Topology Information Extraction from a set of routers. - Optionally export the topology to a JSON file, YAML file or SVG image - ''' - # - # pylint: disable=too-many-arguments - # Topology Information Extraction - while True: - # Extract the topology information - nodes, edges, node_to_systemid = \ - connect_and_extract_topology_isis( - routers, isisd_pwd, verbose) - # Build and export the topology graph - if topo_file_json is not None or topo_graph is not None: - # Builg topology graph - graph = build_topo_graph(nodes, edges) - # Dump relevant information of the network graph to a JSON file - if topo_file_json is not None: - dump_topo_json(graph, topo_file_json) - # Export the network graph as an image file - if topo_graph is not None: - draw_topo(graph, topo_graph) - # Dump relevant information of the network graph to a YAML file - if nodes_file_yaml is not None or edges_file_yaml: - dump_topo_yaml( - nodes=nodes, - edges=edges, - node_to_systemid=node_to_systemid, - nodes_file_yaml=nodes_file_yaml, - edges_file_yaml=edges_file_yaml - ) - # Period = 0 means a single extraction - if period == 0: - break - # Wait 'period' seconds between two extractions - time.sleep(period) - - -# Parse command line options and dump results -def parse_arguments(): - ''' - Command-line arguments parser - ''' - # - parser = ArgumentParser( - description='Topology Information Ex+traction (from ISIS) ' - 'module for SRv6 Controller' - ) - # ip:port of the routers - parser.add_argument( - '-n', '--node-ips', action='store', dest='nodes', required=True, - help='Comma-separated pairs, where ip is the IP address of ' - 'the router and port is the telnet port of the isisd daemon ' - '(e.g. 2000::1-2606,2000::2-2606,2000::3-2606)' - ) - # Topology Information Extraction period - parser.add_argument( - '-p', '--period', dest='period', type=int, - default=DEFAULT_TOPO_EXTRACTION_PERIOD, help='Polling period ' - '(in seconds); a zero value means a single extraction' - ) - # Path of topology file (JSON) - parser.add_argument( - '-t', '--topology-json', dest='topo_file_json', action='store', - default=DEFAULT_TOPOLOGY_FILE, help='JSON file of the extracted ' - 'topology' - ) - # Path of nodes file (YAML) - parser.add_argument( - '-r', '--nodes-yaml', dest='nodes_file_yaml', action='store', - default=DEFAULT_NODES_YAML_FILE, - help='YAML file of the nodes extracted from the topology' - ) - # Path of edges file (YAML) - parser.add_argument( - '-e', '--edges-yaml', dest='edges_file_yaml', action='store', - default=DEFAULT_EDGES_YAML_FILE, - help='JSON file of the edges extracted from the topology' - ) - # Path of topology graph - parser.add_argument( - '-g', '--topo-graph', dest='topo_graph', action='store', default=None, - help='Image file of the exported NetworkX graph' - ) - # Password used to log in to isisd daemon - parser.add_argument( - '-w', '--password', action='store_true', dest='password', - default=DEFAULT_ISISD_PASSWORD, help='Password of the isisd daemon' - ) - # Debug logs - parser.add_argument( - '-d', '--debug', action='store_true', help='Activate debug logs' - ) - # Verbose mode - parser.add_argument( - '-v', '--verbose', action='store_true', dest='verbose', - default=DEFAULT_VERBOSE, help='Enable verbose mode' - ) - # Parse input parameters - args = parser.parse_args() - # Done, return - return args - - -def __main(): - ''' - Entry point for this module - ''' - # - # Let's parse input parameters - args = parse_arguments() - # Setup properly the logger - if args.debug: - logger.setLevel(logging.INFO) - else: - logger.setLevel(logging.INFO) - # Debug settings - server_debug = logger.getEffectiveLevel() == logging.DEBUG - logger.info('SERVER_DEBUG: %s', str(server_debug)) - # Get topology filename JSON - topo_file_json = args.topo_file_json - # Get nodes filename YAML - nodes_file_yaml = args.nodes_file_yaml - # Get edges filename YAML - edges_file_yaml = args.edges_file_yaml - # Get topology graph image filename - topo_graph = args.topo_graph - if topo_graph is not None and \ - not topo_graph.endswith('.svg'): - # Add file extension - topo_graph = '%s.%s' % (topo_graph, 'svg') - # Nodes - nodes = args.nodes - nodes = nodes.split(',') - # Get period between two extractions - period = args.period - # Verbose mode - verbose = args.verbose - # isisd password - pwd = args.password - # Extract topology and build network graph - topology_information_extraction_isis( - routers=nodes, - period=period, - isisd_pwd=pwd, - topo_file_json=topo_file_json, - nodes_file_yaml=nodes_file_yaml, - edges_file_yaml=edges_file_yaml, - topo_graph=topo_graph, - verbose=verbose - ) - - -if __name__ == '__main__': - __main() diff --git a/control_plane/controller/controller/ti_extraction/__init__.py b/control_plane/controller/controller/ti_extraction/__init__.py new file mode 100644 index 0000000..5652472 --- /dev/null +++ b/control_plane/controller/controller/ti_extraction/__init__.py @@ -0,0 +1,29 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Topology Information Extraction +# +# @author Carmine Scarpitta +# + +''' +This package provides a collection of utilities implementing the extraction of +the network topology from a set of nodes. +''' diff --git a/control_plane/controller/controller/ti_extraction/ti_extraction.py b/control_plane/controller/controller/ti_extraction/ti_extraction.py new file mode 100644 index 0000000..d33b4a4 --- /dev/null +++ b/control_plane/controller/controller/ti_extraction/ti_extraction.py @@ -0,0 +1,170 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Topology information extraction +# +# @author Carmine Scarpitta +# + + +# Topology Information Extraction dependencies +from controller.ti_extraction.ti_extraction_utils import DEFAULT_VERBOSE +from controller.ti_extraction.ti_extraction_isis import ( + connect_and_extract_topology_isis, + topology_information_extraction_isis +) + + +# Global variables definition +# +# +# The following parameters are the default arguments used by the functions +# defined in this module. You can override the default values by passing +# your custom argments to the functions +# +# In our experiment we use 'zebra' as default password for isisd +DEFAULT_PASSWORD = 'zebra' + + +class InvalidProtocolError(Exception): + ''' + The protocol is invalid. + ''' + + +def connect_and_extract_topology(ips_ports, protocol, + password=DEFAULT_PASSWORD, + verbose=DEFAULT_VERBOSE): + ''' + Establish a telnet connection to the routing daemon running on a router + and extract the network topology from the router. + For redundancy purposes, this function accepts a list of routers. + + :param ips_ports: A list of pairs ip-port representing IP and port of + the routers you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type ips_ports: list + :param protocol: The routing protocol from which you want to extract the + topology (currently only 'isis' is supported) + :type protocol: str + :param password: The password used to log in to the daemon. + :type password: str + :param verbose: Define whether the verbose mode must be enable or not + (default: False). + :type verbose: bool, optional + :return: A tuple containing the nodes, the edges and the + hostname-to-SystemID mapping. + Each node is represented by its hostname. + The edges are represented as tuples + (node_left, node_right, ip_address), where node_left and + node_right are the endpoints of the edge and ip_address is + the IP address of the subnet associated to the edge. + The hostname-to-SystemID mapping is a dict. + :rtype: tuple + :raises NoISISNodesAvailableError: The provided set of nodes does not + contain any ISIS node. + :raises InvalidProtocolError: The provided set of nodes does no contain + any ISIS node. + ''' + # Which protocol? + if protocol == 'isis': + # ISIS protocol + res = connect_and_extract_topology_isis( + ips_ports=ips_ports, + password=password, + verbose=verbose + ) + else: + # Unknown protocol + raise InvalidProtocolError + # Return the result + return res + + +def topology_information_extraction(routers, protocol, period, password, + topo_file_json=None, nodes_file_yaml=None, + edges_file_yaml=None, topo_graph=None, + verbose=DEFAULT_VERBOSE): + ''' + Extract topological information from a set of routers running a + routing protocol. The routers must execute an instance of routing + protocols from the routing suite FRRRouting. This function can be also + instructed to repeat the extraction at regular intervals. + Optionally the topology can be exported to a JSON file, YAML file or SVG + image. + + :param routers: A list of pairs ip-port representing IP and port of + the nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type routers: list + :param protocol: The routing protocol from which you want to extract the + topology (currently only 'isis' is supported) + :type protocol: str + :param period: The interval between two consecutive extractions. If this + arguments is equals to 0, this function performs a single + extraction and then returns (default: 0). + :type period: int, optional + :param password: The password used to log in to the routing daemon. + :type d: str + :param topo_file_json: The path and the name of the output JSON file. If + this parameter is not provided, the topology is not + exported to a JSON file (default: None). + :type topo_file_json: str, optional + :param nodes_file_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML + file (default: None). + :type nodes_file_yaml: str, optional + :param edges_file_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML + file (default: None). + :type edges_file_yaml: str, optional + :param topo_graph: The path and the name of the output SVG file containing + the topology graph exported as image. If this parameter + is not provided, the topology is not exported to a SVG + file (default: None). + :type topo_graph: str, optional + :param verbose: Define whether the verbose mode must be enable or not + (default: False). + :type verbose: bool, optional + :return: True. + :rtype: bool + ''' + # pylint: disable=too-many-arguments + # + # Which protocol? + if protocol == 'isis': + # ISIS protocol + res = topology_information_extraction_isis( + routers=routers, + period=period, + password=password, + topo_file_json=topo_file_json, + nodes_file_yaml=nodes_file_yaml, + edges_file_yaml=edges_file_yaml, + topo_graph=topo_graph, + verbose=verbose + ) + else: + # Unknown protocol + raise InvalidProtocolError + # Return the result + return res diff --git a/control_plane/controller/controller/ti_extraction/ti_extraction_isis.py b/control_plane/controller/controller/ti_extraction/ti_extraction_isis.py new file mode 100644 index 0000000..6ef2dba --- /dev/null +++ b/control_plane/controller/controller/ti_extraction/ti_extraction_isis.py @@ -0,0 +1,523 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Topology information extraction from ISIS nodes +# +# @author Carmine Scarpitta +# + + +''' +This module contains several utilities useful to extract the Topology +Information Extraction from a set of nodes running the ISIS protocol. +''' + +# General imports +import errno +import logging +import re +import socket +import telnetlib +import time +from argparse import ArgumentParser + +# Topology Information Extraction dependencies +from controller.ti_extraction.ti_extraction_utils import ( + build_topo_graph, + dump_topo_json, + dump_topo_yaml, + draw_topo, + DEFAULT_TOPOLOGY_FILE, + DEFAULT_NODES_YAML_FILE, + DEFAULT_EDGES_YAML_FILE, + DEFAULT_TOPO_EXTRACTION_PERIOD, + DEFAULT_VERBOSE +) + +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + + +# Global variables definition +# +# +# The following parameters are the default arguments used by the functions +# defined in this module. You can override the default values by passing +# your custom argments to the functions +# +# In our experiment we use 'zebra' as default password for isisd +DEFAULT_ISISD_PASSWORD = 'zebra' + + +class NoISISNodesAvailableError(Exception): + ''' + No ISIS nodes available. + ''' + + +def connect_and_extract_topology_isis(ips_ports, + isisd_pwd=DEFAULT_ISISD_PASSWORD, + verbose=DEFAULT_VERBOSE): + ''' + Establish a telnet connection to the isisd process running on a router + and extract the network topology from the router. + For redundancy purposes, this function accepts a list of routers. + + :param ips_ports: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type ips_ports: list + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param verbose: Define whether the verbose mode must be enable or not + (default: False). + :type verbose: bool, optional + :return: A tuple containing the nodes, the edges and the + hostname-to-SystemID mapping. + Each node is represented by its hostname. + The edges are represented as tuples + (node_left, node_right, ip_address), where node_left and + node_right are the endpoints of the edge and ip_address is + the IP address of the subnet associated to the edge. + The hostname-to-SystemID mapping is a dict. + :rtype: tuple + :raises NoISISNodesAvailableError: The provided set of nodes does not + contain any ISIS node. + ''' + # pylint: disable=too-many-branches, too-many-locals, too-many-statements + # + # Let's parse the input + routers = [] + ports = [] + # First create the chunk + for ip_port in ips_ports: + # Then parse the chunk + # + # Separate IP and port + data = ip_port.split('-') + # Append IP to the routers list + routers.append(data[0]) + # Append port to the ports list + ports.append(data[1]) + # Connect to a router and extract the topology + for router, port in zip(routers, ports): + logger.debug('\n********* Connecting to %s-%s *********', + router, port) + # #################################################################### + # Extract router hostnames + try: + # Init telnet and try to establish a connection to the router + telnet_conn = telnetlib.Telnet(router, port) + # Insert isisd password + if isisd_pwd: + telnet_conn.read_until(b'Password: ') + telnet_conn.write(isisd_pwd.encode('ascii') + b'\r\n') + # Terminal length set to 0 to not have interruptions + telnet_conn.write(b'terminal length 0' + b'\r\n') + # Show information about ISIS node + telnet_conn.write(b'show isis hostname' + b'\r\n') + # Exit from the isisd console + telnet_conn.write(b'q' + b'\r\n') + # Convert the extracted information to a string + hostname_details = telnet_conn.read_all().decode('ascii') + except socket.timeout: + # Cannot establish a connection to isisd: timeout expired + logging.error('Error: cannot establish a connection ' + 'to %s on port %s\n', router, port) + except socket.error as err: + # Cannot establish a connection to isisd: socket error + if err.errno != errno.EINTR: + logger.warning('Cannot establish a connection to %s on port' + '%s\n', router, port) + # Let's try to connect to the next router in the list + continue + except BrokenPipeError: + # Telnetlib returned 'BrokenPipeError' + # This can happen if you entered the wrong password + logger.error('Broken pipe. Is the password correct?') + # Let's try to connect to the next router in the list + continue + finally: + # Close telnet + telnet_conn.close() + # #################################################################### + # Extract router database + try: + # Init telnet and try to establish a connection to the router + telnet_conn = telnetlib.Telnet(router, port) + # Insert isisd password + if isisd_pwd: + telnet_conn.read_until(b'Password: ') + telnet_conn.write(isisd_pwd.encode('ascii') + b'\r\n') + # Terminal length set to 0 to not have interruptions + telnet_conn.write(b'terminal length 0' + b'\r\n') + # Show the ISIS database globally, with details. + telnet_conn.write(b'show isis database detail' + b'\r\n') + # Exit from the isisd console + telnet_conn.write(b'q' + b'\r\n') + # Convert the extracted information to a string + database_details = telnet_conn.read_all().decode('ascii') + except socket.error: + # Cannot establish a connection to isisd + logger.warning('Cannot establish a connection to %s on port %s\n', + router, port) + # Let's try to connect to the next router in the list + continue + except BrokenPipeError: + # Telnetlib returned 'BrokenPipeError' + # This can happen if you entered the wrong password + logger.error('Broken pipe. Is the password correct?') + # Let's try to connect to the next router in the list + continue + finally: + # Close telnet + telnet_conn.close() + # #################################################################### + # Process the extracted information + # + # Set of System IDs + system_ids = set() + # Set of hostnames + hostnames = set() + # Mapping System ID to hostname + system_id_to_hostname = dict() + # Mapping hostname to System ID + hostname_to_system_id = dict() + # Reachability info dict: it maps each node hostname to the System IDs + # reachable from it + reachability_info = dict() + # IPv6 reachability dict: it maps IPv6 subnet addresses of the edges + # to the hostnames of the ISIS nodes that are able to reach them + ipv6_reachability = dict() + # Process hostnames + for line in hostname_details.splitlines(): + # Get System ID and hostname + match = re.search('(\\d+.\\d+.\\d+)\\s+(\\S+)', line) + if match: + # Extract System ID + system_id = match.group(1) + # Extract hostname + hostname = match.group(2) + # Update System IDs + system_ids.add(system_id) + # Update hostnames + hostnames.add(hostname) + # Update mappings + system_id_to_hostname[system_id] = hostname + hostname_to_system_id[hostname] = system_id + # Process the ISIS database + for line in database_details.splitlines(): + # Extract the hostname + match = re.search('Hostname: (\\S+)', line) + if match: + # Get the hostname + hostname = match.group(1) + # Add the hostname to the reachability info dict + reachability_info[hostname] = set() + # Extract the extended reachability + match = re.search( + 'Extended Reachability: (\\d+.\\d+.\\d+).\\d+', line) + if match: + # Get the extended reachability info + reachability = match.group(1) + # Add reachability info to the dict + # We exclude the self reachability information from the dict + if reachability != hostname_to_system_id[hostname]: + reachability_info[hostname].add(reachability) + # IPv6 Reachability, e.g. fcf0:0:6:8::/64 (Metric: 10) + match = re.search('IPv6 Reachability: (.+/\\d{1,3})', line) + if match: + # Extract the IPv6 address + ip_addr = match.group(1) + # If not initialized, init IPv6 reachability + if ip_addr not in ipv6_reachability: + # Add IPv6 address to the IPv6 reachability dict + ipv6_reachability[ip_addr] = list() + # Add the hostname to the hosts list of the ip address in the + # ipv6 reachability dict + ipv6_reachability[ip_addr].append(hostname) + # #################################################################### + # Build the topology graph + # + # Nodes set + nodes = hostnames + # Edges set used to store temporary information about the edges + edges_tmp = set() + # Edges set, containing tuple (node_left, node_right, ip_address) + # This set is obtained from the edges_tmp set by adding the IPv6 + # address of the subnet for each edge + edges = set() + # Process the reachability info dict and build a temporary set of + # edges + for hostname, system_ids in reachability_info.items(): + # 'system_ids' is a list containg all the System IDs that are + # reachable from 'hostname' + for system_id in system_ids: + # Translate System ID to hostname and append the edge to the + # edges set + # Each edge is represented as a pair (hostname1, hostname2), + # where the two hostnames are the endpoints of the edge + edges_tmp.add((hostname, system_id_to_hostname[system_id])) + # Process the IPv6 reachability info dict and build an edges set + # containing the edges (obtained from the edges_tmp set) enriched with + # the IPv6 addresses of their subnets + for ip_addr in ipv6_reachability: + # Edge link is bidirectional in this case + # Only take IP addresses of links between 2 nodes + if len(ipv6_reachability[ip_addr]) == 2: + # Take the edge + (node1, node2) = ipv6_reachability[ip_addr] + # Add the IP address of the subnet to the edge and append it to + # the edges set + edges.add((node1, node2, ip_addr)) + # Remove the edge from the temporary set + edges_tmp.remove((node1, node2)) + edges_tmp.remove((node2, node1)) + # For the remaining edges, we don't have IPv6 reachability information + # Therefore we set their IPv6 addresses to None + for (node1, node2) in edges_tmp.copy(): + # Add the edge to the edges set + edges.add((node1, node2, None)) + # Remove the edge from the temporary set + edges_tmp.remove((node1, node2)) + edges_tmp.remove((node2, node1)) + # If the verbose mode is enabled, print nodes and edges extracted from + # the ISIS node + logger.info('Topology extraction completed\n') + if verbose: + logger.info('Nodes:', nodes) + logger.info('Edges:', edges) + logger.info('***************************************') + # Return topology information + return nodes, edges, hostname_to_system_id + # No router available to extract the topology + logger.error('No ISIS node is available') + raise NoISISNodesAvailableError + + +def topology_information_extraction_isis(routers, period, isisd_pwd, + topo_file_json=None, + nodes_file_yaml=None, + edges_file_yaml=None, + topo_graph=None, + verbose=DEFAULT_VERBOSE): + ''' + Extract topological information from a set of routers running the + ISIS protocol. The routers must execute an instance of isisd from the + routing suite FRRRouting. This function can be also instructed to repeat + the extraction at regular intervals. + Optionally the topology can be exported to a JSON file, YAML file or SVG + image. + + :param routers: A list of pairs ip-port representing IP and port of + the ISIS nodes you want to extract the topology from + (e.g. ['fcff:1::1-2608', 'fcff:2::1-2608']). + :type routers: list + :param period: The interval between two consecutive extractions. If this + arguments is equals to 0, this function performs a single + extraction and then returns (default: 0). + :type period: int, optional + :param isisd_pwd: The password used to log in to isisd. + :type isisd_pwd: str + :param topo_file_json: The path and the name of the output JSON file. If + this parameter is not provided, the topology is not + exported to a JSON file (default: None). + :type topo_file_json: str, optional + :param nodes_file_yaml: The path and the name of the output YAML file + containing the nodes. If this parameter is not + provided, the nodes are not exported to a YAML + file (default: None). + :type nodes_file_yaml: str, optional + :param edges_file_yaml: The path and the name of the output YAML file + containing the edges. If this parameter is not + provided, the edges are not exported to a YAML + file (default: None). + :type edges_file_yaml: str, optional + :param topo_graph: The path and the name of the output SVG file containing + the topology graph exported as image. If this parameter + is not provided, the topology is not exported to a SVG + file (default: None). + :type topo_graph: str, optional + :param verbose: Define whether the verbose mode must be enable or not + (default: False). + :type verbose: bool, optional + :return: True. + :rtype: bool + ''' + # pylint: disable=too-many-arguments + # + # Topology Information Extraction + while True: + # Extract the topology information form ISIS + nodes, edges, node_to_systemid = connect_and_extract_topology_isis( + routers, isisd_pwd, verbose) + # Build and export the topology graph + if topo_file_json is not None or topo_graph is not None: + # Build topology graph + graph = build_topo_graph(nodes, edges) + # Export relevant information of the network graph to a JSON file + if topo_file_json is not None: + dump_topo_json(graph, topo_file_json) + # Export the network graph as an image file + if topo_graph is not None: + draw_topo(graph, topo_graph) + # Export relevant information of the network graph to a YAML file + if nodes_file_yaml is not None or edges_file_yaml: + dump_topo_yaml( + nodes=nodes, + edges=edges, + node_to_systemid=node_to_systemid, + nodes_file_yaml=nodes_file_yaml, + edges_file_yaml=edges_file_yaml + ) + # Period = 0 means a single extraction + if period == 0: + break + # Wait 'period' seconds between two extractions + time.sleep(period) + # Done, return + return True + + +# Parse command line options and dump results +def parse_arguments(): + ''' + Command-line arguments parser + ''' + # Initialize parser + parser = ArgumentParser( + description='Topology Information Extraction (from ISIS) ' + 'module for SRv6 Controller' + ) + # Comma-separated pairs of the routers + parser.add_argument( + '-n', '--node-ips', action='store', dest='nodes', required=True, + help='Comma-separated pairs, where ip is the IP address of ' + 'the router and port is the telnet port of the isisd daemon ' + '(e.g. 2000::1-2608,2000::2-2608,2000::3-2608)' + ) + # Interval between two consecutive extractions + parser.add_argument( + '-p', '--period', dest='period', type=int, + default=DEFAULT_TOPO_EXTRACTION_PERIOD, help='Polling period ' + '(in seconds); a zero value means a single extraction' + ) + # Path of topology file (JSON file) + parser.add_argument( + '-t', '--topology-json', dest='topo_file_json', action='store', + default=DEFAULT_TOPOLOGY_FILE, help='JSON file of the extracted ' + 'topology' + ) + # Path of nodes file (YAML file) + parser.add_argument( + '-r', '--nodes-yaml', dest='nodes_file_yaml', action='store', + default=DEFAULT_NODES_YAML_FILE, + help='YAML file of the nodes extracted from the topology' + ) + # Path of edges file (YAML file) + parser.add_argument( + '-e', '--edges-yaml', dest='edges_file_yaml', action='store', + default=DEFAULT_EDGES_YAML_FILE, + help='YAML file of the edges extracted from the topology' + ) + # Path of topology graph (SVG file) + parser.add_argument( + '-g', '--topo-graph', dest='topo_graph', action='store', default=None, + help='Image file of the exported NetworkX graph' + ) + # Password used to log in to isisd daemon + parser.add_argument( + '-w', '--password', action='store_true', dest='password', + default=DEFAULT_ISISD_PASSWORD, help='Password of the isisd daemon' + ) + # Debug logs + parser.add_argument( + '-d', '--debug', action='store_true', help='Activate debug logs' + ) + # Verbose mode + parser.add_argument( + '-v', '--verbose', action='store_true', dest='verbose', + default=DEFAULT_VERBOSE, help='Enable verbose mode' + ) + # Parse input parameters + args = parser.parse_args() + # Done, return + return args + + +def __main(): + ''' + Entry point for this module + ''' + # Let's parse input parameters + args = parse_arguments() + # Setup properly the logger + if args.debug: + logger.setLevel(logging.DEBUG) + else: + logger.setLevel(logging.INFO) + # Debug settings + server_debug = logger.getEffectiveLevel() == logging.DEBUG + logger.info('SERVER_DEBUG: %s', str(server_debug)) + # Get the name of the output JSON file + topo_file_json = args.topo_file_json + # Get name of the output YAML file where we want to save the nodes of the + # extracted topology + nodes_file_yaml = args.nodes_file_yaml + # Get name of the output YAML file where we want to save the edges of the + # extracted topology + edges_file_yaml = args.edges_file_yaml + # Get name of the output image file + # Currently we support only svg format + topo_graph = args.topo_graph + if topo_graph is not None and not topo_graph.endswith('.svg'): + # Add file extension + topo_graph = '%s.%s' % (topo_graph, 'svg') + # 'nodes' is a string containtaing comma-separated pairs + # We need to convert this string to a list by splitting elements + # separated by commas + nodes = args.nodes + nodes = nodes.split(',') + # Get period between two consecutive extractions + period = args.period + # Verbose mode + verbose = args.verbose + # Password of isisd + pwd = args.password + # Extract the topology and build the network graph + # If period > 0, this function will block forever or until an exception + # is raised + # If period = 0, this function will perform a single topology extraction + # and then it returns + topology_information_extraction_isis( + routers=nodes, + period=period, + isisd_pwd=pwd, + topo_file_json=topo_file_json, + nodes_file_yaml=nodes_file_yaml, + edges_file_yaml=edges_file_yaml, + topo_graph=topo_graph, + verbose=verbose + ) + + +if __name__ == '__main__': + __main() diff --git a/control_plane/controller/controller/ti_extraction/ti_extraction_utils.py b/control_plane/controller/controller/ti_extraction/ti_extraction_utils.py new file mode 100644 index 0000000..3807af1 --- /dev/null +++ b/control_plane/controller/controller/ti_extraction/ti_extraction_utils.py @@ -0,0 +1,360 @@ +#!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Topology information extraction +# +# @author Carmine Scarpitta +# + + +''' +Topology Information Extraction utilities. +''' + +# General imports +import json +import logging +import os +import sys + +# Logger reference +logging.basicConfig(level=logging.NOTSET) +logger = logging.getLogger(__name__) + +# Optional imports: +# NetworkX - only required to export the topology in JSON format +# and to draw the topology +# pyaml - only required to export the topology in YAML format +# pygraphviz - only required to export the topology to an image file +try: + import networkx as nx + from networkx.drawing.nx_agraph import write_dot + from networkx.readwrite import json_graph +except ImportError: + logger.warning('networkx library is not installed') +try: + from pyaml import yaml +except ImportError: + logger.warning('pyaml library is not installed') +try: + import pygraphviz # pylint: disable=unused-import # noqa: F401 +except ImportError: + logger.warning('pygraphviz library is not installed') + + +# Global variables definition +# +# +# The following parameters are the default arguments used by the functions +# defined in this module. You can override the default values by passing +# your custom argments to the functions +# +# JSON file containing the exported topology +DEFAULT_TOPOLOGY_FILE = '/tmp/topology.json' +# YAML file containing the exported nodes +DEFAULT_NODES_YAML_FILE = '/tmp/nodes.yaml' +# YAML file containing the exported edges +DEFAULT_EDGES_YAML_FILE = '/tmp/edges.yaml' +# Interval between two consecutive extractions (in seconds) +DEFAULT_TOPO_EXTRACTION_PERIOD = 0 +# DOT file containing an intermediate representation of the toplogy used to +# draw the topology graph +DOT_FILE_TOPO_GRAPH = '/tmp/topology.dot' +# Define whether the verbose mode is enabled or not by default +DEFAULT_VERBOSE = False + + +class OptionalModuleNotLoadedError(Exception): + ''' + The requested feature depends on an optional module that has not been + loaded. + ''' + + +# Utility function to dump relevant information of the topology to a JSON file +def dump_topo_json(graph, topo_file): + ''' + Export the topology graph to a JSON file. + + :param graph: The graph to be exported. + :type graph: class: `networkx.Graph` + :param topo_file: The path and the name of the output JSON file. + :type topo_file: str + :return: True. + :rtype: bool + :raises OptionalModuleNotLoadedError: The NetworkX module required by + dump_topo_json has not has not been + loaded. Is it installed? + ''' + # Export the topology to a JSON file + logger.debug('*** Exporting topology to %s', topo_file) + # This function depends on the NetworkX library, which is a + # optional dependency for this script + # + # Check if the NetworkX library has been imported + if 'networkx' not in sys.modules: + logger.critical('NetworkX library required by dump_topo_json() ' + 'has not been imported. Is it installed?') + raise OptionalModuleNotLoadedError + # Export NetworkX object to a json file (json dump of the topology) + # + # Convert the graph to a node-link format that is suitable for JSON + # serialization + json_topology = json_graph.node_link_data(graph) + # Remove useless information from the links + # A link has the following properties: + # - source, the source of the link + # - target, the destination of the link + # - type, the type of the link (i.e. 'core' or 'edge') + json_topology['links'] = [{ + 'source': link['source'], + 'target': link['target'], + 'type': link.get('type') + } for link in json_topology['links']] + # Remove useless information from the nodes + # IP address is unknown because it is not contained in the nodes + # information, therefore we set it to None + # You can do post-process on the JSON file to add the IP addresses of the + # nodes + # A node has the following properites: + # - id, an identifier for the node + # - ip_address, for the routers the loopback address, for the hosts the + # the IP address of an interface + # - type, the type of the node (i.e. 'router' or 'host') + # - ext_reachability, the System ID of the node # TODO check this! + json_topology['nodes'] = [{ + 'id': node['id'], + 'ip_address': None, + 'type': node.get('type'), + 'ext_reachability': node.get('ext_reachability') + } for node in json_topology['nodes']] + # Export the topology to a JSON file + with open(topo_file, 'w') as outfile: + json.dump(json_topology, outfile, sort_keys=True, indent=2) + # Done, return + logger.info('*** Topology exported\n') + return True + + +def dump_topo_yaml(nodes, edges, node_to_systemid=None, + nodes_file_yaml=None, edges_file_yaml=None): + ''' + Export the topology graph to a YAML file and return YAML-like + representations of the nodes and the edges. + + :param nodes: List of nodes. Each node is represented as a string (e.g. + the hostname). + :type nodes: set + :param edges: List of edges. The edges are represented as tuples + (node_left, node_right, ip_address), where node_left and + node_right are the endpoints of the edge and ip_address is + the IP address of the subnet associated to the edge. + :type edges: set + :param node_to_systemid: A dict mapping hostnames to System IDs. If this + argument is not provided, the System ID + information is not exported. + :type node_to_systemid: dict, optional + :param nodes_file_yaml: The path and the name of the output YAML file + containing the nodes. If this argument is not + provided, the nodes are not exported to a file. + :type nodes_file_yaml: str, optional + :param edges_file_yaml: The path and the name of the output YAML file + containing the edges. If this argument is not + provided, the edges are not exported to a file. + :type edges_file_yaml: str, optional + :return: A pair (nodes, edges), where nodes is a list containing the nodes + represented as dicts and edges is a list containing the edges + represented as dicts. + A node has the following fields: + - _key, an identifier for the node + - ip_address, for the routers the loopback address, for the + hosts the the IP address of an interface + - type, the type of the node (i.e. 'router' or 'host') + - ext_reachability, the System ID of the node + A link has the following fields: + - _key, an identifier for the edge + - _from, the source of the link + - _to, the destination of the link + - type, the type of the link (i.e. 'core' or 'edge') + :rtype: tuple + :raises OptionalModuleNotLoadedError: The pyaml module required by + dump_topo_yaml has not has not been + loaded. Is it installed? + ''' + # Export the topology to a YAML file + logger.debug('*** Exporting topology to YAML file') + # This function depends on the pyaml library, which is a + # optional dependency for this script + # + # Check if the pyaml library has been imported + if 'pyaml' not in sys.modules: + logger.critical('pyaml library required by dump_topo_yaml() ' + 'has not been imported. Is it installed?') + raise OptionalModuleNotLoadedError + # node_to_systemid is a optional argument + # If not passed, we init it to an empty dict and the System ID information + # is not exported + if node_to_systemid is None: + node_to_systemid = dict() + # Export nodes in YAML format + # A node has the following properites: + # - _key, an identifier for the node + # - ip_address, for the routers the loopback address, for the hosts the + # the IP address of an interface + # - type, the type of the node (i.e. 'router' or 'host') + # - ext_reachability, the System ID of the node # TODO check this! + # IP address is unknown because it is not contained in the nodes + # information, therefore we set it to None + # You can do post-process on the JSON file to add the IP addresses of the + # nodes + nodes_yaml = [{ + '_key': node, + 'type': 'router', + 'ip_address': None, + 'ext_reachability': node_to_systemid.get(node) + } for node in nodes] + # Write nodes to a YAML file + if nodes_file_yaml is not None: + logger.debug('*** Exporting topology nodes to %s', nodes_file_yaml) + with open(nodes_file_yaml, 'w') as outfile: + yaml.dump(nodes_yaml, outfile) + # Export edges in YAML format + # A link has the following properties: + # - _key, an identifier for the edge + # - _from, the source of the link + # - _to, the destination of the link + # - type, the type of the link (i.e. 'core' or 'edge') + # Character '/' is not accepted in key string in arango, using + # '-' instead + # Since the edges are unidirectional, for each link in the graph we need + # two separate edges + edges_yaml = [{ + '_key': '%s-dir1' % edge[2].replace('/', '-'), + '_from': 'nodes/%s' % edge[0], + '_to': 'nodes/%s' % edge[1], + 'type': 'core' + } for edge in edges] + [{ + '_key': '%s-dir2' % edge[2].replace('/', '-'), + '_from': 'nodes/%s' % edge[1], + '_to': 'nodes/%s' % edge[0], + 'type': 'core' + } for edge in edges] + # Write edges to a YAML file + if edges_file_yaml is not None: + logger.debug('*** Exporting topology edges to %s', edges_file_yaml) + with open(edges_file_yaml, 'w') as outfile: + yaml.dump(edges_yaml, outfile) + # Done, return a YAML like representation of the nodes and the edges + # Both nodes and edges are lists of entities containing the properities + # described in the above comments + logger.info('Topology exported\n') + return nodes_yaml, edges_yaml + + +# Build NetworkX Topology graph +def build_topo_graph(nodes, edges): + ''' + Take a set of nodes and a set of edges and build a NetworkX topology + graph. + + :param nodes: List of nodes. Each node is represented as a string (e.g. + the hostname). + :type nodes: set + :param edges: List of edges. The edges are represented as tuples + (node_left, node_right, ip_address), where node_left and + node_right are the endpoints of the edge and ip_address is + the IP address of the subnet associated to the edge. + :type edges: set + :return: The network graph. + :rtype: class: `networkx.Graph` + :raises OptionalModuleNotLoadedError: The NetworkX module required by + build_topo_graph has not been + loaded. Is it installed? + ''' + # This function generates a NetworkX graph starting from a set of nodes + # and a set of edges + logger.debug('*** Building topology graph') + # This function depends on the NetworkX library, which is a + # optional dependency for this script + # + # Check if the NetworkX library has been imported + if 'networkx' not in sys.modules: + logger.critical('NetworkX library required by build_topo_graph() ' + 'has not been imported. Is it installed?') + return None + # Generate an empty NetworkX graph + graph = nx.Graph() + # Add nodes to the graph + for node in nodes: + graph.add_node(node) + # Add edges to the graph + # Only node_left and node_right are added to the graph, ip_address is + # ignored + for edge in edges: + graph.add_edge(edge[0], edge[1]) + # Return the NetworkX graph + logger.info('*** Graph builded successfully\n') + return graph + + +# Utility function to export the NetworkX graph as an image file +def draw_topo(graph, svg_topo_file, dot_topo_file=DOT_FILE_TOPO_GRAPH): + ''' + Export the NetworkX graph to a SVG image. + + :param graph: The graph to be exported. + :type graph: class: `networkx.Graph` + :param svg_topo_file: The path and the name of the output .svg file. + :type svg_topo_file: str + :param dot_topo_file: The path and the name of the .dot file required to + draw the graph. This is just a temporary file with + containing an intermediate representation of the + topology (default: /tmp/topology.dot). + :type dot_topo_file: str, optional + :return: True. + :rtype: bool + :raises OptionalModuleNotLoadedError: NetworkX or pygraph modules required + by draw_topo has not been loaded. + Are they installed? + ''' + # This function exports the topology graph as an image file (in svg + # format) + logger.debug('*** Saving topology graph image to %s', svg_topo_file) + # This function depends on the NetworkX and pygraphviz libraries, which + # are optional dependencies for this script + # + # Check if the NetworkX library has been imported + if 'networkx' not in sys.modules: + logger.critical('NetworkX library required by draw_topo() ' + 'has not been imported. Is it installed?') + raise OptionalModuleNotLoadedError + # Check if the pygraphviz library has been imported + if 'pygraphviz' not in sys.modules: + logger.critical('pygraphviz library required by draw_topo() ' + 'has not been imported. Is it installed?') + raise OptionalModuleNotLoadedError + # Create dot topology file, an intermediate representation + # of the topology used to export as an image + write_dot(graph, dot_topo_file) + # Convert .dot to .svg + os.system('dot -Tsvg %s -o %s' % (dot_topo_file, svg_topo_file)) + logger.info('*** Topology exported\n') + # Done, return + return True diff --git a/control_plane/controller/controller/utils.py b/control_plane/controller/controller/utils.py index 1e71e61..b89975a 100644 --- a/control_plane/controller/controller/utils.py +++ b/control_plane/controller/controller/utils.py @@ -18,7 +18,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# Utils for controller +# Utils for SDN Controller # # @author Carmine Scarpitta # @@ -37,27 +37,65 @@ import grpc # Proto dependencies -import commons_pb2 +from commons_pb2 import (STATUS_SUCCESS, + STATUS_OPERATION_NOT_SUPPORTED, + STATUS_BAD_REQUEST, + STATUS_INTERNAL_ERROR, + STATUS_INVALID_GRPC_REQUEST, + STATUS_FILE_EXISTS, + STATUS_NO_SUCH_PROCESS, + STATUS_INVALID_ACTION, + STATUS_GRPC_SERVICE_UNAVAILABLE, + STATUS_GRPC_UNAUTHORIZED, + STATUS_NOT_CONFIGURED, + STATUS_ALREADY_CONFIGURED, + STATUS_NO_SUCH_DEVICE) # Logger reference logging.basicConfig(level=logging.NOTSET) logger = logging.getLogger(__name__) -# Utiliy function to check if the IP -# is a valid IPv6 address +class InvalidArgumentError(Exception): + ''' + Invalid argument. + ''' + + +class PolicyNotFoundError(Exception): + ''' + Policy not found. + ''' + + +class NoMeasurementDataAvailableError(Exception): + ''' + No measurement data are available. + ''' +# Utiliy function to check if an IP address is a valid IPv6 address def validate_ipv6_address(ip_address): ''' - Return True if the provided IP address is a valid IPv6 address + Return True if the provided IP address is a valid IPv6 address. + + :param ip_address: The IP address to validate. + :type ip_address: str + :return: True if the IP address is a valid IPv6 address, False otherwise. + :rtype: bool ''' if ip_address is None: + # No address provided return False try: + # Try to cast the provided argument to an IPv6Interface object IPv6Interface(ip_address) + # If the cast gives no exceptions, the argument is a valid IPv6 + # address return True except AddressValueError: + # If the cast results in a AddressValueError exception, the provided + # argument is not a IPv6 address return False @@ -65,40 +103,63 @@ def validate_ipv6_address(ip_address): # is a valid IPv4 address def validate_ipv4_address(ip_address): ''' - Return True if the provided IP address is a valid IPv4 address + Return True if the provided IP address is a valid IPv4 address. + + :param ip_address: The IP address to validate. + :type ip_address: str + :return: True if the IP address is a valid IPv4 address, False otherwise. + :rtype: bool ''' if ip_address is None: + # No address provided return False try: + # Try to cast the provided argument to an IPv4Interface object IPv4Interface(ip_address) + # If the cast gives no exceptions, the argument is a valid IPv4 + # address return True except AddressValueError: + # If the cast results in a AddressValueError exception, the provided + # argument is not a IPv4 address return False # Utiliy function to get the IP address family def get_address_family(ip_address): ''' - Return the family of the provided IP address - or None if the IP is invalid + Return the family of the provided IP address or None if the IP is invalid. + + :param ip_address: The IP address to validate. + :type ip_address: str + :return: An integer representing the address family. This can be: + - socket.AF_INET (for IPv4 address family) + - socket.AF_INET6 (for IPv6 address family) + :rtype: int ''' + # Is an IPv6 address? if validate_ipv6_address(ip_address): - # IPv6 address return AF_INET6 + # Is an IPv4 address? if validate_ipv4_address(ip_address): - # IPv4 address return AF_INET # Invalid address return None -# Utiliy function to check if the IP -# is a valid IP address +# Utiliy function to check if an IP address is a valid IP address (IPv6 +# address or IPv4 address) def validate_ip_address(ip_address): ''' Return True if the provided IP address - is a valid IPv4 or IPv6 address''' - # + is a valid IPv4 or IPv6 address + + :param ip_address: The IP address to validate. + :type ip_address: str + :return: True if the IP address is a valid IP address, False otherwise. + :rtype: bool + ''' + # Is a valid IPv6 address or IPv4 address? return validate_ipv4_address(ip_address) or \ validate_ipv6_address(ip_address) @@ -106,17 +167,23 @@ def validate_ip_address(ip_address): # Build a grpc stub def get_grpc_session(server_ip, server_port, secure=False, certificate=None): ''' - Create a Channel to a server. + Create a gRPC Channel to a server. - :param server_ip: The IP address of the gRPC server + :param server_ip: The IP address of the gRPC server. :type server_ip: str - :param server_port: The port of the gRPC server + :param server_port: The port of the gRPC server. :type server_port: int - + :param secure: Define whether to use a secure channel instead of a + unsecure one. Secure channels use TLS instead of TCP. + :type secure: bool, optional + :param certificate: File containing the certificate needed for the secure + mode. + :type certificate: str, optional :return: The requested gRPC Channel or None if the operation has failed. :rtype: class: `grpc._channel.Channel` + :raises InvalidArgumentError: Either the gRPC address or the certificate + file are invalid. ''' - # Get family of the gRPC IP addr_family = get_address_family(server_ip) # Build address depending on the family @@ -129,12 +196,14 @@ def get_grpc_session(server_ip, server_port, secure=False, certificate=None): else: # Invalid address logger.fatal('Invalid gRPC address: %s', server_ip) - return None + raise InvalidArgumentError # If secure we need to establish a channel with the secure endpoint if secure: + # Secure mode requires a certificate file (certificate of a + # Certification Authority) if certificate is None: - logger.fatal('Certificate required for gRPC secure mode') - return None + logger.fatal('Certificate is required for gRPC secure mode') + raise InvalidArgumentError # Open the certificate file with open(certificate, 'rb') as certificate_file: certificate = certificate_file.read() @@ -142,26 +211,27 @@ def get_grpc_session(server_ip, server_port, secure=False, certificate=None): grpc_client_credentials = grpc.ssl_channel_credentials(certificate) channel = grpc.secure_channel(server_ip, grpc_client_credentials) else: + # Secure mode is disabled, establish a insecure channel channel = grpc.insecure_channel(server_ip) # Return the channel return channel -# Human-readable gRPC return status +# Human-readable representation of the gRPC return statuses +# This is used to convert the status codes returned by the gRPCs into textual +# human-readable descriptions status_code_to_str = { - commons_pb2.STATUS_SUCCESS: 'Success', - commons_pb2.STATUS_OPERATION_NOT_SUPPORTED: ('Operation ' - 'not supported'), - commons_pb2.STATUS_BAD_REQUEST: 'Bad request', - commons_pb2.STATUS_INTERNAL_ERROR: 'Internal error', - commons_pb2.STATUS_INVALID_GRPC_REQUEST: 'Invalid gRPC request', - commons_pb2.STATUS_FILE_EXISTS: 'An entity already exists', - commons_pb2.STATUS_NO_SUCH_PROCESS: 'Entity not found', - commons_pb2.STATUS_INVALID_ACTION: 'Invalid seg6local action', - commons_pb2.STATUS_GRPC_SERVICE_UNAVAILABLE: ('gRPC service not ' - 'available'), - commons_pb2.STATUS_GRPC_UNAUTHORIZED: 'Unauthorized', - commons_pb2.STATUS_NOT_CONFIGURED: 'Node not configured' + STATUS_SUCCESS: 'Success', + STATUS_OPERATION_NOT_SUPPORTED: 'Operation not supported', + STATUS_BAD_REQUEST: 'Bad request', + STATUS_INTERNAL_ERROR: 'Internal error', + STATUS_INVALID_GRPC_REQUEST: 'Invalid gRPC request', + STATUS_FILE_EXISTS: 'An entity already exists', + STATUS_NO_SUCH_PROCESS: 'Entity not found', + STATUS_INVALID_ACTION: 'Invalid seg6local action', + STATUS_GRPC_SERVICE_UNAVAILABLE: 'gRPC service not available', + STATUS_GRPC_UNAUTHORIZED: 'Unauthorized', + STATUS_NOT_CONFIGURED: 'Node not configured' } @@ -178,30 +248,30 @@ def print_status_message(status_code, success_msg, failure_msg): :type failure_msg: str ''' # - if status_code == commons_pb2.STATUS_SUCCESS: + if status_code == STATUS_SUCCESS: # Success - print('%s (status code %s - %s)' - % (success_msg, status_code, - status_code_to_str.get(status_code, 'Unknown'))) + logger.info('%s (status code %s - %s)' + % (success_msg, status_code, + status_code_to_str.get(status_code, 'Unknown'))) else: # Error - print('%s (status code %s - %s)' - % (failure_msg, status_code, - status_code_to_str.get(status_code, 'Unknown'))) + logger.error('%s (status code %s - %s)' + % (failure_msg, status_code, + status_code_to_str.get(status_code, 'Unknown'))) STATUS_CODE_TO_DESC = { - commons_pb2.STATUS_SUCCESS: 'Operation completed successfully', - commons_pb2.STATUS_OPERATION_NOT_SUPPORTED: 'Error: Operation not supported', - commons_pb2.STATUS_BAD_REQUEST: 'Error: Bad request', - commons_pb2.STATUS_INTERNAL_ERROR: 'Error: Internal error', - commons_pb2.STATUS_INVALID_GRPC_REQUEST: 'Error: Invalid gRPC request', - commons_pb2.STATUS_FILE_EXISTS: 'Error: Entity already exists', - commons_pb2.STATUS_NO_SUCH_PROCESS: 'Error: Entity not found', - commons_pb2.STATUS_INVALID_ACTION: 'Error: Invalid action', - commons_pb2.STATUS_GRPC_SERVICE_UNAVAILABLE: 'Error: Unreachable grPC server', - commons_pb2.STATUS_GRPC_UNAUTHORIZED: 'Error: Unauthorized', - commons_pb2.STATUS_NOT_CONFIGURED: 'Error: Not configured', - commons_pb2.STATUS_ALREADY_CONFIGURED: 'Error: Already configured', - commons_pb2.STATUS_NO_SUCH_DEVICE: 'Error: Device not found', + STATUS_SUCCESS: 'Operation completed successfully', + STATUS_OPERATION_NOT_SUPPORTED: 'Error: Operation not supported', + STATUS_BAD_REQUEST: 'Error: Bad request', + STATUS_INTERNAL_ERROR: 'Error: Internal error', + STATUS_INVALID_GRPC_REQUEST: 'Error: Invalid gRPC request', + STATUS_FILE_EXISTS: 'Error: Entity already exists', + STATUS_NO_SUCH_PROCESS: 'Error: Entity not found', + STATUS_INVALID_ACTION: 'Error: Invalid action', + STATUS_GRPC_SERVICE_UNAVAILABLE: 'Error: Unreachable grPC server', + STATUS_GRPC_UNAUTHORIZED: 'Error: Unauthorized', + STATUS_NOT_CONFIGURED: 'Error: Not configured', + STATUS_ALREADY_CONFIGURED: 'Error: Already configured', + STATUS_NO_SUCH_DEVICE: 'Error: Device not found', } diff --git a/control_plane/examples/arangodb/load_topo_on_arango.py b/control_plane/examples/arangodb/load_topo_on_arango.py index bfc2ae0..67f6348 100644 --- a/control_plane/examples/arangodb/load_topo_on_arango.py +++ b/control_plane/examples/arangodb/load_topo_on_arango.py @@ -29,7 +29,7 @@ import logging # Controller dependencies -from controller.arangodb_utils import extract_topo_from_isis_and_load_on_arango +from controller.db_utils.arangodb.arangodb_utils import extract_topo_from_isis_and_load_on_arango # from controller.arangodb_utils import extract_topo_from_isis # from controller.arangodb_utils import load_topo_on_arango diff --git a/control_plane/examples/srv6_pm/srv6_pm_example.py b/control_plane/examples/srv6_pm/srv6_pm_example.py index ea351a5..4def5f0 100644 --- a/control_plane/examples/srv6_pm/srv6_pm_example.py +++ b/control_plane/examples/srv6_pm/srv6_pm_example.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/examples/srv6_tunnels/create_tunnel_r1r4r8.py b/control_plane/examples/srv6_tunnels/create_tunnel_r1r4r8.py index c18249a..9189159 100644 --- a/control_plane/examples/srv6_tunnels/create_tunnel_r1r4r8.py +++ b/control_plane/examples/srv6_tunnels/create_tunnel_r1r4r8.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/examples/srv6_tunnels/create_tunnel_r1r7r8.py b/control_plane/examples/srv6_tunnels/create_tunnel_r1r7r8.py index 2e9e7e9..84bcd7f 100644 --- a/control_plane/examples/srv6_tunnels/create_tunnel_r1r7r8.py +++ b/control_plane/examples/srv6_tunnels/create_tunnel_r1r7r8.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/examples/srv6_tunnels/remove_tunnel_r1r4r8.py b/control_plane/examples/srv6_tunnels/remove_tunnel_r1r4r8.py index ebd678d..a0c27e8 100644 --- a/control_plane/examples/srv6_tunnels/remove_tunnel_r1r4r8.py +++ b/control_plane/examples/srv6_tunnels/remove_tunnel_r1r4r8.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/examples/srv6_tunnels/remove_tunnel_r1r7r8.py b/control_plane/examples/srv6_tunnels/remove_tunnel_r1r7r8.py index 09dd0ee..1a68e98 100644 --- a/control_plane/examples/srv6_tunnels/remove_tunnel_r1r7r8.py +++ b/control_plane/examples/srv6_tunnels/remove_tunnel_r1r7r8.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/examples/srv6_tunnels/shift_path.py b/control_plane/examples/srv6_tunnels/shift_path.py index b1f04bb..1843ae2 100644 --- a/control_plane/examples/srv6_tunnels/shift_path.py +++ b/control_plane/examples/srv6_tunnels/shift_path.py @@ -1,7 +1,7 @@ #!/usr/bin/python ########################################################################## -# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome 'Tor Vergata') +# Copyright (C) 2020 Carmine Scarpitta - (Consortium GARR and University of Rome "Tor Vergata") # www.garr.it - www.uniroma2.it/netgroup # # @@ -12,7 +12,7 @@ # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an 'AS IS' BASIS, +# distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. diff --git a/control_plane/node-manager/node_manager/__init__.py b/control_plane/node-manager/node_manager/__init__.py index 013e4b7..1ca5ce8 100644 --- a/control_plane/node-manager/node_manager/__init__.py +++ b/control_plane/node-manager/node_manager/__init__.py @@ -1 +1,30 @@ #!/usr/bin/python + +########################################################################## +# Copyright (C) 2020 Carmine Scarpitta +# (Consortium GARR and University of Rome "Tor Vergata") +# www.garr.it - www.uniroma2.it/netgroup +# +# +# Licensed under the Apache License, Version 2.0 (the 'License'); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Node manager +# +# @author Carmine Scarpitta +# + +''' +This package provides an implementation of a Node Manager. The Node Manager +is a component that allows the SDN Controller to control a node (e.g. enforce +configuration into the node or get some information aboout the node) +''' diff --git a/control_plane/node-manager/node_manager/constants.py b/control_plane/node-manager/node_manager/constants.py index 5870377..d7f8e38 100644 --- a/control_plane/node-manager/node_manager/constants.py +++ b/control_plane/node-manager/node_manager/constants.py @@ -45,7 +45,7 @@ STATUS_NO_SUCH_DEVICE) # Forwarding Engine -FWD_ENGINE = { +FWD_ENGINE_STR_TO_INT = { 'VPP': FwdEngine.Value('VPP'), 'Linux': FwdEngine.Value('Linux'), 'P4': FwdEngine.Value('P4'), diff --git a/control_plane/node-manager/node_manager/pm_manager.py b/control_plane/node-manager/node_manager/pm_manager.py index 9d96922..f20f55b 100644 --- a/control_plane/node-manager/node_manager/pm_manager.py +++ b/control_plane/node-manager/node_manager/pm_manager.py @@ -1,8 +1,11 @@ #!/usr/bin/python -"""Implementation of SRv6 PM Manager""" +''' +Implementation of SRv6 PM Manager +''' +# General imports import atexit import logging import os @@ -53,7 +56,7 @@ class TWAMPController(srv6pmService_pb2_grpc.SRv6PMServicer): - """gRPC request handler""" + '''gRPC request handler''' def __init__(self, session_sender=None, session_reflector=None, packet_receiver=None): @@ -70,7 +73,7 @@ def __init__(self, session_sender=None, self.configured = False elif self.sender is not None and self.reflector is not None and \ self.packet_receiver is not None: - # The node has be configured "statically" + # The node has be configured 'statically' self.configured = True else: # Partial configuration is not allowed @@ -78,7 +81,7 @@ def __init__(self, session_sender=None, sys.exit(-2) def startExperimentSender(self, request, context): - """Start an experiment as sender""" + '''Start an experiment as sender''' print('GRPC CONTROLLER: startExperimentSender') # Check if the node has been configured @@ -114,7 +117,7 @@ def startExperimentSender(self, request, context): return srv6pmSender_pb2.StartExperimentSenderReply(status=status) def stopExperimentSender(self, request, context): - """Stop an experiment running on sender""" + '''Stop an experiment running on sender''' print('GRPC CONTROLLER: stopExperimentSender') # Check if the node has been configured @@ -131,7 +134,7 @@ def stopExperimentSender(self, request, context): return srv6pmCommons_pb2.StopExperimentReply(status=status) def startExperimentReflector(self, request, context): - """Start an experiment as reflector""" + '''Start an experiment as reflector''' print('GRPC CONTROLLER: startExperimentReflector') # Check if the node has been configured @@ -161,7 +164,7 @@ def startExperimentReflector(self, request, context): return srv6pmReflector_pb2.StartExperimentReflectorReply(status=status) def stopExperimentReflector(self, request, context): - """Stop an experiment on the reflector""" + '''Stop an experiment on the reflector''' print('GRPC CONTROLLER: startExperimentReflector') # Check if the node has been configured @@ -178,7 +181,7 @@ def stopExperimentReflector(self, request, context): return srv6pmCommons_pb2.StopExperimentReply(status=status) def retriveExperimentResults(self, request, context): - """Retrieve results from the sender""" + '''Retrieve results from the sender''' print('GRPC CONTROLLER: retriveExperimentResults') # Check if the node has been configured @@ -219,7 +222,7 @@ def retriveExperimentResults(self, request, context): return response def setConfiguration(self, request, context): - """Inject the configuration on the node""" + '''Inject the configuration on the node''' print('GRPC CONTROLLER: setConfiguration') # Check if this node is already configured @@ -299,7 +302,7 @@ def setConfiguration(self, request, context): return srv6pmCommons_pb2.SetConfigurationReply(status=status) def resetConfiguration(self, request, context): - """Clear the current configuration""" + '''Clear the current configuration''' print('GRPC CONTROLLER: resetConfiguration') # Check if this node is not configured: @@ -347,14 +350,14 @@ def resetConfiguration(self, request, context): def add_pm_manager_to_server(server): - """Attach PM Manager gRPC server to an existing server""" + '''Attach PM Manager gRPC server to an existing server''' srv6pmService_pb2_grpc.add_SRv6PMServicer_to_server( TWAMPController(), server) def serve(ip_addr, gprc_port, recv_interf, epbf_out_interf, epbf_in_interf): - """Start gRPC server""" + '''Start gRPC server''' driver = EbpfInterf( in_interfaces=epbf_in_interf, @@ -383,7 +386,7 @@ def serve(ip_addr, gprc_port, recv_interf, epbf_out_interf, epbf_in_interf): def __main(): - """Entry point for this module""" + '''Entry point for this module''' ip_addr = sys.argv[1] gprc_port = sys.argv[2] diff --git a/control_plane/node-manager/node_manager/srv6_manager.py b/control_plane/node-manager/node_manager/srv6_manager.py index 8f140bc..975edb2 100644 --- a/control_plane/node-manager/node_manager/srv6_manager.py +++ b/control_plane/node-manager/node_manager/srv6_manager.py @@ -23,7 +23,13 @@ # @author Carmine Scarpitta # -"""This module provides an implementation of a SRv6 Manager""" + +''' +This module provides an implementation of a SRv6 Manager. Currently, it +supports "Linux" and "VPP" as forwarding engine. However, the design of this +module is modular and other forwarding engines can be easily added in the +future. +''' # General imports @@ -42,171 +48,224 @@ import commons_pb2 import srv6_manager_pb2 import srv6_manager_pb2_grpc -# Node manager dependencies +# Node Manager dependencies from node_manager.utils import get_address_family +from node_manager.utils import check_root from node_manager.srv6_mgr_linux import SRv6ManagerLinux from node_manager.srv6_mgr_vpp import SRv6ManagerVPP # TODO # Import constants file -from node_manager.constants import FWD_ENGINE - -# Load environment variables from .env file -# load_dotenv() +from node_manager.constants import FWD_ENGINE_STR_TO_INT # Folder containing this script BASE_PATH = os.path.dirname(os.path.realpath(__file__)) +# Logger reference +logging.basicConfig(level=logging.NOTSET) +LOGGER = logging.getLogger(__name__) + # Global variables definition # # -# Netlink error codes -NETLINK_ERROR_NO_SUCH_PROCESS = 3 -NETLINK_ERROR_FILE_EXISTS = 17 -NETLINK_ERROR_NO_SUCH_DEVICE = 19 -NETLINK_ERROR_OPERATION_NOT_SUPPORTED = 95 -# Logger reference -LOGGER = logging.getLogger(__name__) -# -# Default parameters for SRv6 manager +# Default parameters for SRv6 Manager # # Server ip and port DEFAULT_GRPC_IP = '::' DEFAULT_GRPC_PORT = 12345 # Debug option -SERVER_DEBUG = False +DEFAULT_DEBUG = False # Secure option DEFAULT_SECURE = False # Server certificate DEFAULT_CERTIFICATE = 'cert_server.pem' # Server key DEFAULT_KEY = 'key_server.pem' +# Is VPP support enabled by default? +DEFAULT_ENABLE_VPP = False class SRv6Manager(srv6_manager_pb2_grpc.SRv6ManagerServicer): - """gRPC request handler""" + ''' + gRPC request handler + ''' def __init__(self): - # SRv6 Manager for Linux Forwarding Engine - self.srv6_mgr_linux = SRv6ManagerLinux() - # SRv6 Manager for VPP Forwarding Engine - # self.srv6_mgr_vpp = None TODO remove - self.srv6_mgr_vpp = SRv6ManagerVPP() # TODO - - def handle_srv6_path_request(self, operation, request, context): + # Define a dict to map Forwarding Engines to their handlers + # The key of the dict is a numeric code corresponding to the + # Forwarding Engine, the value is an handler for the Forwarding Engine + self.fwd_engine = dict() + # Init SRv6 Manager for Linux Forwarding Engine + # It allows the SDN Controller to control the Linux Forwarding Engine + self.fwd_engine[FWD_ENGINE_STR_TO_INT['Linux']] = SRv6ManagerLinux() + # Init SRv6 Manager for VPP Forwarding Engine, if VPP is enabled + if os.getenv('ENABLE_VPP', DEFAULT_ENABLE_VPP): + # It allows the SDN Controller to control the VPP Forwarding Engine + self.fwd_engine[FWD_ENGINE_STR_TO_INT['VPP']] = SRv6ManagerVPP() + + def handle_srv6_path_request(self, operation, request, context, ret_paths): + ''' + Handler for SRv6 paths. + ''' # pylint: disable=unused-argument - """Handler for SRv6 paths""" - + # + # Process request LOGGER.debug('config received:\n%s', request) # Extract forwarding engine fwd_engine = request.fwd_engine # Perform operation - if fwd_engine == FWD_ENGINE['Linux']: - # Linux forwarding engine - return self.srv6_mgr_linux.handle_srv6_path_request(operation, - request, - context) - if fwd_engine == FWD_ENGINE['VPP']: - # VPP forwarding engine - # TODO gestire caso VPP non abilitato o non disponibile - return self.srv6_mgr_vpp.handle_srv6_path_request( - operation, request, context) - # Unknown forwarding engine - return srv6_manager_pb2.SRv6ManagerReply(status=commons_pb2.StatusCode.Value( - 'STATUS_INTERNAL_ERROR')) # TODO creare un errore specifico - - def handle_srv6_policy_request(self, operation, request, context): + if fwd_engine not in self.fwd_engine: + # Unknown forwarding engine + LOGGER.error('Unknown Forwarding Engine. ' + 'Make sure that it is enabled in the configuration.') + return srv6_manager_pb2.SRv6ManagerReply( + status=commons_pb2.StatusCode.Value('INVALID_FWD_ENGINE')) + # Dispatch the request to the right Forwarding Engine handler and + # return the result + return self.fwd_engine[fwd_engine].handle_srv6_path_request( + operation=operation, + request=request, + context=context, + ret_paths=ret_paths + ) + + def handle_srv6_policy_request(self, operation, request, context, + ret_policies): + ''' + Handler for SRv6 policies. + ''' # pylint: disable=unused-argument - """Handler for SRv6 policies""" - + # + # Process request LOGGER.debug('config received:\n%s', request) # Extract forwarding engine fwd_engine = request.fwd_engine # Perform operation - if fwd_engine == FWD_ENGINE['Linux']: - # Linux forwarding engine does not support SRv6 policy + if fwd_engine not in self.fwd_engine: + # Unknown forwarding engine + LOGGER.error('Unknown Forwarding Engine.' + 'Make sure that it is enabled in the configuration.') return srv6_manager_pb2.SRv6ManagerReply( - status=commons_pb2.STATUS_OPERATION_NOT_SUPPORTED) - if fwd_engine == FWD_ENGINE['VPP']: - # VPP forwarding engine - # TODO gestire caso VPP non abilitato o non disponibile - return self.srv6_mgr_vpp.handle_srv6_policy_request( - operation, request, context) - # Unknown forwarding engine - return srv6_manager_pb2.SRv6ManagerReply(status=commons_pb2.StatusCode.Value( - 'STATUS_INTERNAL_ERROR')) # TODO creare un errore specifico - - def handle_srv6_behavior_request(self, operation, request, context): + status=commons_pb2.StatusCode.Value('INVALID_FWD_ENGINE')) + # Dispatch the request to the right Forwarding Engine handler + return self.fwd_engine[fwd_engine].handle_srv6_policy_request( + operation=operation, + request=request, + context=context, + ret_policies=ret_policies + ) + + def handle_srv6_behavior_request(self, operation, request, context, + ret_behaviors): + ''' + Handler for SRv6 behaviors. + ''' # pylint: disable=unused-argument - """Handler for SRv6 behaviors""" - + # + # Process request LOGGER.debug('config received:\n%s', request) # Extract forwarding engine fwd_engine = request.fwd_engine # Perform operation - if fwd_engine == FWD_ENGINE['Linux']: - # Linux forwarding engine - return self.srv6_mgr_linux.handle_srv6_behavior_request(operation, - request, - context) - if fwd_engine == FWD_ENGINE['VPP']: - # VPP forwarding engine - # TODO gestire caso VPP non abilitato o non disponibile - return self.srv6_mgr_vpp.handle_srv6_behavior_request(operation, - request, - context) - # Unknown forwarding engine - return srv6_manager_pb2.SRv6ManagerReply(status=commons_pb2.StatusCode.Value( - 'STATUS_INTERNAL_ERROR')) # TODO creare un errore specifico + if fwd_engine not in self.fwd_engine: + # Unknown forwarding engine + LOGGER.error('Unknown Forwarding Engine.' + 'Make sure that it is enabled in the configuration.') + return srv6_manager_pb2.SRv6ManagerReply( + status=commons_pb2.StatusCode.Value('INVALID_FWD_ENGINE')) + # Dispatch the request to the right Forwarding Engine handler + return self.fwd_engine[fwd_engine].handle_srv6_behavior_request( + operation=operation, + request=request, + context=context, + ret_behaviors=ret_behaviors + ) def execute(self, operation, request, context): - """This function dispatch the gRPC requests based - on the entity carried in them""" - + ''' + This function dispatch the gRPC requests based on the entity carried + in them. + ''' # Handle operation - # The operation to be executed depends on - # the entity carried by the request message - res = srv6_manager_pb2.SRv6ManagerReply( + # + # The operations to be executed depends on the entity carried by the + # request message + reply = srv6_manager_pb2.SRv6ManagerReply( status=commons_pb2.STATUS_SUCCESS) if request.HasField('srv6_path_request'): + # The message contains at least one SRv6 Path request, so we pass + # the request to the SRv6 Path handler res = self.handle_srv6_path_request( - operation, request.srv6_path_request, context) - if res.status != commons_pb2.STATUS_SUCCESS: - return res + operation=operation, + request=request.srv6_path_request, + context=context, + ret_paths=reply.paths + ) + if res != commons_pb2.STATUS_SUCCESS: + # An error occurred + return srv6_manager_pb2.SRv6ManagerReply( + status=res) if request.HasField('srv6_policy_request'): + # The message contains at least one SRv6 Path request, so we pass + # the request to the SRv6 Policy handler res = self.handle_srv6_policy_request( - operation, request.srv6_policy_request, context) - if res.status != commons_pb2.STATUS_SUCCESS: - return res + operation=operation, + request=request.srv6_policy_request, + context=context, + ret_policies=reply.policies + ) + if res != commons_pb2.STATUS_SUCCESS: + # An error occurred + return srv6_manager_pb2.SRv6ManagerReply( + status=res) if request.HasField('srv6_behavior_request'): + # The message contains at least one SRv6 Path request, so we pass + # the request to the SRv6 Behavior handler res = self.handle_srv6_behavior_request( - operation, request.srv6_behavior_request, context) - return res + operation=operation, + request=request.srv6_behavior_request, + context=context, + ret_behaviors=reply.behaviors + ) + if res != commons_pb2.STATUS_SUCCESS: + # An error occurred + return srv6_manager_pb2.SRv6ManagerReply( + status=res) + # Return the result + return reply def Create(self, request, context): + ''' + RPC used to create a SRv6 entity. + ''' # pylint: disable=invalid-name - """RPC used to create a SRv6 entity""" - + # # Handle Create operation return self.execute('add', request, context) def Get(self, request, context): + ''' + RPC used to get a SRv6 entity. + ''' # pylint: disable=invalid-name - """RPC used to get a SRv6 entity""" - + # # Handle Create operation return self.execute('get', request, context) def Update(self, request, context): + ''' + RPC used to change a SRv6 entity. + ''' # pylint: disable=invalid-name - """RPC used to change a SRv6 entity""" - + # # Handle Remove operation return self.execute('change', request, context) def Remove(self, request, context): + ''' + RPC used to remove a SRv6 entity. + ''' # pylint: disable=invalid-name - """RPC used to remove a SRv6 entity""" - + # # Handle Remove operation return self.execute('del', request, context) @@ -217,8 +276,29 @@ def start_server(grpc_ip=DEFAULT_GRPC_IP, secure=DEFAULT_SECURE, certificate=DEFAULT_CERTIFICATE, key=DEFAULT_KEY): - """Start a gRPC server""" - + ''' + Start a gRPC server that implements the functionality of a SRv6 Manager. + + :param grpc_ip: The IP address on which the gRPC server will listen for + connections (default: "::"). + :type grpc_ip: str, optional + :param grpc_port: The port number on which the gRPC server will listen for + connections (default: 12345). + :type grpc_port: int, optional + :param secure: Define whether to enable the gRPC sercure mode; if secure + mode is enabled, gRPC will use TLS secured channels instead + of TCP channels (default: False). + :type secure: bool, optional + :param certificate: The file containing the certificate of the server to + be used for the gRPC secure mode. If you don't use the + secure mode, you can omit this argument (default: + "cert_server.pem"). + :type certificate: str, optional + :param key: The file containing the key of the server to be used for the + gRPC secure mode. If you don't use the secure mode, you can + omit this argument (default: "key_server.pem"). + :type key: str, optional + ''' # Get family of the gRPC IP addr_family = get_address_family(grpc_ip) # Build address depending on the family @@ -259,19 +339,11 @@ def start_server(grpc_ip=DEFAULT_GRPC_IP, time.sleep(5) -# Check whether we have root permission or not -# Return True if we have root permission, False otherwise -def check_root(): - """ Return True if this program is executed as root, - False otherwise""" - - return os.getuid() == 0 - - # Parse options def parse_arguments(): - """Command-line arguments parser""" - + ''' + Command-line arguments parser + ''' # Get parser parser = ArgumentParser( description='gRPC Southbound APIs for SRv6 Controller' @@ -296,7 +368,8 @@ def parse_arguments(): action='store', default=DEFAULT_KEY, help='Server key file' ) parser.add_argument( - '-d', '--debug', action='store_true', help='Activate debug logs' + '-d', '--debug', action='store_true', help='Activate debug logs', + default=DEFAULT_DEBUG ) # Parse input parameters args = parser.parse_args() @@ -305,8 +378,10 @@ def parse_arguments(): def __main(): - """Entry point for this script""" - + ''' + Entry point for this script + ''' + # Parse the arguments args = parse_arguments() # Setup properly the secure mode secure = args.secure diff --git a/control_plane/node-manager/node_manager/srv6_mgr_linux.py b/control_plane/node-manager/node_manager/srv6_mgr_linux.py index 729856e..8687482 100644 --- a/control_plane/node-manager/node_manager/srv6_mgr_linux.py +++ b/control_plane/node-manager/node_manager/srv6_mgr_linux.py @@ -39,7 +39,6 @@ # Proto dependencies import commons_pb2 -import srv6_manager_pb2 # Load environment variables from .env file # load_dotenv() @@ -141,7 +140,14 @@ def __init__(self): 'uN': self.handle_un_behavior_request, } - def handle_srv6_path_request(self, operation, request, context): + def handle_srv6_policy_request(self, operation, request, context, + ret_policies): + # Linux forwarding engine does not support SRv6 policies + LOGGER.error('SRv6 policy operation not supported by Linux forwarding ' + 'engine') + return commons_pb2.STATUS_INTERNAL_ERROR + + def handle_srv6_path_request(self, operation, request, context, ret_paths): # pylint: disable=unused-argument """Handler for SRv6 paths""" @@ -166,8 +172,14 @@ def handle_srv6_path_request(self, operation, request, context): segments = ['::'] oif = None if path.device != '': + if path.device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE oif = self.interface_to_idx[path.device] elif operation == 'add': + if len(self.non_loopback_interfaces) == 0: + LOGGER.error('No device found') + return commons_pb2.STATUS_NO_SUCH_DEVICE oif = self.interface_to_idx[ self.non_loopback_interfaces[0]] self.ip_route.route(operation, dst=path.destination, @@ -178,19 +190,16 @@ def handle_srv6_path_request(self, operation, request, context): 'mode': path.encapmode, 'segs': segments}) elif operation == 'get': - return srv6_manager_pb2.SRv6ManagerReply( - status=commons_pb2.STATUS_OPERATION_NOT_SUPPORTED) + return commons_pb2.STATUS_OPERATION_NOT_SUPPORTED else: # Operation unknown: this is a bug LOGGER.error('Unrecognized operation: %s', operation) sys.exit(-1) # and create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=commons_pb2.STATUS_SUCCESS) + return commons_pb2.STATUS_SUCCESS except NetlinkError as err: - return srv6_manager_pb2.SRv6ManagerReply( - status=parse_netlink_error(err)) + return parse_netlink_error(err) def handle_end_behavior_request(self, operation, behavior): """Handle seg6local End behavior""" @@ -211,6 +220,10 @@ def handle_end_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -250,6 +263,10 @@ def handle_end_x_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -290,6 +307,10 @@ def handle_end_t_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -330,6 +351,10 @@ def handle_end_dx2_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -370,6 +395,10 @@ def handle_end_dx6_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -410,6 +439,10 @@ def handle_end_dx4_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -450,6 +483,10 @@ def handle_end_dt6_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -490,6 +527,10 @@ def handle_end_dt4_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -529,6 +570,10 @@ def handle_end_b6_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Rebuild segments segments = [] for srv6_segment in behavior.segs: @@ -574,6 +619,10 @@ def handle_end_b6_encaps_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Rebuild segments segments = [] for srv6_segment in behavior.segs: @@ -619,6 +668,10 @@ def handle_un_behavior_request(self, operation, behavior): if operation == 'get': return self.handle_srv6_behavior_get_request(behavior) if operation in ['add', 'change']: + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Build encap info encap = { 'type': 'seg6local', @@ -640,7 +693,10 @@ def handle_un_behavior_request(self, operation, behavior): def handle_srv6_behavior_del_request(self, behavior): """Delete a route""" - + # Check if the device exists + if device not in self.interface_to_idx: + LOGGER.error('No such device') + return commons_pb2.STATUS_NO_SUCH_DEVICE # Extract params segment = behavior.segment device = behavior.device if behavior.device != '' \ @@ -659,7 +715,6 @@ def handle_srv6_behavior_get_request(self, behavior): # pylint checks on this method are temporary disabled # pylint: disable=no-self-use, unused-argument """Get a route""" - LOGGER.info('get opertion not yet implemented\n') return commons_pb2.STATUS_OPERATION_NOT_SUPPORTED @@ -675,7 +730,8 @@ def dispatch_srv6_behavior(self, operation, behavior): LOGGER.error('Error: Unrecognized action: %s', behavior.action) return commons_pb2.STATUS_INVALID_ACTION - def handle_srv6_behavior_request(self, operation, request, context): + def handle_srv6_behavior_request(self, operation, request, context, + ret_behaviors): # pylint: disable=unused-argument """Handler for SRv6 behaviors""" @@ -685,18 +741,16 @@ def handle_srv6_behavior_request(self, operation, request, context): for behavior in request.behaviors: if operation == 'del': res = self.handle_srv6_behavior_del_request(behavior) - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res if operation == 'get': res = self.handle_srv6_behavior_get_request(behavior) - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res # Pass the request to the right handler res = self.dispatch_srv6_behavior(operation, behavior) if res != commons_pb2.STATUS_SUCCESS: - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res # and create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=commons_pb2.STATUS_SUCCESS) + return commons_pb2.STATUS_SUCCESS except NetlinkError as err: - return srv6_manager_pb2.SRv6ManagerReply( - status=parse_netlink_error(err)) + return parse_netlink_error(err) diff --git a/control_plane/node-manager/node_manager/srv6_mgr_vpp.py b/control_plane/node-manager/node_manager/srv6_mgr_vpp.py index 28c2517..178253b 100644 --- a/control_plane/node-manager/node_manager/srv6_mgr_vpp.py +++ b/control_plane/node-manager/node_manager/srv6_mgr_vpp.py @@ -43,7 +43,6 @@ # sys.exit(-2) # Proto dependencies -import srv6_manager_pb2 from node_manager.constants import STATUS_CODE # Folder containing this script @@ -115,8 +114,7 @@ def handle_srv6_src_addr_request(self, operation, request, context): LOGGER.debug('Entering handle_srv6_src_addr_request') if operation in ['add', 'get', 'del']: LOGGER.error('Operation %s not supported', operation) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED']) + return STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED'] if operation in ['change']: # String representing the command to be sent to VPP cmd = 'set sr encaps source addr %s' % str(request.src_addr) @@ -128,17 +126,16 @@ def handle_srv6_src_addr_request(self, operation, request, context): if res != '': # The operation failed logging.error('VPP returned an error: %s', res) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_INTERNAL_ERROR']) + return STATUS_CODE['STATUS_INTERNAL_ERROR'] # Return the result LOGGER.debug('Operation completed successfully') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('Unrecognized operation: %s', operation) sys.exit(-1) - def handle_srv6_policy_request(self, operation, request, context): + def handle_srv6_policy_request(self, operation, request, context, + ret_policies): ''' This function is used to create, delete or change a SRv6 policy, equivalent to: @@ -153,8 +150,7 @@ def handle_srv6_policy_request(self, operation, request, context): if operation in ['change', 'get']: # Currently only "add" and "del" operations are supported LOGGER.error('Operation not yet supported: %s', operation) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED']) + return STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED'] if operation in ['add', 'del']: # Let's push the routes for policy in request.policies: @@ -198,17 +194,15 @@ def handle_srv6_policy_request(self, operation, request, context): if res != '': # The operation failed logging.error('VPP returned an error: %s', res) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_INTERNAL_ERROR']) + return STATUS_CODE['STATUS_INTERNAL_ERROR'] # All the policies have been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('Unrecognized operation: %s', operation) sys.exit(-1) - def handle_srv6_path_request(self, operation, request, context): + def handle_srv6_path_request(self, operation, request, context, ret_paths): ''' Handler for SRv6 paths ''' @@ -219,8 +213,7 @@ def handle_srv6_path_request(self, operation, request, context): if operation in ['change', 'get']: # Currently only "add" and "del" operations are supported LOGGER.error('Operation not yet supported: %s', operation) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED']) + return STATUS_CODE['STATUS_OPERATION_NOT_SUPPORTED'] if operation in ['add', 'del']: # Let's push the routes for path in request.paths: @@ -271,12 +264,10 @@ def handle_srv6_path_request(self, operation, request, context): if res != '': # The operation failed logging.error('VPP returned an error: %s', res) - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_INTERNAL_ERROR']) + return STATUS_CODE['STATUS_INTERNAL_ERROR'] # All the paths have been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('Unrecognized operation: %s', operation) sys.exit(-1) @@ -329,8 +320,7 @@ def handle_end_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -386,8 +376,7 @@ def handle_end_x_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -441,8 +430,7 @@ def handle_end_t_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -496,8 +484,7 @@ def handle_end_dx2_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -552,8 +539,7 @@ def handle_end_dx6_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -608,8 +594,7 @@ def handle_end_dx4_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -663,8 +648,7 @@ def handle_end_dt6_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -718,8 +702,7 @@ def handle_end_dt4_behavior_request(self, operation, behavior): return STATUS_CODE['STATUS_INTERNAL_ERROR'] # The behavior has been processed, create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] # Unknown operation: this is a bug LOGGER.error('BUG - Unrecognized operation: %s', operation) sys.exit(-1) @@ -916,7 +899,8 @@ def dispatch_srv6_behavior(self, operation, behavior): LOGGER.error('Error: Unrecognized action: %s', behavior.action) return STATUS_CODE['STATUS_INVALID_ACTION'] - def handle_srv6_behavior_request(self, operation, request, context): + def handle_srv6_behavior_request(self, operation, request, context, + ret_behaviors): # pylint: disable=unused-argument """Handler for SRv6 behaviors""" LOGGER.debug('config received:\n%s', request) @@ -924,15 +908,14 @@ def handle_srv6_behavior_request(self, operation, request, context): for behavior in request.behaviors: if operation == 'del': res = self.handle_srv6_behavior_del_request(behavior) - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res if operation == 'get': res = self.handle_srv6_behavior_get_request(behavior) - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res # Pass the request to the right handler res = self.dispatch_srv6_behavior(operation, behavior) if res != STATUS_CODE['STATUS_SUCCESS']: - return srv6_manager_pb2.SRv6ManagerReply(status=res) + return res # and create the response LOGGER.debug('Send response: OK') - return srv6_manager_pb2.SRv6ManagerReply( - status=STATUS_CODE['STATUS_SUCCESS']) + return STATUS_CODE['STATUS_SUCCESS'] diff --git a/control_plane/node-manager/node_manager/utils.py b/control_plane/node-manager/node_manager/utils.py index 1c20b86..cdf33eb 100644 --- a/control_plane/node-manager/node_manager/utils.py +++ b/control_plane/node-manager/node_manager/utils.py @@ -26,6 +26,8 @@ """This module contains several utility functions for node manager""" +# General imports +import os from ipaddress import AddressValueError, IPv4Interface, IPv6Interface from socket import AF_INET, AF_INET6 @@ -81,3 +83,13 @@ def validate_ip_address(ip_address): return validate_ipv4_address(ip_address) or \ validate_ipv6_address(ip_address) + + +# Check whether we have root permission or not +# Return True if we have root permission, False otherwise +def check_root(): + ''' + Return True if this program is executed as root, + False otherwise + ''' + return os.getuid() == 0 diff --git a/control_plane/protos/commons.proto b/control_plane/protos/commons.proto index 264622c..840a81f 100644 --- a/control_plane/protos/commons.proto +++ b/control_plane/protos/commons.proto @@ -16,4 +16,5 @@ enum StatusCode { STATUS_NOT_CONFIGURED = 10; STATUS_ALREADY_CONFIGURED = 11; STATUS_NO_SUCH_DEVICE = 12; + INVALID_FWD_ENGINE = 13; }