Merge lp:~waveform/meta-release/ubuntu into lp:~ubuntu-core-dev/meta-release/ubuntu

Proposed by Dave Jones
Status: Merged
Merged at revision: 244
Proposed branch: lp:~waveform/meta-release/ubuntu
Merge into: lp:~ubuntu-core-dev/meta-release/ubuntu
Diff against target: 306 lines (+247/-9)
2 files modified
raspi/os_list_imagingutility_ubuntu.json (+15/-9)
refresh_os_list (+232/-0)
To merge this branch: bzr merge lp:~waveform/meta-release/ubuntu
Reviewer Review Type Date Requested Status
Brian Murray Pending
Review via email: mp+382438@code.launchpad.net

Commit message

Corrects hash for core armhf image in raspi JSON; adds utility for keeping the raspi JSON up to date relatively easily.

To post a comment you must log in.
lp:~waveform/meta-release/ubuntu updated
244. By Brian Murray

merge waveform's branch to Correct hash for core armhf image in raspi JSON; add utility for keeping the raspi JSON up to date relatively easily.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'raspi/os_list_imagingutility_ubuntu.json'
2--- raspi/os_list_imagingutility_ubuntu.json 2020-04-15 15:31:27 +0000
3+++ raspi/os_list_imagingutility_ubuntu.json 2020-04-16 17:10:22 +0000
4@@ -8,7 +8,8 @@
5 "extract_size": 2388864000,
6 "extract_sha256": "4ae38d8fb9d13c52aace1b81c4973b9d059228a62bba313055129314c203dd0a",
7 "image_download_size": 494466080,
8- "release_date": "2020-02-03"
9+ "release_date": "2020-02-03",
10+ "image_download_sha256": "0382a2b87b6937ff645f59f546120e3480f2d5a2c2945712bf373379b9e82f51"
11 },
12 {
13 "name": "Ubuntu 18.04.4 (Pi 3/4)",
14@@ -18,7 +19,8 @@
15 "extract_size": 2624517120,
16 "extract_sha256": "be04570b43c5253b4bfe4beed1d2e9c76e9113ea5e5b3dc004ba83c5b5313dbb",
17 "image_download_size": 499792288,
18- "release_date": "2020-02-03"
19+ "release_date": "2020-02-03",
20+ "image_download_sha256": "f270d4a11fcef7f85ea77bc0642d1c6db2666ae734e9dcc4cb875a31c9f0dc57"
21 },
22 {
23 "name": "Ubuntu 19.10.1 (Pi 2/3/4)",
24@@ -28,7 +30,8 @@
25 "extract_size": 2832030720,
26 "extract_sha256": "7564bf108b958f848a41633c27241fa6b991207467a1f55b0d4451c8a4ad5920",
27 "image_download_size": 642984640,
28- "release_date": "2019-12-03"
29+ "release_date": "2019-12-03",
30+ "image_download_sha256": "d40820ba3933554b9902dc560e8a98d20352ae49b8544e71cacc6fdcfd2c2405"
31 },
32 {
33 "name": "Ubuntu 19.10.1 (Pi 3/4)",
34@@ -38,17 +41,19 @@
35 "extract_size": 3115115520,
36 "extract_sha256": "24b184762a2ebdca0e7f8151d3ea84e82c5e0c4a7f5a85f24f12f1cd5b197560",
37 "image_download_size": 662616972,
38- "release_date": "2019-12-03"
39+ "release_date": "2019-12-03",
40+ "image_download_sha256": "3fcae7490edebd35663cb30dcd7a6317b6b9601d0f59ed5caa3c20dfd936cbb1"
41 },
42 {
43 "name": "Ubuntu Core 18 (Pi 2/3/4)",
44 "description": "Ubuntu Core 18 32-bit IoT OS for armhf architectures",
45 "url": "http://cdimage.ubuntu.com/ubuntu-core/18/stable/current/ubuntu-core-18-armhf+raspi.img.xz",
46 "icon": "https://assets.ubuntu.com/v1/85a9de76-ubuntu-icon.svg",
47- "extract_size": 563676160,
48- "extract_sha256": "21fb0f570b5398ccce65e3c0d2dcca4033d0c78cd9d7636c90d89ae0ec513b0c",
49+ "extract_size": 700517376,
50+ "extract_sha256": "ca6968a7e1c1977ee740b3e29b5b74430fc9aec55b1e0310fae85dae1c1af8e7",
51 "image_download_size": 315745444,
52- "release_date": "2020-04-06"
53+ "release_date": "2020-04-06",
54+ "image_download_sha256": "31682b959c06487307a8cb2eef41b33878d1f6547fd5d3eb0b24256f9bf88c8e"
55 },
56 {
57 "name": "Ubuntu Core 18 (Pi 3/4)",
58@@ -58,7 +63,8 @@
59 "extract_size": 709346304,
60 "extract_sha256": "d005e21d1df4f33f897eaabd7ec66dc1446ac0c537cc3a79dfd9825a174fa12e",
61 "image_download_size": 321941660,
62- "release_date": "2020-04-06"
63+ "release_date": "2020-04-06",
64+ "image_download_sha256": "1c7029f506da40c233b0257b2b395ac54126716cc2aaa87d0a4bf724c216992d"
65 }
66 ]
67-}
68+}
69\ No newline at end of file
70
71=== added file 'refresh_os_list'
72--- refresh_os_list 1970-01-01 00:00:00 +0000
73+++ refresh_os_list 2020-04-16 17:10:22 +0000
74@@ -0,0 +1,232 @@
75+#!/usr/bin/python3
76+
77+import os
78+import io
79+import sys
80+import json
81+import gzip
82+import bz2
83+import lzma
84+import hashlib
85+import argparse
86+import warnings
87+import functools
88+import datetime as dt
89+from html.parser import HTMLParser
90+from urllib.parse import urlsplit, urlunsplit
91+from urllib.request import urlopen, Request
92+from collections import namedtuple, OrderedDict
93+
94+
95+def main(args=None):
96+ """
97+ A script for generating the JSON list required by the Raspberry Pi imaging
98+ utility. Takes an existing JSON list as input and updates it by calculating
99+ new image sizes and check-sums from the source files on the server. Note
100+ that the existing JSON list can be partial. The only mandatory field in
101+ each entry is the "url"; the script will fill out all other missing fields
102+ (albeit with place-holders in the case of "name" and "description" which it
103+ can't derive).
104+ """
105+ if args is None:
106+ args = sys.argv[1:]
107+ parser = argparse.ArgumentParser(description=main.__doc__)
108+ parser.add_argument(
109+ 'input_file', type=argparse.FileType('r', encoding='utf-8'),
110+ help="The partial JSON template to fill out")
111+ parser.add_argument(
112+ 'output_file', nargs='?', type=argparse.FileType('w', encoding='utf-8'),
113+ default=sys.stdout,
114+ help="The output file to create; defaults to stdout")
115+ parser.add_argument(
116+ '-f', '--force', action='store_true',
117+ help="Force the utility to refresh all images even if the release "
118+ "date and download size have not changed in the index")
119+ args = parser.parse_args(args)
120+ try:
121+ update_template(args.input_file, args.output_file, args.force)
122+ except Exception as e:
123+ if not int(os.environ.get('DEBUG', '0')):
124+ print(str(e), file=sys.stderr)
125+ sys.exit(1)
126+ else:
127+ raise
128+
129+
130+def update_template(input_file, output_file, force=False):
131+ template = json.load(input_file, object_pairs_hook=OrderedDict)
132+ if not isinstance(template, dict):
133+ raise ValueError('expected a JSON object in {}'.format(args.input_file))
134+ if template.keys() != {'os_list'}:
135+ raise ValueError('expected a single "os_list" entry')
136+ if not all('url' in entry for entry in template['os_list']):
137+ raise ValueError('all "os_list" entries must contain a "url" entry')
138+
139+ for entry in template['os_list']:
140+ url = entry['url']
141+ source = get_entry(url)
142+ if 'icon' not in entry:
143+ entry['icon'] = 'https://assets.ubuntu.com/v1/85a9de76-ubuntu-icon.svg'
144+ if 'name' not in entry:
145+ warnings.warn(Warning('Inserted placeholder entries; check output'))
146+ entry['name'] = 'PLACEHOLDER'
147+ if 'description' not in entry:
148+ warnings.warn(Warning('Inserted placeholder entries; check output'))
149+ entry['description'] = 'PLACEHOLDER'
150+ update = (
151+ force or
152+ # image_download_sha256 is optional and may be missing, hence the
153+ # unusual default here
154+ entry.get('image_download_sha256', source.sha256) != source.sha256 or
155+ entry.get('image_download_size', 0) != source.size or
156+ dt.datetime.strptime(
157+ entry.get('release_date', '1970-01-01'), '%Y-%m-%d'
158+ ).date() != source.date
159+ )
160+ if update:
161+ print('Updating {}'.format(source.name), file=sys.stderr)
162+ with HashPassthru(urlopen(url)) as compressed_image:
163+ decompresser = {
164+ 'gz': gzip.open,
165+ 'bz2': bz2.open,
166+ 'xz': lambda fd: lzma.open(fd, format=lzma.FORMAT_XZ),
167+ }[url.rsplit('.', 1)[1]]
168+ with decompresser(compressed_image) as decompressed_image:
169+ cksum = hashlib.sha256()
170+ size = 0
171+ while True:
172+ buf = decompressed_image.read(65536)
173+ if not buf:
174+ break
175+ size += len(buf)
176+ cksum.update(buf)
177+ entry['extract_size'] = size
178+ entry['extract_sha256'] = cksum.hexdigest().lower()
179+ if compressed_image.size != source.size:
180+ raise ValueError('Corrupted download; size check failed')
181+ if compressed_image.cksum.hexdigest().lower() != source.sha256:
182+ raise ValueError('Corrupted download; SHA256 check failed')
183+ entry['image_download_size'] = source.size
184+ entry['release_date'] = source.date.strftime('%Y-%m-%d')
185+ else:
186+ print('Entry for {} is up to date'.format(source.name),
187+ file=sys.stderr)
188+ # Always update image_download_sha256 to fill it out when missing
189+ entry['image_download_sha256'] = source.sha256
190+ json.dump(template, output_file, indent=4)
191+
192+
193+class HashPassthru:
194+ def __init__(self, stream):
195+ self.stream = stream
196+ self.cksum = hashlib.sha256()
197+ self.size = 0
198+
199+ def read(self, n):
200+ result = self.stream.read(n)
201+ self.size += len(result)
202+ self.cksum.update(result)
203+ return result
204+
205+ def __enter__(self):
206+ return self
207+
208+ def __exit__(self, *exc):
209+ self.stream.close()
210+
211+
212+class TableParser(HTMLParser):
213+ """
214+ A subclass of :class:`html.parser.HTMLParser` used by :func:`get_entry` for
215+ parsing the <table> out of a web-page representing a directory of OS
216+ images.
217+ """
218+ def __init__(self):
219+ super().__init__(convert_charrefs=True)
220+ self.state = 'html'
221+ self.table = []
222+
223+ def handle_starttag(self, tag, attrs):
224+ if self.state == 'html' and tag == 'table':
225+ self.state = 'table'
226+ elif self.state == 'table' and tag == 'tr':
227+ self.state = 'tr'
228+ self.table.append([])
229+ elif self.state == 'tr' and tag in ('th', 'td'):
230+ self.state = 'td'
231+ self.table[-1].append(None)
232+
233+ def handle_data(self, data):
234+ if self.state == 'td':
235+ self.table[-1][-1] = data
236+
237+ def handle_endtag(self, tag):
238+ if self.state == 'table' and tag == 'table':
239+ self.state = 'html'
240+ elif self.state == 'tr' and tag == 'tr':
241+ self.state = 'table'
242+ elif self.state == 'td' and tag in ('th', 'td'):
243+ self.state = 'tr'
244+
245+
246+IndexEntry = namedtuple('IndexEntry', ('name', 'date', 'sha256', 'size'))
247+
248+
249+def get_entry(url):
250+ """
251+ Given the *url* of an image, returns an :class:`IndexEntry` named tuple
252+ containing the name, generated date, SHA-256 check-sum, and file size.
253+ """
254+ split = urlsplit(url)
255+ path, name = split.path.rsplit('/', 1)
256+ index = urlunsplit(split._replace(path=path + '/'))
257+ entry = get_index(index)[name]
258+ if entry.size is None or entry.sha256 is None:
259+ raise ValueError('unable to retrieve file-size or checksum for '
260+ '{}'.format(url))
261+ return entry
262+
263+
264+@functools.lru_cache()
265+def get_index(url):
266+ """
267+ Given the *url* of a directory containing images, returns a dict mapping
268+ filenames to :class:`IndexEntry` named tuples.
269+ """
270+ parser = TableParser()
271+ with urlopen(url) as page:
272+ parser.feed(page.read().decode('utf-8'))
273+ entries = {}
274+ for row in parser.table:
275+ try:
276+ icon, name, date, size = row
277+ except ValueError:
278+ # Evidently not a file row
279+ continue
280+ name = name.strip()
281+ try:
282+ date = dt.datetime.strptime(date.strip(), '%Y-%m-%d %H:%M').date()
283+ except ValueError:
284+ # Evidently not a file row
285+ continue
286+ sha256 = None
287+ request = Request(url + name, method='HEAD')
288+ with urlopen(request) as head:
289+ size = int(head.getheader('Content-Length', '0'))
290+ entries[name] = IndexEntry(name, date, sha256, size)
291+ if 'SHA256SUMS' in entries:
292+ with urlopen(url + 'SHA256SUMS') as hashes:
293+ for line in hashes:
294+ cksum, name = line.decode('utf-8').strip().split(None, 1)
295+ cksum = cksum.strip().lower()
296+ if name.startswith('*'):
297+ name = name[1:]
298+ try:
299+ entries[name] = entries[name]._replace(sha256=cksum)
300+ except KeyError:
301+ pass
302+ return entries
303+
304+
305+if __name__ == '__main__':
306+ main()

Subscribers

People subscribed via source and target branches