azure_sas.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. #!/usr/bin/env python
  2. from azure_storage.methods import create_blob_sas, create_parent_parser, sas_prep, setup_arguments, write_sas
  3. from argparse import ArgumentParser, RawTextHelpFormatter
  4. import coloredlogs
  5. import logging
  6. import azure
  7. import sys
  8. import os
  9. class AzureContainerSAS(object):
  10. def main(self):
  11. # Validate container name, retrieve connection string, extract account key, create blob service client and
  12. # container clients
  13. self.container_name, \
  14. self.connect_str, \
  15. self.account_key, \
  16. self.blob_service_client, \
  17. self.container_client = sas_prep(container_name=self.container_name,
  18. passphrase=self.passphrase,
  19. account_name=self.account_name,
  20. create=False)
  21. # Create the SAS URLs for the files in the container
  22. self.sas_urls = self.container_sas(container_client=self.container_client,
  23. account_name=self.account_name,
  24. container_name=self.container_name,
  25. account_key=self.account_key,
  26. expiry=self.expiry,
  27. sas_urls=self.sas_urls)
  28. # Return to the requested logging level, as it has been increased to WARNING to suppress the log being
  29. # filled with information from azure.core.pipeline.policies.http_logging_policy
  30. coloredlogs.install(level=self.verbosity.upper())
  31. write_sas(output_file=self.output_file,
  32. sas_urls=self.sas_urls)
  33. # Write the SAS URLs to the output file
  34. write_sas(output_file=self.output_file,
  35. sas_urls=self.sas_urls)
  36. @staticmethod
  37. def container_sas(container_client, account_name, container_name, account_key, expiry, sas_urls):
  38. """
  39. Create SAS URLs for all files in the container
  40. :param container_client: type azure.storage.blob.BlobServiceClient.ContainerClient
  41. :param account_name: type str: Name of the Azure storage account
  42. :param container_name: type str: Name of the container of interest
  43. :param account_key: type str: Account key of Azure storage account
  44. :param expiry: type int: Number of days that the SAS URL will be valid
  45. :param sas_urls: type dict: Dictionary of file name: SAS URL (empty)
  46. :return: populated sas_urls
  47. """
  48. # Create a generator containing all the blobs in the container
  49. generator = container_client.list_blobs()
  50. try:
  51. # Hide the INFO-level messages sent to the logger from Azure by increasing the logging level to WARNING
  52. logging.getLogger().setLevel(logging.WARNING)
  53. for blob_file in generator:
  54. # Create the SAS URLs
  55. sas_urls = create_blob_sas(blob_file=blob_file,
  56. account_name=account_name,
  57. container_name=container_name,
  58. account_key=account_key,
  59. expiry=expiry,
  60. sas_urls=sas_urls)
  61. except azure.core.exceptions.ResourceNotFoundError:
  62. logging.error(f' The specified container, {container_name}, does not exist.')
  63. raise SystemExit
  64. return sas_urls
  65. def __init__(self, container_name, output_file, account_name, passphrase, expiry, verbosity):
  66. # Set the container name variable
  67. self.container_name = container_name
  68. # Output file
  69. if output_file.startswith('~'):
  70. self.output_file = os.path.abspath(os.path.expanduser(os.path.join(output_file)))
  71. else:
  72. self.output_file = os.path.abspath(os.path.join(output_file))
  73. # Ensure that the output file can be used
  74. if not os.path.isfile(self.output_file):
  75. try:
  76. # Create the parental directory for the output file as required
  77. os.makedirs(os.path.dirname(self.output_file), exist_ok=True)
  78. except PermissionError:
  79. logging.error(f'Insufficient permissions to create output file {self.output_file}')
  80. raise SystemExit
  81. try:
  82. open(self.output_file, 'w').close()
  83. except IsADirectoryError:
  84. logging.error(f'A directory or an empty file name was provided for the output file {self.output_file}')
  85. raise SystemExit
  86. except PermissionError:
  87. logging.error(f'Insufficient permissions to create output file {self.output_file}')
  88. raise SystemExit
  89. else:
  90. open(self.output_file, 'w').close()
  91. # Ensure that the expiry provided is valid
  92. try:
  93. assert 0 < expiry < 366
  94. except AssertionError:
  95. logging.error(f'The provided expiry ({expiry}) is invalid. It must be between 1 and 365')
  96. raise SystemExit
  97. self.expiry = expiry
  98. self.verbosity = verbosity
  99. # Initialise necessary class variables
  100. self.passphrase = passphrase
  101. self.account_name = account_name
  102. self.account_key = str()
  103. self.connect_str = str()
  104. self.blob_service_client = None
  105. self.container_client = None
  106. self.sas_urls = dict()
  107. class AzureSAS(object):
  108. def main(self):
  109. # Validate container name, retrieve connection string, extract account key, create blob service client and
  110. # container clients
  111. self.container_name, \
  112. self.connect_str, \
  113. self.account_key, \
  114. self.blob_service_client, \
  115. self.container_client = sas_prep(container_name=self.container_name,
  116. passphrase=self.passphrase,
  117. account_name=self.account_name,
  118. create=False)
  119. # Run the proper method depending on whether a file or a folder is requested
  120. if self.category == 'file':
  121. self.sas_urls = self.file_sas(container_client=self.container_client,
  122. account_name=self.account_name,
  123. container_name=self.container_name,
  124. object_name=self.object_name,
  125. account_key=self.account_key,
  126. expiry=self.expiry,
  127. sas_urls=self.sas_urls)
  128. elif self.category == 'folder':
  129. self.sas_urls = self.folder_sas(container_client=self.container_client,
  130. account_name=self.account_name,
  131. container_name=self.container_name,
  132. object_name=self.object_name,
  133. account_key=self.account_key,
  134. expiry=self.expiry,
  135. sas_urls=self.sas_urls)
  136. else:
  137. logging.error(f'Something is wrong. There is no {self.category} option available')
  138. raise SystemExit
  139. # Return to the requested logging level, as it has been increased to WARNING to suppress the log being
  140. # filled with information from azure.core.pipeline.policies.http_logging_policy
  141. coloredlogs.install(level=self.verbosity.upper())
  142. write_sas(output_file=self.output_file,
  143. sas_urls=self.sas_urls)
  144. @staticmethod
  145. def file_sas(container_client, account_name, container_name, object_name, account_key, expiry, sas_urls):
  146. """
  147. Create a SAS URL for the specified file in Azure storage
  148. :param container_client: type azure.storage.blob.BlobServiceClient.ContainerClient
  149. :param account_name: type str: Name of the Azure storage account
  150. :param container_name: type str: Name of the container of interest
  151. :param object_name: type str: Name and path of file for which a SAS URL is to be created
  152. :param account_key: type str: Account key of Azure storage account
  153. :param expiry: type int: Number of days that the SAS URL will be valid
  154. :param sas_urls: type dict: Dictionary of file name: SAS URL (empty)
  155. :return: populated sas_urls
  156. """
  157. # Create a generator containing all the blobs in the container
  158. generator = container_client.list_blobs()
  159. # Create a boolean to determine if the blob has been located
  160. present = False
  161. # Hide the INFO-level messages sent to the logger from Azure by increasing the logging level to WARNING
  162. logging.getLogger().setLevel(logging.WARNING)
  163. for blob_file in generator:
  164. # Filter for the blob name
  165. if blob_file.name == object_name:
  166. # Update the blob presence variable
  167. present = True
  168. sas_urls = create_blob_sas(blob_file=blob_file,
  169. account_name=account_name,
  170. container_name=container_name,
  171. account_key=account_key,
  172. expiry=expiry,
  173. sas_urls=sas_urls)
  174. # Send a warning to the user that the blob could not be found
  175. if not present:
  176. logging.error(f'Could not locate the desired file {object_name} in container {container_name}')
  177. raise SystemExit
  178. return sas_urls
  179. @staticmethod
  180. def folder_sas(container_client, account_name, container_name, object_name, account_key, expiry, sas_urls):
  181. """
  182. Create SAS URLs for all the files in the specified folder in Azure storage
  183. :param container_client: type azure.storage.blob.BlobServiceClient.ContainerClient
  184. :param account_name: type str: Name of the Azure storage account
  185. :param container_name: type str: Name of the container of interest
  186. :param object_name: type str: Name and path of folder containing files for which SAS URLs are to be created
  187. :param account_key: type str: Account key of Azure storage account
  188. :param expiry: type int: Number of days that the SAS URL will be valid
  189. :param sas_urls: type dict: Dictionary of file name: SAS URL (empty)
  190. :return: populated sas_urls
  191. """
  192. # Create a generator containing all the blobs in the container
  193. generator = container_client.list_blobs()
  194. # Boolean to track whether the folder was located
  195. present = False
  196. # Hide the INFO-level messages sent to the logger from Azure by increasing the logging level to WARNING
  197. logging.getLogger().setLevel(logging.WARNING)
  198. for blob_file in generator:
  199. # Create the path of the file by adding the container name to the path of the file
  200. blob_path = os.path.join(container_name, os.path.split(blob_file.name)[0])
  201. # Ensure that the supplied folder path is present in the blob path
  202. if os.path.normpath(object_name) in os.path.normpath(blob_path):
  203. # Update the folder presence boolean
  204. present = True
  205. sas_urls = create_blob_sas(blob_file=blob_file,
  206. account_name=account_name,
  207. container_name=container_name,
  208. account_key=account_key,
  209. expiry=expiry,
  210. sas_urls=sas_urls)
  211. # Send a warning to the user that the blob could not be found
  212. if not present:
  213. logging.error(f'Could not locate the desired folder {object_name} in container {container_name}')
  214. raise SystemExit
  215. return sas_urls
  216. def __init__(self, object_name, container_name, output_file, account_name, passphrase, expiry, verbosity, category):
  217. # Set the name of the file/folder of interest
  218. self.object_name = object_name
  219. # Set the container name variable
  220. self.container_name = container_name
  221. # Output file
  222. if output_file.startswith('~'):
  223. self.output_file = os.path.abspath(os.path.expanduser(os.path.join(output_file)))
  224. else:
  225. self.output_file = os.path.abspath(os.path.join(output_file))
  226. # Ensure that the output file can be used
  227. if not os.path.isfile(self.output_file):
  228. try:
  229. # Create the parental directory for the output file as required
  230. os.makedirs(os.path.dirname(self.output_file), exist_ok=True)
  231. except PermissionError:
  232. logging.error(f'Insufficient permissions to create output file {self.output_file}')
  233. raise SystemExit
  234. try:
  235. open(self.output_file, 'w').close()
  236. except IsADirectoryError:
  237. logging.error(f'A directory or an empty file name was provided for the output file {self.output_file}')
  238. raise SystemExit
  239. except PermissionError:
  240. logging.error(f'Insufficient permissions to create output file {self.output_file}')
  241. raise SystemExit
  242. else:
  243. open(self.output_file, 'w').close()
  244. # Ensure that the expiry provided is valid
  245. try:
  246. assert 0 < expiry < 366
  247. except AssertionError:
  248. logging.error(f'The provided expiry ({expiry}) is invalid. It must be between 1 and 365')
  249. raise SystemExit
  250. self.expiry = expiry
  251. self.verbosity = verbosity
  252. self.category = category
  253. # Initialise necessary class variables
  254. self.passphrase = passphrase
  255. self.account_name = account_name
  256. self.account_key = str()
  257. self.connect_str = str()
  258. self.blob_service_client = None
  259. self.container_client = None
  260. self.sas_urls = dict()
  261. def container_sas(args):
  262. """
  263. Run the AzureContainerSAS method
  264. :param args: type ArgumentParser arguments
  265. """
  266. logging.info(f'Creating SAS URLs for all files in Azure container {args.container_name}')
  267. # Create the container SAS object
  268. sas = AzureContainerSAS(container_name=args.container_name,
  269. output_file=args.output_file,
  270. account_name=args.account_name,
  271. passphrase=args.passphrase,
  272. expiry=args.expiry,
  273. verbosity=args.verbosity)
  274. sas.main()
  275. def file_sas(args):
  276. """
  277. Run the AzureSAS class for a file
  278. :param args: type ArgumentParser arguments
  279. """
  280. logging.info(f'Creating SAS URL for {args.file} in container {args.container_name} Azure storage account '
  281. f'{args.account_name}')
  282. # Create the file SAS object
  283. sas_file = AzureSAS(object_name=args.file,
  284. container_name=args.container_name,
  285. output_file=args.output_file,
  286. account_name=args.account_name,
  287. passphrase=args.passphrase,
  288. expiry=args.expiry,
  289. verbosity=args.verbosity,
  290. category='file')
  291. sas_file.main()
  292. def folder_sas(args):
  293. """
  294. Run the AzureSAS class for a folder
  295. :param args: type ArgumentParser arguments
  296. """
  297. logging.info(f'Creating SAS URLs for all files in folder {args.folder} in container {args.container_name} in '
  298. f'Azure storage account '
  299. f'{args.account_name}')
  300. # Create the folder SAS object
  301. sas_folder = AzureSAS(object_name=args.folder,
  302. container_name=args.container_name,
  303. output_file=args.output_file,
  304. account_name=args.account_name,
  305. passphrase=args.passphrase,
  306. expiry=args.expiry,
  307. verbosity=args.verbosity,
  308. category='folder')
  309. sas_folder.main()
  310. def cli():
  311. parser = ArgumentParser(description='Create shared access signatures (SAS) URLs for containers/files/folders in '
  312. 'Azure storage. Note that each file in a container/folder has to be downloaded '
  313. 'separately, so if there are 1000 files in the container, 1000 SAS URLs will '
  314. 'be provided')
  315. # Create the parental parser, and the subparser
  316. subparsers, parent_parser = create_parent_parser(parser=parser)
  317. parent_parser.add_argument('-e', '--expiry',
  318. default=10,
  319. type=int,
  320. help='The number of days that the SAS URL will be valid. The minimum is 1, and the '
  321. 'maximum is 365. The default is 10.')
  322. parent_parser.add_argument('-o', '--output_file',
  323. default=os.path.join(os.getcwd(), 'sas.txt'),
  324. help='Name and path of file in which the SAS URLs are to be saved. '
  325. 'Default is $CWD/sas.txt')
  326. # Container SAS subparser
  327. container_subparser = subparsers.add_parser(parents=[parent_parser],
  328. name='container',
  329. description='Create SAS URLs for all files in a container in Azure '
  330. 'storage',
  331. formatter_class=RawTextHelpFormatter,
  332. help='Create SAS URLs for all files in a container in Azure storage')
  333. container_subparser.set_defaults(func=container_sas)
  334. # File SAS subparser
  335. file_subparser = subparsers.add_parser(parents=[parent_parser],
  336. name='file',
  337. description='Create a SAS URL for a file in Azure storage',
  338. formatter_class=RawTextHelpFormatter,
  339. help='Create a SAS URL for a file in Azure storage')
  340. file_subparser.add_argument('-f', '--file',
  341. type=str,
  342. required=True,
  343. help='Path of file in Azure storage from which a SAS URL is to be created. '
  344. 'e.g. 2022-SEQ-0001_S1_L001_R1_001.fastq.gz')
  345. file_subparser.set_defaults(func=file_sas)
  346. # Folder SAS subparser
  347. folder_subparser = subparsers.add_parser(parents=[parent_parser],
  348. name='folder',
  349. description='Create SAS URLs for all files in a folder in Azure storage',
  350. formatter_class=RawTextHelpFormatter,
  351. help='Create SAS URLs for all files in a folder in Azure storage')
  352. folder_subparser.add_argument('-f', '--folder',
  353. type=str,
  354. required=True,
  355. help='Name of the folder for which SAS URLs are to be created for all files. '
  356. 'e.g. InterOp')
  357. folder_subparser.set_defaults(func=folder_sas)
  358. # Set up the arguments, and run the appropriate subparser
  359. arguments = setup_arguments(parser=parser)
  360. # Return to the requested logging level, as it has been increased to WARNING to suppress the log being filled with
  361. # information from azure.core.pipeline.policies.http_logging_policy
  362. coloredlogs.install(level=arguments.verbosity.upper())
  363. logging.info('SAS creation complete')
  364. # Prevent the arguments being printed to the console (they are returned in order for the tests to work)
  365. sys.stderr = open(os.devnull, 'w')
  366. return arguments
  367. if __name__ == '__main__':
  368. cli()