Skip to content

Commit 9d1678e

Browse files
Juliehzlevelyn-ys
andauthored
{storage-preview} Add hns soft delete (Azure#2877)
* draft for hns * fix style * import pass * show work * draft update * use stable blob SDK * support list deleted and undelete * add readme * update sdk * remove personal info * fxi typo * test pass * remove validator * update SDK * update SDK * resolve SDKissue * resolve SDKissue * add more argument for list * test pass with new SDK * test pass * update help and release note * fiix linter and test * refine code * Apply suggestions from code review Co-authored-by: Yishi Wang <yishiwang@microsoft.com> Co-authored-by: Yishi Wang <yishiwang@microsoft.com>
1 parent d5a5290 commit 9d1678e

File tree

167 files changed

+70708
-665
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

167 files changed

+70708
-665
lines changed

src/storage-preview/HISTORY.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
33
Release History
44
===============
5+
0.7.3(2021-05-20)
6+
++++++++++++++++++
7+
* Support soft delete for ADLS Gen2 account
8+
59
0.7.2(2021-04-09)
610
++++++++++++++++++
711
* Remove `az storage blob service-properties` as it is supported in storage-blob-preview extension and Azure CLI

src/storage-preview/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,4 +306,69 @@ az storage account file-service-properties update \
306306
-g MyResourceGroup
307307
```
308308

309+
#### Soft Delete for ADLS Gen2 storage
310+
##### Prepare resource
311+
1. ADLS Gen2 storage account with soft delete support
312+
```
313+
az storage account create \
314+
-n myadls \
315+
-g myresourcegroup \
316+
--hns
317+
```
318+
To get connection string, you could use the following command:
319+
```
320+
az storage account show-connection-string \
321+
-n myadls \
322+
-g myresourcegroup
323+
```
324+
2. Prepare file system in the ADLS Gen2 storage account
325+
```
326+
az storage fs create \
327+
-n myfilesystem \
328+
--connection-string myconnectionstring
329+
```
330+
##### Enable delete retention
331+
```
332+
az storage fs service-properties update \
333+
--delete-retention \
334+
--delete-retention-period 5 \
335+
--connection-string myconnectionstring
336+
```
337+
##### Upload file to file system
338+
```
339+
az storage fs file upload \
340+
-s ".\test.txt" \
341+
-p test \
342+
-f filesystemcetk2triyptlaa \
343+
--connection-string $con
344+
```
345+
##### List deleted path
346+
```
347+
az storage fs file delete \
348+
-p test \
349+
-f filesystemcetk2triyptlaa \
350+
--connection-string $con
351+
```
352+
##### List deleted path
353+
```
354+
az storage fs list-deleted-path \
355+
-f filesystemcetk2triyptlaa \
356+
--connection-string $con
357+
```
358+
##### Undelete deleted path
359+
```
360+
az storage fs undelete-path \
361+
-f filesystemcetk2triyptlaa \
362+
-f filesystemcetk2triyptlaa \
363+
--deleted-path-name test \
364+
--deleted-path-version 132549163 \
365+
--connection-string $con
366+
```
367+
##### Disable delete retention
368+
```
369+
az storage fs service-properties update \
370+
--delete-retention false \
371+
--connection-string $con
372+
```
373+
309374
If you have issues, please give feedback by opening an issue at https://github.com/Azure/azure-cli-extensions/issues.

src/storage-preview/azext_storage_preview/__init__.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from azure.cli.core.commands import AzCommandGroup, AzArgumentContext
99

1010
import azext_storage_preview._help # pylint: disable=unused-import
11-
from .profiles import (CUSTOM_DATA_STORAGE, CUSTOM_MGMT_PREVIEW_STORAGE, CUSTOM_DATA_STORAGE_ADLS,
12-
CUSTOM_DATA_STORAGE_FILESHARE)
11+
from .profiles import CUSTOM_DATA_STORAGE, CUSTOM_MGMT_PREVIEW_STORAGE, CUSTOM_DATA_STORAGE_ADLS, \
12+
CUSTOM_DATA_STORAGE_FILESHARE, CUSTOM_DATA_STORAGE_FILEDATALAKE
1313

1414

1515
class StorageCommandsLoader(AzCommandsLoader):
@@ -20,6 +20,8 @@ def __init__(self, cli_ctx=None):
2020
register_resource_type('latest', CUSTOM_DATA_STORAGE_ADLS, '2019-02-02-preview')
2121
register_resource_type('latest', CUSTOM_MGMT_PREVIEW_STORAGE, '2020-08-01-preview')
2222
register_resource_type('latest', CUSTOM_DATA_STORAGE_FILESHARE, '2020-02-10')
23+
register_resource_type('latest', CUSTOM_DATA_STORAGE_FILEDATALAKE, '2020-06-12')
24+
2325
storage_custom = CliCommandType(operations_tmpl='azext_storage_preview.custom#{}')
2426

2527
super(StorageCommandsLoader, self).__init__(cli_ctx=cli_ctx,
@@ -63,8 +65,9 @@ def register_content_settings_argument(self, settings_class, update, arg_group=N
6365

6466
self.ignore('content_settings')
6567

66-
# The parameter process_md5 is used to determine whether it is compatible with the process_md5 parameter type of Python SDK
67-
# When the Python SDK is fixed (Issue: https://github.com/Azure/azure-sdk-for-python/issues/15919),
68+
# The parameter process_md5 is used to determine whether it is compatible with the process_md5 parameter
69+
# type of Python SDK When the Python SDK is fixed
70+
# (Issue: https://github.com/Azure/azure-sdk-for-python/issues/15919),
6871
# this parameter should not be passed in any more
6972
self.extra('content_type', default=None, help='The content MIME type.', arg_group=arg_group,
7073
validator=get_content_setting_validator(settings_class, update, guess_from_file=guess_from_file,

src/storage-preview/azext_storage_preview/_client_factory.py

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88
from knack.util import CLIError
99
from knack.log import get_logger
1010

11-
from .profiles import CUSTOM_DATA_STORAGE, CUSTOM_MGMT_PREVIEW_STORAGE, CUSTOM_DATA_STORAGE_FILESHARE
11+
12+
from .profiles import CUSTOM_DATA_STORAGE, CUSTOM_MGMT_PREVIEW_STORAGE, CUSTOM_DATA_STORAGE_FILESHARE, \
13+
CUSTOM_DATA_STORAGE_FILEDATALAKE
1214

1315
MISSING_CREDENTIALS_ERROR_MESSAGE = """
1416
Missing credentials to access storage service. The following variations are accepted:
@@ -136,7 +138,6 @@ def cf_mgmt_file_services(cli_ctx, _):
136138

137139

138140
def get_account_url(cli_ctx, account_name, service):
139-
from knack.util import CLIError
140141
if account_name is None:
141142
raise CLIError("Please provide storage account name or connection string.")
142143
storage_endpoint = cli_ctx.cloud.suffixes.storage_endpoint
@@ -172,3 +173,29 @@ def cf_share_directory_client(cli_ctx, kwargs):
172173

173174
def cf_share_file_client(cli_ctx, kwargs):
174175
return cf_share_client(cli_ctx, kwargs).get_file_client(file_path=kwargs.pop('file_path'))
176+
177+
178+
def cf_adls_service(cli_ctx, kwargs):
179+
client_kwargs = {}
180+
t_adls_service = get_sdk(cli_ctx, CUSTOM_DATA_STORAGE_FILEDATALAKE,
181+
'_data_lake_service_client#DataLakeServiceClient')
182+
connection_string = kwargs.pop('connection_string', None)
183+
account_name = kwargs.pop('account_name', None)
184+
account_key = kwargs.pop('account_key', None)
185+
token_credential = kwargs.pop('token_credential', None)
186+
sas_token = kwargs.pop('sas_token', None)
187+
# Enable NetworkTraceLoggingPolicy which logs all headers (except Authorization) without being redacted
188+
client_kwargs['logging_enable'] = True
189+
if connection_string:
190+
return t_adls_service.from_connection_string(conn_str=connection_string, **client_kwargs)
191+
192+
account_url = get_account_url(cli_ctx, account_name=account_name, service='dfs')
193+
credential = account_key or sas_token or token_credential
194+
195+
if account_url and credential:
196+
return t_adls_service(account_url=account_url, credential=credential, **client_kwargs)
197+
return None
198+
199+
200+
def cf_adls_file_system(cli_ctx, kwargs):
201+
return cf_adls_service(cli_ctx, kwargs).get_file_system_client(file_system=kwargs.pop('file_system_name'))

src/storage-preview/azext_storage_preview/_help.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,3 +394,45 @@
394394
- name: Upload a set of files in a local directory to a storage blob directory.
395395
text: az storage blob directory upload -c MyContainer --account-name MyStorageAccount -s "path/to/file*" -d directory --recursive
396396
"""
397+
398+
helps['storage fs list-deleted-path'] = """
399+
type: command
400+
short-summary: List the deleted (file or directory) paths under the specified file system.
401+
examples:
402+
- name: List the deleted (file or directory) paths under the specified file system..
403+
text: |
404+
az storage fs list-deleted-path -f myfilesystem --account-name mystorageccount --account-key 00000000
405+
"""
406+
407+
helps['storage fs service-properties'] = """
408+
type: group
409+
short-summary: Manage storage datalake service properties.
410+
"""
411+
412+
helps['storage fs service-properties show'] = """
413+
type: command
414+
short-summary: Show the properties of a storage account's datalake service, including Azure Storage Analytics.
415+
examples:
416+
- name: Show the properties of a storage account's datalake service
417+
text: |
418+
az storage fs service-properties show --account-name mystorageccount --account-key 00000000
419+
"""
420+
421+
helps['storage fs service-properties update'] = """
422+
type: command
423+
short-summary: Update the properties of a storage account's datalake service, including Azure Storage Analytics.
424+
examples:
425+
- name: Update the properties of a storage account's datalake service
426+
text: |
427+
az storage fs service-properties update --delete-retention --delete-retention-period 7 --account-name mystorageccount --account-key 00000000
428+
"""
429+
430+
helps['storage fs undelete-path'] = """
431+
type: command
432+
short-summary: Restore soft-deleted path.
433+
long-summary: Operation will only be successful if used within the specified number of days set in the delete retention policy.
434+
examples:
435+
- name: Restore soft-deleted path.
436+
text: |
437+
az storage fs undelete-path -f myfilesystem --deleted-path-name dir --deletion-id 0000 --account-name mystorageccount --account-key 00000000
438+
"""

src/storage-preview/azext_storage_preview/_params.py

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
validate_storage_data_plane_list,
1414
process_resource_group, add_upload_progress_callback)
1515

16-
from .profiles import CUSTOM_MGMT_PREVIEW_STORAGE
16+
from .profiles import CUSTOM_MGMT_PREVIEW_STORAGE, CUSTOM_DATA_STORAGE_FILEDATALAKE
1717

1818

1919
def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statements
@@ -54,6 +54,9 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
5454
'e.g."user::rwx,user:john.doe@contoso:rwx,group::r--,other::---,mask::rwx".')
5555
progress_type = CLIArgumentType(help='Include this flag to disable progress reporting for the command.',
5656
action='store_true', validator=add_upload_progress_callback)
57+
timeout_type = CLIArgumentType(
58+
help='Request timeout in seconds. Applies to each call to the service.', type=int
59+
)
5760

5861
with self.argument_context('storage') as c:
5962
c.argument('container_name', container_name_type)
@@ -157,15 +160,15 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
157160

158161
with self.argument_context('storage blob service-properties update') as c:
159162
c.argument('delete_retention', arg_type=get_three_state_flag(), arg_group='Soft Delete',
160-
help='Enables soft-delete.')
163+
help='Enable soft-delete.')
161164
c.argument('days_retained', type=int, arg_group='Soft Delete',
162165
help='Number of days that soft-deleted blob will be retained. Must be in range [1,365].')
163166
c.argument('static_website', arg_group='Static Website', arg_type=get_three_state_flag(),
164-
help='Enables static-website.')
167+
help='Enable static-website.')
165168
c.argument('index_document', help='Represents the name of the index document. This is commonly "index.html".',
166169
arg_group='Static Website')
167170
c.argument('error_document_404_path', options_list=['--404-document'], arg_group='Static Website',
168-
help='Represents the path to the error document that should be shown when an error 404 is issued,'
171+
help='Represent the path to the error document that should be shown when an error 404 is issued,'
169172
' in other words, when a browser requests a page that does not exist.')
170173

171174
with self.argument_context('storage azcopy blob upload') as c:
@@ -374,3 +377,34 @@ def load_arguments(self, _): # pylint: disable=too-many-locals, too-many-statem
374377
c.register_content_settings_argument(t_file_content_settings, update=False, arg_group='Content Settings',
375378
process_md5=True)
376379
c.extra('no_progress', progress_type)
380+
381+
with self.argument_context('storage fs service-properties update', resource_type=CUSTOM_DATA_STORAGE_FILEDATALAKE,
382+
min_api='2020-06-12') as c:
383+
c.argument('delete_retention', arg_type=get_three_state_flag(), arg_group='Soft Delete',
384+
help='Enable soft-delete.')
385+
c.argument('delete_retention_period', type=int, arg_group='Soft Delete',
386+
options_list=['--delete-retention-period', '--period'],
387+
help='Number of days that soft-deleted fs will be retained. Must be in range [1,365].')
388+
c.argument('enable_static_website', options_list=['--static-website'], arg_group='Static Website',
389+
arg_type=get_three_state_flag(),
390+
help='Enable static-website.')
391+
c.argument('index_document', help='Represent the name of the index document. This is commonly "index.html".',
392+
arg_group='Static Website')
393+
c.argument('error_document_404_path', options_list=['--404-document'], arg_group='Static Website',
394+
help='Represent the path to the error document that should be shown when an error 404 is issued,'
395+
' in other words, when a browser requests a page that does not exist.')
396+
397+
for item in ['list-deleted-path', 'undelete-path']:
398+
with self.argument_context('storage fs {}'.format(item)) as c:
399+
c.extra('file_system_name', options_list=['--file-system', '-f'],
400+
help="File system name.", required=True)
401+
c.extra('timeout', timeout_type)
402+
403+
with self.argument_context('storage fs list-deleted-path') as c:
404+
c.argument('path_prefix', help='Filter the results to return only paths under the specified path.')
405+
c.argument('num_results', type=int, help='Specify the maximum number to return.')
406+
c.argument('marker', help='A string value that identifies the portion of the list of containers to be '
407+
'returned with the next listing operation. The operation returns the NextMarker value within '
408+
'the response body if the listing operation did not return all containers remaining to be listed '
409+
'with the current page. If specified, this generator will begin returning results from the point '
410+
'where the previous generator stopped.')

src/storage-preview/azext_storage_preview/_transformers.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ def transform_storage_list_output(result):
122122
return list(result)
123123

124124

125+
# pylint: disable=unused-argument
125126
def transform_file_upload(result):
126127
return None

src/storage-preview/azext_storage_preview/_validators.py

Lines changed: 49 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -76,59 +76,85 @@ def validate_bypass(namespace):
7676
namespace.bypass = ', '.join(namespace.bypass) if isinstance(namespace.bypass, list) else namespace.bypass
7777

7878

79+
def get_config_value(cmd, section, key, default):
80+
return cmd.cli_ctx.config.get(section, key, default)
81+
82+
83+
def is_storagev2(import_prefix):
84+
return import_prefix.startswith('azure.multiapi.storagev2.') or 'datalake' in import_prefix
85+
86+
7987
def validate_client_parameters(cmd, namespace):
8088
""" Retrieves storage connection parameters from environment variables and parses out connection string into
8189
account name and key """
8290
n = namespace
8391

84-
def get_config_value(section, key, default):
85-
return cmd.cli_ctx.config.get(section, key, default)
86-
8792
if hasattr(n, 'auth_mode'):
88-
auth_mode = n.auth_mode or get_config_value('storage', 'auth_mode', None)
93+
auth_mode = n.auth_mode or get_config_value(cmd, 'storage', 'auth_mode', None)
8994
del n.auth_mode
9095
if not n.account_name:
91-
n.account_name = get_config_value('storage', 'account', None)
96+
n.account_name = get_config_value(cmd, 'storage', 'account', None)
9297
if auth_mode == 'login':
93-
n.token_credential = _create_token_credential(cmd.cli_ctx)
94-
95-
# give warning if there are account key args being ignored
96-
account_key_args = [n.account_key and "--account-key", n.sas_token and "--sas-token",
97-
n.connection_string and "--connection-string"]
98-
account_key_args = [arg for arg in account_key_args if arg]
99-
100-
if account_key_args:
101-
logger.warning('In "login" auth mode, the following arguments are ignored: %s',
102-
' ,'.join(account_key_args))
103-
return
98+
prefix = cmd.command_kwargs['resource_type'].import_prefix
99+
# is_storagv2() is used to distinguish if the command is in track2 SDK
100+
# If yes, we will use get_login_credentials() as token credential
101+
if is_storagev2(prefix):
102+
from azure.cli.core._profile import Profile
103+
profile = Profile(cli_ctx=cmd.cli_ctx)
104+
n.token_credential, _, _ = profile.get_login_credentials(subscription_id=n._subscription)
105+
# Otherwise, we will assume it is in track1 and keep previous token updater
106+
else:
107+
n.token_credential = _create_token_credential(cmd.cli_ctx)
108+
109+
if hasattr(n, 'token_credential') and n.token_credential:
110+
# give warning if there are account key args being ignored
111+
account_key_args = [n.account_key and "--account-key", n.sas_token and "--sas-token",
112+
n.connection_string and "--connection-string"]
113+
account_key_args = [arg for arg in account_key_args if arg]
114+
115+
if account_key_args:
116+
logger.warning('In "login" auth mode, the following arguments are ignored: %s',
117+
' ,'.join(account_key_args))
118+
return
104119

105120
if not n.connection_string:
106-
n.connection_string = get_config_value('storage', 'connection_string', None)
121+
n.connection_string = get_config_value(cmd, 'storage', 'connection_string', None)
107122

108123
# if connection string supplied or in environment variables, extract account key and name
109124
if n.connection_string:
110125
conn_dict = validate_key_value_pairs(n.connection_string)
111126
n.account_name = conn_dict.get('AccountName')
112127
n.account_key = conn_dict.get('AccountKey')
113-
if not n.account_name or not n.account_key:
114-
raise CLIError('Connection-string: %s, is malformed. Some shell environments require the '
115-
'connection string to be surrounded by quotes.' % n.connection_string)
128+
n.sas_token = conn_dict.get('SharedAccessSignature')
116129

117130
# otherwise, simply try to retrieve the remaining variables from environment variables
118131
if not n.account_name:
119-
n.account_name = get_config_value('storage', 'account', None)
132+
n.account_name = get_config_value(cmd, 'storage', 'account', None)
120133
if not n.account_key:
121-
n.account_key = get_config_value('storage', 'key', None)
134+
n.account_key = get_config_value(cmd, 'storage', 'key', None)
122135
if not n.sas_token:
123-
n.sas_token = get_config_value('storage', 'sas_token', None)
136+
n.sas_token = get_config_value(cmd, 'storage', 'sas_token', None)
124137

125138
# strip the '?' from sas token. the portal and command line are returns sas token in different
126139
# forms
127140
if n.sas_token:
128141
n.sas_token = n.sas_token.lstrip('?')
129142

143+
# account name with secondary
144+
if n.account_name and n.account_name.endswith('-secondary'):
145+
n.location_mode = 'secondary'
146+
n.account_name = n.account_name[:-10]
147+
130148
# if account name is specified but no key, attempt to query
131149
if n.account_name and not n.account_key and not n.sas_token:
150+
logger.warning('There are no credentials provided in your command and environment, we will query for the '
151+
'account key inside your storage account. \nPlease provide --connection-string, '
152+
'--account-key or --sas-token as credentials, or use `--auth-mode login` if you '
153+
'have required RBAC roles in your command. For more information about RBAC roles '
154+
'in storage, visit '
155+
'https://docs.microsoft.com/en-us/azure/storage/common/storage-auth-aad-rbac-cli. \n'
156+
'Setting the corresponding environment variables can avoid inputting credentials in '
157+
'your command. Please use --help to get more information.')
132158
n.account_key = _query_account_key(cmd.cli_ctx, n.account_name)
133159

134160

0 commit comments

Comments
 (0)