-
Notifications
You must be signed in to change notification settings - Fork 3.3k
[Storage] Add PITR support #12372
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Storage] Add PITR support #12372
Changes from all commits
8096619
dcf6287
f87dfec
6055115
a2b5782
5846280
e265280
ec1a31f
c37dfaf
c7e9367
d36a656
a167082
00f055d
461fe9c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -319,6 +319,11 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem | |
| min_api='2018-07-01') | ||
| c.argument('delete_retention_days', type=int, arg_group='Delete Retention Policy', | ||
| validator=validator_delete_retention_days, min_api='2018-07-01') | ||
| c.argument('enable_restore_policy', arg_type=get_three_state_flag(), arg_group='Restore Policy', | ||
| min_api='2019-06-01', help="Enable blob restore policy when it set to true.") | ||
| c.argument('restore_days', type=int, arg_group='Restore Policy', | ||
| min_api='2019-06-01', help="The number of days for the blob can be restored. It should be greater " | ||
| "than zero and less than Delete Retention Days.") | ||
|
|
||
| with self.argument_context('storage account generate-sas') as c: | ||
| t_account_permissions = self.get_sdk('common.models#AccountPermissions') | ||
|
|
@@ -393,6 +398,17 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem | |
| help=sas_help.format(get_permission_help_string(t_blob_permissions)), | ||
| validator=get_permission_validator(t_blob_permissions)) | ||
|
|
||
| with self.argument_context('storage blob restore', resource_type=ResourceType.MGMT_STORAGE) as c: | ||
| from ._validators import BlobRangeAddAction | ||
| c.argument('blob_ranges', options_list=['--blob-range', '-r'], action=BlobRangeAddAction, nargs='+', | ||
| help='Blob ranges to restore. You need to two values to specify start_range and end_range for each ' | ||
|
||
| 'blob range, e.g. -r blob1 blob2. Note: Empty means account start as start range value, and ' | ||
| 'means account end for end range.') | ||
| c.argument('account_name', acct_name_type, id_part=None) | ||
| c.argument('resource_group_name', required=False, validator=process_resource_group) | ||
| c.argument('time_to_restore', type=get_datetime_type(True), options_list=['--time-to-restore', '-t'], | ||
| help='Restore blob to the specified time, which should be UTC datetime in (Y-m-d\'T\'H:M:S\'Z\').') | ||
|
|
||
| with self.argument_context('storage blob update') as c: | ||
| t_blob_content_settings = self.get_sdk('blob.models#ContentSettings') | ||
| c.register_content_settings_argument(t_blob_content_settings, update=True) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,8 @@ | |
|
|
||
| # pylint: disable=protected-access | ||
|
|
||
| import argparse | ||
|
|
||
| from knack.util import CLIError | ||
| from knack.log import get_logger | ||
|
|
||
|
|
@@ -481,7 +483,6 @@ def validate_entity(namespace): | |
| missing_keys = '{}PartitionKey'.format(missing_keys) \ | ||
| if 'PartitionKey' not in keys else missing_keys | ||
| if missing_keys: | ||
| import argparse | ||
| raise argparse.ArgumentError( | ||
| None, 'incorrect usage: entity requires: {}'.format(missing_keys)) | ||
|
|
||
|
|
@@ -520,7 +521,6 @@ def validate_marker(namespace): | |
| del marker[key] | ||
| marker[new_key] = val | ||
| if expected_keys: | ||
| import argparse | ||
| raise argparse.ArgumentError( | ||
| None, 'incorrect usage: marker requires: {}'.format(' '.join(expected_keys))) | ||
|
|
||
|
|
@@ -908,7 +908,6 @@ def process_file_download_namespace(namespace): | |
|
|
||
|
|
||
| def process_metric_update_namespace(namespace): | ||
| import argparse | ||
| namespace.hour = namespace.hour == 'true' | ||
| namespace.minute = namespace.minute == 'true' | ||
| namespace.api = namespace.api == 'true' if namespace.api else None | ||
|
|
@@ -1118,21 +1117,18 @@ def validate_azcopy_remove_arguments(cmd, namespace): | |
| def as_user_validator(namespace): | ||
| if namespace.as_user: | ||
| if namespace.expiry is None: | ||
| import argparse | ||
| raise argparse.ArgumentError( | ||
| None, 'incorrect usage: specify --expiry when as-user is enabled') | ||
|
|
||
| expiry = get_datetime_type(False)(namespace.expiry) | ||
|
|
||
| from datetime import datetime, timedelta | ||
| if expiry > datetime.utcnow() + timedelta(days=7): | ||
| import argparse | ||
| raise argparse.ArgumentError( | ||
| None, 'incorrect usage: --expiry should be within 7 days from now') | ||
|
|
||
| if ((not hasattr(namespace, 'token_credential') or namespace.token_credential is None) and | ||
| (not hasattr(namespace, 'auth_mode') or namespace.auth_mode != 'login')): | ||
| import argparse | ||
| raise argparse.ArgumentError( | ||
| None, "incorrect usage: specify '--auth-mode login' when as-user is enabled") | ||
|
|
||
|
|
@@ -1161,6 +1157,24 @@ def validator_delete_retention_days(namespace): | |
| "incorrect usage: '--delete-retention-days' must be less than or equal to 365") | ||
|
|
||
|
|
||
| # pylint: disable=too-few-public-methods | ||
| class BlobRangeAddAction(argparse._AppendAction): | ||
| def __call__(self, parser, namespace, values, option_string=None): | ||
| if not namespace.blob_ranges: | ||
| namespace.blob_ranges = [] | ||
| if isinstance(values, list): | ||
| values = ' '.join(values) | ||
| BlobRange = namespace._cmd.get_models('BlobRestoreRange', resource_type=ResourceType.MGMT_STORAGE) | ||
| try: | ||
| start_range, end_range = values.split(' ') | ||
| except (ValueError, TypeError): | ||
| raise CLIError('usage error: --blob-range VARIABLE OPERATOR VALUE') | ||
| namespace.blob_ranges.append(BlobRange( | ||
| start_range=start_range, | ||
| end_range=end_range | ||
|
||
| )) | ||
|
|
||
|
|
||
| def validate_private_endpoint_connection_id(cmd, namespace): | ||
| if namespace.connection_id: | ||
| from azure.cli.core.util import parse_proxy_resource_id | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| # -------------------------------------------------------------------------------------------- | ||
|
|
||
| import os | ||
| from datetime import datetime, timedelta | ||
| from azure.cli.testsdk import (LiveScenarioTest, ResourceGroupPreparer, StorageAccountPreparer, | ||
| JMESPathCheck, JMESPathCheckExists, NoneCheck, api_version_constraint) | ||
| from azure.cli.core.profiles import ResourceType | ||
|
|
@@ -84,3 +85,79 @@ def verify_blob_upload_and_download(self, group, account, file_size_kb, blob_typ | |
| self.assertTrue(os.path.isfile(downloaded), 'The file is not downloaded.') | ||
| self.assertEqual(file_size_kb * 1024, os.stat(downloaded).st_size, | ||
| 'The download file size is not right.') | ||
|
|
||
| @ResourceGroupPreparer(name_prefix="storage_blob_restore") | ||
| @StorageAccountPreparer(name_prefix="storageblobrestore", kind="StorageV2", location="eastus2euap", | ||
| sku='Standard_LRS') | ||
| def test_storage_blob_restore(self, resource_group, storage_account): | ||
| import time | ||
| self.cmd('storage account blob-service-properties update --enable-change-feed --enable-delete-retention --delete-retention-days 2 -n {sa}')\ | ||
| .assert_with_checks(JMESPathCheck('changeFeed.enabled', True), | ||
| JMESPathCheck('deleteRetentionPolicy.enabled', True), | ||
| JMESPathCheck('deleteRetentionPolicy.days', 2)) | ||
| time.sleep(60) | ||
| # Enable Restore Policy | ||
| self.cmd('storage account blob-service-properties update --enable-restore-policy --restore-days 1 -n {sa}')\ | ||
| .assert_with_checks(JMESPathCheck('restorePolicy.enabled', True), | ||
| JMESPathCheck('restorePolicy.days', 1)) | ||
|
|
||
| c1 = self.create_random_name(prefix='containera', length=24) | ||
| c2 = self.create_random_name(prefix='containerb', length=24) | ||
| b1 = self.create_random_name(prefix='blob1', length=24) | ||
| b2 = self.create_random_name(prefix='blob2', length=24) | ||
| b3 = self.create_random_name(prefix='blob3', length=24) | ||
| b4 = self.create_random_name(prefix='blob4', length=24) | ||
|
|
||
| local_file = self.create_temp_file(256) | ||
|
|
||
| account_key = self.cmd('storage account keys list -n {} -g {} --query "[0].value" -otsv' | ||
| .format(storage_account, resource_group)).output | ||
|
|
||
| # Prepare containers and blobs | ||
| for container in [c1, c2]: | ||
| self.cmd('storage container create -n {} --account-name {} --account-key {}'.format( | ||
| container, storage_account, account_key)) \ | ||
| .assert_with_checks(JMESPathCheck('created', True)) | ||
| for blob in [b1, b2, b3, b4]: | ||
| self.cmd('storage blob upload -c {} -f "{}" -n {} --account-name {} --account-key {}'.format( | ||
| container, local_file, blob, storage_account, account_key)) | ||
| self.cmd('storage blob list -c {} --account-name {} --account-key {}'.format( | ||
| container, storage_account, account_key)) \ | ||
| .assert_with_checks(JMESPathCheck('length(@)', 4)) | ||
|
|
||
| self.cmd('storage container delete -n {} --account-name {} --account-key {}'.format( | ||
| container, storage_account, account_key)) \ | ||
| .assert_with_checks(JMESPathCheck('deleted', True)) | ||
|
|
||
| time.sleep(10) | ||
|
|
||
| # Restore blobs, with specific ranges | ||
| time_to_restore = (datetime.utcnow() + timedelta(seconds=-5)).strftime('%Y-%m-%dT%H:%MZ') | ||
|
|
||
| # c1/b1 -> c1/b2 | ||
| start_range = '/'.join([c1, b1]) | ||
| end_range = '/'.join([c1, b2]) | ||
| self.cmd('storage blob restore -t {} -r {} {} --account-name {} -g {}'.format( | ||
| time_to_restore, start_range, end_range, storage_account, resource_group), checks=[ | ||
| JMESPathCheck('status', 'Complete'), | ||
| JMESPathCheck('parameters.blobRanges[0].startRange', start_range), | ||
| JMESPathCheck('parameters.blobRanges[0].endRange', end_range)]) | ||
|
|
||
| self.cmd('storage blob restore -t {} -r {} {} --account-name {} -g {} --no-wait'.format( | ||
| time_to_restore, start_range, end_range, storage_account, resource_group)) | ||
|
||
|
|
||
| time.sleep(90) | ||
|
|
||
| time_to_restore = (datetime.utcnow() + timedelta(seconds=-5)).strftime('%Y-%m-%dT%H:%MZ') | ||
| # c1/b2 -> c2/b3 | ||
| start_range = '/'.join([c1, b2]) | ||
| end_range = '/'.join([c2, b3]) | ||
| self.cmd('storage blob restore -t {} -r {} {} --account-name {} -g {}'.format( | ||
| time_to_restore, start_range, end_range, storage_account, resource_group), checks=[ | ||
| JMESPathCheck('status', 'Complete'), | ||
| JMESPathCheck('parameters.blobRanges[0].startRange', start_range), | ||
| JMESPathCheck('parameters.blobRanges[0].endRange', end_range)]) | ||
|
|
||
| time.sleep(90) | ||
| self.cmd('storage blob restore -t {} --account-name {} -g {} --no-wait'.format( | ||
| time_to_restore, storage_account, resource_group)) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-r container1/blob1 container2/blob2 -r container2/blob3 container2/blob4Is it intersection or union in this case?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you mean for intersection or union? I don't get your point.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-r container1/blob1 container2/blob2means the bolb is double backed up in a blob range?-r xxx -r xxx -r xxx...means backup in multiple blob ranges?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
no. it is a range.
It means blobls from conrainer1/blob1 ... (container1/blon3) ...to container2/blob2.