Basic Usage¶
What do you need to get done!?!¶
First things first: what do you need to get done? scrapli replay contains two similar yet very different testing tools.
The first, is the pytest plugin -- a plugin to mark tests with. This plugin will record scrapli session inputs and outputs and save them, that way you can store these test sessions and re-use them (without needing a "live" device) in your CI setup.
The second, is a "collector", and a "server" that allow you to build semi-interactive SSH servers that you can connect to for testing purposes. This allows you to have "mock"/"fake"/"dummy" SSH server(s) that look and feel like "real" network devices -- as with the pytest plugin, this could be useful in CI, or it could just be handy for offline testing.
As you'd expect, if you are writing tests and wanting to have some reasonable assurances that your code that interacts with scrapli is doing what you think it should be doing, then you probably want to use the pytest plugin! If you just want to have a mock SSH server to play with, then the collector/server may be interesting to you.
Pytest Plugin¶
Overview and Use Case¶
As shown in the quickstart guide, getting going with the pytest plugin is fairly straightforward -- tests that
contain scrapli operations can be marked with the scrapli_replay
marker, causing scrapli replay to automatically
wrap this test and record or replay sessions within the test.
In order for scrapli replay to do this, there is one big caveat: the scrapli connection must be opened within the
test! Projects like pytest-vcr
don't have this requirement because the sessions are stateless HTTP(s) sessions --
this is of course not the case for Telnet/SSH where we have more or less a stateful connection object. This may
sound like a limiting factor for scrapli replay and perhaps it is, however it is relatively easy to work with as
you'll see below!
Here is a very simple example of a class that creates a scrapli connection and has some methods to do stuff:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
Let's pretend we want to write some tests for the do_stuff
method of this example class. We probably want to have
at least three test cases for this method:
- Testing a failed result from the initial show command
- Testing a success event where we properly get and parse the version string
- Testing a failed parsing of the version string
For cases one and three we probably don't want or need scrapli replay -- we could simply patch the send_command
method of scrapli, returning bad data -- either a bad scrapli Response
object, or a Response
object with data
that will cause our regex to fail.
For case number 2, however, we could also patch scrapli and return correct data, this would validate that our function, when given appropriate outputs from scrapli, does what it should do. This would be a valuable test. With scrapli replay, however, we can take this a bit further! We can now create a test case that records actual device inputs and outputs and saves that data in a scrapli replay session. Subsequent tests can then replay that input and output data. Rather than just testing that our regex works w/ some patched response data we can now very simply test not only that, but also scrapli -- ensuring that scrapli is behaving as you would expect!
How it Works¶
Before jumping into how to use scrapli replay, it's worth spending a bit of time to understand how it works. At a
high level, scrapli replay is a Pytest plugin that you can "mark" tests with. By marking a test you are effectively
"wrapping" that test in the scrapli replay ScrapliReplay
class.
The pytest plugin then uses the ScrapliReplay
class as a context manager, yielding to your test within the context
manager. For tests that are marked asyncio
we simply use the async context manager capability instead of the
synchronous version. This selection of sync vs async happens transparently to you -- you just need to mark your
tests with the asyncio
marker if they are asyncio (which you had to do anyway, so no biggie!).
While the ScrapliReplay
context manager is active (while your test is running) ScrapliReplay
patches the open
method of scrapli and a ConnectionProfile
is recorded (host/user/is using password/auth
bypass/etc.). This ConnectionProfile
is stored as part of the scrapli replay session data -- allowing us to
validate that during subsequent test runs the connection information has not changed (if it has we raise an
exception to fail the test).
After the ConnectionProfile
is recorded, the scrapli Channel
(or AsyncChannel
) read and write methods are
patched (replaced) with scrapli replay read/write methods. If the current test iteration is in "record" mode, we
patch with the "record" read/write, otherwise we patch with the "replay" read/write -- these methods do what they
sound like! Recording or replaying session data.
At completion of your test, when the context manager is closing the session will be dumped to a yaml file in your session output directory (by default this is a folder located with your test file).
Due to the fact that scrapli replay uses the open method of scrapli in order to fetch connection data and also to patch the channel objects, there is a requirement that the test actually opens the connection. This sounds perhaps limiting, and probably it is somewhat, however you can fairly easily work around this by having a fixture that returns an object with the connection already opened -- this fixture currently must be scoped to the function level. This will hopefully be improved in further scrapli replay releases to allow us to cache session-wide fixtures.
How to Use it¶
As shown in the quickstart, using scrapli replay is fairly straightforward -- simply mark a test with the correct marker. The complication generally will come from needing to have the connection opened within that test being wrapped -- this section will showcase some basic ways to use scrapli replay, as well as how we can handle the connection opening problem.
Working with the example class from the overview section, let's handle test case number 2. To start, we can do this with the patching method -- without scrapli replay:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
This works reasonably well, and properly tests our regex does indeed find the version string; of course you could actually return a real device output instead of the abbreviated output here as well -- that would make things a bit more "real". This is nice, but it does not test any scrapli behavior at all as scrapli is completely patched out of the test. There must be a better way!
Let's now re-write this test using scrapli replay:
1 2 3 4 5 6 7 |
|
No patching?! Amazing! So... what is going on here?
The Example
class (from the snippet way above here) is created, which causes the scrapli connection to open, then
we call the do_stuff
method which fetches the version and parses it with some regex. Scrapli replay is "aware" of
this test due to the marker -- this basically means that this test is living inside of a scrapli replay context
manager... you can think of it as something like this:
1 2 |
|
An oversimplified example, but not by much!
If you run this example (from the examples dir in the repo) the first time the test is ran, scrapli will actually connect to your device and record the output. This of course means that you need proper credentials/access in order to get this first recording done -- using ssh keys/config file so that you don't need to store any user/creds in your test is a great way to deal with this.
At the end of the test, scrapli replay will dump the "session" data out to a yaml file in a new folder called "scrapli_replay_sessions" that was created in the same directory of your test file (you can change this, see the options section!). This "session" file looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
|
As you can see, connection details are stored (but never credentials) -- in the event of password authentication the password is not stored and is marked as "REDACTED" in the interactions output.
Running the test again you'll notice that its even faster than scrapli normally is! Why? Because there is no actual connection going out to the device, the connection will just be automatically replayed from this session data!
Now if you have a billion tests to write, or you are needing to pass lots of inputs in order to create your scrapli connection objects in every single test... that wouldn't be very fun! In cases like this it would be a great idea to put either the scrapli connection object, or the device containing the connection object into a fixture and allowing pytest to pass that fixture into each test function. Here is a simple example of a fixture for our example setup:
1 2 3 4 5 6 7 8 |
|
And... a test taking advantage of this fixture:
1 2 3 4 |
|
It is important to note that the fixture scope must be set to function
-- again, this is because scrapli replay
requires the connection to be opened within the test it is wrapping in order to properly record the connection
profile and patch the read/write methods!
Pytest Plugin Options¶
scrapli replay supports a handful of arguments to modify its behavior, currently, these are configurable via the pytest cli -- in the future they will likely be configurable by a dedicated fixture as well.
The available options are:
Mode¶
The "replay" mode setting manages how scrapli replay handles replaying or recording sessions. This setting has the following options:
- replay: the default mode; if no session exists scrapli replay will record/create one, otherwise it will "replay" existing sessions (meaning you dont need to connect to a device)
- record: probably not needed often, does at it says -- records things. If a session exists it will auto switch to replay mode (meaning not overwrite the session)
- overwrite: overwrite existing all sessions always
This option is configurable with the --scrapli-replay-mode
switch:
1 |
|
Directory¶
By default, scrapli replay stores the recorded sessions in a directory in the same folder as the test that is being
executed. This is modifiable with the --scrapli-replay-directory
switch:
1 |
|
Overwrite¶
If you need to overwrite only certain test session data, you can do so by using the --scrapli-replay-overwrite
switch. This argument accepts a comma separated list of test names of which to overwrite the session data.
1 |
|
Disable¶
You can disable entirely the scrapli replay functionality -- meaning your tests will run "normally" without
any of the scrapli replay patching/session work happening. This is done with the --scrapli-replay-disable
flag.
1 |
|
Block Network¶
Finally, you can "block" network connections -- this will cause any connection with a valid recorded session to be
"replay"'d as normal, but any tests that would require recording a session will be skipped. The
--scrapli-replay-block-network
flag controls this.
1 |
|
Collector and Server¶
Overview¶
The scrapli replay "collector" and "server" functionality is useful for creating mock ssh servers that are "semi-interactive". You can provide any number of commands (not configs! more on this in a bit) that you would like to collect from a device, and the collector will run the provided commands at all privilege levels, and with and without "on_open" functionality being executed (generally this means with and without paging being disabled). The collector will also collect any on open commands, on close commands, all privilege escalation/deescalation commands, and "unknown" or invalid command output from every privilege level.
Just like the pytest plugin, the scrapli replay collector will output the collected data to a yaml file. This yaml file is then consumed by the scrapli replay server. The server itself is an asyncssh server that does its best to look and feel just like the real device that you collected the data from.
Collector¶
As outlined in the overview section, the collector.... collects things! The collector tries to collect as much info from the device as is practical, with the ultimate goal of being able to allow the server to look pretty close to a real device.
Before continuing, it is important to note that currently the collector can only be used with network devices --
meaning it must be used with a scrapli platform that extends the NetworkDriver
class; moreover it must be used
with a synchronous transport. There will likely not be any asyncio support for the collector (it doesn't seem to
be very valuable to add asyncio support... please open an issue if you disagree!).
To get started with the collector is fairly straight forward, simply create a collector class, passing in the commands you wish to collect, some details about "paging" (more on this in a sec), and the kwargs necessary to create the scrapli connection to collect from:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
If you are familiar with scrapli connections, the above snippet should look fairly similar! In addition to the scrapli connection data we see a few extra things:
channel_inputs
-- a list of "inputs" you wish to send to the device for recording. Each of these inputs will be run at every privilege level of the device, and before and after executing the "on_open" function (if applicable)interact_events
-- similar to "normal" scrapli, a list of lists of tuples of "interact events" to record at each privilege level (and before/after on_open)paging_indicator
-- this is what it sounds like -- a string that lets us know if the device has paginated output datapaging_escape_string
-- a string to send to "cancel" a command output if paging is encountered -- typically an escape, or aq
works for most devices
Note -- you can also pass an existing scrapli connection to the scrapli_connection
argument if you prefer
(instead of the kwargs needed to create a connection)!
Once a collector object has been created, you can open the connection and simply run the collect
method, followed
by the dump
method:
1 2 3 4 |
|
The session data will be dumped to a yaml file called "scrapli_replay_collector_session.yaml" (configurable with the
collector_session_filename
argument) in your current directory. Once you have a session stored, you can run the
"server" to create a semi-interactive ssh server!
Note -- unless you have real dns server(s) setup, and you can resolve things, you should disable domain-lookup -- if you don't the timeouts may (will!?) get exceeded and it will cause collection to fail in confusing ways.
Server¶
Starting the scrapli replay server is simple!
1 2 3 4 5 6 7 8 9 10 11 12 |
|
You can pass whatever port you wish for the port
argument, and the collect_data
must be the collected
data from the collector.
Once the server is running you should be able to SSH to the server on the provided port just as if it were a "real" device! The username and password will always be "scrapli" regardless of what the credentials were for the collected server -- this is done so we never have to deal with or think about storing credentials.
There are several big caveats to be aware of!
- Credentials: username/password (including for "enable" password) will always be "scrapli", as mentioned this is to keep things simple and not deal with storing any credential data
- Configs: configuration things are not supported and probably won't ever be. It would be a lot of work to keep track of when/if a user sends a config and what the resulting configuration would look like.
The remaining major caveats are all around the "paging" behavior of the mock server(s). Before diving into these caveats, it is worth knowing a little bit about how scrapli behaves in general. Typical scrapli connections are opened, and an "on_open" function is executed -- this function normally disables pagination on a device, this is done so scrapli never has to deal with prompts like "--More--" during the output of a command. The collector/server stores the commands that are executed in the "on_open" function, and assumes that these commands disable pagination for the given device (this is true for all core platforms, so it must be true, right? :)). With that out of the way:
- Disabling pagination requires all "on_open" commands to be executed: if the "on_open" command for your platform contains "terminal length 0" and "terminal width 512", both commands must be seen before the server will disable pagination. If you send just "terminal length 0" (even though this disables pagination on IOSXE/etc.) but have not also sent "terminal width 512" the server will show you paginated output!
- Re-enabling pagination/logging in/out: You cannot re-enable pagination on the server, the only way to do this is to exit/re-connect. This is because the collector has no way to know what commands are actually doing/what commands disable/re-enable pagination (and we would never re-enable pagination in scrapli anyway!).
With all the caveats out of the way, let's check out a mock server:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
In the above output we connect to the mock server (with username/password of "scrapli") and execute the "show version" command -- as paging has not been disabled we get the lovely "--More--" pagination indicator. Simply sending another return here gets us back to our prompt.
Continuing on... let's try to disable paging:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
Whoops - you can see that sending "terminal width 511" (instead of the "correct" command from the "on_open" function "terminal width 512") caused the server to send us an "Unknown command" output -- similar to if you sent an bad command on a "real" switch.
Now that we have paging disabled, we can try the "show version" command again:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 |
|
That looks about right! How about config mode?
1 2 3 4 5 6 7 |
|
Sending a "show" command in config mode fails like you'd expect too. This is because we "collected" all the requested inputs at every privilege level. We can't send configs really because we didn't collect any and collector/server is not built to deal with configs anyway.
Ok, back down to exec?
1 2 3 4 5 6 7 8 9 |
|
Down to exec no problem, and back up to privilege exec -- remember that the password is "scrapli"!
Thats about it for scrapli replay server -- the hope is that this can be useful for folks to do a bit of offline testing of basic scrapli (or whatever else really) scripts!