nixadm 1.0.6
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 +7 -0
- data/README.md +26 -0
- data/src/lib/nixadm/backup.rb +267 -0
- data/src/lib/nixadm/db/postgres.rb +9 -0
- data/src/lib/nixadm/db/postgresql.rb +217 -0
- data/src/lib/nixadm/pipeline.rb +477 -0
- data/src/lib/nixadm/util.rb +210 -0
- data/src/lib/nixadm/version.rb +11 -0
- data/src/lib/nixadm/zfs.rb +564 -0
- metadata +52 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: c7d91b74d9982ad275f6432d2ed60e6835a3e2bc05e057da200c927200c00998
|
4
|
+
data.tar.gz: 2873bd8d7aed1583f8bb7464a8114911f4886af8b79024336b7c99d978177ebb
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f2055495232a0a6d87f9fa7174daead46b7148606d633677e06b046c20237f2f43951510b085169b9e5cb668a6c8fef0ed850cbafccc3775a481ec4df5346d3a
|
7
|
+
data.tar.gz: ac9bef4a7749c7f812b0fbea27965bd1a0e5209120d56b6ca1ea9629585e84cbaf84b28eeffbbcc8f75dee47472be18c85943f40eec0703a37c7456679e9d18a
|
data/README.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# NixAdm
|
2
|
+
|
3
|
+
## About
|
4
|
+
|
5
|
+
This is a general library designed to aid with system administration tasks,
|
6
|
+
specifically to replace all shell scripting with Ruby code.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
You can install directly from the command line via Ruby gems as follows:
|
11
|
+
|
12
|
+
gem install nixadm
|
13
|
+
|
14
|
+
## Building from Source
|
15
|
+
|
16
|
+
This project uses CMake. To build the gem:
|
17
|
+
|
18
|
+
cmake .
|
19
|
+
make gem
|
20
|
+
|
21
|
+
The resultant gem will be generated in the <tt>pkg</tt> directory.
|
22
|
+
|
23
|
+
## License
|
24
|
+
|
25
|
+
Copyright information is located in the COPYING file. The software license is
|
26
|
+
located in the LICENSE file.
|
@@ -0,0 +1,267 @@
|
|
1
|
+
require 'nixadm/util'
|
2
|
+
require 'nixadm/zfs'
|
3
|
+
|
4
|
+
module NixAdm
|
5
|
+
|
6
|
+
# Base backup class. Provides centralized logging facility and job start/stop
|
7
|
+
# templates.
|
8
|
+
class Backup < NixAdm::Command
|
9
|
+
|
10
|
+
attr_reader :host
|
11
|
+
|
12
|
+
def initialize(host, options = {})
|
13
|
+
super(host, options)
|
14
|
+
|
15
|
+
if not $backup_dir.nil?
|
16
|
+
@logfile = File.open("#{$backup_dir}/backup.log", 'w')
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def run()
|
21
|
+
startBackup()
|
22
|
+
yield
|
23
|
+
endBackup()
|
24
|
+
end
|
25
|
+
|
26
|
+
def startBackup()
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def endBackup()
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
#------------------------------------------------------------------------------
|
35
|
+
# Utility functions
|
36
|
+
#------------------------------------------------------------------------------
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def cd(path)
|
41
|
+
Dir.chdir(path)
|
42
|
+
end
|
43
|
+
|
44
|
+
def replicationStatus(mfs, sfs)
|
45
|
+
h1 = mfs.pool.host.name
|
46
|
+
p1 = mfs.pool.name
|
47
|
+
f1 = mfs.name
|
48
|
+
h2 = sfs.pool.host.name
|
49
|
+
p2 = sfs.pool.name
|
50
|
+
f2 = sfs.name
|
51
|
+
|
52
|
+
return "replicating #{h1}:#{p1}/#{f1} -> #{h2}:#{p2}/#{f2}"
|
53
|
+
end
|
54
|
+
|
55
|
+
# Run general shell command and log output
|
56
|
+
def run_command(command)
|
57
|
+
sys = NixAdm::Pipeline.new()
|
58
|
+
|
59
|
+
sys.classic(command) do |stdin, stdout|
|
60
|
+
stdin.close()
|
61
|
+
|
62
|
+
while true
|
63
|
+
line = stdout.gets
|
64
|
+
break if line.nil?
|
65
|
+
@logfile.write(line)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
# Executes a command, captures output into file and zips the file
|
71
|
+
# @param command A command or array containing a pipeline of commands
|
72
|
+
# @param filename The name (path) of the file to dump database to
|
73
|
+
def zipStore(host, command, filename)
|
74
|
+
store host, command, filename, zip: true
|
75
|
+
end
|
76
|
+
|
77
|
+
# Executes a command, captures output into file
|
78
|
+
# @param command A command or array containing a pipeline of commands
|
79
|
+
# @param filename The name (path) of the file to dump database to
|
80
|
+
# @param zip If true, gzip the file.
|
81
|
+
def store(host, command, filename, zip: false)
|
82
|
+
sys = NixAdm::Pipeline.new()
|
83
|
+
pipeline = nil
|
84
|
+
|
85
|
+
sys.debug = debug()
|
86
|
+
|
87
|
+
# If the host we are performing this command on is the same host, don't
|
88
|
+
# resolve with host as secong argument, as this just makes that host ssh
|
89
|
+
# back into itself to run the command.
|
90
|
+
if @host == host
|
91
|
+
pipeline = resolveCommand(command)
|
92
|
+
else
|
93
|
+
pipeline = resolveCommand(command, host)
|
94
|
+
end
|
95
|
+
|
96
|
+
sys.run([pipeline]) do |stdin, stdout|
|
97
|
+
stdin.close()
|
98
|
+
|
99
|
+
dumpfile = File.open(filename, 'wb')
|
100
|
+
|
101
|
+
while true
|
102
|
+
data = stdout.read(1024)
|
103
|
+
break if data == nil
|
104
|
+
dumpfile.write(data)
|
105
|
+
end
|
106
|
+
|
107
|
+
dumpfile.close()
|
108
|
+
end
|
109
|
+
|
110
|
+
if zip == true
|
111
|
+
# Compress the file
|
112
|
+
sys.run "gzip -f #{filename}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def backup_dir(dir)
|
117
|
+
return "#{$backup_dir}/#{dir}"
|
118
|
+
end
|
119
|
+
|
120
|
+
# Replicates ZFS filesystem(s) on from host to backup hosts
|
121
|
+
#
|
122
|
+
# @param host The main host we are backing up
|
123
|
+
# @param filesystems An array of ZFS filesystems to replicate
|
124
|
+
#
|
125
|
+
# @param targets An array or hashes defining target hosts to replicate
|
126
|
+
# to. It has the following form:
|
127
|
+
#
|
128
|
+
# targets = [ { host: 'root@midway', root: 'pool' },
|
129
|
+
# { host: 'root@backup.c21bowman.com', root: 'pool' },
|
130
|
+
# { host: 'root@arc2', root: 'pool' } ]
|
131
|
+
#
|
132
|
+
def replicate(host, objects, targets)
|
133
|
+
admin = NixAdm::ZFS::Admin.new(host)
|
134
|
+
|
135
|
+
objects.each do |object|
|
136
|
+
|
137
|
+
# First look for matching filesystem
|
138
|
+
master_object = admin.object(object)
|
139
|
+
|
140
|
+
if master_object.nil?
|
141
|
+
raise "Error: #{object} does not exist"
|
142
|
+
end
|
143
|
+
|
144
|
+
startSourceZfsReplication master_object
|
145
|
+
|
146
|
+
failed = false
|
147
|
+
targets.each do | target |
|
148
|
+
|
149
|
+
if target[:host].nil? or target[:pool].nil?
|
150
|
+
raise "Invalid arguments: missing host or pool"
|
151
|
+
end
|
152
|
+
|
153
|
+
slave = NixAdm::ZFS::Host.new(target[:host])
|
154
|
+
pool = slave.pool(target[:pool])
|
155
|
+
|
156
|
+
attempts = 0
|
157
|
+
while attempts < 2
|
158
|
+
slave_object = pool.object(master_object.name)
|
159
|
+
|
160
|
+
break if not slave_object.nil?
|
161
|
+
|
162
|
+
# If we get here, it means the remote object does not exists. If this
|
163
|
+
# is a filesystem, we must first create it. If it's a zvol, we do
|
164
|
+
# nothing.
|
165
|
+
|
166
|
+
$stderr.puts "Dest ZFS entity '#{pool.name}/#{master_object.name}' " +
|
167
|
+
"does not exist ... creating"
|
168
|
+
|
169
|
+
if master_object.is_a?(NixAdm::ZFS::Filesystem)
|
170
|
+
if pool.createFilesystem(master_object.name) == false
|
171
|
+
msg = "Error: ZFS entity '#{pool.name}/#{master_object.name}' " + \
|
172
|
+
"does not exist and could not be created"
|
173
|
+
raise msg
|
174
|
+
end
|
175
|
+
elsif master_object.is_a?(NixAdm::ZFS::Volume)
|
176
|
+
if pool.createVolume(master_object.name) == false
|
177
|
+
msg = "Error: ZFS entity '#{pool.name}/#{master_object.name}' " + \
|
178
|
+
"does not exist and could not be created"
|
179
|
+
raise msg
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
attempts += 1
|
184
|
+
end
|
185
|
+
|
186
|
+
if block_given?
|
187
|
+
yield master_object, slave_object
|
188
|
+
end
|
189
|
+
|
190
|
+
if admin.replicate(master_object, slave_object) == true
|
191
|
+
endDestZfsReplication slave_object
|
192
|
+
else
|
193
|
+
failed = true
|
194
|
+
msg = "FAILED: #{job} - #{admin.status.msg}"
|
195
|
+
log msg, admin.status.val
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
if failed == false
|
200
|
+
endSourceZfsReplication master_object
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def startSourceZfsReplication(master_object)
|
206
|
+
return success()
|
207
|
+
end
|
208
|
+
|
209
|
+
def endDestZfsReplication(slave_object)
|
210
|
+
return success()
|
211
|
+
end
|
212
|
+
|
213
|
+
def endSourceZfsReplication(master_object)
|
214
|
+
return success()
|
215
|
+
end
|
216
|
+
|
217
|
+
# Trim snaphots on given filesystem
|
218
|
+
#
|
219
|
+
# @param host The host name
|
220
|
+
# @param filesystems A list of filesystems to trim snapshots
|
221
|
+
def trim(host, objects)
|
222
|
+
admin = NixAdm::ZFS::Admin.new(host)
|
223
|
+
|
224
|
+
objects.each do |object|
|
225
|
+
master_object = admin.object(object)
|
226
|
+
|
227
|
+
if not master_object.nil?
|
228
|
+
master_object.trimSnapshots()
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
end
|
234
|
+
|
235
|
+
class PrimaryBackup < Backup
|
236
|
+
|
237
|
+
def initialize(host)
|
238
|
+
super
|
239
|
+
end
|
240
|
+
|
241
|
+
def startSourceZfsReplication(master_object)
|
242
|
+
return master_object.createSnapshot()
|
243
|
+
end
|
244
|
+
|
245
|
+
def endSourceZfsReplication(master_object)
|
246
|
+
return master_object.trimSnapshots()
|
247
|
+
end
|
248
|
+
|
249
|
+
end # module PrimaryBackup
|
250
|
+
|
251
|
+
class SecondaryBackup < Backup
|
252
|
+
|
253
|
+
def initialize(host)
|
254
|
+
super
|
255
|
+
end
|
256
|
+
|
257
|
+
def endDestZfsReplication(slave_object)
|
258
|
+
return slave_object.trimSnapshots()
|
259
|
+
end
|
260
|
+
|
261
|
+
def endSourceZfsReplication(master_object)
|
262
|
+
return master_object.trimSnapshots()
|
263
|
+
end
|
264
|
+
|
265
|
+
end # module SecondaryBackup
|
266
|
+
|
267
|
+
end # module NixAdm
|
@@ -0,0 +1,217 @@
|
|
1
|
+
require 'nixadm/util'
|
2
|
+
require 'nixadm/backup'
|
3
|
+
|
4
|
+
module NixAdm
|
5
|
+
module PostgreSQL
|
6
|
+
|
7
|
+
module Replication
|
8
|
+
|
9
|
+
class Node
|
10
|
+
|
11
|
+
include NixAdm::Util
|
12
|
+
|
13
|
+
def initialize(host, config_file=nil)
|
14
|
+
@host = host
|
15
|
+
@db = connectDb(host, config_file)
|
16
|
+
|
17
|
+
if @db.nil?
|
18
|
+
raise "Connection failed to #{host}"
|
19
|
+
end
|
20
|
+
|
21
|
+
logSystemInit(config_file)
|
22
|
+
@logfields[:system] = 'psql'
|
23
|
+
end
|
24
|
+
|
25
|
+
def status()
|
26
|
+
return @db.exec('select * from pg_stat_replication')
|
27
|
+
end
|
28
|
+
|
29
|
+
end # class Node
|
30
|
+
|
31
|
+
# Primary database instance
|
32
|
+
class Primary < Node
|
33
|
+
|
34
|
+
include NixAdm::Util
|
35
|
+
|
36
|
+
attr_reader :db
|
37
|
+
|
38
|
+
def initialize(host, config_file=nil)
|
39
|
+
super
|
40
|
+
end
|
41
|
+
|
42
|
+
def currentLsm()
|
43
|
+
return @db.exec('select pg_current_wal_lsn()')[0]
|
44
|
+
end
|
45
|
+
|
46
|
+
end # class Master
|
47
|
+
|
48
|
+
# Hot standby instance
|
49
|
+
class Secondary < Node
|
50
|
+
|
51
|
+
include NixAdm::Util
|
52
|
+
|
53
|
+
attr_reader :primary
|
54
|
+
|
55
|
+
def initialize(primary, host, config_file=nil)
|
56
|
+
@primary = primary
|
57
|
+
|
58
|
+
super(host, config_file)
|
59
|
+
end
|
60
|
+
|
61
|
+
def currentLsm()
|
62
|
+
return @db.exec('select pg_last_wal_receive_lsn()')[0]
|
63
|
+
end
|
64
|
+
|
65
|
+
end # class Replica
|
66
|
+
|
67
|
+
end # module Replication
|
68
|
+
|
69
|
+
# This class exports an entire PostgreSQL database cluster to $backup_dir/db,
|
70
|
+
# including the relevant global settings.
|
71
|
+
#
|
72
|
+
# Example:
|
73
|
+
#
|
74
|
+
# export = Export.new('db', { :debug => true })
|
75
|
+
# export.run()
|
76
|
+
|
77
|
+
class Export < NixAdm::Backup
|
78
|
+
|
79
|
+
attr_accessor :filesystems, :targets
|
80
|
+
|
81
|
+
def initialize(host, options = {})
|
82
|
+
super host, options: options
|
83
|
+
|
84
|
+
@ssh_opts = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
|
85
|
+
end
|
86
|
+
|
87
|
+
def log(msg, code=0)
|
88
|
+
$stderr.puts msg
|
89
|
+
end
|
90
|
+
|
91
|
+
#------------------------------------------------------------------------------
|
92
|
+
# Main backup function
|
93
|
+
#------------------------------------------------------------------------------
|
94
|
+
|
95
|
+
def run()
|
96
|
+
begin
|
97
|
+
backup_databases()
|
98
|
+
rescue Exception => ex
|
99
|
+
log "Backup failed: #{ex.to_s}\n #{ex.backtrace}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
#------------------------------------------------------------------------------
|
104
|
+
# Individual backup procedures
|
105
|
+
#------------------------------------------------------------------------------
|
106
|
+
|
107
|
+
# This does a pg_dump of all databases in plaintext format. It gzips them all.
|
108
|
+
def backup_databases()
|
109
|
+
cd backup_dir('db')
|
110
|
+
|
111
|
+
# List of databases on server
|
112
|
+
databases = listDatabases()
|
113
|
+
|
114
|
+
# List of databases to exclude
|
115
|
+
exclude =
|
116
|
+
{
|
117
|
+
'template0' => true,
|
118
|
+
'template1' => true
|
119
|
+
}
|
120
|
+
|
121
|
+
# Backup individual databases
|
122
|
+
databases.each do |db|
|
123
|
+
next if exclude.has_key?(db)
|
124
|
+
|
125
|
+
begin
|
126
|
+
log "Dumping: #{db}"
|
127
|
+
store @host, "pg_dump -p 5433 #{db}", "#{db}.db"
|
128
|
+
rescue
|
129
|
+
puts $!
|
130
|
+
log "Failed #{@host} #{db}: #{$?.to_s}", 1
|
131
|
+
next
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
# Dump the globals
|
136
|
+
globals_file = 'globals.sql'
|
137
|
+
log "Dumping globals to #{globals_file}"
|
138
|
+
store @host, 'pg_dumpall -g -p 5433', globals_file
|
139
|
+
end
|
140
|
+
|
141
|
+
protected
|
142
|
+
|
143
|
+
def listDatabases()
|
144
|
+
command = %Q{ psql -At -p 5433 postgres -c 'select datname from pg_database' }
|
145
|
+
pipeline = resolveCommand(command)
|
146
|
+
|
147
|
+
dbs = []
|
148
|
+
exec([pipeline]) do |line|
|
149
|
+
dbs << line.strip
|
150
|
+
end
|
151
|
+
|
152
|
+
return dbs
|
153
|
+
end
|
154
|
+
|
155
|
+
end # class Export
|
156
|
+
|
157
|
+
# This class imports everything exported from the Export class. Before
|
158
|
+
# importing, you need to make sure you initialize your database with proper
|
159
|
+
# encoding and collation. To do so, switch user to postgres. Then run:
|
160
|
+
#
|
161
|
+
# /usr/local/bin/initdb --pgdata=/var/lib/pgsql9/data/ -E 'UTF-8' \
|
162
|
+
# --lc-collate='en_US.UTF-8' --lc-ctype='en_US.UTF-8';
|
163
|
+
#
|
164
|
+
# /usr/local/bin/createuser -sl root
|
165
|
+
#
|
166
|
+
# Then start Postgres. Now you can import databases
|
167
|
+
|
168
|
+
class Import < NixAdm::Backup
|
169
|
+
|
170
|
+
def initialize(host, options = {})
|
171
|
+
super host, options: options
|
172
|
+
|
173
|
+
@ssh_opts = '-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no'
|
174
|
+
end
|
175
|
+
|
176
|
+
#------------------------------------------------------------------------------
|
177
|
+
# Main backup function
|
178
|
+
#------------------------------------------------------------------------------
|
179
|
+
|
180
|
+
def run()
|
181
|
+
begin
|
182
|
+
import_databases()
|
183
|
+
rescue Exception => ex
|
184
|
+
log "Import failed: #{ex.to_s}\n #{ex.backtrace}"
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def import_databases()
|
189
|
+
dbs = Dir.entries($backup_dir)
|
190
|
+
|
191
|
+
# Restore globals first
|
192
|
+
system "psql -p 5433 -d postgres -f #{$backup_dir}/globals.sql"
|
193
|
+
|
194
|
+
dbs.each do |file|
|
195
|
+
next if File.extname(file) != '.db'
|
196
|
+
|
197
|
+
db = file.split('.')[0]
|
198
|
+
puts "Loading #{db}"
|
199
|
+
|
200
|
+
sql = %q{ select count(*) from pg_database where datname='dba' }
|
201
|
+
count = %x{ psql -p 5433 postgres -At -c "#{sql}" }
|
202
|
+
count.strip!
|
203
|
+
|
204
|
+
if count == '1'
|
205
|
+
%x{ psql -p 5433 postgres -c 'drop database #{db}' }
|
206
|
+
end
|
207
|
+
|
208
|
+
%x{ psql -p 5433 postgres -c 'create database #{db}' }
|
209
|
+
%x{ psql -p 5433 -d #{db} -f #{$backup_dir}/#{file} }
|
210
|
+
%x{ psql -p 5433 #{db} -c 'analyze' }
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
end # class Import
|
215
|
+
|
216
|
+
end # module PostgreSQL
|
217
|
+
end # module NixAdm
|