tmux-connector 0.8.6 → 0.9.7

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -11,7 +11,7 @@ Manage multiple servers using SSH and [tmux].
11
11
  * multiple connections to individual servers possible
12
12
  * sessions can be persisted (actually recreated) after computer restarts
13
13
  - they are lost only if you delete them explicitly
14
- * panes not associated with hosts
14
+ * panes not associated with hosts available
15
15
 
16
16
 
17
17
  ## Quick tease
@@ -28,20 +28,24 @@ Manage multiple servers using SSH and [tmux].
28
28
 
29
29
  - send to all servers (in 'staging' session) `sudo su` command
30
30
 
31
- `tcon send staging 'top' -g 'lbs'`
31
+ `tcon send staging -v -g 'lbs' 'top'`
32
32
 
33
- - send `top` command to all loadbalancing nodes in 'staging' session
33
+ - send `top` command to all loadbalancing nodes in 'staging' session and report
34
+ the number of affected nodes
34
35
 
35
- `tcon send production 'tail -f /var/log/syslog' -f 'rdb'`
36
+ `tcon send production -f 'worker' 'tail -f /var/log/syslog'`
36
37
 
37
- - send `tail -f /var/log/syslog` command to all database nodes in 'production'
38
+ - send `tail -f /var/log/syslog` command to all worker nodes in 'production'
38
39
  session
39
40
 
40
- `tcon send production -f 'rdb' 'C-c'`
41
+ `tcon send production -f 'worker :: <,3]; 7; <9,13>' 'C-c'`
41
42
 
42
- - send `Ctrl-C` to all database nodes in 'production' session (if executed
43
+ - send `Ctrl-C` to some worker nodes in 'production' session, if executed
43
44
  after the `tail -f /var/log/syslog` command, it will exit the `tail -f`
44
45
  command
46
+ - if there are worker nodes numbered from 1 to 20 (e.g. staging-worker-1,
47
+ staging-worker-2, etc.) this command affects worker nodes: 1, 2, 3, 7, 10,
48
+ 11 and 12
45
49
 
46
50
  `tcon resume s#3`
47
51
 
@@ -65,7 +69,7 @@ Usage:
65
69
  tcon delete (<session-name> | --all)
66
70
  tcon list
67
71
  tcon send <session-name> (<command> | --command-file=<file>)
68
- [ --server-filter=<regex> | --group-filter=<regex>
72
+ [ --server-filter=<filter> | --group-filter=<regex>
69
73
  | --filter=<regex> | --window=<index> ]
70
74
  [--verbose]
71
75
  tcon --help
@@ -78,11 +82,14 @@ Options:
78
82
  <command> Command to be executed on remote server[s].
79
83
  <regex> String that represents valid Ruby regex.
80
84
  <index> 0-based index.
85
+ <filter> Filter consisting of a valid ruby regex and
86
+ optionally of a special predicate.
87
+ For more information see README file.
81
88
  -s --ssh-config=file Path to ssh config file [default: ~/.ssh/config].
82
89
  -n --session-name=name Name of the session to be used in the tcon command.
83
90
  -p --purpose=description Description of session's purpose.
84
91
  --all Delete all existing sessions.
85
- -f --server-filter=regex Filter to select a subset of the servers via
92
+ -f --server-filter=filter Filter to select a subset of the servers via
86
93
  host names.
87
94
  -g --group-filter=regex Filter to select a subset of the servers via
88
95
  group membership.
@@ -98,6 +105,62 @@ Options:
98
105
  --version Show version.
99
106
  ~~~
100
107
 
108
+ ### Send command
109
+
110
+ Send command sends user specified command(s) to chosen panes.
111
+
112
+ Once more, here's the command syntax:
113
+ ~~~
114
+ tcon send <session-name> (<command> | --command-file=<file>)
115
+ [ --server-filter=<filter> | --group-filter=<regex>
116
+ | --filter=<regex> | --window=<index> ]
117
+ [--verbose]
118
+ ~~~
119
+ where long flags can be shortened to: `-c`, `-f`, `-g`, `-r`, `-w` and `-v`.
120
+
121
+ There are 4 flags used to filter the servers (panes) that are going to receive
122
+ the command(s):
123
+
124
+ * `-f` filters the servers via host names
125
+ - accept valid ruby regex and optionally intervals specification
126
+ - syntax: `<ruby-regex>` or `<ruby-regex> :: <intervals-specification>`
127
+ * `-g` filters the servers via group names
128
+ - accepts valid ruby regex
129
+ * `-r` filters the servers via host names or group names
130
+ - accepts valid ruby regex
131
+ * `-w` filters the servers that belong to particular layout window
132
+ - accepts 0-based index
133
+
134
+ The `-f` filter can accept optional interval specification:
135
+ * interval specification operates on __'sort-by'__ part of host names (defined in
136
+ configuration file)
137
+ - intervals specification consists of one or more elements separated
138
+ by semicollon (the split regex is: `/;\s*/`)
139
+ - individual element is either an interval description or white-listed element
140
+ - interval description consists of 4 parts:
141
+ + '[' or '<' - include or exclude the first element
142
+ + first element (can be empty)
143
+ + ',' (can have trailing whitespace)
144
+ + second element (can be empty)
145
+ + ']' or '>' - include or exclude the last element
146
+ - regex used:
147
+ `/(?<start>[\[<])(?<first>[^,]*),\s*(?<second>[^\]>]*)(?<end>[\]>])/`
148
+ - examples:
149
+ + `2; 4` - 2, 4
150
+ + `[1,3]` - 1, 2, 3
151
+ + `[1, 3>` - 1, 2
152
+ + `<1, 3>` - 2
153
+ + `<,5>` - ... 3, 4
154
+ + `[5, >` - 5, 6, ...
155
+
156
+ Hopefully now the following examples make more sense:
157
+
158
+ ~~~bash
159
+ tcon send staging 'sudo su'
160
+ tcon send staging -v -g 'lbs' 'top'
161
+ tcon send production -f 'worker' 'tail -f /var/log/syslog'
162
+ tcon send production -f 'worker :: <,3]; 7; <9,13>' 'C-c'
163
+ ~~~
101
164
 
102
165
  ## Configuration
103
166
 
@@ -18,7 +18,7 @@ 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> | --group-filter=<regex>
21
+ [ --server-filter=<filter> | --group-filter=<regex>
22
22
  | --filter=<regex> | --window=<index> ]
23
23
  [--verbose]
24
24
  tcon --help
@@ -31,11 +31,14 @@ Options:
31
31
  <command> Command to be executed on remote server[s].
32
32
  <regex> String that represents valid Ruby regex.
33
33
  <index> 0-based index.
34
+ <filter> Filter consisting of a valid ruby regex and
35
+ optionally of a special predicate.
36
+ For more information see README file.
34
37
  -s --ssh-config=file Path to ssh config file [default: ~/.ssh/config].
35
38
  -n --session-name=name Name of the session to be used in the tcon command.
36
39
  -p --purpose=description Description of session's purpose.
37
40
  --all Delete all existing sessions.
38
- -f --server-filter=regex Filter to select a subset of the servers via
41
+ -f --server-filter=filter Filter to select a subset of the servers via
39
42
  host names.
40
43
  -g --group-filter=regex Filter to select a subset of the servers via
41
44
  group membership.
@@ -5,6 +5,7 @@ require_relative '../tmux_handler'
5
5
  module TmuxConnector
6
6
  class Send
7
7
  attr_reader :commands
8
+ attr_reader :filter_predicate
8
9
  attr_reader :group_filter
9
10
  attr_reader :name
10
11
  attr_reader :server_filter
@@ -18,7 +19,8 @@ module TmuxConnector
18
19
 
19
20
  load_commands args
20
21
 
21
- @server_filter = Regexp.new(args['--server-filter']) rescue nil
22
+ process_server_filter args['--server-filter'] rescue raise "error parsing server filter ('#{ args['--server-filter'] }')"
23
+
22
24
  @group_filter = Regexp.new(args['--group-filter']) rescue nil
23
25
 
24
26
  @server_filter ||= Regexp.new(args['--filter']) rescue nil
@@ -30,11 +32,16 @@ module TmuxConnector
30
32
  end
31
33
 
32
34
  def run()
33
- session.tmux_session.send_commands(commands, server_filter, group_filter, window, verbose)
35
+ opts = {
36
+ verbose: verbose,
37
+ filter_predicate: filter_predicate
38
+ }
39
+
40
+ session.tmux_session.send_commands(commands, server_filter, group_filter, window, opts)
34
41
  end
35
42
 
36
43
  private
37
-
44
+
38
45
  def load_commands(args)
39
46
  if args['<command>']
40
47
  @commands = args['<command>']
@@ -44,5 +51,74 @@ module TmuxConnector
44
51
  @commands = open(file) { |f| f.read }
45
52
  end
46
53
  end
54
+
55
+ def process_server_filter(raw_filter)
56
+ return unless raw_filter
57
+
58
+ str_filter, str_predicate = raw_filter.split(' :: ')
59
+
60
+ @server_filter = (Regexp.new(str_filter) rescue nil)
61
+
62
+ return if str_predicate.nil?
63
+
64
+ predicate_parts = parse_predicate(str_predicate)
65
+ @filter_predicate = build_predicate(predicate_parts)
66
+ end
67
+
68
+ def parse_predicate(str_predicate)
69
+ return nil if str_predicate.nil?
70
+
71
+ predicate_delimiter = /;\s*/
72
+ interval_regex = /(?<start>[\[<])(?<first>[^,]*),\s*(?<second>[^\]>]*)(?<end>[\]>])/
73
+
74
+ return str_predicate.split(predicate_delimiter).map do |element|
75
+ m = element.match(interval_regex)
76
+ Hash[ m.names.map(&:to_sym).zip(m.captures) ] rescue element
77
+ end
78
+ end
79
+
80
+ def build_predicate(parts)
81
+ return lambda do |sort_value|
82
+ return false if sort_value.nil?
83
+
84
+ numeric_comparison = sort_value.instance_of? Fixnum
85
+
86
+ intervals, elements = parts.partition { |e| e.instance_of? Hash }
87
+
88
+ elements = elements.map(&:to_i) if numeric_comparison
89
+ return true if elements.include? sort_value
90
+
91
+ intervals.each do |interval|
92
+ first = interval[:first]
93
+ second = interval[:second]
94
+
95
+ matches = true
96
+
97
+ unless first.empty?
98
+ first = Integer(first, 10) if numeric_comparison
99
+
100
+ if interval[:start] == '['
101
+ matches &&= sort_value >= first
102
+ elsif interval[:start] == '<'
103
+ matches &&= sort_value > first
104
+ end
105
+ end
106
+
107
+ unless second.empty?
108
+ second = Integer(second, 10) if numeric_comparison
109
+
110
+ if interval[:end] == ']'
111
+ matches &&= sort_value <= second
112
+ elsif interval[:end] == '>'
113
+ matches &&= sort_value < second
114
+ end
115
+ end
116
+
117
+ return true if matches
118
+ end
119
+
120
+ return false
121
+ end
122
+ end
47
123
  end
48
124
  end
@@ -22,7 +22,6 @@ module TmuxConnector
22
22
  end
23
23
  raise "no hosts matching given configuration found, check your configuration file" if hosts.empty?
24
24
 
25
-
26
25
  generate_groups
27
26
  generate_merge_rules
28
27
 
@@ -47,7 +46,21 @@ module TmuxConnector
47
46
  @groups.merge! Hash[hostless.map { |name, count| [ name, [FakeHost.new(name, count)] ] }]
48
47
  end
49
48
 
50
- sort_groups!
49
+ update_sort_values!
50
+
51
+ groups.each do |_, hosts|
52
+ hosts.sort_by!(&:sort_value)
53
+ end
54
+ end
55
+
56
+ def update_sort_values!()
57
+ groups.each do |_, hosts|
58
+ numbers_only = hosts.all? { |e| e.sort_value =~ /^[-+]?[0-9]+$/ }
59
+
60
+ if numbers_only
61
+ hosts.each { |h| h.sort_value = Integer(h.sort_value, 10) }
62
+ end
63
+ end
51
64
  end
52
65
 
53
66
  def generate_merge_rules()
@@ -59,16 +72,5 @@ module TmuxConnector
59
72
  end
60
73
  groups.keys.each { |e| @merge_rules[e] ||= e }
61
74
  end
62
-
63
- def sort_groups!()
64
- groups.each do |k, v|
65
- numbers_only = v.all? { |e| e.sort_value =~ /^[-+]?[0-9]+$/ }
66
- if numbers_only
67
- v.sort! { |a, b| a.sort_value.to_i <=> b.sort_value.to_i }
68
- else
69
- v.sort_by!(&:sort_value)
70
- end
71
- end
72
- end
73
75
  end
74
76
  end
@@ -1,9 +1,10 @@
1
1
  module TmuxConnector
2
2
  class Host
3
+ attr_accessor :sort_value
4
+
3
5
  attr_reader :count
4
6
  attr_reader :display_name
5
7
  attr_reader :group_id
6
- attr_reader :sort_value
7
8
  attr_reader :ssh_name
8
9
 
9
10
  def initialize(name, config)
@@ -36,16 +36,31 @@ module TmuxConnector
36
36
  execute
37
37
  end
38
38
 
39
- def send_commands(send_commands, server_regex, group_regex, window, verbose)
39
+ def send_commands(send_commands, server_regex, group_regex, window, opts={})
40
+ predicate = opts[:filter_predicate]
41
+
40
42
  count = 0
41
43
  each_pane do |window_index, pane_index, pane|
42
44
  if window
43
45
  matches = window == window_index.to_s
44
46
  else
45
47
  matches = server_regex.nil? && group_regex.nil?
46
- matches ||= !server_regex.nil? && pane.host.ssh_name.match(server_regex)
47
48
  matches ||= !group_regex.nil? && pane.host.group_id.match(group_regex)
48
49
  matches ||= !group_regex.nil? && session.merge_rules[pane.host.group_id].match(group_regex)
50
+
51
+ unless server_regex.nil?
52
+ name_match = pane.host.ssh_name.match(server_regex)
53
+
54
+ if predicate
55
+ begin
56
+ matches ||= name_match && predicate.call(pane.host.sort_value)
57
+ rescue
58
+ raise "error while using send predicate for '#{ pane.host.display_name }'; command already sent to #{ count } servers"
59
+ end
60
+ else
61
+ matches ||= name_match
62
+ end
63
+ end
49
64
  end
50
65
 
51
66
  if matches
@@ -54,7 +69,7 @@ module TmuxConnector
54
69
  end
55
70
  end
56
71
 
57
- puts "command sent to #{ count } server[s]" if verbose
72
+ puts "command sent to #{ count } server[s]" if opts[:verbose]
58
73
  end
59
74
 
60
75
  private
@@ -115,7 +130,7 @@ HERE
115
130
  ssh_config_path = File.expand_path session.args['--ssh-config']
116
131
 
117
132
  each_pane do |window_index, pane_index, pane|
118
- next unless pane.host.instance_of? TmuxConnector::Host
133
+ next unless pane.host.instance_of? Host
119
134
 
120
135
  ssh_command = "ssh -F #{ ssh_config_path } #{ pane.host.ssh_name }"
121
136
  commands << "tmux send-keys -t #{ name }:#{ window_index }.#{ pane_index } '#{ ssh_command }' C-m"
@@ -1,3 +1,3 @@
1
1
  module TmuxConnector
2
- VERSION = "0.8.6"
2
+ VERSION = "0.9.7"
3
3
  end
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.8.6
4
+ version: 0.9.7
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-15 00:00:00.000000000 Z
12
+ date: 2013-06-17 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: docopt