Skip to content

Commit 9c2ef76

Browse files
Merge pull request #250 from puppetlabs/SOLARCH-581
(SOLARCH-581) Implement restore plan
2 parents 37148c2 + c788897 commit 9c2ef76

8 files changed

+428
-7
lines changed

plans/backup.pp

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
# @api private
12
# @summary Backup the core user settings for puppet infrastructure
23
#
34
# This plan can backup data as outlined at insert doc
@@ -110,7 +111,8 @@
110111

111112
run_command(@("CMD"/L), $primary_target)
112113
umask 0077 \
113-
&& tar -czf ${shellquote($backup_directory)}.tar.gz ${shellquote($backup_directory)} \
114+
&& cd ${shellquote(dirname($backup_directory))} \
115+
&& tar -czf ${shellquote($backup_directory)}.tar.gz ${shellquote(basename($backup_directory))} \
114116
&& rm -rf ${shellquote($backup_directory)}
115117
| CMD
116118

plans/restore.pp

+235
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# @api private
2+
# @summary Restore the core user settings for puppet infrastructure from backup
3+
#
4+
# This plan can restore data to puppet infrastructure for DR and rebuilds
5+
#
6+
plan peadm::restore (
7+
# This plan should be run on the primary server
8+
Peadm::SingleTargetSpec $targets,
9+
10+
# Which data to restore
11+
Peadm::Recovery_opts $restore = {},
12+
13+
# Path to the recovery tarball
14+
Pattern[/.*\.tar\.gz$/] $input_file,
15+
) {
16+
peadm::assert_supported_bolt_version()
17+
18+
$recovery_opts = (peadm::recovery_opts_default() + $restore)
19+
$cluster = run_task('peadm::get_peadm_config', $targets).first.value
20+
$arch = peadm::assert_supported_architecture(
21+
getvar('cluster.params.primary_host'),
22+
getvar('cluster.params.replica_host'),
23+
getvar('cluster.params.primary_postgresql_host'),
24+
getvar('cluster.params.replica_postgresql_host'),
25+
getvar('cluster.params.compiler_hosts'),
26+
)
27+
28+
$primary_target = peadm::get_targets(getvar('cluster.params.primary_host'), 1)
29+
$replica_target = peadm::get_targets(getvar('cluster.params.replica_host'), 1)
30+
$compiler_targets = peadm::get_targets(getvar('cluster.params.compiler_hosts'))
31+
32+
# Determine the array of targets to which the PuppetDB PostgreSQL database
33+
# should be restored to. This could be as simple as just the primary server,
34+
# or it could be two separate PostgreSQL servers.
35+
$puppetdb_postgresql_targets = peadm::flatten_compact([
36+
getvar('cluster.params.primary_postgresql_host') ? {
37+
undef => $primary_target,
38+
default => peadm::get_targets(getvar('cluster.params.primary_postgresql_host'), 1),
39+
},
40+
getvar('cluster.params.replica_postgresql_host') ? {
41+
undef => $replica_target,
42+
default => peadm::get_targets(getvar('cluster.params.replica_postgresql_host'), 1),
43+
},
44+
])
45+
46+
$puppetdb_targets = peadm::flatten_compact([
47+
$primary_target,
48+
$replica_target,
49+
$compiler_targets,
50+
])
51+
52+
$recovery_directory = "${dirname($input_file)}/${basename("${input_file}", '.tar.gz')}"
53+
54+
run_command(@("CMD"/L), $primary_target)
55+
umask 0077 \
56+
&& cd ${shellquote(dirname($recovery_directory))} \
57+
&& tar -xzf ${shellquote($input_file)}
58+
| CMD
59+
60+
# Map of recovery option name to array of database hosts to restore the
61+
# relevant .dump content to.
62+
$restore_databases = {
63+
'orchestrator' => [$primary_target],
64+
'activity' => [$primary_target],
65+
'rbac' => [$primary_target],
66+
'puppetdb' => $puppetdb_postgresql_targets,
67+
}.filter |$key,$_| {
68+
$recovery_opts[$key] == true
69+
}
70+
71+
if getvar('recovery_opts.classifier') {
72+
out::message('# Restoring classification')
73+
run_task('peadm::backup_classification', $primary_target,
74+
directory => $recovery_directory
75+
)
76+
out::message("# Backed up current classification to ${recovery_directory}/classification_backup.json")
77+
78+
run_task('peadm::transform_classification_groups', $primary_target,
79+
source_directory => "${recovery_directory}/classifier",
80+
working_directory => $recovery_directory
81+
)
82+
83+
run_task('peadm::restore_classification', $primary_target,
84+
classification_file => "${recovery_directory}/classification_backup.json",
85+
)
86+
}
87+
88+
if getvar('recovery_opts.ca') {
89+
out::message('# Restoring ca and ssl certificates')
90+
run_command(@("CMD"/L), $primary_target)
91+
/opt/puppetlabs/bin/puppet-backup restore \
92+
--scope=certs \
93+
--tempdir=${shellquote($recovery_directory)} \
94+
--force \
95+
${shellquote($recovery_directory)}/classifier/pe_backup-*tgz
96+
| CMD
97+
}
98+
99+
# Use PuppetDB's /pdb/admin/v1/archive API to SAVE data currently in PuppetDB.
100+
# Otherwise we'll completely lose it if/when we restore.
101+
# TODO: consider adding a heuristic to skip when innappropriate due to size
102+
# or other factors.
103+
if getvar('recovery_opts.puppetdb') {
104+
run_command(@("CMD"/L), $primary_target)
105+
/opt/puppetlabs/bin/puppet-db export ${shellquote($recovery_directory)}/puppetdb-archive.bin
106+
| CMD
107+
}
108+
109+
## shutdown services
110+
run_command(@("CMD"/L), $primary_target)
111+
systemctl stop pe-console-services pe-nginx pxp-agent pe-puppetserver \
112+
pe-orchestration-services puppet pe-puppetdb
113+
| CMD
114+
115+
# Restore secrets/keys.json if it exists
116+
out::message('# Restoring ldap secret key if it exists')
117+
run_command(@("CMD"/L), $primary_target)
118+
test -f ${shellquote($recovery_directory)}/rbac/keys.json \
119+
&& cp -rp ${shellquote($recovery_directory)}/keys.json /etc/puppetlabs/console-services/conf.d/secrets/ \
120+
|| echo secret ldap key doesnt exist
121+
| CMD
122+
123+
# IF restoring orchestrator restore the secrets to /etc/puppetlabs/orchestration-services/conf.d/secrets/
124+
if getvar('recovery_opts.orchestrator') {
125+
out::message('# Restoring orchestrator secret keys')
126+
run_command(@("CMD"/L), $primary_target)
127+
cp -rp ${shellquote($recovery_directory)}/orchestrator/secrets/* /etc/puppetlabs/orchestration-services/conf.d/secrets/
128+
| CMD
129+
}
130+
131+
#$database_to_restore.each |Integer $index, Boolean $value | {
132+
$restore_databases.each |$name,$database_targets| {
133+
out::message("# Restoring ${name} database")
134+
$dbname = "pe-${shellquote($name)}"
135+
136+
# Drop pglogical extensions and schema if present
137+
run_command(@("CMD"/L), $database_targets)
138+
su - pe-postgres -s /bin/bash -c \
139+
"/opt/puppetlabs/server/bin/psql \
140+
--tuples-only \
141+
-d '${dbname}' \
142+
-c 'DROP SCHEMA IF EXISTS pglogical CASCADE;'"
143+
| CMD
144+
145+
run_command(@("CMD"/L), $database_targets)
146+
su - pe-postgres -s /bin/bash -c \
147+
"/opt/puppetlabs/server/bin/psql \
148+
-d '${dbname}' \
149+
-c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'"
150+
| CMD
151+
152+
# To allow db user to restore the database grant temporary privileges
153+
run_command(@("CMD"/L), $database_targets)
154+
su - pe-postgres -s /bin/bash -c \
155+
"/opt/puppetlabs/server/bin/psql \
156+
-d '${dbname}' \
157+
-c 'ALTER USER \"${dbname}\" WITH SUPERUSER;'"
158+
| CMD
159+
160+
# Restore database. If there are multiple database restore targets, perform
161+
# the restore(s) in parallel.
162+
parallelize($database_targets) |$database_target| {
163+
run_command(@("CMD"/L), $primary_target)
164+
/opt/puppetlabs/server/bin/pg_restore \
165+
-j 4 \
166+
-d "sslmode=verify-ca \
167+
host=${shellquote($database_target.peadm::certname())} \
168+
sslcert=/etc/puppetlabs/puppetdb/ssl/${shellquote($primary_target.peadm::certname())}.cert.pem \
169+
sslkey=/etc/puppetlabs/puppetdb/ssl/${shellquote($primary_target.peadm::certname())}.private_key.pem \
170+
sslrootcert=/etc/puppetlabs/puppet/ssl/certs/ca.pem \
171+
dbname=${dbname} \
172+
user=${dbname}" \
173+
-Fd ${recovery_directory}/${name}/${dbname}.dump.d
174+
| CMD
175+
}
176+
177+
# Remove db user privileges post restore
178+
run_command(@("CMD"/L), $database_targets)
179+
su - pe-postgres -s /bin/bash -c \
180+
"/opt/puppetlabs/server/bin/psql \
181+
-d '${dbname}' \
182+
-c 'ALTER USER \"${dbname}\" WITH NOSUPERUSER;'"
183+
| CMD
184+
185+
# Drop pglogical extension and schema (again) if present after db restore
186+
run_command(@("CMD"/L), $database_targets)
187+
su - pe-postgres -s /bin/bash -c \
188+
"/opt/puppetlabs/server/bin/psql \
189+
--tuples-only \
190+
-d '${dbname}' \
191+
-c 'DROP SCHEMA IF EXISTS pglogical CASCADE;'"
192+
| CMD
193+
194+
run_command(@("CMD"/L), $database_targets)
195+
su - pe-postgres -s /bin/bash -c \
196+
"/opt/puppetlabs/server/bin/psql \
197+
-d '${dbname}' \
198+
-c 'DROP EXTENSION IF EXISTS pglogical CASCADE;'"
199+
| CMD
200+
}
201+
202+
# Use `puppet infra` to ensure correct file permissions, restart services,
203+
# etc. Make sure not to try and get config data from the classifier, which
204+
# isn't yet up and running.
205+
run_command(@("CMD"/L), $primary_target)
206+
/opt/puppetlabs/bin/puppet-infrastructure configure --no-recover
207+
| CMD
208+
209+
# If we have replicas reinitalise them
210+
run_command(@("CMD"/L), $replica_target)
211+
/opt/puppetlabs/bin/puppet-infra reinitialize replica -y
212+
| CMD
213+
214+
# Use PuppetDB's /pdb/admin/v1/archive API to MERGE previously saved data
215+
# into the restored database.
216+
# TODO: consider adding a heuristic to skip when innappropriate due to size
217+
# or other factors.
218+
if getvar('recovery_opts.puppetdb') {
219+
run_command(@("CMD"/L), $primary_target)
220+
/opt/puppetlabs/bin/puppet-db import ${shellquote($recovery_directory)}/puppetdb-archive.bin
221+
| CMD
222+
}
223+
224+
# Run Puppet to pick up last remaining config tweaks
225+
run_task('peadm::puppet_runonce', $primary_target)
226+
227+
apply($primary_target){
228+
file { $recovery_directory :
229+
ensure => 'absent',
230+
force => true
231+
}
232+
}
233+
234+
return("success")
235+
}

spec/plans/restore_spec.rb

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
require 'spec_helper'
2+
3+
describe 'peadm::restore' do
4+
include BoltSpec::Plans
5+
let(:params) { { 'primary_host' => 'primary', 'backup_timestamp' => '2022-03-29_16:57:41' } }
6+
7+
it 'runs with default params' do
8+
allow_apply
9+
pending('a lack of support for functions requires a workaround to be written')
10+
expect_task('peadm::get_peadm_config').always_return({ 'primary_postgresql_host' => 'postgres' })
11+
expect_out_message.with_params('# Backing up ca and ssl certificates')
12+
# The commands all have a timestamp in them and frankly its proved to hard with bolt spec to work this out
13+
allow_any_command
14+
expect_out_message.with_params('# Restoring classification')
15+
expect_out_message.with_params('# Backed up current classification to /tmp/classification_backup.json')
16+
expect_out_message.with_params('# Restoring ca and ssl certificates')
17+
expect_out_message.with_params('# Restoring database pe-orchestrator')
18+
expect_out_message.with_params('# Restoring database pe-activity')
19+
expect_out_message.with_params('# Restoring database pe-rbac')
20+
expect_out_message.with_params('# Restoring classification')
21+
expect_task('peadm::backup_classification')
22+
expect(run_plan('peadm::restore', params)).to be_ok
23+
end
24+
end

tasks/get_peadm_config.rb

+13-6
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,19 @@ def config
1818
# Compute values
1919
primary = groups.pinned('PE Master')
2020
replica = groups.pinned('PE HA Replica')
21-
server_a = server('puppet/server', 'A')
22-
server_b = server('puppet/server', 'B')
21+
server_a = server('puppet/server', 'A', [primary, replica].compact)
22+
server_b = server('puppet/server', 'B', [primary, replica].compact)
2323
primary_letter = primary.eql?(server_a) ? 'A' : 'B'
2424
replica_letter = primary_letter.eql?('A') ? 'B' : 'A'
25+
26+
configured_postgresql_servers = [
27+
groups.dig('PE Primary A', 'config_data', 'puppet_enterprise::profile::puppetdb', 'database_host'),
28+
groups.dig('PE Primary B', 'config_data', 'puppet_enterprise::profile::puppetdb', 'database_host'),
29+
].compact
30+
2531
postgresql = {
26-
'A' => server('puppet/puppetdb-database', 'A'),
27-
'B' => server('puppet/puppetdb-database', 'B'),
32+
'A' => server('puppet/puppetdb-database', 'A', configured_postgresql_servers),
33+
'B' => server('puppet/puppetdb-database', 'B', configured_postgresql_servers),
2834
}
2935

3036
# Build and return the task output
@@ -78,10 +84,11 @@ def compilers
7884
end
7985
end
8086

81-
def server(role, letter)
87+
def server(role, letter, certname_array)
8288
query = 'inventory[certname] { '\
8389
' trusted.extensions."1.3.6.1.4.1.34380.1.1.9812" = "' + role + '" and ' \
84-
' trusted.extensions."1.3.6.1.4.1.34380.1.1.9813" = "' + letter + '"}'
90+
' trusted.extensions."1.3.6.1.4.1.34380.1.1.9813" = "' + letter + '" and ' \
91+
' certname in ' + certname_array.to_json + '}'
8592

8693
server = pdb_query(query).map { |n| n['certname'] }
8794
raise "More than one #{letter} #{role} server found!" unless server.size <= 1

tasks/restore_classification.json

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"puppet_task_version": 1,
3+
"supports_noop": false,
4+
"description": "A short description of this task",
5+
"parameters": {
6+
"classification_file": {
7+
"type": "String",
8+
"description": "The full path to a backed up or transformed classification file",
9+
"default": "/tmp/transformed_classification.json"
10+
}
11+
}
12+
}

tasks/restore_classification.rb

+45
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
#!/opt/puppetlabs/puppet/bin/ruby
2+
3+
# Puppet Task Name: backup_classification
4+
require 'net/https'
5+
require 'uri'
6+
require 'json'
7+
require 'puppet'
8+
9+
# RestoreClassifiation task class
10+
class RestoreClassification
11+
def initialize(params)
12+
@classification_file = params['classification_file']
13+
end
14+
15+
def execute!
16+
restore_classification
17+
puts "Classification restored from #{@classification_file}"
18+
end
19+
20+
private
21+
22+
def https_client
23+
client = Net::HTTP.new('localhost', '4433')
24+
client.use_ssl = true
25+
client.cert = @cert ||= OpenSSL::X509::Certificate.new(File.read(Puppet.settings[:hostcert]))
26+
client.key = @key ||= OpenSSL::PKey::RSA.new(File.read(Puppet.settings[:hostprivkey]))
27+
client.verify_mode = OpenSSL::SSL::VERIFY_NONE
28+
client
29+
end
30+
31+
def restore_classification
32+
classification = https_client
33+
classification_post = Net::HTTP::Post.new('/classifier-api/v1/import-hierarchy', 'Content-Type' => 'application/json')
34+
classification_post.body = File.read(@classification_file)
35+
classification.request(classification_post)
36+
end
37+
end
38+
# Run the task unless an environment flag has been set, signaling not to. The
39+
# environment flag is used to disable auto-execution and enable Ruby unit
40+
# testing of this task.
41+
unless ENV['RSPEC_UNIT_TEST_MODE']
42+
Puppet.initialize_settings
43+
task = RestoreClassification.new(JSON.parse(STDIN.read))
44+
task.execute!
45+
end
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"description": "Transform the user groups from a source backup to a list of groups on the target server",
3+
"parameters": {
4+
"source_directory": {
5+
"type": "String",
6+
"description": "Location of Source node group yaml file"
7+
},
8+
"working_directory": {
9+
"type": "String",
10+
"description": "Location of target node group yaml file and where to create the transformed file"
11+
}
12+
},
13+
"input_method": "stdin",
14+
"implementations": [
15+
{"name": "transform_classification_groups.py"}
16+
]
17+
}

0 commit comments

Comments
 (0)