Merge lp:~abentley/juju-release-tools/append-images into lp:juju-release-tools

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 281
Proposed branch: lp:~abentley/juju-release-tools/append-images
Merge into: lp:juju-release-tools
Diff against target: 524 lines (+515/-0)
2 files modified
make_aws_image_streams.py (+204/-0)
tests/test_make_aws_image_streams.py (+311/-0)
To merge this branch: bzr merge lp:~abentley/juju-release-tools/append-images
Reviewer Review Type Date Requested Status
Curtis Hovey (community) code Approve
Review via email: mp+289380@code.launchpad.net

Commit message

Support generating image streams for Centos7.

Description of the change

This branch implements support for generating Centos7 image streams.

It generates streams directly, instead of generating the intermediate JSON representation. This avoids two issues that prevented json2streams from working:
- It always uses "content-download" as the datatype.
- It fails if the JSON does not contain a 'size', even if it has no path.

It takes advantage of the Juju 2.0 credentials format, which is more convenient, even giving a standardized way of finding AWS credentials.

The product code aw0evgkw8e5c1q413zgy5pjce is used to find images. This is the official CentOS product code: https://wiki.centos.org/Cloud/AWS

This apppears to be the only way we can be certain to select the official images.

We have no aws-gov credentials, so these regions are skipped. http://cloud-images.ubuntu.com/releases/streams/v1/com.ubuntu.cloud:released:aws.json does not appear to have any existing gov entries, either.

To post a comment you must log in.
Revision history for this message
Curtis Hovey (sinzui) wrote :

Thank you.

review: Approve (code)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added file 'make_aws_image_streams.py'
--- make_aws_image_streams.py 1970-01-01 00:00:00 +0000
+++ make_aws_image_streams.py 2016-03-17 14:53:09 +0000
@@ -0,0 +1,204 @@
1#!/usr/bin/env python3
2from __future__ import print_function
3
4from argparse import ArgumentParser
5from datetime import datetime
6import os
7import sys
8from textwrap import dedent
9import yaml
10
11from boto import ec2
12from simplestreams.generate_simplestreams import (
13 items2content_trees,
14 )
15from simplestreams.json2streams import (
16 Item,
17 write_juju_streams,
18 )
19from simplestreams.util import timestamp
20
21
22def get_parameters(argv=None):
23 """Return streams, creds_filename for this invocation.
24
25 streams is the directory to write streams into.
26 creds_filename is the filename to get credentials from.
27 """
28 parser = ArgumentParser(description=dedent("""
29 Write image streams for AWS images. Only CentOS 7 is currently
30 supported."""))
31 parser.add_argument('streams', help='The directory to write streams to.')
32 args = parser.parse_args(argv)
33 try:
34 juju_data = os.environ['JUJU_DATA']
35 except KeyError:
36 print('JUJU_DATA must be set to a directory containing'
37 ' credentials.yaml.', file=sys.stderr)
38 sys.exit(1)
39 creds_filename = os.path.join(juju_data, 'credentials.yaml')
40 return args.streams, creds_filename
41
42
43def make_aws_credentials(creds):
44 """Convert credentials from juju format to AWS/Boto format."""
45 for creds in creds.values():
46 return {
47 'aws_access_key_id': creds['access-key'],
48 'aws_secret_access_key': creds['secret-key'],
49 }
50 else:
51 raise LookupError('No credentials found!')
52
53
54def is_china(region):
55 """Determine whether the supplied region is in AWS-China."""
56 return region.endpoint.endswith('.amazonaws.com.cn')
57
58
59def iter_region_connection(credentials, china_credentials):
60 """Iterate through connections for all regions except gov.
61
62 AWS-China regions will be connected using china_credentials.
63 US-GOV regions will be skipped.
64 All other regions will be connected using credentials.
65 """
66 regions = ec2.regions()
67 for region in regions:
68 if 'us-gov' in region.name:
69 continue
70 if is_china(region):
71 yield region.connect(**china_credentials)
72 else:
73 yield region.connect(**credentials)
74
75
76def iter_centos_images(credentials, china_credentials):
77 """Iterate through CentOS 7 images in standard AWS and AWS China."""
78 for conn in iter_region_connection(credentials, china_credentials):
79 images = conn.get_all_images(filters={
80 'owner_alias': 'aws-marketplace',
81 'product_code': 'aw0evgkw8e5c1q413zgy5pjce',
82 # 'name': 'CentOS Linux 7*',
83 })
84 for image in images:
85 yield image
86
87
88def make_item_name(region, vtype, store):
89 """Determine the item_name, given an image's attributes.
90
91 :param region: The region name
92 :param vtype: The virtualization type
93 :param store: The root device type.
94 """
95 # This is a port of code from simplestreams/tools/make-test-data, which is
96 # not provided as library code.
97 dmap = {
98 "north": "nn",
99 "northeast": "ne",
100 "east": "ee",
101 "southeast": "se",
102 "south": "ss",
103 "southwest": "sw",
104 "west": "ww",
105 "northwest": "nw",
106 "central": "cc",
107 }
108 itmap = {
109 'pv': {'instance': "pi", "ebs": "pe", "ssd": "es", "io1": "eo"},
110 'hvm': {'instance': "hi", "ebs": "he", "ssd": "hs", "io1": "ho"}
111 }
112 if store == "instance-store":
113 store = 'instance'
114 elif '-' in store:
115 store = store.split('-')[-1]
116 if vtype == "paravirtual":
117 vtype = "pv"
118
119 # create the item key:
120 # - 2 letter country code (us) . 3 for govcloud (gww)
121 # - 2 letter direction (nn=north, nw=northwest, cc=central)
122 # - 1 digit number
123 # - 1 char for virt type
124 # - 1 char for root-store type
125
126 # Handle special case of 'gov' regions
127 pre_cc = ""
128 _region = region
129 if '-gov-' in region:
130 _region = region.replace('gov-', '')
131 pre_cc = "g"
132
133 (cc, direction, num) = _region.split("-")
134
135 ikey = pre_cc + cc + dmap[direction] + num + itmap[vtype][store]
136 return ikey
137
138
139def make_item(image, now):
140 """Convert Centos 7 Boto image to simplestreams Item.
141
142 :param now: the current datetime.
143 """
144 if image.architecture != 'x86_64':
145 raise ValueError(
146 'Architecture is "{}", not "x86_64".'.format(image.architecture))
147 if not image.name.startswith('CentOS Linux 7 '):
148 raise ValueError(
149 'Name "{}" does not begin with "CentOS Linux 7".'.format(
150 image.name))
151 item_name = make_item_name(image.region.name, image.virtualization_type,
152 image.root_device_type)
153 version_name = now.strftime('%Y%m%d')
154 content_id = 'com.ubuntu.cloud.released:aws'
155 if is_china(image.region):
156 content_id = 'com.ubuntu.cloud.released:aws-cn'
157 else:
158 content_id = 'com.ubuntu.cloud.released:aws'
159 return Item(
160 content_id, 'com.ubuntu.cloud:server:centos7:amd64', version_name,
161 item_name, {
162 'endpoint': 'https://{}'.format(image.region.endpoint),
163 'region': image.region.name,
164 'arch': 'amd64',
165 'os': 'centos',
166 'virt': image.virtualization_type,
167 'id': image.id,
168 'version': 'centos7',
169 'label': 'release',
170 'release': 'centos7',
171 'release_codename': 'centos7',
172 'release_title': 'Centos 7',
173 'root_store': image.root_device_type,
174 })
175
176
177def write_streams(credentials, china_credentials, now, streams):
178 """Write image streams for Centos 7.
179
180 :param credentials: The standard AWS credentials.
181 :param china_credentials: The AWS China crentials.
182 :param now: The current datetime.
183 :param streams: The directory to store streams metadata in.
184 """
185 items = [make_item(i, now) for i in iter_centos_images(
186 credentials, china_credentials)]
187 updated = timestamp()
188 data = {'updated': updated, 'datatype': 'image-ids'}
189 trees = items2content_trees(items, data)
190 write_juju_streams(streams, trees, updated)
191
192
193def main():
194 streams, creds_filename = get_parameters()
195 with open(creds_filename) as creds_file:
196 all_credentials = yaml.safe_load(creds_file)['credentials']
197 credentials = make_aws_credentials(all_credentials['aws'])
198 china_credentials = make_aws_credentials(all_credentials['aws-china'])
199 now = datetime.utcnow()
200 write_streams(credentials, china_credentials, now, streams)
201
202
203if __name__ == '__main__':
204 main()
0205
=== added file 'tests/test_make_aws_image_streams.py'
--- tests/test_make_aws_image_streams.py 1970-01-01 00:00:00 +0000
+++ tests/test_make_aws_image_streams.py 2016-03-17 14:53:09 +0000
@@ -0,0 +1,311 @@
1from datetime import datetime
2import json
3import os
4from StringIO import StringIO
5from unittest import TestCase
6
7from mock import (
8 Mock,
9 patch,
10 )
11
12from make_aws_image_streams import (
13 is_china,
14 iter_centos_images,
15 iter_region_connection,
16 get_parameters,
17 make_aws_credentials,
18 make_item,
19 make_item_name,
20 write_streams,
21 )
22from utils import temp_dir
23
24
25class TestIsChina(TestCase):
26
27 def test_is_china(self):
28 region = Mock()
29 region.endpoint = 'foo.amazonaws.com.cn'
30 self.assertIs(True, is_china(region))
31 region.endpoint = 'foo.amazonaws.com'
32 self.assertIs(False, is_china(region))
33
34
35def make_mock_region(stem, name=None, endpoint=None):
36 if endpoint is None:
37 endpoint = '{}-end'.format(stem)
38 region = Mock(endpoint=endpoint)
39 if name is None:
40 name = '{}-name'.format(stem)
41 region.name = name
42 return region
43
44
45class IterRegionConnection(TestCase):
46
47 def test_iter_region_connection(self):
48 east = make_mock_region('east')
49 west = make_mock_region('west')
50 aws = {}
51 with patch('make_aws_image_streams.ec2.regions', autospec=True,
52 return_value=[east, west]) as regions_mock:
53 connections = [x for x in iter_region_connection(aws, None)]
54 regions_mock.assert_called_once_with()
55 self.assertEqual(
56 [east.connect.return_value, west.connect.return_value],
57 connections)
58 east.connect.assert_called_once_with(**aws)
59 west.connect.assert_called_once_with(**aws)
60
61 def test_gov_region(self):
62 east = make_mock_region('east')
63 gov = make_mock_region('west', name='foo-us-gov-bar')
64 aws = {}
65 with patch('make_aws_image_streams.ec2.regions', autospec=True,
66 return_value=[east, gov]) as regions_mock:
67 connections = [x for x in iter_region_connection(aws, None)]
68 regions_mock.assert_called_once_with()
69 self.assertEqual(
70 [east.connect.return_value], connections)
71 east.connect.assert_called_once_with(**aws)
72 self.assertEqual(0, gov.connect.call_count)
73
74 def test_china_region(self):
75 east = make_mock_region('east')
76 west = make_mock_region('west', endpoint='west-end.amazonaws.com.cn')
77 east.name = 'east-name'
78 west.name = 'west-name'
79 aws = {'name': 'aws'}
80 aws_cn = {'name': 'aws-cn'}
81 with patch('make_aws_image_streams.ec2.regions', autospec=True,
82 return_value=[east, west]) as regions_mock:
83 connections = [x for x in iter_region_connection(aws, aws_cn)]
84 regions_mock.assert_called_once_with()
85 self.assertEqual(
86 [east.connect.return_value, west.connect.return_value],
87 connections)
88 east.connect.assert_called_once_with(**aws)
89 west.connect.assert_called_once_with(**aws_cn)
90
91
92class IterCentosImages(TestCase):
93
94 def test_iter_centos_images(self):
95 aws = {'name': 'aws'}
96 aws_cn = {'name': 'aws-cn'}
97 east_imgs = ['east-1', 'east-2']
98 west_imgs = ['west-1', 'west-2']
99 east_conn = Mock()
100 east_conn.get_all_images.return_value = east_imgs
101 west_conn = Mock()
102 west_conn.get_all_images.return_value = west_imgs
103 with patch('make_aws_image_streams.iter_region_connection',
104 return_value=[east_conn, west_conn],
105 autospec=True) as irc_mock:
106 imgs = list(iter_centos_images(aws, aws_cn))
107 self.assertEqual(east_imgs + west_imgs, imgs)
108 irc_mock.assert_called_once_with(aws, aws_cn)
109 east_conn.get_all_images.assert_called_once_with(filters={
110 'owner_alias': 'aws-marketplace',
111 'product_code': 'aw0evgkw8e5c1q413zgy5pjce',
112 })
113 west_conn.get_all_images.assert_called_once_with(filters={
114 'owner_alias': 'aws-marketplace',
115 'product_code': 'aw0evgkw8e5c1q413zgy5pjce',
116 })
117
118
119class TestMakeAWSCredentials(TestCase):
120
121 def test_happy_path(self):
122 aws_credentials = make_aws_credentials({'credentials': {
123 'access-key': 'foo',
124 'secret-key': 'bar',
125 }})
126 self.assertEqual({
127 'aws_access_key_id': 'foo',
128 'aws_secret_access_key': 'bar',
129 }, aws_credentials)
130
131 def test_no_credentials(self):
132 with self.assertRaisesRegexp(LookupError, 'No credentials found!'):
133 make_aws_credentials({})
134
135 def test_multiple_credentials(self):
136 # If multiple credentials are present, an arbitrary credential will be
137 # used.
138 aws_credentials = make_aws_credentials({
139 'credentials-1': {
140 'access-key': 'foo',
141 'secret-key': 'bar',
142 },
143 'credentials-2': {
144 'access-key': 'baz',
145 'secret-key': 'qux',
146 },
147 })
148 self.assertIn(aws_credentials, [
149 {'aws_access_key_id': 'foo', 'aws_secret_access_key': 'bar'},
150 {'aws_access_key_id': 'baz', 'aws_secret_access_key': 'qux'},
151 ])
152
153
154def make_mock_image(region_name='us-northeast-3'):
155 image = Mock(virtualization_type='hvm', id='qux',
156 root_device_type='ebs', architecture='x86_64')
157 image.name = 'CentOS Linux 7 foo'
158 image.region.endpoint = 'foo'
159 image.region.name = region_name
160 return image
161
162
163class TetMakeItem(TestCase):
164
165 def test_happy_path(self):
166 image = make_mock_image()
167 now = datetime(2001, 02, 03)
168 item = make_item(image, now)
169 self.assertEqual(item.content_id, 'com.ubuntu.cloud.released:aws')
170 self.assertEqual(item.product_name,
171 'com.ubuntu.cloud:server:centos7:amd64')
172 self.assertEqual(item.item_name, 'usne3he')
173 self.assertEqual(item.version_name, '20010203')
174 self.assertEqual(item.data, {
175 'endpoint': 'https://foo',
176 'region': 'us-northeast-3',
177 'arch': 'amd64',
178 'os': 'centos',
179 'virt': 'hvm',
180 'id': 'qux',
181 'version': 'centos7',
182 'label': 'release',
183 'release': 'centos7',
184 'release_codename': 'centos7',
185 'release_title': 'Centos 7',
186 'root_store': 'ebs',
187 })
188
189 def test_china(self):
190 image = make_mock_image()
191 image.region.endpoint = 'foo.amazonaws.com.cn'
192 now = datetime(2001, 02, 03)
193 item = make_item(image, now)
194 self.assertEqual(item.content_id, 'com.ubuntu.cloud.released:aws-cn')
195 self.assertEqual(item.data['endpoint'], 'https://foo.amazonaws.com.cn')
196
197 def test_not_x86_64(self):
198 image = make_mock_image()
199 image.architecture = 'ppc128'
200 now = datetime(2001, 02, 03)
201 with self.assertRaisesRegexp(ValueError,
202 'Architecture is "ppc128", not'
203 ' "x86_64".'):
204 make_item(image, now)
205
206 def test_not_centos_7(self):
207 image = make_mock_image()
208 image.name = 'CentOS Linux 8'
209 now = datetime(2001, 02, 03)
210 with self.assertRaisesRegexp(ValueError,
211 'Name "CentOS Linux 8" does not begin'
212 ' with "CentOS Linux 7".'):
213 make_item(image, now)
214
215
216class TestGetParameters(TestCase):
217
218 def test_happy_path(self):
219 with patch.dict(os.environ, {'JUJU_DATA': 'foo'}):
220 streams, creds_filename = get_parameters(['bar'])
221 self.assertEqual(creds_filename, 'foo/credentials.yaml')
222 self.assertEqual(streams, 'bar')
223
224 def test_no_juju_data(self):
225 stderr = StringIO()
226 with self.assertRaises(SystemExit):
227 with patch('sys.stderr', stderr):
228 get_parameters(['bar'])
229 self.assertEqual(
230 stderr.getvalue(),
231 'JUJU_DATA must be set to a directory containing'
232 ' credentials.yaml.\n')
233
234
235class TestMakeItemName(TestCase):
236
237 def test_make_item_name(self):
238 item_name = make_item_name('us-east-1', 'paravirtual', 'instance')
239 self.assertEqual(item_name, 'usee1pi')
240 item_name = make_item_name('cn-northwest-3', 'hvm', 'ebs')
241 self.assertEqual(item_name, 'cnnw3he')
242
243
244def load_json(parent, filename):
245 with open(os.path.join(parent, 'streams', 'v1', filename)) as f:
246 return json.load(f)
247
248
249class TestWriteStreams(TestCase):
250
251 def test_write_streams(self):
252 now = datetime(2001, 02, 03)
253 credentials = {'name': 'aws'}
254 china_credentials = {'name': 'aws-cn'}
255 east_conn = Mock()
256 east_image = make_mock_image(region_name='us-east-1')
257 east_conn.get_all_images.return_value = [east_image]
258 west_conn = Mock()
259 west_image = make_mock_image(region_name='us-west-1')
260 west_conn.get_all_images.return_value = [west_image]
261 with temp_dir() as streams:
262 with patch('make_aws_image_streams.iter_region_connection',
263 return_value=[east_conn, west_conn],
264 autospec=True) as irc_mock:
265 with patch('make_aws_image_streams.timestamp',
266 return_value='now'):
267 with patch('sys.stderr'):
268 write_streams(credentials, china_credentials, now,
269 streams)
270 index = load_json(streams, 'index.json')
271 index2 = load_json(streams, 'index2.json')
272 releases = load_json(streams, 'com.ubuntu.cloud.released-aws.json')
273 irc_mock.assert_called_once_with(credentials, china_credentials)
274 self.assertEqual(
275 {'format': 'index:1.0', 'updated': 'now', 'index': {}}, index)
276 self.assertEqual(
277 {'format': 'index:1.0', 'updated': 'now', 'index': {
278 'com.ubuntu.cloud.released:aws': {
279 'format': 'products:1.0',
280 'updated': 'now',
281 'datatype': 'image-ids',
282 'path': 'streams/v1/com.ubuntu.cloud.released-aws.json',
283 'products': ['com.ubuntu.cloud:server:centos7:amd64'],
284 }
285 }}, index2)
286 expected = {
287 'content_id': 'com.ubuntu.cloud.released:aws',
288 'format': 'products:1.0',
289 'updated': 'now',
290 'datatype': 'image-ids',
291 'products': {'com.ubuntu.cloud:server:centos7:amd64': {
292 'root_store': 'ebs',
293 'endpoint': 'https://foo',
294 'arch': 'amd64',
295 'release_title': 'Centos 7',
296 'label': 'release',
297 'release_codename': 'centos7',
298 'version': 'centos7',
299 'virt': 'hvm',
300 'release': 'centos7',
301 'os': 'centos',
302 'id': 'qux',
303 'versions': {'20010203': {
304 'items': {
305 'usww1he': {'region': 'us-west-1'},
306 'usee1he': {'region': 'us-east-1'},
307 }
308 }},
309 }},
310 }
311 self.assertEqual(releases, expected)

Subscribers

People subscribed via source and target branches