expectacle 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.rubocop.yml +9 -0
- data/.rubocop_todo.yml +27 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +21 -0
- data/README.md +158 -0
- data/Rakefile +20 -0
- data/bin/setup +8 -0
- data/exe/readne +3 -0
- data/exe/run_command +43 -0
- data/expectacle.gemspec +25 -0
- data/lib/expectacle/thrower.rb +129 -0
- data/lib/expectacle/thrower_base.rb +165 -0
- data/lib/expectacle/version.rb +3 -0
- data/lib/expectacle.rb +7 -0
- data/vendor/commands/cisco_save_config_tftp.yml +2 -0
- data/vendor/commands/cisco_show_arp.yml +4 -0
- data/vendor/commands/pica8_ovs_info.yml +3 -0
- data/vendor/commands/ssg_get_arp.yml +4 -0
- data/vendor/commands/ssg_save_config_tftp.yml +2 -0
- data/vendor/hosts/fw.yml +15 -0
- data/vendor/hosts/l2switch.yml +16 -0
- data/vendor/hosts/ofswitch.yml +14 -0
- data/vendor/prompts/c3750g_prompt.yml +9 -0
- data/vendor/prompts/pica8_prompt.yml +9 -0
- data/vendor/prompts/ssg_prompt.yml +9 -0
- metadata +87 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 0d1decbec1db35ee28a8b21210f8f61c42cff269
|
4
|
+
data.tar.gz: 33563494544b430040b0c1488280f48474a48062
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: fc8348cd4cf0fecc78dc5b72ad45169d144d01d1a5c467236cf661e3492c0a2782579b4e97be562ea4fe4a8db7dabfef314c47d65a14556f5635f1c686ddd4b7
|
7
|
+
data.tar.gz: e870762f009586844c50b4ea06e5a7bed8179a94d06800542a602599d90b80bf6e1a4e9f3ad4966797f61b241459dbfc29ac58941f8aa52cffbe1e2a327aeb8d
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2016-10-26 12:34:05 +0900 using RuboCop version 0.44.1.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 1
|
10
|
+
Metrics/AbcSize:
|
11
|
+
Max: 18
|
12
|
+
|
13
|
+
# Offense count: 2
|
14
|
+
# Configuration parameters: CountComments.
|
15
|
+
Metrics/ClassLength:
|
16
|
+
Max: 140
|
17
|
+
|
18
|
+
# Offense count: 1
|
19
|
+
# Configuration parameters: CountComments.
|
20
|
+
Metrics/MethodLength:
|
21
|
+
Max: 12
|
22
|
+
|
23
|
+
# Offense count: 1
|
24
|
+
# Configuration parameters: AllowedVariables.
|
25
|
+
Style/GlobalVars:
|
26
|
+
Exclude:
|
27
|
+
- 'lib/expectacle/thrower_base.rb'
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 TODO: Write your name
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
# Expectacle
|
2
|
+
|
3
|
+
Expectacle ("expect + spectacle") is a small wrapper of `pty`/`expect`.
|
4
|
+
It can send commands (command-list) to hosts (including network devices etc)
|
5
|
+
using telnet/ssh session.
|
6
|
+
|
7
|
+
Expectacle is portable (instead of less feature).
|
8
|
+
Because it depends on only standard modules (YAML, ERB, PTY, Expect and Logger).
|
9
|
+
It can work on almost ruby(>2.2) system without installation other gems. (probably...)
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'expectacle'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
$ bundle
|
22
|
+
|
23
|
+
Or install it yourself as:
|
24
|
+
|
25
|
+
$ gem install expectacle
|
26
|
+
|
27
|
+
## Usage
|
28
|
+
|
29
|
+
### Utility Script
|
30
|
+
See `bin/run_command` and `vendor` directory.
|
31
|
+
|
32
|
+
`run_command` can send commands to hosts.
|
33
|
+
|
34
|
+
$ bundle exec ./bin/run_command l2switch.yml cisco_show_arp.yml
|
35
|
+
|
36
|
+
- `l2switch.yml` is host-list file.
|
37
|
+
It is a data definitions for each hosts to send commands.
|
38
|
+
- At username and password (login/enable) parameter,
|
39
|
+
you can write environment variables with ERB manner to avoid write raw login information.
|
40
|
+
- `bin/readne` is a small utility shell-script to set environment variable from CLI.
|
41
|
+
|
42
|
+
```
|
43
|
+
$ export L2SW_USER=`./bin/readne`
|
44
|
+
Input: (type username)
|
45
|
+
$ export L2SW_PASS=`./bin/readne`
|
46
|
+
Input: (type password)
|
47
|
+
```
|
48
|
+
|
49
|
+
- `Expectacle::Thrower` read prompt-file by "type" parameter in host-list file.
|
50
|
+
- In prompt-file, prompt regexps that used for interactive operation to host
|
51
|
+
are defined. (These regexp are common information for some host-groups. (vendor, OS, ...))
|
52
|
+
- Prompt-file is searched by filename: `#{type}_prompt.yml`,
|
53
|
+
`type` parameter defined in host-list file.
|
54
|
+
- `cisco_show_arp.yml` is command-list file.
|
55
|
+
- it is a list of commands.
|
56
|
+
- Each files are written by YAML.
|
57
|
+
|
58
|
+
## Parameter Definitions
|
59
|
+
|
60
|
+
### Expectacle::Thrower
|
61
|
+
|
62
|
+
`Expectacle::Thrower` argument description.
|
63
|
+
- `:timeout` : (Optional) Timeout interval (sec) to connect a host.
|
64
|
+
(default: 60sec)
|
65
|
+
- `:verbose` : (Optional) When `:verbose` is `false`,
|
66
|
+
`Expectacle` does not output spawned process input/output to standard-out(`$stdout`).
|
67
|
+
(default: `true`)
|
68
|
+
- `:base_dir`: (Optional) Base path to search host/prompt/command files.
|
69
|
+
(default: current working directory (`Dir.pwd`))
|
70
|
+
- `#{base_dir}/commands`: command-list file directory.
|
71
|
+
- `#{base_dir}/prompts` : prompt-file directory.
|
72
|
+
- `#{base_dir}/hosts` : host-file directory.
|
73
|
+
- `:logger` : (Optional) IO object to logging `Expectacle` operations.
|
74
|
+
(default: `$stdout`)
|
75
|
+
|
76
|
+
**Notice** : When `Expectacle` success to connect(spawn) host,
|
77
|
+
it will change the user mode to privilege (root/super-user/enable) at first, ASAP.
|
78
|
+
All commands are executed with privilege mode at the host.
|
79
|
+
|
80
|
+
### Host-list parameter
|
81
|
+
Host-list file is a list of host-parameters.
|
82
|
+
- `:hostname`: Indication String of host name.
|
83
|
+
- `:type`: Host type (used to choose prompt-file).
|
84
|
+
- `:ipaddr`: IP(v4) address to connect host.
|
85
|
+
- `:protocol`: Protocol to connect host. (telnet or ssh)
|
86
|
+
- `:username`: Login name.
|
87
|
+
- `:password`: Login password.
|
88
|
+
- `:enable`: Password to be privilege mode.
|
89
|
+
|
90
|
+
It can use ERB to set values from environment variable in `:username`, `:password` and `:enable`.
|
91
|
+
|
92
|
+
You can add other parameter(s) to refer in command-list files.
|
93
|
+
See also: "Command list" section.
|
94
|
+
|
95
|
+
### Prompt parameter
|
96
|
+
Prompt file is a table of prompt regexp of host group(type).
|
97
|
+
- `:password`: Login password prompt
|
98
|
+
- `:username`: Login username prompt
|
99
|
+
- `:sub1`: Sub command prompt
|
100
|
+
- `:sub2`: Sub command prompt
|
101
|
+
- `:yn`: Yes/No prompt
|
102
|
+
- `:command1`: Command prompt (normal mode)
|
103
|
+
- `:command2`: Command prompt (privilege mode)
|
104
|
+
- `enable_password`: Enable password prompt
|
105
|
+
- `enable_command`: command to be privilege mode
|
106
|
+
(Only this parameter is not a "prompt regexp")
|
107
|
+
|
108
|
+
### Command list with ERB
|
109
|
+
Command-list is a simple list of command-string.
|
110
|
+
A command-string can contain host-parameter reference by ERB.
|
111
|
+
|
112
|
+
For example, if you want to save configuration of a Cisco device to tftp server:
|
113
|
+
- Add a parameter to tftp server info (IP address) in host-list file. (`vendor/hosts/l2switch.yml`)
|
114
|
+
```YAML
|
115
|
+
- :hostname : 'l2sw1'
|
116
|
+
:type : 'c3750g'
|
117
|
+
:ipaddr : '192.168.0.1'
|
118
|
+
:protocol : 'ssh'
|
119
|
+
:username : "<%= ENV['L2SW_USER'] %>"
|
120
|
+
:password : "<%= ENV['L2SW_PASS'] %>"
|
121
|
+
:enable : "<%= ENV['L2SW_PASS'] %>"
|
122
|
+
:tftp_server: '192.168.2.16'
|
123
|
+
- :hostname : 'l2sw2'
|
124
|
+
:type : 'c3750g'
|
125
|
+
:ipaddr : '192.168.0.2'
|
126
|
+
:protocol : 'ssh'
|
127
|
+
:username : "<%= ENV['L2SW_USER'] %>"
|
128
|
+
:password : "<%= ENV['L2SW_PASS'] %>"
|
129
|
+
:enable : "<%= ENV['L2SW_PASS'] %>"
|
130
|
+
:tftp_server: '192.168.2.16'
|
131
|
+
```
|
132
|
+
|
133
|
+
- Write command-list file using ERB.
|
134
|
+
When send a command to host, ERB string was evaluated in `Expectacle::Thrower` bindings.
|
135
|
+
Then, it can refer host-parameter as `@host_param` hash. (`vendor/commands/cisco_save_config_tftp.yml`)
|
136
|
+
- When exec below command-list, host configuration will be saved a file as `l2sw1.confg` on tftp server.
|
137
|
+
|
138
|
+
```YAML
|
139
|
+
- "copy run start"
|
140
|
+
- "copy run tftp://<%= @host_param[:tftp_server] %>/<%= @host_param[:hostname] %>.confg"
|
141
|
+
```
|
142
|
+
|
143
|
+
## TODO
|
144
|
+
|
145
|
+
### Sub prompt operation (interactive command)
|
146
|
+
Feature for sub-prompt (interactive command) is not enough.
|
147
|
+
Now, Expectacle sends fixed command for sub-prompt.
|
148
|
+
(These actions were defined for cisco to execute above "copy run" example...)
|
149
|
+
- Yex/No (`:yn`) : always sends "yes"
|
150
|
+
- Sub prompt (`:sub1` and `:sub2`) : Empty string (RETURN)
|
151
|
+
|
152
|
+
## Contributing
|
153
|
+
|
154
|
+
Bug reports and pull requests are welcome on GitHub at <https://github.com/stereocat/expectacle>.
|
155
|
+
|
156
|
+
## License
|
157
|
+
|
158
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
2
|
+
task default: [:spec, :rubocop]
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'rspec/core/rake_task'
|
6
|
+
RSpec::Core::RakeTask.new
|
7
|
+
rescue LoadError
|
8
|
+
task :spec do
|
9
|
+
$stderr.puts 'RSpec is disabled'
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
begin
|
14
|
+
require 'rubocop/rake_task'
|
15
|
+
RuboCop::RakeTask.new
|
16
|
+
rescue LoadError
|
17
|
+
task :rubocop do
|
18
|
+
$stderr.puts 'RuboCop is disabled'
|
19
|
+
end
|
20
|
+
end
|
data/bin/setup
ADDED
data/exe/readne
ADDED
data/exe/run_command
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'expectacle'
|
5
|
+
require 'optparse'
|
6
|
+
require 'yaml'
|
7
|
+
|
8
|
+
option = {}
|
9
|
+
opt = OptionParser.new
|
10
|
+
opt.on('-h', '--hosts=FILE', 'Host list file') do |value|
|
11
|
+
option[:hosts] = value
|
12
|
+
end
|
13
|
+
opt.on('-c', '--commands=FILE', 'Command list file') do |value|
|
14
|
+
option[:commands] = value
|
15
|
+
end
|
16
|
+
opt.on('-b', '--base-dir=DIR', 'Base directory path') do |value|
|
17
|
+
option[:base_dir] = value
|
18
|
+
end
|
19
|
+
opt.on('-p', '--preview', 'Preview parameter') do |value|
|
20
|
+
option[:preview] = value
|
21
|
+
end
|
22
|
+
opt.on('-r', '--run', 'Run(exec) commands to each hosts') do |value|
|
23
|
+
option[:run] = value
|
24
|
+
end
|
25
|
+
opt.parse!(ARGV)
|
26
|
+
|
27
|
+
file_dir = File.dirname(File.expand_path(__FILE__))
|
28
|
+
base_dir = if option.key?(:base_dir)
|
29
|
+
option[:base_dir]
|
30
|
+
else
|
31
|
+
File.expand_path('../vendor', file_dir)
|
32
|
+
end
|
33
|
+
thrower = Expectacle::Thrower.new(base_dir: base_dir)
|
34
|
+
|
35
|
+
hosts = YAML.load_file(File.join(thrower.hosts_dir, option[:hosts]))
|
36
|
+
commands = YAML.load_file(File.join(thrower.commands_dir, option[:commands]))
|
37
|
+
if option[:preview]
|
38
|
+
thrower.preview_parameter(hosts, commands)
|
39
|
+
elsif option[:run]
|
40
|
+
thrower.run_command_for_all_hosts(hosts, commands)
|
41
|
+
else
|
42
|
+
STDERR.puts "#{$PROGRAM_NAME}: Action(preview/run) did not specified."
|
43
|
+
end
|
data/expectacle.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'expectacle/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'expectacle'
|
8
|
+
spec.version = Expectacle::VERSION
|
9
|
+
spec.authors = ['stereocat']
|
10
|
+
spec.email = ['stereocat@gmail.com']
|
11
|
+
|
12
|
+
spec.summary = 'Simple expect wrapper to send commands to a devices.'
|
13
|
+
spec.description = 'Expectacle is simple wrapper of pty/expect.'
|
14
|
+
spec.homepage = 'https://github.com/stereocat/expectacle'
|
15
|
+
spec.license = 'MIT'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
18
|
+
f.match(%r{^(test|spec|features)/})
|
19
|
+
end
|
20
|
+
spec.bindir = 'exe'
|
21
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
22
|
+
spec.require_paths = ['lib']
|
23
|
+
|
24
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
25
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'expectacle/thrower_base'
|
4
|
+
|
5
|
+
module Expectacle
|
6
|
+
# Thrower logic(command list operation)
|
7
|
+
class Thrower < ThrowerBase
|
8
|
+
def run_command_for_all_hosts(hosts, commands)
|
9
|
+
@commands = commands
|
10
|
+
hosts.each do |each|
|
11
|
+
@host_param = each
|
12
|
+
run_command_for_host
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def preview_parameter(hosts, commands)
|
17
|
+
@commands = commands
|
18
|
+
hosts.each do |each|
|
19
|
+
@host_param = each
|
20
|
+
preview_command_for_host
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def run_command_for_host
|
27
|
+
ready_to_open_host_session do |spawn_cmd|
|
28
|
+
open_interactive_process(spawn_cmd) do
|
29
|
+
run_command
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def preview_command_for_host
|
35
|
+
ready_to_open_host_session do |spawn_cmd|
|
36
|
+
preview_command spawn_cmd
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def run_command
|
41
|
+
do_on_interactive_process do |match|
|
42
|
+
@logger.debug "Read: #{match}"
|
43
|
+
exec_each_prompt match[1]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def preview_host_param
|
48
|
+
host_param = @host_param.dup
|
49
|
+
enable_mode = @enable_mode
|
50
|
+
@enable_mode = false
|
51
|
+
host_param[:username] = embed_user_name(binding)
|
52
|
+
host_param[:password] = embed_password(binding)
|
53
|
+
@enable_mode = true
|
54
|
+
host_param[:enable] = embed_password(binding)
|
55
|
+
@enable_mode = enable_mode
|
56
|
+
host_param
|
57
|
+
end
|
58
|
+
|
59
|
+
def preview_commands
|
60
|
+
@commands.map { |cmd| embed_command(cmd, binding) }
|
61
|
+
end
|
62
|
+
|
63
|
+
def preview_command(spawn_cmd)
|
64
|
+
data = {}
|
65
|
+
data[:spawn_cmd] = spawn_cmd
|
66
|
+
data[:prompt] = @prompt
|
67
|
+
data[:host] = preview_host_param
|
68
|
+
data[:commands] = preview_commands
|
69
|
+
print YAML.dump(data)
|
70
|
+
end
|
71
|
+
|
72
|
+
def exec_rest_commands
|
73
|
+
if !@commands.empty?
|
74
|
+
yield
|
75
|
+
else
|
76
|
+
write_and_logging 'Send break: ', 'exit'
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def exec_in_privilege_mode
|
81
|
+
exec_rest_commands do
|
82
|
+
# Notice: @commands changed
|
83
|
+
command = @commands.shift
|
84
|
+
write_and_logging 'Send command: ', embed_command(command, binding)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def exec_in_normal_mode
|
89
|
+
exec_rest_commands do
|
90
|
+
write_and_logging 'Send enable command: ', @prompt[:enable_command]
|
91
|
+
@enable_mode = true
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def exec_by_mode(prompt)
|
96
|
+
case prompt
|
97
|
+
when /#{@prompt[:command2]}/
|
98
|
+
exec_in_privilege_mode
|
99
|
+
when /#{@prompt[:command1]}/
|
100
|
+
exec_in_normal_mode
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def exec_by_sub_prompt(prompt)
|
105
|
+
case prompt
|
106
|
+
when /#{@prompt[:yn]}/
|
107
|
+
# it must match before sub_prompt
|
108
|
+
write_and_logging 'Send yes: ', 'yes'
|
109
|
+
when /#{@prompt[:sub1]}/, /#{@prompt[:sub2]}/
|
110
|
+
write_and_logging 'Send return: ', ''
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
def exec_each_prompt(prompt)
|
115
|
+
case prompt
|
116
|
+
when /#{@prompt[:password]}/, /#{@prompt[:enable_password]}/
|
117
|
+
write_and_logging 'Send password', embed_password(binding), true
|
118
|
+
when /#{@prompt[:username]}/
|
119
|
+
write_and_logging 'Send username: ', embed_user_name(binding)
|
120
|
+
when /#{@prompt[:command2]}/, /#{@prompt[:command1]}/
|
121
|
+
exec_by_mode(prompt)
|
122
|
+
when /#{@prompt[:yn]}/, /#{@prompt[:sub1]}/, /#{@prompt[:sub2]}/
|
123
|
+
exec_by_sub_prompt(prompt)
|
124
|
+
else
|
125
|
+
@logger.error "Unknown prompt #{prompt}"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,165 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pty'
|
4
|
+
require 'expect'
|
5
|
+
require 'yaml'
|
6
|
+
require 'erb'
|
7
|
+
require 'logger'
|
8
|
+
|
9
|
+
module Expectacle
|
10
|
+
# Basic state setup/management
|
11
|
+
class ThrowerBase
|
12
|
+
attr_accessor :logger
|
13
|
+
attr_reader :base_dir
|
14
|
+
|
15
|
+
def initialize(timeout: 60, verbose: true,
|
16
|
+
base_dir: Dir.pwd, logger: $stdout)
|
17
|
+
# remote connection timeout (sec)
|
18
|
+
@timeout = timeout
|
19
|
+
# cli mode flag
|
20
|
+
@enable_mode = false
|
21
|
+
# debug (use debug print to stdout)
|
22
|
+
$expect_verbose = verbose
|
23
|
+
# base dir
|
24
|
+
@base_dir = File.expand_path(base_dir)
|
25
|
+
# logger
|
26
|
+
setup_default_logger(logger)
|
27
|
+
end
|
28
|
+
|
29
|
+
def prompts_dir
|
30
|
+
File.join @base_dir, 'prompts'
|
31
|
+
end
|
32
|
+
|
33
|
+
def hosts_dir
|
34
|
+
File.join @base_dir, 'hosts'
|
35
|
+
end
|
36
|
+
|
37
|
+
def commands_dir
|
38
|
+
File.join @base_dir, 'commands'
|
39
|
+
end
|
40
|
+
|
41
|
+
private
|
42
|
+
|
43
|
+
def ready_to_open_host_session
|
44
|
+
# prompt regexp of device
|
45
|
+
load_prompt_file
|
46
|
+
spawn_cmd = make_spawn_command
|
47
|
+
if @prompt && spawn_cmd
|
48
|
+
yield spawn_cmd
|
49
|
+
else
|
50
|
+
@logger.error 'Invalid parameter in param file(S)'
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def do_on_interactive_process
|
55
|
+
until @reader.eof?
|
56
|
+
@reader.expect(expect_regexp, @timeout) do |match|
|
57
|
+
yield match
|
58
|
+
end
|
59
|
+
end
|
60
|
+
rescue Errno::EIO => error
|
61
|
+
# on linux, PTY raises Errno::EIO when spawned process closed.
|
62
|
+
@logger.debug "PTY raises Errno::EIO, #{error.message}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def open_interactive_process(spawn_cmd)
|
66
|
+
@logger.info "Begin spawn: #{spawn_cmd}"
|
67
|
+
PTY.spawn(spawn_cmd) do |reader, writer, _pid|
|
68
|
+
@enable_mode = false
|
69
|
+
@reader = reader
|
70
|
+
@writer = writer
|
71
|
+
@writer.sync = true
|
72
|
+
yield
|
73
|
+
end
|
74
|
+
@logger.info "End spawn: #{@host_param[:hostname]}"
|
75
|
+
end
|
76
|
+
|
77
|
+
def ssh_command
|
78
|
+
['ssh',
|
79
|
+
'-o StrictHostKeyChecking=no',
|
80
|
+
'-o KexAlgorithms=+diffie-hellman-group1-sha1', # for old cisco device
|
81
|
+
"-l #{embed_user_name(binding)}",
|
82
|
+
@host_param[:ipaddr]].join(' ')
|
83
|
+
end
|
84
|
+
|
85
|
+
def make_spawn_command
|
86
|
+
case @host_param[:protocol]
|
87
|
+
when /^telnet$/i
|
88
|
+
['telnet', @host_param[:ipaddr]].join(' ')
|
89
|
+
when /^ssh$/i
|
90
|
+
ssh_command
|
91
|
+
else
|
92
|
+
@logger.error "Unknown protocol #{@host_param[:protocol]}"
|
93
|
+
nil
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def load_yaml_file(file_type, file_name)
|
98
|
+
YAML.load_file file_name
|
99
|
+
rescue StandardError => error
|
100
|
+
@logger.error "Cannot load #{file_type}: #{file_name}"
|
101
|
+
raise error
|
102
|
+
end
|
103
|
+
|
104
|
+
def load_prompt_file
|
105
|
+
prompt_file = "#{prompts_dir}/#{@host_param[:type]}_prompt.yml"
|
106
|
+
@prompt = load_yaml_file('prompt file', prompt_file)
|
107
|
+
end
|
108
|
+
|
109
|
+
def setup_default_logger(logger_io)
|
110
|
+
@logger = Logger.new(logger_io)
|
111
|
+
@logger.level = Logger::INFO
|
112
|
+
@logger.progname = 'Expectacle'
|
113
|
+
@logger.datetime_format = '%Y-%m-%d %H:%M:%D %Z'
|
114
|
+
@logger.formatter = proc do |severity, datetime, progname, msg|
|
115
|
+
"#{datetime} #{progname} [#{severity}] #{msg}\n"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def expect_regexp
|
120
|
+
/
|
121
|
+
( #{@prompt[:password]} | #{@prompt[:enable_password]}
|
122
|
+
| #{@prompt[:username]}
|
123
|
+
| #{@prompt[:command1]} | #{@prompt[:command2]}
|
124
|
+
| #{@prompt[:sub1]} | #{@prompt[:sub2]}
|
125
|
+
| #{@prompt[:yn]}
|
126
|
+
)\s*$
|
127
|
+
/x
|
128
|
+
end
|
129
|
+
|
130
|
+
def write_and_logging(message, command, secret = false)
|
131
|
+
logging_message = secret ? message : message + command
|
132
|
+
@logger.info logging_message
|
133
|
+
@writer.puts command
|
134
|
+
end
|
135
|
+
|
136
|
+
def check_embed_envvar(command)
|
137
|
+
return unless command =~ /<%=\s*ENV\[[\'\"]?(.+)[\'\"]\]?\s*%>/
|
138
|
+
envvar_name = Regexp.last_match(1)
|
139
|
+
if !ENV.key?(envvar_name)
|
140
|
+
@logger.error "Variable name: #{envvar_name} is not found in ENV"
|
141
|
+
elsif ENV[envvar_name] =~ /^\s*$/
|
142
|
+
@logger.warn "Env var: #{envvar_name} exists, but null string"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def embed_password(binding)
|
147
|
+
@host_param[:enable] = '_NOT_DEFINED_' unless @host_param.key?(:enable)
|
148
|
+
base_str = @enable_mode ? @host_param[:enable] : @host_param[:password]
|
149
|
+
check_embed_envvar(base_str)
|
150
|
+
passwd_erb = ERB.new(base_str)
|
151
|
+
passwd_erb.result(binding)
|
152
|
+
end
|
153
|
+
|
154
|
+
def embed_command(command, binding)
|
155
|
+
command_erb = ERB.new(command)
|
156
|
+
command_erb.result(binding)
|
157
|
+
end
|
158
|
+
|
159
|
+
def embed_user_name(binding)
|
160
|
+
check_embed_envvar(@host_param[:username])
|
161
|
+
uname_erb = ERB.new(@host_param[:username])
|
162
|
+
uname_erb.result(binding)
|
163
|
+
end
|
164
|
+
end
|
165
|
+
end
|
data/lib/expectacle.rb
ADDED
data/vendor/hosts/fw.yml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
- :hostname : 'fw1'
|
2
|
+
:type : 'ssg'
|
3
|
+
:ipaddr : '192.168.20.161'
|
4
|
+
:protocol : 'telnet'
|
5
|
+
:username : "<%= ENV['FW_USER'] %>"
|
6
|
+
:password : "<%= ENV['FW_PASS'] %>"
|
7
|
+
:tftp_server : '192.168.20.170'
|
8
|
+
- :hostname : 'fw2'
|
9
|
+
:type : 'ssg'
|
10
|
+
:ipaddr : '192.168.20.162'
|
11
|
+
:protocol : 'telnet'
|
12
|
+
:username : "<%= ENV['FW_USER'] %>"
|
13
|
+
:password : "<%= ENV['FW_PASS'] %>"
|
14
|
+
:tftp_server : '192.168.20.170'
|
15
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
- :hostname : 'l2sw1'
|
2
|
+
:type : 'c3750g'
|
3
|
+
:ipaddr : '192.168.20.150'
|
4
|
+
:protocol : 'ssh'
|
5
|
+
:username : "<%= ENV['L2SW_USER'] %>"
|
6
|
+
:password : "<%= ENV['L2SW_PASS'] %>"
|
7
|
+
:enable : "<%= ENV['L2SW_PASS'] %>"
|
8
|
+
:tftp_server: '192.168.20.170'
|
9
|
+
- :hostname : 'l2sw2'
|
10
|
+
:type : 'c3750g'
|
11
|
+
:ipaddr : '192.168.20.151'
|
12
|
+
:protocol : 'ssh'
|
13
|
+
:username : "<%= ENV['L2SW_USER'] %>"
|
14
|
+
:password : "<%= ENV['L2SW_PASS'] %>"
|
15
|
+
:enable : "<%= ENV['L2SW_PASS'] %>"
|
16
|
+
:tftp_server: '192.168.20.170'
|
@@ -0,0 +1,14 @@
|
|
1
|
+
- :hostname : 'ofs1'
|
2
|
+
:type : 'pica8'
|
3
|
+
:ipaddr : '192.168.20.155'
|
4
|
+
:protocol : 'ssh'
|
5
|
+
:username : "<%= ENV['OFS1_USER'] %>"
|
6
|
+
:password : "<%= ENV['OFS_PASS'] %>"
|
7
|
+
:enable : "<%= ENV['OFS_PASS'] %>"
|
8
|
+
- :hostname : 'ofs2'
|
9
|
+
:type : 'pica8'
|
10
|
+
:ipaddr : '192.168.20.156'
|
11
|
+
:protocol : 'ssh'
|
12
|
+
:username : "<%= ENV['OFS2_USER'] %>"
|
13
|
+
:password : "<%= ENV['OFS_PASS'] %>"
|
14
|
+
:enable : "<%= ENV['OFS_PASS'] %>"
|
@@ -0,0 +1,9 @@
|
|
1
|
+
:password : '^Password\s*:'
|
2
|
+
:username : '^Username\s*:'
|
3
|
+
:sub1 : '\][:\?]'
|
4
|
+
:sub2 : '\[confirm\]'
|
5
|
+
:yn : '\[yes\/no\]:'
|
6
|
+
:command1 : '^[\w\-]+>'
|
7
|
+
:command2 : '^[\w\-]+(:?\(config\))?\#'
|
8
|
+
:enable_password : 'SAME_AS_LOGIN_PASSWORD'
|
9
|
+
:enable_command : 'enable'
|
@@ -0,0 +1,9 @@
|
|
1
|
+
:password : '^.+password\s*:'
|
2
|
+
:username : 'DO_NOT_USE_FOR_PICA8'
|
3
|
+
:sub1 : 'DO_NOT_USE_FOR_PICA8'
|
4
|
+
:sub2 : 'DO_NOT_USE_FOR_PICA8'
|
5
|
+
:yn : 'DO_NOT_USE_FOR_PICA8'
|
6
|
+
:command1 : '^[\w]+@[\w\-]+\$' # username@host-name$
|
7
|
+
:command2 : '^(:?root|admin)@[\w\-]+\$'
|
8
|
+
:enable_password : '^Password:\s*'
|
9
|
+
:enable_command : 'su'
|
@@ -0,0 +1,9 @@
|
|
1
|
+
:password : '^password:\s*'
|
2
|
+
:username : '^login:\s*'
|
3
|
+
:sub1 : 'DO_NOT_USE_FOR_SSG'
|
4
|
+
:sub2 : 'DO_NOT_USE_FOR_SSG'
|
5
|
+
:yn : '\[yes\/no\]:'
|
6
|
+
:command1 : 'DO_NOT_USE_FOR_SSG'
|
7
|
+
:command2 : '^[\w\-]+:[\w\-]+\([A-Z]+\)'
|
8
|
+
:enable_password : 'DO_NOT_USE_FOR_SSG'
|
9
|
+
:enable_command: '' # none
|
metadata
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: expectacle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- stereocat
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-10-28 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '10.0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '10.0'
|
27
|
+
description: Expectacle is simple wrapper of pty/expect.
|
28
|
+
email:
|
29
|
+
- stereocat@gmail.com
|
30
|
+
executables:
|
31
|
+
- readne
|
32
|
+
- run_command
|
33
|
+
extensions: []
|
34
|
+
extra_rdoc_files: []
|
35
|
+
files:
|
36
|
+
- ".gitignore"
|
37
|
+
- ".rspec"
|
38
|
+
- ".rubocop.yml"
|
39
|
+
- ".rubocop_todo.yml"
|
40
|
+
- Gemfile
|
41
|
+
- LICENSE.txt
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- bin/setup
|
45
|
+
- exe/readne
|
46
|
+
- exe/run_command
|
47
|
+
- expectacle.gemspec
|
48
|
+
- lib/expectacle.rb
|
49
|
+
- lib/expectacle/thrower.rb
|
50
|
+
- lib/expectacle/thrower_base.rb
|
51
|
+
- lib/expectacle/version.rb
|
52
|
+
- vendor/commands/cisco_save_config_tftp.yml
|
53
|
+
- vendor/commands/cisco_show_arp.yml
|
54
|
+
- vendor/commands/pica8_ovs_info.yml
|
55
|
+
- vendor/commands/ssg_get_arp.yml
|
56
|
+
- vendor/commands/ssg_save_config_tftp.yml
|
57
|
+
- vendor/hosts/fw.yml
|
58
|
+
- vendor/hosts/l2switch.yml
|
59
|
+
- vendor/hosts/ofswitch.yml
|
60
|
+
- vendor/prompts/c3750g_prompt.yml
|
61
|
+
- vendor/prompts/pica8_prompt.yml
|
62
|
+
- vendor/prompts/ssg_prompt.yml
|
63
|
+
homepage: https://github.com/stereocat/expectacle
|
64
|
+
licenses:
|
65
|
+
- MIT
|
66
|
+
metadata: {}
|
67
|
+
post_install_message:
|
68
|
+
rdoc_options: []
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
77
|
+
requirements:
|
78
|
+
- - ">="
|
79
|
+
- !ruby/object:Gem::Version
|
80
|
+
version: '0'
|
81
|
+
requirements: []
|
82
|
+
rubyforge_project:
|
83
|
+
rubygems_version: 2.5.1
|
84
|
+
signing_key:
|
85
|
+
specification_version: 4
|
86
|
+
summary: Simple expect wrapper to send commands to a devices.
|
87
|
+
test_files: []
|