cknife 1.1.2 → 1.2.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: 83aba2bc52997c794e3eadab342a6ec8339bd828
4
- data.tar.gz: dab6ec5e7f2d89a5e0259601b4d1c70ee4ab35ba
3
+ metadata.gz: 42b1aec5549d2d6e08113f9d867e4faad1cf3954
4
+ data.tar.gz: b30a5ad505f0c656eaf4fb800704a5d4824492b1
5
5
  SHA512:
6
- metadata.gz: 0fd636788f7d6888014b955e4dd63c36fe418b6240faf46b8a6fad48b2b56223df959cca63358c044d672b3b068a6f80b7017e184a88255aaebba0802fbd7c9a
7
- data.tar.gz: b8892ac926673ef19c386d2d3cc5d3519f50b6590bc4e78ed08c67ea3949ee3159893209c2fb49c052942564e721f87e9723062999ca6bacadd67e0eb59efb05
6
+ metadata.gz: 9e023ec2bb2a311ea6afabe95b7325de60e94de53f2854b018160aa99f4fa30d301b48c5266bfa07e60107a21f5575538187e7a6b464167be188fd8638bcc92c
7
+ data.tar.gz: ddc7b4e2541e650e1b3df6d28e110c63972aff17baf38ac4d4e48735519a62a8c4a13893c58ae6342ebd120f2a563a01f8fa59d4b0806422f4302897dfd135ee
@@ -116,7 +116,7 @@ GEM
116
116
  mime-types (>= 1.16, < 3.0)
117
117
  netrc (~> 0.7)
118
118
  semver2 (3.4.2)
119
- thor (0.14.6)
119
+ thor (0.20.0)
120
120
  thread_safe (0.3.6)
121
121
  treetop (1.4.15)
122
122
  polyglot
data/VERSION CHANGED
@@ -1 +1 @@
1
- 1.1.2
1
+ 1.2.0
@@ -2,16 +2,16 @@
2
2
  # DO NOT EDIT THIS FILE DIRECTLY
3
3
  # Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
4
4
  # -*- encoding: utf-8 -*-
5
- # stub: cknife 1.1.2 ruby lib
5
+ # stub: cknife 1.2.0 ruby lib
6
6
 
7
7
  Gem::Specification.new do |s|
8
8
  s.name = "cknife".freeze
9
- s.version = "1.1.2"
9
+ s.version = "1.2.0"
10
10
 
11
11
  s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version=
12
12
  s.require_paths = ["lib".freeze]
13
13
  s.authors = ["Michael Rivera".freeze]
14
- s.date = "2018-10-09"
14
+ s.date = "2018-10-10"
15
15
  s.description = "A collection of command line tools, especially for popular API services.".freeze
16
16
  s.email = "soymrmike@gmail.com".freeze
17
17
  s.executables = ["cknifeaws".freeze, "cknifedub".freeze, "cknifemail".freeze, "cknifemon".freeze, "cknifemysql".freeze, "cknifenowtimestamp".freeze, "cknifepg".freeze, "cknifewcdir".freeze, "cknifezerigo".freeze]
@@ -23,34 +23,61 @@ class CKnifePg < Thor
23
23
  "-h #{conf[:host]} -p #{conf[:port]} -U #{conf[:username]} --no-password"
24
24
  end
25
25
 
26
+ # Leaves out options to simplify output.
27
+ def psql_easy
28
+ "psql #{connection_options} -d #{conf[:database]}"
29
+ end
30
+
26
31
  def psql_invocation
27
- "psql #{connection_options} -d #{conf[:database]} --no-align --tuples-only"
32
+ "#{psql_easy} --no-align --tuples-only"
28
33
  end
29
34
 
30
35
  def pg_pass_file
31
36
  @pg_pass_file = ".pgpass"
32
37
  end
33
38
 
39
+ def dc(cmd)
40
+ puts "PGPASSFILE=#{pg_pass_file} #{cmd}"
41
+ end
42
+
34
43
  def pg_pass_file_execute(cmd, input = nil)
35
44
  return if !@session_ok
36
- puts cmd
45
+ dc(cmd) if options[:verbose]
37
46
  stdin, stdout, stderr, wait_thread = Open3.popen3({'PGPASSFILE' => pg_pass_file}, cmd)
38
47
  if input
39
- puts input
48
+ puts input if options[:verbose]
40
49
  stdin.write input
41
50
  stdin.close
42
51
  end
43
52
  output = stdout.read
44
53
  output += stderr.read
45
- $stdout.write output
54
+ $stdout.write output if options[:verbose]
46
55
  stdout.close
47
56
  stderr.close
48
57
  result = wait_thread.value.to_i
49
- @session_ok = @session_ok && (result == 0)
58
+
59
+ if result != 0
60
+ @session_ok = false
61
+ msg = "An error occurred."
62
+ msg += " If the --verbose flag is available for this command, you may try turning it on." if !options[:verbose]
63
+ say(msg, :red)
64
+ end
65
+
66
+ # I'm not sure why I use the block to this method. 2018-10-09
50
67
  yield if block_given?
51
68
  output
52
69
  end
53
70
 
71
+ def pg_str
72
+ "#{conf[:host]}:#{conf[:port]}:*:#{conf[:username]}:#{conf[:password]}"
73
+ end
74
+
75
+ def write_pg_pass_file
76
+ # pgpass format
77
+ File.open(pg_pass_file, "w", 0600) { |f| f.write pg_str }
78
+ pg_pass_file
79
+ end
80
+
54
81
  def with_pg_pass_file
55
82
  if @session_live
56
83
  return yield
@@ -59,41 +86,55 @@ class CKnifePg < Thor
59
86
  @session_live = true
60
87
  @session_ok = true
61
88
 
89
+ existing_pgpass = false
62
90
  if File.exists?(pg_pass_file)
63
- say("This generates a pgpass file but one is already on disk. Exiting.")
64
- return
91
+ existing_pgpass = true
92
+ s = File.read(pg_pass_file)
93
+ if s != pg_str
94
+ say("A .pgpass file is present, but it does not match your database configuration. The contents of the .pgpass file must exactly match what this tool would generate. Please reconcile the .pgpass file with your configuration, and then try again. You can also delete the .pgpass file since this tool generates one in order to do its job (and removes it after finishing).", :red)
95
+ return
96
+ end
65
97
  end
66
98
 
67
- # pgpass format
68
- File.open(pg_pass_file, "w", 0600) { |f| f.write "#{conf[:host]}:#{conf[:port]}:*:#{conf[:username]}:#{conf[:password]}" }
69
-
70
- result = yield # don't know what we're planning to do with the result here...
99
+ write_pg_pass_file
71
100
 
72
- FileUtils.rm(pg_pass_file)
73
- if File.exists?(pg_pass_file)
74
- say("Failed to remove pg_pass file. Please remove it for security purposes.")
101
+ result = nil
102
+ begin
103
+ result = yield # don't know what we're planning to do with the result here...
104
+ ensure
105
+ if !existing_pgpass
106
+ FileUtils.rm(pg_pass_file)
107
+ if File.exists?(pg_pass_file)
108
+ say("Failed to remove .pgpass file. Please remove it for your infrastructure's security.")
109
+ end
110
+ else
111
+ say("Left existing .pgpass file on disk.", :yellow)
112
+ end
75
113
  end
76
114
 
77
- say
78
- say("Command failed.", :red) if !@session_ok
79
115
  @session_live = false
80
-
81
116
  result
82
117
  end
83
- end
84
118
 
85
- desc "disconnect", "Disconnect all sessions from the database. You must have a superuser configured for this to work."
86
- def disconnect
87
- with_pg_pass_file do
88
- my_pid = pg_pass_file_execute(psql_invocation, "select pg_backend_pid();").split.first
89
- ids = pg_pass_file_execute(psql_invocation, "SELECT procpid FROM pg_stat_activity WHERE datname = '#{conf[:database]}' AND procpid != #{my_pid};")
90
- ids.split.each do |pid|
91
- pg_pass_file_execute(psql_invocation, "select pg_terminate_backend(#{pid});")
92
- end
119
+ # Created to check for 9.2 system schema changes.
120
+ # Returns [9, 5, 12] for "9.5.12"
121
+ #
122
+ def pg_version
123
+ return @version if @version
124
+ output, stderr = Open3.capture2("psql --version")
125
+ output =~ Regexp.new("([0-9]+).([0-9]+)(.([0-9]+))")
126
+ v1, v2, v3 = $1, $2, $4
127
+ @version = [v1.to_i, v2.to_i, v3.to_i]
128
+ end
129
+
130
+ def pid_col_name
131
+ (pg_version[0] >= 9 and pg_version[1] >= 2) ? "pid" : "procpid"
93
132
  end
133
+
94
134
  end
95
135
 
96
136
  desc "capture", "Capture a dump of the database to db(current timestamp).dump."
137
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
97
138
  def capture
98
139
  file_name = "db" + Time.now.strftime("%Y%m%d%H%M%S") + ".dump"
99
140
 
@@ -104,29 +145,39 @@ class CKnifePg < Thor
104
145
  end
105
146
  end
106
147
 
107
- desc "sessions", "List active sessions in this database and provide a string suitable for giving to kill for stopping those sessions."
148
+ desc "sessions", "List active sessions connected to this database. Also see the --kill option."
149
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
150
+ method_option :kill, :default => false, :type => :boolean, :desc => "Kill all active sessions found. You must have sufficient privileges or correct process ownership for this to work."
108
151
  def sessions
109
152
  with_pg_pass_file do
110
- my_pid = pg_pass_file_execute(psql_invocation, "select pg_backend_pid();").split.first
111
- ids_output = pg_pass_file_execute(psql_invocation, "SELECT procpid, application_name FROM pg_stat_activity WHERE datname = '#{conf[:database]}' AND procpid != #{my_pid};")
153
+ if !options[:kill]
154
+ my_pid = pg_pass_file_execute(psql_invocation, "select pg_backend_pid();").split.first
155
+ ids_output = pg_pass_file_execute(psql_invocation, "SELECT #{pid_col_name}, application_name FROM pg_stat_activity WHERE datname = '#{conf[:database]}' AND #{pid_col_name} != #{my_pid};")
156
+
157
+ return if ids_output.nil?
112
158
 
113
- if ids_output.nil?
114
- say("Error while looking for session information. Possibly a failed login.")
115
- else
116
159
  table = ids_output.split.map { |line| line.split("|") }
117
160
  print_table([["PID", "Application Name"]] + table, :indent => 2)
118
- ids = table.map { |row| row.first }
161
+ say("To kill all active sessions connected to this database, you can use the --kill option with this command.")
162
+ else
163
+ sql = "SELECT pg_terminate_backend(pg_stat_activity.#{pid_col_name})
164
+ FROM pg_stat_activity
165
+ WHERE pg_stat_activity.datname = '#{conf[:database]}'
166
+ AND pid <> pg_backend_pid();"
119
167
 
120
- say("If you would like to kill these sessions, you can do so with this command:")
121
- say("kill -9 #{ids.join(' ')}")
168
+ with_pg_pass_file do
169
+ pg_pass_file_execute(psql_invocation, sql) do
170
+ say("Terminated all sessions.") if @session_ok
171
+ end
172
+ end
122
173
  end
123
174
  end
124
175
  end
125
176
 
126
- desc "restore", "Restore a file. Use the one with the most recent mtime by default. Searches for db*.dump files in the CWD."
127
- method_options :filename => nil
128
- def restore
129
- to_restore = options[:filename] if options[:filename]
177
+ desc "restore [FILENAME]?", "Restore a file. If no filename is provided, searches for db*.dump files in the $CWD. It will pick the one with the most recent mtime."
178
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
179
+ def restore(filename=nil)
180
+ to_restore = filename if filename
130
181
  if to_restore.nil?
131
182
  files = Dir["db*.dump"]
132
183
  with_mtime = files.map { |f| [f, File.mtime(f)] }
@@ -136,7 +187,7 @@ class CKnifePg < Thor
136
187
  end
137
188
 
138
189
  if to_restore.nil?
139
- say("No backups file to restore. None given on the command line and none could be found in the CWD.", :red)
190
+ say("No backups file to restore. No file given on the command line, and no files could be found in the $CWD.", :red)
140
191
  return
141
192
  else
142
193
  if !yes?("Restore #{to_restore}?", :green)
@@ -163,4 +214,133 @@ class CKnifePg < Thor
163
214
  end
164
215
  end
165
216
  end
217
+
218
+ desc "schema [TABLE]?", "Dump the schema for all tables, or one table you specify."
219
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
220
+ def schema(table=nil)
221
+ table_string = table.nil? ? "" : "--table=#{table.strip}"
222
+ with_pg_pass_file do
223
+ output = pg_pass_file_execute("pg_dump #{connection_options} --no-owner --clean --if-exists --schema-only #{table_string} #{conf[:database]}")
224
+
225
+ # This command is so verbose that we only print the output if
226
+ # we didn't already do so in verbose mode.
227
+ puts output if !options[:verbose]
228
+ end
229
+ end
230
+
231
+ desc "tables", "List all tables."
232
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
233
+ def tables(table=nil)
234
+ sql = "SELECT
235
+ table_schema || '.' || table_name
236
+ FROM
237
+ information_schema.tables
238
+ WHERE
239
+ table_type = 'BASE TABLE'
240
+ AND
241
+ table_schema NOT IN ('pg_catalog', 'information_schema');"
242
+
243
+ with_pg_pass_file do
244
+ output = pg_pass_file_execute(psql_invocation, sql)
245
+ say ("removing public. prefixes.") if options[:verbose]
246
+ tables = output.split.map do |ts|
247
+ ts =~ /^public\.(.*)$/
248
+ $1
249
+ end
250
+ print_in_columns(tables)
251
+ end
252
+ end
253
+
254
+ desc "fexec [FILE]", "Execute a SQL script from a file on disk."
255
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
256
+ def fexec(file)
257
+ if !File.exists?(file)
258
+ say("'#{file}' does not exist.")
259
+ return
260
+ end
261
+
262
+ with_pg_pass_file do
263
+ pg_pass_file_execute("#{psql_invocation} -f #{file}") do
264
+ say("Ran #{file} SQL script.")
265
+ end
266
+ end
267
+ end
268
+
269
+ desc "createdb", "Create a database having the name specified in your configuration. Assumes you have privileges to do this."
270
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
271
+ def createdb
272
+ with_pg_pass_file do
273
+ pg_pass_file_execute("createdb #{connection_options} #{conf[:database]}") do
274
+ say("Created #{conf[:database]} database.") if @session_ok
275
+ end
276
+ end
277
+ end
278
+
279
+ desc "dropdb", "Drop the database specified in your configuration."
280
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
281
+ def dropdb
282
+ with_pg_pass_file do
283
+ pg_pass_file_execute("dropdb #{connection_options} #{conf[:database]};") do
284
+ say("Dropped #{conf[:database]} database.") if @session_ok
285
+ end
286
+ end
287
+ end
288
+
289
+ desc "perms", "List all users (roles) for the database, and their attributes (which approximate privileges)."
290
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
291
+ def perms
292
+ with_pg_pass_file do
293
+ output = pg_pass_file_execute("#{psql_easy}", "\\du")
294
+ puts output if !options[:verbose]
295
+ end
296
+ end
297
+
298
+ desc "passfile", "Write a .pgpass file in $CWD. Useful for starting a psql session on your own."
299
+ def passfile
300
+ connect_msg = "Connect command: PGPASSFILE=.pgpass #{psql_easy}"
301
+ if File.exists?(pg_pass_file)
302
+ say("A .pgpass file is already present.")
303
+ say(connect_msg)
304
+ return
305
+ end
306
+
307
+ f = write_pg_pass_file
308
+ say("Wrote #{pg_pass_file} to $CWD.")
309
+ say(connect_msg)
310
+ say("Remember to delete the .pgpass file when you are finished.")
311
+ end
312
+
313
+ desc "dpassfile", "Delete the .pgpass file in $CWD, assuming it exactly matches what would be generated by this tool."
314
+ def dpassfile
315
+ if !File.exists?(pg_pass_file)
316
+ say("No .pgpass file to delete.")
317
+ return
318
+ end
319
+
320
+ s = File.read(pg_pass_file)
321
+ if s == pg_str
322
+ File.unlink(pg_pass_file)
323
+ say("Deleted .pgpass file.")
324
+ else
325
+ say("The .pgpass file's contents do not match what this tool would have generated. Assuming you are trying to delete a .pgpass file that this tool did not generate, please inspect the file to ensure it contains what you expect, and then delete it yourself.", :red)
326
+ end
327
+ end
328
+
329
+ desc "psql", "Launches a psql session. Requires that you prepare a .pgpass file, unless you use --passfile. You can create a .pgpass file with the passfile command."
330
+ method_option :passfile, :type => :boolean, :default => false, :desc => "Write .pgpass file if it doesn't exist."
331
+ method_option :verbose, :default => false, :type => :boolean, :desc => "Show which commands are invoked, any input given to them, and any output they give back."
332
+ def psql
333
+ if !File.exists?(pg_pass_file)
334
+ if !options[:passfile]
335
+ say("You must prepare a .pgpass file for this command, or use --passfile to have this tool create it for you. Alternatively, you can create a .pgpass file with the passfile command and delete it later with the dpassfile command.")
336
+ return
337
+ end
338
+
339
+ write_pg_pass_file
340
+ end
341
+
342
+ dc(psql_easy) if options[:verbose]
343
+ exec({'PGPASSFILE' => pg_pass_file}, psql_easy)
344
+ end
345
+
166
346
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: cknife
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.2
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Rivera
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-09 00:00:00.000000000 Z
11
+ date: 2018-10-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rest-client