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 +4 -4
- data/Gemfile.lock +1 -1
- data/VERSION +1 -1
- data/cknife.gemspec +3 -3
- data/lib/cknife/cknife_pg.rb +220 -40
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 42b1aec5549d2d6e08113f9d867e4faad1cf3954
|
4
|
+
data.tar.gz: b30a5ad505f0c656eaf4fb800704a5d4824492b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9e023ec2bb2a311ea6afabe95b7325de60e94de53f2854b018160aa99f4fa30d301b48c5266bfa07e60107a21f5575538187e7a6b464167be188fd8638bcc92c
|
7
|
+
data.tar.gz: ddc7b4e2541e650e1b3df6d28e110c63972aff17baf38ac4d4e48735519a62a8c4a13893c58ae6342ebd120f2a563a01f8fa59d4b0806422f4302897dfd135ee
|
data/Gemfile.lock
CHANGED
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
1.
|
1
|
+
1.2.0
|
data/cknife.gemspec
CHANGED
@@ -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.
|
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.
|
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-
|
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]
|
data/lib/cknife/cknife_pg.rb
CHANGED
@@ -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
|
-
"
|
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
|
-
|
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
|
-
|
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
|
-
|
64
|
-
|
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
|
-
|
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
|
-
|
73
|
-
|
74
|
-
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
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
|
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
|
-
|
111
|
-
|
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
|
-
|
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
|
-
|
121
|
-
|
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.
|
127
|
-
|
128
|
-
def restore
|
129
|
-
to_restore =
|
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.
|
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.
|
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-
|
11
|
+
date: 2018-10-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rest-client
|