tmux-connector 0.8.6 → 0.9.7

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 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