Service Wrapper¶
The Service Wrapper is a class designed to wrap various methods of a Connection object with pre, post, and exception handlers, adding extra functionality to existing Connection methods. It allows users to extend and customize the behavior of specific Connection methods without modifying the original implementation.
Here are some typical use cases of service wrappers:
tracking the sequence/occurance of CLI command calls throughout a script
debug-logging the input arguments to, and output values from particular services.
building a LRU cache based on script inputs and device states.
etc.
Usage¶
To use the Service Wrapper, follow these steps:
1) Create a child class derived from the pyats.connections.ServiceWrapper
class. This child class will serve as the Service Wrapper for the desired
Connection methods.
2) Set the conn_type
and service_name
of the child class to the Connection
type and service name that the Service Wrapper will be applied to. This will
typically be unicon.Connection
and the name of the service method, such as
execute
or configure
. The order
class variable can also be set to
determine the order in which the Service Wrappers will be applied.
3) Define the conditions under which the Service Wrapper should be applied to
the service using the applicable
method.
4) (Optional) Implement the necessary methods in the child class to define pre, post, and exception handling behavior. These methods will be executed at different stages during the service call.
5) (Optional) Use Testcase Steps
to provide more robust reporting and
leverage the pyATS reporter functionality.
6) (Optional) Configure CLI parser arguments in the child class using the
configure_parser
method. This is used to enable easy integration with CLI
interfaces.
Note
Error handlers can suppress an exception, and/or track/register it internally. By default the built-in error handler will simply raise the current exception. Developer can modify that to suppress the current exception being handled, and return a fixed/altered result.
Service Wrapper Class¶
The Service Wrapper base class provides several methods that can be overwritten by the user to customize the behavior of the service call:
call_service
¶
- This method is responsible for calling the wrapped service. By default, it calls
the service and passes any arguments that were provided to it. It returns the output of the service call, which can be utilized in the
post_service
method.If an error occurs, it calls the
exception_service
method. This method can be completely overloaded to change the behavior of the service call.
def call_service(self, *args, **kwargs) -> Any:
"""Call the service three times"""
ret_list = []
for _ in range(3):
try:
ret_list.append(self.service(*args, **kwargs))
except Exception as e:
logger.exception(f"Exception occurred: {e}")
return ret_list
exception_service
¶
The exception_service
method is called if an exception occurs during the
service call inside the call_service
method. It is an abstracted method that
will only run if the child class implements it. It can return anything to be
used in the post_service
method.
def exception_service(self, e: Exception, *args, **kwargs) -> Any:
logger.exception(f"Exception occurred: {e}")
return f'Exception occurred: {e}'
pre_service
¶
- The
pre_service
method is called before thecall_service
method is executed. - It is an abstracted method that will only run if the child class implements it.
- This method allows performing any necessary actions or setup before the actual
service call.
def pre_service(self, *args, **kwargs) -> None:
logger.info(f"Running pre_service on {self.service_name}")
logger.info(f"{self.service_name} args: {args} kwargs: {kwargs}")
post_service
¶
- The
post_service
method is called after thecall_service
method is executed. - It is an abstracted method that will only run if the child class implements it.
This method is used to handle any post-processing actions after the service call. It receives the output of the
call_service
method as an argument.
def post_service(self, output: Any, *args, **kwargs) -> None:
logger.info(f"Running post_service on {self.service_name}")
logger.info(f"{self.service_name} output: {output}")
configure_parser
¶
The configure_parser
method is called when the Service Wrapper is loaded. It
is used to configure CLI parser arguments, similar to an easypy plugin. This is
a classmethod that requires definition from the implementor. It takes in a
parser
argument, which is an instance of the argparse.ArgumentParser
class.
@classmethod
def configure_parser(cls, parser: argparse.ArgumentParser) -> None:
parser.add_argument(
'--service-wrapper-arg',
dest='service_wrapper_arg',
action='store_true',
help='Service Wrapper argument',
)
return parser
applicable
¶
The applicable
class method is used to determine whether the Service Wrapper
should be applied to the service. This is also a classmethod that requires
definition from the implementor. It takes in a connection
argument, which is
an instance of the pyats.connections.BaseConnection
class. It should return a
boolean value: True
if the Service Wrapper should be applied, and False
otherwise.
This is in addition to the default conn_type
and service_name
checks that
are done. This allows for more fine-grained control over which services the
Service Wrapper is applied to. For example, the Service Wrapper can be applied
to a specific device type, or only when a certain argument is passed to the
service.
# Override applicable to check if the device is an IOSXE device
@classmethod
def applicable(cls, connection: BaseConnection, *args, **kwargs) -> bool:
"""Ensure the device OS is iosxe and the service wrapper argument is passed"""
return connection.device.os == 'iosxe'
Important Attributes¶
- The Service Wrapper base class also provides several attributes that can be used
in the Service Wrapper methods:
self.service
The service method that the Service Wrapper is applied to
Note that this should be exclusively used to call the wrapper services.
You cannot call
self.execute
for example as this does not exist.
self.connection
The Connection object that the Service Wrapper is applied to
Note that caution should be taken when calling wrapped attributes on the
connection object. It’s possible to create an infinite loop if the wrapped attribute is also wrapped by the Service Wrapper.
self.device
The device object that the Connection object is connected to
self.logger
The logger object that can be used to log messages specific to the Service
Wrapper
self.testcase
The testcase object that can be used to access testcase data
This is the same testcase object that is passed to easypy plugins and has
access to testcase data through
self.testcase.parameters
self.args
The arguments as defined in the
configure_parser
method
Class Variables¶
- In addition to the methods, the Service Wrapper base class also provides several
class variables that must be set to define which service the Service Wrapper will be applied to:
conn_type
The Connection type that the Service Wrapper will be applied to. Options
include
# This is a catch-all for all Connection types pyats.connections.BaseConnection # This is the standard unicon connection unicon.Connection # Used for rest connections rest.connector.Rest # Used for yang connections yang.connector.Gnmi yang.connector.Netconf
service_name
The service the wrapper will be used on. This will be the name of the
service method, such as
execute
orconfigure
order
This is an optional variable. It’s is used to determine the order in which
the Service Wrappers will be applied. It is an integer value, with higher values being applied first. The default value is
0
.
Examples¶
Execute Service Wrapper - IOSXE Device¶
This is an example of a service wrapper for wrapping the execute service on an IOSXE device
Note
This example wrapper will run for ALL IOSXE devices when this wrapper script is installed. If this wrapper is installed in a shared environment, be aware that it affects all user jobs and any IOSXE devices in use.
import unicon
from pyats.connections import ServiceWrapper
class ExampleWrapper(ServiceWrapper):
conn_type = unicon.Connection
service_name = 'execute'
@classmethod
def configure_parser(cls, parser) -> None:
parser.add_argument(
'--service-wrapper-arg',
dest='service_wrapper_arg',
action='store_true',
help='Service Wrapper argument',
)
return parser
@classmethod
def applicable(cls, connection, *args, **kwargs) -> bool:
return connection.device.os == 'iosxe'
def pre_service(self, *args, **kwargs) -> None:
self.logger.info(f"Running command: {args[0]} on {self.device.name}")
def post_service(self, output, *args, **kwargs) -> None:
self.logger.info(f"Output: {output}")
def call_service(self, *args, **kwargs) -> Any:
try:
self.logger.info(f"Calling service: {self.service_name}")
return self.service(*args, **kwargs)
except Exception as e:
return self.exception_service(e, *args, **kwargs)
def exception_service(self, e, *args, **kwargs):
self.logger.exception(f"Exception occurred: {e}")
return f'Exception occurred: {e}'
Steps¶
Inside of each service wrapper method you are able to pass a steps
argument
that will automatically pick up the Testcase’s current steps
object, which
allows for use of the context manager style of reporting, failing, and passing
a test.
with steps.start() as step:
step.passed('Passed')
This can be used in any of the four service wrapper methods.
from pyats.connections import ServiceWrapper
class ExampleWrapper(ServiceWrapper):
@classmethod
def applicable(cls, connection, *args, **kwargs) -> bool:
return True
def pre_service(self, steps, *args, **kwargs) -> None:
with steps.start('Pre Service Step') as step:
step.passed('Sucessfully ran pre service step')
def post_service(self, output, steps, *args, **kwargs) -> None:
with steps.start('Post Service Step') as step:
step.passed('Sucessfully ran post service step')
def call_service(self, steps, *args, **kwargs) -> None:
with steps.start('Call Service Step') as step:
step.passed('Sucessfully ran call service step')
def exception_service(self, e, steps, *args, **kwargs) -> None:
with steps.start('Exception Service Step') as step:
step.passed('Sucessfully ran exception service step')
Note
The steps
argument is only filled when the service wrapper is run in
the context of a Testcase. If no Testcase is found, the steps
argument
will be None
. Keep this in mind if your service wrapper is used in a
standalone context with no Testcase.
Discovery¶
There are two methods to enable pyATS to discover service wrappers. The first method is to configure the service wrapper in the pyats configuration file. The second method is to use an entry point.
Configuration Method¶
Once the service wrapper is created, you can utilize it by adding it to the pyats configuration file. You can read up on how to configure that here.
[service_wrapper]
example_wrapper = path.to.module:ExampleWrapper
Entrypoint Method¶
Additionally, you can utilize it by adding it as an entry
point in your package’s setup file through the pyats.connections.wrapper
entry point descriptor. This enables the service wrapper to be called and
employed within the context of your package, facilitating seamless integration
and utilization of the wrapped functionalities.
setup(
...,
# console entry point
entry_points = {
'pyats.connections.wrapper': [
'example_wrapper = path.to.path:ExampleWrapper'
]
}
)