Merge lp:~sinzui/juju-release-tools/azure-rc3 into lp:juju-release-tools

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 312
Proposed branch: lp:~sinzui/juju-release-tools/azure-rc3
Merge into: lp:juju-release-tools
Diff against target: 292 lines (+91/-38)
2 files modified
azure_publish_tools.py (+28/-11)
tests/test_azure_publish_tools.py (+63/-27)
To merge this branch: bzr merge lp:~sinzui/juju-release-tools/azure-rc3
Reviewer Review Type Date Requested Status
Aaron Bentley (community) Approve
Review via email: mp+297812@code.launchpad.net

Description of the change

switch azure_publish_tools to use azure==2.0.0rc3

The feature-slave accidentally got the old azure lib, breaking the azure HA test. We need to switch everything
to the same library to avoid this maintenance burden

This branch updates azure_publish_tools to use the new azure storage lib (azure-storage (0.31.0)). This lib is automatically installed with azure==2.0.0rc3, but is not apart of that ARM project. This lib is an evolution
of version 0.8.0. Generic blobs are avoided in this new lib.

We use the BlockBlobService now and it manages the types, so a few methods loose some args.
It works with our existing account.
It suggests it wants an SharedAccessKey, but we do not require it...we are all powerful.
The public put_blob() method was removed.
put_block() does the initial put_blob().
The block ids must be caste to BlobBlock() which appears to manage state.
The optional BlobProperties were moved into a ContentSetting object

I have put, synced, and deleted several files and this

I added test coverage for delete_files()

My next branch will deal with the pip dep changes.

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :

This looks good. It would be nice if the credentials.yaml could be used, but I am not sure whether it contains the right values.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'azure_publish_tools.py'
2--- azure_publish_tools.py 2016-02-23 14:47:09 +0000
3+++ azure_publish_tools.py 2016-06-17 21:21:24 +0000
4@@ -17,7 +17,12 @@
5 import socket
6 import sys
7
8-from azure.storage import BlobService
9+from azure.storage.blob import (
10+ BlobBlock,
11+ BlockBlobService,
12+ ContentSettings,
13+ Include,
14+ )
15
16
17 mimetypes.init()
18@@ -69,11 +74,13 @@
19 """Return the SyncFile info about files under the specified prefix."""
20 files = []
21 for blob in blob_service.list_blobs(
22- JUJU_DIST, prefix=prefix, include='metadata'):
23+ JUJU_DIST, prefix=prefix, include=Include.METADATA):
24 sync_file = SyncFile(
25- path=blob.name, md5content=blob.properties.content_md5,
26+ path=blob.name,
27+ md5content=blob.properties.content_settings.content_md5,
28 size=blob.properties.content_length,
29- mimetype=blob.properties.content_type, local_path='')
30+ mimetype=blob.properties.content_settings.content_type,
31+ local_path='')
32 files.append(sync_file)
33 return sorted(files, key=attrgetter('path'))
34
35@@ -123,7 +130,6 @@
36 the azure restrictions. The blocks are then assembled into a blob
37 with the md5 content (base64 encoded digest).
38 """
39- blob_service.put_blob(JUJU_DIST, sync_file.path, '', 'BlockBlob')
40 block_ids = []
41 index = 0
42 with open(sync_file.local_path, 'rb') as local_file:
43@@ -135,7 +141,7 @@
44 try:
45 blob_service.put_block(
46 JUJU_DIST, sync_file.path, data, block_id)
47- block_ids.append(block_id)
48+ block_ids.append(BlobBlock(id=block_id))
49 index += 1
50 break
51 except socket.error as e:
52@@ -147,10 +153,12 @@
53 break
54 for i in range(0, 3):
55 try:
56+ content_settings = ContentSettings(
57+ content_type=sync_file.mimetype,
58+ content_md5=sync_file.md5content)
59 blob_service.put_block_list(
60 JUJU_DIST, sync_file.path, block_ids,
61- x_ms_blob_content_type=sync_file.mimetype,
62- x_ms_blob_content_md5=sync_file.md5content)
63+ content_settings=content_settings)
64 break
65 except socket.error as e:
66 if e.errno not in (socket.errno.ECONNREFUSED,
67@@ -242,7 +250,8 @@
68 """Execute the commands from the command line."""
69 parser = get_option_parser()
70 args = parser.parse_args()
71- blob_service = BlobService()
72+ blob_service = BlockBlobService(
73+ account_name=args.account_name, account_key=args.account_key)
74 if args.command == SYNC:
75 return sync_files(blob_service, args.prefix, args.local_dir, args)
76 if args.purpose not in PURPOSES:
77@@ -264,12 +273,20 @@
78 parser.add_argument("-d", "--dry-run", action="store_true",
79 default=False, help=dr_help)
80 parser.add_argument('-v', '--verbose', action="store_true",
81- default=False, help='Increse verbosity.')
82+ default=False, help='Increase verbosity.')
83
84
85 def get_option_parser():
86 """Return the option parser for this program."""
87- parser = ArgumentParser("Manage objects in Azure blob storage")
88+ parser = ArgumentParser(description="Manage objects in Azure blob storage")
89+ parser.add_argument(
90+ '--account-name',
91+ default=os.environ.get('AZURE_STORAGE_ACCOUNT', None),
92+ help="The azure storage account, or env AZURE_STORAGE_ACCOUNT.")
93+ parser.add_argument(
94+ '--account-key',
95+ default=os.environ.get('AZURE_STORAGE_ACCESS_KEY', None),
96+ help="The azure storage account, or env AZURE_STORAGE_ACCESS_KEY.")
97 subparsers = parser.add_subparsers(help='sub-command help', dest='command')
98 for command in (LIST, PUBLISH, DELETE):
99 subparser = subparsers.add_parser(command, help='Command to run')
100
101=== modified file 'tests/test_azure_publish_tools.py'
102--- tests/test_azure_publish_tools.py 2015-10-19 15:10:46 +0000
103+++ tests/test_azure_publish_tools.py 2016-06-17 21:21:24 +0000
104@@ -4,6 +4,7 @@
105
106 from azure_publish_tools import (
107 DELETE,
108+ delete_files,
109 get_option_parser,
110 get_local_files,
111 get_local_sync_files,
112@@ -25,6 +26,12 @@
113 write_file,
114 )
115
116+from azure.storage.blob import (
117+ BlobBlock,
118+ ContentSettings,
119+ Include,
120+ )
121+
122
123 md5sum = {
124 'qux': '2FsSE0c8L9fCBFAgprnGKw==',
125@@ -41,13 +48,14 @@
126 def test_list(self):
127 args = self.parse_args(['list', 'mypurpose'])
128 self.assertEqual(Namespace(
129- command=LIST, purpose='mypurpose'), args)
130+ command=LIST, purpose='mypurpose',
131+ account_key=None, account_name=None), args)
132
133 def test_publish(self):
134 args = self.parse_args(['publish', 'mypurpose', 'mypath'])
135 self.assertEqual(Namespace(
136 command=PUBLISH, purpose='mypurpose', dry_run=False, verbose=False,
137- path='mypath'), args)
138+ path='mypath', account_key=None, account_name=None), args)
139
140 def test_publish_dry_run(self):
141 args = self.parse_args(['publish', 'mypurpose', 'mypath', '--dry-run'])
142@@ -65,7 +73,7 @@
143 args = self.parse_args(['delete', 'mypurpose', 'mypath'])
144 self.assertEqual(Namespace(
145 command=DELETE, purpose='mypurpose', dry_run=False, verbose=False,
146- path=['mypath']), args)
147+ path=['mypath'], account_key=None, account_name=None), args)
148
149 def test_delete_dry_run(self):
150 args = self.parse_args(['delete', 'mypurpose', 'mypath', '--dry-run'])
151@@ -83,7 +91,7 @@
152 args = self.parse_args(['sync', 'mypath', 'myprefix'])
153 self.assertEqual(Namespace(
154 command=SYNC, prefix='myprefix', dry_run=False, verbose=False,
155- local_dir='mypath'), args)
156+ local_dir='mypath', account_key=None, account_name=None), args)
157
158 def test_sync_dry_run(self):
159 args = self.parse_args(['sync', 'mypath', 'myprefix', '--dry-run'])
160@@ -101,9 +109,9 @@
161 class FakeBlobProperties:
162
163 def __init__(self, md5, length, content_type):
164- self.content_md5 = md5
165 self.content_length = length
166- self.content_type = content_type
167+ self.content_settings = ContentSettings(
168+ content_type=content_type, content_md5=md5)
169
170
171 class FakeBlob:
172@@ -130,35 +138,31 @@
173 self.containers = {JUJU_DIST: blobs}
174
175 def list_blobs(self, container_name, prefix=None, marker=None,
176- maxresults=None, include=None, delimiter=None):
177+ timeout=None, include=None, delimiter=None):
178 if marker is not None:
179 raise NotImplementedError('marker not implemented.')
180- if maxresults is not None:
181- raise NotImplementedError('maxresults not implemented.')
182- if include != 'metadata':
183- raise NotImplementedError('include must be "metadata".')
184+ if timeout is not None:
185+ raise NotImplementedError('timeout not implemented.')
186+ if include != Include.METADATA:
187+ raise NotImplementedError('include must be "Include.METADATA".')
188 if delimiter is not None:
189 raise NotImplementedError('delimiter not implemented.')
190 return [b for p, b in self.containers[container_name].items()
191 if p.startswith(prefix)]
192
193- def put_blob(self, container_name, blob_name, blob, x_ms_blob_type):
194- if x_ms_blob_type != 'BlockBlob':
195- raise NotImplementedError('x_ms_blob_type not implemented.')
196- if blob != '':
197- raise NotImplementedError('blob not implemented.')
198- self.containers[container_name][blob_name] = FakeBlob(blob_name)
199-
200 def put_block(self, container_name, blob_name, block, block_id):
201- self.containers[container_name][blob_name]._blocks[block_id] = block
202+ if blob_name not in self.containers[container_name]:
203+ self.containers[container_name][blob_name] = FakeBlob(blob_name)
204+ self.containers[container_name][blob_name]._blocks[
205+ BlobBlock(block_id).id] = block
206
207 def put_block_list(self, container_name, blob_name, block_list,
208- content_md5=None, x_ms_blob_content_type=None,
209- x_ms_blob_content_encoding=None,
210- x_ms_blob_content_language=None,
211- x_ms_blob_content_md5=None):
212+ content_settings=None):
213 pass
214
215+ def delete_blob(self, container_name, blob_name):
216+ del self.containers[container_name][blob_name]
217+
218
219 class TestGetPublishedFiles(QuietTestCase):
220
221@@ -238,7 +242,7 @@
222 self.assertEqual(['tools/index.json', 'tools/index2.json'],
223 service.containers[JUJU_DIST].keys())
224 blob = service.containers[JUJU_DIST]['tools/index2.json']
225- self.assertEqual({'MA==': '{}\n'}, blob._blocks)
226+ self.assertEqual({BlobBlock('MA==').id: '{}\n'}, blob._blocks)
227
228 def test_same_local_remote(self):
229 args = Namespace(verbose=False, dry_run=False)
230@@ -270,7 +274,7 @@
231 self.assertEqual(['tools/index2.json'],
232 service.containers[JUJU_DIST].keys())
233 blob = service.containers[JUJU_DIST]['tools/index2.json']
234- self.assertEqual({'MA==': '{}\n'}, blob._blocks)
235+ self.assertEqual({BlobBlock('MA==').id: '{}\n'}, blob._blocks)
236
237 def test_different_local_remote_dry_run(self):
238 args = Namespace(verbose=False, dry_run=True)
239@@ -322,7 +326,7 @@
240 self.assertEqual(['tools/index.json', 'tools/index2.json'],
241 service.containers[JUJU_DIST].keys())
242 blob = service.containers[JUJU_DIST]['tools/index2.json']
243- self.assertEqual({'MA==': '{}\n'}, blob._blocks)
244+ self.assertEqual({BlobBlock('MA==').id: '{}\n'}, blob._blocks)
245
246 def test_different_local_remote(self):
247 args = Namespace(verbose=False, dry_run=False)
248@@ -337,7 +341,7 @@
249 self.assertEqual(['tools/index2.json'],
250 service.containers[JUJU_DIST].keys())
251 blob = service.containers[JUJU_DIST]['tools/index2.json']
252- self.assertEqual({'MA==': '{}\n'}, blob._blocks)
253+ self.assertEqual({BlobBlock('MA==').id: '{}\n'}, blob._blocks)
254
255 def test_different_local_remote_dry_run(self):
256 args = Namespace(verbose=False, dry_run=True)
257@@ -449,3 +453,35 @@
258 os.symlink('foo', foo_path)
259 result = get_local_sync_files('bools', local_dir)
260 self.assertEqual([], result)
261+
262+
263+class TestDeleteFiles(TestCase):
264+
265+ def test_delete_files(self):
266+ args = Namespace(verbose=False, dry_run=False)
267+ file1 = SyncFile(
268+ 'index.json', 33, 'md5-asdf', 'application/json', '')
269+ file2 = SyncFile(
270+ 'other.json', 33, 'md5-asdf', 'application/json', '')
271+ blob_service = FakeBlobService({
272+ 'tools/index.json': FakeBlob.from_sync_file(file1),
273+ 'tools/other.json': FakeBlob.from_sync_file(file2)
274+ })
275+ delete_files(blob_service, 'released', ['index.json'], args)
276+ self.assertIsNone(
277+ blob_service.containers[JUJU_DIST].get('tools/index.json'))
278+ self.assertEqual(
279+ file2.path,
280+ blob_service.containers[JUJU_DIST]['tools/other.json'].name)
281+
282+ def test_delete_files_dry_run(self):
283+ args = Namespace(verbose=False, dry_run=True)
284+ file1 = SyncFile(
285+ 'index.json', 33, 'md5-asdf', 'application/json', '')
286+ blob_service = FakeBlobService({
287+ 'tools/index.json': FakeBlob.from_sync_file(file1),
288+ })
289+ delete_files(blob_service, 'released', ['index.json'], args)
290+ self.assertEqual(
291+ file1.path,
292+ blob_service.containers[JUJU_DIST]['tools/index.json'].name)

Subscribers

People subscribed via source and target branches