Skip to content

Commit 113c698

Browse files
authored
Merge pull request #83 from VeritasOS/feature/mssql-dsp-api-workflow
Feature/mssql dsp api workflow
2 parents 5de3584 + 2409a2a commit 113c698

21 files changed

Lines changed: 1900 additions & 3 deletions

recipes/python/backup-restore/README.md

Lines changed: 78 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# NetBackup VMware agentless single and group VM backup and restore APIs code samples
1+
# NetBackup backup and restore APIs code samples of VMware agentless single, group VM and Microsoft SQL Server
22

33
## Executing the scripts:
44

@@ -135,3 +135,80 @@ Execution flow of group VM backup and restore script:
135135
- Verify the status of jobs
136136
- Perform bulk restore
137137
- Perform the cleanup(e.g. remove bulk instant access VMs, subscription, protection plan, VM group and vcenter)
138+
139+
### - Microsoft SQL Server Protection and Recovery workflow
140+
141+
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.
142+
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>`
144+
145+
All parameters can also be passed as command line arguments.
146+
- `python mssql_db_backup_restore.py -h`
147+
```
148+
usage: mssql_db_backup_restore.py [-h] [--master_server MASTER_SERVER]
149+
[--master_server_port MASTER_SERVER_PORT]
150+
[--master_username MASTER_USERNAME]
151+
[--master_password MASTER_PASSWORD]
152+
[--mssql_instance MSSQL_INSTANCE]
153+
[--mssql_database MSSQL_DATABASE]
154+
[--mssql_server_name MSSQL_SERVER_NAME]
155+
[--mssql_use_localcreds MSSQL_USE_LOCALCREDS]
156+
[--mssql_domain MSSQL_DOMAIN]
157+
[--mssql_username MSSQL_USERNAME]
158+
[--mssql_password MSSQL_PASSWORD]
159+
[--stu_name STU_NAME]
160+
[--protection_plan_name PROTECTION_PLAN_NAME]
161+
[--asset_type ASSET_TYPE]
162+
[--restore_db_prefix RESTORE_DB_PREFIX]
163+
[--restore_db_path RESTORE_DB_PATH]
164+
[--recoveralluserdbs RECOVERALLUSERDBS]
165+
Mssql backup and alternate database recovery scenario
166+
167+
Arguments:
168+
-h, --help show this help message and exit
169+
--master_server MASTER_SERVER
170+
NetBackup master server name
171+
--master_server_port MASTER_SERVER_PORT
172+
NetBackup master server port
173+
--master_username MASTER_USERNAME
174+
NetBackup master server username
175+
--master_password MASTER_PASSWORD
176+
NetBackup master server password
177+
--mssql_instance MSSQL_INSTANCE
178+
MSSQL Instance name
179+
--mssql_database MSSQL_DATABASE
180+
MSSQL Database name
181+
--mssql_server_name MSSQL_SERVER_NAME
182+
MSSQL server name, this is used in the filter for GET assets API.
183+
--mssql_use_localcreds MSSQL_USE_LOCALCREDS
184+
MSSQL server use locally defined creds
185+
--mssql_domain MSSQL_DOMAIN
186+
MSSQL server domain
187+
--mssql_username MSSQL_USERNAME
188+
MSSQL sysadmin username
189+
--mssql_password MSSQL_PASSWORD
190+
MSSQL sysadmin user password
191+
--stu_name STU_NAME Storage Unit name
192+
--protection_plan_name PROTECTION_PLAN_NAME
193+
Protection plan name
194+
--asset_type ASSET_TYPE
195+
MSSQL asset type (AvailabilityGroup, Instance, Database)
196+
--restore_db_prefix RESTORE_DB_PREFIX
197+
Restore database name prefix
198+
--restore_db_path RESTORE_DB_PATH
199+
Restore database path
200+
--recover_alluserdbs RECOVERALLUSERDBS
201+
Recover all User databases to the mssql_instance specfied with a database name prefix.
202+
203+
Execution flow of a Single MSSQL database protection and alternate database recovery workflow:
204+
- Login to Master Server get authorization token for API use
205+
- Add Credential with Credential Management API
206+
- Create a MSSQL Instance Asset and associate Credential
207+
- Asset API to find the MSSQL Instance asset id for subscription in a Protection Plan
208+
- Create MSSQL Protection Plan and configure MSSQL database policy attribute to SkipOffline databases
209+
- Subscribe the MSSQL Instance Assset in Protection Plan
210+
- Fetch Asset id for database for alternate recovery
211+
- Get recoverypoint for the database asset using its asset id
212+
- Perform alternate database recovery of the database and report recovery job id or Perfrom alternate recovery of all user databases, if recover_alluserdbs is specified.
213+
- Cleanup by removing subscription of Instance in Protection Plan, Remove Protection Plan and remove Mssql Credential
214+

recipes/python/backup-restore/common.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import sys
1010
import time
1111
import requests
12+
import uuid
1213

1314
headers = {"Content-Type" : "application/vnd.netbackup+json;version=4.0"}
1415

@@ -111,14 +112,15 @@ def create_protection_plan(baseurl, token, protection_plan_name, storage_unit_na
111112
url = f"{baseurl}servicecatalog/slos?meta=accessControlId"
112113

113114
cur_dir = os.path.dirname(os.path.abspath(__file__))
115+
cur_dir = cur_dir + os.sep + "sample-payloads" + os.sep
114116
file_name = os.path.join(cur_dir, "create_protection_plan_template.json")
115117
with open(file_name, 'r') as file_handle:
116118
data = json.load(file_handle)
117119
data['data']['attributes']['name'] = protection_plan_name
118120
data['data']['attributes']['policyNamePrefix'] = protection_plan_name
119121
data['data']['attributes']['schedules'][0]['backupStorageUnit'] = storage_unit_name
120122
data['data']['attributes']['allowSubscriptionEdit'] = False
121-
123+
122124
status_code, response_text = rest_request('POST', url, headers, data=data)
123125
validate_response(status_code, 201, response_text)
124126
protection_plan_id = response_text['data']['id']
@@ -150,6 +152,20 @@ def get_subscription(baseurl, token, protection_plan_id, subscription_id):
150152
validate_response(status_code, 200, response_text)
151153
print(f"Sucessfully fetched the subscription:[{subscription_id}] details.")
152154

155+
def protection_plan_backupnow(baseurl, token, protection_plan_id, asset_id):
156+
""" This function will trigger the backup of given asset using protection plan"""
157+
headers.update({'Authorization': token})
158+
url = f"{baseurl}servicecatalog/slos/{protection_plan_id}/backup-now"
159+
selection_type = "ASSET"
160+
payload = {"data": {"type": "backupNowRequest",
161+
"attributes": {"selectionType": selection_type, "selectionId": asset_id}}}
162+
163+
status_code, response_text = rest_request('POST', url, headers, data=payload)
164+
validate_response(status_code, 202, response_text)
165+
backup_job_id = response_text['data'][0]['id']
166+
print(f"Started backup for asset:[{asset_id}] and backup id is:[{backup_job_id}]")
167+
return backup_job_id
168+
153169
# Get job details
154170
def get_job_details(baseurl, token, jobid):
155171
""" This function return the job details """
@@ -257,13 +273,28 @@ def rest_request(request_type, uri, header=None, **kwargs):
257273
print(f"Response text:[{response.text}]")
258274
return response.status_code, response_text
259275

276+
def get_recovery_points(baseurl, token, workload_type, asset_id):
277+
""" This function return the recovery point of given asset """
278+
print(f"Get the recovery points for asset:[{asset_id}]")
279+
headers.update({'Authorization': token})
280+
url = f"{baseurl}recovery-point-service/workloads/{workload_type}/"\
281+
f"recovery-points?filter=assetId eq '{asset_id}'"
282+
status_code, response_text = rest_request('GET', url, headers)
283+
validate_response(status_code, 200, response_text)
284+
if (len(response_text['data'])>0):
285+
recoverypoint_id = response_text['data'][0]['id']
286+
else:
287+
recoverypoint_id = ""
288+
return recoverypoint_id
289+
260290
# Validate the response code of the request
261291
def validate_response(actual_status_code, expected_status_code, response_text):
262292
""" This function validate the response status code with expected response code """
263293
if actual_status_code == expected_status_code:
264-
print(f"Successfully validate the response status code:[{expected_status_code}]")
294+
print(f"Successfully validated the response status code:[{expected_status_code}]")
265295
else:
266296
print(f"Actual status code:[{actual_status_code}] not match "\
267297
f"with expected status code:[{expected_status_code}]")
268298
raise Exception(f"Response Error:[{response_text['errorMessage']}] and "\
269299
f"details:[{response_text['errorDetails']}]")
300+
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
""" This script execute the MSSQL Instance backup and database restore scenario. """
2+
3+
## The script can be run with Python 3.6 or higher version.
4+
5+
## The script requires 'requests' library to make the API calls.
6+
## The library can be installed using the command: pip install requests.
7+
8+
import argparse
9+
import common
10+
import time
11+
import workload_mssql
12+
13+
PARSER = argparse.ArgumentParser(description="MSSQL Instance backup and Database restore scenario")
14+
PARSER.add_argument("--master_server", type=str, help="NetBackup master server name")
15+
PARSER.add_argument("--master_server_port", type=int, help="NetBackup master server port", required=False)
16+
PARSER.add_argument("--master_username", type=str, help="NetBackup master server username")
17+
PARSER.add_argument("--master_password", type=str, help="NetBackup master server password")
18+
PARSER.add_argument("--mssql_instance", type=str, help="MSSQL Instance name")
19+
PARSER.add_argument("--mssql_database", type=str, help="MSSQL Database name")
20+
PARSER.add_argument("--mssql_server_name", type=str, help="MSSQL server name")
21+
PARSER.add_argument("--mssql_use_localcreds", type=int, help="MSSQL server use locally defined creds", default=0)
22+
PARSER.add_argument("--mssql_domain", type=str, help="MSSQL server domain")
23+
PARSER.add_argument("--mssql_username", type=str, help="MSSQL sysadmin username")
24+
PARSER.add_argument("--mssql_password", type=str, help="MSSQL sysadmin user password")
25+
PARSER.add_argument("--stu_name", type=str, help="Storage Unit name")
26+
PARSER.add_argument("--protection_plan_name", type=str, help="Protection plan name")
27+
PARSER.add_argument("--asset_type", type=str, help="MSSQL asset type (AvailabilityGroup, Instance, Database)", required=False)
28+
PARSER.add_argument("--restore_db_prefix", type=str, help="Restore database name prefix", required=True)
29+
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)
31+
32+
ARGS = PARSER.parse_args()
33+
34+
if __name__ == '__main__':
35+
WORKLOAD_TYPE = 'mssql'
36+
PROTECTION_PLAN_ID = ''
37+
SUBSCRIPTION_ID = ''
38+
ASSET_TYPE = ARGS.asset_type if ARGS.asset_type else 'instance'
39+
ALT_DB = ARGS.restore_db_prefix
40+
41+
BASEURL = common.get_nbu_base_url(ARGS.master_server, ARGS.master_server_port)
42+
TOKEN = common.get_authenticate_token(BASEURL, ARGS.master_username, ARGS.master_password)
43+
INSTANCE_NAME = ARGS.mssql_instance
44+
DATABASE_NAME = ARGS.mssql_database
45+
ALT_DB_PATH = ARGS.restore_db_path
46+
ALLDATABASES=[]
47+
print(f"User authentication completed for master server:[{ARGS.master_server}]")
48+
49+
try:
50+
print(f"Setup the environment for Mssql Server:[{ARGS.mssql_server_name}]")
51+
print(f"Setup the environment for Mssql Server:[{INSTANCE_NAME}]")
52+
CREDENTIAL_ID, CREDENTIAL_NAME = workload_mssql.add_mssql_credential(BASEURL, TOKEN, ARGS.mssql_use_localcreds, ARGS.mssql_domain, ARGS.mssql_username, ARGS.mssql_password)
53+
INSTANCE_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "instance", ARGS.mssql_server_name, INSTANCE_NAME)
54+
if (INSTANCE_ID != ""):
55+
print(f"Instance [{INSTANCE_ID}] already exists, updating credentials")
56+
workload_mssql.update_mssql_instance_credentials(BASEURL, TOKEN, INSTANCE_ID, CREDENTIAL_NAME)
57+
else:
58+
print(f"Instance Asset not present, create and register it ")
59+
workload_mssql.create_and_register_mssql_instance(BASEURL, TOKEN, INSTANCE_NAME, ARGS.mssql_server_name, CREDENTIAL_NAME);
60+
61+
# you can change the subscription to a specific Instance, AvailabilityGroup or database
62+
SUBSCRIPTION_ASSET_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, ASSET_TYPE, ARGS.mssql_server_name, INSTANCE_NAME)
63+
print(f"Asset Subscribed for protection:[{SUBSCRIPTION_ASSET_ID}]")
64+
# find the instance assetid and start a deepdiscovery on it for databases
65+
INSTANCE_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "instance", ARGS.mssql_server_name, INSTANCE_NAME)
66+
67+
print(f"Start Discovery on the instance [{INSTANCE_NAME}] on the host [{ARGS.mssql_server_name}]")
68+
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)
74+
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)
78+
#timeout is set at 300 seconds (5 mins to keep looking if the backups are complete)
79+
common.verify_job_state(BASEURL, TOKEN, BACKUP_JOB_ID, 'DONE', timeout=300)
80+
81+
# give nbwebservice 30 seconds to service any queued tasks, before launching recoveries
82+
time.sleep(30)
83+
if (ARGS.recoveralluserdbs != 1):
84+
# fetch the asset
85+
RECOVERY_ASSET_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "database", ARGS.mssql_server_name, DATABASE_NAME, INSTANCE_NAME)
86+
RECOVERY_POINT = common.get_recovery_points(BASEURL, TOKEN, WORKLOAD_TYPE, RECOVERY_ASSET_ID)
87+
print(f"Perform Mssql single database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
88+
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)
90+
print(f"Recovery initiated , follow Job #: [{RECOVERY_JOB_ID}]")
91+
else:
92+
print(f"Perform alternate recovery of all databases")
93+
#get all databases and its recovery points
94+
ALLDATABASES = workload_mssql.get_mssql_alldbs(BASEURL, TOKEN, ARGS.mssql_server_name, INSTANCE_NAME)
95+
print(f"Total Databases found [{len(ALLDATABASES)}]")
96+
systemdbs_set = set(['master', 'model', 'msdb'])
97+
for elem in ALLDATABASES:
98+
DATABASE_NAME = elem.databasename
99+
RECOVERY_ASSET_ID = elem.assetid
100+
if (DATABASE_NAME in systemdbs_set):
101+
print(f"Skipping recovery of system database [{DATABASE_NAME}]")
102+
else:
103+
RECOVERY_POINT = common.get_recovery_points(BASEURL, TOKEN, WORKLOAD_TYPE, RECOVERY_ASSET_ID)
104+
if (RECOVERY_POINT != ""):
105+
print(f"Perform Mssql database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
106+
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)
108+
else:
109+
print(f"Skipping recovery, could not find RecoveryPoint for [{DATABASE_NAME}] assetid [{RECOVERY_ASSET_ID}]")
110+
111+
finally:
112+
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)
116+
workload_mssql.remove_mssql_credential(BASEURL, TOKEN, CREDENTIAL_ID)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
{
2+
"data": {
3+
"type": "slov3",
4+
"attributes": {
5+
"description": "Protection Plan for MSSQL workload",
6+
"name": "",
7+
"schedules": [
8+
{
9+
"backupStorageUnit": "",
10+
"backupWindows": [
11+
{
12+
"dayOfWeek": 1,
13+
"startSeconds": 0,
14+
"durationSeconds": 86400
15+
}
16+
],
17+
"frequencySeconds": 86400,
18+
"retention": {
19+
"value": 2,
20+
"unit": "WEEKS"
21+
}
22+
}
23+
],
24+
"allowSubscriptionEdit": "False",
25+
"workloadType": "MSSQL",
26+
"policyNamePrefix": ""
27+
}
28+
}
29+
}

recipes/python/backup-restore/create_protection_plan_template.json renamed to recipes/python/backup-restore/sample-payloads/create_protection_plan_template.json

File renamed without changes.
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"data":{
3+
"type":"query",
4+
"attributes":{
5+
"queryName":"delete-assets",
6+
"workloads":["mssql"],
7+
"parameters":{"objectList":[
8+
{
9+
"correlationId":"18",
10+
"id":"assetid-uuid",
11+
"asset":{
12+
"assetType": "INSTANCE"
13+
}
14+
}
15+
]}
16+
}
17+
}
18+
}

0 commit comments

Comments
 (0)