tmux-connector 0.0.4 → 0.8.5
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.md +40 -15
- data/lib/tmux-connector/commands/send.rb +4 -1
- data/lib/tmux-connector/host.rb +16 -2
- data/lib/tmux-connector/layout.rb +20 -1
- data/lib/tmux-connector/tmux_handler.rb +28 -24
- data/lib/tmux-connector/version.rb +1 -1
- data/lib/tmux-connector.rb +5 -2
- data/spec/config_spec.rb +4 -0
- data/spec/fixtures/configs.yml +16 -0
- metadata +2 -2
data/README.md
CHANGED
@@ -4,11 +4,14 @@ Manage multiple servers using SSH and [tmux].
|
|
4
4
|
|
5
5
|
|
6
6
|
## Features:
|
7
|
+
* connect to multiple servers at once
|
8
|
+
* expressive layouts customizable for different server groups available
|
9
|
+
* issue commands to all servers or just a selected (custom) subgroup
|
7
10
|
* work on multiple sessions in parallel
|
11
|
+
* multiple connections to individual servers possible
|
8
12
|
* sessions can be persisted (actually recreated) after computer restarts
|
9
13
|
- they are lost only if you delete them explicitly
|
10
|
-
|
11
|
-
* issuing commands to all servers or just a selected subgroups
|
14
|
+
|
12
15
|
|
13
16
|
## Quick tease
|
14
17
|
|
@@ -61,8 +64,9 @@ Usage:
|
|
61
64
|
tcon delete (<session-name> | --all)
|
62
65
|
tcon list
|
63
66
|
tcon send <session-name> (<command> | --command-file=<file>)
|
64
|
-
[--server-filter=<regex>
|
65
|
-
|
67
|
+
[ --server-filter=<regex> | --group-filter=<regex>
|
68
|
+
| --filter=<regex> | --window=<index> ]
|
69
|
+
[--verbose]
|
66
70
|
tcon --help
|
67
71
|
tcon --version
|
68
72
|
|
@@ -72,6 +76,7 @@ Options:
|
|
72
76
|
<session-name> Name that identifies the session. Must be unique.
|
73
77
|
<command> Command to be executed on remote server[s].
|
74
78
|
<regex> String that represents valid Ruby regex.
|
79
|
+
<index> 0-based index.
|
75
80
|
-s --ssh-config=file Path to ssh config file [default: ~/.ssh/config].
|
76
81
|
-n --session-name=name Name of the session to be used in the tcon command.
|
77
82
|
-p --purpose=description Description of session's purpose.
|
@@ -83,6 +88,7 @@ Options:
|
|
83
88
|
-r --filter=regex Filter to select a subset of the servers via
|
84
89
|
host names or group membership.
|
85
90
|
Combines --server-filter and --group-filter.
|
91
|
+
-w --window=index Select a window via (0-based) index.
|
86
92
|
-c --command-file=file File containing the list of commands to be
|
87
93
|
executed on remote server[s].
|
88
94
|
-v --verbose Report how many servers were affected by the send
|
@@ -99,7 +105,7 @@ that hard and here I provide exhaustive details about configuration files.
|
|
99
105
|
|
100
106
|
(If there is enough interest, in future versions there could be a special
|
101
107
|
command to simplify generation of configuration files. To accelerate the
|
102
|
-
process, open an issue or drop me an email:
|
108
|
+
process, open an issue or drop me an email: ivan@<< username >>.com)
|
103
109
|
|
104
110
|
Let's get to it.
|
105
111
|
|
@@ -115,7 +121,7 @@ UserKnownHostsFile=/dev/null
|
|
115
121
|
Host staging.cache-staging-1
|
116
122
|
Hostname ec2-111-42-111-42.eu-west-1.compute.amazonaws.com
|
117
123
|
Port 4242
|
118
|
-
IdentityFile /Users/
|
124
|
+
IdentityFile /Users/some-user/.ssh/some-pem-file.pem
|
119
125
|
User ubuntu
|
120
126
|
|
121
127
|
Host dev.database-staging-1
|
@@ -145,12 +151,6 @@ Host dev.node-staging-127
|
|
145
151
|
Host dev.node-staging-129
|
146
152
|
<< omitted >>
|
147
153
|
|
148
|
-
Host dev.node-staging-130
|
149
|
-
<< omitted >>
|
150
|
-
|
151
|
-
Host dev.node-staging-135
|
152
|
-
<< omitted >>
|
153
|
-
|
154
154
|
<< ... >>
|
155
155
|
~~~
|
156
156
|
|
@@ -165,7 +165,7 @@ regex-parts-to:
|
|
165
165
|
sort-by: [3]
|
166
166
|
~~~
|
167
167
|
|
168
|
-
And here's a 'real world' configuration file that shows
|
168
|
+
And here's a 'real world' configuration file that shows off all the available
|
169
169
|
options and could be use with previous ssh config file:
|
170
170
|
|
171
171
|
~~~yaml
|
@@ -181,6 +181,11 @@ name:
|
|
181
181
|
merge-groups:
|
182
182
|
misc: ['cache', 'db', 'mongodb']
|
183
183
|
lbs: ['haproxy', 'nginx']
|
184
|
+
multiple-hosts:
|
185
|
+
regexes:
|
186
|
+
- !ruby-regexp '(nginx|haproxy)-'
|
187
|
+
- !ruby-regexp '(db)-'
|
188
|
+
counts: [2, 3]
|
184
189
|
layout:
|
185
190
|
default:
|
186
191
|
custom:
|
@@ -262,6 +267,25 @@ In this example two different kinds of loadbalancers are grouped together.
|
|
262
267
|
Note that the servers from merge groups can later be referenced with both
|
263
268
|
original and merge-group name.
|
264
269
|
|
270
|
+
* * *
|
271
|
+
(optional) field __'multiple-hosts'__ contains __'regexes'__ and __'counts'__
|
272
|
+
fields. With those, some hosts can have multiple connections established, not
|
273
|
+
just the default one connection per host.
|
274
|
+
|
275
|
+
For example:
|
276
|
+
~~~yaml
|
277
|
+
multiple-hosts:
|
278
|
+
regexes:
|
279
|
+
- !ruby-regexp '(nginx|haproxy)-'
|
280
|
+
- !ruby-regexp '(db)-'
|
281
|
+
counts: [2, 3]
|
282
|
+
~~~
|
283
|
+
creates 2 connections for each of nginx or haproxy nodes, as well as 3
|
284
|
+
connection for db nodes.
|
285
|
+
|
286
|
+
Fields __'regexes'__ and __'counts'__ must have the same number of elements.
|
287
|
+
Each element in __'regexes'__ must contain valid ruby regex.
|
288
|
+
|
265
289
|
* * *
|
266
290
|
Finally, what's left is the (optional) __layout__ definition:
|
267
291
|
|
@@ -284,6 +308,7 @@ The layouts are applied individually to any merge group and to any normal
|
|
284
308
|
a group then layout allows on a single window, next window for that group is
|
285
309
|
added. Servers from different groups never share a window.
|
286
310
|
|
311
|
+
|
287
312
|
## Requirements
|
288
313
|
To be able to use the gem you should have ruby 1.9+ and tmux installed on a *nix
|
289
314
|
(Mac OS X, Linux, ...) machine. (Windows: here be dragons)
|
@@ -351,7 +376,7 @@ information, check my [dotfiles].
|
|
351
376
|
4. Push to the branch (`git push origin my-new-feature`)
|
352
377
|
5. Create new Pull Request
|
353
378
|
|
354
|
-
Or just mail me, mail:
|
379
|
+
Or just mail me, mail: ivan@<< username >>.com
|
355
380
|
|
356
381
|
This is my first real gem, so all your comments are more than welcome.
|
357
382
|
I'd really appreciate ruby code improvements/refactoring comments or usability
|
@@ -363,7 +388,7 @@ comments (all other are welcome too). Just _drop me a line_. :)
|
|
363
388
|
Take a look at `TODO.md` file (in the repository) for ideas about additional
|
364
389
|
features in new versions.
|
365
390
|
|
366
|
-
|
391
|
+
ivan@<< username >>.com
|
367
392
|
|
368
393
|
I'd be happy to hear from you.
|
369
394
|
|
@@ -10,6 +10,7 @@ module TmuxConnector
|
|
10
10
|
attr_reader :server_filter
|
11
11
|
attr_reader :session
|
12
12
|
attr_reader :verbose
|
13
|
+
attr_reader :window
|
13
14
|
|
14
15
|
def initialize(args)
|
15
16
|
@name = args['<session-name>']
|
@@ -23,11 +24,13 @@ module TmuxConnector
|
|
23
24
|
@server_filter ||= Regexp.new(args['--filter']) rescue nil
|
24
25
|
@group_filter ||= Regexp.new(args['--filter']) rescue nil
|
25
26
|
|
27
|
+
@window = args['--window']
|
28
|
+
|
26
29
|
@session = Session.load_by_name args['<session-name>']
|
27
30
|
end
|
28
31
|
|
29
32
|
def run()
|
30
|
-
session.tmux_session.send_commands(commands, server_filter, group_filter, verbose)
|
33
|
+
session.tmux_session.send_commands(commands, server_filter, group_filter, window, verbose)
|
31
34
|
end
|
32
35
|
|
33
36
|
private
|
data/lib/tmux-connector/host.rb
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
module TmuxConnector
|
2
2
|
class Host
|
3
|
-
attr_reader :
|
3
|
+
attr_reader :count
|
4
4
|
attr_reader :display_name
|
5
5
|
attr_reader :group_id
|
6
6
|
attr_reader :sort_value
|
7
|
+
attr_reader :ssh_name
|
7
8
|
|
8
9
|
def initialize(name, config)
|
9
10
|
@ssh_name = name
|
@@ -12,6 +13,8 @@ module TmuxConnector
|
|
12
13
|
@display_name = create_display_name groups, config
|
13
14
|
@sort_value = config['regex-parts-to']['sort-by'].map { |i| groups[i] }.join '-'
|
14
15
|
@group_id = config['regex-parts-to']['group-by'].map { |i| groups[i] }.join '-'
|
16
|
+
|
17
|
+
@count = get_count config
|
15
18
|
end
|
16
19
|
|
17
20
|
def to_s()
|
@@ -20,7 +23,7 @@ module TmuxConnector
|
|
20
23
|
|
21
24
|
private
|
22
25
|
|
23
|
-
def create_display_name
|
26
|
+
def create_display_name(groups, config)
|
24
27
|
if config['name']
|
25
28
|
parts = []
|
26
29
|
groups.each_with_index do |e, i|
|
@@ -32,5 +35,16 @@ module TmuxConnector
|
|
32
35
|
|
33
36
|
return @ssh_name
|
34
37
|
end
|
38
|
+
|
39
|
+
def get_count(config)
|
40
|
+
multiple = config['multiple-hosts']
|
41
|
+
return 1 if multiple.nil?
|
42
|
+
|
43
|
+
[ multiple['regexes'], multiple['counts'] ].transpose.each do |re, n|
|
44
|
+
return n if ssh_name.match re
|
45
|
+
end
|
46
|
+
|
47
|
+
return 1
|
48
|
+
end
|
35
49
|
end
|
36
50
|
end
|
@@ -1,4 +1,18 @@
|
|
1
1
|
module TmuxConnector
|
2
|
+
class Pane
|
3
|
+
attr_reader :host
|
4
|
+
attr_reader :name
|
5
|
+
attr_reader :ordinal
|
6
|
+
|
7
|
+
def initialize(host, ordinal)
|
8
|
+
@host = host
|
9
|
+
@ordinal = ordinal
|
10
|
+
|
11
|
+
@name = host.display_name
|
12
|
+
@name += "##{ ordinal }" if ordinal > 1
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
2
16
|
class Layout
|
3
17
|
attr_reader :groups
|
4
18
|
attr_reader :merge_rules
|
@@ -46,7 +60,12 @@ module TmuxConnector
|
|
46
60
|
n = config['tmux']['max-panes']
|
47
61
|
end
|
48
62
|
|
49
|
-
hosts.
|
63
|
+
panes = hosts.reduce([]) do |acc, h|
|
64
|
+
h.count.times { |i| acc << Pane.new(h, i + 1) }
|
65
|
+
acc
|
66
|
+
end
|
67
|
+
|
68
|
+
panes.each_slice(n).with_index do |arr, i|
|
50
69
|
window = {
|
51
70
|
name: "#{ group_name }##{ i + 1 }",
|
52
71
|
group_name: group_name,
|
@@ -36,12 +36,16 @@ module TmuxConnector
|
|
36
36
|
execute
|
37
37
|
end
|
38
38
|
|
39
|
-
def send_commands(send_commands, server_regex, group_regex, verbose)
|
39
|
+
def send_commands(send_commands, server_regex, group_regex, window, verbose)
|
40
40
|
count = 0
|
41
|
-
each_pane do |window_index, pane_index,
|
42
|
-
|
43
|
-
|
44
|
-
|
41
|
+
each_pane do |window_index, pane_index, pane|
|
42
|
+
if window
|
43
|
+
matches = window == window_index.to_s
|
44
|
+
else
|
45
|
+
matches = server_regex.nil? && group_regex.nil?
|
46
|
+
matches ||= !server_regex.nil? && pane.host.ssh_name.match(server_regex)
|
47
|
+
matches ||= !group_regex.nil? && session.merge_rules[pane.host.group_id].match(group_regex)
|
48
|
+
end
|
45
49
|
|
46
50
|
if matches
|
47
51
|
system("tmux send-keys -t #{ name }:#{ window_index }.#{ pane_index } '#{ send_commands }' C-m")
|
@@ -88,7 +92,7 @@ HERE
|
|
88
92
|
size = (100.0 * (w[:panes].size - pi - 1) / (w[:panes].size - pi)).round
|
89
93
|
|
90
94
|
commands << "tmux split-window -p #{ size } -t #{ name }:#{ wi }" unless pi == 0
|
91
|
-
commands << tmux_set_title_cmd(p.
|
95
|
+
commands << tmux_set_title_cmd(p.name, wi, pi)
|
92
96
|
end
|
93
97
|
|
94
98
|
commands << "tmux select-layout -t #{ name }:#{ wi } #{ w[:tmux] } &> /dev/null"
|
@@ -101,7 +105,7 @@ HERE
|
|
101
105
|
end
|
102
106
|
|
103
107
|
def clear_panes()
|
104
|
-
each_pane do |window_index, pane_index|
|
108
|
+
each_pane do |window_index, pane_index, _|
|
105
109
|
commands << "tmux send-keys -t #{ name }:#{ window_index }.#{ pane_index } clear C-m"
|
106
110
|
end
|
107
111
|
end
|
@@ -109,8 +113,8 @@ HERE
|
|
109
113
|
def connect()
|
110
114
|
ssh_config_path = File.expand_path session.args['--ssh-config']
|
111
115
|
|
112
|
-
each_pane do |window_index, pane_index,
|
113
|
-
ssh_command = "ssh -F #{ ssh_config_path } #{ host.ssh_name }"
|
116
|
+
each_pane do |window_index, pane_index, pane|
|
117
|
+
ssh_command = "ssh -F #{ ssh_config_path } #{ pane.host.ssh_name }"
|
114
118
|
commands << "tmux send-keys -t #{ name }:#{ window_index }.#{ pane_index } '#{ ssh_command }' C-m"
|
115
119
|
end
|
116
120
|
end
|
@@ -126,14 +130,14 @@ HERE
|
|
126
130
|
def each_pane(&block)
|
127
131
|
session.windows.each_with_index do |window, window_index|
|
128
132
|
if window[:tmux]
|
129
|
-
window[:panes].each_with_index do |
|
130
|
-
yield(window_index, pane_index,
|
133
|
+
window[:panes].each_with_index do |pane, pane_index|
|
134
|
+
yield(window_index, pane_index, pane)
|
131
135
|
end
|
132
136
|
else
|
133
137
|
pane_index = 0
|
134
138
|
window[:panes].each do |g|
|
135
|
-
g.each do |
|
136
|
-
yield(window_index, pane_index,
|
139
|
+
g.each do |pane|
|
140
|
+
yield(window_index, pane_index, pane)
|
137
141
|
pane_index += 1
|
138
142
|
end
|
139
143
|
end
|
@@ -144,25 +148,25 @@ HERE
|
|
144
148
|
def create_custom_layout(window, window_index)
|
145
149
|
direction = (window[:flow] == 'horizontal') ? ['-h', '-v'] : ['-v', '-h']
|
146
150
|
|
147
|
-
|
151
|
+
in_window_index = 0
|
148
152
|
window[:panes].each_with_index do |group, group_index|
|
149
|
-
commands << "tmux select-pane -t #{ name }:#{ window_index }.#{
|
153
|
+
commands << "tmux select-pane -t #{ name }:#{ window_index }.#{ in_window_index }"
|
150
154
|
|
151
|
-
# create pane in a next row ahead of time so pane indexes match
|
155
|
+
# create tmux-pane in a next row ahead of time so tmux-pane indexes match host-panes
|
152
156
|
if group_index < window[:panes].size - 1
|
153
157
|
size = (100.0 * (window[:panes].size - group_index - 1) / (window[:panes].size - group_index)).round
|
154
158
|
commands << "tmux split-window #{ direction[1] } -p #{ size } -t #{ name }:#{ window_index }"
|
155
|
-
|
156
|
-
commands << tmux_set_title_cmd(
|
157
|
-
commands << "tmux select-pane -t #{ name }:#{ window_index }.#{
|
159
|
+
pane_name = window[:panes][group_index + 1][0].name
|
160
|
+
commands << tmux_set_title_cmd(pane_name, window_index, -1)
|
161
|
+
commands << "tmux select-pane -t #{ name }:#{ window_index }.#{ in_window_index }"
|
158
162
|
end
|
159
163
|
|
160
|
-
group.each_with_index do |
|
161
|
-
size = (100.0 * (group.size -
|
162
|
-
commands << "tmux split-window #{ direction[0] } -p #{ size } -t #{ name }:#{ window_index }" unless
|
163
|
-
commands << tmux_set_title_cmd(
|
164
|
+
group.each_with_index do |pane, pane_index|
|
165
|
+
size = (100.0 * (group.size - pane_index) / (group.size - pane_index + 1)).round
|
166
|
+
commands << "tmux split-window #{ direction[0] } -p #{ size } -t #{ name }:#{ window_index }" unless pane_index == 0
|
167
|
+
commands << tmux_set_title_cmd(pane.name, window_index, in_window_index)
|
164
168
|
|
165
|
-
|
169
|
+
in_window_index += 1
|
166
170
|
end
|
167
171
|
end
|
168
172
|
end
|
data/lib/tmux-connector.rb
CHANGED
@@ -18,8 +18,9 @@ Usage:
|
|
18
18
|
tcon delete (<session-name> | --all)
|
19
19
|
tcon list
|
20
20
|
tcon send <session-name> (<command> | --command-file=<file>)
|
21
|
-
[--server-filter=<regex>
|
22
|
-
|
21
|
+
[ --server-filter=<regex> | --group-filter=<regex>
|
22
|
+
| --filter=<regex> | --window=<index> ]
|
23
|
+
[--verbose]
|
23
24
|
tcon --help
|
24
25
|
tcon --version
|
25
26
|
|
@@ -29,6 +30,7 @@ Options:
|
|
29
30
|
<session-name> Name that identifies the session. Must be unique.
|
30
31
|
<command> Command to be executed on remote server[s].
|
31
32
|
<regex> String that represents valid Ruby regex.
|
33
|
+
<index> 0-based index.
|
32
34
|
-s --ssh-config=file Path to ssh config file [default: ~/.ssh/config].
|
33
35
|
-n --session-name=name Name of the session to be used in the tcon command.
|
34
36
|
-p --purpose=description Description of session's purpose.
|
@@ -40,6 +42,7 @@ Options:
|
|
40
42
|
-r --filter=regex Filter to select a subset of the servers via
|
41
43
|
host names or group membership.
|
42
44
|
Combines --server-filter and --group-filter.
|
45
|
+
-w --window=index Select a window via (0-based) index.
|
43
46
|
-c --command-file=file File containing the list of commands to be
|
44
47
|
executed on remote server[s].
|
45
48
|
-v --verbose Report how many servers were affected by the send
|
data/spec/config_spec.rb
CHANGED
@@ -46,5 +46,9 @@ describe "Configuration file" do
|
|
46
46
|
it_should_behave_like "config test", 'layout-group'
|
47
47
|
it_should_behave_like "config test", 'layout-both'
|
48
48
|
end
|
49
|
+
|
50
|
+
describe "multiple hosts" do
|
51
|
+
it_should_behave_like "config test", 'multiple-hosts'
|
52
|
+
end
|
49
53
|
end
|
50
54
|
end
|
data/spec/fixtures/configs.yml
CHANGED
@@ -113,3 +113,19 @@ layout-both:
|
|
113
113
|
max-horizontal: 2
|
114
114
|
max-vertical: 2
|
115
115
|
panes-flow: vertical
|
116
|
+
|
117
|
+
multiple-hosts:
|
118
|
+
input:
|
119
|
+
<<: *input-min
|
120
|
+
multiple-hosts:
|
121
|
+
regexes:
|
122
|
+
- !ruby-regexp '(loadbalancer)-'
|
123
|
+
- !ruby-regexp '(database)-'
|
124
|
+
counts: [2, 3]
|
125
|
+
expected:
|
126
|
+
<<: *expected-min
|
127
|
+
multiple-hosts:
|
128
|
+
regexes:
|
129
|
+
- !ruby-regexp '(loadbalancer)-'
|
130
|
+
- !ruby-regexp '(database)-'
|
131
|
+
counts: [2, 3]
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: tmux-connector
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.8.5
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-06-
|
12
|
+
date: 2013-06-08 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: docopt
|