runssh 0.1.0
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.
- data/README.rdoc +45 -0
- data/Rakefile +64 -0
- data/bin/runssh +31 -0
- data/bin/runssh_comp.sh +24 -0
- data/gpl-2.0.txt +339 -0
- data/lib/runsshlib/cli.rb +287 -0
- data/lib/runsshlib/config_file.rb +167 -0
- data/lib/runsshlib/ssh_backend.rb +36 -0
- data/lib/runsshlib.rb +44 -0
- data/spec/fixtures/runssh.yml +19 -0
- data/spec/runsshlib/cli_spec.rb +334 -0
- data/spec/runsshlib/config_file_spec.rb +275 -0
- data/spec/runsshlib/ssh_backend_spec.rb +68 -0
- data/spec/spec_helper.rb +85 -0
- metadata +133 -0
@@ -0,0 +1,287 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2010 Haim Ashkenazi
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
#
|
18
|
+
require 'trollop'
|
19
|
+
|
20
|
+
module RunSSHLib
|
21
|
+
class CLI
|
22
|
+
|
23
|
+
COMMAND = %w(shell add del update print import export)
|
24
|
+
|
25
|
+
# Initialize new CLI instance and parse the supplied
|
26
|
+
# arguments.
|
27
|
+
def initialize(args)
|
28
|
+
args.unshift '-h' if args.empty?
|
29
|
+
args.unshift '-h' if args == ['help']
|
30
|
+
@global_options = parse_args(args)
|
31
|
+
|
32
|
+
# workaround to enable 'help COMMAND' functionality.
|
33
|
+
if args.first == 'help'; args.shift; args << '-h'; end
|
34
|
+
# indicate path completion request
|
35
|
+
@completion_requested = args.delete('?')
|
36
|
+
|
37
|
+
@cmd = extract_subcommand(args)
|
38
|
+
@options = parse_subcommand(@cmd, args)
|
39
|
+
@c = init_config
|
40
|
+
# path in ConfigFile uses symbols and not strings
|
41
|
+
@path = args.map { |e| e.to_sym }
|
42
|
+
rescue ConfigError, InvalidSubCommandError, Errno::ENOENT => e
|
43
|
+
Trollop.die e.message
|
44
|
+
end
|
45
|
+
|
46
|
+
# run
|
47
|
+
def run
|
48
|
+
# did the user request completions? if not run the approproate command.
|
49
|
+
if @completion_requested
|
50
|
+
puts @c.list_groups(@path)
|
51
|
+
else
|
52
|
+
command_name = 'run_' + @cmd
|
53
|
+
m = method(command_name.to_sym)
|
54
|
+
m.call(@path)
|
55
|
+
end
|
56
|
+
rescue ConfigError => e
|
57
|
+
Trollop.die e.message
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# Parses main arguments. Returns the output of Trollop::options
|
63
|
+
def parse_args(args)
|
64
|
+
Trollop::options(args) do
|
65
|
+
# TODO: This should be generated automatically somehow!!
|
66
|
+
banner <<-EOS
|
67
|
+
Usage: runssh [global_options] COMMAND [options] <path>
|
68
|
+
|
69
|
+
A utility to bookmark multiple ssh connections in heirarchial order.
|
70
|
+
|
71
|
+
COMMAND : One of the commands mentioned below. It's possible to
|
72
|
+
type only part of the command as long as it's not ambiguous.
|
73
|
+
<path> : A space separated list of names (e.g, one two three)
|
74
|
+
For available completions append " ?" to the end of path.
|
75
|
+
|
76
|
+
Available commands:
|
77
|
+
* shell : Open ssh shell on remote host
|
78
|
+
* add : Add host definition
|
79
|
+
* del : Delete host definition
|
80
|
+
* update : Update host definition
|
81
|
+
* print : Print host definition
|
82
|
+
* import : Import configuration
|
83
|
+
* export : Export configuration
|
84
|
+
|
85
|
+
For help on commands run:
|
86
|
+
runssh help COMMAND
|
87
|
+
|
88
|
+
Global options:
|
89
|
+
EOS
|
90
|
+
opt :config_file, "alternate config file",
|
91
|
+
:type => :string, :short => :f
|
92
|
+
version "RunSSH version #{Version::STRING}"
|
93
|
+
stop_on_unknown
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# Etracts the subcommand from args. Throws InvalidSubCommandError if
|
98
|
+
# invalid or ambigious subcommand
|
99
|
+
def extract_subcommand(args)
|
100
|
+
cmd = args.shift
|
101
|
+
if COMMAND.include? cmd
|
102
|
+
cmd
|
103
|
+
else
|
104
|
+
cmdopts = COMMAND.select { |item| item =~ /^#{cmd}/ }
|
105
|
+
raise InvalidSubCommandError, 'invalid command' unless
|
106
|
+
cmdopts.length == 1
|
107
|
+
cmdopts.first
|
108
|
+
end
|
109
|
+
rescue RegexpError
|
110
|
+
raise InvalidSubCommandError, 'invalid command'
|
111
|
+
end
|
112
|
+
|
113
|
+
# handles argument parsing for all subcomand. It doesn't contain
|
114
|
+
# any logic, nor does it handle errors. It just parses the
|
115
|
+
# arguments and put the result into @options.
|
116
|
+
def parse_subcommand(cmd, args)
|
117
|
+
case cmd
|
118
|
+
when 'shell'
|
119
|
+
Trollop::options(args) do
|
120
|
+
banner <<-EOS
|
121
|
+
Usage: runssh [global_options] shell [options] <path>
|
122
|
+
|
123
|
+
Connect to the specified host using ssh.
|
124
|
+
|
125
|
+
<path> : See main help for description of path.
|
126
|
+
|
127
|
+
Options:
|
128
|
+
EOS
|
129
|
+
opt :login, "override the login in the configuration",
|
130
|
+
:type => :string
|
131
|
+
end
|
132
|
+
when 'add'
|
133
|
+
Trollop::options(args) do
|
134
|
+
banner <<-EOS
|
135
|
+
Usage: runssh [global_options] add [options] <path>
|
136
|
+
|
137
|
+
Add a new host definition at the supplied <path>. <path> must not exit!
|
138
|
+
|
139
|
+
<path> : See main help for description of path.
|
140
|
+
|
141
|
+
Options:
|
142
|
+
EOS
|
143
|
+
opt :host_name, 'The name or address of the host (e.g, host.example.com)',
|
144
|
+
:short => :n, :type => :string, :required => true
|
145
|
+
opt :user, 'The user to connect as (optional)',
|
146
|
+
:short => :u, :type => :string
|
147
|
+
end
|
148
|
+
when 'update'
|
149
|
+
Trollop::options(args) do
|
150
|
+
banner <<-EOS
|
151
|
+
Usage: runssh [global_options] update [options] <path>
|
152
|
+
|
153
|
+
Update host definition specified by <path> with new settings. The host
|
154
|
+
definition is completely replaced by the new definition (e.g, You can
|
155
|
+
not specify only new host and expect the user to remain the old one).
|
156
|
+
|
157
|
+
<path> : See main help for description of path.
|
158
|
+
|
159
|
+
Options:
|
160
|
+
EOS
|
161
|
+
opt :host_name, 'The name or address of the host (e.g, host.example.com)',
|
162
|
+
:short => :n, :type => :string, :required => true
|
163
|
+
opt :user, 'The user to connect as (optional)',
|
164
|
+
:short => :u, :type => :string
|
165
|
+
end
|
166
|
+
when 'del'
|
167
|
+
Trollop::options(args) do
|
168
|
+
banner <<-EOS
|
169
|
+
Usage: runssh [global_options] del [options] <path>
|
170
|
+
|
171
|
+
Delete host definitions or `empty` groups (e.g, groups that contained
|
172
|
+
only one host definition which was deleted). You'll be prompted for
|
173
|
+
verification.
|
174
|
+
|
175
|
+
<path> : See main help for description of path.
|
176
|
+
|
177
|
+
Options:
|
178
|
+
EOS
|
179
|
+
opt :yes, 'Delete without verification'
|
180
|
+
end
|
181
|
+
when 'print'
|
182
|
+
Trollop::options(args) do
|
183
|
+
banner <<-EOS
|
184
|
+
Usage: runssh [global_options] print [options] <path>
|
185
|
+
|
186
|
+
Print host configuration to the console.
|
187
|
+
|
188
|
+
<path> : See main help for description of path.
|
189
|
+
|
190
|
+
Options:
|
191
|
+
EOS
|
192
|
+
end
|
193
|
+
when 'import'
|
194
|
+
Trollop::options(args) do
|
195
|
+
banner <<-EOS
|
196
|
+
Usage: runssh [global_options] import [options]
|
197
|
+
|
198
|
+
Imports a new configuration.
|
199
|
+
CAREFULL: This completely overrides the current configuration!
|
200
|
+
|
201
|
+
Options:
|
202
|
+
EOS
|
203
|
+
opt :input_file, 'The yaml file to import from',
|
204
|
+
:type => :string, :required => true
|
205
|
+
end
|
206
|
+
when 'export'
|
207
|
+
Trollop::options(args) do
|
208
|
+
banner <<-EOS
|
209
|
+
Usage runssh [global_options] export [options]
|
210
|
+
|
211
|
+
Exports the configuration to a YAML file.
|
212
|
+
|
213
|
+
Options
|
214
|
+
EOS
|
215
|
+
opt :output_file, 'The output file',
|
216
|
+
:type => :string, :required => true
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def init_config
|
222
|
+
config = @global_options[:config_file] ?
|
223
|
+
@global_options[:config_file] : DEFAULT_CONFIG
|
224
|
+
ConfigFile.new(config)
|
225
|
+
end
|
226
|
+
|
227
|
+
def run_shell(path)
|
228
|
+
host = @c.get_host(path)
|
229
|
+
s = SshBackend.new(host, @options)
|
230
|
+
s.shell
|
231
|
+
end
|
232
|
+
|
233
|
+
def run_add(path)
|
234
|
+
# extract the host definition name
|
235
|
+
host = path.pop
|
236
|
+
@c.add_host_def(path, host,
|
237
|
+
HostDef.new(@options[:host_name], @options[:user]))
|
238
|
+
end
|
239
|
+
|
240
|
+
def run_update(path)
|
241
|
+
@c.update_host_def(path,
|
242
|
+
HostDef.new(@options[:host_name], @options[:user]))
|
243
|
+
end
|
244
|
+
|
245
|
+
def run_del(path)
|
246
|
+
question = "Are you sure you want to delete \"" + path.join(':') + "\""
|
247
|
+
if verify_yn(question)
|
248
|
+
@c.delete_path(path)
|
249
|
+
else
|
250
|
+
puts 'canceled'
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
def run_print(path)
|
255
|
+
host = @c.get_host(path)
|
256
|
+
output = "Host definition for: #{path.last}",
|
257
|
+
" * host: #{host.name}",
|
258
|
+
" * user: #{host.login ? host.login : 'current user'}"
|
259
|
+
puts output
|
260
|
+
end
|
261
|
+
|
262
|
+
# we don't use path here, it's just for easier invocation.
|
263
|
+
def run_import(path)
|
264
|
+
question = "Importing a file OVERWRITES existing configuration. " +
|
265
|
+
"Are you sure"
|
266
|
+
if verify_yn(question)
|
267
|
+
@c.import(@options[:input_file])
|
268
|
+
else
|
269
|
+
puts 'canceled'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
# we don't use path here, it's just for easier invocation
|
274
|
+
def run_export(path)
|
275
|
+
@c.export(@options[:output_file])
|
276
|
+
end
|
277
|
+
|
278
|
+
# Verifies a presented question. If response is 'y' it returns
|
279
|
+
# true, else false.
|
280
|
+
#
|
281
|
+
# The supplied question should not include the (y/n)? postfix.
|
282
|
+
def verify_yn question
|
283
|
+
print question, " (y/n)? "
|
284
|
+
gets.chomp == 'y'
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
@@ -0,0 +1,167 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2010 Haim Ashkenazi
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
#
|
18
|
+
|
19
|
+
module RunSSHLib
|
20
|
+
|
21
|
+
# Handles configuration file for the application.
|
22
|
+
#
|
23
|
+
# The configuration consists of nested hashes which keys either
|
24
|
+
# points to another hash or to host definition.
|
25
|
+
#
|
26
|
+
# The configuration file should use Marshal to save/load
|
27
|
+
# configuration, but should also be able to import/export
|
28
|
+
# to/from yaml file.
|
29
|
+
class ConfigFile
|
30
|
+
|
31
|
+
# Initialize new ConfigFile. Uses supplied config_file or the default
|
32
|
+
# '~/.runssh'. If file doesn't exist, it issues a warning and creates
|
33
|
+
# a new empty one.
|
34
|
+
def initialize(config_file)
|
35
|
+
@config_file = config_file
|
36
|
+
if File.exists? config_file
|
37
|
+
File.open(config_file) { |io| @config = Marshal.load(io) }
|
38
|
+
else
|
39
|
+
# warn "Config file not found. It must be the first time you run this app..."
|
40
|
+
@config = Hash.new
|
41
|
+
save
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Add host definition to config file.
|
46
|
+
#
|
47
|
+
# path:: An array of symbols that represent the path
|
48
|
+
# for the host. e.g, [:client, :datacenter1].
|
49
|
+
# name:: The name of the host definition as symbol.
|
50
|
+
# host_def:: A HostDef instance.
|
51
|
+
def add_host_def(path, name, host_def)
|
52
|
+
# sanity
|
53
|
+
raise ConfigError.new('Invalid host definition') unless host_def.instance_of? HostDef
|
54
|
+
|
55
|
+
k = path.inject(@config) do |hsh, key|
|
56
|
+
if hsh.include? key
|
57
|
+
if hsh[key].instance_of? HostDef
|
58
|
+
raise ConfigError.new('Cannot override host definition with path!')
|
59
|
+
end
|
60
|
+
hsh[key]
|
61
|
+
else
|
62
|
+
hsh[key] = {}
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
raise ConfigError.new('path already exist!') if k.include? name
|
67
|
+
|
68
|
+
k[name] = host_def
|
69
|
+
save
|
70
|
+
end
|
71
|
+
|
72
|
+
# Update host definition (host_def) at the specified path.
|
73
|
+
# Raises ConfigError if doesn't already exist!
|
74
|
+
def update_host_def(path, host_def)
|
75
|
+
# sanity
|
76
|
+
raise ConfigError.new('Invalid host definition!') if not
|
77
|
+
host_def.instance_of? HostDef
|
78
|
+
|
79
|
+
# we need to separate the host name from the path
|
80
|
+
# in order to get the key of the host definition.
|
81
|
+
host = path.pop
|
82
|
+
groups = retrieve_path(path, "Invalid path!")
|
83
|
+
raise ConfigError, 'Invalid path!' unless groups
|
84
|
+
if groups.include? host
|
85
|
+
raise ConfigError.new("Cannot overwrite group with host definition") unless
|
86
|
+
groups[host].instance_of? HostDef
|
87
|
+
groups[host] = host_def
|
88
|
+
else
|
89
|
+
raise ConfigError.new("Host definition doesn't exist!")
|
90
|
+
end
|
91
|
+
save
|
92
|
+
end
|
93
|
+
|
94
|
+
# Returns the host definition in the specified path.
|
95
|
+
# path:: is an array of symbols which translates to nested hash keys.
|
96
|
+
# Raises:: ConfigError if not found or if path points to a group.
|
97
|
+
def get_host(path)
|
98
|
+
host = retrieve_path(path,
|
99
|
+
%Q{host definition (#{path.join(' => ')}) doesn't exist!})
|
100
|
+
if not host
|
101
|
+
raise ConfigError.new(%Q{host definition (#{path.join(' => ')}) doesn't exist!})
|
102
|
+
elsif host.instance_of? Hash
|
103
|
+
raise ConfigError.new(%Q("#{path.join(' => ')}" is a group, not host definition!))
|
104
|
+
end
|
105
|
+
|
106
|
+
host
|
107
|
+
end
|
108
|
+
|
109
|
+
# List all available sub groups inside path.
|
110
|
+
def list_groups(path)
|
111
|
+
value = retrieve_path(path, 'Invalid path!')
|
112
|
+
if value.instance_of? Hash
|
113
|
+
value.keys
|
114
|
+
else
|
115
|
+
[]
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# This will delete any path if it's a host definition
|
120
|
+
# or an empty group.
|
121
|
+
def delete_path(path)
|
122
|
+
# we need access to the delete key, not just the value
|
123
|
+
mykey = path.pop
|
124
|
+
value = retrieve_path(path, 'Invalid path!')
|
125
|
+
raise ConfigError.new('Invalid path!') unless value
|
126
|
+
|
127
|
+
if value[mykey].instance_of? HostDef or value[mykey] == {}
|
128
|
+
value.delete(mykey)
|
129
|
+
elsif not value[mykey]
|
130
|
+
raise ConfigError.new('Invalid path!')
|
131
|
+
else
|
132
|
+
raise ConfigError.new('Supplied path is non-empty group!')
|
133
|
+
end
|
134
|
+
|
135
|
+
save
|
136
|
+
end
|
137
|
+
|
138
|
+
# Export config as YAML to the supplied file.
|
139
|
+
def import(file)
|
140
|
+
require 'yaml'
|
141
|
+
@config = YAML.load_file(file)
|
142
|
+
save
|
143
|
+
end
|
144
|
+
|
145
|
+
# Import config from YAML from the specified file.
|
146
|
+
def export(file)
|
147
|
+
require 'yaml'
|
148
|
+
File.open(file, 'w') { |out| YAML.dump(@config, out) }
|
149
|
+
end
|
150
|
+
|
151
|
+
private
|
152
|
+
|
153
|
+
def save
|
154
|
+
require 'ftools'
|
155
|
+
# create backup (File.copy always seems to overwrite existing file)
|
156
|
+
File.copy(@config_file, @config_file + '.bak') if File.exists? @config_file
|
157
|
+
File.open(@config_file, 'w') { |out| Marshal.dump(@config, out) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def retrieve_path(path, error)
|
161
|
+
host = path.inject(@config) do |hsh, ky|
|
162
|
+
raise ConfigError.new(error) unless hsh
|
163
|
+
hsh[ky]
|
164
|
+
end
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2010 Haim Ashkenazi
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
#
|
18
|
+
|
19
|
+
module RunSSHLib
|
20
|
+
|
21
|
+
# A class to handle ssh operations.
|
22
|
+
class SshBackend
|
23
|
+
# New backend with host/login details.
|
24
|
+
def initialize(host_def, overrides)
|
25
|
+
@host = host_def.name
|
26
|
+
@user = overrides[:login] ? overrides[:login] : host_def.login
|
27
|
+
end
|
28
|
+
|
29
|
+
# run shell on remote host.
|
30
|
+
def shell
|
31
|
+
command = "ssh #{@user ? %Q(-l #{@user}) : ''} #{@host}"
|
32
|
+
exec command
|
33
|
+
end
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
data/lib/runsshlib.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
#
|
2
|
+
# Copyright (C) 2010 Haim Ashkenazi
|
3
|
+
#
|
4
|
+
# This program is free software; you can redistribute it and/or
|
5
|
+
# modify it under the terms of the GNU General Public License
|
6
|
+
# as published by the Free Software Foundation; either version 2
|
7
|
+
# of the License, or (at your option) any later version.
|
8
|
+
#
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
12
|
+
# GNU General Public License for more details.
|
13
|
+
#
|
14
|
+
# You should have received a copy of the GNU General Public License
|
15
|
+
# along with this program; if not, write to the Free Software
|
16
|
+
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
|
17
|
+
#
|
18
|
+
|
19
|
+
require 'runsshlib/cli'
|
20
|
+
require 'runsshlib/config_file'
|
21
|
+
require 'runsshlib/ssh_backend'
|
22
|
+
|
23
|
+
# Main RunSSHLib module.
|
24
|
+
module RunSSHLib
|
25
|
+
|
26
|
+
DEFAULT_CONFIG = File.expand_path('~/.runssh')
|
27
|
+
|
28
|
+
# Indicates configuration error
|
29
|
+
class ConfigError < StandardError; end
|
30
|
+
|
31
|
+
# Indicates invalid command
|
32
|
+
class InvalidSubCommandError < StandardError; end
|
33
|
+
|
34
|
+
# A placeholder for host definitions
|
35
|
+
HostDef = Struct.new(:name, :login)
|
36
|
+
|
37
|
+
module Version
|
38
|
+
MAJOR = 0
|
39
|
+
MINOR = 1
|
40
|
+
BUILD = 0
|
41
|
+
|
42
|
+
STRING = [MAJOR, MINOR, BUILD].compact.join('.')
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
---
|
2
|
+
:cust1:
|
3
|
+
:dc1:
|
4
|
+
:host2: !ruby/struct:RunSSHLib::HostDef
|
5
|
+
name: b.host.com
|
6
|
+
login: user1
|
7
|
+
:host1: !ruby/struct:RunSSHLib::HostDef
|
8
|
+
name: a.host.com
|
9
|
+
login: user1
|
10
|
+
:dc2:
|
11
|
+
:host1: !ruby/struct:RunSSHLib::HostDef
|
12
|
+
name: c.host.com
|
13
|
+
login: user3
|
14
|
+
:cust2:
|
15
|
+
:dc:
|
16
|
+
:internal:
|
17
|
+
:somehost: !ruby/struct:RunSSHLib::HostDef
|
18
|
+
name: a.example.com
|
19
|
+
login: otheruser
|