Skip to content

Commit

Permalink
Merge pull request #83 from VeritasOS/feature/mssql-dsp-api-workflow
Browse files Browse the repository at this point in the history
Feature/mssql dsp api workflow
  • Loading branch information
rkennedy authored Dec 12, 2020
2 parents 5de3584 + 2409a2a commit 113c698
Show file tree
Hide file tree
Showing 21 changed files with 1,900 additions and 3 deletions.
79 changes: 78 additions & 1 deletion recipes/python/backup-restore/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# NetBackup VMware agentless single and group VM backup and restore APIs code samples
# NetBackup backup and restore APIs code samples of VMware agentless single, group VM and Microsoft SQL Server

## Executing the scripts:

Expand Down Expand Up @@ -135,3 +135,80 @@ Execution flow of group VM backup and restore script:
- Verify the status of jobs
- Perform bulk restore
- Perform the cleanup(e.g. remove bulk instant access VMs, subscription, protection plan, VM group and vcenter)

### - Microsoft SQL Server Protection and Recovery workflow

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.

`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>`

All parameters can also be passed as command line arguments.
- `python mssql_db_backup_restore.py -h`
```
usage: mssql_db_backup_restore.py [-h] [--master_server MASTER_SERVER]
[--master_server_port MASTER_SERVER_PORT]
[--master_username MASTER_USERNAME]
[--master_password MASTER_PASSWORD]
[--mssql_instance MSSQL_INSTANCE]
[--mssql_database MSSQL_DATABASE]
[--mssql_server_name MSSQL_SERVER_NAME]
[--mssql_use_localcreds MSSQL_USE_LOCALCREDS]
[--mssql_domain MSSQL_DOMAIN]
[--mssql_username MSSQL_USERNAME]
[--mssql_password MSSQL_PASSWORD]
[--stu_name STU_NAME]
[--protection_plan_name PROTECTION_PLAN_NAME]
[--asset_type ASSET_TYPE]
[--restore_db_prefix RESTORE_DB_PREFIX]
[--restore_db_path RESTORE_DB_PATH]
[--recoveralluserdbs RECOVERALLUSERDBS]
Mssql backup and alternate database recovery scenario
Arguments:
-h, --help show this help message and exit
--master_server MASTER_SERVER
NetBackup master server name
--master_server_port MASTER_SERVER_PORT
NetBackup master server port
--master_username MASTER_USERNAME
NetBackup master server username
--master_password MASTER_PASSWORD
NetBackup master server password
--mssql_instance MSSQL_INSTANCE
MSSQL Instance name
--mssql_database MSSQL_DATABASE
MSSQL Database name
--mssql_server_name MSSQL_SERVER_NAME
MSSQL server name, this is used in the filter for GET assets API.
--mssql_use_localcreds MSSQL_USE_LOCALCREDS
MSSQL server use locally defined creds
--mssql_domain MSSQL_DOMAIN
MSSQL server domain
--mssql_username MSSQL_USERNAME
MSSQL sysadmin username
--mssql_password MSSQL_PASSWORD
MSSQL sysadmin user password
--stu_name STU_NAME Storage Unit name
--protection_plan_name PROTECTION_PLAN_NAME
Protection plan name
--asset_type ASSET_TYPE
MSSQL asset type (AvailabilityGroup, Instance, Database)
--restore_db_prefix RESTORE_DB_PREFIX
Restore database name prefix
--restore_db_path RESTORE_DB_PATH
Restore database path
--recover_alluserdbs RECOVERALLUSERDBS
Recover all User databases to the mssql_instance specfied with a database name prefix.
Execution flow of a Single MSSQL database protection and alternate database recovery workflow:
- Login to Master Server get authorization token for API use
- Add Credential with Credential Management API
- Create a MSSQL Instance Asset and associate Credential
- Asset API to find the MSSQL Instance asset id for subscription in a Protection Plan
- Create MSSQL Protection Plan and configure MSSQL database policy attribute to SkipOffline databases
- Subscribe the MSSQL Instance Assset in Protection Plan
- Fetch Asset id for database for alternate recovery
- Get recoverypoint for the database asset using its asset id
- 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.
- Cleanup by removing subscription of Instance in Protection Plan, Remove Protection Plan and remove Mssql Credential
35 changes: 33 additions & 2 deletions recipes/python/backup-restore/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import sys
import time
import requests
import uuid

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

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

cur_dir = os.path.dirname(os.path.abspath(__file__))
cur_dir = cur_dir + os.sep + "sample-payloads" + os.sep
file_name = os.path.join(cur_dir, "create_protection_plan_template.json")
with open(file_name, 'r') as file_handle:
data = json.load(file_handle)
data['data']['attributes']['name'] = protection_plan_name
data['data']['attributes']['policyNamePrefix'] = protection_plan_name
data['data']['attributes']['schedules'][0]['backupStorageUnit'] = storage_unit_name
data['data']['attributes']['allowSubscriptionEdit'] = False

status_code, response_text = rest_request('POST', url, headers, data=data)
validate_response(status_code, 201, response_text)
protection_plan_id = response_text['data']['id']
Expand Down Expand Up @@ -150,6 +152,20 @@ def get_subscription(baseurl, token, protection_plan_id, subscription_id):
validate_response(status_code, 200, response_text)
print(f"Sucessfully fetched the subscription:[{subscription_id}] details.")

def protection_plan_backupnow(baseurl, token, protection_plan_id, asset_id):
""" This function will trigger the backup of given asset using protection plan"""
headers.update({'Authorization': token})
url = f"{baseurl}servicecatalog/slos/{protection_plan_id}/backup-now"
selection_type = "ASSET"
payload = {"data": {"type": "backupNowRequest",
"attributes": {"selectionType": selection_type, "selectionId": asset_id}}}

status_code, response_text = rest_request('POST', url, headers, data=payload)
validate_response(status_code, 202, response_text)
backup_job_id = response_text['data'][0]['id']
print(f"Started backup for asset:[{asset_id}] and backup id is:[{backup_job_id}]")
return backup_job_id

# Get job details
def get_job_details(baseurl, token, jobid):
""" This function return the job details """
Expand Down Expand Up @@ -257,13 +273,28 @@ def rest_request(request_type, uri, header=None, **kwargs):
print(f"Response text:[{response.text}]")
return response.status_code, response_text

def get_recovery_points(baseurl, token, workload_type, asset_id):
""" This function return the recovery point of given asset """
print(f"Get the recovery points for asset:[{asset_id}]")
headers.update({'Authorization': token})
url = f"{baseurl}recovery-point-service/workloads/{workload_type}/"\
f"recovery-points?filter=assetId eq '{asset_id}'"
status_code, response_text = rest_request('GET', url, headers)
validate_response(status_code, 200, response_text)
if (len(response_text['data'])>0):
recoverypoint_id = response_text['data'][0]['id']
else:
recoverypoint_id = ""
return recoverypoint_id

# Validate the response code of the request
def validate_response(actual_status_code, expected_status_code, response_text):
""" This function validate the response status code with expected response code """
if actual_status_code == expected_status_code:
print(f"Successfully validate the response status code:[{expected_status_code}]")
print(f"Successfully validated the response status code:[{expected_status_code}]")
else:
print(f"Actual status code:[{actual_status_code}] not match "\
f"with expected status code:[{expected_status_code}]")
raise Exception(f"Response Error:[{response_text['errorMessage']}] and "\
f"details:[{response_text['errorDetails']}]")

116 changes: 116 additions & 0 deletions recipes/python/backup-restore/mssql_db_backup_restore.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
""" This script execute the MSSQL Instance backup and database restore scenario. """

## The script can be run with Python 3.6 or higher version.

## The script requires 'requests' library to make the API calls.
## The library can be installed using the command: pip install requests.

import argparse
import common
import time
import workload_mssql

PARSER = argparse.ArgumentParser(description="MSSQL Instance backup and Database restore scenario")
PARSER.add_argument("--master_server", type=str, help="NetBackup master server name")
PARSER.add_argument("--master_server_port", type=int, help="NetBackup master server port", required=False)
PARSER.add_argument("--master_username", type=str, help="NetBackup master server username")
PARSER.add_argument("--master_password", type=str, help="NetBackup master server password")
PARSER.add_argument("--mssql_instance", type=str, help="MSSQL Instance name")
PARSER.add_argument("--mssql_database", type=str, help="MSSQL Database name")
PARSER.add_argument("--mssql_server_name", type=str, help="MSSQL server name")
PARSER.add_argument("--mssql_use_localcreds", type=int, help="MSSQL server use locally defined creds", default=0)
PARSER.add_argument("--mssql_domain", type=str, help="MSSQL server domain")
PARSER.add_argument("--mssql_username", type=str, help="MSSQL sysadmin username")
PARSER.add_argument("--mssql_password", type=str, help="MSSQL sysadmin user password")
PARSER.add_argument("--stu_name", type=str, help="Storage Unit name")
PARSER.add_argument("--protection_plan_name", type=str, help="Protection plan name")
PARSER.add_argument("--asset_type", type=str, help="MSSQL asset type (AvailabilityGroup, Instance, Database)", required=False)
PARSER.add_argument("--restore_db_prefix", type=str, help="Restore database name prefix", required=True)
PARSER.add_argument("--restore_db_path", type=str, help="Restore database path", required=True)
PARSER.add_argument("--recoveralluserdbs", type=int, help="Recover all user databases", required=False, default=0)

ARGS = PARSER.parse_args()

if __name__ == '__main__':
WORKLOAD_TYPE = 'mssql'
PROTECTION_PLAN_ID = ''
SUBSCRIPTION_ID = ''
ASSET_TYPE = ARGS.asset_type if ARGS.asset_type else 'instance'
ALT_DB = ARGS.restore_db_prefix

BASEURL = common.get_nbu_base_url(ARGS.master_server, ARGS.master_server_port)
TOKEN = common.get_authenticate_token(BASEURL, ARGS.master_username, ARGS.master_password)
INSTANCE_NAME = ARGS.mssql_instance
DATABASE_NAME = ARGS.mssql_database
ALT_DB_PATH = ARGS.restore_db_path
ALLDATABASES=[]
print(f"User authentication completed for master server:[{ARGS.master_server}]")

try:
print(f"Setup the environment for Mssql Server:[{ARGS.mssql_server_name}]")
print(f"Setup the environment for Mssql Server:[{INSTANCE_NAME}]")
CREDENTIAL_ID, CREDENTIAL_NAME = workload_mssql.add_mssql_credential(BASEURL, TOKEN, ARGS.mssql_use_localcreds, ARGS.mssql_domain, ARGS.mssql_username, ARGS.mssql_password)
INSTANCE_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "instance", ARGS.mssql_server_name, INSTANCE_NAME)
if (INSTANCE_ID != ""):
print(f"Instance [{INSTANCE_ID}] already exists, updating credentials")
workload_mssql.update_mssql_instance_credentials(BASEURL, TOKEN, INSTANCE_ID, CREDENTIAL_NAME)
else:
print(f"Instance Asset not present, create and register it ")
workload_mssql.create_and_register_mssql_instance(BASEURL, TOKEN, INSTANCE_NAME, ARGS.mssql_server_name, CREDENTIAL_NAME);

# you can change the subscription to a specific Instance, AvailabilityGroup or database
SUBSCRIPTION_ASSET_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, ASSET_TYPE, ARGS.mssql_server_name, INSTANCE_NAME)
print(f"Asset Subscribed for protection:[{SUBSCRIPTION_ASSET_ID}]")
# find the instance assetid and start a deepdiscovery on it for databases
INSTANCE_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "instance", ARGS.mssql_server_name, INSTANCE_NAME)

print(f"Start Discovery on the instance [{INSTANCE_NAME}] on the host [{ARGS.mssql_server_name}]")
workload_mssql.mssql_instance_deepdiscovery(BASEURL, TOKEN, INSTANCE_ID)
# create protection plan and subscribe the assettype to it
PROTECTION_PLAN_ID = workload_mssql.create_mssql_protection_plan(BASEURL, TOKEN, ARGS.protection_plan_name, ARGS.stu_name, "SQL_SERVER")
# update protection plan to set MSSQL policy settings to skip offline databases
workload_mssql.update_protection_plan_mssql_attr(BASEURL, TOKEN, ARGS.protection_plan_name, PROTECTION_PLAN_ID, skip_offline_db=1)
SUBSCRIPTION_ID = common.subscription_asset_to_slo(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)

# MSSQL backup restore
print("Start MSSQL backup")
BACKUP_JOB_ID = common.protection_plan_backupnow(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ASSET_ID)
#timeout is set at 300 seconds (5 mins to keep looking if the backups are complete)
common.verify_job_state(BASEURL, TOKEN, BACKUP_JOB_ID, 'DONE', timeout=300)

# give nbwebservice 30 seconds to service any queued tasks, before launching recoveries
time.sleep(30)
if (ARGS.recoveralluserdbs != 1):
# fetch the asset
RECOVERY_ASSET_ID = workload_mssql.get_mssql_asset_info(BASEURL, TOKEN, "database", ARGS.mssql_server_name, DATABASE_NAME, INSTANCE_NAME)
RECOVERY_POINT = common.get_recovery_points(BASEURL, TOKEN, WORKLOAD_TYPE, RECOVERY_ASSET_ID)
print(f"Perform Mssql single database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
ALT_DB = ALT_DB + DATABASE_NAME
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)
print(f"Recovery initiated , follow Job #: [{RECOVERY_JOB_ID}]")
else:
print(f"Perform alternate recovery of all databases")
#get all databases and its recovery points
ALLDATABASES = workload_mssql.get_mssql_alldbs(BASEURL, TOKEN, ARGS.mssql_server_name, INSTANCE_NAME)
print(f"Total Databases found [{len(ALLDATABASES)}]")
systemdbs_set = set(['master', 'model', 'msdb'])
for elem in ALLDATABASES:
DATABASE_NAME = elem.databasename
RECOVERY_ASSET_ID = elem.assetid
if (DATABASE_NAME in systemdbs_set):
print(f"Skipping recovery of system database [{DATABASE_NAME}]")
else:
RECOVERY_POINT = common.get_recovery_points(BASEURL, TOKEN, WORKLOAD_TYPE, RECOVERY_ASSET_ID)
if (RECOVERY_POINT != ""):
print(f"Perform Mssql database [{DATABASE_NAME}] alternate recovery:[{ARGS.mssql_server_name}]")
ALT_DB = ARGS.restore_db_prefix + DATABASE_NAME
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)
else:
print(f"Skipping recovery, could not find RecoveryPoint for [{DATABASE_NAME}] assetid [{RECOVERY_ASSET_ID}]")

finally:
print("Start cleanup")
# Cleanup the created protection plan
common.remove_subscription(BASEURL, TOKEN, PROTECTION_PLAN_ID, SUBSCRIPTION_ID)
common.remove_protectionplan(BASEURL, TOKEN, PROTECTION_PLAN_ID)
workload_mssql.remove_mssql_credential(BASEURL, TOKEN, CREDENTIAL_ID)
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"data": {
"type": "slov3",
"attributes": {
"description": "Protection Plan for MSSQL workload",
"name": "",
"schedules": [
{
"backupStorageUnit": "",
"backupWindows": [
{
"dayOfWeek": 1,
"startSeconds": 0,
"durationSeconds": 86400
}
],
"frequencySeconds": 86400,
"retention": {
"value": 2,
"unit": "WEEKS"
}
}
],
"allowSubscriptionEdit": "False",
"workloadType": "MSSQL",
"policyNamePrefix": ""
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"data":{
"type":"query",
"attributes":{
"queryName":"delete-assets",
"workloads":["mssql"],
"parameters":{"objectList":[
{
"correlationId":"18",
"id":"assetid-uuid",
"asset":{
"assetType": "INSTANCE"
}
}
]}
}
}
}
Loading

0 comments on commit 113c698

Please sign in to comment.