1#!/usr/bin/env python3 2# 3# Copyright (c) 2016 Intel, Inc. 4# 5# SPDX-License-Identifier: GPL-2.0-only 6# 7# DESCRIPTION 8# This module provides parser for kickstart format 9# 10# AUTHORS 11# Ed Bartosh <ed.bartosh> (at] linux.intel.com> 12 13"""Kickstart parser module.""" 14 15import os 16import shlex 17import logging 18import re 19 20from argparse import ArgumentParser, ArgumentError, ArgumentTypeError 21 22from wic.engine import find_canned 23from wic.partition import Partition 24from wic.misc import get_bitbake_var 25 26logger = logging.getLogger('wic') 27 28__expand_var_regexp__ = re.compile(r"\${[^{}@\n\t :]+}") 29 30def expand_line(line): 31 while True: 32 m = __expand_var_regexp__.search(line) 33 if not m: 34 return line 35 key = m.group()[2:-1] 36 val = get_bitbake_var(key) 37 if val is None: 38 logger.warning("cannot expand variable %s" % key) 39 return line 40 line = line[:m.start()] + val + line[m.end():] 41 42class KickStartError(Exception): 43 """Custom exception.""" 44 pass 45 46class KickStartParser(ArgumentParser): 47 """ 48 This class overwrites error method to throw exception 49 instead of producing usage message(default argparse behavior). 50 """ 51 def error(self, message): 52 raise ArgumentError(None, message) 53 54def sizetype(default, size_in_bytes=False): 55 def f(arg): 56 """ 57 Custom type for ArgumentParser 58 Converts size string in <num>[S|s|K|k|M|G] format into the integer value 59 """ 60 try: 61 suffix = default 62 size = int(arg) 63 except ValueError: 64 try: 65 suffix = arg[-1:] 66 size = int(arg[:-1]) 67 except ValueError: 68 raise ArgumentTypeError("Invalid size: %r" % arg) 69 70 71 if size_in_bytes: 72 if suffix == 's' or suffix == 'S': 73 return size * 512 74 mult = 1024 75 else: 76 mult = 1 77 78 if suffix == "k" or suffix == "K": 79 return size * mult 80 if suffix == "M": 81 return size * mult * 1024 82 if suffix == "G": 83 return size * mult * 1024 * 1024 84 85 raise ArgumentTypeError("Invalid size: %r" % arg) 86 return f 87 88def overheadtype(arg): 89 """ 90 Custom type for ArgumentParser 91 Converts overhead string to float and checks if it's bigger than 1.0 92 """ 93 try: 94 result = float(arg) 95 except ValueError: 96 raise ArgumentTypeError("Invalid value: %r" % arg) 97 98 if result < 1.0: 99 raise ArgumentTypeError("Overhead factor should be > 1.0" % arg) 100 101 return result 102 103def cannedpathtype(arg): 104 """ 105 Custom type for ArgumentParser 106 Tries to find file in the list of canned wks paths 107 """ 108 scripts_path = os.path.abspath(os.path.dirname(__file__) + '../../..') 109 result = find_canned(scripts_path, arg) 110 if not result: 111 raise ArgumentTypeError("file not found: %s" % arg) 112 return result 113 114def systemidtype(arg): 115 """ 116 Custom type for ArgumentParser 117 Checks if the argument sutisfies system id requirements, 118 i.e. if it's one byte long integer > 0 119 """ 120 error = "Invalid system type: %s. must be hex "\ 121 "between 0x1 and 0xFF" % arg 122 try: 123 result = int(arg, 16) 124 except ValueError: 125 raise ArgumentTypeError(error) 126 127 if result <= 0 or result > 0xff: 128 raise ArgumentTypeError(error) 129 130 return arg 131 132class KickStart(): 133 """Kickstart parser implementation.""" 134 135 DEFAULT_EXTRA_SPACE = 10*1024 136 DEFAULT_OVERHEAD_FACTOR = 1.3 137 138 def __init__(self, confpath): 139 140 self.partitions = [] 141 self.bootloader = None 142 self.lineno = 0 143 self.partnum = 0 144 145 parser = KickStartParser() 146 subparsers = parser.add_subparsers() 147 148 part = subparsers.add_parser('part') 149 part.add_argument('mountpoint', nargs='?') 150 part.add_argument('--active', action='store_true') 151 part.add_argument('--align', type=int) 152 part.add_argument('--offset', type=sizetype("K", True)) 153 part.add_argument('--exclude-path', nargs='+') 154 part.add_argument('--include-path', nargs='+', action='append') 155 part.add_argument('--change-directory') 156 part.add_argument("--extra-space", type=sizetype("M")) 157 part.add_argument('--fsoptions', dest='fsopts') 158 part.add_argument('--fspassno', dest='fspassno') 159 part.add_argument('--fstype', default='vfat', 160 choices=('ext2', 'ext3', 'ext4', 'btrfs', 161 'squashfs', 'vfat', 'msdos', 'erofs', 162 'swap', 'none')) 163 part.add_argument('--mkfs-extraopts', default='') 164 part.add_argument('--label') 165 part.add_argument('--use-label', action='store_true') 166 part.add_argument('--no-table', action='store_true') 167 part.add_argument('--ondisk', '--ondrive', dest='disk', default='sda') 168 part.add_argument("--overhead-factor", type=overheadtype) 169 part.add_argument('--part-name') 170 part.add_argument('--part-type') 171 part.add_argument('--rootfs-dir') 172 part.add_argument('--type', default='primary', 173 choices = ('primary', 'logical')) 174 part.add_argument('--hidden', action='store_true') 175 176 # --size and --fixed-size cannot be specified together; options 177 # ----extra-space and --overhead-factor should also raise a parser 178 # --error, but since nesting mutually exclusive groups does not work, 179 # ----extra-space/--overhead-factor are handled later 180 sizeexcl = part.add_mutually_exclusive_group() 181 sizeexcl.add_argument('--size', type=sizetype("M"), default=0) 182 sizeexcl.add_argument('--fixed-size', type=sizetype("M"), default=0) 183 184 part.add_argument('--source') 185 part.add_argument('--sourceparams') 186 part.add_argument('--system-id', type=systemidtype) 187 part.add_argument('--use-uuid', action='store_true') 188 part.add_argument('--uuid') 189 part.add_argument('--fsuuid') 190 part.add_argument('--no-fstab-update', action='store_true') 191 part.add_argument('--mbr', action='store_true') 192 193 bootloader = subparsers.add_parser('bootloader') 194 bootloader.add_argument('--append') 195 bootloader.add_argument('--configfile') 196 bootloader.add_argument('--ptable', choices=('msdos', 'gpt', 'gpt-hybrid'), 197 default='msdos') 198 bootloader.add_argument('--timeout', type=int) 199 bootloader.add_argument('--source') 200 201 include = subparsers.add_parser('include') 202 include.add_argument('path', type=cannedpathtype) 203 204 self._parse(parser, confpath) 205 if not self.bootloader: 206 logger.warning('bootloader config not specified, using defaults\n') 207 self.bootloader = bootloader.parse_args([]) 208 209 def _parse(self, parser, confpath): 210 """ 211 Parse file in .wks format using provided parser. 212 """ 213 with open(confpath) as conf: 214 lineno = 0 215 for line in conf: 216 line = line.strip() 217 lineno += 1 218 if line and line[0] != '#': 219 line = expand_line(line) 220 try: 221 line_args = shlex.split(line) 222 parsed = parser.parse_args(line_args) 223 except ArgumentError as err: 224 raise KickStartError('%s:%d: %s' % \ 225 (confpath, lineno, err)) 226 if line.startswith('part'): 227 # SquashFS does not support filesystem UUID 228 if parsed.fstype == 'squashfs': 229 if parsed.fsuuid: 230 err = "%s:%d: SquashFS does not support UUID" \ 231 % (confpath, lineno) 232 raise KickStartError(err) 233 if parsed.label: 234 err = "%s:%d: SquashFS does not support LABEL" \ 235 % (confpath, lineno) 236 raise KickStartError(err) 237 # erofs does not support filesystem labels 238 if parsed.fstype == 'erofs' and parsed.label: 239 err = "%s:%d: erofs does not support LABEL" % (confpath, lineno) 240 raise KickStartError(err) 241 if parsed.fstype == 'msdos' or parsed.fstype == 'vfat': 242 if parsed.fsuuid: 243 if parsed.fsuuid.upper().startswith('0X'): 244 if len(parsed.fsuuid) > 10: 245 err = "%s:%d: fsuuid %s given in wks kickstart file " \ 246 "exceeds the length limit for %s filesystem. " \ 247 "It should be in the form of a 32 bit hexadecimal" \ 248 "number (for example, 0xABCD1234)." \ 249 % (confpath, lineno, parsed.fsuuid, parsed.fstype) 250 raise KickStartError(err) 251 elif len(parsed.fsuuid) > 8: 252 err = "%s:%d: fsuuid %s given in wks kickstart file " \ 253 "exceeds the length limit for %s filesystem. " \ 254 "It should be in the form of a 32 bit hexadecimal" \ 255 "number (for example, 0xABCD1234)." \ 256 % (confpath, lineno, parsed.fsuuid, parsed.fstype) 257 raise KickStartError(err) 258 if parsed.use_label and not parsed.label: 259 err = "%s:%d: Must set the label with --label" \ 260 % (confpath, lineno) 261 raise KickStartError(err) 262 # using ArgumentParser one cannot easily tell if option 263 # was passed as argument, if said option has a default 264 # value; --overhead-factor/--extra-space cannot be used 265 # with --fixed-size, so at least detect when these were 266 # passed with non-0 values ... 267 if parsed.fixed_size: 268 if parsed.overhead_factor or parsed.extra_space: 269 err = "%s:%d: arguments --overhead-factor and --extra-space not "\ 270 "allowed with argument --fixed-size" \ 271 % (confpath, lineno) 272 raise KickStartError(err) 273 else: 274 # ... and provide defaults if not using 275 # --fixed-size iff given option was not used 276 # (again, one cannot tell if option was passed but 277 # with value equal to 0) 278 if '--overhead-factor' not in line_args: 279 parsed.overhead_factor = self.DEFAULT_OVERHEAD_FACTOR 280 if '--extra-space' not in line_args: 281 parsed.extra_space = self.DEFAULT_EXTRA_SPACE 282 283 self.partnum += 1 284 self.partitions.append(Partition(parsed, self.partnum)) 285 elif line.startswith('include'): 286 self._parse(parser, parsed.path) 287 elif line.startswith('bootloader'): 288 if not self.bootloader: 289 self.bootloader = parsed 290 # Concatenate the strings set in APPEND 291 append_var = get_bitbake_var("APPEND") 292 if append_var: 293 self.bootloader.append = ' '.join(filter(None, \ 294 (self.bootloader.append, append_var))) 295 else: 296 err = "%s:%d: more than one bootloader specified" \ 297 % (confpath, lineno) 298 raise KickStartError(err) 299