Skip to content

Commit

Permalink
Create garderos model and model unit tests
Browse files Browse the repository at this point in the history
- garderos.rb: new garderos model
- spec/model/: new unit test framework for models with ssh input
- node.rb: added a debug message before creating a crashfile
  • Loading branch information
cheramr committed Aug 30, 2024
1 parent 48885d4 commit 1b082e1
Show file tree
Hide file tree
Showing 8 changed files with 407 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- fortios: variable `fullconfig` to get the configuration with default values. Fixes: #3159 (@robertcheramy)
- model for VMWare NSX DFW (@elmobp)
- model for F5OS (@teunvink)
- model for garderos (@robertcheramy)
- unit tests framework for models with ssh input (@robertcheramy)

### Changed
- h3c: change prompt to expect either angle (user-view) or square (system-view) brackets (@nl987)
Expand Down
100 changes: 99 additions & 1 deletion docs/Creating-Models.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ A user may wish to extend an existing model to collect the output of additional

This methodology allows local site changes to be preserved during Oxidized version updates / gem updates. It also enables convenient local development of new models.

## Index
- [Creating a new model](#creating-a-new-model)
- [Extending an existing model with a new command](#extending-an-existing-model-with-a-new-command)
- [Create unit tests for the model](#create-unit-tests-for-the-model)
- [Advanced features](#advanced-features)
- [Monkey-patching blocks in existing models](#monkey-patching-blocks-in-existing-models)
- [Help](#help)

## Creating a new model

An Oxidized model, at minimum, requires just three elements:
Expand All @@ -21,13 +29,19 @@ class RootWare < Oxidized::Model
using Refinements

cmd 'show complete-config'

cfg :ssh do
pre_logout 'exit'
end
end
```

This model, as-is will:

* Log into the device and expect the default prompt.
* Log into the device with ssh and expect the default prompt.
* Upon matching it, execute the command `show complete-config`
* Collect the output.
* Logout with the command `exit`

It is often useful to, at minimum, define the following additional elements for any newly introduced module:

Expand Down Expand Up @@ -72,6 +86,90 @@ Intuitively, it is also possible to:
* Create a completely new model, with a new name, for a new operating system type.
* Testing/validation of an updated model from the [Oxidized GitHub repo models](https://github.com/ytti/oxidized/tree/master/lib/oxidized/model) by placing an updated model in the proper location without disrupting the gem-supplied model files.

## Create unit tests for the model
If you want the model to be integrated into oxidized, you can
[submit a pull request on github](https://github.com/ytti/oxidized/pulls).
This is a greatly appreciated submission, as there are probably other users
using the same network device as you are.

We ask you to write a unit test for the model, in order to be sure further developments don't break your model, and to facilitate debugging issues without having access to a physical network device for the model. Writing a model unit test for SSH should be straightforward, and it is described in the next lines. If you encounter problems, open an issue or ask for help within the pull request.

You can have a look at the [Garderos unit test](spec/model/garderos_spec.rb) for an example. The model unit test consists of (at least) two files:
- a yaml file under `examples/model/`, containing the data used to simulate the network device.
- Please name your file `<model>_<hardware type>_<software_version>.yaml`, for example in the garderos unit test: `garderos_R7709_003_006_068.yaml`.
- You can create multiple files in order to support multiple devices or software versions.
- You may append a comment after the software version to differentiate between two tested features (something like `garderos_R7709_003_006_068_with_ipsec.yaml`).
- a ruby script containing the tests under `spec/model/`.
- It is named `<model>_spec.rb`, for the garderos model: `garderos_spec.rb`.
- The script described below is a minimal example; you can add as many tests as needed.

### YAML description to simulate the network device.
The yaml file has three sections:
- init_prompt: describing the lines send by the device before we can send a command. It may include motd banners, and mus include the first prompt.
- commands: the commands the model sends to the network device and the expected output. Do not forget the command needed to logout from the device.
- oxidized_output: the expected output of oxidized, so that you can compare it to the output generated by the unit test.

The outputs are multiline and use yaml block scalars (`|`), with the trailing \n removed (`-` after `|`). The outputs includes the echo of the given command and the next prompt. Some escape characters are interpreted, currently \n, \r, \x\<octal char number\>, \\\\

Here is a shortened example of a YAML file:
```yaml
---
# Trailing white spaces are coded as \x20 because some editors automatically remove trailing white spaces
init_prompt: |-
\e[4m\rLAB-R1234_Garderos#\e[m\x20
commands:
show system version: |-
show system version
grs-gwuz-armel/003_005_068 (Garderos; 2021-04-30 16:19:35)
\e[4m\rLAB-R1234_Garderos#\e[m\x20
# ...
exit: ""
oxidized_output: |-
# grs-gwuz-armel/003_005_068 (Garderos; 2021-04-30 16:19:35)
#\x20
# ...
```

### Model unit test
When creating the unit test, it is handy to have a specific section for testing different
prompts without testing the whole configuration. This is done by the first test in the following
example. The second tests takes the defined yaml file, runs the model against it and
compares the result against the yaml-section `oxidized_output`.

```ruby
require_relative 'model_helper'

describe 'model/Garderos' do
# For each test, we initialize oxidized to some default values
# and create a node with the model we want to test
# replace 'garderos' with your model
before(:each) do
init_model_helper
@node = Oxidized::Node.new(name: 'example.com',
input: 'ssh',
model: 'garderos')
end

it 'matches different prompts' do
_('LAB-R1234_Garderos# ').must_match Garderos.prompt
end

# Name the test after the tesed HW and SW. Link to your yaml data
it 'runs on R7709 with OS 003_006_068' do
mockmodel = MockSsh.new('examples/model/garderos_R7709_003_006_068.yaml')
Net::SSH.stubs(:start).returns mockmodel

status, result = @node.run

_(status).must_equal :success
_(result.to_cfg).must_equal mockmodel.oxidized_output
end
end
```

The unit tests use [minitest/spec](https://github.com/minitest/minitest?tab=readme-ov-file#specs-) and [mocha](https://github.com/freerange/mocha).
If you need more expectations for you tests, have a look at the [minitest documentation for expectations](https://docs.seattlerb.org/minitest/Minitest/Expectations.html)

## Advanced features

The loosely-coupled architecture of Oxidized allows for easy extensibility in more advanced use cases as well.
Expand Down
101 changes: 101 additions & 0 deletions examples/model/garderos_R7709_003_006_068.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
---
# Trailing white spaces are coded as \x20 because some editors automatically remove trailing white spaces
init_prompt: |-
\e[4m\rLAB-R1234_Garderos#\e[m\x20
commands:
show system version: |-
show system version
grs-gwuz-armel/003_005_068 (Garderos; 2021-04-30 16:19:35)
\e[4m\rLAB-R1234_Garderos#\e[m\x20
show system serial: |-
show system serial
Serial : R77079012345
Hardware: Model R-7700, Board GWUZ, Ethernet
\e[4m\rLAB-R1234_Garderos#\e[m\x20
show hardware wwan wwan0 sim: |-
show hardware wwan wwan0 sim
Unknown command 'wwan'.
\e[4m\rLAB-R1234_Garderos#\e[m\x20
# This is a not working configuration - but it shows everything we need to make unit tests
show configuration running: |-
show configuration running
acl.ipv4.input.1.action=ACCEPT
acl.ipv4.input.1.description=allow ssh from management
acl.ipv4.input.1.dest-ports=22
acl.ipv4.input.1.protocol=tcp
acl.ipv4.input.1.source-network=10.42.0.0/24
acl.ipv4.input.999.action=DROP
hardware.rs-232.1.enable=true
hardware.rs-232.1.name=ttyS0
interface.eth.1.description=WAN
interface.eth.1.ip-assignment=static
interface.eth.1.ipv4=10.42.101.5/24
interface.eth.1.name=eth1
route.ipv4.1.gateway=10.42.101.1
route.ipv4.1.network=10.0.0.0/8
service.console.0.authenticate.0.client-ref=TACACS-1
service.console.0.authenticate.0.type=tacacs+
service.console.0.authenticate.1.client-ref=TACACS-2
service.console.0.authenticate.1.type=tacacs+
service.console.0.authenticate.2.type=local
service.console.0.hardware-ref=ttyS0
service.snmp.query-agent.enable=true
service.snmp.query-agent.server.0.community.0.name=SECRET
service.tacacs.client.0.name=TACACS-1
service.tacacs.client.0.server.1.key={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
service.tacacs.client.0.server.1.name=10.42.0.42
service.tacacs.client.1.name=TACACS-2
service.tacacs.client.1.server.1.key={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
service.tacacs.client.1.server.1.name=10.42.0.43
system.name=LAB-R1234_Garderos
system.secret={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD
system.timezone=Europe/Berlin
tunnel.ipsec.2.auth.psk.psk={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
user.account.0.level=15
user.account.0.name=oxidized
user.account.0.password={sha256}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD
user.enable.0.password={sha256}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD
\e[4m\rLAB-R1234_Garderos#\e[m\x20
exit: ""
oxidized_output: |-
# grs-gwuz-armel/003_005_068 (Garderos; 2021-04-30 16:19:35)
#\x20
# Serial : R77079012345
# Hardware: Model R-7700, Board GWUZ, Ethernet
#\x20
acl.ipv4.input.1.action=ACCEPT
acl.ipv4.input.1.description=allow ssh from management
acl.ipv4.input.1.dest-ports=22
acl.ipv4.input.1.protocol=tcp
acl.ipv4.input.1.source-network=10.42.0.0/24
acl.ipv4.input.999.action=DROP
hardware.rs-232.1.enable=true
hardware.rs-232.1.name=ttyS0
interface.eth.1.description=WAN
interface.eth.1.ip-assignment=static
interface.eth.1.ipv4=10.42.101.5/24
interface.eth.1.name=eth1
route.ipv4.1.gateway=10.42.101.1
route.ipv4.1.network=10.0.0.0/8
service.console.0.authenticate.0.client-ref=TACACS-1
service.console.0.authenticate.0.type=tacacs+
service.console.0.authenticate.1.client-ref=TACACS-2
service.console.0.authenticate.1.type=tacacs+
service.console.0.authenticate.2.type=local
service.console.0.hardware-ref=ttyS0
service.snmp.query-agent.enable=true
service.snmp.query-agent.server.0.community.0.name=SECRET
service.tacacs.client.0.name=TACACS-1
service.tacacs.client.0.server.1.key={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
service.tacacs.client.0.server.1.name=10.42.0.42
service.tacacs.client.1.name=TACACS-2
service.tacacs.client.1.server.1.key={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
service.tacacs.client.1.server.1.name=10.42.0.43
system.name=LAB-R1234_Garderos
system.secret={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD
system.timezone=Europe/Berlin
tunnel.ipsec.2.auth.psk.psk={enc2}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDD
user.account.0.level=15
user.account.0.name=oxidized
user.account.0.password={sha256}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD
user.enable.0.password={sha256}AAAAAAAAAABBBBBBBBBBCCCCCCCCCCDDDDDDDDDD\n
43 changes: 43 additions & 0 deletions lib/oxidized/model/garderos.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
class Garderos < Oxidized::Model
using Refinements
# Garderos GmbH https://www.garderos.com/
# Routers for harsh environments
# grs = Garderos Router Software

# remove all ANSI escape codes, as GRS uses them :-(
# the prompt does not need to match escape codes, as they have been removed
expect %r{\e\[\d*m\r?} do |data, re|
data.gsub re, ''
end

prompt %r{[\w-]+# }
comment '# '

cmd :all do |cfg|
# Remove the echo of the entered command and the prompt after it
cfg.cut_both
end

cmd 'show system version' do |cfg|
comment "#{cfg}\n"
end

cmd 'show system serial' do |cfg|
comment "#{cfg}\n"
end

# If we have a radio modem installed, we'd like to list the SIM Card
cmd 'show hardware wwan wwan0 sim' do |cfg|
if cfg.start_with? 'Unknown command'
''
else
comment "#{cfg}\n"
end
end

cmd 'show configuration running'

cfg :ssh do
pre_logout 'exit'
end
end
2 changes: 2 additions & 0 deletions lib/oxidized/node.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ def run_input(input)
@err_reason = err.message.to_s
false
rescue StandardError => e
# Send a message in debug mode in case we are not able to create a crashfile
Oxidized.logger.send(:debug, '%s raised %s with msg "%s", creating crashfile' % [ip, e.class, e.message])
crashdir = Oxidized.config.crash.directory
crashfile = Oxidized.config.crash.hostnames? ? name : ip.to_s
FileUtils.mkdir_p(crashdir) unless File.directory?(crashdir)
Expand Down
28 changes: 28 additions & 0 deletions spec/model/garderos_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
require_relative 'model_helper'

describe 'model/Garderos' do
before(:each) do
init_model_helper
@node = Oxidized::Node.new(name: 'example.com',
input: 'ssh',
model: 'garderos')
end

it 'matches different prompts' do
# Pretty prompt
# Note that the real prompt looks like "\e[4m\rLAB-R1234_Garderos#\e[m\x20"
# The ANSI escape sequences are cleaned by the model (expect),
# this is tested in the test 'runs on R7709 with OS 003_006_068'
_('LAB-R1234_Garderos# ').must_match Garderos.prompt
end

it 'runs on R7709 with OS 003_006_068' do
mockmodel = MockSsh.new('examples/model/garderos_R7709_003_006_068.yaml')
Net::SSH.stubs(:start).returns mockmodel

status, result = @node.run

_(status).must_equal :success
_(result.to_cfg).must_equal mockmodel.oxidized_output
end
end
Loading

0 comments on commit 1b082e1

Please sign in to comment.