oxidized 0.30.1 → 0.32.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +3 -4
- data/.github/workflows/stale.yml +4 -2
- data/.rubocop.yml +18 -3
- data/.rubocop_todo.yml +4 -11
- data/CHANGELOG.md +93 -1
- data/CONTRIBUTING.md +5 -0
- data/Dockerfile +84 -20
- data/README.md +5 -21
- data/Rakefile +31 -2
- data/docs/Configuration.md +50 -14
- data/docs/Creating-Models.md +75 -4
- data/docs/DeviceSimulation.md +184 -0
- data/docs/Hooks.md +39 -5
- data/docs/Issues.md +97 -0
- data/docs/Model-Notes/APC_AOS.md +29 -16
- data/docs/Model-Notes/Cumulus.md +5 -0
- data/docs/Model-Notes/FSOS.md +6 -0
- data/docs/Model-Notes/FortiOS.md +21 -5
- data/docs/Model-Notes/HPEAruba.md +31 -0
- data/docs/Model-Notes/OS6.md +10 -0
- data/docs/Model-Notes/RouterOS.md +15 -0
- data/docs/Model-Notes/SikluMHTG.md +7 -0
- data/docs/ModelUnitTests.md +186 -0
- data/docs/Outputs.md +2 -0
- data/docs/Release.md +18 -15
- data/docs/Sources.md +21 -0
- data/docs/Supported-OS-Types.md +14 -7
- data/docs/Troubleshooting.md +35 -0
- data/examples/podman-compose/Makefile +59 -17
- data/examples/podman-compose/README.md +63 -27
- data/examples/podman-compose/docker-compose.yml +11 -2
- data/examples/podman-compose/gitserver/.gitignore +1 -0
- data/examples/podman-compose/gitserver/Dockerfile +14 -0
- data/examples/podman-compose/model-simulation/Dockerfile-model +1 -1
- data/examples/podman-compose/model-simulation/asternos.sh +2 -0
- data/examples/podman-compose/oxidized-config/.gitignore +2 -0
- data/examples/podman-compose/oxidized-config/config +1 -1
- data/examples/podman-compose/oxidized-config/config_csv-file +46 -0
- data/examples/podman-compose/oxidized-config/config_csv-gitserver +56 -0
- data/examples/podman-compose/oxidized-ssh/.gitignore +1 -0
- data/extra/device2yaml.rb +245 -0
- data/extra/gitdiff-msteams.sh +32 -5
- data/extra/nagios_check_failing_nodes.rb +1 -1
- data/extra/rest_client.rb +1 -1
- data/lib/oxidized/config.rb +8 -2
- data/lib/oxidized/hook/githubrepo.rb +37 -7
- data/lib/oxidized/hook/slackdiff.rb +29 -7
- data/lib/oxidized/input/http.rb +1 -0
- data/lib/oxidized/input/ssh.rb +13 -5
- data/lib/oxidized/input/telnet.rb +1 -1
- data/lib/oxidized/manager.rb +17 -16
- data/lib/oxidized/model/aos7.rb +2 -0
- data/lib/oxidized/model/aoscx.rb +16 -2
- data/lib/oxidized/model/aosw.rb +8 -2
- data/lib/oxidized/model/apc_aos.rb +1 -1
- data/lib/oxidized/model/arubainstant.rb +90 -0
- data/lib/oxidized/model/asa.rb +2 -1
- data/lib/oxidized/model/asyncos.rb +1 -1
- data/lib/oxidized/model/audiocodes.rb +2 -2
- data/lib/oxidized/model/cnos.rb +13 -10
- data/lib/oxidized/model/cumulus.rb +19 -2
- data/lib/oxidized/model/dlink.rb +1 -0
- data/lib/oxidized/model/dlinknextgen.rb +3 -0
- data/lib/oxidized/model/edgecos.rb +2 -1
- data/lib/oxidized/model/enterprise_sonic.rb +46 -0
- data/lib/oxidized/model/eos.rb +2 -0
- data/lib/oxidized/model/f5os.rb +17 -0
- data/lib/oxidized/model/firewareos.rb +10 -1
- data/lib/oxidized/model/fortios.rb +24 -1
- data/lib/oxidized/model/fsos.rb +5 -1
- data/lib/oxidized/model/garderos.rb +43 -0
- data/lib/oxidized/model/h3c.rb +1 -1
- data/lib/oxidized/model/ibos.rb +1 -0
- data/lib/oxidized/model/ios.rb +20 -12
- data/lib/oxidized/model/iosxr.rb +1 -1
- data/lib/oxidized/model/junos.rb +1 -1
- data/lib/oxidized/model/kornfeldos.rb +33 -0
- data/lib/oxidized/model/lenovonos.rb +2 -0
- data/lib/oxidized/model/linuxgeneric.rb +1 -1
- data/lib/oxidized/model/model.rb +2 -2
- data/lib/oxidized/model/netgear.rb +1 -1
- data/lib/oxidized/model/nodegrid.rb +1 -1
- data/lib/oxidized/model/nsxdfw.rb +30 -0
- data/lib/oxidized/model/nxos.rb +2 -1
- data/lib/oxidized/model/os6.rb +48 -0
- data/lib/oxidized/model/rgos.rb +1 -1
- data/lib/oxidized/model/riverbed.rb +104 -0
- data/lib/oxidized/model/routeros.rb +2 -2
- data/lib/oxidized/model/saos.rb +18 -1
- data/lib/oxidized/model/siklumhtg.rb +22 -0
- data/lib/oxidized/model/sonicos.rb +8 -2
- data/lib/oxidized/model/tplink.rb +1 -0
- data/lib/oxidized/model/uplinkolt.rb +46 -0
- data/lib/oxidized/model/vyatta.rb +2 -2
- data/lib/oxidized/model/xos.rb +7 -0
- data/lib/oxidized/node.rb +30 -18
- data/lib/oxidized/nodes.rb +13 -5
- data/lib/oxidized/output/file.rb +45 -42
- data/lib/oxidized/output/git.rb +185 -160
- data/lib/oxidized/output/gitcrypt.rb +188 -186
- data/lib/oxidized/output/http.rb +53 -51
- data/lib/oxidized/output/output.rb +6 -4
- data/lib/oxidized/source/csv.rb +44 -49
- data/lib/oxidized/source/http.rb +63 -81
- data/lib/oxidized/source/jsonfile.rb +63 -0
- data/lib/oxidized/source/source.rb +73 -18
- data/lib/oxidized/source/sql.rb +66 -59
- data/lib/oxidized/version.rb +2 -2
- data/oxidized.gemspec +25 -18
- 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
|
14
|
-
|
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 "
|
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 "
|
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
|
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
|
-
|
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
|
49
|
-
|
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
|
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
|
55
|
-
make
|
56
|
-
|
57
|
-
make stop-local - Stops the
|
58
|
-
|
59
|
-
make
|
60
|
-
|
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
|
2
|
-
This
|
3
|
-
podman
|
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
|
-
|
6
|
-
|
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
|
10
|
-
|
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
|
-
|
13
|
-
|
14
|
-
|
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
|
18
|
-
Bookworm (Version 12)
|
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
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
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
|
32
|
-
|
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
|
-
##
|
47
|
-
Feel free
|
48
|
-
|
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
|
51
|
-
When developing oxidized
|
52
|
-
|
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
|
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.
|
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
|
@@ -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
|