def __init__(self, disk_img, mount_ok=None): """ disk_img is the full path to a partitioned disk image mount_ok is a function that is passed the mount point and returns True if it should be mounted. """ self.mount_dev = None self.mount_size = None self.mount_dir = None self.disk_img = disk_img self.mount_ok = mount_ok # Default is to mount partition with /etc/passwd if not self.mount_ok: self.mount_ok = lambda mount_dir: os.path.isfile(mount_dir+"/etc/passwd") # Example kpartx output # kpartx -p p -v -a /tmp/diskV2DiCW.im # add map loop2p1 (253:2): 0 3481600 linear /dev/loop2 2048 # add map loop2p2 (253:3): 0 614400 linear /dev/loop2 3483648 kpartx_output = runcmd_output(["kpartx", "-v", "-a", "-s", self.disk_img]) logger.debug(kpartx_output) # list of (deviceName, sizeInBytes) self.loop_devices = [] for line in kpartx_output.splitlines(): # add map loop2p3 (253:4): 0 7139328 linear /dev/loop2 528384 # 3rd element is size in 512 byte blocks if line.startswith("add map "): fields = line[8:].split() self.loop_devices.append( (fields[0], int(fields[3])*512) )
def loop_attach(outfile): '''Attach a loop device to the given file. Return the loop device name. Raises CalledProcessError if losetup fails.''' dev = runcmd_output(["losetup", "--find", "--show", outfile]) # Sometimes the loop device isn't ready yet, make extra sure before returning loop_waitfor(dev.strip(), outfile) return dev.strip()
def get_loop_name(path): '''Return the loop device associated with the path. Raises RuntimeError if more than one loop is associated''' buf = runcmd_output(["losetup", "-j", path]) if len(buf.splitlines()) > 1: # there should never be more than one loop device listed raise RuntimeError("multiple loops associated with %s" % path) name = os.path.basename(buf.split(":")[0]) return name
def __init__(self, disk_img, mount_ok=None, submount=None): """ :param str disk_img: The full path to a partitioned disk image :param mount_ok: A function that is passed the mount point and returns True if it should be mounted. :param str submount: Directory inside mount_dir to mount at If mount_ok is not set it will look for /etc/passwd If the partition is found it will be mounted under a temporary directory and self.temp_dir set to it. If submount is passed it will be created and mounted there instead, with self.mount_dir set to point to it. self.mount_dev is set to the loop device, and self.mount_size is set to the size of the partition. When no subdir is passed self.temp_dir and self.mount_dir will be the same. """ self.mount_dev = None self.mount_size = None self.mount_dir = None self.disk_img = disk_img self.mount_ok = mount_ok self.submount = submount self.temp_dir = None # Default is to mount partition with /etc/passwd if not self.mount_ok: self.mount_ok = lambda mount_dir: os.path.isfile(mount_dir+"/etc/passwd") # Example kpartx output # kpartx -p p -v -a /tmp/diskV2DiCW.im # add map loop2p1 (253:2): 0 3481600 linear /dev/loop2 2048 # add map loop2p2 (253:3): 0 614400 linear /dev/loop2 3483648 kpartx_output = runcmd_output(["kpartx", "-v", "-a", "-s", self.disk_img]) logger.debug(kpartx_output) # list of (deviceName, sizeInBytes) self.loop_devices = [] for line in kpartx_output.splitlines(): # add map loop2p3 (253:4): 0 7139328 linear /dev/loop2 528384 # 3rd element is size in 512 byte blocks if line.startswith("add map "): fields = line[8:].split() self.loop_devices.append( (fields[0], int(fields[3])*512) )
def runcmd(self, *cmdlist): ''' runcmd CMD [--chdir=DIR] [ARG ...] Run the given command with the given arguments. If "--chdir=DIR" is given, change to the named directory before executing the command. NOTE: All paths given MUST be COMPLETE, ABSOLUTE PATHS to the file or files mentioned. ${root}/${inroot}/${outroot} are good for constructing these paths. FURTHER NOTE: Please use this command only as a last resort! Whenever possible, you should use the existing template commands. If the existing commands don't do what you need, fix them! Examples: (this should be replaced with a "find" function) runcmd find ${root} -name "*.pyo" -type f -delete %for f in find(root, name="*.pyo"): remove ${f} %endfor ''' cwd = None cmd = cmdlist logger.debug('running command: %s', cmd) if cmd[0].startswith("--chdir="): cwd = cmd[0].split('=',1)[1] cmd = cmd[1:] try: output = runcmd_output(cmd, cwd=cwd) if output: logger.debug('command output:\n%s', output) logger.debug("command finished successfully") except CalledProcessError as e: if e.output: logger.debug('command output:\n%s', e.output) logger.debug('command returned failure (%d)', e.returncode) raise
def kpartx_disk_img(disk_img): """Attach a disk image's partitions to /dev/loopX using kpartx :param disk_img: The full path to a partitioned disk image :type disk_img: str :returns: list of (loopXpN, size) :rtype: list of tuples """ # Example kpartx output # kpartx -p p -v -a /tmp/diskV2DiCW.im # add map loop2p1 (253:2): 0 3481600 linear /dev/loop2 2048 # add map loop2p2 (253:3): 0 614400 linear /dev/loop2 3483648 kpartx_output = runcmd_output(["kpartx", "-v", "-a", "-s", disk_img]) logger.debug(kpartx_output) # list of (deviceName, sizeInBytes) loop_devices = [] for line in kpartx_output.splitlines(): # add map loop2p3 (253:4): 0 7139328 linear /dev/loop2 528384 # 3rd element is size in 512 byte blocks if line.startswith("add map "): fields = line[8:].split() loop_devices.append((fields[0], int(fields[3]) * 512)) return loop_devices
def runcmd(self, *cmdlist): ''' runcmd CMD [ARG ...] Run the given command with the given arguments. NOTE: All paths given MUST be COMPLETE, ABSOLUTE PATHS to the file or files mentioned. ${root}/${inroot}/${outroot} are good for constructing these paths. FURTHER NOTE: Please use this command only as a last resort! Whenever possible, you should use the existing template commands. If the existing commands don't do what you need, fix them! Examples: (this should be replaced with a "find" function) runcmd find ${root} -name "*.pyo" -type f -delete %for f in find(root, name="*.pyo"): remove ${f} %endfor ''' cmd = cmdlist logger.debug('running command: %s', cmd) if cmd[0].startswith("--chdir="): logger.error("--chdir is no longer supported for runcmd.") raise ValueError("--chdir is no longer supported for runcmd.") try: stdout = runcmd_output(cmd) if stdout: logger.debug('command output:\n%s', stdout) logger.debug("command finished successfully") except CalledProcessError as e: if e.output: logger.error('command output:\n%s', e.output) logger.error('command returned failure (%d)', e.returncode) raise
def loop_attach(outfile): '''Attach a loop device to the given file. Return the loop device name. Raises CalledProcessError if losetup fails.''' dev = runcmd_output(["losetup", "--find", "--show", outfile]) return dev.strip()
def module_desc(mod): output = runcmd_output(["modinfo", "-F", "description", mod]) return output.strip()
def test_runcmd_output(self): cmd = ["python3", "-c", "import sys; print('Everyone needs Thneeds'); sys.exit(0)"] stdout = runcmd_output(cmd) self.assertEqual(stdout.strip(), "Everyone needs Thneeds")
def run(self, dbo, product, version, release, variant="", bugurl="", isfinal=False, workdir=None, outputdir=None, buildarch=None, volid=None, domacboot=True, doupgrade=True, remove_temp=False, installpkgs=None, size=2, add_templates=None, add_template_vars=None, add_arch_templates=None, add_arch_template_vars=None): assert self._configured installpkgs = installpkgs or [] # get lorax version try: import pylorax.version except ImportError: vernum = "devel" else: vernum = pylorax.version.num if domacboot: try: runcmd(["rpm", "-q", "hfsplus-tools"]) except CalledProcessError: logger.critical("you need to install hfsplus-tools to create mac images") sys.exit(1) # set up work directory self.workdir = workdir or tempfile.mkdtemp(prefix="pylorax.work.") if not os.path.isdir(self.workdir): os.makedirs(self.workdir) # set up log directory logdir = self.conf.get("lorax", "logdir") if not os.path.isdir(logdir): os.makedirs(logdir) self.init_stream_logging() self.init_file_logging(logdir) logger.debug("version is %s", vernum) logger.debug("using work directory %s", self.workdir) logger.debug("using log directory %s", logdir) # set up output directory self.outputdir = outputdir or tempfile.mkdtemp(prefix="pylorax.out.") if not os.path.isdir(self.outputdir): os.makedirs(self.outputdir) logger.debug("using output directory %s", self.outputdir) # do we have root privileges? logger.info("checking for root privileges") if not os.geteuid() == 0: logger.critical("no root privileges") sys.exit(1) # is selinux disabled? # With selinux in enforcing mode the rpcbind package required for # dracut nfs module, which is in turn required by anaconda module, # will not get installed, because it's preinstall scriptlet fails, # resulting in an incomplete initial ramdisk image. # The reason is that the scriptlet runs tools from the shadow-utils # package in chroot, particularly groupadd and useradd to add the # required rpc group and rpc user. This operation fails, because # the selinux context on files in the chroot, that the shadow-utils # tools need to access (/etc/group, /etc/passwd, /etc/shadow etc.), # is wrong and selinux therefore disallows access to these files. logger.info("checking the selinux mode") if selinux.is_selinux_enabled() and selinux.security_getenforce(): logger.critical("selinux must be disabled or in Permissive mode") sys.exit(1) # do we have a proper dnf base object? logger.info("checking dnf base object") if not isinstance(dbo, dnf.Base): logger.critical("no dnf base object") sys.exit(1) self.inroot = dbo.conf.installroot logger.debug("using install root: %s", self.inroot) if not buildarch: buildarch = get_buildarch(dbo) logger.info("setting up build architecture") self.arch = ArchData(buildarch) for attr in ('buildarch', 'basearch', 'libdir'): logger.debug("self.arch.%s = %s", attr, getattr(self.arch,attr)) logger.info("setting up build parameters") self.product = DataHolder(name=product, version=version, release=release, variant=variant, bugurl=bugurl, isfinal=isfinal) logger.debug("product data: %s", self.product) # NOTE: if you change isolabel, you need to change pungi to match, or # the pungi images won't boot. isolabel = volid or "%s-%s-%s" % (self.product.name, self.product.version, self.arch.basearch) if len(isolabel) > 32: logger.fatal("the volume id cannot be longer than 32 characters") sys.exit(1) templatedir = self.conf.get("lorax", "sharedir") # NOTE: rb.root = dbo.conf.installroot (== self.inroot) rb = RuntimeBuilder(product=self.product, arch=self.arch, dbo=dbo, templatedir=templatedir, installpkgs=installpkgs, add_templates=add_templates, add_template_vars=add_template_vars) logger.info("installing runtime packages") rb.install() # write .buildstamp buildstamp = BuildStamp(self.product.name, self.product.version, self.product.bugurl, self.product.isfinal, self.arch.buildarch) buildstamp.write(joinpaths(self.inroot, ".buildstamp")) if self.debug: rb.writepkglists(joinpaths(logdir, "pkglists")) rb.writepkgsizes(joinpaths(logdir, "original-pkgsizes.txt")) logger.info("doing post-install configuration") rb.postinstall() # write .discinfo discinfo = DiscInfo(self.product.release, self.arch.basearch) discinfo.write(joinpaths(self.outputdir, ".discinfo")) logger.info("backing up installroot") installroot = joinpaths(self.workdir, "installroot") linktree(self.inroot, installroot) logger.info("generating kernel module metadata") rb.generate_module_data() logger.info("cleaning unneeded files") rb.cleanup() if self.debug: rb.writepkgsizes(joinpaths(logdir, "final-pkgsizes.txt")) logger.info("creating the runtime image") runtime = "images/install.img" compression = self.conf.get("compression", "type") compressargs = self.conf.get("compression", "args").split() # pylint: disable=no-member if self.conf.getboolean("compression", "bcj"): if self.arch.bcj: compressargs += ["-Xbcj", self.arch.bcj] else: logger.info("no BCJ filter for arch %s", self.arch.basearch) rb.create_runtime(joinpaths(installroot,runtime), compression=compression, compressargs=compressargs, size=size) rb.finished() logger.info("preparing to build output tree and boot images") treebuilder = TreeBuilder(product=self.product, arch=self.arch, inroot=installroot, outroot=self.outputdir, runtime=runtime, isolabel=isolabel, domacboot=domacboot, doupgrade=doupgrade, templatedir=templatedir, add_templates=add_arch_templates, add_template_vars=add_arch_template_vars, workdir=self.workdir) logger.info("rebuilding initramfs images") dracut_args = ["--xz", "--install", "/.buildstamp"] anaconda_args = dracut_args + ["--add", "anaconda pollcdrom"] # ppc64 cannot boot an initrd > 32MiB so remove some drivers if self.arch.basearch in ("ppc64", "ppc64le"): dracut_args.extend(["--omit-drivers", REMOVE_PPC64_DRIVERS]) # Only omit dracut modules from the initrd so that they're kept for # upgrade.img anaconda_args.extend(["--omit", REMOVE_PPC64_MODULES]) treebuilder.rebuild_initrds(add_args=anaconda_args) if doupgrade: # Build upgrade.img. It'd be nice if these could coexist in the same # image, but that would increase the size of the anaconda initramfs, # which worries some people (esp. PPC tftpboot). So they're separate. try: # If possible, use the 'fedup' plymouth theme themes = runcmd_output(['plymouth-set-default-theme', '--list'], root=installroot) if 'fedup' in themes.splitlines(): os.environ['PLYMOUTH_THEME_NAME'] = 'fedup' except RuntimeError: pass upgrade_args = dracut_args + ["--add", "system-upgrade"] treebuilder.rebuild_initrds(add_args=upgrade_args, prefix="upgrade") logger.info("populating output tree and building boot images") treebuilder.build() # write .treeinfo file and we're done treeinfo = TreeInfo(self.product.name, self.product.version, self.product.variant, self.arch.basearch) for section, data in treebuilder.treeinfo_data.items(): treeinfo.add_section(section, data) treeinfo.write(joinpaths(self.outputdir, ".treeinfo")) # cleanup if remove_temp: remove(self.workdir)