hotdog 0.17.1 → 0.18.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 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