Beispiel #1
0
    def test_removal_bootloader(self) -> None:
        td = Path(self.td.name)

        class MockBootloader(Bootloader):
            name = 'mock'

            def __call__(self) -> typing.Iterable[str]:
                yield str(td / 'does-not-exist')
                yield str(td / 'kernel.old')

        self.assertEqual(
            get_removal_list(self.kernels,
                             sorter=VersionSort(),
                             limit=1,
                             destructive=False,
                             bootloader=MockBootloader()), {
                                 self.kernels[2]: [
                                     'vmlinuz does not exist',
                                     'not referenced by bootloader (mock)'
                                 ],
                                 self.kernels[3]: [
                                     'vmlinuz does not exist',
                                     'not referenced by bootloader (mock)'
                                 ],
                             })
Beispiel #2
0
 def test_removal_destructive(self) -> None:
     self.assertEqual(
         get_removal_list(self.kernels,
                          sorter=VersionSort(),
                          limit=1,
                          destructive=True), {
                              self.kernels[2]: ['vmlinuz does not exist'],
                              self.kernels[3]: ['vmlinuz does not exist'],
                              self.kernels[0]: ['unwanted'],
                          })
Beispiel #3
0
 def test_removal_current_stray(self, uname: MagicMock) -> None:
     uname.return_value = ('Linux', 'localhost', '3.stray', '', 'x86_64')
     self.assertEqual(
         get_removal_list(self.kernels,
                          sorter=VersionSort(),
                          limit=1,
                          destructive=True), {
                              self.kernels[3]: ['vmlinuz does not exist'],
                              self.kernels[0]: ['unwanted'],
                          })
Beispiel #4
0
def main(argv: typing.List[str]) -> int:
    kernel_parts = [x.value for x in KernelFileType.__members__.values()]
    bootloaders: typing.List[typing.Type[Bootloader]] = [
        LILO, GRUB2, GRUB, Yaboot, Symlinks
    ]
    layouts: typing.List[typing.Type[Layout]] = [BlSpecLayout, StdLayout]
    sorts = [MTimeSort, VersionSort]

    argp = argparse.ArgumentParser(description=ecleankern_desc.strip())
    argp.add_argument('-V', '--version', action='version', version=__version__)

    group = argp.add_argument_group('action control')
    group.add_argument('-A',
                       '--ask',
                       action='store_true',
                       help='Ask before removing each kernel')
    group.add_argument('-l',
                       '--list-kernels',
                       action='store_true',
                       help='List kernel files and exit')
    group.add_argument('-p',
                       '--pretend',
                       action='store_true',
                       help='Print the list of kernels to be removed '
                       'and exit')

    group = argp.add_argument_group('system configuration')
    group.add_argument('-b',
                       '--bootloader',
                       default='auto',
                       help=f'Bootloader used (auto, '
                       f'{", ".join(b.name for b in bootloaders)})')
    group.add_argument('-L',
                       '--layout',
                       default='auto',
                       help=f'Layout used (auto, '
                       f'{", ".join(l.name for l in layouts)})')
    group.add_argument('-r',
                       '--root',
                       type=Path,
                       default=Path('/'),
                       help='Alternate filesystem root to use')

    group = argp.add_argument_group('kernel selection')
    group.add_argument('-a',
                       '--all',
                       action='store_true',
                       help='Remove all kernels unless used by bootloader')
    group.add_argument('-d',
                       '--destructive',
                       action='store_true',
                       help='Destructive mode: remove kernels even when '
                       'referenced by bootloader')
    group.add_argument('-n',
                       '--num',
                       type=int,
                       default=0,
                       help='Leave only newest NUM kernels (see also: '
                       '--sort-order)')
    group.add_argument('-s',
                       '--sort-order',
                       default='version',
                       help=f'Kernel sort order ('
                       f'{", ".join(s.name for s in sorts)}); '
                       f'default: version')

    group = argp.add_argument_group('misc options')
    group.add_argument('-D',
                       '--debug',
                       action='store_true',
                       help='Enable debugging output')
    group.add_argument('-M',
                       '--no-mount',
                       action='store_false',
                       help='Disable (re-)mounting /boot if necessary')
    group.add_argument('--no-bootloader-update',
                       action='store_true',
                       help='Do not update bootloader configuration '
                       'after removing kernels (if supported '
                       'by the bootloader')
    group.add_argument('--no-kernel-install',
                       action='store_true',
                       help='Do not call kernel-install while removing '
                       'kernels (if installed)')
    group.add_argument('-x',
                       '--exclude',
                       default='',
                       help=f'Exclude kernel parts from being removed '
                       f'(comma-separated, supported parts: '
                       f'{", ".join(kernel_parts)})')

    all_args = []
    config_dirs = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':')
    config_dirs.insert(0, os.environ.get('XDG_CONFIG_HOME', '~/.config'))
    for x in reversed(config_dirs):
        try:
            with open(Path(os.path.expanduser(x)) / 'eclean-kernel.rc',
                      'r') as f:
                all_args.extend(shlex.split(f.read(), comments=True))
        except FileNotFoundError:
            pass
        except NotADirectoryError:
            # XDG_CONFIG_* does not have to be correct
            pass

    all_args.extend(argv)
    args = argp.parse_args(all_args)

    exclusions = []
    for x in frozenset(args.exclude.split(',')):
        if not x:
            continue
        elif x not in kernel_parts:
            argp.error(f'Invalid kernel part: {x}')
        elif x == 'vmlinuz':
            argp.error('Kernel exclusion unsupported')
        exclusions.append(KernelFileType(x))

    if args.debug:
        logging.getLogger().setLevel(logging.DEBUG)
    else:
        logging.getLogger().setLevel(logging.INFO)

    for layout_cls in layouts:
        if args.layout in ('auto', layout_cls.name):
            try:
                layout = layout_cls(root=args.root)
                break
            except LayoutNotFound as e:
                logging.debug(f'Layout failed: {layout_cls}; '
                              f'exception: {e}')
    else:
        # auto should never fail -- std always succeeds
        assert args.layout != 'auto'
        argp.error(f'Invalid layout: {args.layout}')
    logging.debug(f'Layout: {layout}')

    bootloader: typing.Optional[Bootloader] = None
    for bootloader_cls in bootloaders:
        if args.bootloader == 'auto':
            try:
                bootloader = bootloader_cls()
                break
            except BootloaderNotFound:
                logging.debug(f'Bootloader failed: {bootloader_cls}')
        elif args.bootloader == bootloader_cls.name:
            bootloader = bootloader_cls()
            break
    logging.debug(f'Bootloader: {bootloader}')

    for sort_cls in sorts:
        if args.sort_order == sort_cls.name:
            break
    else:
        argp.error(f'Invalid sort order: {args.sort}')
    sorter = sort_cls()
    logging.debug(f'Sorter: {sorter}')

    bootfs = DummyMount()
    try:
        import pymountboot
    except ImportError:
        logging.debug('unable to import pymountboot, /boot mounting disabled.')
    else:
        if not args.no_mount:
            bootfs = pymountboot.BootMountpoint()

    try:
        try:
            bootfs.mount()
        except RuntimeError:
            raise MountError()

        try:
            kernels = layout.find_kernels(exclusions=exclusions)

            if args.list_kernels:
                ordered = sorted(kernels, key=sorter.key, reverse=True)
                for k in ordered:
                    print(f'{k.version} [{k.real_kv}]')
                    for kf in sorted(k.all_files, key=lambda f: f.path):
                        print(f'- {kf.ftype.value}: {kf.path}')
                    ts = time.strftime("%Y-%m-%d %H:%M:%S",
                                       time.gmtime(k.mtime))
                    print(f'- last modified: {ts}')
                return 0

            removals = get_removal_list(kernels,
                                        limit=None if args.all else args.num,
                                        sorter=sorter,
                                        bootloader=bootloader,
                                        destructive=args.destructive)

            has_kernel_install = False
            has_bootloader_postrm = False
            if args.root == Path('/'):
                if not args.no_kernel_install:
                    try:
                        (subprocess.Popen(
                            ['kernel-install', '--help'],
                            stdout=subprocess.PIPE,
                            stderr=subprocess.PIPE).communicate())
                        has_kernel_install = True
                    except FileNotFoundError:
                        pass

                if (not args.no_bootloader_update and bootloader is not None
                        and bootloader.has_postrm()):
                    has_bootloader_postrm = True

            if not removals:
                print('No outdated kernels found.')
                return 0

            print('Legend:')
            print('[-] file being removed')
            if not args.pretend:
                print('[x] file does not exist (anymore)')
            print('[+] file being kept (used by other kernels')
            print()

            if args.pretend:
                print('These are the kernels which would be removed:')

                file_removals = list(get_removable_files(removals, kernels))

                for k, reason, files in file_removals:
                    print(f'- {k.version}: {", ".join(reason)}')
                    for kf in k.all_files:
                        if kf.path in files:
                            sign = '-'
                        else:
                            sign = '+'
                        print(f' [{sign}] {kf.path}')
                if has_kernel_install:
                    print('kernel-install will be called to perform '
                          'prerm tasks.')
                if has_bootloader_postrm:
                    assert bootloader is not None
                    print(f'Bootloader {bootloader.name} config will '
                          f'be updated.')
            else:
                bootfs.rwmount()
                for k in removals:
                    k.check_writable()

                nremoved = 0

                for k, reason in list(removals.items()):
                    while args.ask:
                        ans = input(
                            f'Remove {k.version} '
                            f'({", ".join(reason)})? [Yes/No]').lower()
                        if 'yes'.startswith(ans):
                            break
                        elif 'no'.startswith(ans):
                            del removals[k]
                            break
                        else:
                            print(f'Unknown answer ({ans}).')

                file_removals = list(get_removable_files(removals, kernels))

                for k, reason, files in file_removals:
                    print(f'* Removing kernel {k.version} '
                          f'({", ".join(reason)})')

                    if has_kernel_install:
                        cmd = ['kernel-install', 'remove']
                        for kf in k.all_files:
                            if isinstance(kf, KernelImage):
                                scmd = cmd + [
                                    kf.internal_version,
                                    str(kf.path)
                                ]
                                p = subprocess.Popen(scmd)
                                if p.wait() != 0:
                                    print(f'* kernel-install exited '
                                          f'with {p.returncode} status')

                    for kf in k.all_files:
                        if kf.path in files:
                            sign = '-'
                            if kf.path in files:
                                try:
                                    sign = '-' if kf.remove() else '+'
                                except FileNotFoundError:
                                    sign = 'x'
                        else:
                            sign = '+'
                        print(f' [{sign}] {kf.path}')
                    nremoved += 1

                if nremoved:
                    print(f'Removed {nremoved} kernels')
                    if has_bootloader_postrm:
                        assert bootloader is not None
                        bootloader.postrm()

            return 0
        finally:
            try:
                bootfs.umount()
            except RuntimeError:
                print('Note: unmounting /boot failed')
        return 0
    except Exception as e:
        if args.debug:
            raise
        print('eclean-kernel has met the following issue:\n')

        if hasattr(e, 'friendly_desc'):
            print(getattr(e, 'friendly_desc'))
        else:
            print(f'  {e!r}')

        print('''
If you believe that the mentioned issue is a bug, please report it
to https://github.com/mgorny/eclean-kernel/issues. If possible,
please attach the output of 'eclean-kernel --list-kernels' and your
regular eclean-kernel call with additional '--debug' argument.''')
        return 1
Beispiel #5
0
 def test_removal_no_limit(self) -> None:
     self.assertEqual(
         get_removal_list(self.kernels, sorter=VersionSort(), limit=0), {
             self.kernels[2]: ['vmlinuz does not exist'],
             self.kernels[3]: ['vmlinuz does not exist'],
         })
Beispiel #6
0
 def test_removal_no_bootloader(self) -> None:
     with self.assertRaises(SystemError):
         get_removal_list(self.kernels,
                          sorter=VersionSort(),
                          limit=1,
                          destructive=False)