nixadm 1.0.6
Sign up to get free protection for your applications and to get access to all the features.
- 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
|