hotdog 0.17.1 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: bd0467d9928ac5366eec849cb7def564dd0d8ae3
4
- data.tar.gz: 9588371163cbb077511b8b094360bb4ac4c7573d
3
+ metadata.gz: a32689688c7824af117417f98913a377e5f8e273
4
+ data.tar.gz: e814b609418012c7192cd8ada949a12392651288
5
5
  SHA512:
6
- metadata.gz: 4f7f359862f6d3b7b79a89d03b04406e3a4efb07655084f9526346c1db88601b45252918a910e5c2081bafff8b8ef334d9555e37b12c2db73915dda1a2c1b748
7
- data.tar.gz: cd802bb419145c53342d0409aa1623e376b7577aaa09be595f2474c13bae62a2b96470d258839b0e9c12f69c002dda5b7e98204c1b81b9a75342fc30a337663c
6
+ metadata.gz: 7b02bd4a8dc956cbcb74cf6854d6a540c197b2d4e96215b201aa4a06348d5ff6c9f3747edf4751ddda299f15b9400ebced4dd7ab11a498c7017d9a81d31a49ff
7
+ data.tar.gz: 062f80089aaa87daf0310578a728cceee13d9ad94d475fbb4be604553414831d42f0a8065a4a97896ed8a17d3f6d000f135948237cc8677d6a870d7ea6d7c5cf
@@ -25,7 +25,7 @@ module Hotdog
25
25
  application: self,
26
26
  confdir: find_confdir(File.expand_path(".")),
27
27
  debug: false,
28
- expiry: 1800,
28
+ expiry: 3600,
29
29
  fixed_string: false,
30
30
  force: false,
31
31
  format: "plain",
@@ -24,22 +24,29 @@ module Hotdog
24
24
  end
25
25
 
26
26
  def run(args=[], options={})
27
- args.each do |arg|
27
+ scopes = args.map { |arg|
28
28
  if arg.index(":").nil?
29
- scope = "host:#{arg}"
29
+ "host:#{arg}"
30
30
  else
31
- scope = arg
31
+ arg
32
32
  end
33
+ }
34
+ scopes.each do |scope|
33
35
  with_retry(options) do
34
36
  schedule_downtime(scope, options)
35
37
  end
36
38
  end
37
-
38
- # Remove persistent.db to schedule update on next invocation
39
- if @db
40
- close_db(@db)
39
+ hosts = scopes.select { |scope| scope.start_with?("host:") }.map { |scope|
40
+ scope.slice("host:".length, scope.length)
41
+ }
42
+ if 0 < hosts.length
43
+ if open_db
44
+ hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT) do |hosts|
45
+ execute_db(@db, "DELETE FROM hosts_tags WHERE host_id IN ( SELECT id FROM hosts WHERE name IN (%s) )" % hosts.map { "?" }.join(", "), hosts)
46
+ execute_db(@db, "DELETE FROM hosts WHERE name IN (%s)" % hosts.map { "?" }.join(", "), hosts)
47
+ end
48
+ end
41
49
  end
42
- FileUtils.rm_f(File.join(options[:confdir], PERSISTENT_DB))
43
50
  end
44
51
 
45
52
  private
@@ -34,15 +34,24 @@ module Hotdog
34
34
  infile.seek(0)
35
35
  end
36
36
  begin
37
- stats = Parallel.map(hosts.each_with_index.to_a, in_threads: parallelism(hosts)) { |host, i|
38
- cmdline = build_command_string(host, @remote_command, options)
39
- identifier = options[:show_identifier] ? host : nil
40
- success = exec_command(identifier, cmdline, index: i, output: true, infile: (infile ? infile.path : nil))
41
- if !success && options[:stop_on_error]
42
- raise StopException.new
43
- end
44
- success
37
+ hosts_cmdlines = hosts.map { |host|
38
+ [host, build_command_string(host, @remote_command, options)]
45
39
  }
40
+ if options[:dry_run]
41
+ stats = hosts_cmdlines.map { |host, cmdline|
42
+ STDOUT.puts(cmdline)
43
+ true
44
+ }
45
+ else
46
+ stats = Parallel.map(hosts_cmdlines.each_with_index.to_a, in_threads: parallelism(hosts)) { |(host, cmdline), i|
47
+ identifier = options[:show_identifier] ? host : nil
48
+ success = exec_command(identifier, cmdline, index: i, output: true, infile: (infile ? infile.path : nil))
49
+ if !success && options[:stop_on_error]
50
+ raise StopException.new
51
+ end
52
+ success
53
+ }
54
+ end
46
55
  if stats.all?
47
56
  exit(0)
48
57
  else
@@ -13,32 +13,6 @@ module Hotdog
13
13
  cmdline = ["scp"] + build_command_options(options) + Shellwords.split(command).map { |token| token.gsub(/@(?=:)/, host) }
14
14
  Shellwords.join(cmdline)
15
15
  end
16
-
17
- def build_command_options(options={})
18
- arguments = []
19
- if options[:forward_agent]
20
- # nop
21
- end
22
- if options[:ssh_config]
23
- cmdline << "-F" << File.expand_path(options[:ssh_config])
24
- end
25
- if options[:identity_file]
26
- arguments << "-i" << options[:identity_file]
27
- end
28
- if options[:user]
29
- arguments << "-o" << "User=#{options[:user]}"
30
- end
31
- if options[:options]
32
- arguments += options[:options].flat_map { |option| ["-o", option] }
33
- end
34
- if options[:port]
35
- arguments << "-P" << options[:port]
36
- end
37
- if options[:verbose]
38
- arguments << "-v"
39
- end
40
- arguments
41
- end
42
16
  end
43
17
  end
44
18
  end
@@ -36,11 +36,7 @@ module Hotdog
36
36
  if @remote_command
37
37
  logger.warn("ignore remote command: #{@remote_command}")
38
38
  end
39
- expression = args.join(" ").strip
40
- if expression.empty?
41
- # return everything if given expression is empty
42
- expression = "*"
43
- end
39
+ expression = rewrite_expression(args.join(" ").strip)
44
40
 
45
41
  begin
46
42
  node = parse(expression)
@@ -114,6 +110,15 @@ module Hotdog
114
110
  raise("parser error: unknown expression: #{node.inspect}")
115
111
  end
116
112
  end
113
+
114
+ private
115
+ def rewrite_expression(expression)
116
+ if expression.empty?
117
+ # return everything if given expression is empty
118
+ expression = "*"
119
+ end
120
+ expression
121
+ end
117
122
  end
118
123
  end
119
124
  end
@@ -15,32 +15,6 @@ module Hotdog
15
15
  end
16
16
  Shellwords.join(cmdline)
17
17
  end
18
-
19
- def build_command_options(options={})
20
- arguments = []
21
- if options[:forward_agent]
22
- # nop
23
- end
24
- if options[:ssh_config]
25
- cmdline << "-F" << File.expand_path(options[:ssh_config])
26
- end
27
- if options[:identity_file]
28
- arguments << "-i" << options[:identity_file]
29
- end
30
- if options[:user]
31
- arguments << "-o" << "User=#{options[:user]}"
32
- end
33
- if options[:options]
34
- arguments += options[:options].flat_map { |option| ["-o", option] }
35
- end
36
- if options[:port]
37
- arguments << "-P" << options[:port]
38
- end
39
- if options[:verbose]
40
- arguments << "-v"
41
- end
42
- arguments
43
- end
44
18
  end
45
19
  end
46
20
  end
@@ -9,32 +9,35 @@ module Hotdog
9
9
  module Commands
10
10
  class SshAlike < Search
11
11
  def define_options(optparse, options={})
12
- default_option(options, :options, [])
13
- default_option(options, :user, nil)
14
- default_option(options, :port, nil)
15
- default_option(options, :identity_file, nil)
16
- default_option(options, :forward_agent, false)
12
+ default_option(options, :ssh_options, {})
17
13
  default_option(options, :color, :auto)
18
14
  default_option(options, :max_parallelism, Parallel.processor_count)
19
15
  default_option(options, :shuffle, false)
20
16
  default_option(options, :ssh_config, nil)
17
+ optparse.on("-C", "Enable compression.") do |v|
18
+ options[:ssh_options]["Compression"] = "yes"
19
+ end
21
20
  optparse.on("-F SSH_CONFIG", "Specifies an alternative per-user SSH configuration file.") do |configfile|
22
21
  options[:ssh_config] = configfile
23
22
  end
24
- optparse.on("-o SSH_OPTION", "Passes this string to ssh command through shell. This option may be given multiple times") do |option|
25
- options[:options] += [option]
23
+ optparse.on("--dry-run", "Dry run.") do |v|
24
+ options[:dry_run] = v
25
+ end
26
+ optparse.on("-o SSH_OPTION", "Passes this string to ssh command through shell. This option may be given multiple times") do |ssh_option|
27
+ ssh_option_key, ssh_option_value = ssh_option.split("=", 2)
28
+ options[:ssh_options][ssh_option_key] = ssh_option_value
26
29
  end
27
30
  optparse.on("-i SSH_IDENTITY_FILE", "SSH identity file path") do |path|
28
- options[:identity_file] = path
31
+ options[:ssh_options]["IdentityFile"] = path
29
32
  end
30
33
  optparse.on("-A", "Enable agent forwarding", TrueClass) do |b|
31
- options[:forward_agent] = b
34
+ options[:ssh_options]["ForwardAgent"] = "yes"
32
35
  end
33
36
  optparse.on("-p PORT", "Port of the remote host", Integer) do |port|
34
- options[:port] = port
37
+ options[:ssh_options]["Port"] = port
35
38
  end
36
39
  optparse.on("-u SSH_USER", "SSH login user name") do |user|
37
- options[:user] = user
40
+ options[:ssh_options]["User"] = user
38
41
  end
39
42
  optparse.on("-v", "--verbose", "Enable verbose ode") do |v|
40
43
  options[:verbose] = v
@@ -54,11 +57,7 @@ module Hotdog
54
57
  end
55
58
 
56
59
  def run(args=[], options={})
57
- expression = args.join(" ").strip
58
- if expression.empty?
59
- # return everything if given expression is empty
60
- expression = "*"
61
- end
60
+ expression = rewrite_expression(args.join(" ").strip)
62
61
 
63
62
  begin
64
63
  node = parse(expression)
@@ -69,15 +68,20 @@ module Hotdog
69
68
 
70
69
  result0 = evaluate(node, self)
71
70
  tuples, fields = get_hosts_with_search_tags(result0, node)
72
- if options[:shuffle]
73
- tuples = tuples.shuffle()
74
- end
75
71
  tuples = filter_hosts(tuples)
76
72
  validate_hosts!(tuples, fields)
77
73
  run_main(tuples.map {|tuple| tuple.first }, options)
78
74
  end
79
75
 
80
76
  private
77
+ def rewrite_expression(expression)
78
+ expression = super(expression)
79
+ if options[:shuffle]
80
+ expression = "SHUFFLE((#{expression}))"
81
+ end
82
+ expression
83
+ end
84
+
81
85
  def parallelism(hosts)
82
86
  options[:max_parallelism] || hosts.size
83
87
  end
@@ -116,24 +120,10 @@ module Hotdog
116
120
 
117
121
  def build_command_options(options={})
118
122
  cmdline = []
119
- if options[:forward_agent]
120
- cmdline << "-A"
121
- end
122
123
  if options[:ssh_config]
123
124
  cmdline << "-F" << File.expand_path(options[:ssh_config])
124
125
  end
125
- if options[:identity_file]
126
- cmdline << "-i" << options[:identity_file]
127
- end
128
- if options[:user]
129
- cmdline << "-l" << options[:user]
130
- end
131
- if options[:options]
132
- cmdline += options[:options].flat_map { |option| ["-o", option] }
133
- end
134
- if options[:port]
135
- cmdline << "-p" << options[:port].to_s
136
- end
126
+ cmdline += options[:ssh_options].flat_map { |k, v| ["-o", "#{k}=#{v}"] }
137
127
  if options[:verbose]
138
128
  cmdline << "-v"
139
129
  end
@@ -210,6 +200,14 @@ module Hotdog
210
200
  end
211
201
 
212
202
  private
203
+ def rewrite_expression(expression)
204
+ expression = super(expression)
205
+ if options[:index]
206
+ expression = "SLICE((#{expression}), #{options[:index]}, 1)"
207
+ end
208
+ expression
209
+ end
210
+
213
211
  def filter_hosts(tuples)
214
212
  tuples = super
215
213
  if options[:index] and options[:index] < tuples.length
@@ -232,7 +230,12 @@ module Hotdog
232
230
  def run_main(hosts, options={})
233
231
  cmdline = build_command_string(hosts.first, @remote_command, options)
234
232
  logger.debug("execute: #{cmdline}")
235
- exec(cmdline)
233
+ if options[:dry_run]
234
+ STDOUT.puts(cmdline)
235
+ exit(0)
236
+ else
237
+ exec(cmdline)
238
+ end
236
239
  exit(127)
237
240
  end
238
241
  end
@@ -24,24 +24,25 @@ module Hotdog
24
24
  end
25
25
 
26
26
  def run(args=[], options={})
27
- args.each do |host_name|
28
- host_name = host_name.sub(/\Ahost:/, "")
29
-
27
+ hosts = args.map { |arg|
28
+ arg.sub(/\Ahost:/, "")
29
+ }
30
+ hosts.each do |host|
30
31
  if options[:tags].empty?
31
32
  # nop
32
33
  else
33
34
  # add all as user tags
34
35
  with_retry(options) do
35
- add_tags(host_name, options[:tags], source=options[:tag_source])
36
+ add_tags(host, options[:tags], source=options[:tag_source])
36
37
  end
37
38
  end
38
39
  end
39
-
40
- # Remove persistent.db to schedule update on next invocation
41
- if @db
42
- close_db(@db)
40
+ if open_db
41
+ create_tags(@db, options[:tags])
42
+ options[:tags].each do |tag|
43
+ associae_tag_hosts(@db, tag, hosts)
44
+ end
43
45
  end
44
- FileUtils.rm_f(File.join(options[:confdir], PERSISTENT_DB))
45
46
  end
46
47
 
47
48
  private
@@ -24,33 +24,40 @@ module Hotdog
24
24
  end
25
25
 
26
26
  def run(args=[], options={})
27
- args.each do |host_name|
28
- host_name = host_name.sub(/\Ahost:/, "")
27
+ hosts = args.map { |arg|
28
+ arg.sub(/\Ahost:/, "")
29
+ }
29
30
 
31
+ hosts.each do |host|
30
32
  if options[:tags].empty?
31
33
  # delete all user tags
32
34
  with_retry do
33
- detach_tags(host_name, source=options[:tag_source])
35
+ detach_tags(host, source=options[:tag_source])
34
36
  end
35
37
  else
36
- host_tags = with_retry { host_tags(host_name, source=options[:tag_source]) }
38
+ host_tags = with_retry { host_tags(host, source=options[:tag_source]) }
37
39
  old_tags = host_tags["tags"]
38
40
  new_tags = old_tags - options[:tags]
39
41
  if old_tags == new_tags
40
42
  # nop
41
43
  else
42
44
  with_retry do
43
- update_tags(host_name, new_tags, source=options[:tag_source])
45
+ update_tags(host, new_tags, source=options[:tag_source])
44
46
  end
45
47
  end
46
48
  end
47
49
  end
48
50
 
49
- # Remove persistent.db to schedule update on next invocation
50
- if @db
51
- close_db(@db)
51
+ if options[:tags].empty?
52
+ # refresh all persistent.db since there is no way to identify user tags
53
+ remove_db
54
+ else
55
+ if open_db
56
+ options[:tags].each do |tag|
57
+ disassociate_tag_hosts(tag, hosts)
58
+ end
59
+ end
52
60
  end
53
- FileUtils.rm_f(File.join(options[:confdir], PERSISTENT_DB))
54
61
  end
55
62
 
56
63
  private
@@ -37,12 +37,13 @@ module Hotdog
37
37
  cancel_downtime(downtime["id"], options)
38
38
  end
39
39
  end
40
-
41
- # Remove persistent.db to schedule update on next invocation
42
- if @db
43
- close_db(@db)
40
+ hosts = scopes.select { |scope| scope.start_with?("host:") }.map { |scope|
41
+ scope.slice("host:".length, scope.length)
42
+ }
43
+ if 0 < hosts.length
44
+ # refresh all persistent.db to retrieve information about up'd host
45
+ remove_db
44
46
  end
45
- FileUtils.rm_f(File.join(options[:confdir], PERSISTENT_DB))
46
47
  end
47
48
 
48
49
  private
@@ -92,25 +92,16 @@ module Hotdog
92
92
  get_hosts_fields(host_ids, fields)
93
93
  else
94
94
  if @options[:listing]
95
- q1 = "SELECT DISTINCT tags.name FROM hosts_tags " \
96
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
97
- "WHERE hosts_tags.host_id IN (%s);"
98
95
  if @options[:primary_tag]
99
96
  fields = [
100
97
  @options[:primary_tag],
101
98
  "host",
102
- ] + host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |host_ids|
103
- execute(q1 % host_ids.map { "?" }.join(", "), host_ids).map { |row| row.first }.reject { |tag_name|
104
- tag_name == @options[:primary_tag]
105
- }
106
- }
99
+ ] + get_fields(host_ids).reject { |tag_name| tag_name == @options[:primary_tag] }
107
100
  get_hosts_fields(host_ids, fields)
108
101
  else
109
102
  fields = [
110
103
  "host",
111
- ] + host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |host_ids|
112
- execute(q1 % host_ids.map { "?" }.join(", "), host_ids).map { |row| row.first }
113
- }
104
+ ] + get_fields(host_ids)
114
105
  get_hosts_fields(host_ids, fields)
115
106
  end
116
107
  else
@@ -124,48 +115,85 @@ module Hotdog
124
115
  end
125
116
  end
126
117
 
127
- def get_hosts_fields(host_ids, fields)
128
- if fields.empty?
118
+ def get_fields(host_ids)
119
+ host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |host_ids|
120
+ q = "SELECT DISTINCT tags.name FROM hosts_tags " \
121
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
122
+ "WHERE hosts_tags.host_id IN (%s);" % host_ids.map { "?" }.join(", ")
123
+ execute(q, host_ids).map { |row| row.first }
124
+ }.uniq
125
+ end
126
+
127
+ def get_hosts_fields(host_ids, fields, options={})
128
+ case fields.length
129
+ when 0
129
130
  [[], fields]
131
+ when 1
132
+ get_hosts_field(host_ids, fields.first, options)
130
133
  else
131
- fields_without_host = fields.reject { |tag_name| tag_name == "host" }
132
- if fields == fields_without_host
133
- host_names = {}
134
+ if fields.find { |field| /\Ahost\z/i =~ field }
135
+ host_names = Hash[execute("SELECT id, name FROM hosts WHERE id IN (%s);" % host_ids.map { "?" }.join(", "), host_ids).map { |row| row.to_a }]
134
136
  else
135
- host_names = Hash[
136
- host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |host_ids|
137
- execute("SELECT id, name FROM hosts WHERE id IN (%s)" % host_ids.map { "?" }.join(", "), host_ids).map { |row| row.to_a }
138
- }
139
- ]
137
+ host_names = {}
140
138
  end
141
- q1 = "SELECT LOWER(tags.name), GROUP_CONCAT(tags.value, ',') FROM hosts_tags " \
142
- "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
143
- "WHERE hosts_tags.host_id = ? AND tags.name IN (%s) " \
144
- "GROUP BY tags.name;"
145
- result = host_ids.map { |host_id|
146
- tag_values = Hash[
147
- fields_without_host.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 1).flat_map { |fields_without_host|
148
- execute(q1 % fields_without_host.map { "?" }.join(", "), [host_id] + fields_without_host).map { |row| row.to_a }
149
- }
150
- ]
151
- fields.map { |tag_name|
152
- if tag_name == "host"
153
- host_names.fetch(host_id, nil)
154
- else
155
- tag_value = tag_values.fetch(tag_name.downcase, nil)
156
- if tag_value
157
- if tag_value.empty?
158
- tag_name # use `tag_name` as `tag_value` for the tags without any values
159
- else
160
- tag_value
161
- end
162
- else
163
- nil
164
- end
165
- end
139
+
140
+ [host_ids.map { |host_id| get_host_fields(host_id, fields, options.merge(host_names: host_names)) }.map { |result, fields| result }, fields]
141
+ end
142
+ end
143
+
144
+ def get_host_fields(host_id, fields, options={})
145
+ field_values = {"host" => options.fetch(:host_names, {}).fetch(host_id, nil)}
146
+
147
+ fields.reject { |field| /\Ahost\z/i =~ field }.uniq.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 1).each do |fields|
148
+ q = "SELECT LOWER(tags.name), GROUP_CONCAT(tags.value, ',') FROM hosts_tags " \
149
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
150
+ "WHERE hosts_tags.host_id = ? AND tags.name IN (%s) " \
151
+ "GROUP BY tags.name;" % fields.map { "?" }.join(", ")
152
+ execute(q, [host_id] + fields).each do |row|
153
+ field_values[row[0]] = row[1]
154
+ end
155
+ end
156
+
157
+ result = fields.map { |tag_name|
158
+ tag_value = field_values.fetch(tag_name.downcase, nil)
159
+ display_tag(tag_name, tag_value)
160
+ }
161
+ [result, fields]
162
+ end
163
+
164
+ def get_hosts_field(host_ids, field, options={})
165
+ if /\Ahost\z/i =~ field
166
+ result = host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT).flat_map { |host_ids|
167
+ execute("SELECT name FROM hosts WHERE id IN (%s)" % host_ids.map { "?" }.join(", "), host_ids).map { |row| row.to_a }
168
+ }
169
+ else
170
+ result = host_ids.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 1).flat_map { |host_ids|
171
+ q = "SELECT LOWER(tags.name), GROUP_CONCAT(tags.value, ',') FROM hosts_tags " \
172
+ "INNER JOIN tags ON hosts_tags.tag_id = tags.id " \
173
+ "WHERE hosts_tags.host_id IN (%s) AND tags.name = ? " \
174
+ "GROUP BY hosts_tags.host_id, tags.name;" % host_ids.map { "?" }.join(", ")
175
+ r = execute(q, host_ids + [field]).map { |tag_name, tag_value|
176
+ [display_tag(tag_name, tag_value)]
166
177
  }
178
+ if r.empty?
179
+ host_ids.map { [nil] }
180
+ else
181
+ r
182
+ end
167
183
  }
168
- [result, fields]
184
+ end
185
+ [result, [field]]
186
+ end
187
+
188
+ def display_tag(tag_name, tag_value)
189
+ if tag_value
190
+ if tag_value.empty?
191
+ tag_name # use `tag_name` as `tag_value` for the tags without any values
192
+ else
193
+ tag_value
194
+ end
195
+ else
196
+ nil
169
197
  end
170
198
  end
171
199
 
@@ -178,23 +206,19 @@ module Hotdog
178
206
  db.close()
179
207
  end
180
208
 
181
- def update_db(options={})
209
+ def open_db(options={})
182
210
  options = @options.merge(options)
183
- if @db.nil?
211
+ if @db
212
+ @db
213
+ else
184
214
  FileUtils.mkdir_p(options[:confdir])
185
215
  persistent = File.join(options[:confdir], PERSISTENT_DB)
186
216
 
187
217
  if (not options[:force] and File.exist?(persistent) and Time.new < File.mtime(persistent) + options[:expiry]) or options[:offline]
188
218
  begin
189
219
  persistent_db = SQLite3::Database.new(persistent)
190
- persistent_db.execute(<<-EOS)
191
- SELECT hosts_tags.host_id FROM hosts_tags
192
- INNER JOIN hosts ON hosts_tags.host_id = hosts.id
193
- INNER JOIN tags ON hosts_tags.tag_id = tags.id
194
- LIMIT 1;
195
- EOS
220
+ persistent_db.execute("SELECT hosts_tags.host_id FROM hosts_tags INNER JOIN hosts ON hosts_tags.host_id = hosts.id INNER JOIN tags ON hosts_tags.tag_id = tags.id LIMIT 1;")
196
221
  @db = persistent_db
197
- return
198
222
  rescue SQLite3::SQLException
199
223
  if options[:offline]
200
224
  raise(RuntimeError.new("no database available on offline mode"))
@@ -203,81 +227,56 @@ module Hotdog
203
227
  end
204
228
  end
205
229
  end
230
+ @db
231
+ end
232
+ end
206
233
 
234
+ def update_db(options={})
235
+ options = @options.merge(options)
236
+ if open_db(options)
237
+ @db
238
+ else
207
239
  memory_db = SQLite3::Database.new(":memory:")
208
- execute_db(memory_db, <<-EOS)
209
- CREATE TABLE IF NOT EXISTS hosts (
210
- id INTEGER PRIMARY KEY AUTOINCREMENT,
211
- name VARCHAR(255) NOT NULL COLLATE NOCASE
212
- );
213
- EOS
214
- execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS hosts_name ON hosts ( name );")
215
- execute_db(memory_db, <<-EOS)
216
- CREATE TABLE IF NOT EXISTS tags (
217
- id INTEGER PRIMARY KEY AUTOINCREMENT,
218
- name VARCHAR(200) NOT NULL COLLATE NOCASE,
219
- value VARCHAR(200) NOT NULL COLLATE NOCASE
220
- );
221
- EOS
222
- execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS tags_name_value ON tags ( name, value );")
223
- execute_db(memory_db, <<-EOS)
224
- CREATE TABLE IF NOT EXISTS hosts_tags (
225
- host_id INTEGER NOT NULL,
226
- tag_id INTEGER NOT NULL
227
- );
228
- EOS
229
- execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS hosts_tags_host_id_tag_id ON hosts_tags ( host_id, tag_id );")
240
+ execute_db(memory_db, "CREATE TABLE IF NOT EXISTS hosts (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(255) NOT NULL COLLATE NOCASE);")
241
+ execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS hosts_name ON hosts (name);")
242
+ execute_db(memory_db, "CREATE TABLE IF NOT EXISTS tags (id INTEGER PRIMARY KEY AUTOINCREMENT, name VARCHAR(200) NOT NULL COLLATE NOCASE, value VARCHAR(200) NOT NULL COLLATE NOCASE);")
243
+ execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS tags_name_value ON tags (name, value);")
244
+ execute_db(memory_db, "CREATE TABLE IF NOT EXISTS hosts_tags (host_id INTEGER NOT NULL, tag_id INTEGER NOT NULL);")
245
+ execute_db(memory_db, "CREATE UNIQUE INDEX IF NOT EXISTS hosts_tags_host_id_tag_id ON hosts_tags (host_id, tag_id);")
230
246
 
231
247
  all_tags = get_all_tags()
232
248
 
233
249
  memory_db.transaction do
234
250
  known_tags = all_tags.keys.map { |tag| split_tag(tag) }.uniq
235
- known_tags.each_slice(SQLITE_LIMIT_COMPOUND_SELECT / 2) do |known_tags|
236
- q = "INSERT OR IGNORE INTO tags (name, value) VALUES %s" % known_tags.map { "(?, ?)" }.join(", ")
237
- execute_db(memory_db, q, known_tags)
238
- end
251
+ create_tags(memory_db, known_tags)
239
252
 
240
253
  known_hosts = all_tags.values.reduce(:+).uniq
241
- known_hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT) do |known_hosts|
242
- q = "INSERT OR IGNORE INTO hosts (name) VALUES %s" % known_hosts.map { "(?)" }.join(", ")
243
- execute_db(memory_db, q, known_hosts)
244
- end
254
+ create_hosts(memory_db, known_hosts)
245
255
 
246
256
  all_tags.each do |tag, hosts|
247
- hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 2) do |hosts|
248
- q = "INSERT OR REPLACE INTO hosts_tags (host_id, tag_id) " \
249
- "SELECT host.id, tag.id FROM " \
250
- "( SELECT id FROM hosts WHERE name IN (%s) ) AS host, " \
251
- "( SELECT id FROM tags WHERE name = ? AND value = ? LIMIT 1 ) AS tag;" % hosts.map { "?" }.join(", ")
252
- begin
253
- execute_db(memory_db, q, (hosts + split_tag(tag)))
254
- rescue SQLite3::RangeException => error
255
- # FIXME: bulk insert occationally fails even if there are no errors in bind parameters
256
- # `bind_param': bind or column index out of range (SQLite3::RangeException)
257
- logger.warn("bulk insert failed due to #{error.message}. fallback to normal insert.")
258
- hosts.each do |host|
259
- q = "INSERT OR REPLACE INTO hosts_tags (host_id, tag_id) " \
260
- "SELECT host.id, tag.id FROM " \
261
- "( SELECT id FROM hosts WHERE name = ? ) AS host, " \
262
- "( SELECT id FROM tags WHERE name = ? AND value = ? LIMIT 1 ) AS tag;"
263
- execute_db(memory_db, q, [host] + split_tag(tag))
264
- end
265
- end
266
- end
257
+ associate_tag_hosts(memory_db, tag, hosts)
267
258
  end
268
259
  end
269
260
 
270
261
  # backup in-memory db to file
262
+ FileUtils.mkdir_p(options[:confdir])
263
+ persistent = File.join(options[:confdir], PERSISTENT_DB)
271
264
  FileUtils.rm_f(persistent)
272
265
  persistent_db = SQLite3::Database.new(persistent)
273
266
  copy_db(memory_db, persistent_db)
274
- close_db(persistent_db)
275
- @db = memory_db
276
- else
277
- @db
267
+ close_db(memory_db)
268
+ @db = persistent_db
278
269
  end
279
270
  end
280
271
 
272
+ def remove_db(options={})
273
+ options = @options.merge(options)
274
+ if @db
275
+ close_db(@db)
276
+ end
277
+ FileUtils.rm_f(File.join(options[:confdir], PERSISTENT_DB))
278
+ end
279
+
281
280
  def execute_db(db, q, args=[])
282
281
  begin
283
282
  logger.debug("execute: #{q} -- #{args.inspect}")
@@ -322,6 +321,52 @@ module Hotdog
322
321
  Hash[responses.fetch(:all_tags, {}).fetch("tags", []).map { |tag, hosts| [tag, hosts.reject { |host| downtimes.include?(host) }] }]
323
322
  end
324
323
 
324
+ def create_hosts(db, hosts)
325
+ hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT) do |hosts|
326
+ q = "INSERT OR IGNORE INTO hosts (name) VALUES %s" % hosts.map { "(?)" }.join(", ")
327
+ execute_db(db, q, hosts)
328
+ end
329
+ end
330
+
331
+ def create_tags(db, tags)
332
+ tags.each_slice(SQLITE_LIMIT_COMPOUND_SELECT / 2) do |tags|
333
+ q = "INSERT OR IGNORE INTO tags (name, value) VALUES %s" % tags.map { "(?, ?)" }.join(", ")
334
+ execute_db(db, q, tags)
335
+ end
336
+ end
337
+
338
+ def associate_tag_hosts(db, tag, hosts)
339
+ hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 2) do |hosts|
340
+ q = "INSERT OR REPLACE INTO hosts_tags (host_id, tag_id) " \
341
+ "SELECT host.id, tag.id FROM " \
342
+ "( SELECT id FROM hosts WHERE name IN (%s) ) AS host, " \
343
+ "( SELECT id FROM tags WHERE name = ? AND value = ? LIMIT 1 ) AS tag;" % hosts.map { "?" }.join(", ")
344
+ begin
345
+ execute_db(db, q, (hosts + split_tag(tag)))
346
+ rescue SQLite3::RangeException => error
347
+ # FIXME: bulk insert occationally fails even if there are no errors in bind parameters
348
+ # `bind_param': bind or column index out of range (SQLite3::RangeException)
349
+ logger.warn("bulk insert failed due to #{error.message}. fallback to normal insert.")
350
+ hosts.each do |host|
351
+ q = "INSERT OR REPLACE INTO hosts_tags (host_id, tag_id) " \
352
+ "SELECT host.id, tag.id FROM " \
353
+ "( SELECT id FROM hosts WHERE name = ? ) AS host, " \
354
+ "( SELECT id FROM tags WHERE name = ? AND value = ? LIMIT 1 ) AS tag;"
355
+ execute_db(db, q, [host] + split_tag(tag))
356
+ end
357
+ end
358
+ end
359
+ end
360
+
361
+ def disassociate_tag_hosts(db, tag, hosts)
362
+ hosts.each_slice(SQLITE_LIMIT_COMPOUND_SELECT - 2) do |hosts|
363
+ q = "DELETE FROM hosts_tags " \
364
+ "WHERE tag_id IN ( SELECT id FROM tags WHERE name = ? AND value = ? LIMIT 1 );" \
365
+ "AND host_id IN ( SELECT id FROM hosts WHERE name IN (%s) ) " % hosts.map { "?" }.join(", ")
366
+ execute_db(db, q, split_tag(tag) + hosts)
367
+ end
368
+ end
369
+
325
370
  def dog()
326
371
  @dog ||= Dogapi::Client.new(application.api_key, application.application_key)
327
372
  end
@@ -425,8 +425,13 @@ module Hotdog
425
425
  @function = :ORDER_BY
426
426
  when "REVERSE", "reverse"
427
427
  @function = :REVERSE
428
+ when "SAMPLE", "sample"
429
+ @function = :HEAD
430
+ args[0] = FuncallNode.new("SHUFFLE", [args[0]])
428
431
  when "SHUFFLE", "shuffle"
429
432
  @function = :SHUFFLE
433
+ when "SLICE", "slice"
434
+ @function = :SLICE
430
435
  when "SORT", "sort"
431
436
  @function = :ORDER_BY
432
437
  when "TAIL", "tail"
@@ -474,6 +479,8 @@ module Hotdog
474
479
  @args[0] = @args[0].optimize(options)
475
480
  when :SHUFFLE
476
481
  @args[0] = @args[0].optimize(options)
482
+ when :SLICE
483
+ @args[0] = @args[0].optimize(options)
477
484
  when :TAIL
478
485
  @args[0] = @args[0].optimize(options)
479
486
  end
@@ -510,6 +517,8 @@ module Hotdog
510
517
  args[0].evaluate(environment, options).reverse()
511
518
  when :SHUFFLE
512
519
  args[0].evaluate(environment, options).shuffle()
520
+ when :SLICE
521
+ args[0].evaluate(environment, options).slice(args[1], args[2] || 1)
513
522
  when :TAIL
514
523
  args[0].evaluate(environment, options).last(args[1] || 1)
515
524
  else
@@ -87,7 +87,7 @@ module Hotdog
87
87
  )
88
88
  }
89
89
  rule(:integer) {
90
- ( match('[1-9]').repeat(1) >> match('[0-9]').repeat(0) \
90
+ ( match('[0-9]').repeat(1) \
91
91
  )
92
92
  }
93
93
  rule(:string) {
@@ -1,3 +1,3 @@
1
1
  module Hotdog
2
- VERSION = "0.17.1"
2
+ VERSION = "0.18.0"
3
3
  end
@@ -1,6 +1,7 @@
1
1
  require "spec_helper"
2
2
  require "hotdog/application"
3
3
  require "hotdog/commands"
4
+ require "hotdog/commands/search"
4
5
 
5
6
  describe "commands" do
6
7
  let(:cmd) {
@@ -99,7 +100,7 @@ describe "commands" do
99
100
  end
100
101
 
101
102
  it "get host fields with host" do
102
- allow(cmd).to receive(:execute).with("SELECT id, name FROM hosts WHERE id IN (?, ?, ?)", [1, 2, 3]) {
103
+ allow(cmd).to receive(:execute).with("SELECT id, name FROM hosts WHERE id IN (?, ?, ?);", [1, 2, 3]) {
103
104
  [[1, "host1"], [2, "host2"], [3, "host3"]]
104
105
  }
105
106
  q1 = [
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hotdog
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.17.1
4
+ version: 0.18.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yamashita Yuu
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-29 00:00:00.000000000 Z
11
+ date: 2016-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler