trainsh 0.2.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.
- 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: []
|