Skip to content

Commit bb2086c

Browse files
committed
Adding in samples on how to do a restore of a singular copy of a backup
1 parent 2409a2a commit bb2086c

File tree

6 files changed

+189
-21
lines changed

6 files changed

+189
-21
lines changed

recipes/python/backup-restore/README.md

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ Execution flow of group VM backup and restore script:
140140

141141
This mssql_db_backup_restore.py script demonstrates how to Protect a MSSQL Database or Instance using a protection plan, and perform a alternate recovery of a single database or all user databases using NetBackup APIs.
142142

143-
`python -W ignore recipes/python/backup-restore/mssql_db_backup_restore.py --master_server <master_server> --master_server_port 1556 --master_username <master_username> --master_password <master_password> --mssql_instance <mssql_instance_name> --mssql_database <mssql_database_name> --mssql_server_name <mssql_server_name> --mssql_use_localcreds 0 --mssql_domain <mssql_domain> --mssql_username <mssql_sysadmin_user> --mssql_password <mssql_sysadmin_pwd> --stu_name <storage_unit_used_in_protection_plan> --protection_plan_name <protection_plan_name> --asset_type <mssql_asset_type> --restore_db_prefix <mssql_restore_database_name_prefix> --restore_db_path <mssql_restore_database_path> --recoveralluserdbs <0|1>`
143+
`python -W ignore recipes/python/backup-restore/mssql_db_backup_restore.py --master_server <master_server> --master_server_port 1556 --master_username <master_username> --master_password <master_password> --mssql_instance <mssql_instance_name> --mssql_database <mssql_database_name> --mssql_server_name <mssql_server_name> --mssql_use_localcreds 0 --mssql_domain <mssql_domain> --mssql_username <mssql_sysadmin_user> --mssql_password <mssql_sysadmin_pwd> --stu_name <storage_unit_used_in_protection_plan> --protection_plan_name <protection_plan_name> --asset_type <mssql_asset_type> --restore_db_prefix <mssql_restore_database_name_prefix> --restore_db_path <mssql_restore_database_path> --recover_all_user_dbs <0|1> --recover_from_copy <1|2> --copy_stu_name <storage_unit_used_for_copy>`
144144

145145
All parameters can also be passed as command line arguments.
146146
- `python mssql_db_backup_restore.py -h`
@@ -161,7 +161,9 @@ usage: mssql_db_backup_restore.py [-h] [--master_server MASTER_SERVER]
161161
[--asset_type ASSET_TYPE]
162162
[--restore_db_prefix RESTORE_DB_PREFIX]
163163
[--restore_db_path RESTORE_DB_PATH]
164-
[--recoveralluserdbs RECOVERALLUSERDBS]
164+
[--recover_all_user_dbs RECOVER_ALL_USER_DBS]
165+
[--recover_from_copy RECOVER_FROM_COPY]
166+
[--copy_stu_name COPY_STU_NAME]
165167
Mssql backup and alternate database recovery scenario
166168
167169
Arguments:
@@ -197,8 +199,12 @@ Arguments:
197199
Restore database name prefix
198200
--restore_db_path RESTORE_DB_PATH
199201
Restore database path
200-
--recover_alluserdbs RECOVERALLUSERDBS
201-
Recover all User databases to the mssql_instance specfied with a database name prefix.
202+
--recover_alluserdbs recover_all_user_dbs
203+
Recover all User databases to the mssql_instance specfied with a database name prefix
204+
--recover_from_copy RECOVER_FROM_COPY
205+
Create a policy with a copy and then recover using the specified copy of the backup
206+
--copy_stu_name COPY_STU_NAME
207+
Storage Unit name for the copy
202208
203209
Execution flow of a Single MSSQL database protection and alternate database recovery workflow:
204210
- Login to Master Server get authorization token for API use

recipes/python/backup-restore/common.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,31 @@ def create_protection_plan(baseurl, token, protection_plan_name, storage_unit_na
127127
print(f"Protection plan created successfully:[{protection_plan_id}]")
128128
return protection_plan_id
129129

130+
def run_netbackup_policy(base_url, token, policy_name):
131+
print(f"Run policy:[{policy_name}]")
132+
headers.update({'Authorization': token})
133+
url = base_url + "/admin/manual-backup/"
134+
135+
cur_dir = os.path.dirname(os.path.abspath(__file__))
136+
cur_dir = cur_dir + os.sep + "sample-payloads" + os.sep
137+
file_name = os.path.join(cur_dir, "post_manual_backup.json")
138+
with open(file_name, 'r') as file_handle:
139+
data = json.load(file_handle)
140+
data['data']['attributes']['policyName'] = policy_name
141+
142+
status_code, response_text = rest_request('POST', url, headers, data=data)
143+
validate_response(status_code, 202, response_text)
144+
print(f"Policy run successfully:[{policy_name}]")
145+
return response_text['data'][0]['id']
146+
147+
def delete_netbackup_policy(base_url, token, policy_name):
148+
print(f"Delete policy:[{policy_name}]")
149+
headers.update({'Authorization': token})
150+
url = base_url + f"/config/policies/{policy_name}"
151+
status_code, response_text = rest_request('DELETE', url, headers, data='')
152+
validate_response(status_code, 204, response_text)
153+
print(f"Policy deleted successfully:[{policy_name}]")
154+
130155
# Subscription asset to SLO
131156
def subscription_asset_to_slo(baseurl, token, protection_plan_id, asset_id, is_vm_group=0):
132157
""" This function subscribe the asset/group asset to protection plan """
@@ -287,6 +312,22 @@ def get_recovery_points(baseurl, token, workload_type, asset_id):
287312
recoverypoint_id = ""
288313
return recoverypoint_id
289314

315+
def get_recovery_point_copy_info(baseurl, token, workload_type, recovery_point_id):
316+
""" This function returns the optional information for a given recovery point"""
317+
print(f"Get the recovery point optional info:[{recovery_point_id}]")
318+
headers.update({'Authorization': token})
319+
include = f"optional{workload_type.capitalize()}RecoveryPointInfo"
320+
url = f"{baseurl}recovery-point-service/workloads/{workload_type}/"\
321+
f"recovery-points/{recovery_point_id}?include={include}"
322+
status_code, response_text = rest_request('GET', url, headers)
323+
validate_response(status_code, 200, response_text)
324+
if (len(response_text['included'])>0):
325+
print(response_text)
326+
copy_info = response_text['included'][0]['attributes']['backupImageCopyInfo']
327+
else:
328+
copy_info = []
329+
return copy_info
330+
290331
# Validate the response code of the request
291332
def validate_response(actual_status_code, expected_status_code, response_text):
292333
""" This function validate the response status code with expected response code """

recipes/python/backup-restore/mssql_db_backup_restore.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@
2727
PARSER.add_argument("--asset_type", type=str, help="MSSQL asset type (AvailabilityGroup, Instance, Database)", required=False)
2828
PARSER.add_argument("--restore_db_prefix", type=str, help="Restore database name prefix", required=True)
2929
PARSER.add_argument("--restore_db_path", type=str, help="Restore database path", required=True)
30-
PARSER.add_argument("--recoveralluserdbs", type=int, help="Recover all user databases", required=False, default=0)
30+
PARSER.add_argument("--recover_all_user_dbs", type=int, help="Recover all user databases", required=False, default=0)
31+
PARSER.add_argument("--recover_from_copy", type=int, help="Recover from the copy number specified", choices=[1,2])
32+
PARSER.add_argument("--copy_stu_name", type=str, help="Storage Unit name for copies", required=False)
3133

3234
ARGS = PARSER.parse_args()
3335

@@ -66,27 +68,32 @@
6668

6769
print(f"Start Discovery on the instance [{INSTANCE_NAME}] on the host [{ARGS.mssql_server_name}]")
6870
workload_mssql.mssql_instance_deepdiscovery(BASEURL, TOKEN, INSTANCE_ID)
69-
# create protection plan and subscribe the assettype to it
70-
PROTECTION_PLAN_ID = workload_mssql.create_mssql_protection_plan(BASEURL, TOKEN, ARGS.protection_plan_name, ARGS.stu_name, "SQL_SERVER")
71-
# update protection plan to set MSSQL policy settings to skip offline databases
72-
workload_mssql.update_protection_plan_mssql_attr(BASEURL, TOKEN, ARGS.protection_plan_name, PROTECTION_PLAN_ID, skip_offline_db=1)
73-
SUBSCRIPTION_ID = common.subscription_asset_to_slo(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)
71+
if(ARGS.recover_from_copy):
72+
workload_mssql.create_netbackup_policy(BASEURL, TOKEN, ARGS.protection_plan_name, ARGS.mssql_server_name, ARGS.stu_name, ARGS.copy_stu_name)
73+
BACKUP_JOB_ID = common.run_netbackup_policy(BASEURL, TOKEN, ARGS.protection_plan_name)
7474

75-
# MSSQL backup restore
76-
print("Start MSSQL backup")
77-
BACKUP_JOB_ID = common.protection_plan_backupnow(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)
75+
else:
76+
# create protection plan and subscribe the assettype to it
77+
PROTECTION_PLAN_ID = workload_mssql.create_mssql_protection_plan(BASEURL, TOKEN, ARGS.protection_plan_name, ARGS.stu_name, "SQL_SERVER")
78+
# update protection plan to set MSSQL policy settings to skip offline databases
79+
workload_mssql.update_protection_plan_mssql_attr(BASEURL, TOKEN, ARGS.protection_plan_name, PROTECTION_PLAN_ID, skip_offline_db=1)
80+
SUBSCRIPTION_ID = common.subscription_asset_to_slo(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)
81+
82+
# MSSQL backup restore
83+
print("Start MSSQL backup")
84+
BACKUP_JOB_ID = common.protection_plan_backupnow(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)
7885
#timeout is set at 300 seconds (5 mins to keep looking if the backups are complete)
7986
common.verify_job_state(BASEURL, TOKEN, BACKUP_JOB_ID, 'DONE', timeout=300)
8087

8188
# give nbwebservice 30 seconds to service any queued tasks, before launching recoveries
8289
time.sleep(30)
83-
if (ARGS.recoveralluserdbs != 1):
90+
if (ARGS.recover_all_user_dbs != 1):
8491
# fetch the asset
8592
RECOVERY_ASSET_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "database", ARGS.mssql_server_name, DATABASE_NAME, INSTANCE_NAME)
8693
RECOVERY_POINT = common.get_recovery_points(BASEURL, TOKEN, WORKLOAD_TYPE, RECOVERY_ASSET_ID)
8794
print(f"Perform Mssql single database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
8895
ALT_DB = ALT_DB + DATABASE_NAME
89-
RECOVERY_JOB_ID = workload_mssql.create_mssql_recovery_request(BASEURL, TOKEN, "post_mssql_singledb_alt_recovery.json", RECOVERY_POINT, RECOVERY_ASSET_ID, ARGS.mssql_username, ARGS.mssql_domain, ARGS.mssql_password, ALT_DB, ALT_DB_PATH, INSTANCE_NAME, ARGS.mssql_server_name)
96+
RECOVERY_JOB_ID = workload_mssql.create_mssql_recovery_request(BASEURL, TOKEN, "post_mssql_singledb_alt_recovery.json", RECOVERY_POINT, RECOVERY_ASSET_ID, ARGS.mssql_username, ARGS.mssql_domain, ARGS.mssql_password, ALT_DB, ALT_DB_PATH, INSTANCE_NAME, ARGS.mssql_server_name, ARGS.recover_from_copy)
9097
print(f"Recovery initiated , follow Job #: [{RECOVERY_JOB_ID}]")
9198
else:
9299
print(f"Perform alternate recovery of all databases")
@@ -104,13 +111,17 @@
104111
if (RECOVERY_POINT != ""):
105112
print(f"Perform Mssql database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
106113
ALT_DB = ARGS.restore_db_prefix + DATABASE_NAME
107-
RECOVERY_JOB_ID = workload_mssql.create_mssql_recovery_request(BASEURL, TOKEN, "post_mssql_singledb_alt_recovery.json", RECOVERY_POINT, RECOVERY_ASSET_ID, ARGS.mssql_username, ARGS.mssql_domain, ARGS.mssql_password, ALT_DB, ALT_DB_PATH, INSTANCE_NAME, ARGS.mssql_server_name)
114+
RECOVERY_JOB_ID = workload_mssql.create_mssql_recovery_request(BASEURL, TOKEN, "post_mssql_singledb_alt_recovery.json", RECOVERY_POINT, RECOVERY_ASSET_ID, ARGS.mssql_username, ARGS.mssql_domain, ARGS.mssql_password, ALT_DB, ALT_DB_PATH, INSTANCE_NAME, ARGS.mssql_server_name, ARGS.recover_from_copy)
108115
else:
109116
print(f"Skipping recovery, could not find RecoveryPoint for [{DATABASE_NAME}] assetid [{RECOVERY_ASSET_ID}]")
110117

111118
finally:
112119
print("Start cleanup")
113-
# Cleanup the created protection plan
114-
common.remove_subscription(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ID)
115-
common.remove_protectionplan(BASEURL, TOKEN, PROTECTION_PLAN_ID)
120+
if(ARGS.recover_from_copy):
121+
# Cleanup the created policy
122+
common.delete_netbackup_policy(BASEURL, TOKEN, ARGS.protection_plan_name)
123+
else:
124+
# Cleanup the created protection plan
125+
common.remove_subscription(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ID)
126+
common.remove_protectionplan(BASEURL, TOKEN, PROTECTION_PLAN_ID)
116127
workload_mssql.remove_mssql_credential(BASEURL, TOKEN, CREDENTIAL_ID)
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"data": {
3+
"type": "policy",
4+
"id": "policy",
5+
"attributes": {
6+
"policy": {
7+
"policyName": "policy",
8+
"policyType": "MS-SQL-Server",
9+
"policyAttributes": {},
10+
"clients": [
11+
{
12+
"databaseName": "$ALL",
13+
"hostName": "host",
14+
"instanceName": "MSSQLSERVER"
15+
}
16+
],
17+
"schedules": [
18+
{
19+
"backupCopies": {
20+
"copies": [
21+
{
22+
"failStrategy": "Continue",
23+
"mediaOwner": "*ANY*",
24+
"retentionLevel": 1,
25+
"retentionPeriod": {
26+
"value": 2,
27+
"unit": "WEEKS"
28+
},
29+
"storage": "storageUnit"
30+
},
31+
{
32+
"failStrategy": "Continue",
33+
"mediaOwner": "*ANY*",
34+
"retentionLevel": 1,
35+
"retentionPeriod": {
36+
"value": 2,
37+
"unit": "WEEKS"
38+
},
39+
"storage": "copyStorageUnit"
40+
}
41+
],
42+
"priority": -1
43+
},
44+
"backupType": "Full Backup",
45+
"frequencySeconds": 604800,
46+
"mediaMultiplexing": 1,
47+
"retriesAllowedAfterRunDay": false,
48+
"scheduleName": "Full",
49+
"scheduleType": "Frequency",
50+
"snapshotOnly": false,
51+
"startWindow": [
52+
{
53+
"dayOfWeek": 1,
54+
"startSeconds": 21600,
55+
"durationSeconds": 43200
56+
}
57+
],
58+
"storageIsSLP": false,
59+
"syntheticBackup": false
60+
}
61+
],
62+
63+
"backupSelections": {
64+
"selections": [
65+
"WHOLE_DATABASE"
66+
]
67+
}
68+
}
69+
}
70+
}
71+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"data": {
3+
"attributes": {
4+
"policyName": "policy",
5+
"scheduleName": "Full"
6+
},
7+
"type": "backupRequest"
8+
}
9+
}

recipes/python/backup-restore/workload_mssql.py

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,27 @@ def create_mssql_protection_plan(baseurl, token, protection_plan_name, storage_u
114114
print(f"Protection plan created successfully:[{protection_plan_id}]")
115115
return protection_plan_id
116116

117+
def create_netbackup_policy(base_url, token, policy_name, client, storage_unit_name, copy_storage_unit_name):
118+
print(f"Create policy:[{policy_name}]")
119+
headers.update({'Authorization': token})
120+
url = base_url + "/config/policies/"
121+
122+
cur_dir = os.path.dirname(os.path.abspath(__file__))
123+
cur_dir = cur_dir + os.sep + "sample-payloads" + os.sep
124+
file_name = os.path.join(cur_dir, "create_mssql_policy_template.json")
125+
with open(file_name, 'r') as file_handle:
126+
data = json.load(file_handle)
127+
data['data']['attributes']['policy']['policyName'] = policy_name
128+
data['data']['id'] = policy_name
129+
data['data']['attributes']['policy']['clients'][0]['hostName'] = client
130+
data['data']['attributes']['policy']['policyAttributes']['storage'] = storage_unit_name
131+
data['data']['attributes']['policy']['schedules'][0]['backupCopies']['copies'][0]['storage'] = storage_unit_name
132+
data['data']['attributes']['policy']['schedules'][0]['backupCopies']['copies'][1]['storage'] = copy_storage_unit_name
133+
134+
status_code, response_text = common.rest_request('POST', url, headers, data=data)
135+
common.validate_response(status_code, 204, response_text)
136+
print(f"Policy created successfully:[{policy_name}]")
137+
117138
# Update SLO Mssql attributes
118139
def update_protection_plan_mssql_attr(baseurl, token, protection_plan_name, protection_plan_id, skip_offline_db=0):
119140
""" Update SLO with mssql attributes """
@@ -244,7 +265,7 @@ def create_and_register_mssql_instance(baseurl, token, instance_name, server_nam
244265
status_code, response_text = common.rest_request('GET', url, headers)
245266

246267
# Create mssql recovery request
247-
def create_mssql_recovery_request(baseurl, token, mssql_recovery_input, rp_id, asset_id, mssql_sysadm_user, mssql_sysadm_domain, mssql_sysadm_pwd, mssql_alt_db_name, mssql_alt_db_path, mssql_instance_name, mssql_server_name):
268+
def create_mssql_recovery_request(baseurl, token, mssql_recovery_input, rp_id, asset_id, mssql_sysadm_user, mssql_sysadm_domain, mssql_sysadm_pwd, mssql_alt_db_name, mssql_alt_db_path, mssql_instance_name, mssql_server_name, recover_from_copy):
248269
""" This function is for mssql recovery request"""
249270
print(f"create mssql recovery request:[{mssql_recovery_input}]")
250271
headers.update({'Authorization': token})
@@ -264,7 +285,16 @@ def create_mssql_recovery_request(baseurl, token, mssql_recovery_input, rp_id, a
264285
data['data']['attributes']['alternateRecoveryOptions']['instanceName'] = mssql_instance_name
265286
data['data']['attributes']['alternateRecoveryOptions']['client'] = mssql_server_name
266287
data['data']['attributes']['alternateRecoveryOptions']['alternateFileLocation']['renameAllFilesToSameLocation'] = mssql_alt_db_path
267-
288+
if(recover_from_copy):
289+
info = common.get_recovery_point_copy_info(baseurl, token, 'mssql', rp_id)
290+
if(len(info)>0):
291+
data['data']['attributes']['recoveryOptions']['mssqlRecoveryCopyInfo'] = {}
292+
data['data']['attributes']['recoveryOptions']['mssqlRecoveryCopyInfo']['fullRecoveryCollection'] = [{}]
293+
for copy in info[0]['copies']:
294+
if(copy['copyNumber'] == recover_from_copy):
295+
copy['storage'].pop('sType',None)
296+
data['data']['attributes']['recoveryOptions']['mssqlRecoveryCopyInfo']['fullRecoveryCollection'][0]['backupId'] = info[0]['backupId']
297+
data['data']['attributes']['recoveryOptions']['mssqlRecoveryCopyInfo']['fullRecoveryCollection'][0]['storage'] = copy['storage']
268298
status_code, response_text = common.rest_request('POST', url, headers, data=data)
269299
common.validate_response(status_code, 201, response_text)
270300
recovery_job_id = response_text['data']['id']

0 commit comments

Comments
 (0)