blob: 1b713cc79802f53450d95d13adefbc30f8fdcd28 [file] [log] [blame]
Xiaochu Liudeed0232018-06-26 10:25:34 -07001# -*- coding: utf-8 -*-
2# Copyright 2018 The Chromium OS Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
6"""Script to generate a DLC (Downloadable Content) artifact."""
7
8from __future__ import print_function
9
10import hashlib
11import json
12import math
13import os
Amin Hassani11a88cf2019-01-29 15:31:24 -080014import shutil
Xiaochu Liudeed0232018-06-26 10:25:34 -070015
16from chromite.lib import commandline
17from chromite.lib import cros_build_lib
18from chromite.lib import osutils
19
20
Amin Hassani2af75a92019-01-22 21:07:45 -080021DLC_META_DIR = 'opt/google/dlc/'
22DLC_IMAGE_DIR = 'build/rootfs/dlc/'
Amin Hassanid5742d32019-01-22 21:13:34 -080023LSB_RELEASE = 'etc/lsb-release'
24
Amin Hassani11a88cf2019-01-29 15:31:24 -080025# This file has major and minor version numbers that the update_engine client
26# supports. These values are needed for generating a delta/full payload.
27UPDATE_ENGINE_CONF = 'etc/update_engine.conf'
28
29_EXTRA_RESOURCES = (
30 UPDATE_ENGINE_CONF,
31)
32
Amin Hassanid5742d32019-01-22 21:13:34 -080033DLC_ID_KEY = 'DLC_ID'
34DLC_NAME_KEY = 'DLC_NAME'
Amin Hassani2af75a92019-01-22 21:07:45 -080035
Amin Hassani22a25eb2019-01-11 14:25:02 -080036_SQUASHFS_TYPE = 'squashfs'
37_EXT4_TYPE = 'ext4'
38
Amin Hassanid5742d32019-01-22 21:13:34 -080039
Xiaochu Liudeed0232018-06-26 10:25:34 -070040def HashFile(file_path):
41 """Calculate the sha256 hash of a file.
42
43 Args:
44 file_path: (str) path to the file.
45
46 Returns:
47 [str]: The sha256 hash of the file.
48 """
49 sha256 = hashlib.sha256()
50 with open(file_path, 'rb') as f:
51 for b in iter(lambda: f.read(2048), b''):
52 sha256.update(b)
53 return sha256.hexdigest()
54
55
Amin Hassani174eb7e2019-01-18 11:11:24 -080056class DlcGenerator(object):
Xiaochu Liudeed0232018-06-26 10:25:34 -070057 """Object to generate DLC artifacts."""
58 # Block size for the DLC image.
59 # We use 4K for various reasons:
60 # 1. it's what imageloader (linux kernel) supports.
61 # 2. it's what verity supports.
62 _BLOCK_SIZE = 4096
63 # Blocks in the initial sparse image.
64 _BLOCKS = 500000
65 # Version of manifest file.
66 _MANIFEST_VERSION = 1
67
Amin Hassanicc7ffce2019-01-11 14:57:52 -080068 # The DLC root path inside the DLC module.
69 _DLC_ROOT_DIR = 'root'
70
Amin Hassani11a88cf2019-01-29 15:31:24 -080071 def __init__(self, src_dir, build_root_dir, install_root_dir, fs_type,
72 pre_allocated_blocks, version, dlc_id, name):
Xiaochu Liudeed0232018-06-26 10:25:34 -070073 """Object initializer.
74
75 Args:
Xiaochu Liudeed0232018-06-26 10:25:34 -070076 src_dir: (str) path to the DLC source root directory.
Amin Hassani11a88cf2019-01-29 15:31:24 -080077 build_root_dir: (str) The path to the build root directory.
Amin Hassani2af75a92019-01-22 21:07:45 -080078 install_root_dir: (str) The path to the root installation directory.
Xiaochu Liudeed0232018-06-26 10:25:34 -070079 fs_type: (str) file system type.
80 pre_allocated_blocks: (int) number of blocks pre-allocated on device.
81 version: (str) DLC version.
82 dlc_id: (str) DLC ID.
83 name: (str) DLC name.
84 """
85 self.src_dir = src_dir
Amin Hassani11a88cf2019-01-29 15:31:24 -080086 self.build_root_dir = build_root_dir
Amin Hassani2af75a92019-01-22 21:07:45 -080087 self.install_root_dir = install_root_dir
Xiaochu Liudeed0232018-06-26 10:25:34 -070088 self.fs_type = fs_type
89 self.pre_allocated_blocks = pre_allocated_blocks
90 self.version = version
91 self.dlc_id = dlc_id
92 self.name = name
Amin Hassani2af75a92019-01-22 21:07:45 -080093
94 self.meta_dir = os.path.join(self.install_root_dir, DLC_META_DIR,
95 self.dlc_id)
96 self.image_dir = os.path.join(self.install_root_dir, DLC_IMAGE_DIR,
97 self.dlc_id)
98 osutils.SafeMakedirs(self.meta_dir)
99 osutils.SafeMakedirs(self.image_dir)
100
Xiaochu Liudeed0232018-06-26 10:25:34 -0700101 # Create path for all final artifacts.
Amin Hassani2af75a92019-01-22 21:07:45 -0800102 self.dest_image = os.path.join(self.image_dir, 'dlc.img')
103 self.dest_table = os.path.join(self.meta_dir, 'table')
104 self.dest_imageloader_json = os.path.join(self.meta_dir, 'imageloader.json')
Xiaochu Liudeed0232018-06-26 10:25:34 -0700105
106 def SquashOwnerships(self, path):
107 """Squash the owernships & permissions for files.
108
109 Args:
110 path: (str) path that contains all files to be processed.
111 """
112 cros_build_lib.SudoRunCommand(['chown', '-R', '0:0', path])
113 cros_build_lib.SudoRunCommand(
114 ['find', path, '-exec', 'touch', '-h', '-t', '197001010000.00', '{}',
115 '+'])
116
117 def CreateExt4Image(self):
118 """Create an ext4 image."""
119 with osutils.TempDir(prefix='dlc_') as temp_dir:
120 mount_point = os.path.join(temp_dir, 'mount_point')
121 # Create a raw image file.
122 with open(self.dest_image, 'w') as f:
123 f.truncate(self._BLOCKS * self._BLOCK_SIZE)
124 # Create an ext4 file system on the raw image.
125 cros_build_lib.RunCommand(
126 ['/sbin/mkfs.ext4', '-b', str(self._BLOCK_SIZE), '-O',
127 '^has_journal', self.dest_image], capture_output=True)
128 # Create the mount_point directory.
129 osutils.SafeMakedirs(mount_point)
130 # Mount the ext4 image.
131 osutils.MountDir(self.dest_image, mount_point, mount_opts=('loop', 'rw'))
Amin Hassanicc7ffce2019-01-11 14:57:52 -0800132
Xiaochu Liudeed0232018-06-26 10:25:34 -0700133 try:
Amin Hassani11a88cf2019-01-29 15:31:24 -0800134 self.SetupDlcImageFiles(mount_point)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700135 finally:
136 # Unmount the ext4 image.
137 osutils.UmountDir(mount_point)
138 # Shrink to minimum size.
139 cros_build_lib.RunCommand(
140 ['/sbin/e2fsck', '-y', '-f', self.dest_image], capture_output=True)
141 cros_build_lib.RunCommand(
142 ['/sbin/resize2fs', '-M', self.dest_image], capture_output=True)
143
144 def CreateSquashfsImage(self):
145 """Create a squashfs image."""
146 with osutils.TempDir(prefix='dlc_') as temp_dir:
Amin Hassani22a25eb2019-01-11 14:25:02 -0800147 squashfs_root = os.path.join(temp_dir, 'squashfs-root')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800148 self.SetupDlcImageFiles(squashfs_root)
Amin Hassani22a25eb2019-01-11 14:25:02 -0800149
150 cros_build_lib.RunCommand(['mksquashfs', squashfs_root, self.dest_image,
151 '-4k-align', '-noappend'],
152 capture_output=True)
153
154 # We changed the ownership and permissions of the squashfs_root
155 # directory. Now we need to remove it manually.
156 osutils.RmDir(squashfs_root, sudo=True)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700157
Amin Hassani11a88cf2019-01-29 15:31:24 -0800158 def SetupDlcImageFiles(self, dlc_dir):
159 """Prepares the directory dlc_dir with all the files a DLC needs.
160
161 Args:
162 dlc_dir: (str) The path to where to setup files inside the DLC.
163 """
164 dlc_root_dir = os.path.join(dlc_dir, self._DLC_ROOT_DIR)
165 osutils.SafeMakedirs(dlc_root_dir)
166 osutils.CopyDirContents(self.src_dir, dlc_root_dir)
167 self.PrepareLsbRelease(dlc_dir)
168 self.CollectExtraResources(dlc_dir)
169 self.SquashOwnerships(dlc_dir)
170
Amin Hassanid5742d32019-01-22 21:13:34 -0800171 def PrepareLsbRelease(self, dlc_dir):
172 """Prepare the file /etc/lsb-release in the DLC module.
173
174 This file is used dropping some identification parameters for the DLC.
175
176 Args:
177 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
178 when we are creating the image.
179 """
180 lsb_release = os.path.join(dlc_dir, LSB_RELEASE)
181 osutils.SafeMakedirs(os.path.dirname(lsb_release))
182
183 fields = {
184 DLC_ID_KEY: self.dlc_id,
185 DLC_NAME_KEY: self.name,
186 }
187 content = ''.join(['%s=%s\n' % (k, v) for k, v in fields.items()])
188 osutils.WriteFile(lsb_release, content)
189
Amin Hassani11a88cf2019-01-29 15:31:24 -0800190 def CollectExtraResources(self, dlc_dir):
191 """Collect the extra resources needed by the DLC module.
192
193 Look at the documentation around _EXTRA_RESOURCES.
194
195 Args:
196 dlc_dir: (str) The path to root directory of the DLC. e.g. mounted point
197 when we are creating the image.
198 """
199 for r in _EXTRA_RESOURCES:
200 source_path = os.path.join(self.build_root_dir, r)
201 target_path = os.path.join(dlc_dir, r)
202 osutils.SafeMakedirs(os.path.dirname(target_path))
203 shutil.copyfile(source_path, target_path)
204
Xiaochu Liudeed0232018-06-26 10:25:34 -0700205 def CreateImage(self):
206 """Create the image and copy the DLC files to it."""
Amin Hassani22a25eb2019-01-11 14:25:02 -0800207 if self.fs_type == _EXT4_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700208 self.CreateExt4Image()
Amin Hassani22a25eb2019-01-11 14:25:02 -0800209 elif self.fs_type == _SQUASHFS_TYPE:
Xiaochu Liudeed0232018-06-26 10:25:34 -0700210 self.CreateSquashfsImage()
211 else:
212 raise ValueError('Wrong fs type: %s used:' % self.fs_type)
213
214 def GetImageloaderJsonContent(self, image_hash, table_hash, blocks):
215 """Return the content of imageloader.json file.
216
217 Args:
218 image_hash: (str) sha256 hash of the DLC image.
219 table_hash: (str) sha256 hash of the DLC table file.
220 blocks: (int) number of blocks in the DLC image.
221
222 Returns:
223 [str]: content of imageloader.json file.
224 """
225 return {
226 'fs-type': self.fs_type,
227 'id': self.dlc_id,
228 'image-sha256-hash': image_hash,
229 'image-type': 'dlc',
230 'is-removable': True,
231 'manifest-version': self._MANIFEST_VERSION,
232 'name': self.name,
233 'pre-allocated-size': self.pre_allocated_blocks * self._BLOCK_SIZE,
234 'size': blocks * self._BLOCK_SIZE,
235 'table-sha256-hash': table_hash,
236 'version': self.version,
237 }
238
239 def GenerateVerity(self):
240 """Generate verity parameters and hashes for the image."""
241 with osutils.TempDir(prefix='dlc_') as temp_dir:
242 hash_tree = os.path.join(temp_dir, 'hash_tree')
243 # Get blocks in the image.
244 blocks = math.ceil(
245 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
246 result = cros_build_lib.RunCommand(
247 ['verity', 'mode=create', 'alg=sha256', 'payload=' + self.dest_image,
248 'payload_blocks=' + str(blocks), 'hashtree=' + hash_tree,
249 'salt=random'], capture_output=True)
250 table = result.output
251
252 # Append the merkle tree to the image.
253 osutils.WriteFile(self.dest_image, osutils.ReadFile(hash_tree), 'a+')
254
255 # Write verity parameter to table file.
256 osutils.WriteFile(self.dest_table, table)
257
258 # Compute image hash.
259 image_hash = HashFile(self.dest_image)
260 table_hash = HashFile(self.dest_table)
261 # Write image hash to imageloader.json file.
262 blocks = math.ceil(
263 os.path.getsize(self.dest_image) / self._BLOCK_SIZE)
264 imageloader_json_content = self.GetImageloaderJsonContent(
265 image_hash, table_hash, int(blocks))
266 with open(self.dest_imageloader_json, 'w') as f:
267 json.dump(imageloader_json_content, f)
268
269 def GenerateDLC(self):
270 """Generate a DLC artifact."""
271 # Create the image and copy the DLC files to it.
272 self.CreateImage()
273 # Generate hash tree and other metadata.
274 self.GenerateVerity()
275
276
277def GetParser():
278 parser = commandline.ArgumentParser(description=__doc__)
279 # Required arguments:
280 required = parser.add_argument_group('Required Arguments')
281 required.add_argument('--src-dir', type='path', metavar='SRC_DIR_PATH',
282 required=True,
283 help='Root directory path that contains all DLC files '
284 'to be packed.')
Amin Hassani11a88cf2019-01-29 15:31:24 -0800285 required.add_argument('--build-root-dir', type='path', metavar='DIR',
286 required=True,
287 help="The root path to the board's build root, e.g. "
288 "/build/eve")
Amin Hassani2af75a92019-01-22 21:07:45 -0800289 required.add_argument('--install-root-dir', type='path', metavar='DIR',
Xiaochu Liudeed0232018-06-26 10:25:34 -0700290 required=True,
Amin Hassani2af75a92019-01-22 21:07:45 -0800291 help='The root path to install DLC images (in %s) and '
292 'metadata (in %s).' % (DLC_IMAGE_DIR, DLC_META_DIR))
Xiaochu Liudeed0232018-06-26 10:25:34 -0700293 required.add_argument('--pre-allocated-blocks', type=int,
294 metavar='PREALLOCATEDBLOCKS', required=True,
295 help='Number of blocks (block size is 4k) that need to'
296 'be pre-allocated on device.')
297 required.add_argument('--version', metavar='VERSION', required=True,
298 help='DLC Version.')
299 required.add_argument('--id', metavar='ID', required=True,
300 help='DLC ID (unique per DLC).')
301 required.add_argument('--name', metavar='NAME', required=True,
302 help='A human-readable name for the DLC.')
Amin Hassani22a25eb2019-01-11 14:25:02 -0800303
304 args = parser.add_argument_group('Arguments')
305 args.add_argument('--fs-type', metavar='FS_TYPE', default=_SQUASHFS_TYPE,
306 choices=(_SQUASHFS_TYPE, _EXT4_TYPE),
307 help='File system type of the image.')
308
Xiaochu Liudeed0232018-06-26 10:25:34 -0700309 return parser
310
311
312def main(argv):
313 opts = GetParser().parse_args(argv)
314 opts.Freeze()
315
Amin Hassani2af75a92019-01-22 21:07:45 -0800316 if opts.fs_type == _EXT4_TYPE:
317 raise Exception('ext4 unsupported, see https://crbug.com/890060')
318
Xiaochu Liudeed0232018-06-26 10:25:34 -0700319 # Generate final DLC files.
Amin Hassani11a88cf2019-01-29 15:31:24 -0800320 dlc_generator = DlcGenerator(src_dir=opts.src_dir,
321 build_root_dir=opts.build_root_dir,
322 install_root_dir=opts.install_root_dir,
323 fs_type=opts.fs_type,
324 pre_allocated_blocks=opts.pre_allocated_blocks,
325 version=opts.version,
326 dlc_id=opts.id,
327 name=opts.name)
Xiaochu Liudeed0232018-06-26 10:25:34 -0700328 dlc_generator.GenerateDLC()