oxidized 0.30.1 → 0.32.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +3 -4
  3. data/.github/workflows/stale.yml +4 -2
  4. data/.rubocop.yml +18 -3
  5. data/.rubocop_todo.yml +4 -11
  6. data/CHANGELOG.md +93 -1
  7. data/CONTRIBUTING.md +5 -0
  8. data/Dockerfile +84 -20
  9. data/README.md +5 -21
  10. data/Rakefile +31 -2
  11. data/docs/Configuration.md +50 -14
  12. data/docs/Creating-Models.md +75 -4
  13. data/docs/DeviceSimulation.md +184 -0
  14. data/docs/Hooks.md +39 -5
  15. data/docs/Issues.md +97 -0
  16. data/docs/Model-Notes/APC_AOS.md +29 -16
  17. data/docs/Model-Notes/Cumulus.md +5 -0
  18. data/docs/Model-Notes/FSOS.md +6 -0
  19. data/docs/Model-Notes/FortiOS.md +21 -5
  20. data/docs/Model-Notes/HPEAruba.md +31 -0
  21. data/docs/Model-Notes/OS6.md +10 -0
  22. data/docs/Model-Notes/RouterOS.md +15 -0
  23. data/docs/Model-Notes/SikluMHTG.md +7 -0
  24. data/docs/ModelUnitTests.md +186 -0
  25. data/docs/Outputs.md +2 -0
  26. data/docs/Release.md +18 -15
  27. data/docs/Sources.md +21 -0
  28. data/docs/Supported-OS-Types.md +14 -7
  29. data/docs/Troubleshooting.md +35 -0
  30. data/examples/podman-compose/Makefile +59 -17
  31. data/examples/podman-compose/README.md +63 -27
  32. data/examples/podman-compose/docker-compose.yml +11 -2
  33. data/examples/podman-compose/gitserver/.gitignore +1 -0
  34. data/examples/podman-compose/gitserver/Dockerfile +14 -0
  35. data/examples/podman-compose/model-simulation/Dockerfile-model +1 -1
  36. data/examples/podman-compose/model-simulation/asternos.sh +2 -0
  37. data/examples/podman-compose/oxidized-config/.gitignore +2 -0
  38. data/examples/podman-compose/oxidized-config/config +1 -1
  39. data/examples/podman-compose/oxidized-config/config_csv-file +46 -0
  40. data/examples/podman-compose/oxidized-config/config_csv-gitserver +56 -0
  41. data/examples/podman-compose/oxidized-ssh/.gitignore +1 -0
  42. data/extra/device2yaml.rb +245 -0
  43. data/extra/gitdiff-msteams.sh +32 -5
  44. data/extra/nagios_check_failing_nodes.rb +1 -1
  45. data/extra/rest_client.rb +1 -1
  46. data/lib/oxidized/config.rb +8 -2
  47. data/lib/oxidized/hook/githubrepo.rb +37 -7
  48. data/lib/oxidized/hook/slackdiff.rb +29 -7
  49. data/lib/oxidized/input/http.rb +1 -0
  50. data/lib/oxidized/input/ssh.rb +13 -5
  51. data/lib/oxidized/input/telnet.rb +1 -1
  52. data/lib/oxidized/manager.rb +17 -16
  53. data/lib/oxidized/model/aos7.rb +2 -0
  54. data/lib/oxidized/model/aoscx.rb +16 -2
  55. data/lib/oxidized/model/aosw.rb +8 -2
  56. data/lib/oxidized/model/apc_aos.rb +1 -1
  57. data/lib/oxidized/model/arubainstant.rb +90 -0
  58. data/lib/oxidized/model/asa.rb +2 -1
  59. data/lib/oxidized/model/asyncos.rb +1 -1
  60. data/lib/oxidized/model/audiocodes.rb +2 -2
  61. data/lib/oxidized/model/cnos.rb +13 -10
  62. data/lib/oxidized/model/cumulus.rb +19 -2
  63. data/lib/oxidized/model/dlink.rb +1 -0
  64. data/lib/oxidized/model/dlinknextgen.rb +3 -0
  65. data/lib/oxidized/model/edgecos.rb +2 -1
  66. data/lib/oxidized/model/enterprise_sonic.rb +46 -0
  67. data/lib/oxidized/model/eos.rb +2 -0
  68. data/lib/oxidized/model/f5os.rb +17 -0
  69. data/lib/oxidized/model/firewareos.rb +10 -1
  70. data/lib/oxidized/model/fortios.rb +24 -1
  71. data/lib/oxidized/model/fsos.rb +5 -1
  72. data/lib/oxidized/model/garderos.rb +43 -0
  73. data/lib/oxidized/model/h3c.rb +1 -1
  74. data/lib/oxidized/model/ibos.rb +1 -0
  75. data/lib/oxidized/model/ios.rb +20 -12
  76. data/lib/oxidized/model/iosxr.rb +1 -1
  77. data/lib/oxidized/model/junos.rb +1 -1
  78. data/lib/oxidized/model/kornfeldos.rb +33 -0
  79. data/lib/oxidized/model/lenovonos.rb +2 -0
  80. data/lib/oxidized/model/linuxgeneric.rb +1 -1
  81. data/lib/oxidized/model/model.rb +2 -2
  82. data/lib/oxidized/model/netgear.rb +1 -1
  83. data/lib/oxidized/model/nodegrid.rb +1 -1
  84. data/lib/oxidized/model/nsxdfw.rb +30 -0
  85. data/lib/oxidized/model/nxos.rb +2 -1
  86. data/lib/oxidized/model/os6.rb +48 -0
  87. data/lib/oxidized/model/rgos.rb +1 -1
  88. data/lib/oxidized/model/riverbed.rb +104 -0
  89. data/lib/oxidized/model/routeros.rb +2 -2
  90. data/lib/oxidized/model/saos.rb +18 -1
  91. data/lib/oxidized/model/siklumhtg.rb +22 -0
  92. data/lib/oxidized/model/sonicos.rb +8 -2
  93. data/lib/oxidized/model/tplink.rb +1 -0
  94. data/lib/oxidized/model/uplinkolt.rb +46 -0
  95. data/lib/oxidized/model/vyatta.rb +2 -2
  96. data/lib/oxidized/model/xos.rb +7 -0
  97. data/lib/oxidized/node.rb +30 -18
  98. data/lib/oxidized/nodes.rb +13 -5
  99. data/lib/oxidized/output/file.rb +45 -42
  100. data/lib/oxidized/output/git.rb +185 -160
  101. data/lib/oxidized/output/gitcrypt.rb +188 -186
  102. data/lib/oxidized/output/http.rb +53 -51
  103. data/lib/oxidized/output/output.rb +6 -4
  104. data/lib/oxidized/source/csv.rb +44 -49
  105. data/lib/oxidized/source/http.rb +63 -81
  106. data/lib/oxidized/source/jsonfile.rb +63 -0
  107. data/lib/oxidized/source/source.rb +73 -18
  108. data/lib/oxidized/source/sql.rb +66 -59
  109. data/lib/oxidized/version.rb +2 -2
  110. data/oxidized.gemspec +25 -18
  111. metadata +115 -21
@@ -6,12 +6,18 @@ help:
6
6
 
7
7
  rights:
8
8
  podman unshare chown -R 30000:30000 oxidized-config oxidized-ssh
9
+ podman unshare chown -R 30001 gitserver/repo.git
9
10
 
10
11
  clean-rights:
11
12
  podman unshare chown -R 0:0 *
12
13
 
13
- start: rights model-image
14
- podman-compose -p oxidized up
14
+ start: gitserver-createrepo rights images
15
+ if [ -f oxidized-config/config ]; then \
16
+ podman-compose -p oxidized up ; \
17
+ else { \
18
+ echo "\n########\noxidized-config/config does not exist"; \
19
+ echo "create one or copy an example in the folder"; \
20
+ } fi
15
21
 
16
22
  run: start
17
23
 
@@ -23,7 +29,7 @@ start-local:
23
29
  if [ -f oxidized-config/config.local ]; then \
24
30
  cp oxidized-config/config.local oxidized-config/config; \
25
31
  else \
26
- echo "oxidized-config/config.local does not exist"; \
32
+ echo "\n########\noxidized-config/config.local does not exist"; \
27
33
  fi
28
34
  $(MAKE) start
29
35
 
@@ -31,31 +37,67 @@ stop-local: stop
31
37
  if [ -f oxidized-config/config.local ]; then \
32
38
  git checkout -- oxidized-config/config; \
33
39
  else \
34
- echo "oxidized-config/config.local does not exist"; \
40
+ echo "\n########\noxidized-config/config.local does not exist"; \
35
41
  fi
36
42
 
43
+ # creates a container image for the model simulation
37
44
  model-image:
38
- podman image exists localhost/local/model || \
45
+ podman image exists local/model || \
39
46
  podman build -t local/model -f model-simulation/Dockerfile-model .
40
47
 
41
48
  model-clean:
42
49
  podman rmi local/model
43
50
 
44
- clean: stop-local model-clean
51
+ # creates a container image for gitserver
52
+ gitserver-image:
53
+ podman image exists local/gitserver || \
54
+ podman build -t local/gitserver gitserver/
55
+
56
+ # create the repo repo.git inside the gitserver mapped volume
57
+ gitserver-createrepo: clean-rights
58
+ if [ ! -d gitserver/repo.git ]; then \
59
+ git init --bare gitserver/repo.git; \
60
+ fi
61
+
62
+ gitserver-clean:
63
+ podman rmi local/gitserver
64
+ rm -rf gitserver/repo.git
65
+
66
+ gitserver-getkey:
67
+ podman exec --user oxidized -t oxidized_oxidized_1 sh -c "ssh-keyscan gitserver > /home/oxidized/.ssh/known_hosts"
68
+
69
+ # build all helper containter images
70
+ images: model-image gitserver-image oxidized-image
71
+
72
+ # build the oxidized image from the curent repository
73
+ oxidized-image:
74
+ podman build -t oxidized:`git describe --tags` -t oxidized:latest ../../
75
+
76
+ # removes the oxidized image
77
+ oxidized-image-clean:
78
+ podman rmi local/oxidized
79
+
80
+ # run evey clean line, even if the previous fails
81
+ clean:
82
+ -$(MAKE) stop-local
83
+ -$(MAKE) model-clean
84
+ -$(MAKE) gitserver-clean
85
+ -$(MAKE) oxidized-image-clean
45
86
 
46
87
  define HELP
47
88
  make help - This help
48
- make rights - Change the rights of mapped folders for user oxidized
49
- in the container
89
+ make rights - Change the rights of mapped folders for the users inside
90
+ the container
50
91
  make clean-rights - Revert the rights of mapped folders to the local user
51
- make start - Start the containter
92
+ make start - Start the pod with all containers (alias - make run)
52
93
  You can interrupt with Ctrl-C, but make sure you run
53
- make stop to realy stop the container
54
- make run - Same as make start
55
- make stop - Stop the containter
56
- make start-local - Starts the container with the local configuration config.local
57
- make stop-local - Stops the container and restores oxidized-config/config from git
58
- make model-image - Creates a local OCI-Image to run simulated devices
59
- make model-clean - Removes the local OCI-Image to run simulated devices
60
- make clean - make stop-local + model-clean
94
+ 'make stop' to realy stop the container
95
+ make stop - Stop the pod
96
+ make start-local - Starts the pod with the local configuration
97
+ oxidized-config/config.local
98
+ make stop-local - Stops the pod and restores
99
+ oxidized-config/config from git
100
+ make gitserver-getkey - stores the public key of the gitserver into
101
+ oxidized-ssh/known_hosts (the pod must be running)
102
+ make clean - reverts everything to its original state
61
103
  endef
@@ -1,35 +1,47 @@
1
- # Running oxidized with podman-compose
2
- This is an example of Oxidized running within an OCI container, provided by
3
- podman and podman-compose.
1
+ # Running Oxidized with podman-compose
2
+ This example demonstrates running Oxidized within an OCI container using
3
+ podman-compose. It’s actively used in Oxidized development to validate the
4
+ container’s functionality and to simulate potential issues.
4
5
 
5
- In order to have the example work out of the box, a network device is simulated.
6
- The model asternos has been chosen because there were not too many commands to
7
- implement.
6
+ While this example uses podman and podman-compose, it should also be compatible
7
+ with docker, as podman supports docker’s CLI.
8
8
 
9
- To run the example, just run `make start`. You should be sure to have installed the
10
- [dependencies](#dependencies) before.
9
+ To make this example work seamlessly, a simulated network device is included.
10
+ The asternos model is used here for simplicity, as it requires minimal commands
11
+ to implement. The simulated output doesn’t replicate real device responses but
12
+ provides changing lines over time to test Oxidized’s functionality.
11
13
 
12
- To exit, press `CTRL-C` or run `make stop` in a separate shell. If you exit
13
- with `CTRL-C`, make sure to run `make stop` after it, in order to clean up the
14
- running environment.
14
+
15
+ The example also provides a Git server to test the interaction with it.
16
+
17
+ # Run the example
18
+ > :warning: the example builds local containers and will require at least 2 GB
19
+ > of disk space along with some CPU and time during the first run.
20
+
21
+ To start the example, simply run `make start`. Ensure you have installed the
22
+ necessary [dependencies](#dependencies) before.
23
+
24
+ To stop, press `CTRL-C` or run `make stop` in a separate shell. If you exit
25
+ with `CTRL-C`, make sure to run `make stop` afterward to properly clean up the
26
+ environment.
15
27
 
16
28
  ## Running Environment
17
- This example of oxidized with podman-compose has been run on Debian
18
- Bookworm (Version 12), but should work with few adaptations on any Linux
29
+ This example of oxidized with podman-compose is running on Debian
30
+ Bookworm (Version 12). It should work with few adaptations on any Linux
19
31
  box running podman, and maybe also with docker.
20
32
 
21
33
  ## Dependencies
22
- You need to install some packages on your debian system:
34
+ To get started, install the required packages on your Debian system:
23
35
  ```shell
24
36
  sudo apt install podman containers-storage podman-compose make
25
37
  ```
26
38
 
27
- You also want to make sure that podman uses the overlay driver for storing its images.
28
- If not, it will save every layer of the container to disk (and not only the delta),
29
- so it will fill your disk very fast.
39
+ Ensure Podman is using the overlay driver for image storage.
40
+ Without this driver, Podman may save every container layer separately rather
41
+ than only the changes, which can quickly consume disk space.
30
42
 
31
- This happens if you run podman without having installed the package `container-storage`
32
- before.
43
+ This issue can occur if podman was run before installing the
44
+ `container-storage` package.
33
45
 
34
46
  ```shell
35
47
  podman info | grep graphDriverName
@@ -43,16 +55,40 @@ You should get this reply
43
55
  If not, the quick way I found to solve it is to delete `~/.local/share/containers/`.
44
56
  Beware - this will delete **all** your containers!
45
57
 
46
- ## I want to adapt this to my needs
47
- Feel free and have fun. You probably want to edit docker-compose.yml in order to remove the
48
- simulated model.
58
+ ## Adapting to your needs
59
+ Feel free to customize this setup as you wish! You may want to edit
60
+ `docker-compose.yml` to remove any containers simulating specific components.
49
61
 
50
- ## Use your own oxidized configuration within the git repository
51
- When developing oxidized and testing the container, you may want to use your
52
- own configuration. This can be done by saving it under `oxidized-config/config.local`
62
+ ## Use your own oxidized configuration in the git repository
63
+ When developing oxidized or testing the container, you may want to use a custom
64
+ configuration. This can be done by saving it under `oxidized-config/config.local`
53
65
 
54
66
  `make start-local` will recognize the local configuration and copy it to
55
67
  `oxidized-config/config` before starting the container.
56
68
 
57
- You shoud stop the container with `make stop-local` in order to restore the original
58
- configuration from git.
69
+ You should stop the container with `make stop-local` in order to restore the
70
+ original configuration from the git repository.
71
+
72
+ In the folder `oxidized-config/, you will also find some example configs,
73
+ for example `config_csv-gitserver`. To use them, just copy the file to `config`.
74
+
75
+ ## Git server public keys
76
+ To enable Oxidized to access the Git server, you'll need to retrieve the
77
+ servers' public SSH keys and store them under `oxidized-ssh/known_hosts`.
78
+ Without this, you will encounter the following error:
79
+
80
+ ```
81
+ ERROR -- : Hook push_to_remote (#<GithubRepo:0x00007f4cff47d918>) failed (#<Rugged::SshError: invalid or unknown remote ssh hostkey>) for event :post_store
82
+ ```
83
+
84
+ While the container environment is running (`make start`), open a separate shell
85
+ and run:
86
+ ```
87
+ make gitserver-getkey
88
+ ```
89
+
90
+ You do not need to restart the container environment; Oxidized will
91
+ automatically use the key the next time it pushes to the remote Git repository.
92
+
93
+
94
+
@@ -1,8 +1,10 @@
1
1
  services:
2
2
  oxidized:
3
3
  # Choose the image that you want to test
4
- # image: docker.io/oxidized/oxidized:0.29.1
5
- image: docker.io/oxidized/oxidized:latest
4
+ # image: docker.io/oxidized/oxidized:0.30.1
5
+ # image: docker.io/oxidized/oxidized:latest
6
+ # local/oxidized is build by "make oxidized-image" and "make run"
7
+ image: local/oxidized
6
8
  ports:
7
9
  - 127.0.0.1:8042:8888/tcp
8
10
  environment:
@@ -13,9 +15,16 @@ services:
13
15
  volumes:
14
16
  - ./oxidized-config:/home/oxidized/.config/oxidized
15
17
  - ./oxidized-ssh:/home/oxidized/.ssh
18
+
16
19
  # This is a simulated network device for the example to work out of the box
17
20
  asternos-device:
18
21
  image: localhost/local/model
19
22
  volumes:
20
23
  - ./model-simulation/asternos.sh:/home/oxidized/.profile
21
24
  - ./model-simulation/asternos.sh:/home/admin/.profile
25
+
26
+ # This is a gitserver to push our configs
27
+ gitserver:
28
+ image: localhost/local/gitserver
29
+ volumes:
30
+ - ./gitserver/repo.git:/home/git/repo.git
@@ -0,0 +1 @@
1
+ repo.git
@@ -0,0 +1,14 @@
1
+ FROM docker.io/phusion/baseimage:noble-1.0.0
2
+
3
+ # Use baseimage-docker's init system.
4
+ CMD ["/sbin/my_init"]
5
+
6
+ # enable ssh
7
+ RUN rm -f /etc/service/sshd/down
8
+ RUN /etc/my_init.d/00_regen_ssh_host_keys.sh
9
+
10
+ # Add user for the gitserver. The password is "git"
11
+ RUN useradd -m git -p '$6$32WDb0LTFyQkLffy$u15COVx7CQ4tgp4JT4DO4LJ96q/jwFSpuZC3WrllNQDNa6nW1LhJKW9rLV57ak3rj9Ln./aRA85jzeof1B0Gi1' -s /bin/bash -u 30001
12
+
13
+ # And install git
14
+ RUN install_clean git
@@ -1,4 +1,4 @@
1
- FROM docker.io/phusion/baseimage:jammy-1.0.2
1
+ FROM docker.io/phusion/baseimage:noble-1.0.0
2
2
 
3
3
  # Use baseimage-docker's init system.
4
4
  CMD ["/sbin/my_init"]
@@ -16,6 +16,8 @@ EOF
16
16
  function show() {
17
17
  if [ "$*" == "version" ]; then
18
18
  echo "Version 1.2.3"
19
+ # Make the output change over time
20
+ date
19
21
  elif [ "$*" == "runningconfiguration all" ]; then
20
22
  cat << EOF
21
23
  ! begin of the configuration
@@ -3,6 +3,8 @@ config.local
3
3
  router.db.local
4
4
 
5
5
  # Ignore logs, retrieved configs...
6
+ pid
6
7
  configs/
7
8
  crash
8
9
  logs/
10
+ oxidized.git/
@@ -10,7 +10,7 @@ use_max_threads: true
10
10
  timeout: 20
11
11
  retries: 3
12
12
  prompt: !ruby/regexp /^([\w.@-]+[#>]\s?)$/
13
- rest: 127.0.0.1:8888
13
+ rest: 0.0.0.0:8888
14
14
  next_adds_job: false
15
15
  vars: {}
16
16
  groups: {}
@@ -0,0 +1,46 @@
1
+ ---
2
+ username: oxidized
3
+ password: oxidized
4
+ resolve_dns: true
5
+ interval: 3600
6
+ use_syslog: false
7
+ debug: false
8
+ threads: 30
9
+ use_max_threads: true
10
+ timeout: 20
11
+ retries: 3
12
+ prompt: !ruby/regexp /^([\w.@-]+[#>]\s?)$/
13
+ rest: 0.0.0.0:8888
14
+ next_adds_job: false
15
+ vars: {}
16
+ groups: {}
17
+ group_map: {}
18
+ models: {}
19
+ pid: "~/.config/oxidized/pid"
20
+ crash:
21
+ directory: "~/.config/oxidized/crashes"
22
+ hostnames: false
23
+ stats:
24
+ history_size: 10
25
+ input:
26
+ default: ssh
27
+ debug: false
28
+ ssh:
29
+ secure: false
30
+ ftp:
31
+ passive: true
32
+ utf8_encoded: true
33
+ output:
34
+ default: file
35
+ file:
36
+ directory: "~/.config/oxidized/configs/"
37
+ source:
38
+ default: csv
39
+ csv:
40
+ file: "~/.config/oxidized/router.db"
41
+ delimiter: !ruby/regexp /:/
42
+ map:
43
+ name: 0
44
+ model: 1
45
+ ip: 2
46
+ gpg: false
@@ -0,0 +1,56 @@
1
+ ---
2
+ username: oxidized
3
+ password: oxidized
4
+ resolve_dns: true
5
+ interval: 3600
6
+ use_syslog: false
7
+ debug: false
8
+ threads: 30
9
+ use_max_threads: true
10
+ timeout: 20
11
+ retries: 3
12
+ prompt: !ruby/regexp /^([\w.@-]+[#>]\s?)$/
13
+ rest: 0.0.0.0:8888
14
+ next_adds_job: false
15
+ vars: {}
16
+ groups: {}
17
+ group_map: {}
18
+ models: {}
19
+ pid: "~/.config/oxidized/pid"
20
+ crash:
21
+ directory: "~/.config/oxidized/crashes"
22
+ hostnames: false
23
+ stats:
24
+ history_size: 10
25
+ input:
26
+ default: ssh
27
+ debug: false
28
+ ssh:
29
+ secure: false
30
+ ftp:
31
+ passive: true
32
+ utf8_encoded: true
33
+ output:
34
+ default: git
35
+ git:
36
+ user: Oxidized
37
+ email: o@example.com
38
+ repo: "~/.config/oxidized/oxidized.git"
39
+ source:
40
+ default: csv
41
+ csv:
42
+ file: "~/.config/oxidized/router.db"
43
+ delimiter: !ruby/regexp /:/
44
+ map:
45
+ name: 0
46
+ model: 1
47
+ ip: 2
48
+ gpg: false
49
+ hooks:
50
+ push_to_remote:
51
+ type: githubrepo
52
+ events:
53
+ - post_store
54
+ remote_repo: git@gitserver:repo.git
55
+ username: git
56
+ password: git
@@ -0,0 +1 @@
1
+ known_hosts
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'net/ssh'
5
+ require 'optparse'
6
+ require 'etc'
7
+ require 'timeout'
8
+
9
+ # This scripts logs in a network device and outputs a yaml file that can be
10
+ # used for model unit tests in spec/model/
11
+ # For more information, see docs/DeviceSimulation.md
12
+
13
+ # This script is quick & dirty - it grew with the time an could be a project
14
+ # for its own. It works, and that should be enough ;-)
15
+
16
+ ################# Methods
17
+ # Runs cmd in the ssh session, either im exec mode or with a tty
18
+ # saves the output to @output
19
+ def ssh_exec(cmd)
20
+ puts "\n### Sending #{cmd}..."
21
+ @output&.puts " #{cmd}: |-"
22
+
23
+ if @exec_mode
24
+ @ssh_output = @ssh.exec! cmd + "\n"
25
+ else
26
+ @ses.send_data cmd + "\n"
27
+ shell_wait
28
+ end
29
+ yaml_output(' ')
30
+ end
31
+
32
+ # Wait for the ssh command to be executed, with an idle timout @idle_timeout
33
+ # Pressing CTRL-C exits the script
34
+ # Pressing ESC termiates the idle timeout
35
+ def shell_wait
36
+ @ssh_output = ''
37
+ # ssh_output gets appended by chanel.on-data (below)
38
+ # We store the current length of @ssh_output in @ssh_output_length
39
+ # if @ssh_output.length is bigger than @ssh_output_length, we got new data
40
+ @ssh_output_length = 0
41
+
42
+ # Keep track of time for idle timeout
43
+ start_time = Time.now
44
+
45
+ # Loop & wait for @idle_timeout seconds after last output
46
+ # 0.1 means that the loop should run at least once per 0.1 second
47
+ @ssh.loop(0.1) do
48
+ # if @ssh_output is longer than our saved length, we got new output
49
+ if @ssh_output_length < @ssh_output.length
50
+ # reset the timer and save the new output length
51
+ start_time = Time.now
52
+ @ssh_output_length = @ssh_output.length
53
+ end
54
+
55
+ # We wait for 0.1 seconds if a key was pressed
56
+ begin
57
+ Timeout.timeout(0.1) do
58
+ # Get input // this is a blocking call
59
+ char = $stdin.getch
60
+ # If ctrl-c is pressed, exit the script
61
+ if char == "\u0003"
62
+ puts '### CTRL-C pressed, exiting'
63
+ cleanup
64
+ exit
65
+ end
66
+ # If escape is pressed, terminate idle timeout
67
+ if char == "\e"
68
+ puts "\n### ESC pressed, skipping idle timeout"
69
+ return false
70
+ else
71
+ # if not, send the char through ssh
72
+ @ses.send_data char
73
+ end
74
+ end
75
+ rescue Timeout::Error
76
+ # No key pressed
77
+ end
78
+
79
+ # exit the loop when the @idle_timeout has been reached (false = exit)
80
+ Time.now - start_time < @idle_timeout
81
+ end
82
+ end
83
+
84
+ def yaml_output(prepend = '')
85
+ # Now print the collected output to @output
86
+ firstline = true
87
+
88
+ # as we want to prepend 'prepend' to each line, we need each_line and chomp
89
+ # chomp removes the trainling \n
90
+ @ssh_output.each_line(chomp: true) do |line|
91
+ # encode line and remove the first and the trailing double quote
92
+ line = line.dump[1..-2]
93
+ if firstline
94
+ # Make sure the leading space of the first line (if present)
95
+ # is coded with \0x20 or YAML block scalars won't work
96
+ line.sub!(/^\A /, '\x20')
97
+ firstline = false
98
+ end
99
+ # Make sure trailing white spaces are coded with \0x20
100
+ line.gsub!(/ $/, '\x20')
101
+ # prepend white spaces for the yaml block scalar
102
+ line = prepend + line
103
+ @output&.puts line
104
+ end
105
+ end
106
+
107
+ def cleanup
108
+ (@ssh.close rescue true) unless @ssh.closed?
109
+ @output&.close
110
+ end
111
+
112
+ ################# Main loop
113
+
114
+ # Define options
115
+ options = {}
116
+ optparse = OptionParser.new do |opts|
117
+ opts.banner = <<~HEREDOC
118
+ Usages:
119
+ - device2yaml.rb [user@]host -i file [options]
120
+ - device2yaml.rb [user@]host -c "command1
121
+ command2
122
+ command3" [options]
123
+
124
+ -i and -c are mutualy exclusive, one must be specified
125
+
126
+ [options]:
127
+ HEREDOC
128
+
129
+ opts.on('-c', '--commands "command list"', 'specify the commands to be run') do |cmds|
130
+ options[:commands] = cmds
131
+ end
132
+ opts.on('-i', '--input file', 'Specify an input file for commands to be run') do |file|
133
+ options[:input] = file
134
+ end
135
+ opts.on('-o', '--output file', 'Specify an output YAML-file') do |file|
136
+ options[:output] = file
137
+ end
138
+ opts.on('-t', '--timeout value', Integer,
139
+ 'Specify the idle timeout beween commands (default: 5 seconds)') do |timeout|
140
+ options[:timeout] = timeout
141
+ end
142
+ opts.on('-e', '--exec-mode', 'Run ssh in exec mode (without tty)') { @exec_mode = true }
143
+ opts.on '-h', '--help', 'Print this help' do
144
+ puts opts
145
+ exit
146
+ end
147
+ end
148
+
149
+ # Catch and parse the first argument
150
+ if ARGV[0] && ARGV[0][0] != '-'
151
+ argument = ARGV.shift
152
+ if argument.include?('@')
153
+ ssh_user, ssh_host = argument.split('@')
154
+ else
155
+ ssh_user = Etc.getlogin
156
+ ssh_host = argument
157
+ end
158
+ else
159
+ puts 'Missing a host to connect to...'
160
+ puts
161
+ puts optparse
162
+ exit 1
163
+ end
164
+
165
+ # Parse the options
166
+ optparse.parse!
167
+
168
+ # Get the commands to be run against ssh_host
169
+ # ^ = xor = exclusive or
170
+ unless options[:commands].nil? ^ options[:input].nil?
171
+ puts "Please provide commands to be run against #{ssh_host} with either option -c or -i"
172
+ puts
173
+ puts optparse
174
+ exit 1
175
+ end
176
+
177
+ if options[:commands]
178
+ ssh_commands = []
179
+ options[:commands].each_line(chomp: true) { |command| ssh_commands << command }
180
+ elsif options[:input]
181
+ ssh_commands = File.read(options[:input]).split(/\n+|\r+/)
182
+ end
183
+
184
+ puts "Running #{ssh_commands} on #{ssh_user}@#{ssh_host}"
185
+
186
+ # Defaut idle timeout: 5 seconds, as tests showed that 2 seconds is too short
187
+ @idle_timeout = options[:timeout] || 5
188
+
189
+ # We will use safe navifation (&.) to call the methods on @output only
190
+ # if @output is not nil
191
+ @output = options[:output] ? File.open(options[:output], 'w') : nil
192
+
193
+ @ssh = Net::SSH.start(ssh_host,
194
+ ssh_user,
195
+ { timeout: 10,
196
+ append_all_supported_algorithms: true })
197
+
198
+ @ssh_output = ''
199
+
200
+ unless @exec_mode
201
+ @ses = @ssh.open_channel do |ch|
202
+ ch.on_data do |_ch, data|
203
+ @ssh_output += data
204
+ # Output the data to stdout for interactive control
205
+ # remove ANSI escape codes, as they can produce problems
206
+ # The code will be printed as '\e[123m' in the output
207
+ print data.gsub("\e", '\e')
208
+ end
209
+ ch.request_pty(term: 'vt100') do |_ch, success_pty|
210
+ raise "Can't get PTY" unless success_pty
211
+
212
+ ch.send_channel_request 'shell' do |_ch, success_shell|
213
+ raise "Can't get shell" unless success_shell
214
+ end
215
+ end
216
+ ch.on_extended_data do |_ch, _type, data|
217
+ $stderr.print "Error: #{data}\n"
218
+ end
219
+ end
220
+ end
221
+
222
+ # YAML begin of file
223
+ @output&.puts '---'
224
+
225
+ if @exec_mode
226
+ # init prompt does not exist and is empty in exec mode
227
+ @output&.puts 'init_prompt:'
228
+ else
229
+ # get motd and first prompt
230
+ @output&.puts 'init_prompt: |-'
231
+ shell_wait
232
+ yaml_output ' '
233
+ end
234
+
235
+ @output&.puts 'commands:'
236
+
237
+ begin
238
+ ssh_commands.each do |cmd|
239
+ ssh_exec cmd
240
+ end
241
+ rescue Errno::ECONNRESET, Net::SSH::Disconnect, IOError => e
242
+ puts "### Connection closed with message: #{e.message}"
243
+ end
244
+
245
+ cleanup