Merge ~smoser/ubuntu/+source/simplestreams:bug/xenial-1686437-keystone-v3 into ubuntu/+source/simplestreams:ubuntu/xenial-devel

Proposed by Scott Moser
Status: Needs review
Proposed branch: ~smoser/ubuntu/+source/simplestreams:bug/xenial-1686437-keystone-v3
Merge into: ubuntu/+source/simplestreams:ubuntu/xenial-devel
Diff against target: 1845 lines (+1729/-11)
10 files modified
debian/changelog (+12/-0)
debian/patches/428-do-not-require-that-hypervisor_config-be-present.patch (+23/-0)
debian/patches/433-glance-ignore-inactive-images.patch (+42/-0)
debian/patches/435-glance-refactor-for-testing.patch (+853/-0)
debian/patches/436-glance-fix-race-conditions.patch (+479/-0)
debian/patches/450-453-454-keystone-v3-support.patch (+13/-10)
debian/patches/455-nova-lxd-support-squashfs-images.patch (+230/-0)
debian/patches/460-glance-handle-v2-auth-with-sessions.patch (+33/-0)
debian/patches/series (+8/-1)
debian/patches/skip-openstack-tests-if-no-libs.patch (+36/-0)
Reviewer Review Type Date Requested Status
Rafael David Tinoco (community) Disapprove
Billy Olsen (community) Approve
Scott Moser (community) Needs Resubmitting
Felipe Reyes (community) Approve
Eric Desrochers Pending
Review via email: mp+341214@code.launchpad.net

Description of the change

Openstack keystone v3 and nova-lxd squashfs SRU

Bugs:
 * bug 1578622: do not require that hypervisor_config be present.
 * bug 1583276: ignore inactive images
 * bug 1584938: glance fix race conditions in sync.
 * bug 1611987: glance-simplestreams-sync charm doesn't support keystone v3
 * bug 1686086: glance mirror and nova-lxd need support for squashfs images
 * bug 1686437: glance sync: need keystone v3 auth support
 * bug 1719879: [artful only] swift client needs to use v1 auth prior to ocata
 * bug 1728982: [artful only] openstack mirror with keystone v3 always imports new images

Merge proposals:
 - xenial: https://code.launchpad.net/~smoser/ubuntu/+source/simplestreams/+git/simplestreams/+merge/341214
 - artful: https://code.launchpad.net/~smoser/ubuntu/+source/simplestreams/+git/simplestreams/+merge/341215

PPA:
  https://launchpad.net/~smoser/+archive/ubuntu/sstream-ks3

To post a comment you must log in.
Revision history for this message
Felipe Reyes (freyes) wrote :

I tested this patch using the procedure described in the test case of bug 1686437 . To make the testing easier I created a ppa ( https://launchpad.net/~freyes/+archive/ubuntu/lp1686437 ). After installing this version running the sync up script successfully finished (connecting to keystone using v3 as verified in the logs)

# /etc/cron.daily/glance_simplestreams_sync
created c99bb337-cb1e-4873-9420-11920d911eee: auto-sync/ubuntu-trusty-14.04-amd64-server-20180404-disk1.img
created 4073e5b1-6dde-4534-8f16-5c7ea126d637: auto-sync/ubuntu-xenial-16.04-amd64-server-20180405-disk1.img

/var/log/glance-simplestreams-sync.log -> http://paste.ubuntu.com/p/7VWYsWKx4y/

review: Approve
Revision history for this message
David Ames (thedac) wrote :

Also Noting here the released version on xenial does not currently support Keystone v3 and blocks Bug #1611987.

For the record, we have been running a bzr branch @455 on serverstack (a keystone v3 cloud) for months now. So the code in simplestreams works.

Revision history for this message
Billy Olsen (billy-olsen) wrote :

The patch looks good to me, but I have a question inlined below regarding the addition of the squashfs format and the implications for syncing multiple image types into a nova lxd cloud using the ftype filter.

Revision history for this message
Scott Moser (smoser) wrote :

Billy made a good point. If we're going to pull this back we should grab revno 455 also.

review: Needs Fixing
Revision history for this message
Scott Moser (smoser) :
review: Needs Resubmitting
Revision history for this message
Scott Moser (smoser) wrote :

Billy and all,
Please re-review and ideally test the PPA upload.

Thanks.
Scott

Revision history for this message
Billy Olsen (billy-olsen) wrote :

Scott, thanks for pulling that back. Felipe is going to rebuild the package in the PPA with this latest patch pulled in. Code looks fine to me so I'll approve. I'll let Felipe comment on the testing of the PPA package.

review: Approve
a55c24b... by Scott Moser

Pull back glance related fixes from upstream.

This pulls back glance related changes up to upstream revision 455.

428-do-not-require-that-hypervisor_config-be-present.patch (LP: #1578622)
433-glance-ignore-inactive-images.patch (LP: #1583276)
436-glance-fix-race-conditions.patch (LP: #1584938)
450-453-454-keystone-v3-support.patch (LP: #1686437, #1728982, #1719879)
455-nova-lxd-support-squashfs-images.patch (LP: #1686086)

db8b81f... by Scott Moser

update changelog

7aa5026... by Scott Moser

Pull in revno 460 for keystone v2 session support.

  460-glance-handle-v2-auth-with-sessions.patch (LP: #1611987)

0ce3c41... by Scott Moser

update changelog

bb79379... by Scott Moser

mark 1.4 released

d197243... by Scott Moser

1.4~ppa0

Revision history for this message
Scott Moser (smoser) wrote :

an upload that I had done got into xenial for bug 1686437.
I've rebased this merge over the top of that.

Same content as before except for debian/changelog.
I've uploaded that to the ppa as 0.1.0~bzr426-0ubuntu1.4~ppa0 .
https://launchpad.net/~smoser/+archive/ubuntu/sstream-ks3/+packages

So from a code perspective, I'm pretty sure that all parties are on board with 0.1.0~bzr426-0ubuntu1.4 being uploaded... but we need to address SRU.

It now references 8 bugs, only 1 of which has an SRU template written.

Revision history for this message
Rafael David Tinoco (rafaeldtinoco) wrote :
review: Disapprove

Unmerged commits

d197243... by Scott Moser

1.4~ppa0

bb79379... by Scott Moser

mark 1.4 released

0ce3c41... by Scott Moser

update changelog

7aa5026... by Scott Moser

Pull in revno 460 for keystone v2 session support.

  460-glance-handle-v2-auth-with-sessions.patch (LP: #1611987)

db8b81f... by Scott Moser

update changelog

a55c24b... by Scott Moser

Pull back glance related fixes from upstream.

This pulls back glance related changes up to upstream revision 455.

428-do-not-require-that-hypervisor_config-be-present.patch (LP: #1578622)
433-glance-ignore-inactive-images.patch (LP: #1583276)
436-glance-fix-race-conditions.patch (LP: #1584938)
450-453-454-keystone-v3-support.patch (LP: #1686437, #1728982, #1719879)
455-nova-lxd-support-squashfs-images.patch (LP: #1686086)

b21bc99... by Scott Moser

Import patches-unapplied version 0.1.0~bzr426-0ubuntu1.3 to ubuntu/xenial-proposed

Imported using git-ubuntu import.

Changelog parent: 42652aa520c7856e51d365c9e158124ace8d81a5

New changelog entries:
  * Openstack: Add keystone v3 auth support (LP: #1686437).

42652aa... by Andres Rodriguez

Import patches-unapplied version 0.1.0~bzr426-0ubuntu1.2 to ubuntu/xenial-proposed

Imported using git-ubuntu import.

Changelog parent: 353cf1dc2f62eb63936244e8b92c51c29b0d1e05

New changelog entries:
  * [SRU] Set custom user agent (LP: #1578624)

353cf1d... by Robie Basak

Import patches-unapplied version 0.1.0~bzr426-0ubuntu1.1 to ubuntu/xenial-proposed

Imported using git-ubuntu import.

Changelog parent: c2750bbdf3e9500a2da0c5f5503cb85d9a59fa00

New changelog entries:
  * Fix signature verification speed (LP: #1580534).

c2750bb... by Scott Moser

Import patches-unapplied version 0.1.0~bzr426-0ubuntu1 to ubuntu/xenial-proposed

Imported using git-ubuntu import.

Changelog parent: 69feb70fd3bbe7f44bc5c20a21d844a83e2d407f

New changelog entries:
  * debian/README.source, debian/new-upstream-snapshot: add
    script to do what README.source said to do before.
  * New upstream snapshot.
    - glance mirror: use 'virt' key for hypervisor type and use names
      'kvm' and 'lxd' rather than 'qemu' and 'lxc' (LP: #1560903)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
diff --git a/debian/changelog b/debian/changelog
index 01c067e..8bffd8a 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,15 @@
1simplestreams (0.1.0~bzr426-0ubuntu1.4~ppa0) xenial; urgency=medium
2
3 * Pull back several upstream fixes to glance sync code:
4 - 428-do-not-require-that-hypervisor_config-be-present.patch (LP: #1578622)
5 - 433-glance-ignore-inactive-images.patch (LP: #1583276)
6 - 436-glance-fix-race-conditions.patch (LP: #1584938)
7 - 450-453-454-keystone-v3-support.patch (LP: #1686437, #1728982, #1719879)
8 - 455-nova-lxd-support-squashfs-images.patch (LP: #1686086)
9 - 460-glance-handle-v2-auth-with-sessions.patch (LP: #1611987)
10
11 -- Scott Moser <smoser@ubuntu.com> Thu, 12 Apr 2018 12:27:47 -0400
12
1simplestreams (0.1.0~bzr426-0ubuntu1.3) xenial-proposed; urgency=medium13simplestreams (0.1.0~bzr426-0ubuntu1.3) xenial-proposed; urgency=medium
214
3 * Openstack: Add keystone v3 auth support (LP: #1686437).15 * Openstack: Add keystone v3 auth support (LP: #1686437).
diff --git a/debian/patches/428-do-not-require-that-hypervisor_config-be-present.patch b/debian/patches/428-do-not-require-that-hypervisor_config-be-present.patch
4new file mode 10064416new file mode 100644
index 0000000..f449dad
--- /dev/null
+++ b/debian/patches/428-do-not-require-that-hypervisor_config-be-present.patch
@@ -0,0 +1,23 @@
1------------------------------------------------------------
2revno: 428
3fixes bug: https://launchpad.net/bugs/1578622
4committer: Scott Moser <smoser@ubuntu.com>
5branch nick: trunk
6timestamp: Thu 2016-05-05 08:19:26 -0400
7message:
8 glance mirror: do not require that hypervisor_config be present
9
10 This just allows for hypervisor_config to not be present.
11=== modified file 'simplestreams/mirrors/glance.py'
12--- a/simplestreams/mirrors/glance.py 2016-03-23 10:00:53 +0000
13+++ b/simplestreams/mirrors/glance.py 2016-05-05 12:19:26 +0000
14@@ -239,7 +239,7 @@
15 t_item['arch'] = arch
16 props['architecture'] = canonicalize_arch(arch)
17
18- if self.config['hypervisor_mapping'] and 'ftype' in flat:
19+ if self.config.get('hypervisor_mapping', False) and 'ftype' in flat:
20 _hypervisor_type = hypervisor_type(flat['ftype'])
21 if _hypervisor_type:
22 props['hypervisor_type'] = _hypervisor_type
23
diff --git a/debian/patches/433-glance-ignore-inactive-images.patch b/debian/patches/433-glance-ignore-inactive-images.patch
0new file mode 10064424new file mode 100644
index 0000000..ee704e0
--- /dev/null
+++ b/debian/patches/433-glance-ignore-inactive-images.patch
@@ -0,0 +1,42 @@
1------------------------------------------------------------
2revno: 433 [merge]
3fixes bug: https://launchpad.net/bugs/1583276
4committer: Scott Moser <smoser@ubuntu.com>
5branch nick: trunk
6timestamp: Fri 2016-05-20 15:33:14 -0400
7message:
8 glance: ignore inactive images
9
10 If connection to glance is broken during GlanceMirror.sync(), and image
11 would be left over in "status": "saving".
12
13 Glance itself tries to protect against that, however when it is restarted
14 (eg. "service glance-api restart"), it does not clean the image up.
15
16 This change makes load_product() ignore any images that do not have
17 "status": "active". Images can only have deleted, active or saving
18 status.
19
20 Note that this isn't perfect. The image in "saving" state is never cleaned
21 up, but at least the resulting cloud works. If we want to clean up broken
22 images, it'd be hard to do on the simplestreams side (glance is better
23 placed to do that: it knows if anything is going on with any of them, eg.
24 is it being currently updated or not).
25------------------------------------------------------------
26Use --include-merged or -n0 to see merged revisions.
27=== modified file 'simplestreams/mirrors/glance.py'
28--- a/simplestreams/mirrors/glance.py 2016-05-05 12:35:42 +0000
29+++ b/simplestreams/mirrors/glance.py 2016-05-20 15:33:51 +0000
30@@ -179,6 +179,11 @@
31 if props.get('content_id') != my_cid:
32 continue
33
34+ if image.get('status') != "active":
35+ LOG.warn("Ignoring inactive image %s with status '%s'" % (
36+ image['id'], image.get('status')))
37+ continue
38+
39 source_content_id = props.get('source_content_id')
40
41 product = props.get('product_name')
42
diff --git a/debian/patches/435-glance-refactor-for-testing.patch b/debian/patches/435-glance-refactor-for-testing.patch
0new file mode 10064443new file mode 100644
index 0000000..404b531
--- /dev/null
+++ b/debian/patches/435-glance-refactor-for-testing.patch
@@ -0,0 +1,853 @@
1------------------------------------------------------------
2revno: 435 [merge]
3committer: Scott Moser <smoser@ubuntu.com>
4branch nick: trunk
5timestamp: Tue 2016-06-14 16:10:56 -0400
6message:
7 GlanceMirror: refactor insert_item for easier testing
8
9 This change refactors GlanceMirror.insert_item() to allow for easier and
10 more contained testing. I needed to do this to understand everything that
11 was going on inside insert_item and other bits of code. There are now four
12 distinct things happening in it:
13
14 1. Download image to a local file from a ContentSource
15 2. Construct extra properties to store in Glance along with image
16 3. Prepare arguments for GlanceClient.images.create() call
17 4. Adapt source simplestreams entry for an image for use in the target
18 simplestreams index
19
20 It should be fully backwards compatible, and test coverage for all the
21 individual steps should be much better (I admit to it not being perfect,
22 but it's a step in the right direction, imho at least).
23------------------------------------------------------------
24Use --include-merged or -n0 to see merged revisions.
25=== modified file 'simplestreams/mirrors/glance.py'
26--- a/simplestreams/mirrors/glance.py 2016-05-20 15:33:51 +0000
27+++ b/simplestreams/mirrors/glance.py 2016-06-10 18:47:52 +0000
28@@ -105,8 +105,15 @@
29 # glance mirror 'image-downloads' content into glance
30 # if provided an object store, it will produce a 'image-ids' mirror
31 class GlanceMirror(mirrors.BasicMirrorWriter):
32+ """
33+ GlanceMirror syncs external simplestreams index and images to Glance.
34+
35+ `client` argument is used for testing to override openstack module:
36+ allows dependency injection of fake "openstack" module.
37+ """
38 def __init__(self, config, objectstore=None, region=None,
39- name_prefix=None, progress_callback=None):
40+ name_prefix=None, progress_callback=None,
41+ client=None):
42 super(GlanceMirror, self).__init__(config=config)
43
44 self.item_filters = self.config.get('item_filters', [])
45@@ -123,7 +130,10 @@
46 self.loaded_content = {}
47 self.store = objectstore
48
49- self.keystone_creds = openstack.load_keystone_creds()
50+ if client is None:
51+ client = openstack
52+
53+ self.keystone_creds = client.load_keystone_creds()
54
55 self.name_prefix = name_prefix or ""
56 if region is not None:
57@@ -131,8 +141,8 @@
58
59 self.progress_callback = progress_callback
60
61- conn_info = openstack.get_service_conn_info('image',
62- **self.keystone_creds)
63+ conn_info = client.get_service_conn_info(
64+ 'image', **self.keystone_creds)
65 self.gclient = get_glanceclient(**conn_info)
66 self.tenant_id = conn_info['tenant_id']
67
68@@ -151,6 +161,12 @@
69 return "streams/v1/%s.json" % content_id
70
71 def load_products(self, path=None, content_id=None):
72+ """
73+ Load metadata for all currently uploaded active images in Glance.
74+
75+ Uses glance as the definitive store, but loads metadata from existing
76+ simplestreams indexes as well.
77+ """
78 my_cid = self.content_id
79
80 # glance is the definitive store. Any data loaded from the store
81@@ -219,105 +235,212 @@
82 def filter_item(self, data, src, target, pedigree):
83 return filters.filter_item(self.item_filters, data, src, pedigree)
84
85- def insert_item(self, data, src, target, pedigree, contentsource):
86- flat = util.products_exdata(src, pedigree, include_top=False)
87-
88- tmp_path = None
89- tmp_del = None
90-
91- name = flat.get('pubname', flat.get('name'))
92- if not name.endswith(flat['item_name']):
93- name += "-%s" % (flat['item_name'])
94-
95- t_item = flat.copy()
96- if 'path' in t_item:
97- del t_item['path']
98-
99- props = {'content_id': target['content_id'],
100- 'source_content_id': src['content_id']}
101- for n in ('product_name', 'version_name', 'item_name'):
102- props[n] = flat[n]
103- del t_item[n]
104-
105- arch = flat.get('arch')
106- if arch:
107- t_item['arch'] = arch
108- props['architecture'] = canonicalize_arch(arch)
109-
110- if self.config.get('hypervisor_mapping', False) and 'ftype' in flat:
111- _hypervisor_type = hypervisor_type(flat['ftype'])
112+ def create_glance_properties(self, content_id, source_content_id,
113+ image_metadata, hypervisor_mapping):
114+ """
115+ Construct extra properties to store in Glance for an image.
116+
117+ Based on source image metadata.
118+ """
119+ properties = {
120+ 'content_id': content_id,
121+ 'source_content_id': source_content_id,
122+ }
123+ # An iterator of properties to carry over: if a property needs
124+ # renaming, uses a tuple (old name, new name).
125+ carry_over = (
126+ 'product_name', 'version_name', 'item_name',
127+ ('os', 'os_distro'), ('version', 'os_version'),
128+ )
129+ for carry_over_property in carry_over:
130+ if isinstance(carry_over_property, tuple):
131+ name_old, name_new = carry_over_property
132+ else:
133+ name_old = name_new = carry_over_property
134+ properties[name_new] = image_metadata[name_old]
135+
136+ if 'arch' in image_metadata:
137+ properties['architecture'] = canonicalize_arch(
138+ image_metadata['arch'])
139+
140+ if hypervisor_mapping and 'ftype' in image_metadata:
141+ _hypervisor_type = hypervisor_type(image_metadata['ftype'])
142 if _hypervisor_type:
143- props['hypervisor_type'] = _hypervisor_type
144- _virt_type = virt_type(_hypervisor_type)
145- if _virt_type:
146- t_item['virt'] = _virt_type
147-
148- if 'os' in flat:
149- props['os_distro'] = flat['os']
150-
151- if 'version' in flat:
152- props['os_version'] = flat['version']
153-
154- fullname = self.name_prefix + name
155+ properties['hypervisor_type'] = _hypervisor_type
156+ return properties
157+
158+ def prepare_glance_arguments(self, full_image_name, image_metadata,
159+ image_md5_hash, image_size, image_properties):
160+ """
161+ Prepare arguments to pass into Glance image creation method.
162+
163+ Uses `image_metadata` for source image to derive image size, md5 hash,
164+ disk format (based on 'ftype' field, if defined, otherwise defaults to
165+ 'qcow2').
166+
167+ If `image_md5_hash` and `image_size` are defined, overrides the
168+ values from image_metadata with their values.
169+
170+ Sets extra image properties to dict `image_properties`.
171+
172+ Returns a dict to use as keyword arguments passed directly to
173+ GlanceClient.images.create().
174+ """
175 create_kwargs = {
176- 'name': fullname,
177- 'properties': props,
178+ 'name': full_image_name,
179 'container_format': 'bare',
180 'is_public': True,
181+ 'properties': image_properties,
182 }
183- if 'size' in data:
184- create_kwargs['size'] = data.get('size')
185-
186- if 'md5' in data:
187- create_kwargs['checksum'] = data.get('md5')
188-
189- if 'ftype' in flat:
190+
191+ if 'size' in image_metadata:
192+ create_kwargs['size'] = image_metadata.get('size')
193+ if 'md5' in image_metadata:
194+ create_kwargs['checksum'] = image_metadata.get('md5')
195+ if image_md5_hash and image_size:
196+ create_kwargs.update({
197+ 'checksum': image_md5_hash,
198+ 'size': image_size,
199+ })
200+
201+ if 'ftype' in image_metadata:
202 create_kwargs['disk_format'] = (
203- disk_format(flat['ftype']) or 'qcow2'
204+ disk_format(image_metadata['ftype']) or 'qcow2'
205 )
206 else:
207 create_kwargs['disk_format'] = 'qcow2'
208
209+ return create_kwargs
210+
211+ def download_image(self, contentsource, image_stream_data):
212+ """
213+ Download an image from contentsource.
214+
215+ `image_stream_data` represents a flattened image metadata structure
216+ to use for any logging messages.
217+
218+ Returns a tuple of (local-image-path, image-size, image-md5-hash).
219+
220+ If download fails, these values will all be None.
221+ """
222+ tmp_path = new_size = new_md5 = None
223+
224+ image_name = image_stream_data.get('pubname')
225+ image_size = image_stream_data.get('size')
226+
227 if self.progress_callback:
228 def progress_wrapper(written):
229 self.progress_callback(dict(status="Downloading",
230- name=flat.get('pubname'),
231- size=data.get('size', 0),
232+ name=image_name,
233+ size=image_size,
234 written=written))
235 else:
236 def progress_wrapper(written):
237 pass
238
239 try:
240- try:
241- (tmp_path, tmp_del) = util.get_local_copy(
242- contentsource, progress_callback=progress_wrapper)
243-
244- if self.modify_hook:
245- (newsize, newmd5) = call_hook(item=t_item, path=tmp_path,
246- cmd=self.modify_hook)
247- create_kwargs['checksum'] = newmd5
248- create_kwargs['size'] = newsize
249- t_item['md5'] = newmd5
250- t_item['size'] = newsize
251-
252- finally:
253- contentsource.close()
254-
255+ tmp_path, _ = util.get_local_copy(
256+ contentsource, progress_callback=progress_wrapper)
257+
258+ if self.modify_hook:
259+ (new_size, new_md5) = call_hook(
260+ item=image_stream_data, path=tmp_path,
261+ cmd=self.modify_hook)
262+ finally:
263+ contentsource.close()
264+
265+ return tmp_path, new_size, new_md5
266+
267+ def adapt_source_entry(self, source_entry, hypervisor_mapping, image_name,
268+ image_md5_hash, image_size):
269+ """
270+ Adapts the source simplestreams dict `source_entry` for use in the
271+ generated local simplestreams index.
272+ """
273+ output_entry = source_entry.copy()
274+
275+ # Drop attributes not needed for the simplestreams index itself.
276+ for property_name in ('path', 'product_name', 'version_name',
277+ 'item_name'):
278+ if property_name in output_entry:
279+ del output_entry[property_name]
280+
281+ if hypervisor_mapping and 'ftype' in output_entry:
282+ _hypervisor_type = hypervisor_type(output_entry['ftype'])
283+ if _hypervisor_type:
284+ _virt_type = virt_type(_hypervisor_type)
285+ if _virt_type:
286+ output_entry['virt'] = _virt_type
287+
288+ output_entry['region'] = self.region
289+ output_entry['endpoint'] = self.auth_url
290+ output_entry['owner_id'] = self.tenant_id
291+
292+ output_entry['name'] = image_name
293+ if image_md5_hash and image_size:
294+ output_entry['md5'] = image_md5_hash
295+ output_entry['size'] = image_size
296+
297+ return output_entry
298+
299+ def insert_item(self, data, src, target, pedigree, contentsource):
300+ """
301+ Upload image into glance and add image metadata to simplestreams index.
302+
303+ `data` is the metadata for a particular image file from the source:
304+ unused since all that data is present in the `src` entry for
305+ the corresponding image as well.
306+ `src` contains the entire simplestreams index from the image syncing
307+ source.
308+ `target` is the simplestreams index for currently available images
309+ in glance (generated by load_products()) to add this item to.
310+ `pedigree` is a "path" to get to the `data` for the image we desire,
311+ a tuple of (product_name, version_name, image_type).
312+ `contentsource` is a ContentSource to download the actual image data
313+ from.
314+ """
315+ # Extract and flatten metadata for a product image matching
316+ # (product-name, version-name, image-type)
317+ # from the tuple `pedigree` in the source simplestreams index.
318+ flattened_img_data = util.products_exdata(
319+ src, pedigree, include_top=False)
320+
321+ tmp_path = None
322+
323+ full_image_name = "{}{}".format(
324+ self.name_prefix,
325+ flattened_img_data.get('pubname', flattened_img_data.get('name')))
326+ if not full_image_name.endswith(flattened_img_data['item_name']):
327+ full_image_name += "-{}".format(flattened_img_data['item_name'])
328+
329+ # Download images locally into a temporary file.
330+ tmp_path, new_size, new_md5 = self.download_image(
331+ contentsource, flattened_img_data)
332+
333+ hypervisor_mapping = self.config.get('hypervisor_mapping', False)
334+
335+ glance_props = self.create_glance_properties(
336+ target['content_id'], src['content_id'], flattened_img_data,
337+ hypervisor_mapping)
338+ create_kwargs = self.prepare_glance_arguments(
339+ full_image_name, flattened_img_data, new_md5, new_size,
340+ glance_props)
341+
342+ target_sstream_item = self.adapt_source_entry(
343+ flattened_img_data, hypervisor_mapping, full_image_name, new_md5,
344+ new_size)
345+
346+ try:
347 create_kwargs['data'] = open(tmp_path, 'rb')
348- ret = self.gclient.images.create(**create_kwargs)
349- t_item['id'] = ret.id
350- print("created %s: %s" % (ret.id, fullname))
351+ glance_image = self.gclient.images.create(**create_kwargs)
352+ target_sstream_item['id'] = glance_image.id
353+ print("created %s: %s" % (glance_image.id, full_image_name))
354
355 finally:
356- if tmp_del and os.path.exists(tmp_path):
357+ if tmp_path and os.path.exists(tmp_path):
358 os.unlink(tmp_path)
359
360- t_item['region'] = self.region
361- t_item['endpoint'] = self.auth_url
362- t_item['owner_id'] = self.tenant_id
363- t_item['name'] = fullname
364- util.products_set(target, t_item, pedigree)
365+ util.products_set(target, target_sstream_item, pedigree)
366
367 def remove_item(self, data, src, target, pedigree):
368 util.products_del(target, pedigree)
369
370=== added file 'tests/unittests/test_glancemirror.py'
371--- a/tests/unittests/test_glancemirror.py 1970-01-01 00:00:00 +0000
372+++ b/tests/unittests/test_glancemirror.py 2016-06-10 18:47:52 +0000
373@@ -0,0 +1,479 @@
374+from simplestreams.contentsource import MemoryContentSource
375+from simplestreams.mirrors.glance import GlanceMirror
376+import simplestreams.util
377+
378+import os
379+from unittest import TestCase
380+
381+
382+class FakeOpenstack(object):
383+ """Fake 'openstack' module replacement for testing GlanceMirror."""
384+ def load_keystone_creds(self):
385+ return {"auth_url": "http://keystone/api/"}
386+
387+ def get_service_conn_info(self, url, region_name=None, auth_url=None):
388+ return {"endpoint": "http://objectstore/api/",
389+ "tenant_id": "bar456"}
390+
391+
392+class FakeImage(object):
393+ """Fake image objects returned by GlanceClient.images.create()."""
394+ def __init__(self, identifier):
395+ self.id = identifier
396+
397+
398+class FakeImages(object):
399+ """Fake GlanceClient.images implementation to track create() calls."""
400+ def __init__(self):
401+ self.create_calls = []
402+
403+ def create(self, **kwargs):
404+ self.create_calls.append(kwargs)
405+ return FakeImage('image-%d' % len(self.create_calls))
406+
407+
408+class FakeGlanceClient(object):
409+ """Fake GlanceClient implementation to track images.create() calls."""
410+ def __init__(self, *args):
411+ self.images = FakeImages()
412+
413+
414+class TestGlanceMirror(TestCase):
415+ """Tests for GlanceMirror methods."""
416+
417+ def setUp(self):
418+ self.config = {"content_id": "foo123"}
419+ self.mirror = GlanceMirror(
420+ self.config, name_prefix="auto-sync/", region="region1",
421+ client=FakeOpenstack())
422+
423+ def test_adapt_source_entry(self):
424+ # Adapts source entry for use in a local simplestreams index.
425+ source_entry = {"source-key": "source-value"}
426+ output_entry = self.mirror.adapt_source_entry(
427+ source_entry, hypervisor_mapping=False, image_name="foobuntu-X",
428+ image_md5_hash=None, image_size=None)
429+
430+ # Source and output entry are different objects.
431+ self.assertNotEqual(source_entry, output_entry)
432+
433+ # Output entry gets a few new properties like the endpoint and
434+ # owner_id taken from the GlanceMirror and OpenStack configuration,
435+ # region from the value passed into GlanceMirror constructor, and
436+ # image name from the passed in value.
437+ # It also contains the source entries as well.
438+ self.assertEqual(
439+ {"endpoint": "http://keystone/api/",
440+ "name": "foobuntu-X",
441+ "owner_id": "bar456",
442+ "region": "region1",
443+ "source-key": "source-value"},
444+ output_entry)
445+
446+ def test_adapt_source_entry_ignored_properties(self):
447+ # adapt_source_entry() drops some properties from the source entry.
448+ source_entry = {"path": "foo",
449+ "product_name": "bar",
450+ "version_name": "baz",
451+ "item_name": "bah"}
452+ output_entry = self.mirror.adapt_source_entry(
453+ source_entry, hypervisor_mapping=False, image_name="foobuntu-X",
454+ image_md5_hash=None, image_size=None)
455+
456+ # None of the values in 'source_entry' are preserved.
457+ for key in ("path", "product_name", "version_name", "item"):
458+ self.assertNotIn("path", output_entry)
459+
460+ def test_adapt_source_entry_image_md5_and_size(self):
461+ # adapt_source_entry() will use passed in values for md5 and size.
462+ # Even old stale values will be overridden when image_md5_hash and
463+ # image_size are passed in.
464+ source_entry = {"md5": "stale-md5"}
465+ output_entry = self.mirror.adapt_source_entry(
466+ source_entry, hypervisor_mapping=False, image_name="foobuntu-X",
467+ image_md5_hash="new-md5", image_size=5)
468+
469+ self.assertEqual("new-md5", output_entry["md5"])
470+ self.assertEqual(5, output_entry["size"])
471+
472+ def test_adapt_source_entry_image_md5_and_size_both_required(self):
473+ # adapt_source_entry() requires both md5 and size to not ignore them.
474+
475+ source_entry = {"md5": "stale-md5"}
476+
477+ # image_size is not passed in, so md5 value is not used either.
478+ output_entry1 = self.mirror.adapt_source_entry(
479+ source_entry, hypervisor_mapping=False, image_name="foobuntu-X",
480+ image_md5_hash="new-md5", image_size=None)
481+ self.assertEqual("stale-md5", output_entry1["md5"])
482+ self.assertNotIn("size", output_entry1)
483+
484+ # image_md5_hash is not passed in, so image_size is not used either.
485+ output_entry2 = self.mirror.adapt_source_entry(
486+ source_entry, hypervisor_mapping=False, image_name="foobuntu-X",
487+ image_md5_hash=None, image_size=5)
488+ self.assertEqual("stale-md5", output_entry2["md5"])
489+ self.assertNotIn("size", output_entry2)
490+
491+ def test_adapt_source_entry_hypervisor_mapping(self):
492+ # If hypervisor_mapping is set to True, 'virt' value is derived from
493+ # the source entry 'ftype'.
494+ source_entry = {"ftype": "disk1.img"}
495+ output_entry = self.mirror.adapt_source_entry(
496+ source_entry, hypervisor_mapping=True, image_name="foobuntu-X",
497+ image_md5_hash=None, image_size=None)
498+
499+ self.assertEqual("kvm", output_entry["virt"])
500+
501+ def test_adapt_source_entry_hypervisor_mapping_ftype_required(self):
502+ # If hypervisor_mapping is set to True, but 'ftype' is missing in the
503+ # source entry, 'virt' value is not added to the returned entry.
504+ source_entry = {}
505+ output_entry = self.mirror.adapt_source_entry(
506+ source_entry, hypervisor_mapping=True, image_name="foobuntu-X",
507+ image_md5_hash=None, image_size=None)
508+
509+ self.assertNotIn("virt", output_entry)
510+
511+ def test_create_glance_properties(self):
512+ # Constructs glance properties to set on image during upload
513+ # based on source image metadata.
514+ source_entry = {
515+ # All of these are carried over and potentially re-named.
516+ "product_name": "foobuntu",
517+ "version_name": "X",
518+ "item_name": "disk1.img",
519+ "os": "ubuntu",
520+ "version": "16.04",
521+ # Other entries are ignored.
522+ "something-else": "ignored",
523+ }
524+ properties = self.mirror.create_glance_properties(
525+ "content-1", "source-1", source_entry, hypervisor_mapping=False)
526+
527+ # Output properties contain content-id and source-content-id based
528+ # on the passed in parameters, and carry over (with changed keys
529+ # for "os" and "version") product_name, version_name, item_name and
530+ # os and version values from the source entry.
531+ self.assertEqual(
532+ {"content_id": "content-1",
533+ "source_content_id": "source-1",
534+ "product_name": "foobuntu",
535+ "version_name": "X",
536+ "item_name": "disk1.img",
537+ "os_distro": "ubuntu",
538+ "os_version": "16.04"},
539+ properties)
540+
541+ def test_create_glance_properties_arch(self):
542+ # When 'arch' is present in the source entry, it is adapted and
543+ # returned inside 'architecture' field.
544+ source_entry = {
545+ "product_name": "foobuntu",
546+ "version_name": "X",
547+ "item_name": "disk1.img",
548+ "os": "ubuntu",
549+ "version": "16.04",
550+ "arch": "amd64",
551+ }
552+ properties = self.mirror.create_glance_properties(
553+ "content-1", "source-1", source_entry, hypervisor_mapping=False)
554+ self.assertEqual("x86_64", properties["architecture"])
555+
556+ def test_create_glance_properties_hypervisor_mapping(self):
557+ # When hypervisor_mapping is requested and 'ftype' is present in
558+ # the image metadata, 'hypervisor_type' is added to returned
559+ # properties.
560+ source_entry = {
561+ "product_name": "foobuntu",
562+ "version_name": "X",
563+ "item_name": "disk1.img",
564+ "os": "ubuntu",
565+ "version": "16.04",
566+ "ftype": "root.tar.gz",
567+ }
568+ properties = self.mirror.create_glance_properties(
569+ "content-1", "source-1", source_entry, hypervisor_mapping=True)
570+ self.assertEqual("lxc", properties["hypervisor_type"])
571+
572+ def test_prepare_glance_arguments(self):
573+ # Prepares arguments to pass to GlanceClient.images.create()
574+ # based on image metadata from the simplestreams source.
575+ source_entry = {}
576+ create_arguments = self.mirror.prepare_glance_arguments(
577+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=None,
578+ image_properties=None)
579+
580+ # Arguments to always pass in contain the image name, container format,
581+ # disk format, whether image is public, and any passed-in properties.
582+ self.assertEqual(
583+ {"name": "foobuntu-X",
584+ "container_format": 'bare',
585+ "disk_format": "qcow2",
586+ "is_public": True,
587+ "properties": None},
588+ create_arguments)
589+
590+ def test_prepare_glance_arguments_disk_format(self):
591+ # Disk format is based on the image 'ftype' (if defined).
592+ source_entry = {"ftype": "root.tar.gz"}
593+ create_arguments = self.mirror.prepare_glance_arguments(
594+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=None,
595+ image_properties=None)
596+
597+ self.assertEqual("root-tar", create_arguments["disk_format"])
598+
599+ def test_prepare_glance_arguments_size(self):
600+ # Size is read from image metadata if defined.
601+ source_entry = {"size": 5}
602+ create_arguments = self.mirror.prepare_glance_arguments(
603+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=None,
604+ image_properties=None)
605+
606+ self.assertEqual(5, create_arguments["size"])
607+
608+ def test_prepare_glance_arguments_checksum(self):
609+ # Checksum is based on the source entry 'md5' value, if defined.
610+ source_entry = {"md5": "foo123"}
611+ create_arguments = self.mirror.prepare_glance_arguments(
612+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=None,
613+ image_properties=None)
614+
615+ self.assertEqual("foo123", create_arguments["checksum"])
616+
617+ def test_prepare_glance_arguments_size_and_md5_override(self):
618+ # Size and md5 hash are overridden from the passed-in values even if
619+ # defined on the source entry.
620+ source_entry = {"size": 5, "md5": "foo123"}
621+ create_arguments = self.mirror.prepare_glance_arguments(
622+ "foobuntu-X", source_entry, image_md5_hash="bar456", image_size=10,
623+ image_properties=None)
624+
625+ self.assertEqual(10, create_arguments["size"])
626+ self.assertEqual("bar456", create_arguments["checksum"])
627+
628+ def test_prepare_glance_arguments_size_and_md5_no_override_hash(self):
629+ # If only one of image_md5_hash or image_size is passed directly in,
630+ # the other value is not overridden either.
631+ source_entry = {"size": 5, "md5": "foo123"}
632+ create_arguments = self.mirror.prepare_glance_arguments(
633+ "foobuntu-X", source_entry, image_md5_hash="bar456",
634+ image_size=None, image_properties=None)
635+
636+ self.assertEqual(5, create_arguments["size"])
637+ self.assertEqual("foo123", create_arguments["checksum"])
638+
639+ def test_prepare_glance_arguments_size_and_md5_no_override_size(self):
640+ # If only one of image_md5_hash or image_size is passed directly in,
641+ # the other value is not overridden either.
642+ source_entry = {"size": 5, "md5": "foo123"}
643+ create_arguments = self.mirror.prepare_glance_arguments(
644+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=10,
645+ image_properties=None)
646+
647+ self.assertEqual(5, create_arguments["size"])
648+ self.assertEqual("foo123", create_arguments["checksum"])
649+
650+ def test_download_image(self):
651+ # Downloads image from a contentsource.
652+ content = "foo bazes the bar"
653+ content_source = MemoryContentSource(
654+ url="http://image-store/fooubuntu-X-disk1.img", content=content)
655+ image_metadata = {"pubname": "foobuntu-X", "size": 5}
656+ path, size, md5_hash = self.mirror.download_image(
657+ content_source, image_metadata)
658+ self.addCleanup(os.unlink, path)
659+ self.assertIsNotNone(path)
660+ self.assertIsNone(size)
661+ self.assertIsNone(md5_hash)
662+
663+ def test_download_image_progress_callback(self):
664+ # Progress callback is called with image name, size, status and buffer
665+ # size after every 10kb of data: 3 times for 25kb of data below.
666+ content = "abcdefghij" * int(1024 * 2.5)
667+ content_source = MemoryContentSource(
668+ url="http://image-store/fooubuntu-X-disk1.img", content=content)
669+ image_metadata = {"pubname": "foobuntu-X", "size": len(content)}
670+
671+ self.progress_calls = []
672+
673+ def log_progress_calls(message):
674+ self.progress_calls.append(message)
675+
676+ self.addCleanup(
677+ setattr, self.mirror, "progress_callback",
678+ self.mirror.progress_callback)
679+ self.mirror.progress_callback = log_progress_calls
680+ path, size, md5_hash = self.mirror.download_image(
681+ content_source, image_metadata)
682+ self.addCleanup(os.unlink, path)
683+
684+ self.assertEqual(
685+ [{"name": "foobuntu-X", "size": 25600, "status": "Downloading",
686+ "written": 10240}] * 3,
687+ self.progress_calls)
688+
689+ def test_download_image_error(self):
690+ # When there's an error during download, contentsource is still closed
691+ # and the error is propagated below.
692+ content = "abcdefghij"
693+ content_source = MemoryContentSource(
694+ url="http://image-store/fooubuntu-X-disk1.img", content=content)
695+ image_metadata = {"pubname": "foobuntu-X", "size": len(content)}
696+
697+ # MemoryContentSource has an internal file descriptor which indicates
698+ # if close() method has been called on it.
699+ self.assertFalse(content_source.fd.closed)
700+
701+ self.addCleanup(
702+ setattr, self.mirror, "progress_callback",
703+ self.mirror.progress_callback)
704+ self.mirror.progress_callback = lambda message: 1/0
705+
706+ self.assertRaises(
707+ ZeroDivisionError,
708+ self.mirror.download_image, content_source, image_metadata)
709+
710+ # We rely on the MemoryContentSource.close() side-effect to ensure
711+ # close() method has indeed been called on the passed-in ContentSource.
712+ self.assertTrue(content_source.fd.closed)
713+
714+ def test_insert_item(self):
715+ # Downloads an image from a contentsource, uploads it into Glance,
716+ # adapting and munging as needed (it updates the keystone endpoint,
717+ # image and owner ids).
718+ # This test is basically an integration test to make sure all the
719+ # methods used by insert_item() are tied together in one good
720+ # fully functioning whole.
721+
722+ # This is a real snippet from the simplestreams index entry for
723+ # Ubuntu 14.04 amd64 image from cloud-images.ubuntu.com as of
724+ # 2016-06-05.
725+ source_index = {
726+ u'content_id': u'com.ubuntu.cloud:released:download',
727+ u'datatype': u'image-downloads',
728+ u'format': u'products:1.0',
729+ u'license': (u'http://www.canonical.com/'
730+ u'intellectual-property-policy'),
731+ u'products': {u'com.ubuntu.cloud:server:14.04:amd64': {
732+ u'aliases': u'14.04,default,lts,t,trusty',
733+ u'arch': u'amd64',
734+ u'os': u'ubuntu',
735+ u'release': u'trusty',
736+ u'release_codename': u'Trusty Tahr',
737+ u'release_title': u'14.04 LTS',
738+ u'support_eol': u'2019-04-17',
739+ u'supported': True,
740+ u'version': u'14.04',
741+ u'versions': {u'20160602': {
742+ u'items': {u'disk1.img': {
743+ u'ftype': u'disk1.img',
744+ u'md5': u'e5436cd36ae6cc298f081bf0f6b413f1',
745+ u'path': (
746+ u'server/releases/trusty/release-20160602/'
747+ u'ubuntu-14.04-server-cloudimg-amd64-disk1.img'),
748+ u'sha256': (u'5b982d7d4dd1a03e88ae5f35f02ed44f'
749+ u'579e2711f3e0f27ea2bff20aef8c8d9e'),
750+ u'size': 259850752}},
751+ u'label': u'release',
752+ u'pubname': u'ubuntu-trusty-14.04-amd64-server-20160602',
753+ }}}
754+ }
755+ }
756+
757+ # "Pedigree" is basically a "path" to get to the image data in
758+ # simplestreams index, going through "products", their "versions",
759+ # and nested "items".
760+ pedigree = (
761+ u'com.ubuntu.cloud:server:14.04:amd64', u'20160602', u'disk1.img')
762+ product = source_index[u'products'][pedigree[0]]
763+ image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
764+
765+ content_source = MemoryContentSource(
766+ url="http://image-store/fooubuntu-X-disk1.img",
767+ content="image-data")
768+
769+ # Use a fake GlanceClient to track arguments passed into
770+ # GlanceClient.images.create().
771+ self.addCleanup(setattr, self.mirror, "gclient", self.mirror.gclient)
772+ self.mirror.gclient = FakeGlanceClient()
773+
774+ target = {
775+ 'content_id': 'auto.sync',
776+ 'datatype': 'image-ids',
777+ 'format': 'products:1.0',
778+ }
779+
780+ self.mirror.insert_item(
781+ image_data, source_index, target, pedigree, content_source)
782+
783+ passed_create_kwargs = self.mirror.gclient.images.create_calls[0]
784+
785+ # There is a 'data' argument pointing to an open file descriptor
786+ # for the locally downloaded image.
787+ self.assertIn("data", passed_create_kwargs)
788+ passed_create_kwargs.pop("data")
789+
790+ expected_create_kwargs = {
791+ 'name': ('auto-sync/'
792+ 'ubuntu-trusty-14.04-amd64-server-20160602-disk1.img'),
793+ 'checksum': u'e5436cd36ae6cc298f081bf0f6b413f1',
794+ 'disk_format': 'qcow2',
795+ 'container_format': 'bare',
796+ 'is_public': True,
797+ 'properties': {
798+ 'os_distro': u'ubuntu',
799+ 'item_name': u'disk1.img',
800+ 'os_version': u'14.04',
801+ 'architecture': 'x86_64',
802+ 'version_name': u'20160602',
803+ 'content_id': 'auto.sync',
804+ 'product_name': u'com.ubuntu.cloud:server:14.04:amd64',
805+ 'source_content_id': u'com.ubuntu.cloud:released:download'},
806+ 'size': '259850752'}
807+ self.assertEqual(
808+ expected_create_kwargs, passed_create_kwargs)
809+
810+ # Almost real resulting data as produced by simplestreams before
811+ # insert_item refactoring to allow for finer-grained testing.
812+ expected_target_index = {
813+ 'content_id': 'auto.sync',
814+ 'datatype': 'image-ids',
815+ 'format': 'products:1.0',
816+ 'products': {
817+ "com.ubuntu.cloud:server:14.04:amd64": {
818+ "aliases": "14.04,default,lts,t,trusty",
819+ "arch": "amd64",
820+ "label": "release",
821+ "os": "ubuntu",
822+ "owner_id": "bar456",
823+ "pubname": "ubuntu-trusty-14.04-amd64-server-20160602",
824+ "release": "trusty",
825+ "release_codename": "Trusty Tahr",
826+ "release_title": "14.04 LTS",
827+ "support_eol": "2019-04-17",
828+ "supported": "True",
829+ "version": "14.04",
830+ "versions": {"20160602": {"items": {"disk1.img": {
831+ "endpoint": "http://keystone/api/",
832+ "ftype": "disk1.img",
833+ "id": "image-1",
834+ "md5": "e5436cd36ae6cc298f081bf0f6b413f1",
835+ "name": ("auto-sync/ubuntu-trusty-14.04-amd64-"
836+ "server-20160602-disk1.img"),
837+ "region": "region1",
838+ "sha256": ("5b982d7d4dd1a03e88ae5f35f02ed44f"
839+ "579e2711f3e0f27ea2bff20aef8c8d9e"),
840+ "size": "259850752"
841+ }}}}
842+ }
843+ }
844+ }
845+
846+ # Apply the condensing as done in GlanceMirror.insert_products()
847+ # to ensure we compare with the desired resulting simplestreams data.
848+ sticky = ['ftype', 'md5', 'sha256', 'size', 'name', 'id', 'endpoint',
849+ 'region']
850+ simplestreams.util.products_condense(target, sticky)
851+
852+ self.assertEqual(expected_target_index, target)
853
diff --git a/debian/patches/436-glance-fix-race-conditions.patch b/debian/patches/436-glance-fix-race-conditions.patch
0new file mode 100644854new file mode 100644
index 0000000..ae55dbe
--- /dev/null
+++ b/debian/patches/436-glance-fix-race-conditions.patch
@@ -0,0 +1,479 @@
1------------------------------------------------------------
2revno: 436 [merge]
3fixes bug: https://launchpad.net/bugs/1584938
4committer: Scott Moser <smoser@ubuntu.com>
5branch nick: trunk
6timestamp: Tue 2016-06-14 16:21:05 -0400
7message:
8 GlanceMirror: fix a couple of race-related problems
9
10 First, what can happen is that .sync() is interrupted for external reasons
11 (service restarts, network issues,...) after first image has been uploaded
12 to Glance when syncing multiple images. On re-run, the uploaded image is
13 considered "synced" already, but since simplestreams index is written out
14 only at the end of the sync, all the metadata is lost. We solve this by
15 adding "simplestreams_metadata" as one of the properties of image in
16 Glance where we put a JSON representation of all the metadata fields from
17 the original simplestreams index entry. We then load them in
18 load_products() if they are defined. This results in a slightly different
19 auto.sync.json entry: "endpoint" and "region" are on the product entry,
20 instead of on the disk1.img entry.
21
22 Secondly, if sync is indeed interrupted after one or several images are
23 completely uploaded (but not all of them), there's no index file at all,
24 so any attempt to make use of them with juju would fail, even though it
25 should be fully functional. We solve this by calling "insert_products()"
26 at the end of the insert_item(). This means that the index is regenerated
27 after every single image is uploaded.
28
29 Along the way, I slightly improve insert_item() tests from the pre-req
30 branch to add a test for minimal data required, rename the existing one
31 using real world data to test_insert_item_full, and re-use that data for a
32 test for a newly added call to insert_products().
33------------------------------------------------------------
34Use --include-merged or -n0 to see merged revisions.
35=== modified file 'simplestreams/mirrors/glance.py'
36--- a/simplestreams/mirrors/glance.py 2016-06-10 18:47:52 +0000
37+++ b/simplestreams/mirrors/glance.py 2016-06-14 20:21:05 +0000
38@@ -25,6 +25,7 @@
39 import copy
40 import errno
41 import glanceclient
42+import json
43 import os
44 import re
45
46@@ -219,6 +220,15 @@
47 except KeyError:
48 item_data = {}
49
50+ # If original simplestreams-metadata is stored on the image,
51+ # use that as well.
52+ if 'simplestreams_metadata' in props:
53+ simplestreams_metadata = json.loads(
54+ props.get('simplestreams_metadata'))
55+ else:
56+ simplestreams_metadata = {}
57+ item_data.update(simplestreams_metadata)
58+
59 item_data.update({'name': image['name'], 'id': image['id']})
60 if 'owner_id' not in item_data:
61 item_data['owner_id'] = self.tenant_id
62@@ -248,10 +258,10 @@
63 }
64 # An iterator of properties to carry over: if a property needs
65 # renaming, uses a tuple (old name, new name).
66- carry_over = (
67- 'product_name', 'version_name', 'item_name',
68- ('os', 'os_distro'), ('version', 'os_version'),
69- )
70+ carry_over_simple = (
71+ 'product_name', 'version_name', 'item_name')
72+ carry_over = carry_over_simple + (
73+ ('os', 'os_distro'), ('version', 'os_version'))
74 for carry_over_property in carry_over:
75 if isinstance(carry_over_property, tuple):
76 name_old, name_new = carry_over_property
77@@ -267,6 +277,16 @@
78 _hypervisor_type = hypervisor_type(image_metadata['ftype'])
79 if _hypervisor_type:
80 properties['hypervisor_type'] = _hypervisor_type
81+
82+ # Store flattened metadata for a source image along with the
83+ # image in 'simplestreams_metadata' property.
84+ simplestreams_metadata = image_metadata.copy()
85+ drop_keys = carry_over_simple + ('path',)
86+ for remove_key in drop_keys:
87+ if remove_key in simplestreams_metadata:
88+ del simplestreams_metadata[remove_key]
89+ properties['simplestreams_metadata'] = json.dumps(
90+ simplestreams_metadata, sort_keys=True)
91 return properties
92
93 def prepare_glance_arguments(self, full_image_name, image_metadata,
94@@ -441,6 +461,9 @@
95 os.unlink(tmp_path)
96
97 util.products_set(target, target_sstream_item, pedigree)
98+ # We can safely ignore path and content arguments since they are
99+ # unused in insert_products below.
100+ self.insert_products(None, target, None)
101
102 def remove_item(self, data, src, target, pedigree):
103 util.products_del(target, pedigree)
104
105=== modified file 'tests/unittests/test_glancemirror.py'
106--- a/tests/unittests/test_glancemirror.py 2016-06-10 18:47:52 +0000
107+++ b/tests/unittests/test_glancemirror.py 2016-06-14 20:21:05 +0000
108@@ -1,11 +1,91 @@
109 from simplestreams.contentsource import MemoryContentSource
110 from simplestreams.mirrors.glance import GlanceMirror
111+from simplestreams.objectstores import MemoryObjectStore
112 import simplestreams.util
113
114+import copy
115+import json
116 import os
117 from unittest import TestCase
118
119
120+# This is a real snippet from the simplestreams index entry for
121+# Ubuntu 14.04 amd64 image from cloud-images.ubuntu.com as of
122+# 2016-06-05.
123+TEST_SOURCE_INDEX_ENTRY = {
124+ u'content_id': u'com.ubuntu.cloud:released:download',
125+ u'datatype': u'image-downloads',
126+ u'format': u'products:1.0',
127+ u'license': (u'http://www.canonical.com/'
128+ u'intellectual-property-policy'),
129+ u'products': {u'com.ubuntu.cloud:server:14.04:amd64': {
130+ u'aliases': u'14.04,default,lts,t,trusty',
131+ u'arch': u'amd64',
132+ u'os': u'ubuntu',
133+ u'release': u'trusty',
134+ u'release_codename': u'Trusty Tahr',
135+ u'release_title': u'14.04 LTS',
136+ u'support_eol': u'2019-04-17',
137+ u'supported': True,
138+ u'version': u'14.04',
139+ u'versions': {u'20160602': {
140+ u'items': {u'disk1.img': {
141+ u'ftype': u'disk1.img',
142+ u'md5': u'e5436cd36ae6cc298f081bf0f6b413f1',
143+ u'path': (
144+ u'server/releases/trusty/release-20160602/'
145+ u'ubuntu-14.04-server-cloudimg-amd64-disk1.img'),
146+ u'sha256': (u'5b982d7d4dd1a03e88ae5f35f02ed44f'
147+ u'579e2711f3e0f27ea2bff20aef8c8d9e'),
148+ u'size': 259850752}},
149+ u'label': u'release',
150+ u'pubname': u'ubuntu-trusty-14.04-amd64-server-20160602',
151+ }}}
152+ }
153+}
154+
155+# "Pedigree" is basically a "path" to get to the image data in simplestreams
156+# index, going through "products", their "versions", and nested "items".
157+TEST_IMAGE_PEDIGREE = (
158+ u'com.ubuntu.cloud:server:14.04:amd64', u'20160602', u'disk1.img')
159+
160+# Almost real resulting data as produced by simplestreams before
161+# insert_item refactoring to allow for finer-grained testing.
162+EXPECTED_OUTPUT_INDEX = {
163+ u'content_id': u'auto.sync',
164+ u'datatype': u'image-ids',
165+ u'format': u'products:1.0',
166+ u'products': {
167+ u"com.ubuntu.cloud:server:14.04:amd64": {
168+ u"aliases": u"14.04,default,lts,t,trusty",
169+ u"arch": u"amd64",
170+ u"label": u"release",
171+ u"os": u"ubuntu",
172+ u"owner_id": u"bar456",
173+ u"pubname": u"ubuntu-trusty-14.04-amd64-server-20160602",
174+ u"release": u"trusty",
175+ u"release_codename": u"Trusty Tahr",
176+ u"release_title": u"14.04 LTS",
177+ u"support_eol": u"2019-04-17",
178+ u"supported": u"True",
179+ u"version": u"14.04",
180+ u"versions": {u"20160602": {u"items": {u"disk1.img": {
181+ u"endpoint": u"http://keystone/api/",
182+ u"ftype": u"disk1.img",
183+ u"id": u"image-1",
184+ u"md5": u"e5436cd36ae6cc298f081bf0f6b413f1",
185+ u"name": (u"auto-sync/ubuntu-trusty-14.04-amd64-"
186+ u"server-20160602-disk1.img"),
187+ u"region": u"region1",
188+ u"sha256": (u"5b982d7d4dd1a03e88ae5f35f02ed44f"
189+ u"579e2711f3e0f27ea2bff20aef8c8d9e"),
190+ u"size": u"259850752"
191+ }}}}
192+ }
193+ }
194+}
195+
196+
197 class FakeOpenstack(object):
198 """Fake 'openstack' module replacement for testing GlanceMirror."""
199 def load_keystone_creds(self):
200@@ -145,8 +225,8 @@
201 "item_name": "disk1.img",
202 "os": "ubuntu",
203 "version": "16.04",
204- # Other entries are ignored.
205- "something-else": "ignored",
206+ # Unknown entries are stored in 'simplestreams_metadata'.
207+ "extra": "value",
208 }
209 properties = self.mirror.create_glance_properties(
210 "content-1", "source-1", source_entry, hypervisor_mapping=False)
211@@ -155,6 +235,8 @@
212 # on the passed in parameters, and carry over (with changed keys
213 # for "os" and "version") product_name, version_name, item_name and
214 # os and version values from the source entry.
215+ # All the fields except product_name, version_name and item_name are
216+ # also stored inside 'simplestreams_metadata' property as JSON data.
217 self.assertEqual(
218 {"content_id": "content-1",
219 "source_content_id": "source-1",
220@@ -162,7 +244,9 @@
221 "version_name": "X",
222 "item_name": "disk1.img",
223 "os_distro": "ubuntu",
224- "os_version": "16.04"},
225+ "os_version": "16.04",
226+ "simplestreams_metadata": (
227+ '{"extra": "value", "os": "ubuntu", "version": "16.04"}')},
228 properties)
229
230 def test_create_glance_properties_arch(self):
231@@ -196,6 +280,26 @@
232 "content-1", "source-1", source_entry, hypervisor_mapping=True)
233 self.assertEqual("lxc", properties["hypervisor_type"])
234
235+ def test_create_glance_properties_simplestreams_no_path(self):
236+ # Other than 'product_name', 'version_name' and 'item_name', if 'path'
237+ # is defined on the source entry, it is also not saved inside the
238+ # 'simplestreams_metadata' property.
239+ source_entry = {
240+ "product_name": "foobuntu",
241+ "version_name": "X",
242+ "item_name": "disk1.img",
243+ "os": "ubuntu",
244+ "version": "16.04",
245+ "path": "/path/to/foo",
246+ }
247+ properties = self.mirror.create_glance_properties(
248+ "content-1", "source-1", source_entry, hypervisor_mapping=False)
249+
250+ # Path is omitted from the simplestreams_metadata property JSON.
251+ self.assertEqual(
252+ '{"os": "ubuntu", "version": "16.04"}',
253+ properties["simplestreams_metadata"])
254+
255 def test_prepare_glance_arguments(self):
256 # Prepares arguments to pass to GlanceClient.images.create()
257 # based on image metadata from the simplestreams source.
258@@ -342,45 +446,86 @@
259 # Downloads an image from a contentsource, uploads it into Glance,
260 # adapting and munging as needed (it updates the keystone endpoint,
261 # image and owner ids).
262- # This test is basically an integration test to make sure all the
263- # methods used by insert_item() are tied together in one good
264- # fully functioning whole.
265
266- # This is a real snippet from the simplestreams index entry for
267- # Ubuntu 14.04 amd64 image from cloud-images.ubuntu.com as of
268- # 2016-06-05.
269+ # We use a minimal source simplestreams index, fake ContentSource and
270+ # GlanceClient, and only test for side-effects of each of the
271+ # subparts of the insert_item method.
272 source_index = {
273 u'content_id': u'com.ubuntu.cloud:released:download',
274- u'datatype': u'image-downloads',
275- u'format': u'products:1.0',
276- u'license': (u'http://www.canonical.com/'
277- u'intellectual-property-policy'),
278 u'products': {u'com.ubuntu.cloud:server:14.04:amd64': {
279- u'aliases': u'14.04,default,lts,t,trusty',
280 u'arch': u'amd64',
281 u'os': u'ubuntu',
282 u'release': u'trusty',
283- u'release_codename': u'Trusty Tahr',
284- u'release_title': u'14.04 LTS',
285- u'support_eol': u'2019-04-17',
286- u'supported': True,
287 u'version': u'14.04',
288 u'versions': {u'20160602': {
289 u'items': {u'disk1.img': {
290 u'ftype': u'disk1.img',
291 u'md5': u'e5436cd36ae6cc298f081bf0f6b413f1',
292- u'path': (
293- u'server/releases/trusty/release-20160602/'
294- u'ubuntu-14.04-server-cloudimg-amd64-disk1.img'),
295- u'sha256': (u'5b982d7d4dd1a03e88ae5f35f02ed44f'
296- u'579e2711f3e0f27ea2bff20aef8c8d9e'),
297 u'size': 259850752}},
298- u'label': u'release',
299 u'pubname': u'ubuntu-trusty-14.04-amd64-server-20160602',
300 }}}
301 }
302 }
303
304+ pedigree = (
305+ u'com.ubuntu.cloud:server:14.04:amd64', u'20160602', u'disk1.img')
306+ product = source_index[u'products'][pedigree[0]]
307+ image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
308+
309+ content_source = MemoryContentSource(
310+ url="http://image-store/fooubuntu-X-disk1.img",
311+ content="image-data")
312+
313+ # Use a fake GlanceClient to track calls and arguments passed to
314+ # GlanceClient.images.create().
315+ self.addCleanup(setattr, self.mirror, "gclient", self.mirror.gclient)
316+ self.mirror.gclient = FakeGlanceClient()
317+
318+ target = {
319+ 'content_id': 'auto.sync',
320+ 'datatype': 'image-ids',
321+ 'format': 'products:1.0',
322+ }
323+
324+ self.mirror.insert_item(
325+ image_data, source_index, target, pedigree, content_source)
326+
327+ passed_create_kwargs = self.mirror.gclient.images.create_calls[0]
328+
329+ # There is a 'data' argument pointing to an open file descriptor
330+ # for the locally downloaded image.
331+ image_content = passed_create_kwargs.pop("data").read()
332+ self.assertEqual(u"image-data", image_content.decode('utf-8'))
333+
334+ # Value of "arch" from source entry is transformed into "architecture"
335+ # image property in Glance: this ensures create_glance_properties()
336+ # is called and result is properly passed.
337+ self.assertEqual(
338+ "x86_64", passed_create_kwargs["properties"]["architecture"])
339+
340+ # MD5 hash from source entry is put into 'checksum' field, and 'name'
341+ # is based on full image name: this ensures prepare_glance_arguments()
342+ # is called.
343+ self.assertEqual(
344+ u'e5436cd36ae6cc298f081bf0f6b413f1',
345+ passed_create_kwargs["checksum"])
346+ self.assertEqual(
347+ u'auto-sync/ubuntu-trusty-14.04-amd64-server-20160602-disk1.img',
348+ passed_create_kwargs["name"])
349+
350+ # Our local endpoint is set in the resulting entry, which ensures
351+ # a call to adapt_source_entry() was indeed made.
352+ target_product = target["products"][pedigree[0]]
353+ target_image = target_product["versions"][pedigree[1]]["items"].get(
354+ pedigree[2])
355+ self.assertEqual(u"http://keystone/api/", target_image["endpoint"])
356+
357+ def test_insert_item_full(self):
358+ # This test uses the full sample entries from the source simplestreams
359+ # index from cloud-images.u.c and resulting local simplestreams index
360+ # files.
361+ source_index = copy.deepcopy(TEST_SOURCE_INDEX_ENTRY)
362+
363 # "Pedigree" is basically a "path" to get to the image data in
364 # simplestreams index, going through "products", their "versions",
365 # and nested "items".
366@@ -409,9 +554,7 @@
367
368 passed_create_kwargs = self.mirror.gclient.images.create_calls[0]
369
370- # There is a 'data' argument pointing to an open file descriptor
371- # for the locally downloaded image.
372- self.assertIn("data", passed_create_kwargs)
373+ # Drop the 'data' item pointing to an open temporary file.
374 passed_create_kwargs.pop("data")
375
376 expected_create_kwargs = {
377@@ -429,46 +572,21 @@
378 'version_name': u'20160602',
379 'content_id': 'auto.sync',
380 'product_name': u'com.ubuntu.cloud:server:14.04:amd64',
381+ 'simplestreams_metadata': (
382+ '{"aliases": "14.04,default,lts,t,trusty", '
383+ '"arch": "amd64", "ftype": "disk1.img", '
384+ '"label": "release", "md5": '
385+ '"e5436cd36ae6cc298f081bf0f6b413f1", "os": "ubuntu", '
386+ '"pubname": "ubuntu-trusty-14.04-amd64-server-20160602", '
387+ '"release": "trusty", "release_codename": "Trusty Tahr", '
388+ '"release_title": "14.04 LTS", "sha256": '
389+ '"5b982d7d4dd1a03e88ae5f35f02ed44f'
390+ '579e2711f3e0f27ea2bff20aef8c8d9e", "size": "259850752", '
391+ '"support_eol": "2019-04-17", "supported": "True", '
392+ '"version": "14.04"}'),
393 'source_content_id': u'com.ubuntu.cloud:released:download'},
394 'size': '259850752'}
395- self.assertEqual(
396- expected_create_kwargs, passed_create_kwargs)
397-
398- # Almost real resulting data as produced by simplestreams before
399- # insert_item refactoring to allow for finer-grained testing.
400- expected_target_index = {
401- 'content_id': 'auto.sync',
402- 'datatype': 'image-ids',
403- 'format': 'products:1.0',
404- 'products': {
405- "com.ubuntu.cloud:server:14.04:amd64": {
406- "aliases": "14.04,default,lts,t,trusty",
407- "arch": "amd64",
408- "label": "release",
409- "os": "ubuntu",
410- "owner_id": "bar456",
411- "pubname": "ubuntu-trusty-14.04-amd64-server-20160602",
412- "release": "trusty",
413- "release_codename": "Trusty Tahr",
414- "release_title": "14.04 LTS",
415- "support_eol": "2019-04-17",
416- "supported": "True",
417- "version": "14.04",
418- "versions": {"20160602": {"items": {"disk1.img": {
419- "endpoint": "http://keystone/api/",
420- "ftype": "disk1.img",
421- "id": "image-1",
422- "md5": "e5436cd36ae6cc298f081bf0f6b413f1",
423- "name": ("auto-sync/ubuntu-trusty-14.04-amd64-"
424- "server-20160602-disk1.img"),
425- "region": "region1",
426- "sha256": ("5b982d7d4dd1a03e88ae5f35f02ed44f"
427- "579e2711f3e0f27ea2bff20aef8c8d9e"),
428- "size": "259850752"
429- }}}}
430- }
431- }
432- }
433+ self.assertEqual(expected_create_kwargs, passed_create_kwargs)
434
435 # Apply the condensing as done in GlanceMirror.insert_products()
436 # to ensure we compare with the desired resulting simplestreams data.
437@@ -476,4 +594,40 @@
438 'region']
439 simplestreams.util.products_condense(target, sticky)
440
441- self.assertEqual(expected_target_index, target)
442+ self.assertEqual(EXPECTED_OUTPUT_INDEX, target)
443+
444+ def test_insert_item_stores_the_index(self):
445+ # Ensure insert_item calls insert_products() to generate the
446+ # resulting simplestreams index file and insert it into store.
447+
448+ source_index = copy.deepcopy(TEST_SOURCE_INDEX_ENTRY)
449+ pedigree = TEST_IMAGE_PEDIGREE
450+ product = source_index[u'products'][pedigree[0]]
451+ image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
452+
453+ content_source = MemoryContentSource(
454+ url="http://image-store/fooubuntu-X-disk1.img",
455+ content="image-data")
456+ self.mirror.store = MemoryObjectStore()
457+
458+ self.addCleanup(setattr, self.mirror, "gclient", self.mirror.gclient)
459+ self.mirror.gclient = FakeGlanceClient()
460+
461+ target = {
462+ 'content_id': 'auto.sync',
463+ 'datatype': 'image-ids',
464+ 'format': 'products:1.0',
465+ }
466+
467+ self.mirror.insert_item(
468+ image_data, source_index, target, pedigree, content_source)
469+
470+ stored_index_content = self.mirror.store.data[
471+ 'streams/v1/auto.sync.json']
472+ stored_index = json.loads(stored_index_content.decode('utf-8'))
473+
474+ # Full index contains the 'updated' key with the date of last update.
475+ self.assertIn(u"updated", stored_index)
476+ del stored_index[u"updated"]
477+
478+ self.assertEqual(EXPECTED_OUTPUT_INDEX, stored_index)
479
diff --git a/debian/patches/keystone-v3-support.patch b/debian/patches/450-453-454-keystone-v3-support.patch
index 3b24355..e3a3adf 100644
--- a/debian/patches/keystone-v3-support.patch
+++ b/debian/patches/450-453-454-keystone-v3-support.patch
@@ -15,11 +15,9 @@ Bug: https://launchpad.net/bugs/1728982
15Bug: https://launchpad.net/bugs/171987915Bug: https://launchpad.net/bugs/1719879
16Author: Scott Moser <smoser@ubuntu.com>16Author: Scott Moser <smoser@ubuntu.com>
1717
18diff --git a/simplestreams/mirrors/glance.py b/simplestreams/mirrors/glance.py
19index 807fe29..a13fede 100644
20--- a/simplestreams/mirrors/glance.py18--- a/simplestreams/mirrors/glance.py
21+++ b/simplestreams/mirrors/glance.py19+++ b/simplestreams/mirrors/glance.py
22@@ -35,7 +35,11 @@ def get_glanceclient(version='1', **kwargs):20@@ -36,7 +36,11 @@ def get_glanceclient(version='1', **kwar
23 kwargs['endpoint'] = _strip_version(kwargs['endpoint'])21 kwargs['endpoint'] = _strip_version(kwargs['endpoint'])
24 pt = ('endpoint', 'token', 'insecure', 'cacert')22 pt = ('endpoint', 'token', 'insecure', 'cacert')
25 kskw = {k: kwargs.get(k) for k in pt if k in kwargs}23 kskw = {k: kwargs.get(k) for k in pt if k in kwargs}
@@ -32,7 +30,7 @@ index 807fe29..a13fede 100644
32 30
33 31
34 def empty_iid_products(content_id):32 def empty_iid_products(content_id):
35@@ -64,6 +68,7 @@ def canonicalize_arch(arch):33@@ -65,6 +69,7 @@ def canonicalize_arch(arch):
36 LXC_FTYPES = [34 LXC_FTYPES = [
37 'root.tar.gz',35 'root.tar.gz',
38 'root.tar.xz',36 'root.tar.xz',
@@ -40,8 +38,15 @@ index 807fe29..a13fede 100644
40 ]38 ]
41 39
42 QEMU_FTYPES = [40 QEMU_FTYPES = [
43diff --git a/simplestreams/objectstores/swift.py b/simplestreams/objectstores/swift.py41@@ -267,7 +272,7 @@ class GlanceMirror(mirrors.BasicMirrorWr
44index d7598a7..9a6eb62 10064442 name_old, name_new = carry_over_property
43 else:
44 name_old = name_new = carry_over_property
45- properties[name_new] = image_metadata[name_old]
46+ properties[name_new] = image_metadata.get(name_old)
47
48 if 'arch' in image_metadata:
49 properties['architecture'] = canonicalize_arch(
45--- a/simplestreams/objectstores/swift.py50--- a/simplestreams/objectstores/swift.py
46+++ b/simplestreams/objectstores/swift.py51+++ b/simplestreams/objectstores/swift.py
47@@ -33,6 +33,15 @@ def get_swiftclient(**kwargs):52@@ -33,6 +33,15 @@ def get_swiftclient(**kwargs):
@@ -60,8 +65,6 @@ index d7598a7..9a6eb62 100644
60 return Connection(**connargs)65 return Connection(**connargs)
61 66
62 67
63diff --git a/simplestreams/openstack.py b/simplestreams/openstack.py
64index 126dea5..d143926 100644
65--- a/simplestreams/openstack.py68--- a/simplestreams/openstack.py
66+++ b/simplestreams/openstack.py69+++ b/simplestreams/openstack.py
67@@ -15,16 +15,47 @@70@@ -15,16 +15,47 @@
@@ -134,7 +137,7 @@ index 126dea5..d143926 100644
134 if missing:137 if missing:
135 raise ValueError("Need values for: %s" % missing)138 raise ValueError("Need values for: %s" % missing)
136 139
137@@ -88,11 +128,49 @@ def get_regions(client=None, services=None, kscreds=None):140@@ -88,11 +128,49 @@ def get_regions(client=None, services=No
138 return list(regions)141 return list(regions)
139 142
140 143
@@ -188,7 +191,7 @@ index 126dea5..d143926 100644
188 191
189 192
190 def get_service_conn_info(service='image', client=None, **kwargs):193 def get_service_conn_info(service='image', client=None, **kwargs):
191@@ -101,21 +179,27 @@ def get_service_conn_info(service='image', client=None, **kwargs):194@@ -101,21 +179,27 @@ def get_service_conn_info(service='image
192 client = get_ksclient(**kwargs)195 client = get_ksclient(**kwargs)
193 196
194 endpoint = _get_endpoint(client, service, **kwargs)197 endpoint = _get_endpoint(client, service, **kwargs)
diff --git a/debian/patches/455-nova-lxd-support-squashfs-images.patch b/debian/patches/455-nova-lxd-support-squashfs-images.patch
195new file mode 100644198new file mode 100644
index 0000000..b4f0270
--- /dev/null
+++ b/debian/patches/455-nova-lxd-support-squashfs-images.patch
@@ -0,0 +1,230 @@
1------------------------------------------------------------
2revno: 455 [merge]
3fixes bug: https://launchpad.net/bugs/1686086
4committer: Scott Moser <smoser@ubuntu.com>
5branch nick: trunk
6timestamp: Thu 2017-11-02 15:03:37 -0400
7message:
8 OpenStack: support uploading squash images for nova-lxd.
9
10 Previously, populating a nova-lxd cloud was possible by using
11 root.tar.gz. A filter like:
12 ftype~(root.tar.gz|root.tar.xz)
13 would cause simplestreams to upload an image with 'disk-format' of
14 root-tar.
15
16 However, Ubuntu 17.04 and newer do not have root.tar.gz or root.tar.xz
17 images available. Currently here is what is available:
18 14.04: root.tar.gz root.tar.xz
19 16.04: root.tar.gz root.tar.xz squashfs
20 17.10: squashfs
21
22 If we simply expected the user to change their filter to include
23 root.tar.xz|squashfs
24 Then they would get two lxd images imported for 16.04 each version.
25
26 The change here is to not do anything for an item insert, but instead
27 insert when the version's insert is called. Then, all the information
28 about what images there are is available, and it can "pick"
29 one or the other. Currently preference is given to the .tar.xz format.
30
31 The end result is that now users can specify an ftype filter of:
32 ftype~(root.tar.gz|root.tar.xz|squashfs)
33 and the right thing will be done.
34
35 Also here is simple knowledge that the squashfs type should be
36 uploaded to glance with a 'disk_format' of 'squashfs'.
37------------------------------------------------------------
38Use --include-merged or -n0 to see merged revisions.
39=== modified file 'simplestreams/mirrors/glance.py'
40--- a/simplestreams/mirrors/glance.py
41+++ b/simplestreams/mirrors/glance.py
42@@ -66,25 +66,27 @@ def canonicalize_arch(arch):
43 return newarch
44
45
46-LXC_FTYPES = [
47- 'root.tar.gz',
48- 'root.tar.xz',
49- 'squashfs',
50-]
51-
52-QEMU_FTYPES = [
53- 'disk.img',
54- 'disk1.img',
55-]
56+LXC_FTYPES = {
57+ 'root.tar.gz': 'root-tar',
58+ 'root.tar.xz': 'root-tar',
59+ 'squashfs': 'squashfs',
60+}
61+
62+QEMU_FTYPES = {
63+ 'disk.img': 'qcow2',
64+ 'disk1.img': 'qcow2',
65+}
66
67
68 def disk_format(ftype):
69- '''Canonicalize disk formats for use in OpenStack'''
70+ '''Canonicalize disk formats for use in OpenStack.
71+ Input ftype is a 'ftype' from a simplestream feed.
72+ Return value is the appropriate 'disk_format' for glance.'''
73 newftype = ftype.lower()
74 if newftype in LXC_FTYPES:
75- return 'root-tar'
76+ return LXC_FTYPES[newftype]
77 if newftype in QEMU_FTYPES:
78- return 'qcow2'
79+ return QEMU_FTYPES[newftype]
80 return None
81
82
83@@ -160,6 +162,7 @@ class GlanceMirror(mirrors.BasicMirrorWr
84 self.content_id = config.get("content_id")
85 self.modify_hook = config.get("modify_hook")
86
87+ self.inserts = {}
88 if not self.content_id:
89 raise TypeError("content_id is required")
90
91@@ -408,7 +411,7 @@ class GlanceMirror(mirrors.BasicMirrorWr
92
93 return output_entry
94
95- def insert_item(self, data, src, target, pedigree, contentsource):
96+ def _insert_item(self, data, src, target, pedigree, contentsource):
97 """
98 Upload image into glance and add image metadata to simplestreams index.
99
100@@ -470,6 +473,55 @@ class GlanceMirror(mirrors.BasicMirrorWr
101 # unused in insert_products below.
102 self.insert_products(None, target, None)
103
104+ def insert_item(self, data, src, target, pedigree, contentsource):
105+ """Queue item to be inserted in subsequent call to insert_version
106+
107+ This adds the item to self.inserts which is then handled in
108+ insert_version. That allows the code to have context on
109+ all the items for a given version, and "choose" one. Ie,
110+ if both root.tar.xz and squashfs are available, preference
111+ can be given to the root.tar.gz.
112+ """
113+
114+ product_name, version_name, item_name = pedigree
115+ if product_name not in self.inserts:
116+ self.inserts[product_name] = {}
117+ if version_name not in self.inserts[product_name]:
118+ self.inserts[product_name][version_name] = {}
119+
120+ if 'ftype' in data:
121+ ftype = data['ftype']
122+ else:
123+ flat = util.products_exdata(src, pedigree, include_top=False)
124+ ftype = flat.get('ftype')
125+ self.inserts[product_name][version_name][item_name] = (
126+ ftype, (data, src, target, pedigree, contentsource))
127+
128+ def insert_version(self, data, src, target, pedigree):
129+ """Upload all images for this version into glance
130+ and add image metadata to simplestreams index.
131+
132+ All the work actually happens in _insert_item.
133+ """
134+
135+ product_name, version_name = pedigree
136+ inserts = self.inserts.get(product_name, {}).get(version_name, [])
137+
138+ rtar_names = [f for f in inserts
139+ if inserts[f][0] in ('root.tar.gz', 'root.tar.xz')]
140+
141+ for _iname, (ftype, iargs) in inserts.items():
142+ if ftype == "squashfs" and rtar_names:
143+ LOG.info("[%s] Skipping ftype 'squashfs' image in preference"
144+ "for root tarball type in %s",
145+ '/'.join(pedigree), rtar_names)
146+ continue
147+ self._insert_item(*iargs)
148+
149+ # we do not specifically do anything for insert_version, but
150+ # call parent.
151+ super(GlanceMirror, self).insert_version(data, src, target, pedigree)
152+
153 def remove_item(self, data, src, target, pedigree):
154 util.products_del(target, pedigree)
155 if 'id' in data:
156--- a/tests/unittests/test_glancemirror.py
157+++ b/tests/unittests/test_glancemirror.py
158@@ -327,6 +327,15 @@ class TestGlanceMirror(TestCase):
159
160 self.assertEqual("root-tar", create_arguments["disk_format"])
161
162+ def test_prepare_glance_arguments_disk_format_squashfs(self):
163+ # squashfs images are acceptable for nova-lxd
164+ source_entry = {"ftype": "squashfs"}
165+ create_arguments = self.mirror.prepare_glance_arguments(
166+ "foobuntu-X", source_entry, image_md5_hash=None, image_size=None,
167+ image_properties=None)
168+
169+ self.assertEqual("squashfs", create_arguments["disk_format"])
170+
171 def test_prepare_glance_arguments_size(self):
172 # Size is read from image metadata if defined.
173 source_entry = {"size": 5}
174@@ -470,7 +479,8 @@ class TestGlanceMirror(TestCase):
175 pedigree = (
176 u'com.ubuntu.cloud:server:14.04:amd64', u'20160602', u'disk1.img')
177 product = source_index[u'products'][pedigree[0]]
178- image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
179+ ver_data = product[u'versions'][pedigree[1]]
180+ image_data = ver_data[u'items'][pedigree[2]]
181
182 content_source = MemoryContentSource(
183 url="http://image-store/fooubuntu-X-disk1.img",
184@@ -489,6 +499,8 @@ class TestGlanceMirror(TestCase):
185
186 self.mirror.insert_item(
187 image_data, source_index, target, pedigree, content_source)
188+ self.mirror.insert_version(
189+ ver_data, source_index, target, pedigree[0:2])
190
191 passed_create_kwargs = self.mirror.gclient.images.create_calls[0]
192
193@@ -532,7 +544,8 @@ class TestGlanceMirror(TestCase):
194 pedigree = (
195 u'com.ubuntu.cloud:server:14.04:amd64', u'20160602', u'disk1.img')
196 product = source_index[u'products'][pedigree[0]]
197- image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
198+ ver_data = product[u'versions'][pedigree[1]]
199+ image_data = ver_data[u'items'][pedigree[2]]
200
201 content_source = MemoryContentSource(
202 url="http://image-store/fooubuntu-X-disk1.img",
203@@ -551,6 +564,8 @@ class TestGlanceMirror(TestCase):
204
205 self.mirror.insert_item(
206 image_data, source_index, target, pedigree, content_source)
207+ self.mirror.insert_version(
208+ image_data, source_index, target, pedigree[0:2])
209
210 passed_create_kwargs = self.mirror.gclient.images.create_calls[0]
211
212@@ -603,7 +618,8 @@ class TestGlanceMirror(TestCase):
213 source_index = copy.deepcopy(TEST_SOURCE_INDEX_ENTRY)
214 pedigree = TEST_IMAGE_PEDIGREE
215 product = source_index[u'products'][pedigree[0]]
216- image_data = product[u'versions'][pedigree[1]][u'items'][pedigree[2]]
217+ ver_data = product[u'versions'][pedigree[1]]
218+ image_data = ver_data[u'items'][pedigree[2]]
219
220 content_source = MemoryContentSource(
221 url="http://image-store/fooubuntu-X-disk1.img",
222@@ -621,6 +637,8 @@ class TestGlanceMirror(TestCase):
223
224 self.mirror.insert_item(
225 image_data, source_index, target, pedigree, content_source)
226+ self.mirror.insert_version(
227+ ver_data, source_index, target, pedigree[0:2])
228
229 stored_index_content = self.mirror.store.data[
230 'streams/v1/auto.sync.json']
diff --git a/debian/patches/460-glance-handle-v2-auth-with-sessions.patch b/debian/patches/460-glance-handle-v2-auth-with-sessions.patch
0new file mode 100644231new file mode 100644
index 0000000..5fca027
--- /dev/null
+++ b/debian/patches/460-glance-handle-v2-auth-with-sessions.patch
@@ -0,0 +1,33 @@
1------------------------------------------------------------
2revno: 460 [merge]
3fixes bug: https://launchpad.net/bugs/1611987
4author: David Ames <david.ames@canonical.com>
5committer: Scott Moser <smoser@ubuntu.com>
6branch nick: trunk
7timestamp: Thu 2018-04-12 12:33:46 -0400
8message:
9 Glance: Handle Keystone v2 with session based authentication
10
11 There are three cases we have to handle:
12 - keystone v2 without sessions
13 - keystone v2 with sessions
14 - keystone v3 with sessions
15
16 We had the first and the last covered but not the middle. This change
17 addresses this.
18------------------------------------------------------------
19Use --include-merged or -n0 to see merged revisions.
20=== modified file 'simplestreams/openstack.py'
21--- a/simplestreams/openstack.py 2017-10-31 13:32:56 +0000
22+++ b/simplestreams/openstack.py 2018-04-10 21:35:53 +0000
23@@ -181,7 +181,8 @@
24 endpoint = _get_endpoint(client, service, **kwargs)
25 # Session client does not have tenant_id set at client.tenant_id
26 # If client.tenant_id not set use method to get it
27- tenant_id = client.tenant_id or client.auth.client.get_project_id()
28+ tenant_id = (client.tenant_id or client.get_project_id(client.session) or
29+ client.auth.client.get_project_id())
30 info = {'token': client.auth_token, 'insecure': kwargs.get('insecure'),
31 'cacert': kwargs.get('cacert'), 'endpoint': endpoint,
32 'tenant_id': tenant_id}
33
diff --git a/debian/patches/series b/debian/patches/series
index 8eaeaf4..6b946bc 100644
--- a/debian/patches/series
+++ b/debian/patches/series
@@ -1,3 +1,10 @@
1read_signed-speed1read_signed-speed
2custom_user_agent_lp1578624.patch2custom_user_agent_lp1578624.patch
3keystone-v3-support.patch3428-do-not-require-that-hypervisor_config-be-present.patch
4433-glance-ignore-inactive-images.patch
5435-glance-refactor-for-testing.patch
6436-glance-fix-race-conditions.patch
7skip-openstack-tests-if-no-libs.patch
8450-453-454-keystone-v3-support.patch
9455-nova-lxd-support-squashfs-images.patch
10460-glance-handle-v2-auth-with-sessions.patch
diff --git a/debian/patches/skip-openstack-tests-if-no-libs.patch b/debian/patches/skip-openstack-tests-if-no-libs.patch
4new file mode 10064411new file mode 100644
index 0000000..43e3665
--- /dev/null
+++ b/debian/patches/skip-openstack-tests-if-no-libs.patch
@@ -0,0 +1,36 @@
1Description: Skip tests of openstack to avoid build-depends.
2 This takes a bit of upstream commit revno 440 to skip openstack
3 tests if the libraries are not present.
4Applied-Upstream: revno 440
5Author: Scott Moser <smoser@ubuntu.com>
6--- a/tests/unittests/test_glancemirror.py
7+++ b/tests/unittests/test_glancemirror.py
8@@ -1,12 +1,17 @@
9 from simplestreams.contentsource import MemoryContentSource
10-from simplestreams.mirrors.glance import GlanceMirror
11-from simplestreams.objectstores import MemoryObjectStore
12+try:
13+ from simplestreams.mirrors.glance import GlanceMirror
14+ from simplestreams.objectstores import MemoryObjectStore
15+ HAVE_OPENSTACK_LIBS = True
16+except ImportError:
17+ HAVE_OPENSTACK_LIBS = False
18+
19 import simplestreams.util
20
21 import copy
22 import json
23 import os
24-from unittest import TestCase
25+from unittest import TestCase, skipIf
26
27
28 # This is a real snippet from the simplestreams index entry for
29@@ -118,6 +123,7 @@ class FakeGlanceClient(object):
30 self.images = FakeImages()
31
32
33+@skipIf(not HAVE_OPENSTACK_LIBS, "no python3 openstack available")
34 class TestGlanceMirror(TestCase):
35 """Tests for GlanceMirror methods."""
36

Subscribers

People subscribed via source and target branches