Merge lp:~sinzui/juju-release-tools/one-cloud-publish into lp:juju-release-tools

Proposed by Curtis Hovey
Status: Merged
Merged at revision: 211
Proposed branch: lp:~sinzui/juju-release-tools/one-cloud-publish
Merge into: lp:juju-release-tools
Diff against target: 263 lines (+254/-0)
2 files modified
publish_streams.py (+127/-0)
tests/test_publish_streams.py (+127/-0)
To merge this branch: bzr merge lp:~sinzui/juju-release-tools/one-cloud-publish
Reviewer Review Type Date Requested Status
Martin Packman (community) Approve
Review via email: mp+268235@code.launchpad.net

Description of the change

Verify local metadata to cloud metadata.

This branch introduces a rewrite of the publish-public-tools.bash script. The publish_streams.py script can verify a local stream with a remote stream just like the older script. eg.

    ../juju-release-tools/publish_streams.py -d -v -r testing released ./juju-dist azure

My next branch will do syncing.

To post a comment you must log in.
Revision history for this message
Martin Packman (gz) wrote :

Looks good, some comments inline.

review: Approve
217. By Curtis Hovey

simplify the CPCS dict.

218. By Curtis Hovey

Updated docstrings.

Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you for the review. I updated the code and commented inline regarding why "stream" is not used in the verification case.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'publish_streams.py'
2--- publish_streams.py 1970-01-01 00:00:00 +0000
3+++ publish_streams.py 2015-08-17 17:00:00 +0000
4@@ -0,0 +1,127 @@
5+#!/usr/bin/python
6+
7+from __future__ import print_function
8+
9+__metaclass__ = type
10+
11+from argparse import ArgumentParser
12+import difflib
13+import os
14+import sys
15+import traceback
16+import urllib2
17+
18+
19+CPCS = {
20+ 'aws': "http://juju-dist.s3.amazonaws.com",
21+ 'azure': "https://jujutools.blob.core.windows.net/juju-tools",
22+ 'canonistack': (
23+ "https://swift.canonistack.canonical.com"
24+ "/v1/AUTH_526ad877f3e3464589dc1145dfeaac60/juju-dist"),
25+ 'hp': (
26+ "https://region-a.geo-1.objects.hpcloudsvc.com"
27+ "/v1/60502529753910/juju-dist"),
28+ 'joyent': (
29+ "https://us-east.manta.joyent.com/cpcjoyentsupport/public/juju-dist"),
30+ }
31+
32+
33+def get_remote_file(url):
34+ """Return the content of a remote file."""
35+ response = urllib2.urlopen(url)
36+ content = response.read()
37+ return content
38+
39+
40+def diff_files(local, remote):
41+ """Return the difference of a local and a remote file.
42+
43+ :return: a tuple of identical (True, None) or different (False, str).
44+ """
45+ with open(local, 'r') as f:
46+ local_lines = f.read().splitlines()
47+ remote_lines = get_remote_file(remote).splitlines()
48+ diff_gen = difflib.unified_diff(local_lines, remote_lines, local, remote)
49+ diff = '\n'.join(list(diff_gen))
50+ if diff:
51+ return False, diff
52+ return True, None
53+
54+
55+def verify_metadata(location, remote_stream, verbose=False):
56+ """Verify all the streams metadata in a cloud matches the local metadata.
57+
58+
59+ This verifies all the streams, not a single stream, to ensure the cloud
60+ has exactly the same metadata as the local instance.
61+ """
62+ local_metadata = os.path.join(location, 'tools', 'streams', 'v1')
63+ remote_metadata = '{}/tools/streams/v1'.format(remote_stream)
64+ if verbose:
65+ print('comparing {} to {}'.format(local_metadata, remote_metadata))
66+ for data_file in os.listdir(local_metadata):
67+ if data_file.endswith('.json'):
68+ local_file = os.path.join(local_metadata, data_file)
69+ remote_file = '{}/{}'.format(remote_metadata, data_file)
70+ if verbose:
71+ print('comparing {}'.format(data_file))
72+ identical, diff = diff_files(local_file, remote_file)
73+ if not identical:
74+ return False, diff
75+ if verbose:
76+ print('All json matches')
77+ return True, None
78+
79+
80+def publish(stream, location, cloud,
81+ remote_root=None, dry_run=False, verbose=False):
82+ """Publish a stream to a cloud and verify it."""
83+ if remote_root:
84+ remote_stream = '{}/{}'.format(CPCS[cloud], remote_root)
85+ else:
86+ remote_stream = CPCS.get(cloud)
87+ verify_metadata(location, remote_stream, verbose=verbose)
88+
89+
90+def parse_args(argv=None):
91+ """Return the argument parser for this program."""
92+ parser = ArgumentParser("Publish streams to a cloud.")
93+ parser.add_argument(
94+ '-d', '--dry-run', action="store_true", help='Do not make changes.')
95+ parser.add_argument(
96+ '-v', '--verbose', action="store_true", help='Increase verbosity.')
97+ parser.add_argument(
98+ '-r', '--remote-root',
99+ help='An alternate root to publish to such as testing or weekly.')
100+ parser.add_argument(
101+ 'stream', help='The agent-stream to publish.',
102+ choices=['released', 'proposed', 'devel'])
103+ parser.add_argument(
104+ 'location', type=os.path.expanduser,
105+ help='The path to the local tree of all streams (tools/).')
106+ parser.add_argument(
107+ 'cloud', help='The destination cloud.',
108+ choices=['streams', 'aws', 'azure', 'hp', 'joyent', 'canonistack'])
109+ return parser.parse_args(argv)
110+
111+
112+def main(argv):
113+ """Publish streams to a cloud."""
114+ args = parse_args(argv)
115+ try:
116+ publish(
117+ args.stream, args.location, args.cloud,
118+ remote_root=args.remote_root, dry_run=args.dry_run,
119+ verbose=args.verbose)
120+ except Exception as e:
121+ print('{}: {}'.format(e.__class__.__name__, e))
122+ if args.verbose:
123+ traceback.print_tb(sys.exc_info()[2])
124+ return 2
125+ if args.verbose:
126+ print("Done.")
127+ return 0
128+
129+
130+if __name__ == '__main__':
131+ sys.exit(main(sys.argv[1:]))
132
133=== added file 'tests/test_publish_streams.py'
134--- tests/test_publish_streams.py 1970-01-01 00:00:00 +0000
135+++ tests/test_publish_streams.py 2015-08-17 17:00:00 +0000
136@@ -0,0 +1,127 @@
137+from mock import patch
138+import os
139+import re
140+from StringIO import StringIO
141+from unittest import TestCase
142+
143+from publish_streams import (
144+ CPCS,
145+ diff_files,
146+ get_remote_file,
147+ main,
148+ parse_args,
149+ publish,
150+ verify_metadata,
151+)
152+
153+from utils import temp_dir
154+
155+
156+class PublishStreamsTestCase(TestCase):
157+
158+ def test_parse_args(self):
159+ args = parse_args(
160+ ['-d', '-v', '-r', 'testing', 'released', '~/juju-dist', 'aws'])
161+ self.assertTrue(args.dry_run)
162+ self.assertTrue(args.verbose)
163+ self.assertEqual('testing', args.remote_root)
164+ self.assertEqual('released', args.stream)
165+ self.assertEqual(os.path.expanduser('~/juju-dist'), args.location)
166+ self.assertEqual('aws', args.cloud)
167+
168+ @patch('publish_streams.publish', autospec=True)
169+ def test_main(self, p_mock):
170+ exit_code = main(['-r', 'testing', 'released', '~/juju-dist', 'aws'])
171+ self.assertEqual(0, exit_code)
172+ p_mock.assert_called_with(
173+ 'released', os.path.expanduser('~/juju-dist'),
174+ 'aws', remote_root='testing', dry_run=False, verbose=False)
175+
176+ @patch('publish_streams.urllib2.urlopen', autospec=True)
177+ def test_get_remote_file(self, uo_mock):
178+ uo_mock.return_value = StringIO('data')
179+ content = get_remote_file('http://foo/bar.json')
180+ uo_mock.assert_called_with('http://foo/bar.json')
181+ self.assertEqual('data', content)
182+
183+ @patch('publish_streams.get_remote_file', autospec=True)
184+ def test_diff_files(self, gr_mock):
185+ gr_mock.return_value = 'one\ntwo\nthree'
186+ with temp_dir() as base:
187+ local_path = os.path.join(base, 'bar.json')
188+ with open(local_path, 'w') as local_file:
189+ local_file.write('one\ntwo\nthree')
190+ identical, diff = diff_files(local_path, 'http://foo/bar.json')
191+ self.assertTrue(identical)
192+ self.assertIsNone(diff)
193+ gr_mock.assert_called_with('http://foo/bar.json')
194+
195+ @patch('publish_streams.get_remote_file', autospec=True)
196+ def test_diff_files_different(self, gr_mock):
197+ gr_mock.return_value = 'one\ntwo\nfour'
198+ with temp_dir() as base:
199+ local_path = os.path.join(base, 'bar.json')
200+ with open(local_path, 'w') as local_file:
201+ local_file.write('one\ntwo\nthree')
202+ identical, diff = diff_files(local_path, 'http://foo/bar.json')
203+ self.assertFalse(identical)
204+ normalized_diff = re.sub('/tmp/.*/bar', '/tmp/bar', diff)
205+ self.assertEqual(
206+ '--- /tmp/bar.json\n\n'
207+ '+++ http://foo/bar.json\n\n'
208+ '@@ -1,3 +1,3 @@\n\n'
209+ ' one\n'
210+ ' two\n'
211+ '-three\n'
212+ '+four',
213+ normalized_diff)
214+
215+ @patch('publish_streams.diff_files', autospec=True)
216+ def test_verify_metadata(self, df_mock):
217+ df_mock.return_value = (True, None)
218+ with temp_dir() as base:
219+ metadata_path = os.path.join(base, 'tools', 'streams', 'v1')
220+ os.makedirs(metadata_path)
221+ metadata_file = os.path.join(metadata_path, 'bar.json')
222+ metadasta_sig = os.path.join(metadata_path, 'bar.json.sig')
223+ with open(metadata_file, 'w') as local_file:
224+ local_file.write('bar.json')
225+ with open(metadasta_sig, 'w') as local_file:
226+ local_file.write('bar.json.sig')
227+ identical, diff = verify_metadata(base, CPCS['aws'], verbose=False)
228+ self.assertTrue(identical)
229+ self.assertIsNone(diff)
230+ self.assertEqual(1, df_mock.call_count)
231+ df_mock.assert_called_with(
232+ metadata_file, '{}/tools/streams/v1/bar.json'.format(CPCS['aws']))
233+
234+ @patch('publish_streams.diff_files', autospec=True)
235+ def test_verify_metadata_with_root_faile(self, df_mock):
236+ df_mock.return_value = (False, 'different')
237+ with temp_dir() as base:
238+ metadata_path = os.path.join(base, 'tools', 'streams', 'v1')
239+ os.makedirs(metadata_path)
240+ metadata_file = os.path.join(metadata_path, 'bar.json')
241+ with open(metadata_file, 'w') as local_file:
242+ local_file.write('bar.json')
243+ identical, diff = verify_metadata(base, CPCS['aws'])
244+ self.assertFalse(identical)
245+ self.assertEqual(diff, 'different')
246+ df_mock.assert_called_with(
247+ metadata_file, '{}/tools/streams/v1/bar.json'.format(CPCS['aws']))
248+
249+ @patch('publish_streams.verify_metadata', autospec=True)
250+ def test_publish(self, vm_mock):
251+ vm_mock.return_value = (True, None)
252+ publish('testing', '/streams/juju-dist', 'aws',
253+ remote_root=None, dry_run=False, verbose=False)
254+ vm_mock.assert_called_with(
255+ '/streams/juju-dist', CPCS['aws'], verbose=False)
256+
257+ @patch('publish_streams.verify_metadata', autospec=True)
258+ def test_publish_with_remote(self, vm_mock):
259+ vm_mock.return_value = (True, None)
260+ publish('testing', '/streams/juju-dist', 'aws',
261+ remote_root='weekly', dry_run=False, verbose=False)
262+ vm_mock.assert_called_with(
263+ '/streams/juju-dist', '%s/weekly' % CPCS['aws'], verbose=False)

Subscribers

People subscribed via source and target branches