trainsh 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +5 -0
- data/README.md +143 -0
- data/lib/trainsh.rb +5 -0
- data/lib/trainsh/cli.rb +261 -0
- data/lib/trainsh/config.rb +15 -0
- data/lib/trainsh/constants.rb +21 -0
- data/lib/trainsh/detectors/target.rb +17 -0
- data/lib/trainsh/detectors/target/env.rb +11 -0
- data/lib/trainsh/detectors/target/kitchen.rb +44 -0
- data/lib/trainsh/errors.rb +2 -0
- data/lib/trainsh/log.rb +7 -0
- data/lib/trainsh/mixin/builtin_commands.rb +156 -0
- data/lib/trainsh/mixin/file_helpers.rb +23 -0
- data/lib/trainsh/mixin/sessions.rb +54 -0
- data/lib/trainsh/session.rb +190 -0
- data/lib/trainsh/version.rb +3 -0
- metadata +214 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6b809837d484a4a100b8d6b6d4718d623bb91990ea72d7181b5e70dbe265282a
|
4
|
+
data.tar.gz: 12a87fc192b92df5f3f0528c620d898c24dc5d7ec849b51081c7d996d57e51fa
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: adef421ee124699ee90b841a57c0ef0653e7377a2d9cb6b3f6b0faa50948cc903e36fe38af4893ac8d60e8a9c94eb1adf3f8c66cd733c163700fac1480d148a4
|
7
|
+
data.tar.gz: 98947781c875c65f82563caeafba4637764c65758ebf8edf60fd16bf0765afe3a286032045957c20c0291248baf75f46dc165f25605492eb920b620a1d7737bc
|
data/CHANGELOG.md
ADDED
data/README.md
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
# TrainSH
|
2
|
+
|
3
|
+
## Summary
|
4
|
+
|
5
|
+
Interactive Shell for Remote Systems
|
6
|
+
|
7
|
+
Based on the Train ecosystem, provide a shell to manage systems via a multitude of transports.
|
8
|
+
|
9
|
+
Train default supports:
|
10
|
+
|
11
|
+
- Docker
|
12
|
+
- Local
|
13
|
+
- SSH (Unix, Windows, Cisco)
|
14
|
+
- WinRM (Windows)
|
15
|
+
|
16
|
+
3rd party plugins support:
|
17
|
+
|
18
|
+
- AWS Systems Manager
|
19
|
+
- LXD
|
20
|
+
- Telnet
|
21
|
+
- Serial/USB interfaces
|
22
|
+
- VMware Guest Operations (VMware Tools)
|
23
|
+
- ...
|
24
|
+
|
25
|
+
## Example uses
|
26
|
+
|
27
|
+
When specifying a password within an URL take care of special characters or the connection will fail.
|
28
|
+
|
29
|
+
```shell
|
30
|
+
trainsh connect local://
|
31
|
+
```
|
32
|
+
|
33
|
+
```shell
|
34
|
+
trainsh connect winrm://Administrator:Passw0rd@10.20.30.40
|
35
|
+
```
|
36
|
+
|
37
|
+
```shell
|
38
|
+
trainsh connect awsssm://i-123456789abc0
|
39
|
+
```
|
40
|
+
|
41
|
+
```shell
|
42
|
+
trainsh connect vsphere-gom://example-vm
|
43
|
+
```
|
44
|
+
|
45
|
+
If the transport has additional options like `key_files`, they need to be added as URL parameters (`ssh://10.20.30.40/?key_files=~/.ssh/id_rsa2`).
|
46
|
+
|
47
|
+
## Shell commands
|
48
|
+
|
49
|
+
There are three separate categories of commands when in the Shell:
|
50
|
+
|
51
|
+
- remote commands
|
52
|
+
- local commands, which are prefixed with `.`
|
53
|
+
- built-in commands, wich are prefixed with `!`
|
54
|
+
|
55
|
+
Anything you type without a prefix gets executed 1:1 on the remote system. Please notice that Train is headless, so any interactive programs will not work but lock up your shell! If you need to edit remote files or use a pager (like `less`), please look into the Built-In Commands section.
|
56
|
+
|
57
|
+
Local commands get executed 1:1 on your local system, so you are able to check things locally or change paths, for example.
|
58
|
+
|
59
|
+
There is also the Sessionprefix `@`, which is described briefly in the "Prompt and Sessions" section.
|
60
|
+
|
61
|
+
## Built-In Commands
|
62
|
+
|
63
|
+
`!!!`
|
64
|
+
Quit TrainSH. Aliases: `quit`, `exit`, `logout`, `disconnect`.
|
65
|
+
|
66
|
+
`!clear-history`
|
67
|
+
Clear your TrainSH history, for example to remove clutter or sensitive information
|
68
|
+
|
69
|
+
`!connect <uri>`
|
70
|
+
Connect to another system. The URI needs to match the format of the used Train transport, which is usually `transportname://host` but varies. See the Train transport's documentation for details.
|
71
|
+
|
72
|
+
`!detect`
|
73
|
+
Re-runs the OS detection which is running automatically on start. This will determine the OS, OS-family and general platform information via Train.
|
74
|
+
|
75
|
+
`!download <remote> <local>`
|
76
|
+
Download a file to the local system. You need to specify the target, if you want to keep the name just use `.` as local part.
|
77
|
+
|
78
|
+
`!edit <remotefile>`
|
79
|
+
Downloads the remote file as temporary file and opens the system default editor locally (`EDITOR` and `VISUAL` environment variables, with fallback to `vi`). Upon exiting the file, it will be uploaded again and overwrite the remote file.
|
80
|
+
|
81
|
+
`!env`
|
82
|
+
Prints the environment variables of your remote shell. This will be filled on first command invocation to save IO. **Currently unsupported for Windows remote systems**
|
83
|
+
|
84
|
+
`!history`
|
85
|
+
Output your TrainSH command history. As this uses the popular Readline library, you can also navigate your history with the Up/Down arrows or use Ctrl-R for reverse search. You can do auto completion for built-in commands.
|
86
|
+
|
87
|
+
`!host`
|
88
|
+
Outputs the remote hostname.
|
89
|
+
|
90
|
+
`!ping`
|
91
|
+
Executes a simple command to measure roundtrip/overhead time.
|
92
|
+
|
93
|
+
`!pwd`
|
94
|
+
Output the remote working directory. This will be filled on first command invocation to save IO.
|
95
|
+
|
96
|
+
`!read <remotefile>`
|
97
|
+
Download the specified file and display it in the system default pager (`PAGER` environment variable, with fallback to `less`).
|
98
|
+
|
99
|
+
`!reconnect`
|
100
|
+
Quits and reopens the current session.
|
101
|
+
|
102
|
+
`!sessions`
|
103
|
+
List all currently active sessions (See "Prompt and Sessions" section). Password information is redacted for security reasons.
|
104
|
+
|
105
|
+
`!session <session_id>`
|
106
|
+
Change to another session by passing its ID (=number)
|
107
|
+
|
108
|
+
`!upload <local> <remote>`
|
109
|
+
Upload files to the target machine
|
110
|
+
|
111
|
+
## Prompt and Sessions
|
112
|
+
|
113
|
+
`OK trainsh(@0 local://trc4023)>`
|
114
|
+
|
115
|
+
The prompt consists roughly of three areas:
|
116
|
+
|
117
|
+
- Exit code of last command ("OK" or the exit code in format `Exx` with xx being the numeric code)
|
118
|
+
- Session indication
|
119
|
+
- Input area
|
120
|
+
|
121
|
+
The session indication includes the ID of the session, so `@0` means session 0, `@1` means session 1 and so on. The second part shows the Train URI for the remote system for easy identification.
|
122
|
+
|
123
|
+
Any TrainSH invocation has at minimum one active session but if you want, you can add more sessions with the `!connect <uri>` built-in. Every session has its own Train backend and storage of current working directory, environment variables etc. You can list active sessions with `!sessions` and switch between them using `!session <session_id>`.
|
124
|
+
|
125
|
+
If you want to execute commands on another session ad-hoc, you can prefix it with the session id: `@1 uname -a` will execute the `uname -a` command on session ID 1.
|
126
|
+
|
127
|
+
Depending on the transport and target, the time between sending a command and retrieving the result will vary widely. In TrainSH this is called the "ping", which measures the minimum overhead/latency for the session. It varies massively between Train plugins and is not only related to the network distance, but also to the way of command execution - which might involve a number of HTTPS requests or the time to invoke a remote shell (examples: AWS SSM and VSphere GOM transports).
|
128
|
+
|
129
|
+
### Under the Hood
|
130
|
+
|
131
|
+
As Train is headless/stateless system, there will be various pre- and postfixed commands to make your life easier. This means that any state-related commands like changing the current directory or modification of environment variables would be unavailable for the next command - as it technically is a separate shell.
|
132
|
+
|
133
|
+
To make this easier, internal commands get attached to your input like this:
|
134
|
+
|
135
|
+
- Prefix: Get remote hostname (discovery task, just on first execution)
|
136
|
+
- Prefix: Set previous environment variables
|
137
|
+
- Prefix: Set previous path
|
138
|
+
- Command
|
139
|
+
- Postfix: Retrieve and save exit code of command
|
140
|
+
- Postfix: Retrieve and save new path
|
141
|
+
- Postfix: Retrieve and save new environment variables
|
142
|
+
|
143
|
+
Output of commands gets separated by outputting a highly random string between, which should not result in false positives. If a false positive occurs for some reason, TrainSH will fail and output an error.
|
data/lib/trainsh.rb
ADDED
data/lib/trainsh/cli.rb
ADDED
@@ -0,0 +1,261 @@
|
|
1
|
+
require_relative '../trainsh'
|
2
|
+
require_relative 'session'
|
3
|
+
|
4
|
+
require_relative 'mixin/builtin_commands'
|
5
|
+
require_relative 'mixin/file_helpers'
|
6
|
+
require_relative 'mixin/sessions'
|
7
|
+
|
8
|
+
# TODO
|
9
|
+
# require_relative 'detectors/target/env.rb'
|
10
|
+
# require_relative 'detectors/target/kitchen.rb'
|
11
|
+
|
12
|
+
require 'colored'
|
13
|
+
require 'fileutils'
|
14
|
+
require 'readline'
|
15
|
+
require 'train'
|
16
|
+
require 'thor'
|
17
|
+
|
18
|
+
module TrainSH
|
19
|
+
class Cli < Thor
|
20
|
+
include Thor::Actions
|
21
|
+
check_unknown_options!
|
22
|
+
# add_runtime_options!
|
23
|
+
|
24
|
+
def self.exit_on_failure?
|
25
|
+
true
|
26
|
+
end
|
27
|
+
|
28
|
+
map %w[version] => :__print_version
|
29
|
+
desc 'version', 'Display version'
|
30
|
+
def __print_version
|
31
|
+
say "#{TrainSH::PRODUCT} #{TrainSH::VERSION} (Ruby #{RUBY_VERSION}-#{RUBY_PLATFORM})"
|
32
|
+
end
|
33
|
+
|
34
|
+
EXIT_COMMANDS = %w[!!! exit quit logout disconnect].freeze
|
35
|
+
INTERACTIVE_COMMANDS = %w[more less vi vim nano].freeze
|
36
|
+
|
37
|
+
no_commands do
|
38
|
+
include TrainSH::Mixin::BuiltInCommands
|
39
|
+
include TrainSH::Mixin::FileHelpers
|
40
|
+
include TrainSH::Mixin::Sessions
|
41
|
+
|
42
|
+
def __disconnect
|
43
|
+
session.close
|
44
|
+
|
45
|
+
say 'Disconnected'
|
46
|
+
end
|
47
|
+
|
48
|
+
# TODO
|
49
|
+
# def __detect_url(url)
|
50
|
+
# url || kitchen_url || ENV['target']
|
51
|
+
# end
|
52
|
+
|
53
|
+
def __detect
|
54
|
+
raise 'No session in __detect' unless session
|
55
|
+
|
56
|
+
platform = session.platform
|
57
|
+
|
58
|
+
say format('Platform: %<title>s %<release>s (%<arch>s)',
|
59
|
+
title: platform.title,
|
60
|
+
release: platform.release,
|
61
|
+
arch: platform.arch)
|
62
|
+
say format('Hierarchy: %<hierarchy>s',
|
63
|
+
hierarchy: platform.family_hierarchy.reverse.join('/'))
|
64
|
+
say 'Measuring ping over connection...'
|
65
|
+
|
66
|
+
# Idle command will also trigger discovery commands on first run
|
67
|
+
session.run_idle
|
68
|
+
|
69
|
+
say format('Ping: %<ping>dms', ping: session.ping) if session.ping
|
70
|
+
end
|
71
|
+
|
72
|
+
def target_detectors
|
73
|
+
Dir[File.join(__dir__, 'lib', '*.rb')].sort.each { |file| require file }
|
74
|
+
|
75
|
+
TrainSH::Detectors::TargetDetector.descendants
|
76
|
+
end
|
77
|
+
|
78
|
+
def execute(input)
|
79
|
+
case input[0]
|
80
|
+
when '.'
|
81
|
+
execute_locally input[1..]
|
82
|
+
when '!'
|
83
|
+
execute_builtin input[1..]
|
84
|
+
when '@'
|
85
|
+
execute_via_session input[1..]
|
86
|
+
else
|
87
|
+
execute_via_train input
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def execute_locally(input)
|
92
|
+
system(input)
|
93
|
+
end
|
94
|
+
|
95
|
+
def execute_via_train(input, session_id = current_session_id)
|
96
|
+
return if interactive_command? input
|
97
|
+
|
98
|
+
command_result = @sessions[session_id].run(input)
|
99
|
+
|
100
|
+
say command_result.stdout unless command_result.stdout && command_result.stdout.empty?
|
101
|
+
say command_result.stderr.red unless command_result.stderr && command_result.stderr.empty?
|
102
|
+
end
|
103
|
+
|
104
|
+
def execute_builtin(input)
|
105
|
+
cmd, *args = input.split
|
106
|
+
|
107
|
+
ruby_cmd = cmd.tr('-', '_')
|
108
|
+
|
109
|
+
if builtin_commands.include? ruby_cmd
|
110
|
+
send("#{BUILTIN_PREFIX}#{ruby_cmd}".to_sym, *args)
|
111
|
+
else
|
112
|
+
say format('Unknown built-in "%<cmd>s"', cmd: cmd)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def execute_via_session(input)
|
117
|
+
session_id, *data = input.split
|
118
|
+
|
119
|
+
session_id = validate_session_id(session_id)
|
120
|
+
if session_id.nil?
|
121
|
+
say 'Expecting valid session id, e.g. `!session 2`'.red
|
122
|
+
return
|
123
|
+
end
|
124
|
+
|
125
|
+
input = data.join(' ')
|
126
|
+
if input.empty?
|
127
|
+
say 'Specify command to execute, e.g. `@0 ls`'
|
128
|
+
return
|
129
|
+
end
|
130
|
+
|
131
|
+
execute_via_train(input, session_id)
|
132
|
+
end
|
133
|
+
|
134
|
+
def interactive_command?(cmd)
|
135
|
+
return unless INTERACTIVE_COMMANDS.any? { |banned| cmd.start_with? banned }
|
136
|
+
|
137
|
+
say 'Cannot execute interactive commands on non-tty sessions'.red
|
138
|
+
end
|
139
|
+
|
140
|
+
def exit_command?(cmd)
|
141
|
+
EXIT_COMMANDS.include? cmd
|
142
|
+
end
|
143
|
+
|
144
|
+
def prompt
|
145
|
+
exitcode = current_session.exitcode
|
146
|
+
exitcode_prefix = exitcode.zero? ? 'OK '.green : format('E%02d ', exitcode).red
|
147
|
+
|
148
|
+
format(::TrainSH::PROMPT,
|
149
|
+
exitcode: exitcode,
|
150
|
+
exitcode_prefix: exitcode_prefix,
|
151
|
+
session_id: current_session_id,
|
152
|
+
backend: current_session.backend || 'unknown',
|
153
|
+
host: current_session.host || 'unknown',
|
154
|
+
path: current_session.pwd || '?')
|
155
|
+
end
|
156
|
+
|
157
|
+
def auto_complete(partial)
|
158
|
+
choices = []
|
159
|
+
|
160
|
+
choices.concat(builtin_commands.map { |cmd| "!#{cmd.tr('_', '-')}" })
|
161
|
+
choices.concat(sessions.map { |session_id| "@#{session_id}" })
|
162
|
+
choices.concat %w[!!!]
|
163
|
+
|
164
|
+
choices.filter { |choice| choice.start_with? partial }
|
165
|
+
end
|
166
|
+
|
167
|
+
# def get_log_level(level)
|
168
|
+
# valid = %w{debug info warn error fatal}
|
169
|
+
#
|
170
|
+
# if valid.include?(level)
|
171
|
+
# l = level
|
172
|
+
# else
|
173
|
+
# l = "info"
|
174
|
+
# end
|
175
|
+
#
|
176
|
+
# Logger.const_get(l.upcase)
|
177
|
+
# end
|
178
|
+
end
|
179
|
+
|
180
|
+
# class_option :log_level, desc: "Log level", aliases: "-l", default: :info
|
181
|
+
class_option :messy, desc: 'Skip deletion of temporary files for speedup', default: false, type: :boolean
|
182
|
+
|
183
|
+
desc 'connect URL', 'Connect to a destination interactively'
|
184
|
+
|
185
|
+
def connect(url)
|
186
|
+
exit unless use_session(url)
|
187
|
+
|
188
|
+
say format('Connected to %<url>s', url: session.url).bold
|
189
|
+
say 'Running platform detection...'
|
190
|
+
__detect
|
191
|
+
|
192
|
+
# History persistence (TODO: Extract)
|
193
|
+
user_conf_dir = File.join(ENV['HOME'], TrainSH::USER_CONF_DIR)
|
194
|
+
history_file = File.join(user_conf_dir, 'history')
|
195
|
+
FileUtils.mkdir_p(user_conf_dir)
|
196
|
+
FileUtils.touch(history_file)
|
197
|
+
File.readlines(history_file).each { |line| Readline::HISTORY.push line.strip }
|
198
|
+
at_exit {
|
199
|
+
history_file = File.join(user_conf_dir, 'history')
|
200
|
+
File.open(history_file, 'w') { |f|
|
201
|
+
f.write Readline::HISTORY.to_a.join("\n")
|
202
|
+
}
|
203
|
+
}
|
204
|
+
|
205
|
+
# Catch Ctrl-C and exit cleanly
|
206
|
+
stty_save = `stty -g`.chomp
|
207
|
+
trap('INT') do
|
208
|
+
puts '^C'
|
209
|
+
system('stty', stty_save)
|
210
|
+
exit
|
211
|
+
end
|
212
|
+
|
213
|
+
# Autocompletion
|
214
|
+
Readline.completion_proc = method(:auto_complete).to_proc
|
215
|
+
Readline.completion_append_character = ' '
|
216
|
+
|
217
|
+
while (input = Readline.readline(prompt, true))
|
218
|
+
if input.empty?
|
219
|
+
Readline::HISTORY.pop
|
220
|
+
next
|
221
|
+
end
|
222
|
+
|
223
|
+
Readline::HISTORY.pop if input.start_with? '!history'
|
224
|
+
|
225
|
+
break if exit_command? input
|
226
|
+
next if interactive_command? input
|
227
|
+
|
228
|
+
execute input
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# desc 'copy FILE/DIR|URL FILE/DIR|URL', 'Copy files or directories'
|
233
|
+
# def copy(url_or_file, url_or_file)
|
234
|
+
# # TODO?
|
235
|
+
# end
|
236
|
+
|
237
|
+
desc 'detect URL', 'Retrieve remote OS and platform information'
|
238
|
+
def detect(url)
|
239
|
+
exit unless use_session(url)
|
240
|
+
__detect
|
241
|
+
end
|
242
|
+
|
243
|
+
# desc 'exec URL -- (COMMAND)', 'Execute remote commands'
|
244
|
+
# method_option :file, aliases: '-f', desc: "command file to read"
|
245
|
+
# def exec
|
246
|
+
# # TODO
|
247
|
+
# # TODO: Also accept getting commands from STDIN and from a file
|
248
|
+
# end
|
249
|
+
|
250
|
+
desc 'list-transports', 'List available transports'
|
251
|
+
def list_transports
|
252
|
+
# TODO: Train only lazy-loads and does not have a full registry
|
253
|
+
# https://github.com/inspec/train/blob/be90ca53ea1c1e8aa7439c504fbee86f4b399d83/lib/train.rb#L38-L61
|
254
|
+
|
255
|
+
# TODO: Filter for only "OS" transports as well
|
256
|
+
transports = %w[local ssh winrm docker]
|
257
|
+
|
258
|
+
say "Available transports: #{transports.sort.join(', ')}"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'mixlib/config'
|
2
|
+
|
3
|
+
module TrainSH
|
4
|
+
module Config
|
5
|
+
extend Mixlib::Config
|
6
|
+
config_strict_mode true
|
7
|
+
|
8
|
+
default :log_level, :info
|
9
|
+
|
10
|
+
default :pager, ENV['PAGER'] || 'less'
|
11
|
+
default :editor, ENV['EDITOR'] || ENV['VISUAL'] || 'vi'
|
12
|
+
|
13
|
+
default :user_config, "~/#{ENV['USER_CONF_DIR']}"
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module TrainSH
|
2
|
+
PRODUCT = 'TrainSH'.freeze
|
3
|
+
|
4
|
+
# The executable for interactive use
|
5
|
+
EXEC = 'trainsh'.freeze
|
6
|
+
|
7
|
+
# Prefix for environment variables
|
8
|
+
ENV_PREFIX = 'TRAINSH_'.freeze
|
9
|
+
|
10
|
+
# The user's configuration directory
|
11
|
+
USER_CONF_DIR = '.trainsh'.freeze
|
12
|
+
|
13
|
+
# Minimum version for remote file manipulation
|
14
|
+
TRAIN_MUTABLE_VERSION = '3.5.0'.freeze
|
15
|
+
|
16
|
+
# Prompt (TODO: Make configuratble)
|
17
|
+
PROMPT = '%<exitcode_prefix>strainsh(@%<session_id>d %<backend>s://%<host>s)> '.freeze
|
18
|
+
|
19
|
+
# Variable to remotely persist exit code
|
20
|
+
EXITCODE_VAR = 'CMD_EXIT'.freeze
|
21
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module TrainSH
|
2
|
+
module Detectors
|
3
|
+
class TargetDetector
|
4
|
+
def self.descendants
|
5
|
+
ObjectSpace.each_object(Class).select { |klass| klass < self }
|
6
|
+
end
|
7
|
+
|
8
|
+
def to_s
|
9
|
+
self.class.to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def url
|
13
|
+
raise "Implement `url` for target detector #{self}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require_relative '../target'
|
2
|
+
|
3
|
+
module TrainSH
|
4
|
+
module Detectors
|
5
|
+
class KitchenTarget < TargetDetector
|
6
|
+
def url
|
7
|
+
# return unless kitchen_available?
|
8
|
+
# return unless kitchen_instance
|
9
|
+
|
10
|
+
# kitchen_config['transport']
|
11
|
+
end
|
12
|
+
|
13
|
+
# private
|
14
|
+
|
15
|
+
# def kitchen_available?
|
16
|
+
# `kitchen`
|
17
|
+
# true
|
18
|
+
# rescue
|
19
|
+
# false
|
20
|
+
# end
|
21
|
+
|
22
|
+
# attr_writer :kitchen_instances
|
23
|
+
# def kitchen_instances
|
24
|
+
# return if @kitchen_instances
|
25
|
+
|
26
|
+
# @kitchen_instances ||= YAML.load(`KITCHEN_LOCAL_YAML=".kitchen.ec2.yaml" kitchen list --json`)
|
27
|
+
# end
|
28
|
+
|
29
|
+
# def kitchen_config
|
30
|
+
# return if @kitchen_config
|
31
|
+
|
32
|
+
# parsed_output = YAML.load(`KITCHEN_LOCAL_YAML=".kitchen.ec2.yaml" kitchen diagnose customize-amazon2`)
|
33
|
+
# @kitchen_config = kitchen_config['instances'].keys.first
|
34
|
+
# end
|
35
|
+
|
36
|
+
# def kitchen_instance_name
|
37
|
+
# kitchen_instances.select! { |instance| instance['last_action'] != nil }
|
38
|
+
|
39
|
+
# return nil if kitchen_instances.count != 1
|
40
|
+
# kitchen_instances.first['instance']
|
41
|
+
# end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/trainsh/log.rb
ADDED
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'train/errors'
|
2
|
+
|
3
|
+
module TrainSH
|
4
|
+
module Mixin
|
5
|
+
module BuiltInCommands
|
6
|
+
BUILTIN_PREFIX = 'builtincmd_'.freeze
|
7
|
+
|
8
|
+
def builtin_commands
|
9
|
+
methods.sort.filter { |method| method.to_s.start_with? BUILTIN_PREFIX }.map { |method| method.to_s.delete_prefix BUILTIN_PREFIX }
|
10
|
+
end
|
11
|
+
|
12
|
+
def builtincmd_clear_history(_args = nil)
|
13
|
+
Readline::HISTORY.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
def builtincmd_connect(url = nil)
|
17
|
+
if url.nil? || url.strip.empty?
|
18
|
+
say 'Expecting session url, e.g. `!connect docker://123456789abcdef0`'.red
|
19
|
+
return false
|
20
|
+
end
|
21
|
+
|
22
|
+
use_session(url)
|
23
|
+
end
|
24
|
+
|
25
|
+
# def builtincmd_copy(source = nil, destination = nil)
|
26
|
+
# # TODO: Copy files between sessions
|
27
|
+
# end
|
28
|
+
|
29
|
+
def builtincmd_detect(_args = nil)
|
30
|
+
__detect
|
31
|
+
end
|
32
|
+
|
33
|
+
def builtincmd_download(remote_path = nil, local_path = nil)
|
34
|
+
if remote_path.nil? || local_path.nil?
|
35
|
+
say 'Expecting remote path and local path, e.g. `!download /etc/passwd /home/ubuntu`'
|
36
|
+
return false
|
37
|
+
end
|
38
|
+
|
39
|
+
return unless train_mutable?
|
40
|
+
|
41
|
+
session.download(remote_path, local_path)
|
42
|
+
rescue ::Train::NotImplementedError
|
43
|
+
say 'Backend for session does not implement download operation'.red
|
44
|
+
end
|
45
|
+
|
46
|
+
def builtincmd_edit(path = nil)
|
47
|
+
if path.nil? || path.strip.empty?
|
48
|
+
say 'Expecting remote path, e.g. `!less /tmp/somefile.txt`'.red
|
49
|
+
return false
|
50
|
+
end
|
51
|
+
|
52
|
+
tempfile = read_file(path)
|
53
|
+
|
54
|
+
localeditor = ENV['EDITOR'] || ENV['VISUAL'] || 'vi' # TODO: configuration, Windows, ...
|
55
|
+
say format('Using local editor `%<editor>s` for %<tempfile>s', editor: localeditor, tempfile: tempfile.path)
|
56
|
+
|
57
|
+
system("#{localeditor} #{tempfile.path}")
|
58
|
+
|
59
|
+
new_content = File.read(tempfile.path)
|
60
|
+
|
61
|
+
write_file(path, new_content)
|
62
|
+
tempfile.unlink
|
63
|
+
rescue ::Train::NotImplementedError
|
64
|
+
say 'Backend for session does not implement file operations'.red
|
65
|
+
end
|
66
|
+
|
67
|
+
def builtincmd_env(_args = nil)
|
68
|
+
puts session.env
|
69
|
+
end
|
70
|
+
|
71
|
+
def builtincmd_read(path = nil)
|
72
|
+
if path.nil? || path.strip.empty?
|
73
|
+
say 'Expecting remote path, e.g. `!read /tmp/somefile.txt`'.red
|
74
|
+
return false
|
75
|
+
end
|
76
|
+
|
77
|
+
tempfile = read_file(path)
|
78
|
+
return false unless tempfile
|
79
|
+
|
80
|
+
localpager = ENV['PAGER'] || 'less' # TODO: configuration, Windows, ...
|
81
|
+
say format('Using local pager `%<pager>s` for %<tempfile>s', pager: localpager, tempfile: tempfile.path)
|
82
|
+
system("#{localpager} #{tempfile.path}")
|
83
|
+
|
84
|
+
tempfile.unlink
|
85
|
+
rescue ::NotImplementedError
|
86
|
+
say 'Backend for session does not implement file operations'.red
|
87
|
+
end
|
88
|
+
|
89
|
+
def builtincmd_history(_args = nil)
|
90
|
+
puts Readline::HISTORY.to_a
|
91
|
+
end
|
92
|
+
|
93
|
+
def builtincmd_host(_args = nil)
|
94
|
+
say session.host
|
95
|
+
end
|
96
|
+
|
97
|
+
def builtincmd_ping(_args = nil)
|
98
|
+
session.run_idle
|
99
|
+
say format('Ping: %<ping>dms', ping: session.ping)
|
100
|
+
end
|
101
|
+
|
102
|
+
def builtincmd_pwd(_args = nil)
|
103
|
+
say session.pwd
|
104
|
+
end
|
105
|
+
|
106
|
+
def builtincmd_reconnect(_args = nil)
|
107
|
+
session.reconnect
|
108
|
+
end
|
109
|
+
|
110
|
+
def builtincmd_sessions(_args = nil)
|
111
|
+
say 'Active sessions:'
|
112
|
+
|
113
|
+
@sessions.each_with_index do |session, idx|
|
114
|
+
say format('[%<idx>d] %<session>s', idx: idx, session: session.url)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def builtincmd_session(session_id = nil)
|
119
|
+
session_id = validate_session_id(session_id)
|
120
|
+
|
121
|
+
if session_id.nil?
|
122
|
+
say 'Expecting valid session id, e.g. `!session 2`'.red
|
123
|
+
return false
|
124
|
+
end
|
125
|
+
|
126
|
+
# TODO: Make this more pretty
|
127
|
+
session_url = @sessions[session_id].url
|
128
|
+
|
129
|
+
use_session(session_url)
|
130
|
+
end
|
131
|
+
|
132
|
+
def builtincmd_upload(local_path = nil, remote_path = nil)
|
133
|
+
if remote_path.nil? || local_path.nil?
|
134
|
+
say 'Expecting remote path and local path, e.g. `!download /home/ubuntu/passwd /etc`'
|
135
|
+
return false
|
136
|
+
end
|
137
|
+
|
138
|
+
return unless train_mutable?
|
139
|
+
|
140
|
+
session.upload(local_path, remote_path)
|
141
|
+
rescue ::Errno::ENOENT
|
142
|
+
say "Local file/directory '#{local_path}' does not exist".red
|
143
|
+
rescue ::NotImplementedError
|
144
|
+
say 'Backend for session does not implement upload operation'.red
|
145
|
+
end
|
146
|
+
|
147
|
+
private
|
148
|
+
|
149
|
+
def train_mutable?
|
150
|
+
return true if session.respond_to?(:upload)
|
151
|
+
|
152
|
+
say "Support for remote file modification needs at least Train #{::TrainSH::TRAIN_MUTABLE_VERSION} (is: #{::Train::VERSION})".red
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'tempfile' unless defined?(Tempfile)
|
2
|
+
|
3
|
+
module TrainSH
|
4
|
+
module Mixin
|
5
|
+
module FileHelpers
|
6
|
+
def read_file(path)
|
7
|
+
remotefile = session.file(path)
|
8
|
+
say format('Remote file %<filename>s does not exist', filename: path) unless remotefile.exist?
|
9
|
+
|
10
|
+
localfile = Tempfile.open
|
11
|
+
localfile.write(remotefile.content || '')
|
12
|
+
localfile.close
|
13
|
+
|
14
|
+
localfile
|
15
|
+
end
|
16
|
+
|
17
|
+
def write_file(path, content)
|
18
|
+
remotefile = session.file(path)
|
19
|
+
remotefile.content = content
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require_relative '../session'
|
2
|
+
|
3
|
+
module TrainSH
|
4
|
+
module Mixin
|
5
|
+
module Sessions
|
6
|
+
def use_session(url)
|
7
|
+
@sessions = [] if @sessions.nil?
|
8
|
+
|
9
|
+
existing_id = @sessions.index { |session| session.url == url }
|
10
|
+
|
11
|
+
if existing_id.nil?
|
12
|
+
@current_session_id = @sessions.count
|
13
|
+
@sessions << TrainSH::Session.new(url)
|
14
|
+
else
|
15
|
+
@current_session_id = existing_id
|
16
|
+
end
|
17
|
+
rescue Train::PluginLoadError
|
18
|
+
say format('No Train plugin found for url %<url>s', url: url).red
|
19
|
+
nil
|
20
|
+
end
|
21
|
+
|
22
|
+
def session(session_id = current_session_id)
|
23
|
+
@sessions[session_id]
|
24
|
+
end
|
25
|
+
|
26
|
+
# ?
|
27
|
+
def sessions
|
28
|
+
(0..@sessions.count - 1).to_a
|
29
|
+
end
|
30
|
+
|
31
|
+
def current_session_id
|
32
|
+
@current_session_id ||= 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def current_session
|
36
|
+
@sessions[current_session_id]
|
37
|
+
end
|
38
|
+
|
39
|
+
def validate_session_id(session_id)
|
40
|
+
unless session_id.match?(/^[0-9]+$/)
|
41
|
+
say 'Expected session id to be numeric'.red
|
42
|
+
return
|
43
|
+
end
|
44
|
+
|
45
|
+
if @sessions[session_id.to_i].nil?
|
46
|
+
say format('No session id [%s] found', session_id).red
|
47
|
+
return
|
48
|
+
end
|
49
|
+
|
50
|
+
session_id.to_i
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'forwardable'
|
3
|
+
|
4
|
+
require 'train'
|
5
|
+
|
6
|
+
module TrainSH
|
7
|
+
class Command
|
8
|
+
MAGIC_STRING = 'mVDK6afaqa6fb7kcMqTpR2aoUFbYsRt889G4eGoI'.freeze
|
9
|
+
|
10
|
+
attr_writer :connection
|
11
|
+
|
12
|
+
def initialize(command, connection)
|
13
|
+
@command = command
|
14
|
+
@connection = connection
|
15
|
+
|
16
|
+
@prefixes = []
|
17
|
+
@postfixes = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def prefix(prefix_command, &block)
|
21
|
+
@prefixes << {
|
22
|
+
command: prefix_command,
|
23
|
+
block: block
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
def postfix(postfix_command, &block)
|
28
|
+
@postfixes << {
|
29
|
+
command: postfix_command,
|
30
|
+
block: block
|
31
|
+
}
|
32
|
+
end
|
33
|
+
|
34
|
+
def run
|
35
|
+
result = @connection.run_command aggregate_commands
|
36
|
+
stdouts = parse(result)
|
37
|
+
|
38
|
+
prefixes_stdout = stdouts.first(@prefixes.count).reverse
|
39
|
+
@prefixes.each_with_index do |prefix, idx|
|
40
|
+
next if prefix[:block].nil?
|
41
|
+
|
42
|
+
prefix[:block].call prefixes_stdout[idx]
|
43
|
+
end
|
44
|
+
@prefixes.count.times { stdouts.shift } unless @prefixes.empty?
|
45
|
+
|
46
|
+
postfixes_stdout = stdouts.last(@postfixes.count)
|
47
|
+
@postfixes.each_with_index do |postfix, idx|
|
48
|
+
next if postfix[:block].nil?
|
49
|
+
|
50
|
+
postfix[:block].call postfixes_stdout[idx]
|
51
|
+
end
|
52
|
+
@postfixes.count.times { stdouts.pop } unless @postfixes.empty?
|
53
|
+
|
54
|
+
raise 'Pre-/Postfix command processing ended up with more than one remaining stdout' if stdouts.count > 1
|
55
|
+
|
56
|
+
result.stdout = stdouts.first
|
57
|
+
# result.stderr = "" # TODO
|
58
|
+
result.exit_status = 0 # TODO
|
59
|
+
result
|
60
|
+
end
|
61
|
+
|
62
|
+
def parse(result)
|
63
|
+
result.stdout
|
64
|
+
.gsub(/\r\n/, "\n")
|
65
|
+
.gsub(/ *$/, '')
|
66
|
+
.split(MAGIC_STRING)
|
67
|
+
.map(&:strip)
|
68
|
+
end
|
69
|
+
|
70
|
+
def aggregate_commands
|
71
|
+
separator = "\necho #{MAGIC_STRING}\n"
|
72
|
+
|
73
|
+
commands = @prefixes.reverse.map { |ary| ary[:command] }
|
74
|
+
commands << "#{@command}\n#{save_exit_code}"
|
75
|
+
commands.concat(@postfixes.map { |ary| ary[:command] })
|
76
|
+
|
77
|
+
commands.join(separator)
|
78
|
+
end
|
79
|
+
|
80
|
+
def save_exit_code
|
81
|
+
@connection.platform.windows? ? "$#{TrainSH::EXITCODE_VAR}=$LastExitCode" : "export #{TrainSH::EXITCODE_VAR}=$?"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
class Session
|
86
|
+
extend Forwardable
|
87
|
+
def_delegators :@connection, :platform, :run_command, :backend_type, :file, :upload, :download
|
88
|
+
|
89
|
+
attr_reader :connection, :backend, :host
|
90
|
+
|
91
|
+
attr_accessor :pwd, :env, :ping, :exitcode
|
92
|
+
|
93
|
+
def initialize(url = nil)
|
94
|
+
connect(url) unless url.nil?
|
95
|
+
end
|
96
|
+
|
97
|
+
def connect(url)
|
98
|
+
@url = url
|
99
|
+
|
100
|
+
data = Train.unpack_target_from_uri(url)
|
101
|
+
|
102
|
+
# TODO: Wire up with "messy" parameter
|
103
|
+
data[:cleanup] = false
|
104
|
+
|
105
|
+
backend = Train.create(data[:backend], data)
|
106
|
+
return false unless backend
|
107
|
+
|
108
|
+
@backend = data[:backend]
|
109
|
+
@host = data[:host]
|
110
|
+
|
111
|
+
@connection = backend.connection
|
112
|
+
connection.wait_until_ready
|
113
|
+
|
114
|
+
at_exit { disconnect }
|
115
|
+
end
|
116
|
+
|
117
|
+
def disconnect
|
118
|
+
puts "Closing session #{url}"
|
119
|
+
|
120
|
+
connection.close
|
121
|
+
end
|
122
|
+
|
123
|
+
def reconnect
|
124
|
+
disconnect
|
125
|
+
|
126
|
+
connect(url)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Redact password information
|
130
|
+
def url
|
131
|
+
Addressable::URI.parse(@url).omit(:password).to_s
|
132
|
+
end
|
133
|
+
|
134
|
+
def run(command, skip_affixes: false)
|
135
|
+
command = Command.new(command, @connection)
|
136
|
+
|
137
|
+
# Save exit code
|
138
|
+
command.postfix(exitcode_get) { |output| @exitcode = output.to_i }
|
139
|
+
|
140
|
+
# Request UTF-8 instead of UTF-16
|
141
|
+
# command.prefix("$PSDefaultParameterValues['Out-File:Encoding'] = 'utf8'") if platform.windows?
|
142
|
+
|
143
|
+
unless skip_affixes
|
144
|
+
command.prefix(pwd_set)
|
145
|
+
command.postfix(pwd_get) { |output| @pwd = output }
|
146
|
+
command.prefix(env_set)
|
147
|
+
command.postfix(env_get) { |output| @env = output }
|
148
|
+
end
|
149
|
+
|
150
|
+
# Discovery tasks
|
151
|
+
command.prefix(host_get) { |output| @host = output } if host.nil? || host == 'unknown'
|
152
|
+
|
153
|
+
command.run
|
154
|
+
end
|
155
|
+
|
156
|
+
def run_idle
|
157
|
+
@ping = ::Benchmark.measure { run('#', skip_affixes: true) }.real * 1000
|
158
|
+
end
|
159
|
+
|
160
|
+
private
|
161
|
+
|
162
|
+
def exitcode_get
|
163
|
+
"echo $#{TrainSH::EXITCODE_VAR}"
|
164
|
+
end
|
165
|
+
|
166
|
+
def host_get
|
167
|
+
'hostname'
|
168
|
+
end
|
169
|
+
|
170
|
+
def pwd_get
|
171
|
+
platform.windows? ? '(Get-Location).Path' : 'pwd'
|
172
|
+
end
|
173
|
+
|
174
|
+
def pwd_set(path = pwd)
|
175
|
+
return '' if path.nil?
|
176
|
+
|
177
|
+
platform.windows? ? "Set-Location #{path}" : "cd #{path}"
|
178
|
+
end
|
179
|
+
|
180
|
+
# TODO: Preserve Windows environment variables
|
181
|
+
def env_get
|
182
|
+
platform.windows? ? '' : 'export'
|
183
|
+
end
|
184
|
+
|
185
|
+
# TODO: Preserve Windows environment variables
|
186
|
+
def env_set
|
187
|
+
platform.windows? ? '' : env
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
metadata
ADDED
@@ -0,0 +1,214 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: trainsh
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.2.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Thomas Heinen
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2021-02-25 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bump
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0.9'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0.9'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: bundler-audit
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0.7'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0.7'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mdl
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0.9'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0.9'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: overcommit
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0.55'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0.55'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rake
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '12.3'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '12.3'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '3.10'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.10'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: rubocop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0.92'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0.92'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: colored
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '1.2'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '1.2'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: readline
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - "~>"
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0.0'
|
132
|
+
type: :runtime
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - "~>"
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0.0'
|
139
|
+
- !ruby/object:Gem::Dependency
|
140
|
+
name: thor
|
141
|
+
requirement: !ruby/object:Gem::Requirement
|
142
|
+
requirements:
|
143
|
+
- - "~>"
|
144
|
+
- !ruby/object:Gem::Version
|
145
|
+
version: '1.1'
|
146
|
+
type: :runtime
|
147
|
+
prerelease: false
|
148
|
+
version_requirements: !ruby/object:Gem::Requirement
|
149
|
+
requirements:
|
150
|
+
- - "~>"
|
151
|
+
- !ruby/object:Gem::Version
|
152
|
+
version: '1.1'
|
153
|
+
- !ruby/object:Gem::Dependency
|
154
|
+
name: train
|
155
|
+
requirement: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '3.5'
|
160
|
+
type: :runtime
|
161
|
+
prerelease: false
|
162
|
+
version_requirements: !ruby/object:Gem::Requirement
|
163
|
+
requirements:
|
164
|
+
- - ">="
|
165
|
+
- !ruby/object:Gem::Version
|
166
|
+
version: '3.5'
|
167
|
+
description: Based on the Train ecosystem, provide a shell to manage systems via a
|
168
|
+
multitude of transports.
|
169
|
+
email:
|
170
|
+
- theinen@tecracer.de
|
171
|
+
executables: []
|
172
|
+
extensions: []
|
173
|
+
extra_rdoc_files: []
|
174
|
+
files:
|
175
|
+
- CHANGELOG.md
|
176
|
+
- README.md
|
177
|
+
- lib/trainsh.rb
|
178
|
+
- lib/trainsh/cli.rb
|
179
|
+
- lib/trainsh/config.rb
|
180
|
+
- lib/trainsh/constants.rb
|
181
|
+
- lib/trainsh/detectors/target.rb
|
182
|
+
- lib/trainsh/detectors/target/env.rb
|
183
|
+
- lib/trainsh/detectors/target/kitchen.rb
|
184
|
+
- lib/trainsh/errors.rb
|
185
|
+
- lib/trainsh/log.rb
|
186
|
+
- lib/trainsh/mixin/builtin_commands.rb
|
187
|
+
- lib/trainsh/mixin/file_helpers.rb
|
188
|
+
- lib/trainsh/mixin/sessions.rb
|
189
|
+
- lib/trainsh/session.rb
|
190
|
+
- lib/trainsh/version.rb
|
191
|
+
homepage: https://github.com/tecracer-chef/trainsh
|
192
|
+
licenses:
|
193
|
+
- Nonstandard
|
194
|
+
metadata: {}
|
195
|
+
post_install_message:
|
196
|
+
rdoc_options: []
|
197
|
+
require_paths:
|
198
|
+
- lib
|
199
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
200
|
+
requirements:
|
201
|
+
- - ">="
|
202
|
+
- !ruby/object:Gem::Version
|
203
|
+
version: '2.6'
|
204
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
205
|
+
requirements:
|
206
|
+
- - ">="
|
207
|
+
- !ruby/object:Gem::Version
|
208
|
+
version: '0'
|
209
|
+
requirements: []
|
210
|
+
rubygems_version: 3.0.3
|
211
|
+
signing_key:
|
212
|
+
specification_version: 4
|
213
|
+
summary: Interactive Shell for Remote Systems
|
214
|
+
test_files: []
|