Skip to content

Commit 10fba27

Browse files
authored
Merge pull request #350 from Duke-GCB/azure-ddd
Add ddd command line tool for Cloud Storage
2 parents 0b4eb27 + ce332f5 commit 10fba27

11 files changed

Lines changed: 112 additions & 122 deletions

File tree

ddsc/azure/__main__.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
"""Duke data service command line project management utility."""
22
import sys
3-
from ddsc.ddsclient import DDSClient, AZURE_BACKING_STORAGE
43
from ddsc.exceptions import DDSUserException
4+
from ddsc.ddsclient import DDSClient, AZURE_BACKING_STORAGE
5+
from ddsc.config import create_config
6+
7+
8+
class AzureDDSClient(DDSClient):
9+
def __init__(self):
10+
super().__init__(backing_storage=AZURE_BACKING_STORAGE)
11+
12+
def _create_config(self, args):
13+
azure_container_name = None
14+
if "azure_container_name" in args:
15+
azure_container_name = args.azure_container_name
16+
return create_config(
17+
allow_insecure_config_file=args.allow_insecure_config_file,
18+
azure_container_name=azure_container_name
19+
)
520

621

722
def main(args=None):
823
if args is None:
924
args = sys.argv[1:]
10-
client = DDSClient(backing_storage=AZURE_BACKING_STORAGE)
25+
client = AzureDDSClient()
1126
try:
1227
client.run_command(args)
1328
except DDSUserException as ex:

ddsc/azure/api.py

Lines changed: 12 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import re
22
import json
33
import os.path
4-
from datetime import datetime, timedelta
5-
from azure.mgmt.storage import StorageManagementClient
6-
from azure.storage.filedatalake import DataLakeServiceClient, generate_file_system_sas
4+
from azure.storage.filedatalake import DataLakeServiceClient
75
from azure.core.exceptions import ResourceNotFoundError
8-
from azure.identity import TokenCachePersistenceOptions, SharedTokenCacheCredential, ChainedTokenCredential, \
9-
DeviceCodeCredential
6+
from azure.identity import DeviceCodeCredential, TokenCachePersistenceOptions, ChainedTokenCredential,\
7+
SharedTokenCacheCredential
108
from msgraph.core import GraphClient
119
from ddsc.azure.azcopy import create_azcopy, group_by_dirname
1210
from ddsc.azure.delivery import DataDelivery
@@ -91,11 +89,8 @@ def ensure_user_exists(self, netid):
9189

9290

9391
class Bucket(object):
94-
def __init__(self, credential, subscription_id, resource_group, storage_account, container_name):
95-
self.resource_group = resource_group
96-
self.storage_mgmt_client = StorageManagementClient(credential=credential, subscription_id=subscription_id)
97-
self.service = DataLakeServiceClient(f"https://{storage_account}.dfs.core.windows.net/", credential=credential,
98-
scopes=DLS_SCOPES)
92+
def __init__(self, credential, storage_account, container_name):
93+
self.service = DataLakeServiceClient(f"https://{storage_account}.dfs.core.windows.net/", credential=credential)
9994
self.file_system = self.service.get_file_system_client(file_system=container_name)
10095
self.azcopy = create_azcopy()
10196

@@ -116,28 +111,6 @@ def move_directory(self, source, destination):
116111
def get_file_properties(self, file_path):
117112
return self.file_system.get_file_client(file_path).get_file_properties()
118113

119-
def get_storage_account_key1(self):
120-
return self.storage_mgmt_client.storage_accounts.list_keys(
121-
resource_group_name=self.resource_group,
122-
account_name=self.service.account_name).keys[0].value
123-
124-
def get_sas_token(self, hours=6):
125-
account_key = self.get_storage_account_key1()
126-
return generate_file_system_sas(
127-
account_name=self.service.account_name,
128-
credential=account_key,
129-
file_system_name=self.file_system.file_system_name,
130-
permission="rwdl",
131-
protocol='https',
132-
expiry=datetime.utcnow() + timedelta(hours=hours)
133-
)
134-
135-
def get_sas_url(self, path, hours=6):
136-
account_name = self.service.account_name
137-
bucket_name = self.file_system.file_system_name
138-
token = self.get_sas_token(hours=hours)
139-
return f"https://{account_name}.blob.core.windows.net/{bucket_name}/{path}?{token}"
140-
141114
def get_url(self, path):
142115
account_name = self.service.account_name
143116
bucket_name = self.file_system.file_system_name
@@ -254,20 +227,21 @@ def __str__(self):
254227

255228

256229
class AzureApi(object):
257-
def __init__(self, config, credential, subscription_id, resource_group, storage_account, container_name):
230+
def __init__(self, config, credential):
258231
self.config = config
259232
self.users = Users(credential)
260233
self.current_user_netid = self.users.get_current_user_netid()
261-
self.bucket = Bucket(credential, subscription_id, resource_group, storage_account, container_name)
234+
container_name = config.azure_container_name
235+
if not container_name:
236+
container_name = self.current_user_netid
237+
self.bucket = Bucket(credential, config.azure_storage_account, container_name)
262238

263239
def list_projects(self):
264-
path_dicts = self.bucket.get_paths(path=self.current_user_netid, recursive=False)
240+
path_dicts = self.bucket.get_paths(path="", recursive=False)
265241
return [AzureProject(self, path_dict) for path_dict in path_dicts if path_dict["is_directory"] is True]
266242

267243
def get_project_by_name(self, name):
268244
path = name
269-
if "/" not in path:
270-
path = f"{self.current_user_netid}/{name}"
271245
try:
272246
return AzureProject(self, self.bucket.get_directory_properties(path))
273247
except ResourceNotFoundError:
@@ -301,9 +275,6 @@ def get_file_paths(self, path):
301275
def get_file_properties(self, file_path):
302276
return self.bucket.get_file_properties(file_path)
303277

304-
def get_sas_url(self):
305-
return self.bucket.get_sas_url(path=self.current_user_netid)
306-
307278
def add_user_to_project(self, project, netid, auth_role):
308279
user_id, user_name = self.users.get_id_and_name(netid)
309280
role = self.get_auth_role_by_id(auth_role)
@@ -317,15 +288,11 @@ def remove_user_from_project(self, project, netid):
317288

318289
def upload_paths(self, project_name, paths, dry_run):
319290
project_path = project_name
320-
if "/" not in project_path:
321-
project_path = f"{self.current_user_netid}/{project_name}/"
322291
self.bucket.upload_paths(project_path, paths, dry_run)
323292
print("\nUpload complete.\nSee azcopy log file for details about transferred files.\n\n")
324293

325294
def download_paths(self, project_name, include_paths, exclude_paths, destination, dry_run):
326295
project_path = project_name
327-
if "/" not in project_path:
328-
project_path = f"{self.current_user_netid}/{project_name}/"
329296
self.bucket.download_paths(project_path, include_paths, exclude_paths, destination, dry_run=dry_run)
330297
print("\nDownload complete.\nSee azcopy log file for details about transferred files.\n\n")
331298

@@ -352,15 +319,10 @@ def deliver(self, project, netid, resend, user_message, share_usernames):
352319

353320

354321
def create_azure_api(config):
355-
# Setup to use token cache or prompt user to login with a URL (DeviceCodeCredential)
356322
cache_persistence_options = TokenCachePersistenceOptions(allow_unencrypted_storage=True)
357323
credential = ChainedTokenCredential(
358324
SharedTokenCacheCredential(),
359325
DeviceCodeCredential(cache_persistence_options=cache_persistence_options))
360326
return AzureApi(
361327
config=config,
362-
credential=credential,
363-
subscription_id=config.azure_subscription_id,
364-
resource_group=config.azure_resource_group,
365-
storage_account=config.azure_storage_account,
366-
container_name=config.azure_container_name)
328+
credential=credential)

ddsc/azure/commands.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
CHECK_COMMAND_NOT_SUPPORTED_MSG = "Error: The check command is not supported or needed for the Azure backend.\n"
99
SHARE_NOT_SUPPORTED_MSG = "Error: The share command is not supported for the Azure backend.\n"
1010
COPY_NOT_SUPPORTED_FOR_AZURE_MSG = "Error: The --copy option is not supported for the Azure backend.\n"
11+
COMMAND_NOT_SUPPORTED_MSG = "Error: This command is not supported for the Azure backend.\n"
1112

1213

1314
class BaseAzureCommand(object):
@@ -74,16 +75,12 @@ def run(self, args):
7475

7576
class AzureAddUserCommand(BaseAzureCommand):
7677
def run(self, args):
77-
project = self.get_project(args)
78-
netid = self.get_netid(args)
79-
self.azure_api.add_user_to_project(project=project, netid=netid, auth_role=args.auth_role)
78+
raise DDSUserException(COMMAND_NOT_SUPPORTED_MSG)
8079

8180

8281
class AzureRemoveUserCommand(BaseAzureCommand):
8382
def run(self, args):
84-
project = self.get_project(args)
85-
netid = self.get_netid(args)
86-
self.azure_api.remove_user_from_project(project=project, netid=netid)
83+
raise DDSUserException(COMMAND_NOT_SUPPORTED_MSG)
8784

8885

8986
class AzureDownloadCommand(BaseAzureCommand):
@@ -135,8 +132,7 @@ def run(self, args):
135132

136133
class AzureListAuthRolesCommand(BaseAzureCommand):
137134
def run(self, args):
138-
for auth_role in self.azure_api.get_auth_roles():
139-
print(auth_role.id, "-", auth_role.description)
135+
raise DDSUserException(COMMAND_NOT_SUPPORTED_MSG)
140136

141137

142138
class AzureMoveCommand(BaseAzureCommand):

ddsc/azure/delivery.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import requests
22
import requests.exceptions
33
import json
4-
from ddsc.core.ddsapi import DataServiceAuth
54
from ddsc.exceptions import DDSUserException
65
from ddsc.core.d4s2 import UNAUTHORIZED_MESSAGE
76

7+
MISSING_DELIVERY_TOKEN_MSG = """
8+
ERROR: Missing credential to deliver projects.
9+
Please add 'delivery_token' to ~/.ddsclient config file.
10+
"""
11+
812

913
class DataDelivery(object):
1014
def __init__(self, config, api):
@@ -52,12 +56,13 @@ def _ensure_user_exists(self, netid):
5256

5357
class DeliveryApi(object):
5458
def __init__(self, config):
55-
auth = DataServiceAuth(config)
59+
if not config.delivery_token:
60+
raise DDSUserException(MISSING_DELIVERY_TOKEN_MSG)
5661
# Switch to v3 for azure endpoints (Once DDS API is deprecated switch default to v3)
5762
self.deliveries_url = config.azure_delivery_url + "/az-deliveries/"
5863
self.json_headers = {
5964
'Content-Type': 'application/json',
60-
'X-DukeDS-Authorization': auth.get_auth()
65+
'Authorization': 'Token ' + config.delivery_token
6166
}
6267

6368
def create_delivery(self, payload):
@@ -105,5 +110,5 @@ def _check_response(response):
105110
if response.status_code == 401:
106111
raise DDSUserException(UNAUTHORIZED_MESSAGE)
107112
if not 200 <= response.status_code < 300:
108-
msg_fmt = "Request to {} failed with {}:\n{}."
113+
msg_fmt = "Request to {} failed with {}:\n{}"
109114
raise DDSUserException(msg_fmt.format(response.url, response.status_code, response.text))

ddsc/azure/tests/test_azure_api.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_get_id_and_name(self, mock_graph_client):
6767

6868
class TestBucket(TestCase):
6969
def setUp(self):
70-
self.bucket = Bucket("Credentials", "subscription1", "resourceGroup2", "storageAccount3", "container4")
70+
self.bucket = Bucket("Credentials", "storageAccount3", "container4")
7171
self.bucket.storage_mgmt_client = Mock()
7272
self.bucket.service = Mock(account_name="storageAccount3")
7373
self.bucket.file_system = Mock(file_system_name="container4")
@@ -103,16 +103,6 @@ def test_get_file_properties(self):
103103
fc = self.bucket.file_system.get_file_client.return_value
104104
self.assertEqual(result, fc.get_file_properties.return_value)
105105

106-
def test_get_storage_account_key1(self):
107-
result = self.bucket.get_storage_account_key1()
108-
self.assertEqual(result, "SomeKey")
109-
110-
@patch('ddsc.azure.api.generate_file_system_sas')
111-
def test_get_sas_url(self, mock_generate_file_system_sas):
112-
mock_generate_file_system_sas.return_value = "sas=Key"
113-
url = self.bucket.get_sas_url("user1/mouse/file1.txt")
114-
self.assertEqual(url, "https://storageAccount3.blob.core.windows.net/container4/user1/mouse/file1.txt?sas=Key")
115-
116106
def test_get_url(self):
117107
url = self.bucket.get_url("user1/mouse/file1.txt")
118108
self.assertEqual(url, "https://storageAccount3.blob.core.windows.net/container4/user1/mouse/file1.txt")
@@ -239,8 +229,7 @@ class TestAzureApi(TestCase):
239229
def setUp(self, mock_bucket, mock_users):
240230
self.config = Mock()
241231
self.credential = Mock()
242-
self.api = AzureApi(config=self.config, credential=self.credential, subscription_id='sub2',
243-
resource_group='rg3', storage_account='sa4', container_name='cn5')
232+
self.api = AzureApi(config=self.config, credential=self.credential)
244233
self.api.current_user_netid = 'user1'
245234
self.bucket = self.api.bucket
246235
self.users = self.api.users
@@ -262,7 +251,7 @@ def test_list_projects(self):
262251
def test_get_project_by_name(self):
263252
project = self.api.get_project_by_name(name="mouse")
264253
self.assertEqual(project.name, "mouse")
265-
self.bucket.get_directory_properties.assert_called_with('user1/mouse')
254+
self.bucket.get_directory_properties.assert_called_with('mouse')
266255
self.bucket.get_directory_properties.side_effect = ResourceNotFoundError()
267256
self.assertEqual(self.api.get_project_by_name(name="mouse"), None)
268257

@@ -292,10 +281,6 @@ def test_get_file_properties(self):
292281
self.assertEqual(self.api.get_file_properties("user1/mouse/file1.txt"),
293282
self.bucket.get_file_properties.return_value)
294283

295-
def test_get_sas_url(self):
296-
self.assertEqual(self.api.get_sas_url(), self.bucket.get_sas_url.return_value)
297-
self.bucket.get_sas_url.assert_called_with(path='user1')
298-
299284
@patch('ddsc.azure.api.print')
300285
def test_add_user_to_project(self, mock_print):
301286
self.api.add_user_to_project(Mock(path="user1/mouse"), netid="user2", auth_role="file_uploader")
@@ -315,13 +300,13 @@ def test_remove_user_from_project(self, mock_print):
315300
@patch('ddsc.azure.api.print')
316301
def test_upload_paths(self, mock_print):
317302
self.api.upload_paths(project_name="mouse", paths="/tmp/data.txt", dry_run=False)
318-
self.bucket.upload_paths.assert_called_with('user1/mouse/', '/tmp/data.txt', False)
303+
self.bucket.upload_paths.assert_called_with('mouse', '/tmp/data.txt', False)
319304

320305
@patch('ddsc.azure.api.print')
321306
def test_download_paths(self, mock_print):
322307
self.api.download_paths(project_name="mouse", include_paths=None, exclude_paths=None, destination="/tmp/mouse",
323308
dry_run=False)
324-
self.bucket.download_paths.assert_called_with('user1/mouse/', None, None, '/tmp/mouse', dry_run=False)
309+
self.bucket.download_paths.assert_called_with('mouse', None, None, '/tmp/mouse', dry_run=False)
325310

326311
@patch('ddsc.azure.api.print')
327312
def test_move_path(self, mock_print):

0 commit comments

Comments
 (0)