From 5acf3fa07bec8ae8475899379937110d41eaffcc Mon Sep 17 00:00:00 2001 From: Packit Date: Aug 31 2020 13:37:43 +0000 Subject: boom-boot-1.0 base --- diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e7e8241 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +*.pyc +*.swp +*.swo +*.egg-info +*.log +*.orig +.coverage +build/ +dist/ +_build/ +__pycache__ +doc/html +doc/doctrees diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ece38a8 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,11 @@ +language: python +python: + - "2.7" + - "3.6" + - "3.7" +install: + - "pip install -r requirements.txt" + - "python setup.py install" +script: + - "pycodestyle boom" + - "nosetests -v --with-cover --cover-package=boom" diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..e54f9f4 --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..06a542d --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,8 @@ +include COPYING +include README.md +recursive-include etc * +recursive-include doc * +recursive-include tests/boom * +recursive-include tests/loader * +recursive-include examples * +recursive-include man/ * diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e55479 --- /dev/null +++ b/README.md @@ -0,0 +1,711 @@ +# Boom + +Boom is a *boot manager* for Linux systems using boot loaders that +support the [BootLoader Specification][0] for boot entry configuration. +It is based on the boot manager design discussed in the +[Boot-to-snapshot design v0.6][1] document. + +Boom requires a BLS compatible boot loader to function: either the +`systemd-boot` project, or `Grub2` with the `bls` patch (*Red Hat* +Grub2 builds include this support in both *Red Hat Enterprise Linux 7* +and *Fedora*). + +Boom allows for flexible boot configuration and simplifies the +creation of new or modified boot entries: for example to boot +snapshot images of the system created using LVM2 or BTRFS. + +Boom does not modify the existing boot loader configuration (other +than to insert the additional entries managed by boom - see +[Grub2 Integration](#grub2-integration)): the existing boot +configuration is maintained and any distribution integration (e.g. +kernel installation and update scripts) will continue to function as +before. + + + * [Boom](#boom) + * [Reporting bugs](#reporting-bugs) + * [Mailing list](#mailing-list) + * [Building and installing Boom](#building-and-installing-boom) + * [Building an RPM package](#building-an-rpm-package) + * [The boom command](#the-boom-command) + * [Operating System Profiles and Boot Entries](#operating-system-profiles-and-boot-entries) + * [OsProfile](#osprofile) + * [OsProfile templates](#osprofile-templates) + * [BootEntry](#bootentry) + * [Boom subcommands](#boom-subcommands) + * [create](#create) + * [delete](#delete) + * [clone](#clone) + * [show](#show) + * [list](#list) + * [edit](#edit) + * [Reporting commands](#reporting-commands) + * [Getting help](#getting-help) + * [Configuring Boom](#configuring-boom) + * [Creating an OsProfile](#creating-an-osprofile) + * [Creating a BootEntry](#creating-a-bootentry) + * [Grub2 Integration](#grub2-integration) + * [Submenu support](#submenu-support) + * [Python API](#python-api) + * [Command API](#command-api) + * [Object API](#object-api) + * [Patches and pull requests](#patches-and-pull-requests) + * [Documentation](#documentation) + +Boom aims to be a simple and extensible, and to be able to create boot +configurations for a wide range of Linux system configurations and boot +parameters. + +This project is hosted at: + + * http://github.com/bmr-cymru/boom + +For the latest version, to contribute, and for more information, please visit +the project pages or join the mailing list. + +To clone the current master (development) branch run: + +``` +git clone git://github.com/bmr-cymru/boom.git +``` +## Reporting bugs + +Please report bugs via the mailing list or by opening an issue in the [GitHub +Issue Tracker][2] + +## Mailing list + +The [dm-devel][3] is the mailing list for any boom-related questions and +discussion. Patch submissions and reviews are welcome too. + +## Building and installing Boom + +A `setuptools` based build script is provided: local installations and +package builds can be performed by running `python setup.py` and a +setup command. See `python setup.py --help` for detailed information on +the available options and commands. + +### Builds and packages +Binary packages for Fedora and Red Hat Enterprise Linux are available +from the [copr repository][9]. These builds use the RPM spec file +distributed in the git repository and include all the necessary +library modules, binaries, and configuration files needed to install +and use boom. + +To enable the repository on Fedora, run: + +``` +# dnf copr enable bmr/boom +``` + +The python2 and python3 versions of boom may be installed by running: + +``` +# dnf -y install python2-boom python3-boom +``` + +Note that although both python 2 and 3 versions of the library are +provided only one package contains the `boom` binary, depending on +the system default python runtime for that distribution version. + +## The boom command + +The `boom` command is the main interface to the boom boot manager. +It is able to create, delete, edit and display boot entries, +operating system and host profiles and provides reports showing the +available profiles and entries, and their configurations. + +Boom commands normally operate on a particular object type: a boot +entry, a host profile or an OS profile. Commands are also provided +to manipulate legacy boot loader configurations (for systems that +do not natively support the BLS standard). + +``` +# boom [entry] # `BootEntry` command +``` + +``` +# boom profile # `OsProfile` command +``` + +``` +# boom hostprofile # `HostProfile` command +``` + +``` +# boom legacy # Legacy boot loader commands +``` + +If no command type is given `entry` is assumed. + +### Profiles and Boot Entries + +The two main object types in boom are the `Profile` and `BootEntry`. +Profiles support tailoring boot entry configuration to either a +specific operating system distribution (`OsProfile`), or a specific +installation (`HostProfile`, based on the system `machine-id`). + +Boom stores boot loader entries (`BootEntry`) in the system BLS loader +directory - normally `/boot/loader/entries`. + +Boom `OsProfile` files are stored in the boom configuration directory, +`/boot/boom/profiles` and `HostProfile` data is found in +`/boot/boom/hosts`. + +The location of the boot file system may be overridden using the +`--boot-dir` command line option and the location of both the boot +file system and boom configuration directory may be overridden by +calling the `boom.set_boot_path()` and `boom.set_boom_path()` +functions. + +These options are primarily of use for testing, or for working with +boom data from a system other than the running host. + +Boom configuration data is stored in the `/boot` file system to permit +the tool to be run from any booted instance of any installed operating +system. + +#### OsProfile + +An `OsProfile` stores identity information and templates used to write +bootloader configurations for an instance of an operating system. The +identity is based on values from the `/etc/os-release` file, and the +available templates allow customisation of the kernel and initramfs +images, kernel options and other properties required to boot an instance +of that OS. + +A set of `OsProfile` files can be pre-installed with boom, or generated +using the command line tool. + +An `OsProfile` is uniquely identified by its *OS Identifier*, or +*os_id*, a SHA2 hash computed on the `OsProfile` identity fields. +All SHA identifiers are displayed by default using the minimum width +necessary to ensure uniqueness: all command line arguments accepting +an identifier also accept any unique prefix of a valid identifier. + +##### OsProfile templates + +The template properties of an `OsProfile` (kernel pattern, initramfs +pattern, LVM2 and BTRFS root options and kernel command line options) +may include format strings that are expanded when creating a new +``BootEntry``. + +The available keys are: + + * `%{version}` - the kernel version + * `%{lvm_root_lv}` - the LVM2 logical volume containing the root file + system in `vg/lv` notation. + * `%{btrfs_subvol_id}` - the BTRFS subvolume identifier to use. + * `%{btrfs_subvol_path}` - the BTRFS subvolume path to use. + * `%{root_device}` - The system root device, relative to `/`. + * `%{options}` - Kernel command line options, including the root + device specification and options. + +Default template values are supplied when creating a new `OsProfile`; +these can be overridden by specifying alternate values on the command +line. The defaults are suitable for most Linux operating systems but +can be customised to allow for particular OS requirements, or to set +custom behaviours. + +#### HostProfile + +A `HostProfile` provides an additional means to customise the boot +configuration on a per-installation basis. Use of host profiles is +optional: if no `HostProfile` exits for a given host then the +default values from the corresponding `OsProfile` are used. + +Values specified in a Boom `HostProfile` are automatically applied +whenever a boot entry for the corresponding `machine-id` is created +or edited. Multiple Boom `HostProfile` templates can be defined for +a given system and distinguished by a *label*: for example +'production', 'debug' or other profile labels used to identify +and group commonly-used sets of boot options. + +Host profiles can be used to add or remove kernel command line +options, or to modify existing template values provided by the +`OsProfile` (including the location and naming of the kernel, +initramfs and other boot images). This can be used to automatically +apply settings where required, for example adding `nomodeset` or +other kernel command line parameters if required for that +installation, or modifying the command line to enable or disable +debugging, logging or storage activation options. + +Like the `OsProfile`, a `HostProfile` is uniquely identified by +a `HostId` identifier. + +#### BootEntry + +A `BootEntry` is an individual bootloader entry for one instance of an +operating system. It includes all the parameters required for the +boot loader to load the OS, and for the kernel and user space to +boot the environment (including configuration of LVM2 logical volumes +and BTRFS subvolumes). + +The `BootEntry` stored on-disk is generated from the templates stored +in an associated `OsProfile` and boot parameters configuration provided +by command line arguments. + +Boom uses BLS[0] notation as the canonical format for the boot entry +store. + +An `BootEntry` is uniquely identified by its *Boot Identifier*, or +*boot_id*, a SHA2 hash computed on the `BootEntry` boot parameter +fields (note that this means that changing the parameters of an +existing `BootEntry` will also change its `boot_id`. All SHA +identifiers are displayed by default using the minimum width +necessary to ensure uniqueness: all command line arguments +accepting an identifier also accept any unique prefix of a valid +identifier. + +### Boom subcommands + +For both profile and boot entry command types, boom provides six +subcommands: + + * `create` + * `delete --profile OS_ID | --host-profile HOST_ID | --boot-id BOOT_ID [...]` + * `clone --profile OS_ID | --host-profile HOST_ID | --boot-id BOOT_ID [...]` + * `show` + * `list` + * `edit` + +#### create + +Create a new `OsProfile` `HostProfile`, or `BootEntry` using the +values entered on the command line. + +#### delete + +Delete the specified profile or BootEntry. + +#### clone + +Create a new profile or `BootEntry` by cloning an existing object +and modifying its properties. A `boot_id`, `os_id` or `host_id` +must be used to select the object to clone. Any remaining command +line options modify the newly created object. + +#### show + +Display the specified objects in human readable format. + +#### list + +List objects matching selection criteria as a tabular report. + +#### edit + +Modify an existing profile or `BootEntry` by changing one or more +of its attributes. + +It is not possible to change the name, short name, version, or +version identifier of an `OsProfile` using this command, since these +fields form the `OsProfile` identifier: to modify one of these +fields use the `clone` command to create a new profile specifying +the attribute to be changed. + +When editing a BootEntry, the `boot_id` will change: this is +because the options that define an entry form the entry's identity. +The new `boot_id` is written to the terminal on success. + +### Reporting commands + +The `boom entry list` and `boom host|profile list` commands generate +a tabular report as output. To control the list of displayed fields +use the `-o/--options FIELDS` argument: + +``` +boom list -oboot_id,version +BootId Version +fb3286f 3.10-1.el7.fc24.x86_64 +1031ab0 3.10-23.el7 +a559d3a 2.6.32-232.el6 +a559d3a 2.6.32-232.el6 +2c89556 2.2.2-2.fc24.x86_64 +e79db6a 1.1.1-1.fc24.x86_64 +d85f2c3 3.10.1-1.el7 +2fc3f4f 4.1.1-100.fc24 +d85f2c3 3.10.1-1.el7 +``` + +To add extra fields to the default selection, prefix the field list +with the `+` character: + +``` +boom list -o+kernel,initramfs +BootID Version OsID Name OsVersion Kernel Initramfs +fb3286f 3.10-1.el7.fc24.x86_64 3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) /boot/vmlinuz-3.10-1.el7.fc24.x86_64 /boot/initramfs-3.10-1.el7.fc24.x86_64.img +1031ab0 3.10-23.el7 3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) /boot/vmlinuz-3.10-23.el7 /boot/initramfs-3.10-23.el7.img +a559d3a 2.6.32-232.el6 98c3edb Red Hat Enterprise Linux Server 6 (Server) /boot/kernel-2.6.32-232.el6 /boot/initramfs-2.6.32-232.el6.img +a559d3a 2.6.32-232.el6 98c3edb Red Hat Enterprise Linux Server 6 (Server) /boot/kernel-2.6.32-232.el6 /boot/initramfs-2.6.32-232.el6.img +d85f2c3 3.10.1-1.el7 3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) /boot/vmlinuz-3.10.1-1.el7 /boot/initramfs-3.10.1-1.el7.img +d85f2c3 3.10.1-1.el7 3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) /boot/vmlinuz-3.10.1-1.el7 /boot/initramfs-3.10.1-1.el7.img +e19586b 7.7.7 3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) /boot/vmlinuz-7.7.7 /boot/initramfs-7.7.7.img +``` + +To display the available fields for either report use the field +name `help`. + +`BootEntry` fields: +``` +boom list -o help +Boot loader entries Fields +-------------------------- + bootid - Boot identifier [sha] + title - Entry title [str] + options - Kernel options [str] + kernel - Kernel image [str] + initramfs - Initramfs image [str] + machineid - Machine identifier [sha] + +OS profiles Fields +------------------ + osid - OS identifier [sha] + osname - OS name [str] + osshortname - OS short name [str] + osversion - OS version [str] + osversion_id - Version identifier [str] + unamepattern - UTS name pattern [str] + kernelpattern - Kernel image pattern [str] + initrdpattern - Initrd pattern [str] + lvm2opts - LVM2 options [str] + btrfsopts - BTRFS options [str] + options - Kernel options [str] + +Boot parameters Fields +---------------------- + version - Kernel version [str] + rootdev - Root device [str] + rootlv - Root logical volume [str] + subvolpath - BTRFS subvolume path [str] + subvolid - BTRFS subvolume ID [num] +``` + +`OsProfile` fields: +``` +boom profile list -o help +OS profiles Fields +------------------ + osid - OS identifier [sha] + osname - OS name [str] + osshortname - OS short name [str] + osversion - OS version [str] + osversion_id - Version identifier [str] + unamepattern - UTS name pattern [str] + kernelpattern - Kernel image pattern [str] + initrdpattern - Initrd pattern [str] + lvm2opts - LVM2 options [str] + btrfsopts - BTRFS options [str] + options - Kernel options [str] +``` + +`HostProfile` fields: +``` +boom host list -o help +Host profiles Fields +-------------------- + hostid - Host identifier [sha] + machineid - Machine identifier [sha] + osid - OS identifier [sha] + hostname - Host name [str] + label - Host label [str] + kernelpattern - Kernel image pattern [str] + initrdpattern - Initrd pattern [str] + lvm2opts - LVM2 options [str] + btrfsopts - BTRFS options [str] + options - Kernel options [str] + profilepath - On-disk profile path [str] + addopts - Added Options [str] + delopts - Deleted Options [str] +``` + +### Getting help + +Help is available for the `boom` command and each command line option. + +Run the command with `--help` to display the full usage message: + +``` +# boom --help +``` + +## Configuring Boom + +### Creating an OsProfile +To automatically generate boot configuration Boom needs an *Operating +System Profile* for the system(s) for which it will create entries. + +And *OsProfile* is a collection of attributes that describe the OS +identity and provide templates for boot loader entries. + +The identity information comprising an `OsProfile` is taken from the +`os-release` file for the distribution. Additional properties, +such as the UTS release pattern to match for the distribution, +are either provided on the boom command line or are set to default +values. + +To create an `OsProfile` for the running system, use the +`-H/--from-host'` command line option: + +``` +# boom profile create --from-host --uname-pattern fc26 +Created profile with os_id d4439b7: + OS ID: "d4439b7d2f928c39f1160c0b0291407e5990b9e0", + Name: "Fedora", Short name: "fedora", + Version: "26 (Workstation Edition)", Version ID: "26", + UTS release pattern: "fc26", + Kernel pattern: "/kernel-%{version}", Initramfs pattern: "/initramfs-%{version}.img", + Root options (LVM2): "rd.lvm.lv=%{lvm_root_lv}", + Root options (BTRFS): "rootflags=%{btrfs_subvolume}", + Options: "root=%{root_device} ro %{root_opts}" +``` + +The `--uname-pattern` `OsProfile` property is an otional but recommended +pattern (regular expression) that should match the UTS release (`uname`) +strings reported by the operating system. + +The uname pattern is used when an on-disk boot loader entry is found that +does not contain an OS identifier (for e.g. a manually edited entry, or +one created by a different program). + +### Creating a HostProfile +Boom can optionally apply further customisation to the boot entries +it creates by defining a *HostProfile*. The host profile can be used +to modify the templates (boot image names and paths, boot entry +titles, kernel command line options etc) provided by the `OsProfile`. + +To create a new host profile for the current system use the +`host create` command, specifying the parameters to modify. For +example, to create a new host profile for a system running Fedora 30 +that adds the "debug" kernel command line argument, and removes the +"rhgb" and "quiet" arguments run: + +``` +boom profile list --name Fedora --osversionid 30 +OsID Name OsVersion +8896596 Fedora 30 (Workstation Edition) + +boom host create --profile 8896596 --add-opts debug --del-opts "rhgb quiet" +Created host profile with host_id ff4266a: + Host ID: "ff4266a7a0ceac789d65df75a1edd47b832dd9c5", + Host name: "localhost.localdomain", + Machine ID: "653b444d513a43239c37deae4f5fe644", + OS ID: "8896596a45fcc9e36e9c87aee77ab3e422da2635", + Add options: "debug", Del options: "rhgb quiet", + Name: "Fedora", Short name: "fedora", Version: "30 (Workstation Edition)", + Version ID: "30", UTS release pattern: "fc30", + Kernel pattern: "/vmlinuz-%{version}", Initramfs pattern: "/initramfs-%{version}.img", + Root options (LVM2): "rd.lvm.lv=%{lvm_root_lv}", + Root options (BTRFS): "rootflags=%{btrfs_subvolume}", + Options: "root=%{root_device} ro %{root_opts}" +``` + +### Creating a BootEntry +To create a new boot entry using an existing `OsProfile`, use the +`boom create` command, specifying the `OsProfile` using its assigned +identifier: + +``` +# boom profile list --short-name rhel +OsID Name OsVersion +98c3edb Red Hat Enterprise Linux Server 6 (Server) +c0b921e Red Hat Enterprise Linux Server 7 (Server) + +# boom create --profile 3fc389b --title "RHEL7 snapshot" --version 3.10-272.el7 --root-lv vg00/lvol0-snap +Created entry with boot_id a5aef11: +title RHEL7 snapshot +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-272.el7 +linux /boot/vmlinuz-3.10-272.el7 +initrd /boot/initramfs-3.10-272.el7.img +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet +``` + +Once the entry has been created it will appear in the boot loader +menu as configured: + +``` + Red Hat Enterprise Linux Server (3.10.0-327.el7.x86_64) 7.2 (Maipo) + Red Hat Enterprise Linux Server (3.10.0-272.el7.x86_64) 7.2 (Maipo) + RHEL7 Snapshot + + + + + + + + + + + + Use the ↑ and ↓ keys to change the selection. + Press 'e' to edit the selected item, or 'c' for a command prompt. +``` + +If creating an entry for the currently running kernel version, and the +OsProfile of the running host, these options can be omitted from the +create command: + +``` +# boom create --title "Fedora 26 snapshot" --root-lv vg_hex/root-snap-f26 +Created entry with boot_id d12c177: + title Fedora 26 snapshot + machine-id 611f38fd887d41dea7eb3403b2730a76 + version 4.13.5-200.fc26.x86_64 + linux /kernel-4.13.5-200.fc26.x86_64 + initrd /initramfs-4.13.5-200.fc26.x86_64.img + options root=/dev/vg_hex/root-snap-f26 ro rd.lvm.lv=vg_hex/root-snap-f26 +``` + +``` + Red Hat Enterprise Linux Server (3.10.0-327.el7.x86_64) 7.2 (Maipo) + Red Hat Enterprise Linux Server (3.10.0-272.el7.x86_64) 7.2 (Maipo) + Fedora 26 snapshot (4.13.5-200.fc26.x86_64) + RHEL7 Snapshot + + + + + + + + + + + + Use the ↑ and ↓ keys to change the selection. + Press 'e' to edit the selected item, or 'c' for a command prompt. +``` +## Grub2 Integration + +Boom includes scripts to integrate with versions of `grub2` that support +the BLS extension (including the builds of Grub shipped with Fedora and +Red Hat Enterprise Linux). + +The scripts support optionally placing all boom-managed entries into a +separate named submenu. + +### Submenu support + +To place all boom-managed boot entries into a separate submenu edit the +file `/etc/default/boom` and set the `BOOM_USE_SUBMENU` variable to `yes`: + +``` +BOOM_USE_SUBMENU="yes" +``` + +To change the name of the submenu modify the `BOOM_SUBMENU_NAME` variable: + +``` +BOOM_SUBMENU_NAME="Snapshots" +``` + +After modifying the file run the `grub2-mkconfig` program to update the +Grub boot loader configuration. + +If submenu support is enabled a new entry (named `Snapshots` in this +example) will appear at the bottom of the main Grub2 menu: + +``` + Red Hat Enterprise Linux Server (3.10.0-327.el7.x86_64) 7.2 (Maipo) + Red Hat Enterprise Linux Server (3.10.0-272.el7.x86_64) 7.2 (Maipo) + Snapshots + + + + + + + + + + + + Use the ↑ and ↓ keys to change the selection. + Press 'e' to edit the selected item, or 'c' for a command prompt. +``` + +Hitting `enter` on the submenu item will display the available boom +boot entries: + +``` + RHEL7 Snapshot (3.10.0-327.el7.x86_64) 2017-10-10 + RHEL7 Snapshot (3.10.0-327.el7.x86_64) 2017-10-01 + RHEL7 Snapshot (3.10.0-272.el7.x86_64) 2017-09-20 + RHEL7 Snapshot (3.10.0-272.el7.x86_64) 2017-08-13 + Fedora 24 (4.11.12-100.fc24.x86_64) + + + + + + + + + Use the ↑ and ↓ keys to change the selection. + Press 'e' to edit the selected item, or 'c' for a command prompt. + Press Escape to return to the previous menu. +``` + +## Python API +Boom also supports programatic use via a Python API. The API is flexible +and allows greater customisation than is possible using the command line +tool. + +Two interfaces are provided: a procedural command-driven interface that +closely mimics the command line tool (the boom CLI is implemented using +this interface), and a native object interface that provides complete +access to boom's capabilities and full control over boom `OsProfile` +`BootEntry`, and `BootParams` objects. User-defined tabular reports +may also be created using the `boom.report` module. + +### Command API +The command API is implemented in the `boom.command` sub-module. Programs +wishing to use the command API can just import this module: + +``` +import boom.command +``` + +The command API is [documented][7] at [readthedocs.org][6]. + +### Object API +The object API is implemented in several `boom` sub-modules: + + * `boom` + * `boom.bootloader` + * `boom.config` + * `boom.osprofile` + * `boom.hostprofile` + * `boom.report` + +Applications using the object API need only import the sub-modules that +contain the needed interfaces. + +The object API is [documented][8] at [readthedocs.org][6]. + +## Patches and pull requests + +Patches can be submitted via the mailing list or as GitHub pull requests. If +using GitHub please make sure your branch applies to the current master as a +'fast forward' merge (i.e. without creating a merge commit). Use the `git +rebase` command to update your branch to the current master if necessary. + +## Documentation + +API [documentation][4] is automatically generated using [Sphinx][5] +and [Read the Docs][6]. + +Installation and user documentation will be added in a future update. + + [0]: https://systemd.io/BOOT_LOADER_SPECIFICATION + [1]: https://github.com/bmr-cymru/snapshot-boot-docs + [2]: https://github.com/bmr-cymru/boom/issues + [3]: https://www.redhat.com/mailman/listinfo/dm-devel + [4]: https://boom.readthedocs.org/en/latest/index.html# + [5]: http://sphinx-doc.org/ + [6]: https://www.readthedocs.org/ + [7]: https://boom.readthedocs.io/en/latest/boom.html#module-boom.command + [8]: https://boom.readthedocs.io/en/latest/boom.html + [9]: https://copr.fedorainfracloud.org/coprs/bmr/boom/ diff --git a/bin/boom b/bin/boom new file mode 100755 index 0000000..f5c8a89 --- /dev/null +++ b/bin/boom @@ -0,0 +1,8 @@ +#!/usr/bin/python2 + +import sys +from boom.command import main + +if __name__ == '__main__': + r = main(sys.argv) + sys.exit(r) diff --git a/boom.spec b/boom.spec new file mode 100644 index 0000000..c8288b5 --- /dev/null +++ b/boom.spec @@ -0,0 +1,196 @@ +%global summary A set of libraries and tools for managing boot loader entries +%global sphinx_docs 1 + +Name: boom +Version: 1.0 +Release: 1%{?dist} +Summary: %{summary} + +License: GPLv2 +URL: https://github.com/snapshotmanager/boom +Source0: https://github.com/snapshotmanager/boom/archive/%{version}.tar.gz + +BuildArch: noarch + +BuildRequires: python3-setuptools +BuildRequires: python3-devel +%if 0%{?sphinx_docs} +BuildRequires: python3-sphinx +%endif + +Requires: python3-boom = %{version}-%{release} +Requires: %{name}-conf = %{version}-%{release} + +%package -n python3-boom +Summary: %{summary} +%{?python_provide:%python_provide python3-boom} +Requires: python3 +Recommends: (lvm2 or brtfs-progs) +Recommends: %{name}-conf = %{version}-%{release} + +# There used to be a boom package in fedora, and there is boom packaged in +# copr. How to tell which one is installed? We need python3-boom and no boom +# only. +Conflicts: boom + +%package conf +Summary: %{summary} + +%package grub2 +Summary: %{summary} +Supplements: (grub2 and boom-boot = %{version}-%{release}) + +%description +Boom is a boot manager for Linux systems using boot loaders that support +the BootLoader Specification for boot entry configuration. + +Boom requires a BLS compatible boot loader to function: either the +systemd-boot project, or Grub2 with the BLS patch (Red Hat Grub2 builds +include this support in both Red Hat Enterprise Linux 7 and Fedora). + +%description -n python3-boom +Boom is a boot manager for Linux systems using boot loaders that support +the BootLoader Specification for boot entry configuration. + +Boom requires a BLS compatible boot loader to function: either the +systemd-boot project, or Grub2 with the BLS patch (Red Hat Grub2 builds +include this support in both Red Hat Enterprise Linux 7 and Fedora). + +This package provides python3 boom module. + +%description conf +Boom is a boot manager for Linux systems using boot loaders that support +the BootLoader Specification for boot entry configuration. + +Boom requires a BLS compatible boot loader to function: either the +systemd-boot project, or Grub2 with the BLS patch (Red Hat Grub2 builds +include this support in both Red Hat Enterprise Linux 7 and Fedora). + +This package provides configuration files for boom. + +%description grub2 +Boom is a boot manager for Linux systems using boot loaders that support +the BootLoader Specification for boot entry configuration. + +Boom requires a BLS compatible boot loader to function: either the +systemd-boot project, or Grub2 with the BLS patch (Red Hat Grub2 builds +include this support in both Red Hat Enterprise Linux 7 and Fedora). + +This package provides integration scripts for grub2 bootloader. + +%prep +%setup -q -n boom-%{commit} +# NOTE: Do not use backup extension - MANIFEST.in is picking them + +%build +%if 0%{?sphinx_docs} +make -C doc html +rm doc/_build/html/.buildinfo +mv doc/_build/html doc/html +rm -r doc/_build +%endif + +%py3_build + +%install +%py3_install + +# Install Grub2 integration scripts +mkdir -p ${RPM_BUILD_ROOT}/etc/grub.d +mkdir -p ${RPM_BUILD_ROOT}/etc/default +install -m 755 etc/grub.d/42_boom ${RPM_BUILD_ROOT}/etc/grub.d +install -m 644 etc/default/boom ${RPM_BUILD_ROOT}/etc/default + +# Make configuration directories +# mode 0700 - in line with /boot/grub2 directory: +install -d -m 700 ${RPM_BUILD_ROOT}/boot/boom/profiles +install -d -m 700 ${RPM_BUILD_ROOT}/boot/boom/hosts +install -d -m 700 ${RPM_BUILD_ROOT}/boot/loader/entries +install -m 644 examples/boom.conf ${RPM_BUILD_ROOT}/boot/boom + +mkdir -p ${RPM_BUILD_ROOT}/%{_mandir}/man8 +mkdir -p ${RPM_BUILD_ROOT}/%{_mandir}/man5 +install -m 644 man/man8/boom.8 ${RPM_BUILD_ROOT}/%{_mandir}/man8 +install -m 644 man/man5/boom.5 ${RPM_BUILD_ROOT}/%{_mandir}/man5 + +rm doc/Makefile +rm doc/conf.py + +# Test suite currently does not operate in rpmbuild environment +#%%check +#%%{__python3} setup.py test + +%files +%license COPYING +%doc README.md +%{_bindir}/boom +%doc %{_mandir}/man*/boom.* + +%files -n python3-boom +%license COPYING +%doc README.md +%{python3_sitelib}/* +%doc doc +%doc examples +%doc tests + +%files conf +%license COPYING +%doc README.md +%dir /boot/boom +%config(noreplace) /boot/boom/boom.conf +%dir /boot/boom/profiles +%dir /boot/boom/hosts +%dir /boot/loader/entries + +%files grub2 +%license COPYING +%doc README.md +%{_sysconfdir}/grub.d/42_boom +%config(noreplace) %{_sysconfdir}/default/boom + + +%changelog +* Wed Nov 27 2019 Bryn M. Reeves - 1.0-1 +- Bump release for boom-1.0 + +* Thu Oct 03 2019 Miro Hrončok - 1.0-0.5.20190329git6ff3e08 +- Rebuilt for Python 3.8.0rc1 (#1748018) + +* Mon Aug 19 2019 Miro Hrončok - 1.0-0.4.20190329git6ff3e08 +- Rebuilt for Python 3.8 + +* Wed Jul 24 2019 Fedora Release Engineering - 1.0-0.3.20190329git6ff3e08 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_31_Mass_Rebuild + +* Thu May 09 2019 Marian Csontos 1.0-0.2.20190329git6ff3e08 +- Fix packaging issues. + +* Thu May 09 2019 Marian Csontos 1.0-0.1.20190329git6ff3e08 +- Pre-release of new version. + +* Thu Jan 31 2019 Fedora Release Engineering - 0.9-5 +- Rebuilt for https://fedoraproject.org/wiki/Fedora_30_Mass_Rebuild + +* Tue Jul 17 2018 Marian Csontos 0.9-4 +- Change dependencies. + +* Mon Jul 16 2018 Marian Csontos 0.9-3 +- Split executable, python module and configuration. + +* Wed Jun 27 2018 Marian Csontos 0.9-2 +- Spin off grub2 into subpackage + +* Wed Jun 27 2018 Marian Csontos 0.9-1 +- Update to new upstream 0.9. +- Fix boot_id caching. + +* Fri Jun 08 2018 Marian Csontos 0.8.5-6.2 +- Remove example files from /boot/boom/profiles. + +* Fri May 11 2018 Marian Csontos 0.8.5-6.1 +- Files in /boot are treated as configuration files. + +* Thu Apr 26 2018 Marian Csontos 0.8.5-6 +- Package upstream version 0.8-5.6 + diff --git a/boom/__init__.py b/boom/__init__.py new file mode 100644 index 0000000..eb5f776 --- /dev/null +++ b/boom/__init__.py @@ -0,0 +1,40 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# boom/__init__.py - Boom package initialisation +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""This module provides classes and functions for creating, displaying, +and manipulating boot loader entries complying with the Boot Loader +Specification. + +The ``boom`` package contains global definitions, functions to configure +the Boom environment, logging infrastructure for the package and a +``Selection`` class used to select one or more ``OsProfile``, +``HostProfile``, ``BootEntry``, or ``BootParams`` object according to +specified selection criteria. + +Individual sub-modules provide interfaces to the various components of +Boom: operating system and host profiles, boot loader entries and boot +parameters, the boom CLI and procedural API and a simple reporting +module to produce tabular reports on Boom objects. + +See the sub-module documentation for specific information on the +classes and interfaces provided, and the ``boom`` tool help output and +manual page for information on using the command line interface. +""" +from __future__ import print_function + +from ._boom import * +from ._boom import __all__ + +__version__ = "1.0" + +# vim: set et ts=4 sw=4 : diff --git a/boom/_boom.py b/boom/_boom.py new file mode 100644 index 0000000..4a84b78 --- /dev/null +++ b/boom/_boom.py @@ -0,0 +1,910 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# boom/_boom.py - Boom package initialisation +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""This module provides the declarations, classes, and functions exposed +in the main ``boom`` module. Users of boom should not import this module +directly: it will be imported automatically with the top level module. +""" +from __future__ import print_function + +from os.path import exists as path_exists, isabs, isdir, join as path_join +from os import listdir +import logging +import string + +#: The location of the system ``/boot`` directory. +DEFAULT_BOOT_PATH = "/boot" + +#: The default path for Boom configuration files. +DEFAULT_BOOM_DIR = "boom" + +#: The root directory for Boom configuration files. +DEFAULT_BOOM_PATH = path_join(DEFAULT_BOOT_PATH, DEFAULT_BOOM_DIR) + +#: Configuration file mode +BOOT_CONFIG_MODE = 0o644 + +#: The default configuration file location +BOOM_CONFIG_FILE = "boom.conf" +DEFAULT_BOOM_CONFIG_PATH = path_join(DEFAULT_BOOM_PATH, BOOM_CONFIG_FILE) +__boom_config_path = DEFAULT_BOOM_CONFIG_PATH + +#: Kernel version string, in ``uname -r`` format. +FMT_VERSION = "version" +#: LVM2 root logical volume in ``vg/lv`` format. +FMT_LVM_ROOT_LV = "lvm_root_lv" +#: LVM2 kernel command line options +FMT_LVM_ROOT_OPTS = "lvm_root_opts" +#: BTRFS subvolume specification. +FMT_BTRFS_SUBVOLUME = "btrfs_subvolume" +#: BTRFS subvolume ID specification. +FMT_BTRFS_SUBVOL_ID = "btrfs_subvol_id" +#: BTRFS subvolume path specification. +FMT_BTRFS_SUBVOL_PATH = "btrfs_subvol_path" +#: BTRFS kernel command line options +FMT_BTRFS_ROOT_OPTS = "btrfs_root_opts" +#: Root device path. +FMT_ROOT_DEVICE = "root_device" +#: Root device options. +FMT_ROOT_OPTS = "root_opts" +#: Linux kernel image +FMT_KERNEL = "kernel" +#: Initramfs image +FMT_INITRAMFS = "initramfs" +#: OS Profile name +FMT_OS_NAME = "os_name" +#: OS Profile short name +FMT_OS_SHORT_NAME = "os_short_name" +#: OS Profile version +FMT_OS_VERSION = "os_version" +#: OS Profile version ID +FMT_OS_VERSION_ID = "os_version_id" + +#: List of all possible format keys. +FORMAT_KEYS = [ + FMT_VERSION, + FMT_LVM_ROOT_LV, FMT_LVM_ROOT_OPTS, + FMT_BTRFS_SUBVOL_ID, FMT_BTRFS_SUBVOL_PATH, + FMT_BTRFS_SUBVOLUME, FMT_BTRFS_ROOT_OPTS, + FMT_ROOT_DEVICE, FMT_ROOT_OPTS, + FMT_KERNEL, FMT_INITRAMFS, + FMT_OS_NAME, FMT_OS_SHORT_NAME, + FMT_OS_VERSION, FMT_OS_VERSION_ID +] + +BOOM_LOG_DEBUG = logging.DEBUG +BOOM_LOG_INFO = logging.INFO +BOOM_LOG_WARN = logging.WARNING +BOOM_LOG_ERROR = logging.ERROR + +_log_levels = ( + BOOM_LOG_DEBUG, + BOOM_LOG_INFO, + BOOM_LOG_WARN, + BOOM_LOG_ERROR +) + +_log = logging.getLogger(__name__) + +_log_debug = _log.debug +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +# Boom debugging levels +BOOM_DEBUG_PROFILE = 1 +BOOM_DEBUG_ENTRY = 2 +BOOM_DEBUG_REPORT = 4 +BOOM_DEBUG_COMMAND = 8 +BOOM_DEBUG_ALL = (BOOM_DEBUG_PROFILE | + BOOM_DEBUG_ENTRY | + BOOM_DEBUG_REPORT | + BOOM_DEBUG_COMMAND) + +__debug_mask = 0 + + +class BoomError(Exception): + """Base class of all Boom exceptions. + """ + pass + + +class BoomLogger(logging.Logger): + """BoomLogger() + + Boom logging wrapper class: wrap the Logger.debug() method + to allow filtering of submodule debug messages by log mask. + + This allows us to selectively control which messages are + logged in the library without having to tamper with the + Handler, Filter or Formatter configurations (which belong + to the client application using the library). + """ + + mask_bits = 0 + + def set_debug_mask(self, mask_bits): + """Set the debug mask for this ``BoomLogger``. + + This should normally be set to the ``BOOM_DEBUG_*`` value + corresponding to the ``boom`` sub-module that this instance + of ``BoomLogger`` belongs to. + + :param mask_bits: The bits to set in this logger's mask. + :rtype: None + """ + if mask_bits < 0 or mask_bits > BOOM_DEBUG_ALL: + raise ValueError("Invalid BoomLogger mask bits: 0x%x" % + (mask_bits & ~BOOM_DEBUG_ALL)) + + self.mask_bits = mask_bits + + def debug_masked(self, msg, *args, **kwargs): + """Log a debug message if it passes the current debug mask. + + Log the specified message if it passes the current logger + debug mask. + + :param msg: the message to be logged + :rtype: None + """ + if self.mask_bits & get_debug_mask(): + self.debug(msg, *args, **kwargs) + + +logging.setLoggerClass(BoomLogger) + + +def get_debug_mask(): + """Return the current debug mask for the ``boom`` package. + + :returns: The current debug mask value + :rtype: int + """ + return __debug_mask + + +def set_debug_mask(mask): + """Set the debug mask for the ``boom`` package. + + :param mask: the logical OR of the ``BOOM_DEBUG_*`` + values to log. + :rtype: None + """ + global __debug_mask + if mask < 0 or mask > BOOM_DEBUG_ALL: + raise ValueError("Invalid boom debug mask: %d" % mask) + __debug_mask = mask + + +class BoomConfig(object): + """Class representing boom persistent configuration values. + """ + + # Initialise members from global defaults + + boot_path = DEFAULT_BOOT_PATH + boom_path = DEFAULT_BOOM_PATH + + legacy_enable = False + legacy_format = "grub1" + legacy_sync = True + + def __str__(self): + """Return a string representation of this ``BoomConfig`` in + boom.conf (INI) notation. + """ + cstr = "" + cstr += '[defaults]\n' + cstr += 'boot_path = %s\n' % self.boot_path + cstr += 'boom_path = %s\n\n' % self.boom_path + + cstr += '[legacy]\n' + cstr += 'enable = %s\n' % self.legacy_enable + cstr += 'format = %s\n' % self.legacy_format + cstr += 'sync = %s' % self.legacy_sync + + return cstr + + def __repr__(self): + """Return a string representation of this ``BoomConfig`` in + BoomConfig initialiser notation. + """ + cstr = ('BoomConfig(boot_path="%s",boom_path="%s",' % + (self.boot_path, self.boom_path)) + cstr += ('enable_legacy=%s,legacy_format="%s",' % + (self.legacy_enable, self.legacy_format)) + cstr += 'legacy_sync=%s)' % self.legacy_sync + return cstr + + def __init__(self, boot_path=None, boom_path=None, legacy_enable=None, + legacy_format=None, legacy_sync=None): + """Initialise a new ``BoomConfig`` object with the supplied + configuration values, or defaults for any unset arguments. + + :param boot_path: the path to the system /boot volume + :param boom_path: the path to the boom configuration dir + :param legacy_enable: enable legacy bootloader support + :param legacy_format: the legacy bootlodaer format to write + :param legacy_sync: the legacy sync mode + """ + self.boot_path = boot_path or self.boot_path + self.boom_path = boom_path or self.boom_path + self.legacy_enable = legacy_enable or self.legacy_enable + self.legacy_format = legacy_format or self.legacy_format + self.legacy_sync = legacy_sync or self.legacy_sync + + +__config = BoomConfig() + + +def set_boom_config(config): + """Set the active configuration to the object ``config`` (which may + be any class that includes the ``BoomConfig`` attributes). + + :param config: a configuration object + :returns: None + :raises: TypeError if ``config`` does not appear to have the + correct attributes. + """ + global __config + + def has_value(obj, attr): + return hasattr(obj, attr) and getattr(obj, attr) is not None + + if not (has_value(config, "boot_path") and has_value(config, "boom_path")): + raise TypeError("config does not appear to be a BoomConfig object.") + + __config = config + + +def get_boom_config(): + """Return the active ``BoomConfig`` object. + + :rtype: BoomConfig + :returns: the active configuration object + """ + return __config + + +def get_boot_path(): + """Return the currently configured boot file system path. + + :returns: the path to the /boot file system. + :rtype: str + """ + return __config.boot_path + + +def get_boom_path(): + """Return the currently configured boom configuration path. + + :returns: the path to the BOOT/boom directory. + :rtype: str + """ + return __config.boom_path + + +def set_boot_path(boot_path): + """Sets the location of the boot file system to ``boot_path``. + + The path defaults to the '/boot/' mount directory in the root + file system: this may be overridden by calling this function + with a different path. + + Calling ``set_boom_root_path()`` will re-set the value returned + by ``get_boom_path()`` to the default boom configuration sub- + directory within the new boot file system. The location of the + boom configuration path may be configured separately by calling + ``set_boom_root_path()`` after setting the boot path. + + :param boot_path: the path to the 'boom/' directory containing + boom profiles and configuration. + :returnsNone: ``None`` + :raises: ValueError if ``boot_path`` does not exist. + """ + global __config + if not isabs(boot_path): + raise ValueError("boot_path must be an absolute path: %s" % boot_path) + + if not path_exists(boot_path): + raise ValueError("Path '%s' does not exist" % boot_path) + + __config.boot_path = boot_path + _log_debug("Set boot path to: %s" % boot_path) + __config.boom_path = path_join(boot_path, DEFAULT_BOOM_DIR) + + # If a boom/ directory exists at the boot path, automatically set + # the boom path to it. Otherwise, we assume that the caller will + # set the path explicitly to some non-default location. + boom_path = path_join(boot_path, "boom") + if path_exists(boom_path) and isdir(boom_path): + set_boom_path(path_join(__config.boot_path, "boom")) + + +def set_boom_path(boom_path): + """Set the location of the boom configuration directory. + + Set the location of the boom configuration path stored in + the active configuration to ``boom_path``. This defaults to the + 'boom/' sub-directory in the boot file system specified by + ``config.boot_path``: this may be overridden by calling this + function with a different path. + + :param boom_path: the path to the 'boom/' directory containing + boom profiles and configuration. + :returns: ``None`` + :raises: ValueError if ``boom_path`` does not exist. + """ + global __config + err_str = "Boom path %s does not exist" % boom_path + if isabs(boom_path) and not path_exists(boom_path): + raise ValueError(err_str) + elif not path_exists(path_join(__config.boot_path, boom_path)): + raise ValueError(err_str) + + if not isabs(boom_path): + boom_path = path_join(__config.boot_path, boom_path) + + if not path_exists(path_join(boom_path, "profiles")): + raise ValueError("Path does not contain a valid boom configuration" + ": %s" % path_join(boom_path, "profiles")) + + _log_debug("Set boom path to: %s" % boom_path) + __config.boom_path = boom_path + set_boom_config_path(__config.boom_path) + + +def get_boom_config_path(): + """Return the currently configured boom configuration file path. + + :rtype: str + :returns: the current boom configuration file path + """ + return __boom_config_path + + +def set_boom_config_path(path): + """Set the boom configuration file path. + """ + global __boom_config_path + path = path or get_boom_config_path() + if not isabs(path): + path = path_join(get_boom_path()) + if isdir(path): + path = path_join(path, BOOM_CONFIG_FILE) + if not path_exists(path): + raise IOError(ENOENT, "File not found: '%s'" % path) + __boom_config_path = path + _log_debug("set boom_config_path to '%s'" % path) + + +def parse_btrfs_subvol(subvol): + """Parse a BTRFS subvolume string. + + Parse a BTRFS subvolume specification into either a subvolume + path string, or a string containing a subvolume identifier. + + :param subvol: The subvolume parameter to parse + :returns: A string containing the subvolume path or ID + :rtype: ``str`` + :raises: ValueError if no valid subvolume was found + """ + if not subvol: + return None + + subvol_id = None + try: + subvol_id = int(subvol) + subvol = str(subvol_id) + except ValueError: + if not subvol.startswith('/'): + raise ValueError("Unrecognised BTRFS subvolume: %s" % subvol) + return subvol + + +# +# Selection criteria class +# + +class Selection(object): + """Selection() + Selection criteria for boom BootEntry, OsProfile HostProfile, + and BootParams. + + Selection criteria specified as a simple boolean AND of all + criteria with a non-None value. + """ + + # BootEntry fields + boot_id = None + title = None + version = None + machine_id = None + linux = None + initrd = None + efi = None + options = None + devicetree = None + + # BootParams fields + root_device = None + lvm_root_lv = None + btrfs_subvol_path = None + btrfs_subvol_id = None + + # OsProfile fields + os_id = None + os_name = None + os_short_name = None + os_version = None + os_version_id = None + os_uname_pattern = None + os_kernel_pattern = None + os_initramfs_pattern = None + os_root_opts_lvm2 = None + os_root_opts_btrfs = None + os_options = None + + # HostProfile fields + host_id = None + host_name = None + host_label = None + host_short_name = None + host_add_opts = None + host_del_opts = None + + #: Selection criteria applying to BootEntry objects + entry_attrs = [ + "boot_id", "title", "version", "machine_id", "linux", "initrd", "efi", + "options", "devicetree" + ] + + #: Selection criteria applying to BootParams objects + params_attrs = [ + "root_device", "lvm_root_lv", "btrfs_subvol_path", "btrfs_subvol_id" + ] + + #: Selection criteria applying to OsProfile objects + profile_attrs = [ + "os_id", "os_name", "os_short_name", "os_version", "os_version_id", + "os_uname_pattern", "os_kernel_pattern", "os_initramfs_pattern", + "os_root_opts_lvm2", "os_root_opts_btrfs", "os_options" + ] + + #: Selection criteria applying to HostProfile objects + host_attrs = [ + "host_id", "host_name", "host_label", "host_short_name", + "host_add_opts", "host_del_opts", "machine_id" + ] + + all_attrs = entry_attrs + params_attrs + profile_attrs + host_attrs + + def __str__(self): + """Format this ``Selection`` object as a human readable string. + + :returns: A human readable string representation of this + Selection object + :rtype: string + """ + all_attrs = self.all_attrs + attrs = [attr for attr in all_attrs if self.__attr_has_value(attr)] + strval = "" + tail = ", " + for attr in set(attrs): + strval += "%s='%s'%s" % (attr, getattr(self, attr), tail) + return strval.rstrip(tail) + + def __repr__(self): + """Format this ``Selection`` object as a machine readable string. + + The returned string may be passed to the Selection + initialiser to duplicate the original Selection. + + :returns: A machine readable string representation of this + Selection object + :rtype: string + """ + return "Selection(" + str(self) + ")" + + def __init__(self, boot_id=None, title=title, version=version, + machine_id=None, linux=None, initrd=None, + efi=None, root_device=None, lvm_root_lv=None, + btrfs_subvol_path=None, btrfs_subvol_id=None, + os_id=None, os_name=None, os_short_name=None, + os_version=None, os_version_id=None, os_options=None, + os_uname_pattern=None, os_kernel_pattern=None, + os_initramfs_pattern=None, host_id=None, + host_name=None, host_label=None, host_short_name=None, + host_add_opts=None, host_del_opts=None): + """Initialise a new Selection object. + + Initialise a new Selection object with the specified selection + criteria. + + :param boot_id: The boot_id to match + :param title: The title to match + :param version: The version to match + :param machine_id: The machine_id to match + :param linux: The BootEntry kernel image to match + :param initrd: The BootEntry initrd image to match + :param efi: The BootEntry efi image to match + :param root_device: The root_device to match + :param lvm_root_lv: The lvm_root_lv to match + :param btrfs_subvol_path: The btrfs_subvol_path to match + :param btrfs_subvol_id: The btrfs_subvol_id to match + :param os_id: The os_id to match + :param os_name: The os_name to match + :param os_short_name: The os_short_name to match + :param os_version: The os_version to match + :param os_version_id: The os_version_id to match + :param os_options: The os_options to match + :param os_uname_pattern: The os_uname_pattern to match + :param os_kernel_pattern: The kernel_pattern to match + :param os_initramfs_pattern: The initramfs_pattern to match + :param host_id: The host identifier to match + :param host_name: The host name to match + :param host_label: The host label to match + :param host_short_name: The host short name to match + :param host_add_opts: Host add options to match + :param host_del_opts: Host del options to match + :returns: A new Selection instance + :rtype: Selection + """ + self.boot_id = boot_id + self.title = title + self.version = version + self.machine_id = machine_id + self.linux = linux + self.initrd = initrd + self.efi = efi + self.root_device = root_device + self.lvm_root_lv = lvm_root_lv + self.btrfs_subvol_path = btrfs_subvol_path + self.btrfs_subvol_id = btrfs_subvol_id + self.os_id = os_id + self.os_name = os_name + self.os_short_name = os_short_name + self.os_version = os_version + self.os_version_id = os_version_id + self.os_options = os_options + self.os_uname_pattern = os_uname_pattern + self.os_kernel_pattern = os_kernel_pattern + self.os_initramfs_pattern = os_initramfs_pattern + self.host_id = host_id + self.host_name = host_name + self.host_label = host_label + self.host_short_name = host_short_name + self.host_add_opts = host_add_opts + self.host_del_opts = host_del_opts + + @classmethod + def from_cmd_args(cls, args): + """Initialise Selection from command line arguments. + + Construct a new ``Selection`` object from the command line + arguments in ``cmd_args``. Each set selection attribute from + ``cmd_args`` is copied into the Selection. The resulting + object may be passed to either the ``BootEntry``, + ``OsProfile``, or ``HostProfile`` search functions + (``find_entries``, ``find_profiles``, and + ``find_host_profiles``), as well as the ``boom.command`` + calls that accept a selection argument. + + :param args: The command line selection arguments. + :returns: A new Selection instance + :rtype: Selection + """ + subvol = parse_btrfs_subvol(args.btrfs_subvolume) + if subvol and subvol.startswith('/'): + btrfs_subvol_path = subvol + btrfs_subvol_id = None + elif subvol: + btrfs_subvol_id = subvol + btrfs_subvol_path = None + else: + btrfs_subvol_id = btrfs_subvol_path = None + + s = Selection(boot_id=args.boot_id, title=args.title, + version=args.version, machine_id=args.machine_id, + linux=args.linux, initrd=args.initrd, efi=args.efi, + root_device=args.root_device, + lvm_root_lv=args.root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, + os_id=args.profile, os_name=args.name, + os_short_name=args.short_name, + os_version=args.os_version, + os_version_id=args.os_version_id, + os_options=args.os_options, + os_uname_pattern=args.uname_pattern, + host_id=args.host_profile) + + _log_debug("Initialised %s from arguments" % repr(s)) + return s + + def __attr_has_value(self, attr): + """Test whether an attribute is defined. + + Return ``True`` if the specified attribute name is currently + defined, or ``False`` otherwise. + + :param attr: The name of the attribute to test + :returns: ``True`` if ``attr`` is set or ``False`` otherwise + :rtype: bool + """ + return hasattr(self, attr) and getattr(self, attr) is not None + + def check_valid_selection(self, entry=False, params=False, + profile=False, host=False): + """Check a Selection for valid criteria. + + Check this ``Selection`` object to ensure it contains only + criteria that are valid for the specified object type(s). + + Returns ``None`` if the object passes the check, or raise + ``ValueError`` if invalid criteria exist. + + :param entry: ``Selection`` may include BootEntry data + :param params: ``Selection`` may include BootParams data + :param profile: ``Selection`` may include OsProfile data + :param host: ``Selection`` may include Host data + :returns: ``None`` on success + :rtype: ``NoneType`` + :raises: ``ValueError`` if excluded criteria are present + """ + valid_attrs = [] + invalid_attrs = [] + + if entry: + valid_attrs += self.entry_attrs + if entry or params: + valid_attrs += self.params_attrs + if profile or host: + valid_attrs += self.profile_attrs + if host: + valid_attrs += self.host_attrs + + for attr in self.all_attrs: + if self.__attr_has_value(attr) and attr not in valid_attrs: + invalid_attrs.append(attr) + + if invalid_attrs: + invalid = ", ".join(invalid_attrs) + raise ValueError("Invalid criteria for selection type: %s" % + invalid) + + def is_null(self): + """Test this Selection object for null selection criteria. + + Return ``True`` if this ``Selection`` object matches all + objects, or ``False`` otherwise. + + :returns: ``True`` if this Selection is null + :rtype: bool + """ + all_attrs = self.all_attrs + attrs = [attr for attr in all_attrs if self.__attr_has_value(attr)] + return not any(attrs) + + +# +# Generic routines for parsing name-value pairs. +# + +def blank_or_comment(line): + """Test whether line is empty of contains a comment. + + Test whether the ``line`` argument is either blank, or a + whole-line comment. + + :param line: the line of text to be checked. + :returns: ``True`` if the line is blank or a comment, + and ``False`` otherwise. + :rtype: bool + """ + return not line.strip() or line.lstrip().startswith('#') + + +def parse_name_value(nvp, separator="="): + """Parse a name value pair string. + + Parse a ``name='value'`` style string into its component parts, + stripping quotes from the value if necessary, and return the + result as a (name, value) tuple. + + :param nvp: A name value pair optionally with an in-line + comment. + :param separator: The separator character used in this name + value pair, or ``None`` to splir on white + space. + :returns: A ``(name, value)`` tuple. + :rtype: (string, string) tuple. + """ + val_err = ValueError("Malformed name/value pair: %s" % nvp) + try: + # Only strip newlines: values may contain embedded + # whitespace anywhere within the string. + name, value = nvp.rstrip('\n').split(separator, 1) + except ValueError: + raise val_err + + # Value cannot start with '=' + if value.startswith('='): + raise val_err + + name = name.strip() + value = value.lstrip() + + if "#" in value: + value, comment = value.split("#", 1) + + valid_name_chars = string.ascii_letters + string.digits + "_-,.'\"" + bad_chars = [c for c in name if c not in valid_name_chars] + if any(bad_chars): + raise ValueError("Invalid characters in name: %s (%s)" % + (name, bad_chars)) + + if value.startswith('"') or value.startswith("'"): + value = value[1:-1] + return (name, value) + + +def find_minimum_sha_prefix(shas, min_prefix): + """Find the minimum SHA prefix length guaranteeing uniqueness. + + Find the minimum unique prefix for the set of SHA IDs in the set + ``shas``. + + :param shas: A set of SHA IDs + :param min_prefix: Initial minimum prefix value + :returns: The minimum unique prefix length for the set + :rtype: int + """ + shas = list(shas) + shas.sort() + for sha in shas: + if shas.index(sha) == len(shas) - 1: + continue + + def _next_sha(shas, sha): + return shas[shas.index(sha) + 1] + + while sha[:min_prefix] == _next_sha(shas, sha)[:min_prefix]: + min_prefix += 1 + return min_prefix + + +def min_id_width(min_prefix, objs, attr): + """Calculate the minimum unique width for id values. + + Calculate the minimum width to ensure uniqueness when displaying + id values. + + :param min_prefix: The minimum allowed unique prefix. + :param objs: An interrable containing objects to check. + :param attr: The attribute to compare. + + :returns: the minimum id width. + :rtype: int + """ + if not objs: + return min_prefix + + ids = set() + for obj in objs: + ids.add(getattr(obj, attr)) + return find_minimum_sha_prefix(ids, min_prefix) + + +def load_profiles_for_class(profile_class, profile_type, + profiles_path, profile_ext): + """Load profiles from disk. + + Load the set of profiles found at the path ``profiles_path`` + into the list ``profiles``. The list should be cleared before + calling this function if the prior contents are no longer + required. + + The profile class to be instantiated is specified by the + ``profile_class`` argument. An optional ``type`` may be + specified to describe the profile type in error messages. + If ``type`` is unset the class name is used instead. + + This function is intended for use by profile implementations + that share common on-disk profile handling. + + :param profile_class: The profile class to instantiate. + :param profile_type: A string description of the profile type. + :param profiles_path: Path to the on-disk profile directory. + :param profile_ext: Extension of profile files. + + :returns: None + """ + profile_files = listdir(profiles_path) + _log_info("Loading %s profiles from %s" % (profile_type, profiles_path)) + for pf in profile_files: + if not pf.endswith(".%s" % profile_ext): + continue + pf_path = path_join(profiles_path, pf) + try: + profile_class(profile_file=pf_path) + except Exception as e: + _log_warn("Failed to load %s from '%s': %s" % + (profile_class.__name__, pf_path, e)) + continue + + +__all__ = [ + # boom module constants + 'DEFAULT_BOOT_PATH', 'DEFAULT_BOOM_PATH', + 'BOOM_CONFIG_FILE', + + # Profile format keys + 'FMT_VERSION', + 'FMT_LVM_ROOT_LV', + 'FMT_LVM_ROOT_OPTS', + 'FMT_BTRFS_SUBVOLUME', + 'FMT_BTRFS_SUBVOL_ID', + 'FMT_BTRFS_SUBVOL_PATH', + 'FMT_BTRFS_ROOT_OPTS', + 'FMT_ROOT_DEVICE', + 'FMT_ROOT_OPTS', + 'FMT_KERNEL', + 'FMT_INITRAMFS', + 'FMT_OS_NAME', + 'FMT_OS_SHORT_NAME', + 'FMT_OS_VERSION', + 'FMT_OS_VERSION_ID', + 'FORMAT_KEYS', + + # API Classes + 'BoomConfig', 'Selection', + + # Path configuration + 'get_boot_path', + 'get_boom_path', + 'set_boot_path', + 'set_boom_path', + 'set_boom_config_path', + 'get_boom_config_path', + + # Persistent configuration + 'set_boom_config', + 'get_boom_config', + + # boom exception base class + 'BoomError', + + # Boom logger class (used by test suite) + 'BoomLogger', + # Debug logging + 'get_debug_mask', + 'set_debug_mask', + 'BOOM_DEBUG_PROFILE', + 'BOOM_DEBUG_ENTRY', + 'BOOM_DEBUG_REPORT', + 'BOOM_DEBUG_COMMAND', + 'BOOM_DEBUG_ALL', + + # Utility routines + 'blank_or_comment', + 'parse_name_value', + 'parse_btrfs_subvol', + 'find_minimum_sha_prefix', + 'min_id_width', + 'load_profiles_for_class' +] + +# vim: set et ts=4 sw=4 diff --git a/boom/bootloader.py b/boom/bootloader.py new file mode 100644 index 0000000..d35a717 --- /dev/null +++ b/boom/bootloader.py @@ -0,0 +1,2472 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# bootloader.py - Boom BLS bootloader manager +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.bootloader`` module defines classes for working with +on-disk boot loader entries: the ``BootEntry`` class represents an +individual boot loader entry, and the ``BootParams`` class +encapsulates the parameters needed to boot an instance of the +operating system. The kernel version and root device configuration +of an existing ``BootEntry`` may be changed by modifying or +substituting its ``BootParams`` object (this may also be used to +'clone' configuration from one entry to another). + +Functions are provided to read and write boot loader entries from an +on-disk store (normally located at ``/boot/loader/entries``), and to +retrieve particular ``BootEntry`` objects based on a variety of +selection criteria. + +The ``BootEntry`` class includes named properties for each boot entry +attribute ("entry key"). In addition, the class serves as a container +type, allowing attributes to be accessed via dictionary-style indexing. +This simplifies iteration over a profile's key / value pairs and allows +straightforward access to all members in scripts and the Python shell. + +All entry key names are made available as named members of the module: +``BOOT_ENTRY_*``, and the ``ENTRY_KEYS`` list. A map of Boom key names +to BLS keys is available in the ``KEY_MAP`` dictionary (a reverse map +is also provided in the ``MAP_KEY`` member). + +""" +from __future__ import print_function + +from boom import * +from boom.osprofile import * +from boom.hostprofile import find_host_profiles + +from os.path import basename, exists as path_exists, join as path_join +from subprocess import Popen, PIPE +from tempfile import mkstemp +from os import listdir, rename, fdopen, chmod, unlink, fdatasync, stat, dup +from stat import S_ISBLK +from hashlib import sha1 +import logging +import re + +#: The path to the BLS boot entries directory relative to /boot +ENTRIES_PATH = "loader/entries" + +#: The format used to construct entry file names. +BOOT_ENTRIES_FORMAT = "%s-%s-%s.conf" + +#: A regular expression matching the boom file name format. +BOOT_ENTRIES_PATTERN = r"(\w*)-(\w{1,7})-([a-zA-Z0-9.\-_]*)" + +#: The file mode with which BLS entries should be created. +BOOT_ENTRY_MODE = 0o644 + +#: The ``BootEntry`` title key. +BOOM_ENTRY_TITLE = "BOOM_ENTRY_TITLE" +#: The ``BootEntry`` version key. +BOOM_ENTRY_VERSION = "BOOM_ENTRY_VERSION" +#: The ``BootEntry`` machine_id key. +BOOM_ENTRY_MACHINE_ID = "BOOM_ENTRY_MACHINE_ID" +#: The ``BootEntry`` linux key. +BOOM_ENTRY_LINUX = "BOOM_ENTRY_LINUX" +#: The ``BootEntry`` initrd key. +BOOM_ENTRY_INITRD = "BOOM_ENTRY_INITRD" +#: The ``BootEntry`` efi key. +BOOM_ENTRY_EFI = "BOOM_ENTRY_EFI" +#: The ``BootEntry`` options key. +BOOM_ENTRY_OPTIONS = "BOOM_ENTRY_OPTIONS" +#: The ``BootEntry`` device tree key. +BOOM_ENTRY_DEVICETREE = "BOOM_ENTRY_DEVICETREE" +#: The ``BootEntry`` architecture key. +BOOM_ENTRY_ARCHITECTURE = "BOOM_ENTRY_ARCHITECTURE" +#: The ``BootEntry`` boot identifier key. +BOOM_ENTRY_BOOT_ID = "BOOM_ENTRY_BOOT_ID" + +# +# Optional and non-standard BLS keys +# +# The keys defined here are optional and implementation defined: +# They may only be used in a ``BootEntry`` if the corresponding +# ``OsProfile`` or ``HostProfile`` permits them. +# + +#: The Red Hat ``BootEntry`` grub_users key. +BOOM_ENTRY_GRUB_USERS = "BOOM_ENTRY_GRUB_USERS" +#: The Red Hat ``BootEntry`` grub_arg key. +BOOM_ENTRY_GRUB_ARG = "BOOM_ENTRY_GRUB_ARG" +#: The Red Hat ``BootEntry`` grub_class key. +BOOM_ENTRY_GRUB_CLASS = "BOOM_ENTRY_GRUB_CLASS" +#: The Red Hat ``BootEntry`` id key. +BOOM_ENTRY_GRUB_ID = "BOOM_ENTRY_GRUB_ID" + +#: Optional keys not defined by the upstream BLS specification. +OPTIONAL_KEYS = [ + BOOM_ENTRY_GRUB_USERS, + BOOM_ENTRY_GRUB_ARG, + BOOM_ENTRY_GRUB_CLASS, + BOOM_ENTRY_GRUB_ID +] + +#: An ordered list of all possible ``BootEntry`` keys. +ENTRY_KEYS = [ + # We require a title for each entry (BLS does not) + BOOM_ENTRY_TITLE, + # MACHINE_ID is optional in BLS, however, since the standard suggests + # that it form part of the file name for compliant snippets, it is + # effectively mandatory. + BOOM_ENTRY_MACHINE_ID, + BOOM_ENTRY_VERSION, + # One of either BOOM_ENTRY_LINUX or BOOM_ENTRY_EFI must be present. + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, + BOOM_ENTRY_INITRD, BOOM_ENTRY_OPTIONS, + BOOM_ENTRY_DEVICETREE, BOOM_ENTRY_ARCHITECTURE, + # Optional implementation defined BLS keys + BOOM_ENTRY_GRUB_USERS, BOOM_ENTRY_GRUB_ARG, BOOM_ENTRY_GRUB_CLASS, + BOOM_ENTRY_GRUB_ID +] + +#: Map Boom entry names to BLS keys +KEY_MAP = { + BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_VERSION: "version", + BOOM_ENTRY_MACHINE_ID: "machine_id", + BOOM_ENTRY_LINUX: "linux", + BOOM_ENTRY_INITRD: "initrd", + BOOM_ENTRY_EFI: "efi", + BOOM_ENTRY_OPTIONS: "options", + BOOM_ENTRY_DEVICETREE: "devicetree", + BOOM_ENTRY_ARCHITECTURE: "architecture", + BOOM_ENTRY_GRUB_USERS: "grub_users", + BOOM_ENTRY_GRUB_ARG: "grub_arg", + BOOM_ENTRY_GRUB_CLASS: "grub_class", + BOOM_ENTRY_GRUB_ID: "id" +} + +#: Default values for optional keys +OPTIONAL_KEY_DEFAULTS = { + BOOM_ENTRY_GRUB_USERS: "$grub_users", + BOOM_ENTRY_GRUB_ARG: "kernel", + BOOM_ENTRY_GRUB_CLASS: "--unrestricted", + BOOM_ENTRY_GRUB_ID: None +} + + +def optional_key_default(key): + """Return the default value for the optional key ``key``. + + :param key: A Boom optional entry key. + :returns: The default value for optional key ``key``. + :rtype: str + """ + if key not in OPTIONAL_KEY_DEFAULTS.keys(): + raise ValueError("Unknown optional BootEntry key: %s" % key) + return OPTIONAL_KEY_DEFAULTS[key] + + +def key_to_bls_name(key): + """Return the BLS key name for the corresponding Boom entry key. + + :param key: A Boom entry key. + :returns: A string representing the BLS key name. + :rtype: str + """ + if key not in KEY_MAP.keys(): + raise ValueError("Unknown BootEntry key: %s" % key) + return KEY_MAP[key] + + +def __make_map_key(key_map): + """Compatibility function to generate a reverse dictionary on + Python 2.6 which does not support dictionary comprehension + notation. + """ + map_key = {} + for k, v in key_map.items(): + map_key[v] = k + return map_key + + +#: Map BLS entry keys to Boom names +MAP_KEY = __make_map_key(KEY_MAP) + +#: Grub2 environment variable expansion character +GRUB2_EXPAND_ENV = "$" + +# Module logging configuration +_log = logging.getLogger(__name__) +_log.set_debug_mask(BOOM_DEBUG_ENTRY) + +_log_debug = _log.debug +_log_debug_entry = _log.debug_masked +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +#: The global list of boot entries. +_entries = None + +#: Pattern for forming root device paths from LVM2 names. +DEV_PATTERN = "/dev/%s" + + +def boom_entries_path(): + """Return the path to the boom profiles directory. + + :returns: The boom profiles path. + :rtype: str + """ + return path_join(get_boot_path(), ENTRIES_PATH) + + +#: Private constants for Grub2 integration checks +#: Paths outside /boot are referenced relative to /boot. +__grub_cfg = "grub2/grub.cfg" +__etc_grub_d = "../etc/grub.d" +__boom_grub_d = "42_boom" +__etc_default = "../etc/default" +__boom_defaults = "boom" + + +def check_bootloader(): + """Check the configuration state of the system bootloader to ensure + that Boom integration is enabled. Currently only Grub2 with the + Red Hat BLS patches is supported. + """ + boot_path = get_boot_path() + + grub_cfg = path_join(boot_path, __grub_cfg) + if not path_exists(grub_cfg): + _log_warn("No Grub2 configuration file found") + return False + + boom_grub_d = path_join(boot_path, __etc_grub_d, __boom_grub_d) + if not path_exists(boom_grub_d): + _log_warn("Boom grub2 script missing from '%s'" % __etc_grub_d) + return False + + defaults_file = path_join(boot_path, __etc_default, __boom_defaults) + if not path_exists(defaults_file): + _log_warn("Boom configuration file missing from '%s'" % defaults_file) + return False + + def is_yes(val): + return val == "y" or val == "yes" + + submenu_enabled = False + with open(defaults_file, "r") as dfile: + for line in dfile: + (name, value) = parse_name_value(line) + if name == "BOOM_ENABLE_GRUB" and not is_yes(value): + _log_warn("Boom grub2 integration is disabled in '%s'" % + defaults_file) + if name == "BOOM_USE_SUBMENU" and is_yes(value): + _log_info("Boom grub2 submenu support enabled") + submenu_enabled = True + if name == "BOOM_SUBMENU_NAME" and submenu_enabled: + _log_info("Boom grub2 submenu name is '%s'" % value) + + found_boom_grub = False + found_bls = False + blscfg = "blscfg" + with open(grub_cfg) as gfile: + for line in gfile: + if blscfg in line: + _log_info("Found BLS import statement in '%s'" % grub_cfg) + found_bls = True + if "BEGIN" in line and __boom_grub_d in line: + _log_info("Found Boom Grub2 integration in '%s'" % grub_cfg) + found_boom_grub = True + + return found_boom_grub or found_bls + + +class BoomRootDeviceError(BoomError): + """Boom exception indicating an invalid root device. + """ + pass + + +def check_root_device(dev): + """Test for the presence of root device ``dev`` and return if it + exists in the configured /dev directory and is a valid block + device, or raise ``BoomRootDeviceError`` otherwise. + + The exception string indicates the class of error: missing + path or not a block device. + + :param dev: the root device to check for. + :raises: BoomRootDeviceError if ``dev`` is invalid. + :returns: None + """ + if not path_exists(dev): + raise BoomRootDeviceError("Device '%s' not found." % dev) + + st = stat(dev) + if not S_ISBLK(st.st_mode): + raise BoomRootDeviceError("Path '%s' is not a block device." % dev) + + +def _match_root_lv(root_device, rd_lvm_lv): + """Return ``True`` if ``rd_lvm_lv`` is the logical volume + represented by ``root_device`` or ``False`` otherwise. + + The root_device for an LVM2 LV may be in one of two possible + forms: + + root_device=/dev/mapper/vg-lv + root_device=/dev/vg/lv + + """ + def dm_split_name(name): + for i in range(1, len(name)): + if name[i] == '-': + if name[i - 1] != '-' and name[i + 1] != '-': + return (name[0:i], name[i + 1:]) + + # root_device=/dev/vg/lv + if rd_lvm_lv == root_device[5:]: + return True + if "mapper" in root_device: + (vg, lv) = dm_split_name(root_device.split("/")[-1]) + if rd_lvm_lv == "%s/%s" % (vg, lv): + return True + return False + + +def _grub2_get_env(name): + """Return the value of the Grub2 environment variable with name + ``name`` as a string. + + :param name: The name of the environment variable to return. + :returns: The value of the named environment variable. + :rtype: string + """ + grub_cmd = ["grub2-editenv", "list"] + try: + p = Popen(grub_cmd, stdin=None, stdout=PIPE, stderr=PIPE) + out = p.communicate()[0] + except OSError as e: + _log_error("Could not obtain grub2 environment: %s" % e) + return "" + + for line in out.splitlines(): + (env_name, value) = line.split('=', 1) + if name == env_name: + return value.strip() + return "" + + +def _expand_vars(args): + """Expand a ``BootEntry`` option string that may contain + references to Grub2 environment variables using shell + style ``$value`` notation. + """ + var_char = GRUB2_EXPAND_ENV + if var_char not in args: + return args + + for arg in args.split(): + if arg.startswith(var_char): + env_name = arg[1:] + args = args.replace(arg, _grub2_get_env(env_name)) + return args + + +class BootParams(object): + """The ``BootParams`` class encapsulates the information needed to + boot an instance of the operating system: the kernel version, + root device, and root device options. + + A ``BootParams`` object is used to configure a ``BootEntry`` + and to generate configuration keys for the entry based on an + attached OsProfile. + """ + #: The kernel version of the instance. + _version = None + + #: The path to the root device + _root_device = None + + #: The LVM2 logical volume containing the root file system + _lvm_root_lv = None + + #: The BTRFS subvolume path to be used as the root file system. + _btrfs_subvol_path = None + + #: The ID of the BTRFS subvolume to be used as the root file system. + _btrfs_subvol_id = None + + #: A list of additional kernel options to append + _add_opts = [] + + #: A list of kernel options to drop + _del_opts = [] + + #: Generation counter for dirty detection + generation = 0 + + def __str(self, quote=False, prefix="", suffix=""): + """Format BootParams as a string. + + Format this ``BootParams`` object as a string, with optional + prefix, suffix, and value quoting. + + :param quote: A bool indicating whether to quote values. + :param prefix: An optional prefix string to be concatenated + with the start of the formatted string. + :param suffix: An optional suffix string to be concatenated + with the end of the formatted string. + :returns: a formatted representation of this ``BootParams``. + :rtype: string + """ + bp_str = prefix + + fields = ["version", "root_device", "lvm_root_lv", + "btrfs_subvol_path", "btrfs_subvol_id"] + params = ( + self.root_device, + self.lvm_root_lv, + self.btrfs_subvol_path, self.btrfs_subvol_id + ) + + # arg + bp_str += self.version if not quote else '"%s"' % self.version + bp_str += ", " + + # kwargs + + bp_fmt = "%s=%s, " if not quote else '%s="%s", ' + for fv in [fv for fv in zip(fields[1:], params) if fv[1]]: + bp_str += bp_fmt % fv + + return bp_str.rstrip(", ") + suffix + + def __str__(self): + """Format BootParams as a human-readable string. + + Format this ``BootParams`` object as a human-readable string. + + :returns: A human readable string representation of this + ``BootParams`` object. + + :rtype: string + """ + return self.__str() + + def __repr__(self): + """Format BootParams as a machine-readable string. + + Format this ``BootParams`` object as a machine-readable + string. The string returned is in the form of a call to the + ``BootParams`` constructor. + + :returns: a machine readable string represenatation of this + ``BootParams`` object. + """ + return self.__str(quote=True, prefix="BootParams(", suffix=")") + + def __init__(self, version, root_device=None, lvm_root_lv=None, + btrfs_subvol_path=None, btrfs_subvol_id=None, + add_opts=None, del_opts=None): + """Initialise a new ``BootParams`` object. + + The root device is specified via the ``root_device`` + argument as a path relative to the root file system. + + The LVM2 logical volume containing the root file system is + specified using ``lvm_root_lv`` if LVM2 is used. + + For instances using LVM2, if the ``lvm_root_lv`` argument is + set and ``root_device`` is unset, ``root_device`` is assumed + to be the normal path of the logical volume specified by the + ``lvm_root_lv`` argument. + + For instances using BTRFS, the ``root_device`` argument is + always required. + + Instances using BTRFS may select a subvolume to be mounted + as the root file system by specifying either the subvolume + path or id via ``btrfs_subvol_path`` and + ``btrfs_subvol_id``. + + ``BootParams()`` raises ValueError if a required argument is + missing, or if conflicting arguments are present. + + :param version: The version string for this BootParams + object. + :param root_device: The root device for this BootParams + object. + :param lvm_root_lv: The LVM2 logical volume containing the + root file system, for systems that use + LVM. + :param btrfs_subvol_path: The BTRFS subvolume path + containing the root file system, + for systems using BTRFS. + :param btrfs_subvol_id: The BTRFS subvolume ID containing + the root file system, for systems + using BTRFS. + :param add_opts: A list containing additional kernel + options to be appended to the command line. + :param del_opts: A list containing kernel options to be + dropped from the command line. + :returns: a newly initialised BootParams object. + :rtype: class BootParams + :raises: ValueError + """ + if not version: + raise ValueError("version argument is required.") + + self.version = version + + if root_device: + self.root_device = root_device + + if lvm_root_lv: + if not root_device: + self.root_device = DEV_PATTERN % lvm_root_lv + self.lvm_root_lv = lvm_root_lv + + if btrfs_subvol_path and btrfs_subvol_id: + raise ValueError("Only one of btrfs_subvol_path and " + "btrfs_subvol_id allowed.") + + if btrfs_subvol_path: + self.btrfs_subvol_path = btrfs_subvol_path + if btrfs_subvol_id: + self.btrfs_subvol_id = btrfs_subvol_id + + self.add_opts = add_opts or [] + self.del_opts = del_opts or [] + + _log_debug_entry("Initialised %s" % repr(self)) + + # We have to use explicit properties for BootParam attributes since + # we need to track modifications to the BootParams values to allow + # a containing BootEntry to mark itself as dirty. + + @property + def version(self): + """Return this ``BootParams`` object's version. + """ + return self._version + + @version.setter + def version(self, value): + """Set this ``BootParams`` object's version. + """ + self.generation += 1 + self._version = value + + @property + def root_device(self): + """Return this ``BootParams`` object's root_device. + """ + return self._root_device + + @root_device.setter + def root_device(self, value): + """Set this ``BootParams`` object's root_device. + """ + self.generation += 1 + self._root_device = value + + @property + def lvm_root_lv(self): + """Return this ``BootParams`` object's lvm_root_lv. + """ + return self._lvm_root_lv + + @lvm_root_lv.setter + def lvm_root_lv(self, value): + """Set this ``BootParams`` object's lvm_root_lv. + """ + self.generation += 1 + self._lvm_root_lv = value + + @property + def btrfs_subvol_path(self): + """Return this ``BootParams`` object's btrfs_subvol_path. + """ + return self._btrfs_subvol_path + + @btrfs_subvol_path.setter + def btrfs_subvol_path(self, value): + """Set this ``BootParams`` object's btrfs_subvol_path. + """ + self.generation += 1 + self._btrfs_subvol_path = value + + @property + def btrfs_subvol_id(self): + """Return this ``BootParams`` object's btrfs_subvol_id. + """ + return self._btrfs_subvol_id + + @btrfs_subvol_id.setter + def btrfs_subvol_id(self, value): + """Set this ``BootParams`` object's btrfs_subvol_id. + """ + self.generation += 1 + self._btrfs_subvol_id = value + + @property + def add_opts(self): + """Return this ``BootParams`` object's add_opts. + """ + return self._add_opts + + @add_opts.setter + def add_opts(self, value): + """Set this ``BootParams`` object's add_opts. + """ + self.generation += 1 + self._add_opts = value + + @property + def del_opts(self): + """Return this ``BootParams`` object's del_opts. + """ + return self._del_opts + + @del_opts.setter + def del_opts(self, value): + """Set this ``BootParams`` object's del_opts. + """ + self.generation += 1 + self._del_opts = value + + def has_btrfs(self): + """Return ``True`` if this BootParams object is configured to + use BTRFS. + + :returns: True if BTRFS is in use, or False otherwise + :rtype: bool + """ + return any((self.btrfs_subvol_id, self.btrfs_subvol_path)) + + def has_lvm2(self): + """Return ``True`` if this BootParams object is configured to + use LVM2. + + :returns: True if LVM2 is in use, or False otherwise + :rtype: bool + """ + return self.lvm_root_lv is not None and len(self.lvm_root_lv) + + @classmethod + def from_entry(cls, be): + """Recover BootParams from BootEntry. + + Recover BootParams values from a templated BootEntry: each + key subject to template substitution is transformed into a + regular expression, matching the element and capturing the + corresponding BootParams value. + + A BootEntry object that has no attached OsProfile cannot be + reversed since no templates exist to match the entry against: + in this case None is returned but no exception is raised. + The entry may be modified and re-written, but no templating + is possible unless a new, valid, OsProfile is attached. + + :param be: The BootEntry to recover BootParams from. + :returns: A newly initialised BootParams object. + :rtype: ``BootParams`` + :raises: ValueError if expected values cannot be matched. + """ + osp = be._osp + # Version is written directly from BootParams + version = be.version + bp = BootParams(version) + matches = {} + + _log_debug_entry("Initialising BootParams() from " + "BootEntry(boot_id='%s')" % be.boot_id) + + opts_regexes = osp.make_format_regexes(osp.options) + if not opts_regexes: + return None + + _log_debug_entry("Matching options regex list with %d entries" % + len(opts_regexes)) + _log_debug_entry("Options regex list: %s" % str(opts_regexes)) + + for rgx_word in opts_regexes: + (name, exp) = rgx_word + value = "" + for word in be.expand_options.split(): + match = re.search(exp, word) if name else re.match(exp, word) + if match: + matches[word] = True + if len(match.groups()): + value = match.group(1) + _log_debug_entry("Matched: '%s' (%s)" % + (value, name)) + if name == "lvm_root_lv": + if not _match_root_lv(bp.root_device, value): + continue + setattr(bp, name, value) + continue + + # The root_device key is handled specially since it is required + # for a valid BootEntry. + if name == 'root_device' and not value: + _log_warn("Entry with boot_id=%s has no root_device" + % be.boot_id) + setattr(bp, name, "") + + def is_add(opt): + """Return ``True`` if ``opt`` was appended to this options line, + and was not generated from the active ``OsProfile`` template, + or from expansion of a bootloader environment variable. + """ + def opt_in_expansion(opt): + """Return ``True`` if ``opt`` is contained in the expansion of + a bootloader environment variable embedded in this entry's + options string. + + :param opt: A kernel command line option. + :returns: ``True`` if ``opt`` is defined in a bootloader + environment variable, or ``False`` otherwise. + :rtype: bool + """ + if GRUB2_EXPAND_ENV not in be.options: + return False + return opt not in _expand_vars(be.options) + + if opt not in matches.keys(): + if opt not in be._osp.options: + if not opt_in_expansion(opt): + return True + return False + + def is_del(opt): + """Return ``True`` if the option regex `opt` has been deleted + from this options line. An option is dropped if it is in + the ``OsProfile`` template and is absent from the option + line. + + Optional boot parameters (e.g. rd.lvm.lv and rootflags) + are ignored since these are only templated when the + corresponding boot parameter is set. + + The fact that an option is dropped is recorded for later + templating operations. + """ + # Ignore optional boot parameters + ignore_bp = ['rootflags', 'rd.lvm.lv', 'subvol', 'subvolid'] + opt_name = opt.split('=')[0] + matched_opts = [k.split('=')[0] for k in matches.keys()] + if opt_name not in matched_opts and opt_name not in ignore_bp: + return True + return False + + # Compile list of unique non-template options + bp.add_opts = [opt for opt in be.options.split() if is_add(opt)] + bp.add_opts = list(set(bp.add_opts)) + + # Compile list of deleted template options + bp.del_opts = [o for o in [r[1] for r in opts_regexes] if is_del(o)] + + _log_debug_entry("Parsed %s" % repr(bp)) + + return bp + + +def _add_entry(entry): + """Add a new entry to the list of loaded on-disk entries. + + :param entry: The ``BootEntry`` to add. + """ + global _entries + if _entries is None: + load_entries() + if entry not in _entries: + _entries.append(entry) + + +def _del_entry(entry): + """Remove a ``BootEntry`` from the list of loaded entries. + + :param entry: The ``BootEntry`` to remove. + """ + global _entries + _entries.remove(entry) + + +def drop_entries(): + """Drop all in-memory entries. + + Clear the list of in-memory entries and reset the BootEntry + list to the default state. + + :returns: None + """ + global _entries + _entries = [] + + +def load_entries(machine_id=None): + """ Load boot entries into memory. + + Load boot entries from ``boom.bootloader.boom_entries_path()``. + + If ``machine_id`` is specified only matching entries will be + considered. + + :param machine_id: A ``machine_id`` value to match. + """ + global _entries + if not profiles_loaded(): + load_profiles() + + entries_path = boom_entries_path() + + drop_entries() + + _log_info("Loading boot entries from '%s'" % entries_path) + for entry in listdir(entries_path): + if not entry.endswith(".conf"): + continue + if machine_id and machine_id not in entry: + _log_debug_entry("Skipping entry with machine_id!='%s'", + machine_id) + continue + entry_path = path_join(entries_path, entry) + try: + _add_entry(BootEntry(entry_file=entry_path)) + except Exception as e: + _log_info("Could not load BootEntry '%s': %s" % + (entry_path, e)) + + _log_info("Loaded %d entries" % len(_entries)) + + +def write_entries(): + """Write out boot entries. + + Write all currently loaded boot entries to + ``boom.bootloader.boom_entries_path()``. + """ + global _entries + for be in _entries: + try: + be.write_entry() + except Exception as e: + _log_warn("Could not write BootEntry(boot_id='%s'): %s" % + (be.disp_boot_id, e)) + + +def min_boot_id_width(): + """Calculate the minimum unique width for boot_id values. + + Calculate the minimum width to ensure uniqueness when displaying + boot_id values. + + :returns: the minimum boot_id width. + :rtype: int + """ + return min_id_width(7, _entries, "boot_id") + + +def select_params(s, bp): + """Test BootParams against Selection criteria. + + Test the supplied ``BootParams`` against the selection criteria + in ``s`` and return ``True`` if it passes, or ``False`` + otherwise. + + :param s: Selection criteria + :param bp: The BootParams to test + :rtype: bool + :returns: True if BootParams passes selection or ``False`` + otherwise. + """ + if s.root_device and s.root_device != bp.root_device: + return False + if s.lvm_root_lv and s.lvm_root_lv != bp.lvm_root_lv: + return False + if s.btrfs_subvol_path and s.btrfs_subvol_path != bp.btrfs_subvol_path: + return False + if s.btrfs_subvol_id and s.btrfs_subvol_id != bp.btrfs_subvol_id: + return False + + return True + + +def select_entry(s, be): + """Test BootEntry against Selection criteria. + + Test the supplied ``BootEntry`` against the selection criteria + in ``s`` and return ``True`` if it passes, or ``False`` + otherwise. + + :param s: The selection criteria + :param be: The BootEntry to test + :rtype: bool + :returns: True if BootEntry passes selection or ``False`` + otherwise. + """ + if not select_profile(s, be._osp): + return False + + if s.boot_id and not be.boot_id.startswith(s.boot_id): + return False + if s.title and be.title != s.title: + return False + if s.version and be.version != s.version: + return False + if s.machine_id and be.machine_id != s.machine_id: + return False + + if not select_params(s, be.bp): + return False + + return True + + +def find_entries(selection=None): + """Find boot entries matching selection criteria. + + Return a list of ``BootEntry`` objects matching the specified + criteria. Matching proceeds as the logical 'and' of all criteria. + Criteria that are unset (``None``) are ignored. + + If no ``BootEntry`` matches the specified criteria the empty list + is returned. + + Boot entries will be automatically loaded from disk if they are + not already in memory. + + :param selection: A ``Selection`` object specifying the match + criteria for the operation. + :returns: a list of ``BootEntry`` objects. + :rtype: list + """ + global _entries + + if not _entries: + load_entries() + + matches = [] + + # Use null search criteria if unspecified + selection = selection if selection else Selection() + + selection.check_valid_selection(entry=True, params=True, profile=True) + + _log_debug_entry("Finding entries for %s" % repr(selection)) + + for be in _entries: + if select_entry(selection, be): + matches.append(be) + _log_debug_entry("Found %d entries" % len(matches)) + return matches + + +def _transform_key(key_name): + """Transform key characters between Boom and BLS notation. + + Transform all occurrences of '_' in ``key_name`` to '-' or vice + versa. + + Key names on-disk use a hyphen as the word separator, for e.g. + "machine-id". We cannot use this character for Python attributes + since it collides with the subtraction operator. + + :param key_name: The key name to be transformed. + + :returns: The transformed key name. + + :rtype: string + """ + _exclude_keys = OPTIONAL_KEYS + + # Red Hat's non-upstream BLS keys use '_', rather than '-' (unlike + # the standard BLS keys). + if key_name in MAP_KEY and MAP_KEY[key_name] in _exclude_keys: + return key_name + + if key_name in ["grub_users", "grub_class", "grub_arg"]: + return key_name + if "_" in key_name: + return key_name.replace("_", "-") + if "-" in key_name: + return key_name.replace("-", "_") + return key_name + + +class BootEntry(object): + """A class representing a BLS compliant boot entry. + + A ``BootEntry`` exposes two sets of properties that are the + keys of a BootLoader Specification boot entry. + + The properties of a ``BootEntry`` that is not associated with an + ``OsProfile`` (for e.g. one read from disk) are the literal + values read from a file or set through the API. + + When an ``OSProfile`` is attached to a ``BootEntry``, it is used + as a template to fill out the values of keys for properties + including the kernel and initramfs file name. This is used to + create new ``BootEntry`` objects to be written to disk. + + An ``OsProfile`` can be attached to a ``BootEntry`` when it is + created, or at a later time by calling the ``set_os_profile()`` + method. + """ + _entry_data = None + _unwritten = False + _last_path = None + _comments = None + _osp = None + _bp = None + _bp_generation = None + + # Read only state for foreign BLS entries + read_only = False + + # boot_id cache + __boot_id = None + + def __str(self, quote=False, prefix="", suffix="", tail="\n", + sep=" ", bls=True, no_boot_id=False, expand=False): + """Format BootEntry as a string. + + Return a human or machine readable representation of this + BootEntry. + + :param quote: True if values should be quoted or False otherwise. + + :param prefix:An optional prefix string to be concatenated with + with the start of the formatted string. + + :param suffix: An optional suffix string to be concatenated + with the end of the formatted string. + + :param tail: A string to be concatenated between subsequent + records in the formatted string. + + :param sep: A separator to be inserted between each name and + value. Normally either ' ' or '='. + + :param bls: Generate output using BootLoader Specification + syntax and key names. + + :param no_boot_id: Do not include the BOOM_ENTRY_BOOT_ID key in the + returned string. Used internally in + order to avoid recursion when calculating + the BOOM_ENTRY_BOOT_ID checksum. + + :returns: A string representation. + + :rtype: string + """ + be_str = prefix + + for key in [k for k in ENTRY_KEYS if getattr(self, KEY_MAP[k])]: + attr = KEY_MAP[key] + key_fmt = '%s%s"%s"' if quote else '%s%s%s' + key_fmt += tail + + if attr == "options" and expand: + attr_val = getattr(self, "expand_options") + else: + attr_val = getattr(self, attr) + + if bls: + key_data = (_transform_key(attr), sep, attr_val) + else: + key_data = (key, sep, attr_val) + be_str += key_fmt % key_data + + # BOOM_ENTRY_BOOT_ID requires special handling to avoid + # recursion from the boot_id property method (which uses the + # string representation of the object to calculate the + # checksum). + if not bls and not no_boot_id: + key_fmt = ('%s%s"%s"' if quote else '%s%s%s') + tail + boot_id_data = [BOOM_ENTRY_BOOT_ID, sep, self.boot_id] + be_str += key_fmt % tuple(boot_id_data) + + return be_str.rstrip(tail) + suffix + + def __str__(self): + """Format BootEntry as a human-readable string in BLS notation. + + Format this BootEntry as a string containing a BLS + configuration snippet. + + :returns: a BLS configuration snippet corresponding to this entry. + + :rtype: string + """ + return self.__str() + + def __repr__(self): + """Format BootEntry as a machine-readable string. + + Return a machine readable representation of this BootEntry, + in constructor notation. + + :returns: A string in BootEntry constructor syntax. + + :rtype: str + """ + return self.__str(quote=True, prefix="BootEntry(entry_data={", + suffix="})", tail=", ", sep=": ", bls=False) + + def __len__(self): + """Return the length (key count) of this ``BootEntry``. + + :returns: the ``BootEntry`` length as an integer. + :rtype: ``int`` + """ + return len(self._entry_data) + + def __eq__(self, other): + """Test for equality between this ``BootEntry`` and another + object. + + Equality for ``BootEntry`` objects is true if the both + ``boot_id`` values match. + + :param other: The object against which to test. + + :returns: ``True`` if the objects are equal and ``False`` + otherwise. + :rtype: bool + """ + if not hasattr(other, "boot_id"): + return False + if self.boot_id == other.boot_id: + return True + return False + + def __getitem__(self, key): + """Return an item from this ``BootEntry``. + + :returns: the item corresponding to the key requested. + :rtype: the corresponding type of the requested key. + :raises: TypeError if ``key`` is of an invalid type. + KeyError if ``key`` is valid but not present. + """ + if not isinstance(key, str): + raise TypeError("BootEntry key must be a string.") + + if key in self._entry_data: + return self._entry_data[key] + if key == BOOM_ENTRY_LINUX: + return self.linux + if key == BOOM_ENTRY_INITRD: + return self.initrd + if key == BOOM_ENTRY_OPTIONS: + return self.options + if key == BOOM_ENTRY_DEVICETREE: + return self.devicetree + if key == BOOM_ENTRY_EFI: + return self.efi + if key == BOOM_ENTRY_BOOT_ID: + return self.boot_id + if self.bp and key == BOOM_ENTRY_VERSION: + return self.bp.version + + raise KeyError("BootEntry key %s not present." % key) + + def __setitem__(self, key, value): + """Set the specified ``BootEntry`` key to the given value. + + :param key: the ``BootEntry`` key to be set. + :param value: the value to set for the specified key. + """ + if not isinstance(key, str): + raise TypeError("BootEntry key must be a string.") + + if key == BOOM_ENTRY_VERSION and self.bp: + self.bp.version = value + elif key == BOOM_ENTRY_LINUX and self.bp: + self.linux = value + elif key == BOOM_ENTRY_INITRD and self.bp: + self.initrd = value + elif key == BOOM_ENTRY_OPTIONS and self.bp: + self.options = value + elif key == BOOM_ENTRY_DEVICETREE and self.bp: + self.devicetree = value + elif key == BOOM_ENTRY_EFI and self.bp: + self.efi = value + elif key == BOOM_ENTRY_BOOT_ID: + raise TypeError("'boot_id' property does not support assignment") + elif key in self._entry_data: + self._entry_data[key] = value + else: + raise KeyError("BootEntry key %s not present." % key) + + def keys(self): + """Return the list of keys for this ``BootEntry``. + + Return a copy of this ``BootEntry``'s keys as a list of + key name strings. + + :returns: the current list of ``BotoEntry`` keys. + :rtype: list of str + """ + keys = list(self._entry_data.keys()) + add_keys = [BOOM_ENTRY_LINUX, BOOM_ENTRY_INITRD, BOOM_ENTRY_OPTIONS] + + # Sort the item list to give stable list ordering on Py3. + keys = sorted(keys, reverse=True) + + if self.bp: + add_keys.append(BOOM_ENTRY_VERSION) + + for k in add_keys: + if k not in self._entry_data: + keys.append(k) + + return keys + + def values(self): + """Return the list of values for this ``BootEntry``. + + Return a copy of this ``BootEntry``'s values as a list. + + :returns: the current list of ``BotoEntry`` values. + :rtype: list + """ + values = list(self._entry_data.values()) + add_values = [self.linux, self.initrd, self.options] + + # Sort the item list to give stable list ordering on Py3. + values = sorted(values, reverse=True) + + if self.bp: + add_values.append(self.version) + + return values + add_values + + def items(self): + """Return the items list for this BootEntry. + + Return a copy of this ``BootEntry``'s ``(key, value)`` + pairs as a list. + + :returns: the current list of ``BotoEntry`` items. + :rtype: list of ``(key, value)`` tuples. + """ + items = list(self._entry_data.items()) + + add_items = [ + (BOOM_ENTRY_LINUX, self.linux), + (BOOM_ENTRY_INITRD, self.initrd), + (BOOM_ENTRY_OPTIONS, self.options) + ] + + if self.bp: + add_items.append((BOOM_ENTRY_VERSION, self.version)) + + # Sort the item list to give stable list ordering on Py3. + items = sorted(items, key=lambda i: i[0], reverse=True) + + return items + add_items + + def _dirty(self): + """Mark this ``BootEntry`` as needing to be written to disk. + + A newly created ``BootEntry`` object is always dirty and + a call to its ``write_entry()`` method will always write + a new boot entry file. Writes may be avoided for entries + that are not marked as dirty. + + A clean ``BootEntry`` is marked as dirty if a new value + is written to any of its writable properties. + + :rtype: None + """ + if self.read_only: + raise ValueError("Entry with boot_id='%s' is read-only." % + self.disp_boot_id) + + # Clear cached boot_id: it will be regenerated on next access + self.__boot_id = None + self._unwritten = True + + def __os_id_from_comment(self, comment): + """Retrive OsProfile from BootEntry comment. + + Attempt to set this BootEntry's OsProfile using a comment + string stored in the entry file. The comment must be of the + form "OsIdentifier: ". If found the value is treated + as authoritative and a reference to the corresponding + ``OsProfile`` is stored in the object's ``_osp`` member. + + Any comment lines that do not contain an OsIdentifier tag + are returned as a multi-line string. + + :param comment: The comment to attempt to parse + :returns: Comment lines not containing an OsIdentifier + :rtype: str + """ + if "OsIdentifier:" not in comment: + return + + outlines = "" + for line in comment.splitlines(): + (key, os_id) = line.split(":") + os_id = os_id.strip() + osp = get_os_profile_by_id(os_id) + + # An OsIdentifier comment is automatically added to the + # entry when it is written: do not add the read value to + # the comment list. + if not self._osp and osp: + self._osp = osp + _log_debug_entry("Parsed os_id='%s' from comment" % + osp.disp_os_id) + else: + outlines += line + "\n" + return outlines + + def __match_os_profile(self): + """Attempt to find a matching OsProfile for this BootEntry. + + Attempt to guess the correct ``OsProfile`` to use with + this ``BootEntry`` by probing each loaded ``OsProfile`` + in turn until a profile recognises the entry. If no match + is found the entrie's ``OsProfile`` is set to ``None``. + + Probing is only used in the case that a loaded entry has + no embedded OsIdentifier string. All entries written by + Boom include the OsIdentifier value: probing is primarily + useful for entries that have been manually written or + edited. + """ + self._osp = match_os_profile(self) + + def __match_host_profile(self): + """Attempt to find a matching HostProfile for this BootEntry. + + Try to find a ``HostProfile`` with a matching machine_id, + and if one is found, wrap this ``BootEntry``'s operating + system profile with the host. + + This method must be called with a valid ``BootParams`` + object attached. + """ + if BOOM_ENTRY_MACHINE_ID in self._entry_data: + machine_id = self._entry_data[BOOM_ENTRY_MACHINE_ID] + hps = find_host_profiles(Selection(machine_id=machine_id)) + self._osp = hps[0] if hps else self._osp + + # Import add/del options from HostProfile if attached. + if hasattr(self._osp, "add_opts"): + self.bp.add_opts = self._osp.add_opts.split() + + if hasattr(self._osp, "del_opts"): + self.bp.del_opts = self._osp.del_opts.split() + + def __from_data(self, entry_data, boot_params): + """Initialise a new BootEntry from in-memory data. + + Initialise a new ``BootEntry`` object with data from the + dictionary ``entry_data`` (and optionally the supplied + ``BootParams`` object). The supplied dictionary should be + indexed by Boom entry key names (``BOOM_ENTRY_*``). + + Raises ``ValueError`` if required keys are missing + (``BOOM_ENTRY_TITLE``, and either ``BOOM_ENTRY_LINUX`` or + ``BOOM_ENTRY_EFI``). + + This method should not be called directly: to build a new + ``BootEntry`` object from in-memory data, use the class + initialiser with the ``entry_data`` argument. + + :param entry_data: A dictionary mapping Boom boot entry key + names to values + :param boot_params: Optional BootParams to attach to the new + BootEntry object + :returns: None + :rtype: None + :raises: ValueError + """ + if BOOM_ENTRY_TITLE not in entry_data: + raise ValueError("BootEntry missing BOOM_ENTRY_TITLE") + + if BOOM_ENTRY_LINUX not in entry_data: + if BOOM_ENTRY_EFI not in entry_data: + raise ValueError("BootEntry missing BOOM_ENTRY_LINUX or" + " BOOM_ENTRY_EFI") + + self._entry_data = {} + for key in [k for k in ENTRY_KEYS if k in entry_data]: + self._entry_data[key] = entry_data[key] + + if not self._osp: + self.__match_os_profile() + + self.machine_id = self.machine_id or "" + self.architecture = self.architecture or "" + + if boot_params: + self.bp = boot_params + # boot_params is always authoritative + self._entry_data[BOOM_ENTRY_VERSION] = self.bp.version + else: + # Attempt to recover BootParams from entry data + self.bp = BootParams.from_entry(self) + + if self.machine_id: + # Wrap OsProfile in HostProfile if available + self.__match_host_profile() + + if self.bp: + def _pop_if_set(key): + if key in _entry_data: + if _entry_data[key] == getattr(self, KEY_MAP[key]): + _entry_data.pop(key) + + # Copy the current _entry_data and clear self._entry_data to + # allow comparison of stored value with template. + _entry_data = self._entry_data + self._entry_data = {} + + # Clear templated keys from _entry_data and if the value + # read from entry_data is identical to that generated by the + # current OsProfile and BootParams. + _pop_if_set(BOOM_ENTRY_VERSION) + _pop_if_set(BOOM_ENTRY_LINUX) + _pop_if_set(BOOM_ENTRY_INITRD) + _pop_if_set(BOOM_ENTRY_OPTIONS) + self._entry_data = _entry_data + + def __from_file(self, entry_file, boot_params): + """Initialise a new BootEntry from on-disk data. + + Initialise a new ``BootEntry`` using the entry data in + ``entry_file`` (and optionally the supplied ``BootParams`` + object). + + Raises ``ValueError`` if required keys are missing + (``BOOM_ENTRY_TITLE``, and either ``BOOM_ENTRY_LINUX`` or + ``BOOM_ENTRY_EFI``). + + This method should not be called directly: to build a new + ``BootEntry`` object from entry file data, use the class + initialiser with the ``entry_file`` argument. + + :param entry_file: The path to a file containing a BLS boot + entry + :param boot_params: Optional BootParams to attach to the new + BootEntry object + :returns: None + :rtype: None + :raises: ValueError + """ + entry_data = {} + comments = {} + comment = "" + + entry_basename = basename(entry_file) + _log_debug("Loading BootEntry from '%s'" % entry_basename) + + with open(entry_file, "r") as ef: + for line in ef: + if blank_or_comment(line): + comment += line if line else "" + else: + bls_key, value = parse_name_value(line, separator=None) + # Convert BLS key name to Boom notation + key = _transform_key(bls_key) + if key not in MAP_KEY: + raise LookupError("Unknown BLS key '%s'" % bls_key) + key = MAP_KEY[_transform_key(bls_key)] + entry_data[key] = value + if comment: + comment = self.__os_id_from_comment(comment) + if not comment: + continue + comments[key] = comment + comment = "" + self._comments = comments + + self.__from_data(entry_data, boot_params) + + match = re.match(BOOT_ENTRIES_PATTERN, entry_basename) + if not match or len(match.groups()) <= 1: + _log_info("Marking unknown boot entry as read-only: %s" % + entry_basename) + self.read_only = True + else: + if self.disp_boot_id != match.group(2): + _log_info("Entry file name does not match boot_id: %s" % + entry_basename) + + self._last_path = entry_file + self._unwritten = False + + def __init__(self, title=None, machine_id=None, osprofile=None, + boot_params=None, entry_file=None, entry_data=None, + architecture=None, allow_no_dev=False): + """Initialise new BootEntry. + + Initialise a new ``BootEntry`` object from the specified + file or using the supplied values. + + If ``osprofile`` is specified the profile is attached to the + new ``BootEntry`` and will be used to supply templates for + ``BootEntry`` values. + + A ``BootParams`` object may be supplied using the + ``boot_params`` keyword argument. The object will be used to + provide values for subsitution using the patterns defined by + the configured ``OsProfile``. + + If ``entry_file`` is specified the ``BootEntry`` will be + initialised from the values found in the file, which should + contain a valid BLS snippet in UTF-8 encoding. The file may + contain blank lines and comments (lines beginning with '#'), + and these will be preserved if the entry is re-written. + + If ``entry_file`` is not specified, both ``title`` and + ``machine_id`` must be given. + + The ``entry_data`` keyword argument is an optional argument + used to initialise a ``BootEntry`` from a dictionary mapping + ``BOOM_ENTRY_*`` keys to ``BootEntry`` values. It may be used to + initialised a new ``BootEntry`` using the strings obtained + from a call to ``BootEntry.__repr__()``. + + :param title: The title for this ``BootEntry``. + + :param machine_id: The ``machine_id`` of this ``BootEntry``. + + :param osprofile: An optional ``OsProfile`` to attach to + this ``BootEntry``. + + :param boot_params: An optional ``BootParams`` object to + initialise this ``BooyEntry``. + + :param entry_file: An optional path to a file in the file + system containing a boot entry in BLS + notation. + + :param entry_data: An optional dictionary of ``BootEntry`` + key to value mappings to initialise + this ``BootEntry`` from. + + :param architecture: An optional BLS architecture string. + + :returns: A new ``BootEntry`` object. + + :rtype: BootEntry + """ + # An osprofile kwarg always takes precedent over either an + # 'OsIdentifier' comment or a matched osprofile value. + self._osp = osprofile + + if entry_data: + self.__from_data(entry_data, boot_params) + return + if entry_file: + self.__from_file(entry_file, boot_params) + return + + self._unwritten = True + + self.bp = boot_params + + # The BootEntry._entry_data dictionary contains data for an existing + # BootEntry that has been read from disk, as well as any overridden + # fields for a new BootEntry with an OsProfile attached. + self._entry_data = {} + + def title_empty(osp, title): + if osp and not osp.title: + return True + elif not osp and not title: + return True + return False + + if title: + self.title = title + elif title_empty(self._osp, title): + raise ValueError("BootEntry title cannot be empty") + + self.machine_id = machine_id or "" + self.architecture = architecture or "" + + if not self._osp: + self.__match_os_profile() + + if self.machine_id: + # Wrap OsProfile in HostProfile if available + self.__match_host_profile() + + if self.bp: + if not allow_no_dev: + check_root_device(self.bp.root_device) + + def _apply_format(self, fmt): + """Apply key format string substitution. + + Apply format key substitution to format string ``fmt``, + using values provided by an attached ``BootParams`` object, + and string patterns from either an associated ``OsProfile`` + object, or values set directly in this ``BootEntry``. + + If the source of data for a key is empty or None, the + string is returned unchanged. + + The currently defined format keys are: + + * ``%{version}`` The kernel version string. + * ``%{lvm_root_lv}`` The LVM2 logical volume containing the + root file system. + * ``%{btrfs_subvolume}`` The root flags specifying the BTRFS + subvolume containing the root file system. + * ``%{root_device}`` The device containing the root file + system. + * ``%{root_opts}`` The command line options required for the + root file system. + * ``%{linux}`` The linux image to boot + * ``%{os_name}`` The OS Profile name + * ``%{os_short_name`` The OS Profile short name + * ``%{os_version}`` The OS Profile version + * ``%{os_version id`` The OS Profile version ID + + :param fmt: The string to be formatted. + + :returns: The formatted string + :rtype: str + """ + key_format = "%%{%s}" + bp = self.bp + + if not fmt: + return "" + + # Table-driven key formatting + # + # Each entry in the format_key_specs table specifies a list of + # possible key substitutions to perform for the named key. Each + # entry of the key_spec list contains a dictionary containing + # one or more attribute sources or predicates. + # + # A key substitution is evaluated if at least one of the listed + # attribute sources is defined, and if all defined predicates + # evaluate to True. A predicate must be a Python callable + # accepting no arguments and returning a boolean. A key_spec + # may also specify an explicit list of needed objects, "bp", + # or "osp", that must exist to evaluate predicates. + # + # Several helper functions exist to obtain key values from the + # appropriate data source (accounting for keys that exist in + # multiple objects as well as keys that return None or empty + # values), to test key_spec predicates, and to safely obtain + # function attributes where the containing object may or may + # not exist. + def get_key_attr(key_spec): + """Return a key's value attribute. + + Return a value from either `BootParams`, `OsProfile`, + or `BootEntry`. Each source is tested in order and the + value is taken from the first object type with a value + for the named key. + """ + def have_attr(): + """Test whether any attribute source for this key exists. + """ + attrs_vals = [ + (BP_ATTR, bp), (OSP_ATTR, self._osp), (BE_ATTR, True) + ] + have = False + for attr, source in attrs_vals: + if attr in key_spec: + have |= source is not None + return have + + val_fmt = "%s" if VAL_FMT not in key_spec else key_spec[VAL_FMT] + + if have_attr(): + if BP_ATTR in key_spec and bp: + value = getattr(bp, key_spec[BP_ATTR]) + elif OSP_ATTR in key_spec: + value = getattr(self._osp, key_spec[OSP_ATTR]) + elif BE_ATTR in key_spec: + value = getattr(self, key_spec[BE_ATTR]) + return val_fmt % value if value is not None else None + else: + return None + + def test_predicates(key_spec): + """Test all defined predicate functions and return `True` if + all evaluate `True`, or `False` otherwise. + """ + if PRED_FN not in key_spec: + return True + predicates = key_spec[PRED_FN] + # Ignore invalid predicates + return all([fn() for fn in predicates if fn]) + + def mkpred(obj, fn): + """Return a callable predicate function for method ``fn`` of + object ``obj`` if ``obj`` is valid and contains ``fn``, + or ``None`` otherwise. + + This is used to safely build predicate function lists + whether or not the objects they reference are defined + or not for a given substitution key. + """ + return getattr(obj, fn) if obj else None + + # Key spec constants + BE_ATTR = "be_attr" + BP_ATTR = "bp_attr" + OSP_ATTR = "osp_attr" + PRED_FN = "pred_fn" + VAL_FMT = "val_fmt" + NEEDS = "needs" + + format_key_specs = { + FMT_VERSION: [{BE_ATTR: "version", BP_ATTR: "version"}], + FMT_LVM_ROOT_LV: [{BP_ATTR: "lvm_root_lv"}], + FMT_LVM_ROOT_OPTS: [{OSP_ATTR: "root_opts_lvm2"}], + FMT_BTRFS_ROOT_OPTS: [{OSP_ATTR: "root_opts_btrfs"}], + FMT_BTRFS_SUBVOLUME: [{BP_ATTR: "btrfs_subvol_id", NEEDS: "bp", + PRED_FN: [mkpred(bp, "has_btrfs")], + VAL_FMT: "subvolid=%s"}, + {BP_ATTR: "btrfs_subvol_path", NEEDS: "bp", + PRED_FN: [mkpred(bp, "has_btrfs")], + VAL_FMT: "subvol=%s"}], + FMT_ROOT_DEVICE: [{BP_ATTR: "root_device", NEEDS: "bp"}], + FMT_ROOT_OPTS: [{BE_ATTR: "root_opts", NEEDS: "bp"}], + FMT_KERNEL: [{BE_ATTR: "linux", NEEDS: "bp"}], + FMT_INITRAMFS: [{BE_ATTR: "initrd", NEEDS: "bp"}], + FMT_OS_NAME: [{OSP_ATTR: "os_name"}], + FMT_OS_SHORT_NAME: [{OSP_ATTR: "os_short_name"}], + FMT_OS_VERSION: [{OSP_ATTR: "os_version"}], + FMT_OS_VERSION_ID: [{OSP_ATTR: "os_version_id"}] + } + + for key_name in format_key_specs.keys(): + key = key_format % key_name + if key not in fmt: + continue + for key_spec in format_key_specs[key_name]: + # Check NEEDS + for k in key_spec.keys(): + if k == NEEDS: + if key_spec[k] == "bp" and not bp: + continue + if key_spec[k] == "osp" and not self._osp: + continue + if not test_predicates(key_spec): + continue + # A key value of None means the key should not be substituted: + # this occurs when accessing a templated attribute of an entry + # that has no attached OsProfile (in which case the format key + # is retained in the formatted text). + # + # If the value is not None, but contains the empty string, the + # value is substituted as normal. + value = get_key_attr(key_spec) + if value is None: + continue + fmt = fmt.replace(key, value) + + return fmt + + def __generate_boot_id(self): + """Generate a new boot_id value. + + Generate a new sha1 profile identifier for this entry, + using the title, version, root_device and any defined + LVM2 or BTRFS snapshot parameters. + + :returns: A ``boot_id`` string + :rtype: str + """ + # The default ``str()`` and ``repr()`` behaviour for + # ``BootEntry`` objects includes the ``boot_id`` value. This + # must be disabled in order to generate the ``boot_id`` to + # avoid recursing into __generate_boot_id() from the string + # formatting methods. + # + # Call the underlying ``__str()`` method directly and disable + # the inclusion of the ``boot_id``. + # + # Other callers should always rely on the standard methods. + boot_id = sha1(self.__str(no_boot_id=True).encode('utf-8')).hexdigest() + _log_debug_entry("Generated new boot_id='%s'" % boot_id) + return boot_id + + def _entry_data_property(self, name): + """Return property value from entry data. + + :param name: The boom key name of the property to return + :returns: The property value from the entry data dictionary + """ + if self._entry_data and name in self._entry_data: + return self._entry_data[name] + return None + + def _have_optional_key(self, key): + """Return ``True`` if optional BLS key ``key`` is permitted by + the attached ``OsProfile``, or ``False`` otherwise. + """ + if not self._osp or not self._osp.optional_keys: + return False + if key not in self._osp.optional_keys: + return False + return True + + def expanded(self): + """Return a string represenatation of this ``BootEntry``, with + any bootloader environment variables expanded to their + current values. + + :returns: A string representation of this ``BootEntry``. + :rtype: string + """ + return self.__str(expand=True) + + @property + def bp(self): + """The ``BootParams`` object associated with this ``BootEntry``. + """ + return self._bp + + @bp.setter + def bp(self, value): + """Set the ``BootParams`` object associated with this + ``BootEntry``. + """ + self._dirty() + self._bp = value + self._bp_generation = self._bp.generation if self._bp else 0 + + @property + def disp_boot_id(self): + """The display boot_id of this entry. + + Return the shortest prefix of this BootEntry's boot_id that + is unique within the current set of loaded entries. + + :getter: return this BootEntry's boot_id. + :type: str + """ + return self.boot_id[:min_boot_id_width()] + + @property + def boot_id(self): + """A SHA1 digest that uniquely identifies this ``BootEntry``. + + :getter: return this ``BootEntry``'s ``boot_id``. + :type: string + """ + # Mark ourself dirty if boot parameters have changed. + if self.bp and self.bp.generation != self._bp_generation: + self._bp_generation = self.bp.generation + self._dirty() + if not self.__boot_id or self._unwritten: + self.__boot_id = self.__generate_boot_id() + return self.__boot_id + + @property + def root_opts(self): + """The root options that should be used for this ``BootEntry``. + + :getter: Returns the root options string for this ``BootEntry``. + :type: string + """ + if not self._osp or not self.bp: + return "" + bp = self.bp + osp = self._osp + root_opts = "%s%s%s" + lvm_opts = "" + if bp.lvm_root_lv: + lvm_opts = self._apply_format(osp.root_opts_lvm2) + + btrfs_opts = "" + if bp.btrfs_subvol_id or bp.btrfs_subvol_path: + btrfs_opts += self._apply_format(osp.root_opts_btrfs) + spacer = " " if lvm_opts and btrfs_opts else "" + return root_opts % (lvm_opts, spacer, btrfs_opts) + + @property + def title(self): + """The title of this ``BootEntry``. + + :getter: returns the ``BootEntry`` title. + :setter: sets this ``BootEntry`` object's title. + :type: string + """ + if BOOM_ENTRY_TITLE in self._entry_data: + return self._entry_data_property(BOOM_ENTRY_TITLE) + + if not self._osp or not self.bp: + return "" + + osp = self._osp + return self._apply_format(osp.title) + + @title.setter + def title(self, title): + if not title: + # It is valid to set an empty title in a HostProfile as long + # as the OsProfile defines one. + if not self._osp or not self._osp.title: + raise ValueError("Entry title cannot be empty") + self._entry_data[BOOM_ENTRY_TITLE] = title + self._dirty() + + @property + def machine_id(self): + """The machine_id of this ``BootEntry``. + + :getter: returns this ``BootEntry`` object's ``machine_id``. + :setter: sets this ``BootEntry`` object's ``machine_id``. + :type: string + """ + return self._entry_data_property(BOOM_ENTRY_MACHINE_ID) + + @machine_id.setter + def machine_id(self, machine_id): + self._entry_data[BOOM_ENTRY_MACHINE_ID] = machine_id + self._dirty() + + @property + def version(self): + """The version string associated with this ``BootEntry``. + + :getter: returns this ``BootEntry`` object's ``version``. + :setter: sets this ``BootEntry`` object's ``version``. + :type: string + """ + if self.bp and BOOM_ENTRY_VERSION not in self._entry_data: + return self.bp.version + return self._entry_data_property(BOOM_ENTRY_VERSION) + + @version.setter + def version(self, version): + self._entry_data[BOOM_ENTRY_VERSION] = version + self._dirty() + + def _options(self, expand=False): + """The command line options for this ``BootEntry``, optionally + expanding any bootloader environment variables to their + current values. + + :param expand: Whether or not to expand bootloader + environment variable references. + :rtype: string + """ + + def add_opts(opts, append): + """Append additional kernel options to this options string. + + Format the elements of list ``append`` as a space separated + string, and return them appended to the existing options + string ``opts``. + + :param opts: A kernel command line options string. + :param append: A list of additional options to append. + :returns: A string with additional options appended. + :rtype: string + """ + extra = " ".join(append) + return "%s %s" % (opts, extra) if append else opts + + def del_opt(opt, drop): + """Return ``True`` if option ``opt`` should be dropped or + ``False`` otherwise. + + Test the option ``opt`` against the drop specification ``drop`` + and return ``True`` if the option should be dropped according + to the spec, or ``False`` otherwise. + + :param opt: A kernel command line option with or without value. + :param drop: A drop specification in Boom del_opts notation + (see ``del_opts`` for further details of syntax). + :returns: ``True`` if the option should be dropped or ``False`` + otherwise. + :rtype: bool + """ + # "name" or "name=value" + if opt in drop: + return True + + # "name=" wildcard + if ("%s=" % opt.split('=')[0]) in drop: + return True + return False + + def del_opts(opts, drop): + """Remove template-supplied kernel options matching ``drop`` from + options string ``opts``. + + A drop specification matches either a simple name, a name and + its full value (in which case both must match), or a name, + followed by '=', indicating that an option with value should + be dropped regardless of the actual value: + + drop name + = drop name and any value + = drop name only if its value == value + + :param opts: A kernel command line options string. + :param drop: A drop specification to apply to ``opts``. + :returns: A kernel command line options string with options + matching ``drop`` removed. + :rtype: string + """ + return " ".join([o for o in opts.split() if not del_opt(o, drop)]) + + def do_null(opts): + """Dummy expansion function. + """ + return opts + + # Optionally expand environment variable references. + do_exp = _expand_vars if expand else do_null + + if BOOM_ENTRY_OPTIONS in self._entry_data: + opts = self._entry_data_property(BOOM_ENTRY_OPTIONS) + if self.bp: + opts = add_opts(opts, self.bp.add_opts) + return do_exp(del_opts(opts, self.bp.del_opts)) + return do_exp(opts) + + if self._osp and self.bp: + opts = self._apply_format(self._osp.options) + opts = add_opts(opts, self.bp.add_opts) + return do_exp(del_opts(opts, self.bp.del_opts)) + + return "" + + @property + def expand_options(self): + """The command line options for this ``BootEntry``, with any + bootloader environment variables expanded to their current + values. + + Return the command line options for this ``BootEntry``, + expanding any Boom or Grub2 substitution notation found. + + :getter: returns the command line for this ``BootEntry``. + :setter: sets the command line for this ``BootEntry``. + :type: string + """ + return self._options(expand=True) + + @property + def options(self): + """The command line options for this ``BootEntry``, including + any bootloader environment variable references as they + appear. + + :getter: returns the command line for this ``BootEntry``. + :setter: sets the command line for this ``BootEntry``. + :type: string + """ + return self._options() + + @options.setter + def options(self, options): + self._entry_data[BOOM_ENTRY_OPTIONS] = options + self._dirty() + + @property + def linux(self): + """The bootable Linux image for this ``BootEntry``. + + :getter: returns the configured ``linux`` image. + :setter: sets the configured ``linux`` image. + :type: string + """ + if not self._osp or BOOM_ENTRY_LINUX in self._entry_data: + return self._entry_data_property(BOOM_ENTRY_LINUX) + + kernel_path = self._apply_format(self._osp.kernel_pattern) + return kernel_path + + @linux.setter + def linux(self, linux): + self._entry_data[BOOM_ENTRY_LINUX] = linux + self._dirty() + + def _initrd(self, expand=False): + """Return the initrd string with or without variable expansion. + + Since some distributions use bootloader environment + variables to define auxiliary initramfs images, the initrd + property is optionally subject to the same variable + expansion as the options property. + + :param expand: ``True`` if variables should be expanded or + ``False`` otherwise. + :returns: An initrd string + :rtype: string + """ + if not self._osp or BOOM_ENTRY_INITRD in self._entry_data: + initrd_string = self._entry_data_property(BOOM_ENTRY_INITRD) + if expand: + return _expand_vars(initrd_string) + return initrd_string + + initramfs_path = self._apply_format(self._osp.initramfs_pattern) + if expand: + return _expand_vars(initrd_string) + return initramfs_path + + @property + def initrd(self): + """The loadable initramfs image for this ``BootEntry``. + + :getter: returns the configured ``initrd`` image. + :getter: sets the configured ``initrd`` image. + :type: string + """ + return self._initrd() + + @property + def expand_initrd(self): + """The loadable initramfs image for this ``BootEntry`` with any + embedded bootloader variable references expanded. + + :getter: returns the configured ``initrd`` image. + :getter: sets the configured ``initrd`` image. + :type: string + """ + return self._initrd(expand=True) + + @initrd.setter + def initrd(self, initrd): + self._entry_data[BOOM_ENTRY_INITRD] = initrd + self._dirty() + + @property + def efi(self): + """The loadable EFI image for this ``BootEntry``. + + :getter: returns the configured EFI application image. + :getter: sets the configured EFI application image. + :type: string + """ + return self._entry_data_property(BOOM_ENTRY_EFI) + + @efi.setter + def efi(self, efi): + self._entry_data[BOOM_ENTRY_EFI] = efi + self._dirty() + + @property + def devicetree(self): + """The devicetree archive for this ``BootEntry``. + + :getter: returns the configured device tree archive. + :getter: sets the configured device tree archive. + :type: string + """ + return self._entry_data_property(BOOM_ENTRY_DEVICETREE) + + @devicetree.setter + def devicetree(self, devicetree): + self._entry_data[BOOM_ENTRY_DEVICETREE] = devicetree + self._dirty() + + @property + def architecture(self): + """The EFI machine type string for this ``BootEntry``. + + :getter: returns the configured architecture. + :setter: sets the architecture for this entry. + :type: string + """ + return self._entry_data_property(BOOM_ENTRY_ARCHITECTURE) + + @architecture.setter + def architecture(self, architecture): + # The empty string means no architecture key + machine_types = ["ia32", "x64", "ia64", "arm", "aa64", ""] + if architecture and not architecture.lower() in machine_types: + raise ValueError("Unknown architecture: '%s'" % architecture) + self._entry_data[BOOM_ENTRY_ARCHITECTURE] = architecture + self._dirty() + + @property + def grub_users(self): + """The current ``grub_users`` key for this entry. + + :getter: Return the current ``grub_users`` value. + :setter: Store a new ``grub_users`` value. + :type: string + """ + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_USERS] + if not self._have_optional_key(bls_key): + return "" + return self._entry_data_property(BOOM_ENTRY_GRUB_USERS) + + @grub_users.setter + def grub_users(self, grub_users): + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_USERS] + if not self._have_optional_key(bls_key): + raise ValueError("OsProfile os_id=%s does not allow '%s'" % + (self._osp.disp_os_id, bls_key)) + self._entry_data[BOOM_ENTRY_GRUB_USERS] = grub_users + + @property + def grub_arg(self): + """The current ``grub_arg`` key for this entry. + + :getter: Return the current ``grub_arg`` value. + :setter: Store a new ``grub_arg`` value. + :type: string + """ + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_ARG] + if not self._have_optional_key(bls_key): + return "" + return self._entry_data_property(BOOM_ENTRY_GRUB_ARG) + + @grub_arg.setter + def grub_arg(self, grub_arg): + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_ARG] + if not self._have_optional_key(bls_key): + raise ValueError("OsProfile os_id=%s does not allow '%s'" % + (self._osp.disp_os_id, bls_key)) + self._entry_data[BOOM_ENTRY_GRUB_ARG] = grub_arg + + @property + def grub_class(self): + """The current ``grub_class`` key for this entry. + + :getter: Return the current ``grub_class`` value. + :setter: Store a new ``grub_class`` value. + :type: string + """ + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_CLASS] + if not self._have_optional_key(bls_key): + return "" + return self._entry_data_property(BOOM_ENTRY_GRUB_CLASS) + + @grub_class.setter + def grub_class(self, grub_class): + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_CLASS] + if not self._have_optional_key(bls_key): + raise ValueError("OsProfile os_id=%s does not allow '%s'" % + (self._osp.disp_os_id, bls_key)) + self._entry_data[BOOM_ENTRY_GRUB_CLASS] = grub_class + + @property + def id(self): + """The value of the ``id`` key for this entry. + + :getter: Return the current ``id`` value. + :setter: Store a new ``id`` value. + :type: string + """ + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_ID] + if not self._have_optional_key(bls_key): + return "" + return self._entry_data_property(BOOM_ENTRY_GRUB_ID) + + @id.setter + def id(self, ident): + bls_key = KEY_MAP[BOOM_ENTRY_GRUB_ID] + if not self._have_optional_key(bls_key): + raise ValueError("OsProfile os_id=%s does not allow '%s'" % + (self._osp.disp_os_id, bls_key)) + self._entry_data[BOOM_ENTRY_GRUB_ID] = ident + + @property + def _entry_path(self): + id_tuple = (self.machine_id, self.boot_id[0:7], self.version) + file_name = BOOT_ENTRIES_FORMAT % id_tuple + return path_join(boom_entries_path(), file_name) + + @property + def entry_path(self): + """The path to the on-disk file containing this ``BootEntry``. + """ + if self.read_only: + return self._last_path + return self._entry_path + + def write_entry(self, force=False, expand=False): + """Write out entry to disk. + + Write out this ``BootEntry``'s data to a file in BLS + format to the path specified by ``boom_entries_path()``. + + The file will be named according to the entry's key values, + and the value of the ``BOOT_ENTRIES_FORMAT`` constant. + Currently the ``machine_id`` and ``version`` keys are used + to construct the file name. + + If the value of ``force`` is ``False`` and the ``OsProfile`` + is not currently marked as dirty (either new, or modified + since the last load operation) the write will be skipped. + + :param force: Force this entry to be written to disk even + if the entry is unmodified. + :param expand: Expand bootloader environment variables in + on-disk entry. + :raises: ``OSError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. + :rtype: None + """ + if not self._unwritten and not force: + return + entry_path = self._entry_path + (tmp_fd, tmp_path) = mkstemp(prefix="boom", dir=boom_entries_path()) + with fdopen(tmp_fd, "w") as f: + # Our original file descriptor will be closed on exit from the + # fdopen with statement: save a copy so that we can call fdatasync + # once at the end of writing rather than on each loop iteration. + tmp_fd = dup(tmp_fd) + if self._osp: + # Insert OsIdentifier comment at top-of-file + f.write("#OsIdentifier: %s\n" % self._osp.os_id) + if expand: + f.write(self.expanded() + "\n") + else: + f.write(str(self) + "\n") + try: + fdatasync(tmp_fd) + rename(tmp_path, entry_path) + chmod(entry_path, BOOT_ENTRY_MODE) + except Exception as e: + _log_error("Error writing entry file %s: %s" % + (entry_path, e)) + try: + unlink(tmp_path) + except Exception: + pass + raise e + + self._last_path = entry_path + self._unwritten = False + + # Add this entry to the list of known on-disk entries + _add_entry(self) + + def update_entry(self, force=False, expand=False): + """Update on-disk entry. + + Update this ``BootEntry``'s on-disk data. + + The file will be named according to the entry's key values, + and the value of the ``BOOT_ENTRIES_FORMAT`` constant. + Currently the ``machine_id`` and ``version`` keys are used + to construct the file name. + + If this ``BootEntry`` previously existed on-disk, and the + ``boot_id`` has changed due to a change in entry key + values, the old ``BootEntry`` file will be unlinked once + the new data has been successfully written. If the entry + does not already exist then calling this method is the + equivalent of calling ``BootEntry.write_entry()``. + + If the value of ``force`` is ``False`` and the ``BootEntry`` + is not currently marked as dirty (either new, or modified + since the last load operation) the write will be skipped. + + :param force: Force this entry to be written to disk even + if the entry is unmodified. + :param expand: Expand bootloader environment variables in + on-disk entry. + :raises: ``OSError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. + :rtype: None + """ + # Cache old entry path + to_unlink = self._last_path + self.write_entry(force=force, expand=expand) + if self._entry_path != to_unlink: + try: + unlink(to_unlink) + except Exception as e: + _log_error("Error unlinking entry file %s: %s" % + (to_unlink, e)) + + def delete_entry(self): + """Remove on-disk BootEntry file. + + Remove the on-disk entry corresponding to this ``BootEntry`` + object. This will permanently erase the current file + (although the current data may be re-written at any time by + calling ``write_entry()``). + + :rtype: ``NoneType`` + :raises: ``OsError`` if an error occurs removing the file or + ``ValueError`` if the entry does not exist. + """ + if not path_exists(self._entry_path): + raise ValueError("Entry does not exist: %s" % self._entry_path) + try: + unlink(self._entry_path) + except Exception as e: + _log_error("Error removing entry file %s: %s" % + (self._entry_path, e)) + raise + + if not self._unwritten: + _del_entry(self) + + +__all__ = [ + # Module constants + 'BOOT_ENTRIES_FORMAT', + 'BOOT_ENTRY_MODE', + + # BootEntry keys + 'BOOM_ENTRY_TITLE', + 'BOOM_ENTRY_VERSION', + 'BOOM_ENTRY_MACHINE_ID', + 'BOOM_ENTRY_LINUX', + 'BOOM_ENTRY_INITRD', + 'BOOM_ENTRY_EFI', + 'BOOM_ENTRY_OPTIONS', + 'BOOM_ENTRY_DEVICETREE', + 'BOOM_ENTRY_GRUB_USERS', + 'BOOM_ENTRY_GRUB_ARG', + 'BOOM_ENTRY_GRUB_CLASS', + 'BOOM_ENTRY_GRUB_ID', + + # Lists of valid BootEntry keys + 'ENTRY_KEYS', + 'OPTIONAL_KEYS', + + # Root device pattern + 'DEV_PATTERN', + + # Boom root device error class + 'BoomRootDeviceError', + + # BootParams and BootEntry objects + 'BootParams', 'BootEntry', + + # BLS Key lookup + 'key_to_bls_name', + + # Default values for optional BootEntry keys + 'optional_key_default', + + # Path configuration + 'boom_entries_path', + + # Entry lookup, load, and write functions + 'drop_entries', 'load_entries', 'write_entries', 'find_entries', + + # Formatting + 'min_boot_id_width', + + # Bootloader integration check + 'check_bootloader' +] + +# vim: set et ts=4 sw=4 : diff --git a/boom/command.py b/boom/command.py new file mode 100644 index 0000000..a113c21 --- /dev/null +++ b/boom/command.py @@ -0,0 +1,2975 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# command.py - Boom BLS bootloader command interface +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.command`` module provides both the Boom command line +interface infrastructure, and a simple procedural interface to the +``boom`` library modules. + +The procedural interface is used by the ``boom`` command line tool, +and may be used by application programs, or interactively in the +Python shell by users who do not require all the features present +in the Boom object API. + +In addition the module contains definitions for ``BoomReport`` +object types and fields that may be of use in implementing custom +reports using the ``boom.report`` module. +""" +from __future__ import print_function + +from boom import * +from boom.osprofile import * +from boom.report import * +from boom.bootloader import * +from boom.hostprofile import * +from boom.legacy import * +from boom.config import * + +from os import environ, uname, getcwd +from os.path import basename, exists as path_exists, isabs, join +from argparse import ArgumentParser +import platform +import logging + +#: The environment variable from which to take the location of the +#: ``/boot`` file system. +BOOM_BOOT_PATH_ENV = "BOOM_BOOT_PATH" + +#: Path to the system machine-id file +_MACHINE_ID = "/etc/machine-id" +#: Path to the legacy system machine-id file +_DBUS_MACHINE_ID = "/var/lib/dbus/machine-id" + +# Module logging configuration +_log = logging.getLogger(__name__) +_log.set_debug_mask(BOOM_DEBUG_COMMAND) + +_log_debug = _log.debug +_log_debug_cmd = _log.debug_masked +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +_default_log_level = logging.WARNING +_console_handler = None + + +# +# Reporting object types +# + +class BoomReportObj(object): + """BoomReportObj() + The universal object type used for all reports generated by + the Boom CLI. Individual fields map to one of the contained + objects via the ``BoomReportObjType`` object's ``data_fn`` + method. It is an error to attempt to report an object that + is undefined: the BoomReportObj used for a report must + contain values for each object type that the specified list + of fields will attempt to access. + + This allows a single report to include fields from both a + ``BootEntry`` object and an attached ``OsProfile``. + """ + be = None + osp = None + hp = None + + def __init__(self, boot_entry=None, os_profile=None, host_profile=None): + """Initialise new BoomReportObj objects. + + Construct a new BoomReportObj object containing the + specified BootEntry and or OsProfile objects. + + :returns: a new BoomReportObj. + :rtype: ``BoomReportObj`` + """ + self.be = boot_entry + self.osp = os_profile + self.hp = host_profile + + +#: BootEntry report object type +BR_ENTRY = 1 +#: OsProfile report object type +BR_PROFILE = 2 +#: BootParams report object type +BR_PARAMS = 4 +#: HostProfile report object type +BR_HOST = 8 + +#: Report object type table for ``boom.command`` reports. +_report_obj_types = [ + BoomReportObjType( + BR_ENTRY, "Boot loader entries", "entry_", lambda o: o.be), + BoomReportObjType( + BR_PROFILE, "OS profiles", "profile_", lambda o: o.osp), + BoomReportObjType( + BR_PARAMS, "Boot parameters", "param_", lambda o: o.be.bp), + BoomReportObjType( + BR_HOST, "Host profiles", "host_", lambda o: o.hp) +] + +# +# Reporting field definitions +# + +#: Fields derived from OsProfile data. +_profile_fields = [ + BoomFieldType( + BR_PROFILE, "osid", "OsID", "OS identifier", 7, + REP_SHA, lambda f, d: f.report_sha(d.os_id)), + BoomFieldType( + BR_PROFILE, "osname", "Name", "OS name", 24, + REP_STR, lambda f, d: f.report_str(d.os_name)), + BoomFieldType( + BR_PROFILE, "osshortname", "OsShortName", "OS short name", 12, + REP_STR, lambda f, d: f.report_str(d.os_short_name)), + BoomFieldType( + BR_PROFILE, "osversion", "OsVersion", "OS version", 10, + REP_STR, lambda f, d: f.report_str(d.os_version)), + BoomFieldType( + BR_PROFILE, "osversion_id", "VersionID", "Version identifier", 10, + REP_STR, lambda f, d: f.report_str(d.os_version_id)), + BoomFieldType( + BR_PROFILE, "unamepattern", "UnamePattern", "UTS name pattern", 12, + REP_STR, lambda f, d: f.report_str(d.uname_pattern)), + BoomFieldType( + BR_PROFILE, "kernelpattern", "KernPattern", "Kernel image pattern", 13, + REP_STR, lambda f, d: f.report_str(d.kernel_pattern)), + BoomFieldType( + BR_PROFILE, "initrdpattern", "InitrdPattern", "Initrd pattern", 13, + REP_STR, lambda f, d: f.report_str(d.initramfs_pattern)), + BoomFieldType( + BR_PROFILE, "lvm2opts", "LVM2Opts", "LVM2 options", 12, + REP_STR, lambda f, d: f.report_str(d.root_opts_lvm2)), + BoomFieldType( + BR_PROFILE, "btrfsopts", "BTRFSOpts", "BTRFS options", 13, + REP_STR, lambda f, d: f.report_str(d.root_opts_btrfs)), + BoomFieldType( + BR_PROFILE, "options", "Options", "Kernel options", 24, + REP_STR, lambda f, d: f.report_str(d.options)), + BoomFieldType( + BR_PROFILE, "profilepath", "ProfilePath", "On-disk profile path", 12, + REP_STR, lambda f, d: f.report_str(d._profile_path())) +] + +_default_profile_fields = "osid,osname,osversion" +_verbose_profile_fields = _default_profile_fields + ",unamepattern,options" + +_host_fields = [ + BoomFieldType( + BR_HOST, "hostid", "HostID", "Host identifier", 7, + REP_SHA, lambda f, d: f.report_sha(d.host_id)), + BoomFieldType( + BR_HOST, "machineid", "MachineID", "Machine identifier", 10, + REP_SHA, lambda f, d: f.report_sha(d.disp_machine_id)), + BoomFieldType( + BR_HOST, "osid", "OsID", "OS identifier", 7, + REP_SHA, lambda f, d: f.report_sha(d.os_id)), + BoomFieldType( + BR_HOST, "hostname", "HostName", "Host name", 28, + REP_STR, lambda f, d: f.report_str(d.host_name)), + BoomFieldType( + BR_HOST, "label", "Label", "Host label", 12, + REP_STR, lambda f, d: f.report_str(d.label)), + BoomFieldType( + BR_HOST, "kernelpattern", "KernPattern", "Kernel image pattern", 13, + REP_STR, lambda f, d: f.report_str(d.kernel_pattern)), + BoomFieldType( + BR_HOST, "initrdpattern", "InitrdPattern", "Initrd pattern", 13, + REP_STR, lambda f, d: f.report_str(d.initramfs_pattern)), + BoomFieldType( + BR_HOST, "lvm2opts", "LVM2Opts", "LVM2 options", 12, + REP_STR, lambda f, d: f.report_str(d.root_opts_lvm2)), + BoomFieldType( + BR_HOST, "btrfsopts", "BTRFSOpts", "BTRFS options", 13, + REP_STR, lambda f, d: f.report_str(d.root_opts_btrfs)), + BoomFieldType( + BR_HOST, "options", "Options", "Kernel options", 24, + REP_STR, lambda f, d: f.report_str(d.options)), + BoomFieldType( + BR_HOST, "profilepath", "ProfilePath", "On-disk profile path", 12, + REP_STR, lambda f, d: f.report_str(d._profile_path())), + BoomFieldType( + BR_HOST, "addopts", "AddOptions", "Added Options", 12, + REP_STR, lambda f, d: f.report_str(d.add_opts)), + BoomFieldType( + BR_HOST, "delopts", "DelOptions", "Deleted Options", 12, + REP_STR, lambda f, d: f.report_str(d.del_opts)) +] + +_default_host_fields = "hostid,hostname,machineid,osid" +_verbose_host_fields = _default_host_fields + ",options,addopts,delopts" + + +def _int_if_val(val): + """Return an int if val is defined or None otherwise. + + A TypeError exception is raised if val is defined but does + not contain a parsable integer value. + + :param val: The value to convert + :returns: None if val is None or an integer representation of + the string val + :raises: TypeError is val cannot be converted to an int + """ + return int(val) if val is not None else None + + +def _bool_to_yes_no(bval): + """Return the string 'yes' if ``bval`` is ``True`` or 'no' otherwise. + """ + return "yes" if bval else "no" + + +#: Fields derived from BootEntry data. +_entry_fields = [ + BoomFieldType( + BR_ENTRY, "bootid", "BootID", "Boot identifier", 7, + REP_SHA, lambda f, d: f.report_sha(d.boot_id)), + BoomFieldType( + BR_ENTRY, "title", "Title", "Entry title", 24, + REP_STR, lambda f, d: f.report_str(d.title)), + BoomFieldType( + BR_ENTRY, "options", "Options", "Kernel options", 24, + REP_STR, lambda f, d: f.report_str(d.options)), + BoomFieldType( + BR_ENTRY, "kernel", "Kernel", "Kernel image", 32, + REP_STR, lambda f, d: f.report_str(d.linux)), + BoomFieldType( + BR_ENTRY, "initramfs", "Initramfs", "Initramfs image", 40, + REP_STR, lambda f, d: f.report_str(d.initrd)), + BoomFieldType( + BR_ENTRY, "machineid", "MachineID", "Machine identifier", 10, + REP_SHA, lambda f, d: f.report_sha(d.machine_id)), + BoomFieldType( + BR_ENTRY, "entrypath", "EntryPath", "On-disk entry path", 12, + REP_STR, lambda f, d: f.report_str(d.entry_path)), + BoomFieldType( + BR_ENTRY, "entryfile", "EntryFile", "On-disk entry file name", 12, + REP_STR, lambda f, d: f.report_str(basename(d.entry_path))), + BoomFieldType( + BR_ENTRY, "readonly", "ReadOnly", "Entry is read-only", 9, + REP_STR, lambda f, d: f.report_str(_bool_to_yes_no(d.read_only))) +] + +#: Fields derived from BootEntry data, with bootloader variables expanded. +_expand_entry_fields = [ + BoomFieldType( + BR_ENTRY, "bootid", "BootID", "Boot identifier", 7, + REP_SHA, lambda f, d: f.report_sha(d.boot_id)), + BoomFieldType( + BR_ENTRY, "title", "Title", "Entry title", 24, + REP_STR, lambda f, d: f.report_str(d.title)), + BoomFieldType( + BR_ENTRY, "options", "Options", "Kernel options", 24, + REP_STR, lambda f, d: f.report_str(d.expand_options)), + BoomFieldType( + BR_ENTRY, "kernel", "Kernel", "Kernel image", 32, + REP_STR, lambda f, d: f.report_str(d.linux)), + BoomFieldType( + BR_ENTRY, "initramfs", "Initramfs", "Initramfs image", 40, + REP_STR, lambda f, d: f.report_str(d.initrd)), + BoomFieldType( + BR_ENTRY, "machineid", "MachineID", "Machine identifier", 10, + REP_SHA, lambda f, d: f.report_sha(d.machine_id)), + BoomFieldType( + BR_ENTRY, "entrypath", "EntryPath", "On-disk entry path", 12, + REP_STR, lambda f, d: f.report_str(d.entry_path)), + BoomFieldType( + BR_ENTRY, "entryfile", "EntryFile", "On-disk entry file name", 12, + REP_STR, lambda f, d: f.report_str(basename(d.entry_path))), + BoomFieldType( + BR_ENTRY, "readonly", "ReadOnly", "Entry is read-only", 9, + REP_STR, lambda f, d: f.report_str(_bool_to_yes_no(d.read_only))) +] + +#: Fields derived from BootParams data +_params_fields = [ + BoomFieldType( + BR_PARAMS, "version", "Version", "Kernel version", 24, + REP_STR, lambda f, d: f.report_str(d.version)), + BoomFieldType( + BR_PARAMS, "rootdev", "RootDevice", "Root device", 10, + REP_STR, lambda f, d: f.report_str(d.root_device)), + BoomFieldType( + BR_PARAMS, "rootlv", "RootLV", "Root logical volume", 6, + REP_STR, lambda f, d: f.report_str(d.lvm_root_lv or "")), + BoomFieldType( + BR_PARAMS, "subvolpath", "SubvolPath", "BTRFS subvolume path", 10, + REP_STR, lambda f, d: f.report_str(d.btrfs_subvol_path or "")), + BoomFieldType( + BR_PARAMS, "subvolid", "SubvolID", "BTRFS subvolume ID", 8, + REP_NUM, lambda f, d: f.report_num(_int_if_val(d.btrfs_subvol_id))) +] + +_default_entry_fields = "bootid,version,osname,rootdev" +_verbose_entry_fields = (_default_entry_fields + ",options,machineid") + + +def _get_machine_id(): + """Return the current host's machine-id. + + Get the machine-id value for the running system by reading from + ``/etc/machine-id`` and return it as a string. + + :returns: The ``machine_id`` as a string + :rtype: str + """ + if path_exists(_MACHINE_ID): + path = _MACHINE_ID + elif path_exists(_DBUS_MACHINE_ID): + path = _DBUS_MACHINE_ID + else: + return None + + with open(path, "r") as f: + try: + machine_id = f.read().strip() + except Exception as e: + _log_error("Could not read machine-id from '%s': %s" % + (_MACHINE_ID, e)) + machine_id = None + return machine_id + + +def _subvol_from_arg(subvol): + """Parse a BTRFS subvolume from a string argument. + + Parse a BTRFS subvolume path or identifier from a command line + argument string. Numeric values are assumed to be a subvolume ID + and values beginning with a '/' character are assumed to be a + subvolume path. + + :param subvol: A subvolume path or ID string + :returns: (path, id) tuple or (None, None) if neither is found + :rtype: (str, str) + """ + if not subvol: + return (None, None) + subvol = parse_btrfs_subvol(subvol) + if subvol.startswith('/'): + btrfs_subvol_path = subvol + btrfs_subvol_id = None + else: + btrfs_subvol_path = None + btrfs_subvol_id = subvol + return (btrfs_subvol_path, btrfs_subvol_id) + + +def _str_indent(string, indent): + """Indent all lines of a multi-line string. + + Indent each line of the multi line string ``string`` to the + specified indentation level. + + :param string: The string to be indented + :param indent: The number of characters to indent by + :returns: str + """ + outstr = "" + for line in string.splitlines(): + outstr += indent * ' ' + line + '\n' + return outstr.rstrip('\n') + + +def _canonicalize_lv_name(lvname): + """Canonicalize an LVM2 logical volume name as "VG/LV", removing any + "/dev/" prefix and return the result as a string. + + The use of "/dev/mapper/VG-LV" names is not supported. + """ + dev_prefix = DEV_PATTERN % "" + if lvname.startswith(dev_prefix + "mapper/"): + raise ValueError("Logical volume names in /dev/mapper/VG-LV format " + "are not supported.") + if lvname.startswith(dev_prefix): + lvname = lvname[len(dev_prefix):] + if '/' not in lvname or lvname.count('/') != 1: + raise ValueError("Root logical volume name must be in VG/LV format.") + return lvname + + +def __write_legacy(): + """Synchronise boom boot entries with the configured legacy + bootloader format. + """ + config = get_boom_config() + if config.legacy_enable and config.legacy_sync: + clear_legacy_loader() + write_legacy_loader(selection=Selection(), loader=config.legacy_format) + + +def _do_print_type(report_fields, selected, output_fields=None, + opts=None, sort_keys=None): + """Print an object type report (entry, osprofile, hostprofile). + + Helper for list function that generate BoomReports. + + Format a set of entry or profile objects matching the given + criteria and format them as a report, returning the output + as a string. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation + :param output_fields: a comma-separated list of output fields + :param opts: output formatting and control options + :param sort_keys: a comma-separated list of sort keys + :rtype: str + """ + opts = opts if opts is not None else BoomReportOpts() + + br = BoomReport(_report_obj_types, report_fields, output_fields, + opts, sort_keys, None) + + for obj in selected: + # Fixme: handle bes with embedded hp (class test) + br.report_object(obj) + + return br.report_output() + + +def _merge_add_del_opts(orig_opts, opts): + """Merge a set of existing bootparams option alterations with + a set of command-line provided values to produce a single + set of options to add or remove from a cloned or edited + ``BootEntry``. + :param orig_opts: A list of original option modifications + :param opts: A space-separated string containing a list of + command line option modifications + :returns: A single list containing the merged options + """ + # Merge new and cloned kernel options + all_opts = set() + if opts: + all_opts.update(opts.split()) + if orig_opts: + all_opts.update(orig_opts) + + return list(all_opts) + + +# +# Command driven API: BootEntry and OsProfile management and reporting. +# + +# +# BootEntry manipulation +# + +def create_entry(title, version, machine_id, root_device, lvm_root_lv=None, + btrfs_subvol_path=None, btrfs_subvol_id=None, profile=None, + add_opts=None, del_opts=None, write=True, architecture=None, + expand=False, allow_no_dev=False): + """Create new boot loader entry. + + Create the specified boot entry in the configured loader directory. + An error is raised if a matching entry already exists. + + :param title: the title of the new entry. + :param version: the version string for the new entry. + :param machine_id: the machine id for the new entry. + :param root_device: the root device path for the new entry. + :param lvm_root_lv: an optional LVM2 root logical volume. + :param btrfs_subvol_path: an optional BTRFS subvolume path. + :param btrfs_subvol_id: an optional BTRFS subvolume id. + :param profile: A profile to use for this entry. + :param add_opts: A list of additional kernel options to append. + :param del_opts: A list of template-supplied options to drop. + :param write: ``True`` if the entry should be written to disk, + or ``False`` otherwise. + :param architecture: An optional BLS architecture string. + :param expand: Expand bootloader environment variables. + :param allow_no_dev: Accept a non-existent or invalid root dev. + :returns: a ``BootEntry`` object corresponding to the new entry. + :rtype: ``BootEntry`` + :raises: ``ValueError`` if either required values are missing or + a duplicate entry exists, or``OsError`` if an error + occurs while writing the entry file. + """ + if not title and not profile.title: + raise ValueError("Entry title cannot be empty.") + + if not version: + raise ValueError("Entry version cannot be empty.") + + if not machine_id: + raise ValueError("Entry machine_id cannot be empty.") + + if not root_device: + raise ValueError("Entry requires a root_device.") + + if not profile: + raise ValueError("Cannot create entry without OsProfile.") + + add_opts = add_opts.split() if add_opts else [] + del_opts = del_opts.split() if del_opts else [] + + _log_debug_cmd("Effective add options: %s" % add_opts) + _log_debug_cmd("Effective del options: %s" % del_opts) + + bp = BootParams(version, root_device, lvm_root_lv=lvm_root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, + add_opts=add_opts, del_opts=del_opts) + + be = BootEntry(title=title, machine_id=machine_id, + osprofile=profile, boot_params=bp, + architecture=architecture, allow_no_dev=allow_no_dev) + + if find_entries(Selection(boot_id=be.boot_id)): + raise ValueError("Entry already exists (boot_id=%s)." % + be.disp_boot_id) + + if write: + be.write_entry(expand=expand) + __write_legacy() + + return be + + +def delete_entries(selection=None): + """Delete entries matching selection criteria. + + Delete the specified boot entry or entries from the configured + loader directory. If ``boot_id`` is used, or if the criteria + specified match exactly one entry, a single entry is removed. + If ``boot_id`` is not used, and more than one matching entry + is present, all matching entries will be removed. + + Selection criteria may also be expressed via a Selection + object passed to the call using the ``selection`` parameter. + + On success the number of entries removed is returned. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: the number of entries removed. + :rtype: ``int`` + """ + bes = find_entries(selection=selection) + + if not bes: + raise IndexError("No matching entry found.") + + deleted = 0 + for be in bes: + be.delete_entry() + deleted += 1 + + __write_legacy() + + return deleted + + +def clone_entry(selection=None, title=None, version=None, machine_id=None, + root_device=None, lvm_root_lv=None, btrfs_subvol_path=None, + btrfs_subvol_id=None, profile=None, architecture=None, + add_opts=None, del_opts=None, + write=True, expand=False, allow_no_dev=False): + """Clone an existing boot loader entry. + + Create the specified boot entry in the configured loader directory + by cloning all un-set parameters from the boot entry selected by + the ``selection`` argument. + + An error is raised if a matching entry already exists. + + :param selection: criteria matching the entry to clone. + :param title: the title of the new entry. + :param version: the version string for the new entry. + :param machine_id: the machine id for the new entry. + :param root_device: the root device path for the new entry. + :param lvm_root_lv: an optional LVM2 root logical volume. + :param btrfs_subvol_path: an optional BTRFS subvolume path. + :param btrfs_subvol_id: an optional BTRFS subvolume id. + :param profile: A profile to use for this entry. + :param architecture: An optional BLS architecture string. + :param add_opts: A list of additional kernel options to append. + :param del_opts: A list of template-supplied options to drop. + :param write: ``True`` if the entry should be written to disk, + or ``False`` otherwise. + :param expand: Expand bootloader environment variables. + :param allow_no_dev: Allow the block device to not exist. + :returns: a ``BootEntry`` object corresponding to the new entry. + :rtype: ``BootEntry`` + :raises: ``ValueError`` if either required values are missing or + a duplicate entry exists, or``OsError`` if an error + occurs while writing the entry file. + """ + if not selection.boot_id or selection.boot_id is None: + raise ValueError("clone requires boot_id") + + all_args = (title, version, machine_id, root_device, lvm_root_lv, + btrfs_subvol_path, btrfs_subvol_id, profile) + + if not any(all_args): + raise ValueError("clone requires one or more of:\ntitle, version, " + "machine_id, root_device, lvm_root_lv, " + "btrfs_subvol_path, btrfs_subvol_id, profile") + + bes = find_entries(selection=selection) + if not bes: + raise ValueError("No matching entry found for boot ID %s" % + selection.boot_id) + + if len(bes) > 1: + raise ValueError("clone criteria must match exactly one entry") + + be = bes[0] + + _log_debug("Cloning entry with boot_id='%s'" % be.disp_boot_id) + + title = title if title else be.title + version = version if version else be.version + machine_id = machine_id if machine_id else be.machine_id + root_device = root_device if root_device else be.bp.root_device + lvm_root_lv = lvm_root_lv if lvm_root_lv else be.bp.lvm_root_lv + btrfs_subvol_path = (btrfs_subvol_path if btrfs_subvol_path + else be.bp.btrfs_subvol_path) + btrfs_subvol_id = (btrfs_subvol_id if btrfs_subvol_id + else be.bp.btrfs_subvol_id) + profile = profile if profile else be._osp + + add_opts = _merge_add_del_opts(be.bp.add_opts, add_opts) + del_opts = _merge_add_del_opts(be.bp.del_opts, del_opts) + _log_debug_cmd("Effective add options: %s" % add_opts) + _log_debug_cmd("Effective del options: %s" % del_opts) + + bp = BootParams(version, root_device, lvm_root_lv=lvm_root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, + add_opts=add_opts, del_opts=del_opts) + + clone_be = BootEntry(title=title, machine_id=machine_id, + osprofile=profile, boot_params=bp, + architecture=architecture, + allow_no_dev=allow_no_dev) + if find_entries(Selection(boot_id=clone_be.boot_id)): + raise ValueError("Entry already exists (boot_id=%s)." % + clone_be.disp_boot_id) + + orig_be = find_entries(selection)[0] + if orig_be.options != orig_be.expand_options: + clone_be.options = orig_be.options + + if write: + clone_be.write_entry(expand=expand) + __write_legacy() + + return clone_be + + +def edit_entry(selection=None, title=None, version=None, machine_id=None, + root_device=None, lvm_root_lv=None, btrfs_subvol_path=None, + btrfs_subvol_id=None, profile=None, architecture=None, + add_opts=None, del_opts=None, expand=False): + """Edit an existing boot loader entry. + + Modify an existing BootEntry by changing one or more of the + entry values or boot parameters. + + The modified BootEntry is written to disk and returned on + success. + + Modifying a BootEntry causes the entry's boot_id to change, + since the ID is based on the values of all configured boot + keys. + + :param selection: A Selection specifying the boot_id to edit + :param title: The new entry title + :param version: The new entry version + :param machine_id: The new machine_id + :param root_device: The new root device + :param lvm_root_lv: The new LVM root LV + :param btrfs_subvol_path: The new BTRFS subvolume path + :param btrfs_subvol_id: The new BTRFS subvolme ID + :param profile: The host or OS profile for the edited entry + :param architecture: An optional BLS architecture string. + :param add_opts: A list of additional kernel options to append. + :param del_opts: A list of template-supplied options to drop. + :param expand: Expand bootloader environment variables. + + :returns: The modified ``BootEntry`` + :rtype: ``BootEntry`` + """ + all_args = (title, version, machine_id, root_device, lvm_root_lv, + btrfs_subvol_path, btrfs_subvol_id, profile) + + if not any(all_args): + raise ValueError("edit requires one or more of:\ntitle, version, " + "machine_id, root_device, lvm_root_lv, " + "btrfs_subvol_path, btrfs_subvol_id, profile") + + # Discard all selection criteria but boot_id. + selection = Selection(boot_id=selection.boot_id) + + bes = find_entries(selection=selection) + + if not bes: + raise ValueError("No matching entry found for boot ID %s" % + selection.boot_id) + + if len(bes) > 1: + raise ValueError("edit criteria must match exactly one entry") + + be = bes[0] + + _log_debug("Editing entry with boot_id='%s'" % be.disp_boot_id) + + # Use a matching HostProfile is one exists, or the command line + # OsProfile argument if set. + machine_id = machine_id or be.machine_id + version = version or be.version + + add_opts = _merge_add_del_opts(be.bp.add_opts, add_opts) + del_opts = _merge_add_del_opts(be.bp.del_opts, del_opts) + _log_debug_cmd("Effective add options: %s" % add_opts) + _log_debug_cmd("Effective del options: %s" % del_opts) + + be._osp = profile or be._osp + be.title = title or be.title + be.machine_id = machine_id or be.machine_id + be.architecture = architecture or be.architecture + be.bp.version = version + be.bp.root_device = root_device or be.bp.root_device + be.bp.lvm_root_lv = lvm_root_lv or be.bp.lvm_root_lv + be.bp.btrfs_subvol_path = btrfs_subvol_path or be.bp.btrfs_subvol_path + be.bp.btrfs_subvol_id = btrfs_subvol_id or be.bp.btrfs_subvol_id + be.bp.add_opts = add_opts + be.bp.del_opts = del_opts + + be.update_entry(expand=expand) + __write_legacy() + + return be + + +def list_entries(selection=None): + """List entries matching selection criteria. + + Return a list of ``boom.bootloader.BootEntry`` objects matching + the given criteria. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: A list of matching BootEntry objects. + :rtype: list + """ + bes = find_entries(selection=selection) + + return bes + + +def _expand_fields(default_fields, output_fields): + """Expand output fields list from command line arguments. + """ + + if not output_fields: + output_fields = default_fields + elif output_fields.startswith('+'): + output_fields = default_fields + ',' + output_fields[1:] + return output_fields + + +def print_entries(selection=None, output_fields=None, opts=None, + sort_keys=None, expand=None): + """Print boot loader entries matching selection criteria. + + Format a set of ``boom.bootloader.BootEntry`` objects matching + the given criteria, and output them as a report to the file + given in ``out_file``, or ``sys.stdout`` if ``out_file`` is + unset. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation + :param output_fields: a comma-separated list of output fields + :param opts: output formatting and control options + :param sort_keys: a comma-separated list of sort keys + :param expand: Expand bootloader environment variables + :returns: the ``boot_id`` of the new entry + :rtype: str + """ + output_fields = _expand_fields(_default_entry_fields, output_fields) + + bes = find_entries(selection=selection) + selected = [BoomReportObj(be, be._osp, None) for be in bes] + + entry_fields = _expand_entry_fields if expand else _entry_fields + report_fields = entry_fields + _profile_fields + _params_fields + + return _do_print_type(report_fields, selected, output_fields=output_fields, + opts=opts, sort_keys=sort_keys) + + +# +# OsProfile manipulation +# + +def _find_profile(cmd_args, version, machine_id, command, optional=True): + """Find a matching profile (HostProfile or OsProfile) for this + combination of version, machine_id, label and command line + profile arguments + + :param cmd_args: The command argument namespace + :param version: A version string to match + :machine_id: The machine identifier to match + :command: The command name to use in error messages + :returns: A matching ``OsProfile``, ``HostProfile``, or ``None`` + if no match is found. + """ + if not cmd_args.profile: + # Attempt to find a matching OsProfile by version string + osp = match_os_profile_by_version(version) + os_id = osp.os_id if osp else None + if not osp: + print("No matching OsProfile found for version '%s'" % version) + else: + os_id = cmd_args.profile + + osps = find_profiles(Selection(os_id=os_id)) if os_id else None + + # Fail if an explicit profile was given and it is not found. + if not osps and os_id is not None and os_id == cmd_args.profile: + print("OsProfile not found: %s" % os_id) + return None + + if osps and len(osps) > 1: + print("OsProfile ID '%s' is ambiguous" % os_id) + return None + + osp = osps[0] if osps else None + + if osp: + _log_debug("Found OsProfile: %s" % osp.os_id) + + # Attempt to match a host profile to the running host + label = cmd_args.label or "" + host_select = Selection(machine_id=machine_id, host_label=label) + hps = find_host_profiles(host_select) + hp = hps[0] if hps else None + if len(hps) > 1: + # This can only occur if host profiles have been edited outside + # boom's control, such that there are one or more profiles with + # matching machine_id and label. + _log_error("Ambiguous host profile selection") + return None + elif len(hps) == 1: + _log_debug("Found HostProfile: %s" % hps[0].host_id) + if (hp and osp) and not osp.os_id.startswith(hp.os_id): + _log_error("Active host profile (host_id=%s, os_id=%s) " + "conflicts with --profile=%s" % + (hp.disp_host_id, hp.disp_os_id, osp.disp_os_id)) + return None + elif not osp and not hps: + if not optional: + _log_error("%s requires --profile or a matching OsProfile " + "or HostProfile" % command) + return None + + return hp or osp + + +def _uname_heuristic(name, version_id): + """Attempt to guess a uname pattern for a given OS name and + version_id value. + + This is currently supported for Red Hat Enterprise Linux and + Fedora since both distributions provide a fixed string in the + UTS release string that can be used to match candidate kernel + versions against. + + :returns: ``True`` if uname pattern heuristics should be used + for this OS or ``False`` otherwise. + """ + _name_to_uname = { + "Red Hat Enterprise Server": "el", + "Red Hat Enterprise Workstation": "el", + "Fedora": "fc" + } + + if name in _name_to_uname: + return "%s%s" % (_name_to_uname[name], version_id) + return None + + +def _default_optional_keys(osp): + """Set default optional keys for OsProfile + + Attempt to set default optional keys for a given OsProfile + if the distribution is known to support the Red Hat BLS + extensions. + """ + all_optional_keys = "grub_users grub_arg grub_class id" + _default_optional_keys = [ + "Red Hat Enterprise Linux Server", + "Red Hat Enterprise Linux Workstation", + "CentOS Linux", + "Fedora" + ] + if osp.os_name in _default_optional_keys: + return all_optional_keys + return "" + + +def _os_profile_from_file(os_release, uname_pattern, profile_data=None): + """Create OsProfile from os-release file. + + Construct a new ``OsProfile`` object from the specified path, + substituting each set kwarg parameter with the supplied value + in the resulting object. + + :param os_release: The os-release file to read + :param uname_pattern: A replacement uname_pattern value + :param kernel_pattern: A replacement kernel_pattern value + :param initramfs_pattern: A replacement initramfs_pattern value + :param root_opts_lvm2: Replacement LVM2 root options + :param root_opts_btrfs: Replacement BTRFS root options + :param options: Replacement options string template + :returns: A new OsProfile + :rtype: OsProfile + """ + profile_data[BOOM_OS_UNAME_PATTERN] = uname_pattern + osp = OsProfile.from_os_release_file(os_release, profile_data=profile_data) + + # When creating an OsProfile from an os-release file we cannot + # guess the uname_pattern until after the file has been read and + # the os_name and os_version_id values have been set. + if uname_pattern: + osp.uname_pattern = uname_pattern + else: + # Attempt to guess a uname_pattern for operating systems + # that have predictable UTS release patterns. + osp.uname_pattern = _uname_heuristic(osp.os_name, osp.os_version_id) + + if not osp.uname_pattern: + raise ValueError("Could not determine uname pattern for '%s'" % + osp.os_name) + if not osp.optional_keys: + osp.optional_keys = _default_optional_keys(osp) + + osp.write_profile() + return osp + + +def create_profile(name, short_name, version, version_id, + uname_pattern=None, kernel_pattern=None, + initramfs_pattern=None, root_opts_lvm2=None, + root_opts_btrfs=None, options=None, + optional_keys=None, profile_data=None, + profile_file=None): + """Create new operating system profile. + + Create the specified OsProfile in the configured profiles + directory. + + OsProfile key values may be specified either by passing + individual keyword arguments, or by passing a dictionary + of OsProfile key name to value pairs as the ``profile_data`` + argument. If a key is present as both a keyword argument + and in the ``profile_data`` dictionary, the argument will + take precedence. + + An error is raised if a matching profile already exists. + + :param name: The name of the new OsProfile + :param short_name: The short name of the new OsProfile + :param version: The version string of the new OsProfile + :param version_id: The version ID string of the new OsProfile + :param uname_pattern: A uname pattern to match for this profile + :param kernel_pattern: Pattern to generate kernel paths + :param initramfs_pattern: Pattern to generate initramfs paths + :param root_opts_lvm2: Template options for LVM2 entries + :param root_opts_btrfs: Template options for BTRFS entries + :param options: Template kernel command line options + :param profile_data: Dictionary of profile key:value pairs + :param profile_file: File to be used for profile + + :returns: an ``OsProfile`` object for the new profile + :rtype: ``OsProfile`` + :raises: ``ValueError`` if either required values are missing or + a duplicate profile exists, or``OsError`` if an error + occurs while writing the profile file. + """ + def _have_key(pd, arg, key): + return arg or pd and key in pd + + if not profile_data: + profile_data = {} + + if not profile_file: + if not _have_key(profile_data, name, BOOM_OS_NAME): + raise ValueError("Profile name cannot be empty.") + + if not _have_key(profile_data, short_name, BOOM_OS_SHORT_NAME): + raise ValueError("Profile short name cannot be empty.") + + if not _have_key(profile_data, version, BOOM_OS_VERSION): + raise ValueError("Profile version cannot be empty.") + + if not _have_key(profile_data, version_id, BOOM_OS_VERSION_ID): + raise ValueError("Profile version ID cannot be empty.") + + # Allow keyword arguments to override + if name: + profile_data[BOOM_OS_NAME] = name + if short_name: + profile_data[BOOM_OS_SHORT_NAME] = short_name + if version: + profile_data[BOOM_OS_VERSION] = version + if version_id: + profile_data[BOOM_OS_VERSION_ID] = version_id + + if uname_pattern: + profile_data[BOOM_OS_UNAME_PATTERN] = uname_pattern + elif BOOM_OS_UNAME_PATTERN not in profile_data: + # Attempt to guess a uname_pattern for operating systems + # that have predictable UTS release patterns. + pattern = _uname_heuristic( + profile_data[BOOM_OS_NAME], + profile_data[BOOM_OS_VERSION_ID] + ) + if pattern: + profile_data[BOOM_OS_UNAME_PATTERN] = pattern + else: + raise ValueError("Could not determine uname pattern for '%s'" % + profile_data[BOOM_OS_NAME]) + + if kernel_pattern: + profile_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern + if initramfs_pattern: + profile_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern + if root_opts_lvm2: + profile_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 + if root_opts_btrfs: + profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs + if options: + profile_data[BOOM_OS_OPTIONS] = options + if optional_keys: + profile_data[BOOM_OS_OPTIONAL_KEYS] = optional_keys + + if profile_file: + return _os_profile_from_file(profile_file, uname_pattern, + profile_data=profile_data) + + osp = OsProfile(name, short_name, version, version_id, + profile_data=profile_data) + + if not osp.optional_keys: + osp.optional_keys = _default_optional_keys(osp) + + osp.write_profile() + return osp + + +def delete_profiles(selection=None): + """Delete profiles matching selection criteria. + + Delete the specified OsProfile or profiles from the configured + profile directory. If ``os_id`` is used, or if the criteria + specified match exactly one profile, a single entry is removed. + If ``os_id`` is not used, and more than one matching profile + is present, all matching profiles will be removed. + + Selection criteria are expressed via a Selection object + passed to the call using the ``selection`` parameter. + + On success the number of profiles removed is returned. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: the number of entries removed. + :rtype: ``int`` + """ + osps = find_profiles(selection=selection) + + if not osps: + raise IndexError("No matching profiles found.") + + deleted = 0 + for osp in osps: + osp.delete_profile() + deleted += 1 + + return deleted + + +def clone_profile(selection=None, name=None, short_name=None, version=None, + version_id=None, uname_pattern=None, kernel_pattern=None, + initramfs_pattern=None, root_opts_lvm2=None, + root_opts_btrfs=None, options=None): + """Clone an existing operating system profile. + + Create the specified profile in the configured profile directory + by cloning all un-set parameters from the profile selected by + the ``selection`` argument. + + An error is raised if a matching profile already exists, or if + the selection criteria match more than one profile. + + :param selection: criteria matching the profile to clone. + :param name: the name of the new profile. + :param short_name: the short name of the new profile. + :param version: the version string for the new profile. + :param version_id: the version ID string for the new profile. + :param uname_pattern: a uname pattern to match this profile. + :param kernel_pattern: a kernel pattern to match this profile. + :param initramfs_pattern: a initramfs pattern to match this profile. + :param root_opts_lvm2: LVM2 root options template. + :param root_opts_btrfs: BTRFS root options template. + :param options: Kernel options template. + + :returns: a new ``OsProfile`` object. + :rtype: ``OsProfile`` + :raises: ``ValueError`` if either required values are missing or + a duplicate profile exists, or``OsError`` if an error + occurs while writing the profile file. + """ + if not selection.os_id: + raise ValueError("clone requires os_id") + + all_args = ( + name, short_name, version, version_id, uname_pattern, + kernel_pattern, initramfs_pattern, root_opts_lvm2, + root_opts_btrfs, options + ) + + if not any(all_args): + raise ValueError( + 'clone requires one or more of:\nname, ' + 'short_name, version, version_id, uname_pattern,' + 'kernel_pattern, initramfs_pattern, root_opts_lvm2, ' + 'root_opts_btrfs, options' + ) + + osps = find_profiles(selection) + if not(osps): + raise ValueError("No matching profile found: %s" % selection.os_id) + + if len(osps) > 1: + raise ValueError("Clone criteria must match exactly one profile") + + osp = osps.pop() + + # Clone unset keys + name = name or osp.os_name + short_name = short_name or osp.os_short_name + version = version or osp.os_version + version_id = version_id or osp.os_version_id + uname_pattern = uname_pattern or osp.uname_pattern + kernel_pattern = kernel_pattern or osp.kernel_pattern + initramfs_pattern = initramfs_pattern or osp.initramfs_pattern + root_opts_lvm2 = root_opts_lvm2 or osp.root_opts_lvm2 + root_opts_btrfs = root_opts_btrfs or osp.root_opts_btrfs + options = options or osp.options + + clone_osp = OsProfile(name, short_name, version, version_id, + uname_pattern=uname_pattern, + kernel_pattern=kernel_pattern, + initramfs_pattern=initramfs_pattern, + root_opts_lvm2=root_opts_lvm2, + root_opts_btrfs=root_opts_btrfs, options=options) + + clone_osp.write_profile() + + return clone_osp + + +def edit_profile(selection=None, uname_pattern=None, kernel_pattern=None, + initramfs_pattern=None, root_opts_lvm2=None, + root_opts_btrfs=None, options=None, optional_keys=None): + """Edit an existing operating system profile. + + Modify an existing OsProfile by changing one or more of the + profile values. + + The modified OsProfile is written to disk and returned on + success. + + :param selection: A Selection specifying the boot_id to edit + :param uname_pattern: The new uname pattern + :param kernel_pattern: The new kernel pattern + :param initramfs_pattern: The new initramfs pattern + :param root_opts_lvm2: The new LVM2 root options + :param root_opts_btrfs: The new BTRFS root options + :param options: The new kernel options template + :returns: The modified ``OsProfile`` + :rtype: ``OsProfile`` + """ + # Discard all selection criteria but os_id. + selection = Selection(os_id=selection.os_id) + + osp = None + osps = find_profiles(Selection(os_id=selection.os_id)) + if not osps: + raise ValueError("No matching profile found: %s" % selection.os_id) + if len(osps) > 1: + raise ValueError("OS profile identifier '%s' is ambiguous" % + selection.os_id) + + osp = osps.pop() + osp.uname_pattern = uname_pattern or osp.uname_pattern + osp.kernel_pattern = kernel_pattern or osp.kernel_pattern + osp.initramfs_pattern = initramfs_pattern or osp.initramfs_pattern + osp.root_opts_lvm2 = root_opts_lvm2 or osp.root_opts_lvm2 + osp.root_opts_btrfs = root_opts_btrfs or osp.root_opts_btrfs + osp.options = options or osp.options + osp.optional_keys = optional_keys or osp.optional_keys + osp.write_profile() + return osp + + +def list_profiles(selection=None): + """List operating system profiles matching selection criteria. + + Return a list of ``boom.osprofile.OsProfile`` objects matching + the given criteria. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: a list of ``OsProfile`` objects. + :rtype: list + """ + osps = find_profiles(selection=selection) + + return osps + + +def print_profiles(selection=None, opts=None, output_fields=None, + sort_keys=None, expand=False): + """Print operating system profiles matching selection criteria. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation + :param output_fields: a comma-separated list of output fields + :param opts: output formatting and control options + :param sort_keys: a comma-separated list of sort keys + :param expand: unused + :returns: the number of matching profiles output. + :rtype: int + """ + output_fields = _expand_fields(_default_profile_fields, output_fields) + + osps = find_profiles(selection=selection) + selected = [BoomReportObj(None, osp, None) for osp in osps] + + report_fields = _profile_fields + return _do_print_type(report_fields, selected, output_fields=output_fields, + opts=opts, sort_keys=sort_keys) + + +def create_host(machine_id=None, host_name=None, os_id=None, label=None, + kernel_pattern=None, initramfs_pattern=None, + root_opts_lvm2=None, root_opts_btrfs=None, + options=None, add_opts=None, del_opts=None, + host_data=None): + """Create new host profile. + + Create the specified HostProfile in the configured profiles + directory. + + HostProfile key values may be specified either by passing + individual keyword arguments, or by passing a dictionary + of HostProfile key name to value pairs as the ``host_data`` + argument. If a key is present as both a keyword argument + and in the ``host_data`` dictionary, the argument will + take precedence. + + An error is raised if a matching profile already exists. + + :param machine_id: The machine_id of the host + :param host_name: The full name of the new HostProfile + :param label: An optional host label + :param os_id: The os_id for the new host + :param kernel_pattern: Pattern to generate kernel paths + :param initramfs_pattern: Pattern to generate initramfs paths + :param root_opts_lvm2: Template options for LVM2 entries + :param root_opts_btrfs: Template options for BTRFS entries + :param options: Template kernel command line options + :param add_opts: Additional boot options for this profile + :param del_opts: Boot options to delete for this profile + :param host_data: Dictionary of profile key:value pairs + + :returns: a ``HostProfile`` object for the new profile + :rtype: ``HostProfile`` + :raises: ``ValueError`` if either required values are missing or + a duplicate profile exists, or``OsError`` if an error + occurs while writing the profile file. + """ + def _have_key(hd, arg, key): + return arg or hd and key in hd + + if not _have_key(host_data, host_name, BOOM_OS_NAME): + raise ValueError("Host name cannot be empty.") + + if not _have_key(host_data, machine_id, BOOM_OS_VERSION): + raise ValueError("Host machine_id cannot be empty.") + + if not _have_key(host_data, os_id, BOOM_OS_ID): + raise ValueError("Host OS ID cannot be empty.") + + label = label or "" + + if not host_data: + host_data = {} + + # FIXME use kwarg style + + # Allow keyword arguments to override + if machine_id: + host_data[BOOM_ENTRY_MACHINE_ID] = machine_id + if host_name: + host_data[BOOM_HOST_NAME] = host_name + if label: + host_data[BOOM_HOST_LABEL] = label + if os_id: + host_data[BOOM_OS_ID] = os_id + if kernel_pattern: + host_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern + if initramfs_pattern: + host_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern + if root_opts_lvm2: + host_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 + if root_opts_btrfs: + host_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs + if options: + host_data[BOOM_OS_OPTIONS] = options + if add_opts: + host_data[BOOM_HOST_ADD_OPTS] = add_opts + if del_opts: + host_data[BOOM_HOST_DEL_OPTS] = del_opts + + hp = HostProfile(machine_id=machine_id, profile_data=host_data) + + hp.write_profile() + return hp + + +def delete_hosts(selection=None): + """Delete host profiles matching selection criteria. + + Delete the specified ``HostProfile`` or profiles from the + configured profile directory. If ``os_id`` is used, or if the + criteria specified match exactly one profile, a single entry is + removed. If ``host_id`` is not used, and more than one matching + profile is present, all matching profiles will be removed. + + Selection criteria are expressed via a Selection object + passed to the call using the ``selection`` parameter. + + On success the number of profiles removed is returned. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: the number of entries removed. + :rtype: ``int`` + """ + hps = find_host_profiles(selection=selection) + + if not hps: + raise IndexError("No matching host profiles found.") + + deleted = 0 + for hp in hps: + hp.delete_profile() + deleted += 1 + + return deleted + + +def clone_host(selection=None, machine_id=None, host_name=None, label=None, + os_id=None, kernel_pattern=None, initramfs_pattern=None, + root_opts_lvm2=None, root_opts_btrfs=None, + add_opts=None, del_opts=None, options=None): + """Clone an existing host profile. + + Create the specified profile in the configured profile directory + by cloning all un-set parameters from the profile selected by + the ``selection`` argument. + + An error is raised if a matching profile already exists, or if + the selection criteria match more than one profile. + + :param selection: criteria matching the profile to clone. + :param machine_id: the machine_id of the new host profile. + :param host_name: the hostname of the new host profile. + :param label: an optional host label. + :param os_id: the operating system identifier for the host. + :param kernel_pattern: The kernel pattern for the host. + :param initramfs_pattern: The initramfs pattern for the host. + :param root_opts_lvm2: LVM2 root options template. + :param root_opts_btrfs: BTRFS root options template. + :param add_opts: Additional boot options for this profile. + :param del_opts: Boot options to delete for this profile. + :param options: Kernel options template. + + :returns: a new ``HostProfile`` object. + :rtype: ``HostProfile`` + :raises: ``ValueError`` if either required values are missing or + a duplicate profile exists, or``OsError`` if an error + occurs while writing the profile file. + """ + if not selection.host_id: + raise ValueError("clone requires host_id") + + all_args = ( + machine_id, label, host_name, os_id, + kernel_pattern, initramfs_pattern, + root_opts_lvm2, root_opts_btrfs, + add_opts, del_opts, options + ) + + if not any(all_args): + raise ValueError( + 'clone requires one or more of:\n' + '--machine-id, --label, --name, --os-id, ' + '--kernel-pattern, --initramfs_pattern, ' + '--root-opts-lvm2, --root_opts-btrfs, ' + '--add-opts, --del-opts, --options' + ) + + hps = find_host_profiles(selection) + if not(hps): + raise ValueError("No matching host profile found: %s" % + selection.host_id) + + if len(hps) > 1: + raise ValueError("Clone criteria must match exactly one profile") + + hp = hps.pop() + + # Clone unset keys + machine_id = machine_id or hp.machine_id + host_name = host_name or hp.host_name + label = label or "" + os_id = os_id or hp.os_id + initramfs_pattern = initramfs_pattern or hp.initramfs_pattern + kernel_pattern = kernel_pattern or hp.kernel_pattern + root_opts_lvm2 = root_opts_lvm2 or hp.root_opts_lvm2 + root_opts_btrfs = root_opts_btrfs or hp.root_opts_btrfs + add_opts = add_opts or hp.add_opts + del_opts = del_opts or hp.del_opts + options = options or hp.options + + clone_hp = HostProfile(machine_id=machine_id, host_name=host_name, + label=label, os_id=os_id, + kernel_pattern=kernel_pattern, + initramfs_pattern=initramfs_pattern, + root_opts_lvm2=root_opts_lvm2, + root_opts_btrfs=root_opts_btrfs, + add_opts=add_opts, del_opts=del_opts, + options=options) + + clone_hp.write_profile() + + return clone_hp + + +def edit_host(selection=None, machine_id=None, os_id=None, host_name=None, + label=None, kernel_pattern=None, initramfs_pattern=None, + root_opts_lvm2=None, root_opts_btrfs=None, + add_opts=None, del_opts=None, options=None): + """Edit an existing host profile. + + Modify an existing HostProfile by changing one or more of the + profile values. + + The modified HostProfile is written to disk and returned on + success. + + :param selection: A Selection specifying the boot_id to edit + :param machine_id: The machine id for the edited host profile + :param os_id: The OS id for the edited host profile + :param host_name: The host name for the edited host profile + :param label: an optional host label + :param kernel_pattern: The new kernel pattern + :param initramfs_pattern: The new initramfs pattern + :param root_opts_lvm2: The new LVM2 root options + :param root_opts_btrfs: The new BTRFS root options + :param add_opts: Additional boot options for this profile. + :param del_opts: Boot options to delete for this profile. + :param options: The new kernel options template + + :returns: The modified ``HostProfile`` + :rtype: ``HostProfile`` + """ + # Discard all selection criteria but host_id. + selection = Selection(host_id=selection.host_id) + + hps = None + hps = find_host_profiles(selection) + if not hps: + raise ValueError("No matching profile found: %s" % selection.host_id) + if len(hps) > 1: + raise ValueError("OS profile identifier '%s' is ambiguous" % + selection.os_id) + + hp = hps.pop() + hp.delete_profile() + hp.machine_id = machine_id or hp.os_id + hp.host_name = host_name or hp.host_name + hp.label = label or hp.label + hp.os_id = os_id or hp.os_id + hp.kernel_pattern = kernel_pattern or hp.kernel_pattern + hp.initramfs_pattern = initramfs_pattern or hp.initramfs_pattern + hp.root_opts_lvm2 = root_opts_lvm2 or hp.root_opts_lvm2 + hp.root_opts_btrfs = root_opts_btrfs or hp.root_opts_btrfs + hp.options = options or hp.options + hp.write_profile() + return hp + + +def list_hosts(selection=None): + """List host profiles matching selection criteria. + + Return a list of ``boom.hostprofile.HostProfile`` objects + matching the given criteria. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation. + :returns: a list of ``HostProfile`` objects. + :rtype: list + """ + hps = find_host_profiles(selection=selection) + + return hps + + +def print_hosts(selection=None, opts=None, output_fields=None, + sort_keys=None, expand=False): + """Print host profiles matching selection criteria. + + Selection criteria may be expressed via a Selection object + passed to the call using the ``selection`` parameter. + + :param selection: A Selection object giving selection + criteria for the operation + :param output_fields: a comma-separated list of output fields + :param opts: output formatting and control options + :param sort_keys: a comma-separated list of sort keys + :param expand: unused + :returns: the number of matching profiles output + :rtype: int + """ + output_fields = _expand_fields(_default_host_fields, output_fields) + + hps = find_host_profiles(selection=selection) + selected = [BoomReportObj(None, None, hp) for hp in hps] + report_fields = _host_fields + return _do_print_type(report_fields, selected, output_fields=output_fields, + opts=opts, sort_keys=sort_keys) + + +def show_legacy(selection=None, loader=BOOM_LOADER_GRUB1): + """Print boot entries in legacy boot loader formats. + + :param selection: A Selection object giving selection criteria + for the operation + :param loader: Which boot loader to use + """ + (name, decorator, path) = find_legacy_loader(loader, None) + bes = find_entries(selection=selection) + [print(decorator(be)) for be in bes] + + +# +# boom command line tool +# + +def _apply_profile_overrides(boot_entry, cmd_args): + if cmd_args.linux: + boot_entry.linux = cmd_args.linux + + if cmd_args.initrd: + boot_entry.initrd = cmd_args.initrd + + +def _optional_key_to_arg(optional_key): + """Map a Boom optional key name constant to the boom command line + argument it corresponds to. + + Returns the argument name in long option style, or None if no + matching optional key exists. + """ + _key_map = { + BOOM_ENTRY_GRUB_USERS: "--grub-users", + BOOM_ENTRY_GRUB_ARG: "--grub-arg", + BOOM_ENTRY_GRUB_CLASS: "--grub-class" + } + return _key_map[optional_key] if optional_key in _key_map else None + + +def _apply_optional_keys(be, cmd_args): + """Set the optional key values defined by ``cmd_args`` in the + ``BootEntry`` ``be``. This function assumes that the caller + has already checked that the active ``OsProfile`` accepts these + optional keys, or will handle exceptions raised by setting an + invalid optional key. + """ + if cmd_args.id: + be.id = cmd_args.id.strip() + if cmd_args.grub_arg: + be.grub_arg = cmd_args.grub_arg.strip() + if cmd_args.grub_class: + be.grub_class = cmd_args.grub_class.strip() + if cmd_args.grub_users: + be.grub_users = cmd_args.grub_users.strip() + + +def _set_optional_key_defaults(profile, cmd_args): + """Apply default values for all optional keys supported by + ``profile`` to command line arguments ``cmd_args``. + """ + for opt_key in OPTIONAL_KEYS: + bls_key = key_to_bls_name(opt_key) + if bls_key not in profile.optional_keys: + if getattr(cmd_args, bls_key) is not None: + print("Profile with os_id='%s' does not support %s" % + (profile.disp_os_id, _optional_key_to_arg(bls_key))) + return 1 + else: + if getattr(cmd_args, bls_key) is None: + setattr(cmd_args, bls_key, optional_key_default(opt_key)) + + +def _create_cmd(cmd_args, select, opts, identifier): + """Create entry command handler. + + Attempt to create a new boot entry using the arguments + supplied in ``cmd_args`` and return the command status + as an integer. + + :param cmd_args: Command line arguments for the command + :param select: Unused + :returns: integer status code returned from ``main()`` + """ + if not check_bootloader(): + _log_warn("Boom configuration not found in grub.cfg") + _log_warn("Run 'grub2-mkconfig > /boot/grub2/grub.cfg' to enable") + + if identifier is not None: + print("entry create does not accept ") + return 1 + + if not cmd_args.version: + version = get_uts_release() + if not version: + print("create requires --version") + return 1 + else: + version = cmd_args.version + + if not cmd_args.machine_id: + # Use host machine-id by default + machine_id = _get_machine_id() + if not machine_id: + print("Could not determine machine_id") + return 1 + else: + machine_id = cmd_args.machine_id + + if not cmd_args.root_device: + print("create requires --root-device") + return 1 + else: + root_device = cmd_args.root_device + + lvm_root_lv = cmd_args.root_lv if cmd_args.root_lv else None + subvol = cmd_args.btrfs_subvolume + (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol) + + no_dev = cmd_args.no_dev + + profile = _find_profile(cmd_args, version, machine_id, + "create", optional=False) + + if not profile: + return 1 + + _set_optional_key_defaults(profile, cmd_args) + + if not cmd_args.title and not profile.title: + print("create requires --title") + return 1 + else: + # Empty title will be filled out by profile + title = cmd_args.title + + add_opts = cmd_args.add_opts + del_opts = cmd_args.del_opts + + arch = cmd_args.architecture + + try: + be = create_entry(title, version, machine_id, + root_device, lvm_root_lv=lvm_root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, profile=profile, + add_opts=add_opts, del_opts=del_opts, + architecture=arch, write=False, + expand=cmd_args.expand_variables, + allow_no_dev=no_dev) + + except BoomRootDeviceError as brde: + print(brde) + print("Creating an entry with no valid root device requires --no-dev") + return 1 + except ValueError as e: + print(e) + return 1 + + _apply_profile_overrides(be, cmd_args) + _apply_optional_keys(be, cmd_args) + + try: + be.write_entry(expand=cmd_args.expand_variables) + __write_legacy() + except Exception as e: + if cmd_args.debug: + raise + print(e) + return 1 + + print("Created entry with boot_id %s:" % be.disp_boot_id) + print(_str_indent(str(be), 2)) + return 0 + + +def _delete_cmd(cmd_args, select, opts, identifier): + """Delete entry command handler. + + Attempt to delete boot entries matching the selection criteria + given in ``select``. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the entries to remove + :returns: integer status code returned from ``main()`` + """ + # If a boot_id is given as a command line argument treat it as + # a single boot entry to delete and ignore any other criteria. + identifier = identifier or cmd_args.boot_id + if identifier is not None: + select = Selection(boot_id=identifier) + + if not select or select.is_null(): + print("delete requires selection criteria") + return 1 + + if cmd_args.options: + fields = cmd_args.options + elif cmd_args.verbose: + fields = _verbose_entry_fields + else: + fields = None + try: + if cmd_args.verbose: + print_entries(selection=select, output_fields=fields, + opts=opts, sort_keys=cmd_args.sort) + nr = delete_entries(select) + except (ValueError, IndexError) as e: + print(e) + return 1 + + print("Deleted %d entr%s" % (nr, "ies" if nr > 1 else "y")) + return 0 + + +def _clone_cmd(cmd_args, select, opts, identifier): + """Clone entry command handler. + + Attempt to create a new boot entry by cloning an existing + entry. The ``boot_id`` of the supplied ``Selection`` object + is used to select the entry to clone. Any set entry values + supplied in ``cmd_args`` will be used to modify the newly + cloned entry. + + :param cmd_args: Command line arguments for the command + :param select: The ``boot_id`` to clone + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.boot_id + if identifier is not None: + select = Selection(boot_id=identifier) + + if not select or select.is_null(): + print("clone requires selection criteria") + return 1 + + title = cmd_args.title + version = cmd_args.version + root_device = cmd_args.root_device + lvm_root_lv = cmd_args.root_lv + subvol = cmd_args.btrfs_subvolume + (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol) + + if not cmd_args.machine_id: + # Use host machine-id by default + machine_id = _get_machine_id() + if not machine_id: + print("Could not determine machine_id") + return 1 + else: + machine_id = cmd_args.machine_id + + # Discard all selection criteria but boot_id. + select = Selection(boot_id=select.boot_id) + + profile = _find_profile(cmd_args, version, machine_id, "clone") + + add_opts = cmd_args.add_opts + del_opts = cmd_args.del_opts + + arch = cmd_args.architecture + + try: + be = clone_entry(select, title=title, version=version, + machine_id=machine_id, root_device=root_device, + lvm_root_lv=lvm_root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, profile=profile, + add_opts=add_opts, del_opts=del_opts, + architecture=arch, expand=cmd_args.expand_variables, + allow_no_dev=cmd_args.no_dev) + + except ValueError as e: + print(e) + return 1 + + _apply_profile_overrides(be, cmd_args) + + try: + be.write_entry(expand=cmd_args.expand_variables) + __write_legacy() + except Exception as e: + if cmd_args.debug: + raise + print(e) + return 1 + + print("Cloned entry with boot_id %s as boot_id %s:" % + (select.boot_id, be.disp_boot_id)) + print(_str_indent(str(be), 2)) + + return 0 + + +def _show_cmd(cmd_args, select, opts, identifier): + """Show entry command handler. + + Show the boot entries that match the given selection criteria in + BLS boot entry notation: one key per line, with keys and values + separated by a single space character. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the entries to show. + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.boot_id + if identifier is not None: + select = Selection(boot_id=identifier) + + try: + bes = find_entries(selection=select) + except ValueError as e: + print(e) + return 1 + first = True + for be in bes: + ws = "" if first else "\n" + be_str = be.expanded() if cmd_args.expand_variables else str(be) + be_str = _str_indent(be_str, 2) + print("%sBoot Entry (boot_id=%s)\n%s" % (ws, be.disp_boot_id, be_str)) + first = False + return 0 + + +def _generic_list_cmd(cmd_args, select, opts, verbose_fields, print_fn): + """Generic list command implementation. + + Implements a simple list command that applies selection criteria + and calls a print_*() API function to display results. + + Callers should initialise identifier and select appropriately + for the specific command arguments. + + :param cmd_args: the command arguments + :param select: selection criteria + :param opts: reporting options object + :param print_fn: the API call to display results. The function + must accept the selection, output_fields, + opts, and sort_keys keyword arguments + :returns: None + """ + if cmd_args.options: + fields = cmd_args.options + elif cmd_args.verbose: + fields = verbose_fields + else: + fields = None + + try: + print_fn(selection=select, output_fields=fields, + opts=opts, sort_keys=cmd_args.sort, + expand=cmd_args.expand_variables) + except ValueError as e: + print(e) + return 1 + return 0 + + +def _list_cmd(cmd_args, select, opts, identifier): + """List entry command handler. + List the boot entries that match the given selection criteria as + a tabular report, with one boot entry per row. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria fore the entries to list + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.boot_id + if identifier is not None: + select = Selection(boot_id=identifier) + + return _generic_list_cmd(cmd_args, select, opts, _verbose_entry_fields, + print_entries) + + +def _edit_cmd(cmd_args, select, opts, identifier): + """Edit entry command handler. + + Attempt to edit an existing boot entry. The ``boot_id`` of + the supplied ``Selection`` object is used to select the entry + to edit. Any set entry values supplied in ``cmd_args`` will be + used to modify the edited entry. + + :param cmd_args: Command line arguments for the command + :param select: The ``boot_id`` of the entry to edit + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.boot_id + if identifier is not None: + select = Selection(boot_id=identifier) + + if not select or select.is_null(): + print("edit requires selection criteria") + return 1 + + title = cmd_args.title + version = cmd_args.version + root_device = cmd_args.root_device + lvm_root_lv = cmd_args.root_lv + subvol = cmd_args.btrfs_subvolume + (btrfs_subvol_path, btrfs_subvol_id) = _subvol_from_arg(subvol) + + if not cmd_args.machine_id: + # Use host machine-id by default + machine_id = _get_machine_id() + if not machine_id: + print("Could not determine machine_id") + return 1 + else: + machine_id = cmd_args.machine_id + + profile = _find_profile(cmd_args, version, machine_id, "edit") + + arch = cmd_args.architecture + + try: + be = edit_entry(selection=select, title=title, version=version, + machine_id=machine_id, root_device=root_device, + lvm_root_lv=lvm_root_lv, + btrfs_subvol_path=btrfs_subvol_path, + btrfs_subvol_id=btrfs_subvol_id, profile=profile, + architecture=arch, expand=cmd_args.expand_variables) + except ValueError as e: + print(e) + return 1 + + _apply_profile_overrides(be, cmd_args) + + try: + be.write_entry(expand=cmd_args.expand_variables) + __write_legacy() + except Exception as e: + if cmd_args.debug: + raise + print(e) + return 1 + + print("Edited entry, boot_id now: %s" % be.disp_boot_id) + print(_str_indent(str(be), 2)) + return 0 + + +def _create_profile_cmd(cmd_args, select, opts, identifier): + """Create profile command handler. + Attempt to create a new OS profile using the arguments + supplied in ``cmd_args`` and return the command status + as an integer. + + :param cmd_args: Command line arguments for the command + :param select: Unused + :returns: integer status code returned from ``main()`` + """ + if identifier is not None: + print("profile create does not accept ") + return 1 + + if cmd_args.options: + print("Invalid argument for profile create: --options") + return 1 + + if cmd_args.os_release or cmd_args.from_host: + name = None + short_name = None + version = None + version_id = None + release = cmd_args.os_release or "/etc/os-release" + else: + if not cmd_args.name: + print("profile create requires --name") + return 1 + else: + name = cmd_args.name + + if not cmd_args.short_name: + print("profile create requires --short-name") + return 1 + else: + short_name = cmd_args.short_name + + if not cmd_args.os_version: + print("profile create requires --os-version") + return 1 + else: + version = cmd_args.os_version + + if not cmd_args.os_version_id: + print("profile create requires --os-version-id") + return 1 + else: + version_id = cmd_args.os_version_id + release = None + + try: + osp = create_profile(name, short_name, version, version_id, + uname_pattern=cmd_args.uname_pattern, + kernel_pattern=cmd_args.kernel_pattern, + initramfs_pattern=cmd_args.initramfs_pattern, + root_opts_lvm2=cmd_args.lvm_opts, + root_opts_btrfs=cmd_args.btrfs_opts, + options=cmd_args.os_options, + optional_keys=cmd_args.optional_keys, + profile_file=release) + except ValueError as e: + print(e) + return 1 + print("Created profile with os_id %s:" % osp.disp_os_id) + print(_str_indent(str(osp), 2)) + return 0 + + +def _delete_profile_cmd(cmd_args, select, opts, identifier): + """Delete profile command handler. + + Attempt to delete OS profiles matching the selection criteria + given in ``select``. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the profiles to remove + :returns: integer status code returned from ``main()`` + """ + # If an os_id is given as a command line argument treat it as + # a single OsProfile to delete and ignore any other criteria. + identifier = identifier or cmd_args.profile + if identifier is not None: + select = Selection(os_id=identifier) + + if not select or select.is_null(): + print("profile delete requires selection criteria") + return 1 + + if cmd_args.options: + fields = cmd_args.options + elif cmd_args.verbose: + fields = _verbose_profile_fields + else: + fields = None + + try: + if cmd_args.verbose: + print_profiles(select, output_fields=fields, + sort_keys=cmd_args.sort) + nr = delete_profiles(select) + except (ValueError, IndexError) as e: + print(e) + return 1 + print("Deleted %d profile%s" % (nr, "s" if nr > 1 else "")) + return 0 + + +def _clone_profile_cmd(cmd_args, select, opts, identifier): + """Clone profile command handler. + + Attempt to create a new OS profile by cloning an existing + profile. The ``os_id`` of the supplied ``Selection`` object + is used to select the profile to clone. Any set profile values + supplied in ``cmd_args`` will be used to modify the newly + cloned profile. + + :param cmd_args: Command line arguments for the command + :param select: The ``os_id`` to clone + :returns: integer status code returned from ``main()`` + """ + name = cmd_args.name + short_name = cmd_args.short_name + version = cmd_args.os_version + version_id = cmd_args.os_version_id + uname_pattern = cmd_args.uname_pattern + kernel_pattern = cmd_args.kernel_pattern + initramfs_pattern = cmd_args.initramfs_pattern + root_opts_lvm2 = cmd_args.lvm_opts + root_opts_btrfs = cmd_args.btrfs_opts + options = cmd_args.os_options + + identifier = identifier or cmd_args.profile + if identifier is not None: + select = Selection(os_id=identifier) + + if not select or select.is_null(): + print("profile delete requires selection criteria") + return 1 + + # Discard all selection criteria but os_id. + select = Selection(os_id=select.os_id) + + try: + osp = clone_profile(selection=select, name=name, short_name=short_name, + version=version, version_id=version_id, + uname_pattern=uname_pattern, + kernel_pattern=kernel_pattern, + initramfs_pattern=initramfs_pattern, + root_opts_lvm2=root_opts_lvm2, + root_opts_btrfs=root_opts_btrfs, options=options) + + except ValueError as e: + print(e) + return 1 + print("Cloned profile with os_id %s as %s:" % + (select.os_id, osp.disp_os_id)) + print(_str_indent(str(osp), 2)) + return 0 + + +def _generic_show_cmd(select, find_fn, fmt, get_data): + """Generic show command handler. + + Show the objects returned by calling `find_fn` with selection + criteria `select`, using the format string `fmt`, and the data + tuple returned by calling `get_data` for each object. + + :param select: Selection() object with search criteria. + :param find_fn: A find_*() function accepting Selection. + :param fmt: A Python format string. + :param get_data: A function returning a tuple of data values + satisfying the format string `fmt`. + """ + try: + objs = find_fn(select) + except ValueError as e: + print(e) + return 1 + + first = True + for obj in objs: + ws = "" if first else "\n" + print(ws + fmt % get_data(obj)) + first = False + return 0 + + +def _show_profile_cmd(cmd_args, select, opts, identifier): + """Show profile command handler. + + Show the OS profiles that match the given selection criteria in + human readable form. Each matching profile is printed as a + multi-line record, with like attributes grouped together on a + line. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the profiles to show. + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.profile + if identifier is not None: + select = Selection(os_id=identifier) + + def _profile_get_data(osp): + return (osp.disp_os_id, _str_indent(str(osp), 2)) + + fmt = "OS Profile (os_id=%s)\n%s" + return _generic_show_cmd(select, find_profiles, fmt, _profile_get_data) + + +def _list_profile_cmd(cmd_args, select, opts, identifier): + """List profile command handler. + + List the OS profiles that match the given selection criteria as + a tabular report, with one profile per row. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria fore the profiles to list + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.profile + if identifier is not None: + select = Selection(os_id=identifier) + + return _generic_list_cmd(cmd_args, select, opts, _verbose_profile_fields, + print_profiles) + + +def _edit_profile_cmd(cmd_args, select, opts, identifier): + """Edit profile command handler. + + Attempt to edit an existing OS profile. The ``os_id`` of the + supplied ``Selection`` object is used to select the profile to + edit. Any set entry values supplied in ``cmd_args`` will be used + to modify the edited profile. + + :param cmd_args: Command line arguments for the command + :param select: The ``os_id`` of the profile to edit + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.profile + if identifier is not None: + select = Selection(os_id=identifier) + + id_keys = (cmd_args.name, cmd_args.short_name, + cmd_args.version, cmd_args.os_version_id) + + if cmd_args.options: + print("Invalid argument for profile edit: --options") + return 1 + + if any(id_keys): + print("Cannot edit name, short_name, version, or version_id:\n" + "Use 'clone --profile OS_ID'.") + return 1 + + uname_pattern = cmd_args.uname_pattern + kernel_pattern = cmd_args.kernel_pattern + initramfs_pattern = cmd_args.initramfs_pattern + root_opts_lvm2 = cmd_args.lvm_opts + root_opts_btrfs = cmd_args.btrfs_opts + options = cmd_args.os_options + optional_keys = cmd_args.optional_keys + + try: + osp = edit_profile(selection=select, uname_pattern=uname_pattern, + kernel_pattern=kernel_pattern, + initramfs_pattern=initramfs_pattern, + root_opts_lvm2=root_opts_lvm2, + root_opts_btrfs=root_opts_btrfs, options=options, + optional_keys=optional_keys) + except ValueError as e: + print(e) + return 1 + + print("Edited profile:") + print(_str_indent(str(osp), 2)) + return 0 + + +def _create_host_cmd(cmd_args, select, opts, identifier): + """Create host profile command handler. + + Attempt to create a new host profile using the arguments + supplied in ``cmd_args`` and return the command status + as an integer. + + :param cmd_args: Command line arguments for the command + :param select: Unused + :returns: integer status code returned from ``main()`` + """ + if identifier is not None: + print("host profile create does not accept ") + return 1 + + host_name = cmd_args.host_name or platform.node() + + if not host_name: + print("host profile create requires a valid host name to be set" + "or --host-name") + return 1 + + if not cmd_args.machine_id: + # Use host machine-id by default + machine_id = _get_machine_id() + if not machine_id: + print("Could not determine machine_id") + return 1 + else: + machine_id = cmd_args.machine_id + + if not cmd_args.profile: + print("host profile create requires --profile") + return 1 + else: + os_id = cmd_args.profile + + try: + hp = create_host(machine_id=machine_id, os_id=os_id, + host_name=host_name, label=cmd_args.label, + kernel_pattern=cmd_args.kernel_pattern, + initramfs_pattern=cmd_args.initramfs_pattern, + root_opts_lvm2=cmd_args.lvm_opts, + root_opts_btrfs=cmd_args.btrfs_opts, + add_opts=cmd_args.add_opts, + del_opts=cmd_args.del_opts, + options=cmd_args.os_options) + except ValueError as e: + print(e) + return 1 + print("Created host profile with host_id %s:" % hp.disp_host_id) + print(_str_indent(str(hp), 2)) + return 0 + + +def _delete_host_cmd(cmd_args, select, opts, identifier): + """Delete host profile command handler. + + Attempt to delete host profiles matching the selection criteria + given in ``select``. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the profiles to remove + :returns: integer status code returned from ``main()`` + """ + # If a host_id is given as a command line argument treat it as + # a single HostProfile to delete and ignore any other criteria. + identifier = identifier or cmd_args.host_id + if identifier: + select = Selection(host_id=identifier) + + if not select or select.is_null(): + print("host profile delete requires selection criteria") + return 1 + + if cmd_args.options: + fields = cmd_args.options + elif cmd_args.verbose: + fields = _verbose_host_fields + else: + fields = _default_host_fields + + try: + if cmd_args.verbose: + print_hosts(select, output_fields=fields, + sort_keys=cmd_args.sort) + nr = delete_hosts(select) + except (ValueError, IndexError) as e: + print(e) + return 1 + print("Deleted %d profile%s" % (nr, "s" if nr > 1 else "")) + return 0 + + +def _clone_host_cmd(cmd_args, select, opts, identifier): + """Clone host profile command handler. + + Attempt to create a new host profile by cloning an existing + profile. The ``host_id`` of the supplied ``Selection`` object + is used to select the profile to clone. Any set profile values + supplied in ``cmd_args`` will be used to modify the newly + cloned profile. + + :param cmd_args: Command line arguments for the command + :param select: The ``host_id`` to clone + :returns: integer status code returned from ``main()`` + """ + host_name = cmd_args.host_name + os_id = cmd_args.profile + + identifier = identifier or cmd_args.host_id + if identifier is not None: + select = Selection(host_id=identifier) + + # For clone allow the machine_id to be inherited from the original + # HostProfile unless the user has given an explicit argument. + machine_id = cmd_args.machine_id + + # Cloning to modify only the host label is permitted + label = cmd_args.label + + # Discard all selection criteria but host_id. + select = Selection(host_id=select.host_id) + + try: + hp = clone_host(selection=select, machine_id=machine_id, + label=label, os_id=os_id, host_name=host_name, + kernel_pattern=cmd_args.kernel_pattern, + initramfs_pattern=cmd_args.initramfs_pattern, + root_opts_lvm2=cmd_args.lvm_opts, + root_opts_btrfs=cmd_args.btrfs_opts, + add_opts=cmd_args.add_opts, + del_opts=cmd_args.del_opts, + options=cmd_args.os_options) + except ValueError as e: + print(e) + return 1 + print("Cloned profile with host_id %s as %s:" % + (select.host_id, hp.disp_host_id)) + print(_str_indent(str(hp), 2)) + return 0 + + +def _show_host_cmd(cmd_args, select, opts, identifier): + """Show host profile command handler. + + Show the host profiles that match the given selection criteria + in human readable form. Each matching profile is printed as a + multi-line record, with like attributes grouped together on a + line. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria for the profiles to show. + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.host_id + if identifier is not None: + select = Selection(host_id=identifier) + + def _host_get_data(hp): + return (hp.disp_host_id, _str_indent(str(hp), 2)) + + fmt = "Host Profile (host_id=%s)\n%s" + + return _generic_show_cmd(select, find_host_profiles, fmt, _host_get_data) + + +def _list_host_cmd(cmd_args, select, opts, identifier): + """List host profile command handler. + + List the host profiles that match the given selection criteria + as a tabular report, with one profile per row. + + :param cmd_args: Command line arguments for the command + :param select: Selection criteria fore the profiles to list + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.host_id + if identifier is not None: + select = Selection(host_id=identifier) + + return _generic_list_cmd(cmd_args, select, opts, _verbose_host_fields, + print_hosts) + + +def _edit_host_cmd(cmd_args, select, opts, identifier): + """Edit profile command handler. + + Attempt to edit an existing host profile. The ``host_id`` of the + supplied ``Selection`` object is used to select the profile to + edit. Any set entry values supplied in ``cmd_args`` will be used + to modify the edited profile. + + :param cmd_args: Command line arguments for the command + :param select: The ``host_id`` of the profile to edit + :returns: integer status code returned from ``main()`` + """ + identifier = identifier or cmd_args.host_id + if identifier is not None: + select = Selection(host_id=identifier) + + if cmd_args.options: + print("Invalid argument for 'host edit': --options\n" + "To modify profile options template use --os-options") + return 1 + + machine_id = cmd_args.machine_id + os_id = cmd_args.profile + host_name = cmd_args.host_name + kernel_pattern = cmd_args.kernel_pattern + initramfs_pattern = cmd_args.initramfs_pattern + root_opts_lvm2 = cmd_args.lvm_opts + root_opts_btrfs = cmd_args.btrfs_opts + add_opts = cmd_args.add_opts + del_opts = cmd_args.del_opts + options = cmd_args.os_options + + try: + hp = edit_host(selection=select, + machine_id=machine_id, os_id=os_id, + host_name=host_name, kernel_pattern=kernel_pattern, + initramfs_pattern=initramfs_pattern, + root_opts_lvm2=root_opts_lvm2, + root_opts_btrfs=root_opts_btrfs, + add_opts=add_opts, del_opts=del_opts, options=options) + except ValueError as e: + print(e) + return 1 + + print("Edited profile:") + print(_str_indent(str(hp), 2)) + return 0 + + +def _write_legacy_cmd(cmd_args, select, opts, identifier): + if identifier: + print("write legacy does not accept a boot_id") + return 1 + config = get_boom_config() + try: + clear_legacy_loader() + write_legacy_loader(selection=select, loader=config.legacy_format) + except Exception as e: + print(e) + return 1 + + +def _clear_legacy_cmd(cmd_args, select, opts, identifier): + """Remove all boom entries from the legacy bootloader configuration. + + :param cmd_args: Command line arguments for the command + :returns: integer status code returned from ``main()`` + """ + if identifier: + print("write legacy does not accept a boot_id") + return 1 + + try: + clear_legacy_loader() + except BoomLegacyFormatError as e: + print(e) + return 1 + + +def _show_legacy_cmd(cmd_args, select, opts, identifier): + # FIXME: args + config = get_boom_config() + show_legacy(selection=select, loader=config.legacy_format) + + +CREATE_CMD = "create" +DELETE_CMD = "delete" +CLONE_CMD = "clone" +CLEAR_CMD = "clear" +SHOW_CMD = "show" +LIST_CMD = "list" +EDIT_CMD = "edit" + +WRITE_CMD = "write" + +ENTRY_TYPE = "entry" +PROFILE_TYPE = "profile" +HOST_TYPE = "host" +LEGACY_TYPE = "legacy" + +_boom_entry_commands = [ + (CREATE_CMD, _create_cmd), + (DELETE_CMD, _delete_cmd), + (CLONE_CMD, _clone_cmd), + (SHOW_CMD, _show_cmd), + (LIST_CMD, _list_cmd), + (EDIT_CMD, _edit_cmd) +] + +_boom_profile_commands = [ + (CREATE_CMD, _create_profile_cmd), + (DELETE_CMD, _delete_profile_cmd), + (CLONE_CMD, _clone_profile_cmd), + (SHOW_CMD, _show_profile_cmd), + (LIST_CMD, _list_profile_cmd), + (EDIT_CMD, _edit_profile_cmd) +] + +_boom_host_commands = [ + (CREATE_CMD, _create_host_cmd), + (DELETE_CMD, _delete_host_cmd), + (CLONE_CMD, _clone_host_cmd), + (SHOW_CMD, _show_host_cmd), + (LIST_CMD, _list_host_cmd), + (EDIT_CMD, _edit_host_cmd) +] + +_boom_legacy_commands = [ + (WRITE_CMD, _write_legacy_cmd), + (CLEAR_CMD, _clear_legacy_cmd), + (SHOW_CMD, _show_legacy_cmd) +] + +_boom_command_types = [ + (ENTRY_TYPE, _boom_entry_commands), + (PROFILE_TYPE, _boom_profile_commands), + (HOST_TYPE, _boom_host_commands), + (LEGACY_TYPE, _boom_legacy_commands) +] + + +def _id_from_arg(cmd_args, cmdtype, cmd): + if cmd == CREATE_CMD: + if cmdtype == ENTRY_TYPE: + return cmd_args.boot_id + if cmdtype == PROFILE_TYPE: + return cmd_args.profile + else: + if cmd_args.identifier: + return cmd_args.identifier + if cmdtype == ENTRY_TYPE: + return cmd_args.boot_id + if cmdtype == PROFILE_TYPE: + return cmd_args.profile + return None + + +def _match_cmd_type(cmdtype): + for t in _boom_command_types: + if t[0].startswith(cmdtype): + return t + return None + + +def _match_command(cmd, cmds): + for c in cmds: + if cmd == c[0]: + return c + return None + + +def _report_opts_from_args(cmd_args): + opts = BoomReportOpts() + + if not cmd_args: + return opts + + if cmd_args.rows: + opts.columns_as_rows = True + + if cmd_args.separator: + opts.separator = cmd_args.separator + + if cmd_args.name_prefixes: + opts.field_name_prefix = "BOOM_" + opts.unquoted = False + opts.aligned = False + + if cmd_args.no_headings: + opts.headings = False + + return opts + + +def get_uts_release(): + return uname()[2] + + +def setup_logging(cmd_args): + global _console_handler + level = _default_log_level + if cmd_args.verbose and cmd_args.verbose > 1: + level = logging.DEBUG + elif cmd_args.verbose and cmd_args.verbose > 0: + level = logging.INFO + # Configure the package-level logger + boom_log = logging.getLogger("boom") + formatter = logging.Formatter('%(levelname)s - %(message)s') + boom_log.setLevel(level) + _console_handler = logging.StreamHandler() + _console_handler.setLevel(level) + _console_handler.setFormatter(formatter) + boom_log.addHandler(_console_handler) + + +def shutdown_logging(): + logging.shutdown() + + +def set_debug(debug_arg): + if not debug_arg: + return + + mask_map = { + "profile": BOOM_DEBUG_PROFILE, + "entry": BOOM_DEBUG_ENTRY, + "report": BOOM_DEBUG_REPORT, + "command": BOOM_DEBUG_COMMAND, + "all": BOOM_DEBUG_ALL + } + + mask = 0 + for name in debug_arg.split(','): + if name not in mask_map: + raise ValueError("Unknown debug mask: %s" % name) + mask |= mask_map[name] + set_debug_mask(mask) + + +def main(args): + global _boom_entry_commands, _boom_profile_commands, _boom_command_types + parser = ArgumentParser(prog=basename(args[0]), + description="Boom Boot Manager") + + # Default type is boot entry. + if len(args) > 1 and _match_command(args[1], _boom_entry_commands): + args.insert(1, "entry") + + parser.add_argument("type", metavar="[TYPE]", type=str, + help="The command type to run: profile or entry", + action="store") + parser.add_argument("command", metavar="COMMAND", type=str, action="store", + help="The command to run: create, delete, list, edit, " + "clone, show") + parser.add_argument("identifier", metavar="ID", type=str, action="store", + help="An optional profile or boot identifier to " + "operate on", nargs="?", default=None) + parser.add_argument("-a", "--add-opts", "--addopts", metavar="OPTIONS", + help="Additional kernel options to append", type=str) + parser.add_argument("--architecture", metavar="ARCH", default=None, + help="An optional BLS architecture string", type=str) + parser.add_argument("-b", "--boot-id", "--bootid", metavar="BOOT_ID", + type=str, help="The BOOT_ID of a boom boot entry") + parser.add_argument("--boot-dir", "--bootdir", metavar="PATH", type=str, + help="The path to the /boot file system") + parser.add_argument("-B", "--btrfs-subvolume", "--btrfssubvolume", + metavar="SUBVOL", type=str, + help="The path or ID of a BTRFS subvolume") + parser.add_argument("--btrfs-opts", "--btrfsopts", metavar="OPTS", + type=str, help="A template option string for BTRFS " + "devices") + parser.add_argument("-c", "--config", metavar="FILE", type=str, + help="Path to a boom configuration file", default=None) + parser.add_argument("-d", "--del-opts", "--delopts", metavar="OPTIONS", + help="List of kernel options to be dropped", type=str) + parser.add_argument("--debug", metavar="DEBUGOPTS", type=str, + help="A list of debug options to enable") + parser.add_argument("-e", "--efi", metavar="IMG", type=str, + help="An executable EFI application image") + parser.add_argument("-E", "--expand-variables", action="store_true", + help="Expand bootloader environment variables") + parser.add_argument("--grub-arg", metavar="ARGS", type=str, + help="Pass additional arguments to the Grub2 loader") + parser.add_argument("--grub-class", metavar="CLASS", type=str, + help="Specify a Grub2 class for this entry") + parser.add_argument("--grub-users", metavar="USERS", type=str, + help="Grub user list for password protection") + parser.add_argument("--grub-id", metavar="ID", type=str, dest="id", + help="Grub menu identifier string") + parser.add_argument("-H", "--from-host", "--fromhost", + help="Take os-release values from the running host", + action="store_true") + parser.add_argument("-P", "--host-profile", metavar="PROFILE", type=str, + help="A boom host profile identifier") + parser.add_argument("--host-id", metavar="HOSTID", type=str, + help="A host profile identifier") + parser.add_argument("--host-name", metavar="HOSTNAME", type=str, + help="The host name associated with a host profile") + parser.add_argument("-i", "--initrd", metavar="IMG", type=str, + help="A linux initrd image path") + parser.add_argument("-k", "--kernel-pattern", "--kernelpattern", + metavar="PATTERN", type=str, + help="A pattern for generating kernel paths") + parser.add_argument("--label", metavar="LABEL", type=str, + help="Host profile label") + parser.add_argument("-l", "--linux", metavar="IMG", type=str, + help="A linux kernel image path") + parser.add_argument("-L", "--root-lv", "--rootlv", metavar="LV", type=str, + help="An LVM2 root logical volume") + parser.add_argument("--lvm-opts", "--lvmopts", metavar="OPTS", type=str, + help="A template option string for LVM2 devices") + parser.add_argument("-m", "--machine-id", "--machineid", + metavar="MACHINE_ID", type=str, + help="The machine_id value to use") + parser.add_argument("-n", "--name", metavar="OSNAME", type=str, + help="The name of a Boom OsProfile") + parser.add_argument("--name-prefixes", "--nameprefixes", + help="Add a prefix to report field names", + action='store_true'), + parser.add_argument("--no-headings", "--noheadings", action='store_true', + help="Suppress output of report headings"), + parser.add_argument("--no-dev", "--nodev", action='store_true', + help="Disable checks for a valid root device") + parser.add_argument("--optional-keys", metavar="KEYS", type=str, + help="Optional keys allows by this operating system " + "profile") + parser.add_argument("-o", "--options", metavar="FIELDS", type=str, + help="Specify which fields to display") + parser.add_argument("--os-version", "--osversion", metavar="OSVERSION", + help="A Boom OsProfile version", type=str) + parser.add_argument("-O", "--sort", metavar="SORTFIELDS", type=str, + help="Specify which fields to sort by") + parser.add_argument("-I", "--os-version-id", "--osversionid", + help="A Boom OsProfile version ID", + metavar="OSVERSIONID", type=str) + parser.add_argument("--os-options", "--osoptions", metavar="OPTIONS", + help="A Boom OsProfile options template", type=str) + parser.add_argument("--os-release", "--osrelease", metavar="OSRELEASE", + help="Path to an os-release file", type=str) + parser.add_argument("-p", "--profile", metavar="OS_ID", type=str, + help="A boom operating system profile " + "identifier") + parser.add_argument("-r", "--root-device", "--rootdevice", metavar="ROOT", + help="The root device for a boot entry", type=str) + parser.add_argument("-R", "--initramfs-pattern", "--initramfspattern", + type=str, help="A pattern for generating initramfs " + "paths", metavar="PATTERN") + parser.add_argument("--rows", action="store_true", + help="Output report columnes as rows") + parser.add_argument("--separator", metavar="SEP", type=str, + help="Report field separator") + parser.add_argument("-s", "--short-name", "--shortname", + help="A Boom OsProfile short name", + metavar="OSSHORTNAME", type=str) + parser.add_argument("-t", "--title", metavar="TITLE", type=str, + help="The title of a boom boot entry") + parser.add_argument("-u", "--uname-pattern", "--unamepattern", + help="A Boom OsProfile uname pattern", + metavar="PATTERN", type=str) + parser.add_argument("-V", "--verbose", help="Enable verbose ouput", + action="count") + parser.add_argument("-v", "--version", metavar="VERSION", type=str, + help="The kernel version of a boom " + "boot entry") + try: + cmd_args = parser.parse_args(args=args[1:]) + except SystemExit as e: + return e.code + + try: + set_debug(cmd_args.debug) + except ValueError as e: + print(e) + return 1 + setup_logging(cmd_args) + cmd_type = _match_cmd_type(cmd_args.type) + + if cmd_args.boot_dir or BOOM_BOOT_PATH_ENV in environ: + boot_path = cmd_args.boot_dir or environ[BOOM_BOOT_PATH_ENV] + if not isabs(boot_path): + boot_path = join(getcwd(), boot_path) + set_boot_path(boot_path) + set_boom_config_path("boom.conf") + + if cmd_args.config: + set_boom_config_path(cmd_args.config) + + if not path_exists(get_boom_path()): + _log_error("Configuration directory '%s' not found." % + get_boom_path()) + return 1 + + if not path_exists(get_boom_config_path()): + _log_error("Configuration file '%s' not found." % + get_boom_config_path()) + return 1 + + if not path_exists(boom_profiles_path()): + _log_error("OS profile configuration path '%s' not found." % + boom_profiles_path()) + return 1 + + if not path_exists(boom_host_profiles_path()): + _log_error("Host profile configuration path '%s' not found." % + boom_host_profiles_path()) + return 1 + + if not path_exists(boom_entries_path()): + _log_error("Boot loader entries directory '%s' not found." % + boom_entries_path()) + return 1 + + # Parse an LV name from root_lv and re-write the root_device if found + if cmd_args.root_lv: + try: + root_lv = _canonicalize_lv_name(cmd_args.root_lv) + except ValueError as e: + print(e) + print("Invalid logical volume name: '%s'" % cmd_args.root_lv) + return 1 + root_device = DEV_PATTERN % root_lv + if cmd_args.root_device and cmd_args.root_device != root_device: + print("Options --root-lv %s and --root-device %s do not match." % + (root_lv, root_device)) + return 1 + cmd_args.root_device = root_device + cmd_args.root_lv = root_lv + + # Try parsing an LV name from root_device and rewrite root_lv if found + elif cmd_args.root_device: + try: + root_lv = _canonicalize_lv_name(cmd_args.root_device) + cmd_args.root_lv = root_lv + except ValueError: + # No valid VG name + pass + + if not cmd_type: + print("Unknown command type: %s" % cmd_args.type) + return 1 + + type_cmds = cmd_type[1] + command = _match_command(cmd_args.command, type_cmds) + if not command: + print("Unknown command: %s %s" % (cmd_type[0], cmd_args.command)) + return 1 + + select = Selection.from_cmd_args(cmd_args) + opts = _report_opts_from_args(cmd_args) + identifier = _id_from_arg(cmd_args, cmd_type[0], command[0]) + status = 1 + + if cmd_args.debug: + status = command[1](cmd_args, select, opts, identifier) + else: + try: + status = command[1](cmd_args, select, opts, identifier) + except Exception as e: + _log_error("Command failed: %s" % e) + + shutdown_logging() + return status + + +__all__ = [ + # BootEntry manipulation + 'create_entry', 'delete_entries', 'clone_entry', 'edit_entry', + 'list_entries', 'print_entries', + + # OsProfile manipulation + 'create_profile', 'delete_profiles', 'clone_profile', 'edit_profile', + 'list_profiles', 'print_profiles', + + # HostProfile manipulation + 'create_host', 'delete_hosts', 'clone_host', 'edit_host', + 'list_hosts', 'print_hosts' +] + +# vim: set et ts=4 sw=4 : diff --git a/boom/config.py b/boom/config.py new file mode 100644 index 0000000..4ed3d14 --- /dev/null +++ b/boom/config.py @@ -0,0 +1,216 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# config.py - Boom persistent configuration +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.config`` module defines classes, constants and functions +for reading and writing persistent (on-disk) configuration for the boom +library and tools. + +Users of the module can load and write configuration data, and obtain +the values of configuration keys defined in the boom configuration file. +""" +from __future__ import print_function + +from boom import * + +from os.path import dirname + +from os import fdopen, rename, chmod, fdatasync +from tempfile import mkstemp +import logging + +try: + # Python2 + from ConfigParser import SafeConfigParser as ConfigParser, ParsingError +except ModuleNotFoundError: + # Python3 + from configparser import ConfigParser, ParsingError + + +class BoomConfigError(BoomError): + """Base class for boom configuration errors. + """ + pass + + +# Module logging configuration +_log = logging.getLogger(__name__) + +_log_debug = _log.debug +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +# +# Constants for configuration sections and options: to add a new option, +# create a new _CFG_* constant giving the name of the option and add a +# hook to load_boom_config() to set the value when read. +# +# To add a new section add the section name constant as _CFG_SECT_* and +# add a new branch to load_boom_config() to test for the presence of +# the section and handle the options found within. +# +_CFG_SECT_GLOBAL = "global" +_CFG_SECT_LEGACY = "legacy" +_CFG_BOOT_ROOT = "boot_root" +_CFG_BOOM_ROOT = "boom_root" +_CFG_LEGACY_ENABLE = "enable" +_CFG_LEGACY_FMT = "format" +_CFG_LEGACY_SYNC = "sync" + + +def _read_boom_config(path=None): + """Read boom persistent configuration values from the defined path + and return them as a ``BoomConfig`` object. + + :param path: the configuration file to read, or None to read the + currently configured config file path. + + :rtype: BoomConfig + """ + path = path or get_boom_config_path() + _log_debug("reading boom configuration from '%s'" % path) + cfg = ConfigParser() + try: + cfg.read(path) + except ParsingError as e: + _log_error("Failed to parse configuration file '%s': %s" % + (path, e)) + + bc = BoomConfig() + + trues = ['True', 'true', 'Yes', 'yes'] + + if not cfg.has_section(_CFG_SECT_GLOBAL): + raise ValueError("Missing 'global' section in %s" % path) + + if cfg.has_section(_CFG_SECT_GLOBAL): + if cfg.has_option(_CFG_SECT_GLOBAL, _CFG_BOOT_ROOT): + _log_debug("Found global.boot_path") + bc.boot_path = cfg.get(_CFG_SECT_GLOBAL, _CFG_BOOT_ROOT) + if cfg.has_option(_CFG_SECT_GLOBAL, _CFG_BOOM_ROOT): + _log_debug("Found global.boom_path") + bc.boom_path = cfg.get(_CFG_SECT_GLOBAL, _CFG_BOOM_ROOT) + + if cfg.has_section(_CFG_SECT_LEGACY): + if cfg.has_option(_CFG_SECT_LEGACY, _CFG_LEGACY_ENABLE): + _log_debug("Found legacy.enable") + enable = cfg.get(_CFG_SECT_LEGACY, _CFG_LEGACY_ENABLE) + bc.legacy_enable = any([t for t in trues if t in enable]) + + if cfg.has_option(_CFG_SECT_LEGACY, _CFG_LEGACY_FMT): + bc.legacy_format = cfg.get(_CFG_SECT_LEGACY, + _CFG_LEGACY_FMT) + + if cfg.has_option(_CFG_SECT_LEGACY, _CFG_LEGACY_SYNC): + _log_debug("Found legacy.sync") + sync = cfg.get(_CFG_SECT_LEGACY, _CFG_LEGACY_SYNC) + bc.legacy_sync = any([t for t in trues if t in sync]) + + _log_debug("read configuration: %s" % repr(bc)) + bc._cfg = cfg + return bc + + +def load_boom_config(path=None): + """Load boom persistent configuration values from the defined path + and make the them the active configuration. + + :param path: the configuration file to read, or None to read the + currently configured config file path + + :rtype: None + """ + bc = _read_boom_config(path=path) + set_boom_config(bc) + + +def _sync_config(bc, cfg): + """Sync the configuration values of ``BoomConfig`` object ``bc`` to + the ``ConfigParser`` ``cfg``. + """ + def yes_no(value): + if value: + return "yes" + return "no" + + def attr_has_value(obj, attr): + return hasattr(obj, attr) and getattr(obj, attr) is not None + + if attr_has_value(bc, "boot_path"): + cfg.set(_CFG_SECT_GLOBAL, _CFG_BOOT_ROOT, bc.boot_path) + if attr_has_value(bc, "boom_path"): + cfg.set(_CFG_SECT_GLOBAL, _CFG_BOOM_ROOT, bc.boom_path) + if attr_has_value(bc, "legacy_enable"): + cfg.set(_CFG_SECT_LEGACY, _CFG_LEGACY_ENABLE, yes_no(bc.legacy_enable)) + if attr_has_value(bc, "legacy_format"): + cfg.set(_CFG_SECT_LEGACY, _CFG_LEGACY_FMT, bc.legacy_format) + if attr_has_value(bc, "legacy_sync"): + cfg.set(_CFG_SECT_LEGACY, _CFG_LEGACY_SYNC, yes_no(bc.legacy_sync)) + + +def __make_config(bc): + """Create a new ``ConfigParser`` corresponding to the ``BoomConfig`` + object ``bc`` and return the result. + """ + cfg = ConfigParser() + cfg.add_section("global") + cfg.add_section("legacy") + _sync_config(bc, cfg) + return bc + + +def write_boom_config(config=None, path=None): + """Write boom configuration to disk. + + :param config: the configuration values to write, or None to + write the current configuration + :param path: the configuration file to read, or None to read the + currently configured config file path + + :rtype: None + """ + path = path or get_boom_config_path() + cfg_dir = dirname(path) + (tmp_fd, tmp_path) = mkstemp(prefix="boom", dir=cfg_dir) + + config = config or get_boom_config() + + if not config._cfg: + config._cfg = __make_config(config) + else: + _sync_config(config, config._cfg) + + with fdopen(tmp_fd, "w") as f_tmp: + config._cfg.write(f_tmp) + fdatasync(tmp_fd) + + try: + rename(tmp_path, path) + chmod(path, BOOT_CONFIG_MODE) + except Exception as e: + _log_error("Error writing configuration file %s: %s" % + (path, e)) + try: + unlink(tmp_path) + except Exception: + pass + raise e + + +__all__ = [ + 'BoomConfigError', + + # Configuration file handling + 'load_boom_config', + 'write_boom_config' +] diff --git a/boom/hostprofile.py b/boom/hostprofile.py new file mode 100644 index 0000000..cd56f89 --- /dev/null +++ b/boom/hostprofile.py @@ -0,0 +1,1174 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# hostprofile.py - Boom host profiles +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.hostprofile`` module defines the `HostProfile` class that +represents a host system profile. A `HostProfile` defines the identity +of a host and includes template values that override the corresponding +``OsProfile`` defaults for the respective host. + +Functions are provided to read and write host system profiles from +an on-disk store, and to retrieve ``HostProfile`` instances using +various selection criteria. + +The ``HostProfile`` class includes named properties for each profile +attribute ("profile key"). In addition, the class serves as a container +type, allowing attributes to be accessed via dictionary-style indexing. +This simplifies iteration over a profile's key / value pairs and allows +straightforward access to all members in scripts and the Python shell. + +The keys used to access ``HostProfile`` members (and their corresponding +property names) are identical to those used by the ``OsProfile`` class. +""" +from __future__ import print_function + +from boom import * +from boom.osprofile import * + +from hashlib import sha1 +from os.path import join as path_join +import logging +import string + +# Module logging configuration +_log = logging.getLogger(__name__) +_log.set_debug_mask(BOOM_DEBUG_PROFILE) + +_log_debug = _log.debug +_log_debug_profile = _log.debug_masked +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +#: Global host profile list +_host_profiles = [] +_host_profiles_by_id = {} +_host_profiles_by_host_id = {} + +#: Whether profiles have been read from disk +_host_profiles_loaded = False + +#: Boom profiles directory name. +BOOM_HOST_PROFILES = "hosts" + +#: File name format for Boom profiles. +BOOM_HOST_PROFILE_FORMAT = "%s-%s.host" + +#: The file mode with which to create Boom profiles. +BOOM_HOST_PROFILE_MODE = 0o644 + +# Constants for Boom profile keys +#: Constant for the Boom host identifier profile key. +BOOM_HOST_ID = "BOOM_HOST_ID" +#: Constant for the Boom host name profile key. +BOOM_HOST_NAME = "BOOM_HOST_NAME" +#: Constant for the Boom host add options key. +BOOM_HOST_ADD_OPTS = "BOOM_HOST_ADD_OPTS" +#: Constant for the Boom host del options key. +BOOM_HOST_DEL_OPTS = "BOOM_HOST_DEL_OPTS" +#: Constant for the Boom host label key. +BOOM_HOST_LABEL = "BOOM_HOST_LABEL" + +#: Constant for shared machine_id key +BOOM_ENTRY_MACHINE_ID = "BOOM_ENTRY_MACHINE_ID" + +#: Ordered list of possible host profile keys, partitioned into +#: mandatory keys, optional host profile keys, keys mapping to +#: embedded ``OsProfile`` identity data, and ``OsProfile`` pattern +#: keys that may be overridden in the ``HostProfile``. +HOST_PROFILE_KEYS = [ + # HostProfile identifier + BOOM_HOST_ID, + # Machine hostname + BOOM_HOST_NAME, + # Binding to label, machine_id and OsProfile + BOOM_ENTRY_MACHINE_ID, BOOM_OS_ID, + # Optional host profile keys + BOOM_HOST_LABEL, BOOM_HOST_ADD_OPTS, BOOM_HOST_DEL_OPTS, + # Keys 7-10 OS identity keys mapped to the embedded OsProfile. + BOOM_OS_NAME, BOOM_OS_SHORT_NAME, BOOM_OS_VERSION, BOOM_OS_VERSION_ID, + # Keys 11-15 (OsProfile patterns) may be overridden in the host profile. + BOOM_OS_UNAME_PATTERN, BOOM_OS_KERNEL_PATTERN, BOOM_OS_INITRAMFS_PATTERN, + BOOM_OS_ROOT_OPTS_LVM2, BOOM_OS_ROOT_OPTS_BTRFS, + BOOM_OS_OPTIONS +] + +#: A map of Boom host profile keys to human readable key names suitable +#: for use in formatted output. These key names are used to format a +#: ``HostProfile`` object as a human readable string. +HOST_KEY_NAMES = { + # Keys unique to HostProfile + BOOM_HOST_ID: "Host ID", + BOOM_HOST_NAME: "Host name", + BOOM_HOST_LABEL: "Host label", + BOOM_HOST_ADD_OPTS: "Add options", + BOOM_HOST_DEL_OPTS: "Del options", + # Keys shared with BootEntry + BOOM_ENTRY_MACHINE_ID: "Machine ID", + # Keys incorporated from OsProfile + BOOM_OS_ID: OS_KEY_NAMES[BOOM_OS_ID], + BOOM_OS_NAME: OS_KEY_NAMES[BOOM_OS_NAME], + BOOM_OS_SHORT_NAME: OS_KEY_NAMES[BOOM_OS_SHORT_NAME], + BOOM_OS_VERSION: OS_KEY_NAMES[BOOM_OS_VERSION], + BOOM_OS_VERSION_ID: OS_KEY_NAMES[BOOM_OS_VERSION_ID], + BOOM_OS_UNAME_PATTERN: OS_KEY_NAMES[BOOM_OS_UNAME_PATTERN], + BOOM_OS_KERNEL_PATTERN: OS_KEY_NAMES[BOOM_OS_KERNEL_PATTERN], + BOOM_OS_INITRAMFS_PATTERN: OS_KEY_NAMES[BOOM_OS_INITRAMFS_PATTERN], + BOOM_OS_ROOT_OPTS_LVM2: OS_KEY_NAMES[BOOM_OS_ROOT_OPTS_LVM2], + BOOM_OS_ROOT_OPTS_BTRFS: OS_KEY_NAMES[BOOM_OS_ROOT_OPTS_BTRFS], + BOOM_OS_OPTIONS: OS_KEY_NAMES[BOOM_OS_OPTIONS] +} + +#: Boom host profile keys that must exist in a valid profile. +HOST_REQUIRED_KEYS = HOST_PROFILE_KEYS[0:4] + +#: Boom optional host profile configuration keys. +HOST_OPTIONAL_KEYS = HOST_PROFILE_KEYS[4:] + + +def _host_exists(host_id): + """Test whether the specified ``host_id`` already exists. + + Used during ``HostProfile`` initialisation to test if the new + ``host_id`` is already known (and to avoid passing through + find_profiles(), which may trigger recursive profile loading). + + :param host_id: the host identifier to check for + + :returns: ``True`` if the identifier is known or ``False`` + otherwise. + :rtype: bool + """ + global _host_profiles_by_host_id + if not _host_profiles_by_host_id: + return False + if host_id in _host_profiles_by_host_id: + return True + return False + + +def boom_host_profiles_path(): + """Return the path to the boom host profiles directory. + + :returns: The boom host profiles path. + :rtype: str + """ + return path_join(get_boom_path(), BOOM_HOST_PROFILES) + + +def host_profiles_loaded(): + """Test whether profiles have been loaded from disk. + + :rtype: bool + :returns: ``True`` if profiles are loaded in memory or ``False`` + otherwise + """ + return _host_profiles_loaded + + +def drop_host_profiles(): + """Drop all in-memory host profiles. + """ + global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id + global _host_profiles_loaded + + _host_profiles = [] + _host_profiles_by_id = {} + _host_profiles_by_host_id = {} + _host_profiles_loaded = False + + +def load_host_profiles(): + """Load HostProfile data from disk. + + Load the set of host profiles found at the path + ``boom.hostprofile.boom_profiles_path()`` into the global host + profile list. + + This function may be called to explicitly load, or reload the + set of profiles on-disk. Profiles are also loaded implicitly + if an API function or method that requires access to profiles + is invoked (for example, ``boom.bootloader.load_entries()``. + + :returns: None + """ + global _host_profiles_loaded + drop_host_profiles() + profiles_path = boom_host_profiles_path() + load_profiles_for_class(HostProfile, "Host", profiles_path, "host") + + _host_profiles_loaded = True + _log_info("Loaded %d host profiles" % len(_host_profiles)) + + +def write_host_profiles(force=False): + """Write all HostProfile data to disk. + + Write the current list of host profiles to the directory located + at ``boom.osprofile.boom_profiles_path()``. + + :rtype: None + """ + global _host_profiles + _log_debug("Writing host profiles to %s" % boom_host_profiles_path()) + for hp in _host_profiles: + try: + hp.write_profile(force) + except Exception as e: + _log_warn("Failed to write HostProfile(machine_id='%s'): %s" % + (hp.disp_machine_id, e)) + + +def min_host_id_width(): + """Calculate the minimum unique width for host_id values. + + Calculate the minimum width to ensure uniqueness when displaying + host_id values. + + :returns: the minimum host_id width. + :rtype: int + """ + return min_id_width(7, _host_profiles, "host_id") + + +def min_machine_id_width(): + """Calculate the minimum unique width for host_id values. + + Calculate the minimum width to ensure uniqueness when displaying + host_id values. + + :returns: the minimum host_id width. + :rtype: int + """ + return min_id_width(7, _host_profiles, "machine_id") + + +def select_host_profile(s, hp): + """Test the supplied host profile against selection criteria. + + Test the supplied ``HostProfile`` against the selection criteria + in ``s`` and return ``True`` if it passes, or ``False`` + otherwise. + + :param s: The selection criteria + :param hp: The ``HostProfile`` to test + :rtype: bool + :returns: True if ``hp`` passes selection or ``False`` otherwise. + """ + if s.host_id and not hp.host_id.startswith(s.host_id): + return False + if s.machine_id and hp.machine_id != s.machine_id: + return False + if s.host_name and hp.host_name != s.host_name: + return False + if s.host_label and hp.label != s.host_label: + return False + if s.host_short_name and hp.short_name != s.host_short_name: + return False + if s.host_add_opts and hp.add_opts != s.host_add_opts: + return False + if s.host_del_opts and hp.del_opts != s.host_del_opts: + return False + if s.os_id and not hp.os_id.startswith(s.os_id): + return False + if s.os_name and hp.os_name != s.os_name: + return False + if s.os_short_name and hp.os_short_name != s.os_short_name: + return False + if s.os_version and hp.os_version != s.os_version: + return False + if s.os_version_id and hp.os_version_id != s.os_version_id: + return False + if s.os_uname_pattern and hp.uname_pattern != s.os_uname_pattern: + return False + if s.os_kernel_pattern and hp.kernel_pattern != s.os_kernel_pattern: + return False + if (s.os_initramfs_pattern and + hp.initramfs_pattern != s.os_initramfs_pattern): + return False + if s.os_options and hp.options != s.os_options: + return False + return True + + +def find_host_profiles(selection=None, match_fn=select_host_profile): + """Find host profiles matching selection criteria. + + Return a list of ``HostProfile`` objects matching the specified + criteria. Matching proceeds as the logical 'and' of all criteria. + Criteria that are unset (``None``) are ignored. + + If the optional ``match_fn`` parameter is specified, the match + criteria parameters are ignored and each ``HostProfile`` is + tested in turn by calling ``match_fn``. If the matching function + returns ``True`` the ``HostProfile`` will be included in the + results. + + If no ``HostProfile`` matches the specified criteria the empty + list is returned. + + Host profiles will be automatically loaded from disk if they are + not already in memory. + + :param selection: A ``Selection`` object specifying the match + criteria for the operation. + :param match_fn: An optional match function to test profiles. + :returns: a list of ``HostProfile`` objects. + :rtype: list + """ + # Use null search criteria if unspecified + selection = selection if selection else Selection() + + selection.check_valid_selection(host=True) + + if not host_profiles_loaded(): + load_host_profiles() + + matches = [] + + _log_debug_profile("Finding host profiles for %s" % repr(selection)) + for hp in _host_profiles: + if match_fn(selection, hp): + matches.append(hp) + _log_debug_profile("Found %d host profiles" % len(matches)) + matches.sort(key=lambda h: h.host_name) + + return matches + + +def get_host_profile_by_id(machine_id, label=""): + """Find a HostProfile by its machine_id. + + Return the HostProfile object corresponding to ``machine_id``, + or ``None`` if it is not found. + + :rtype: HostProfile + :returns: An HostProfile matching machine_id or None if no match + was found. + """ + global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id + if not host_profiles_loaded(): + load_host_profiles() + if machine_id in _host_profiles_by_id: + if label in _host_profiles_by_id[machine_id]: + return _host_profiles_by_id[machine_id][label] + return None + + +def match_host_profile(entry): + """Attempt to match a BootEntry to a corresponding HostProfile. + + Attempt to find a loaded ``HostProfile`` object with the a + ``machine_id`` that matches the supplied ``BootEntry``. + Checking terminates on the first matching ``HostProfile``. + + :param entry: A ``BootEntry`` object with no attached + ``HostProfile``. + :returns: The corresponding ``HostProfile`` for the supplied + ``BootEntry`` or ``None`` if no match is found. + :rtype: ``BootEntry`` or ``NoneType``. + """ + global _host_profiles, _host_profiles_loaded + + if not host_profiles_loaded(): + load_host_profiles() + + _log_debug("Attempting to match profile for BootEntry(title='%s', " + "version='%s') with machine_id='%s'" % + (entry.title, entry.version, entry.machine_id)) + + # Attempt to match by uname pattern + for hp in _host_profiles: + if hp.machine_id == entry.machine_id: + _log_debug("Matched BootEntry(version='%s', boot_id='%s') " + "to HostProfile(name='%s', machine_id='%s')" % + (entry.version, entry.disp_boot_id, hp.host_name, + hp.machine_id)) + return hp + + return None + + +class HostProfile(BoomProfile): + """ Class HostProfile implements Boom host system profiles. + + Objects of type HostProfile define a host identiry, and optional + fields or ``BootParams`` modifications to be applied to the + specified host. + + Host profiles may modify any non-identity ``OsProfile`` key, + either adding to or replacing the value defined by an embedded + ``OsProfile`` instance. + """ + _profile_data = None + _unwritten = False + _comments = None + + _profile_keys = HOST_PROFILE_KEYS + _required_keys = HOST_REQUIRED_KEYS + _identity_key = BOOM_HOST_ID + + _osp = None + + def _key_data(self, key): + if key in self._profile_data: + return self._profile_data[key] + if key in self.osp._profile_data: + return self.osp._profile_data[key] + return None + + def _have_key(self, key): + """Test for presence of a Host or Os profile key. + """ + return key in self._profile_data or key in self.osp._profile_data + + def __str__(self): + """Format this HostProfile as a human readable string. + + Profile attributes are printed as "Name: value, " pairs, + with like attributes grouped together onto lines. + + :returns: A human readable string representation of this + HostProfile. + + :rtype: string + """ + # FIXME HostProfile breaks + breaks = [ + BOOM_HOST_ID, BOOM_HOST_NAME, BOOM_OS_ID, BOOM_ENTRY_MACHINE_ID, + BOOM_HOST_LABEL, BOOM_OS_VERSION, BOOM_OS_UNAME_PATTERN, + BOOM_HOST_DEL_OPTS, BOOM_OS_INITRAMFS_PATTERN, + BOOM_OS_ROOT_OPTS_LVM2, BOOM_OS_ROOT_OPTS_BTRFS, BOOM_OS_OPTIONS + ] + + fields = [f for f in HOST_PROFILE_KEYS if self._have_key(f)] + hp_str = "" + tail = "" + for f in fields: + hp_str += '%s: "%s"' % (HOST_KEY_NAMES[f], self._key_data(f)) + tail = ",\n" if f in breaks else ", " + hp_str += tail + hp_str = hp_str.rstrip(tail) + return hp_str + + def __repr__(self): + """Format this HostProfile as a machine readable string. + + Return a machine-readable representation of this ``HostProfile`` + object. The string is formatted as a call to the ``HostProfile`` + constructor, with values passed as a dictionary to the + ``profile_data`` keyword argument. + + :returns: a string representation of this ``HostProfile``. + :rtype: string + """ + hp_str = "HostProfile(profile_data={" + fields = [f for f in HOST_PROFILE_KEYS if self._have_key(f)] + for f in fields: + hp_str += '%s:"%s", ' % (f, self._key_data(f)) + hp_str = hp_str.rstrip(", ") + return hp_str + "})" + + def __setitem__(self, key, value): + """Set the specified ``HostProfile`` key to the given value. + + :param key: the ``HostProfile`` key to be set. + :param value: the value to set for the specified key. + """ + + # FIXME: duplicated from OsProfile.__setitem__ -> factor + # osprofile.check_format_key_value(key, value) + # and include isstr() key name validation etc. + + # Map hp key names to a list of format keys which must not + # appear in that key's value: e.g. %{kernel} in the kernel + # pattern profile key. + bad_key_map = { + BOOM_OS_KERNEL_PATTERN: [FMT_KERNEL], + BOOM_OS_INITRAMFS_PATTERN: [FMT_INITRAMFS], + BOOM_OS_ROOT_OPTS_LVM2: [FMT_ROOT_OPTS], + BOOM_OS_ROOT_OPTS_BTRFS: [FMT_ROOT_OPTS], + } + + def _check_format_key_value(key, value, bad_keys): + for bad_key in bad_keys: + if bad_key in value: + raise ValueError("HostProfile.%s cannot contain %s" + % (key, key_from_key_name(bad_key))) + + if not isinstance(key, str): + raise TypeError("HostProfile key must be a string.") + + if key not in HOST_PROFILE_KEYS: + raise ValueError("Invalid HostProfile key: %s" % key) + + if key in bad_key_map: + _check_format_key_value(key, value, bad_key_map[key]) + + self._profile_data[key] = value + + def _generate_id(self): + """Generate a new host identifier. + + Generate a new sha1 profile identifier for this profile, + using the name, machine_id, and os_id and store it in + _profile_data. + + :returns: None + """ + hashdata = (self.machine_id + self.label) + + digest = sha1(hashdata.encode('utf-8')).hexdigest() + self._profile_data[BOOM_HOST_ID] = digest + + def __set_os_profile(self): + """Set this ``HostProfile``'s ``osp`` member to the + corresponding profile for the set ``os_id``. + """ + os_id = self._profile_data[BOOM_OS_ID] + osps = find_profiles(Selection(os_id=os_id)) + if not osps: + raise ValueError("OsProfile not found: %s" % os_id) + if len(osps) > 1: + raise ValueError("OsProfile identifier '%s' is ambiguous" % os_id) + + self.osp = osps[0] + + def _append_profile(self): + """Append a HostProfile to the global profile list + """ + global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id + if _host_exists(self.host_id): + raise ValueError("Profile already exists (host_id=%s)" % + self.disp_host_id) + + _host_profiles.append(self) + machine_id = self.machine_id + if machine_id not in _host_profiles_by_id: + _host_profiles_by_id[machine_id] = {} + _host_profiles_by_id[machine_id][self.label] = self + _host_profiles_by_host_id[self.host_id] = self + + def _from_data(self, host_data, dirty=True): + """Initialise a ``HostProfile`` from in-memory data. + + Initialise a new ``HostProfile`` object using the profile + data in the `host_data` dictionary. + + This method should not be called directly: to build a new + `Hostprofile`` object from in-memory data, use the class + initialiser with the ``host_data`` argument. + + :returns: None + """ + err_str = "Invalid profile data (missing %s)" + + for key in HOST_REQUIRED_KEYS: + if key == BOOM_HOST_ID: + continue + if key not in host_data: + raise ValueError(err_str % key) + + self._profile_data = dict(host_data) + + if BOOM_HOST_ID not in self._profile_data: + self._generate_id() + + self.__set_os_profile() + + if dirty: + self._dirty() + + self._append_profile() + + def __init__(self, machine_id=None, host_name=None, label=None, os_id=None, + kernel_pattern=None, initramfs_pattern=None, + root_opts_lvm2=None, root_opts_btrfs=None, + add_opts="", del_opts="", + options=None, profile_file=None, profile_data=None): + """Initialise a new ``HostProfile`` object. + + If neither ``profile_file`` nor ``profile_data`` is given, + all of ``machine_id``, ``name``, and ``os_id`` must be given. + + These values form the host profile identity and are used to + generate the profile unique identifier. + + :param host_name: The hostname of this system + :param os_id: An OS identifier specifying the ``OsProfile`` + to use with this host profile. + :param profile_data: An optional dictionary mapping from + ``BOOM_*`` keys to profile values. + :param profile_file: An optional path to a file from which + profile data should be loaded. The file + should be in Boom host profile format, + with ``BOOM_*`` key=value pairs. + :returns: A new ``HostProfile`` object. + :rtype: class HostProfile + """ + global _host_profiles + self._profile_data = {} + + # Initialise BoomProfile base class + super(HostProfile, self).__init__(HOST_PROFILE_KEYS, + HOST_REQUIRED_KEYS, BOOM_HOST_ID) + + if profile_data and profile_file: + raise ValueError("Only one of 'profile_data' or 'profile_file' " + "may be specified.") + + if profile_data: + self._from_data(profile_data) + return + if profile_file: + self._from_file(profile_file) + return + + self._dirty() + + required_args = [machine_id, host_name, os_id] + if any([not val for val in required_args]): + raise ValueError("Invalid host profile arguments: machine_id, " + "host_name, and os_id are mandatory.") + + osps = find_profiles(Selection(os_id=os_id)) + if not osps: + raise ValueError("No matching profile found for os_id=%s" % os_id) + if len(osps) > 1: + raise ValueError("OsProfile ID is ambiguous: %s" % os_id) + os_id = osps[0].os_id + + self._profile_data[BOOM_ENTRY_MACHINE_ID] = machine_id + self._profile_data[BOOM_HOST_NAME] = host_name + self._profile_data[BOOM_OS_ID] = os_id + self._profile_data[BOOM_HOST_LABEL] = label + + # Only set keys that have a value in the host profile data dict + if kernel_pattern: + self._profile_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern + if initramfs_pattern: + self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern + if root_opts_lvm2: + self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 + if root_opts_btrfs: + self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs + if add_opts: + self._profile_data[BOOM_HOST_ADD_OPTS] = add_opts + if del_opts: + self._profile_data[BOOM_HOST_DEL_OPTS] = del_opts + if options: + self._profile_data[BOOM_OS_OPTIONS] = options + + self.__set_os_profile() + + self._generate_id() + _host_profiles.append(self) + + # We use properties for the HostProfile attributes: this is to + # allow the values to be stored in a dictionary. Although + # properties are quite verbose this reduces the code volume + # and complexity needed to marshal and unmarshal the various + # file formats used, as well as conversion to and from string + # representations of HostProfile objects. + + # Keys obtained from os-release data form the profile's identity: + # the corresponding attributes are read-only. + + # HostProfile properties: + # + # Profile identity properties (ro): + # host_id + # disp_os_id + # + # Profile identity properties (rw): + # host_name + # machine_id + # os_id + # + # Properties mapped to OsProfile (ro) + # os_name + # os_short_name + # os_version + # os_version_id + # uname_pattern + # + # Properties overridden or mapped to OsProfile (rw) + # kernel_pattern + # initramfs_pattern + # root_opts_lvm2 + # root_opts_btrfs + # options + # + # HostProfile specific properties (rw) + # label + # add_opts + # del_opts + + @property + def disp_os_id(self): + """The display os_id of this profile. + + Return the shortest prefix of this OsProfile's os_id that + is unique within the current set of loaded profiles. + + :getter: return this OsProfile's os_id. + :type: str + """ + return self.osp.disp_os_id + + @property + def host_id(self): + if BOOM_HOST_ID not in self._profile_data: + self._generate_id() + return self._profile_data[BOOM_HOST_ID] + + @property + def disp_host_id(self): + """The display host_id of this profile + + Return the shortest prefix of this HostProfile's os_id that + is unique within the current set of loaded profiles. + + :getter: return this HostProfile's display host_id. + :type: str + """ + return self.host_id[:min_host_id_width()] + + @property + def disp_machine_id(self): + """The machine_id of this host profile. + Return the shortest prefix of this HostProfile's os_id that + is unique within the current set of loaded profiles. + + :getter: return this HostProfile's display host_id. + :type: str + """ + return self.machine_id[:min_machine_id_width()] + + @property + def machine_id(self): + """The machine_id of this host profile. + Return the shortest prefix of this HostProfile's os_id that + is unique within the current set of loaded profiles. + + :getter: return this ``HostProfile``'s display host_id. + :setter: change this ``HostProfile``'s ``machine_id``. This + will change the ``host_id``. + :type: str + """ + return self._profile_data[BOOM_ENTRY_MACHINE_ID] + + @machine_id.setter + def machine_id(self, value): + if value == self._profile_data[BOOM_ENTRY_MACHINE_ID]: + return + self._profile_data[BOOM_ENTRY_MACHINE_ID] = value + self._dirty() + self._generate_id() + + @property + def os_id(self): + """The ``os_id`` of this profile. + + :getter: returns the ``os_id`` as a string. + :type: string + """ + return self.osp.os_id + + @os_id.setter + def os_id(self, value): + if value == self._profile_data[BOOM_OS_ID]: + return + self._profile_data[BOOM_OS_ID] = value + self.__set_os_profile() + self._dirty() + self._generate_id() + + @property + def osp(self): + """The ``OsProfile`` used by this ``HostProfile``. + + :getter: returns the ``OsProfile`` object used by this + ``HostProfile``. + :setter: stores a new ``OsProfile`` for use by this + ``HostProfile`` and updates the stored ``os_id`` + value in the host profile. + """ + return self._osp + + @osp.setter + def osp(self, osp): + if self._osp and osp.os_id == self._osp.os_id: + return + self._osp = osp + self._profile_data[BOOM_OS_ID] = osp.os_id + self._dirty() + self._generate_id() + + @property + def host_name(self): + """The ``host_name`` of this profile. + + Normally set to the hostname of the system corresponding to + this ``HostProfile``. + + :getter: returns the ``host_name`` as a string. + :type: string + """ + return self._profile_data[BOOM_HOST_NAME] + + @host_name.setter + def host_name(self, value): + if value == self._profile_data[BOOM_HOST_NAME]: + return + self._profile_data[BOOM_HOST_NAME] = value + self._dirty() + self._generate_id() + + @property + def short_name(self): + """The ``short_name`` of this profile. + + If ``HostProfile.host_name`` appears to contain a DNS-style name, + return only the host portion. + + :getter: returns the ``short_name`` as a string. + :type: string + """ + host_name = self._profile_data[BOOM_HOST_NAME] + return host_name.split(".")[0] if "." in host_name else host_name + + # + # Properties mapped to OsProfile + # + + @property + def os_name(self): + """The ``os_name`` of this profile. + + :getter: returns the ``os_name`` as a string. + :type: string + """ + return self.osp.os_name + + @property + def os_short_name(self): + """The ``os_short_name`` of this profile. + + :getter: returns the ``os_short_name`` as a string. + :type: string + """ + return self.osp.os_short_name + + @property + def os_version(self): + """The ``os_version`` of this profile. + + :getter: returns the ``os_version`` as a string. + :type: string + """ + return self.osp.os_version + + @property + def os_version_id(self): + """The ``os_version_id`` of this profile. + + :getter: returns the ``os_version_id`` as a string. + :type: string + """ + return self.osp.os_version_id + + @property + def uname_pattern(self): + """The current ``uname_pattern`` setting of this profile. + + :getter: returns the ``uname_pattern`` as a string. + :setter: stores a new ``uname_pattern`` setting. + :type: string + """ + if BOOM_OS_UNAME_PATTERN in self._profile_data: + return self._profile_data[BOOM_OS_UNAME_PATTERN] + return self.osp.uname_pattern + + # + # Properties overridden or mapped to OsProfile + # + + @property + def kernel_pattern(self): + """The current ``kernel_pattern`` setting of this profile. + + :getter: returns the ``kernel_pattern`` as a string. + :setter: stores a new ``kernel_pattern`` setting. + :type: string + """ + if BOOM_OS_KERNEL_PATTERN in self._profile_data: + return self._profile_data[BOOM_OS_KERNEL_PATTERN] + return self.osp.kernel_pattern + + @kernel_pattern.setter + def kernel_pattern(self, value): + kernel_key = key_from_key_name(FMT_KERNEL) + if kernel_key in value: + raise ValueError("HostProfile.kernel cannot contain %s" % + kernel_key) + self._profile_data[BOOM_OS_KERNEL_PATTERN] = value + self._dirty() + + @property + def initramfs_pattern(self): + """The current ``initramfs_pattern`` setting of this profile. + + :getter: returns the ``initramfs_pattern`` as a string. + :setter: store a new ``initramfs_pattern`` setting. + :type: string + """ + if BOOM_OS_INITRAMFS_PATTERN in self._profile_data: + return self._profile_data[BOOM_OS_INITRAMFS_PATTERN] + return self.osp.initramfs_pattern + + @initramfs_pattern.setter + def initramfs_pattern(self, value): + initramfs_key = key_from_key_name(FMT_INITRAMFS) + if initramfs_key in value: + raise ValueError("HostProfile.initramfs cannot contain %s" % + initramfs_key) + self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = value + self._dirty() + + @property + def root_opts_lvm2(self): + """The current LVM2 root options setting of this profile. + + :getter: returns the ``root_opts_lvm2`` value as a string. + :setter: store a new ``root_opts_lvm2`` value. + :type: string + """ + if BOOM_OS_ROOT_OPTS_LVM2 in self._profile_data: + return self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] + + return self.osp.root_opts_lvm2 + + @root_opts_lvm2.setter + def root_opts_lvm2(self, value): + root_opts_key = key_from_key_name(FMT_ROOT_OPTS) + if root_opts_key in value: + raise ValueError("HostProfile.root_opts_lvm2 cannot contain ""%s" % + root_opts_key) + self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = value + self._dirty() + + @property + def root_opts_btrfs(self): + """The current BTRFS root options setting of this profile. + + :getter: returns the ``root_opts_btrfs`` value as a string. + :setter: store a new ``root_opts_btrfs`` value. + :type: string + """ + if BOOM_OS_ROOT_OPTS_BTRFS in self._profile_data: + return self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] + return self.osp.root_opts_btrfs + + @root_opts_btrfs.setter + def root_opts_btrfs(self, value): + root_opts_key = key_from_key_name(FMT_ROOT_OPTS) + if root_opts_key in value: + raise ValueError("HostProfile.root_opts_btrfs cannot contain %s" % + root_opts_key) + self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = value + self._dirty() + + @property + def options(self): + """The current kernel command line options setting for this + profile. + + :getter: returns the ``options`` value as a string. + :setter: store a new ``options`` value. + :type: string + """ + if BOOM_OS_OPTIONS in self._profile_data: + return self._profile_data[BOOM_OS_OPTIONS] + return self.osp.options + + @options.setter + def options(self, value): + self._profile_data[BOOM_OS_OPTIONS] = value + self._dirty() + + @property + def title(self): + """The current title template for this profile. + + :getter: returns the ``title`` value as a string. + :setter: store a new ``title`` value. + :type: string + """ + if BOOM_OS_TITLE not in self._profile_data: + return None + return self._profile_data[BOOM_OS_TITLE] + + @title.setter + def title(self, value): + if not value: + # It is valid to set an empty title in a HostProfile as long + # as the OsProfile defines one. + if not self.osp or not self.osp.title: + raise ValueError("Entry title cannot be empty") + self._profile_data[BOOM_OS_TITLE] = value + self._dirty() + + @property + def optional_keys(self): + if not self.osp or not self.osp.optional_keys: + return "" + return self.osp.optional_keys + + # + # HostProfile specific properties + # + + @property + def add_opts(self): + if BOOM_HOST_ADD_OPTS in self._profile_data: + return self._profile_data[BOOM_HOST_ADD_OPTS] + return "" + + @add_opts.setter + def add_opts(self, opts): + self._profile_data[BOOM_HOST_ADD_OPTS] = opts + self._dirty() + + @property + def del_opts(self): + if BOOM_HOST_DEL_OPTS in self._profile_data: + return self._profile_data[BOOM_HOST_DEL_OPTS] + return "" + + @del_opts.setter + def del_opts(self, opts): + self._profile_data[BOOM_HOST_DEL_OPTS] = opts + self._dirty() + + @property + def label(self): + if BOOM_HOST_LABEL in self._profile_data: + return self._profile_data[BOOM_HOST_LABEL] + return "" + + @label.setter + def label(self, value): + valid_chars = string.ascii_letters + string.digits + "_- " + + if BOOM_HOST_LABEL in self._profile_data: + if self._profile_data[BOOM_HOST_LABEL] == value: + return + + for c in value: + if c not in valid_chars: + raise ValueError("Invalid host label character: '%s'" % c) + + self._profile_data[BOOM_HOST_LABEL] = value + self._dirty() + self._generate_id() + + def _profile_path(self): + """Return the path to this profile's on-disk data. + + Return the full path to this HostProfile in the Boom profiles + directory (or the location to which it will be written, if + it has not yet been written). + + :rtype: str + :returns: The absolute path for this HostProfile's file + """ + if self.label: + label = self.label + if " " in label: + label = label.replace(" ", "_") + names = (self.short_name, label) + name_fmt = "%s-%s" + else: + names = (self.short_name) + name_fmt = "%s" + profile_name = name_fmt % names + profile_id = (self.host_id, profile_name) + profile_path_name = BOOM_HOST_PROFILE_FORMAT % profile_id + return path_join(boom_host_profiles_path(), profile_path_name) + + def write_profile(self, force=False): + """Write out profile data to disk. + + Write out this ``HostProfile``'s data to a file in Boom + format to the paths specified by the current configuration. + + Currently the ``machine_id`` and ``name`` keys are used to + construct the file name. + + If the value of ``force`` is ``False`` and the ``HostProfile`` + is not currently marked as dirty (either new, or modified + since the last load operation) the write will be skipped. + + :param force: Force this profile to be written to disk even + if the entry is unmodified. + :raises: ``OsError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. + """ + path = boom_host_profiles_path() + mode = BOOM_HOST_PROFILE_MODE + self._write_profile(self.host_id, path, mode, force=force) + + def delete_profile(self): + """Delete on-disk data for this profile. + + Remove the on-disk profile corresponding to this + ``HostProfile`` object. This will permanently erase the + current file (although the current data may be re-written at + any time by calling ``write_profile()`` before the object is + disposed of). + + :rtype: ``NoneType`` + :raises: ``OsError`` if an error occurs removing the file or + ``ValueError`` if the profile does not exist. + """ + global _host_profiles, _host_profiles_by_id, _host_profiles_by_host_id + self._delete_profile(self.host_id) + + machine_id = self.machine_id + host_id = self.host_id + if _host_profiles and self in _host_profiles: + _host_profiles.remove(self) + if _host_profiles_by_id and machine_id in _host_profiles_by_id: + _host_profiles_by_id.pop(machine_id) + if _host_profiles_by_host_id and host_id in _host_profiles_by_host_id: + _host_profiles_by_host_id.pop(host_id) + + +__all__ = [ + # Host profiles + 'HostProfile', + 'drop_host_profiles', 'load_host_profiles', 'write_host_profiles', + 'host_profiles_loaded', 'find_host_profiles', 'select_host_profile', + 'get_host_profile_by_id', 'match_host_profile', 'select_host_profile', + + # Host profile keys + 'BOOM_HOST_ID', 'BOOM_HOST_NAME', + 'BOOM_HOST_ADD_OPTS', 'BOOM_HOST_DEL_OPTS', 'BOOM_HOST_LABEL', + 'HOST_PROFILE_KEYS', 'HOST_REQUIRED_KEYS', 'HOST_OPTIONAL_KEYS', + + # Path configuration + 'boom_host_profiles_path', +] + +# vim: set et ts=4 sw=4 : diff --git a/boom/legacy.py b/boom/legacy.py new file mode 100644 index 0000000..7e67f2a --- /dev/null +++ b/boom/legacy.py @@ -0,0 +1,387 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# legacy.py - Boom legacy bootloader manager +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.legacy`` module defines classes and constants for working +with legacy bootloader configuration formats. + +Legacy formats are read-only and can only be updated by synchronising +the entire current set of boot entries to the legacy format, or removing +all entries from the legacy configuration file. +""" +from __future__ import print_function + +from boom import * +from boom.bootloader import * + +from subprocess import Popen, PIPE +from os.path import dirname, exists, isabs, join as path_join +from os import chmod, dup, fdatasync, fdopen, rename, unlink +from tempfile import mkstemp +import logging +import re + +#: Format strings use to construct begin/end markers +BOOM_LEGACY_BEGIN_FMT = "#--- BOOM_%s_BEGIN ---" +BOOM_LEGACY_END_FMT = "#--- BOOM_%s_END ---" + +#: Constants for legacy boot loaders supported by boom +BOOM_LOADER_GRUB1 = "grub1" +BOOM_GRUB1_CFG_PATH = "grub/grub.conf" + +# Module logging configuration +_log = logging.getLogger(__name__) + +_log_debug = _log.debug +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +#: Grub1 root device cache +__grub1_device = None + + +def _get_grub1_device(force=False): + """Determine the current grub1 root device and return it as a + string. This function will attempt to use a cached value + from a previous call (to avoid shelling out to Grub a + second time), unless the ``force`` argument is ``True``. + + If no usable Grub1 environment is detected the function + raises the ``BoomLegacyFormatError`` exception. + + :param force: force the cache to be updated. + """ + # Grub1 device cache + global __grub1_device + + if __grub1_device and not force: + return __grub1_device + + # The grub1 binary + grub_cmd = "grub" + # The command to issue to discover the /boot device + find_cmd = "find /%s\n" % _loader_map[BOOM_LOADER_GRUB1][2] + # Regular expression matching a valid grub device string + find_rgx = r" \(hd\d+,\d+\)" + + try: + _log_debug("Calling grub1 shell with '%s'" % find_cmd) + p = Popen(grub_cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE) + out = p.communicate(input=bytes(find_cmd.encode('utf8'))) + except OSError: + raise BoomLegacyFormatError("Could not execute grub1 shell.") + + for line in out[0].decode('utf8').splitlines(): + if re.match(find_rgx, line): + __grub1_device = line.lstrip().rstrip() + _log_debug("Set grub1 device to '%s'" % __grub1_device) + return __grub1_device + + +class BoomLegacyFormatError(BoomError): + """Boom exception indicating an invalid or corrupt boom legacy + boot configuration, for example, missing begin or end marks + in the legacy bootloader configuration file, or an unknown + or invalid legacy bootloader type. + """ + pass + + +def find_legacy_loader(loader, cfg_path): + """Look up a legacy loader format in the table of available formats + and return a tuple containing the format name, decorator class + and the configuration file path. If ``cfg_path`` is set it will + override the default file location for the format. + + :param loader: the legacy bootloader format to operate on + :param cfg_path: the path to the legacy bootloader configuration + file. If ``cfg_path`` is None the default path + for the specified loader will be used. + :raises BoomLegacyFormatError: if the legacy configuration file + contains invalid boom entries or + the specified legacy format is + unknown or invalid. + :returns: (name, decorator, path) tuple + """ + if not loader: + raise BoomLegacyFormatError("Invalid legacy bootloader format: %s" % + loader) + if loader not in _loader_map: + raise BoomLegacyFormatError("Unknown legacy bootloader format: %s" % + loader) + + (name, decorator, path) = _loader_map[loader] + path = cfg_path or path + return (name, decorator, path) + + +def write_legacy_loader(selection=None, loader=BOOM_LOADER_GRUB1, + cfg_path=None): + """Synchronise boom's configuration with the specified legacy boot + loader. + + For boot loaders that support only a single configuration file + with multiple boot entries, boom will generate a block of + configuration statements bounded by "BOOM_BEGIN"/"BOOM_END" on + a line by themselves and prefixed with the comment character + for that configuration format (e.g. '#'). + + :param selection: A ``Selection`` object specifying the match + criteria for the operation. + :param loader: the legacy boot loader type to write + :param cfg_path: the path to the legacy bootloader configuration + file. If ``cfg_path`` is None the default path + for the specified loader will be used. + """ + (name, decorator, path) = find_legacy_loader(loader, cfg_path) + + if not isabs(path): + path = path_join(get_boot_path(), path) + + cfg_dir = dirname(path) + + if not exists(cfg_dir): + _log_error("Cannot write %s configuration: '%s' does not exist'" % + (name, cfg_dir)) + return + + begin_tag = BOOM_LEGACY_BEGIN_FMT % name + end_tag = BOOM_LEGACY_END_FMT % name + + (tmp_fd, tmp_path) = mkstemp(prefix="boom", dir=cfg_dir) + + try: + with fdopen(tmp_fd, "w") as tmp_f: + # Our original file descriptor will be closed on exit from + # the fdopen with statement: save a copy so that we can call + # fdatasync once at the end of writing rather than on each + # loop iteration. + tmp_fd = dup(tmp_fd) + with open(path, "r") as cfg_f: + for line in cfg_f: + tmp_f.write(line) + tmp_f.write(begin_tag + "\n") + bes = find_entries(selection=selection) + for be in bes: + dbe = decorator(be) + tmp_f.write(str(dbe) + "\n") + tmp_f.write(end_tag + "\n") + except BoomLegacyFormatError as e: + _log_error("Error formatting %s configuration: %s" % (name, e)) + try: + unlink(tmp_path) + except OSError: + _log_error("Error unlinking temporary file '%s'" % tmp_path) + return + + try: + fdatasync(tmp_fd) + rename(tmp_path, path) + chmod(path, BOOT_ENTRY_MODE) + except Exception as e: + _log_error("Error writing legacy configuration file %s: %s" % + (path, e)) + try: + unlink(tmp_path) + except Exception: + pass + raise e + + +def clear_legacy_loader(loader=BOOM_LOADER_GRUB1, cfg_path=None): + """Delete all boom managed entries from the specified legacy boot + loader configuration file. + + If the specified ``loader`` is unknown or invalid the + BoomLegacyFormatError exception is raised. + + This erases any lines from the file beginning with a valid + 'BOOM_*_BEGIN' line and ending with a valid 'BOOM_*_END' line. + + If both marker lines are absent this function has no effect + and no error is raised: the file does not contain any existing + boom legacy configuration entries. + + If one of the two markers is missing this function will not + modify the file and a BoomLegacyFormatError exception is + raised internally and recorded in the log. Legacy configuration + cannot be written in this case as the file is in an inconsistent + state that boom cannot automatically correct. + + If the configuration path is not absolute it is assumed to be + relative to the configured system '/boot' directory as returned + by ``boom.get_boot_path()``. + + :param loader: the legacy bootloader format to operate on + :param cfg_path: the path to the legacy bootloader configuration + file. If ``cfg_path`` is None the default path + for the specified loader will be used. + :returns: None + """ + def _legacy_format_error(err, fmt_data): + """Helper function to clean up the temporary file and raise the + corresponding BoomLegacyFormatError exception. + """ + if type(fmt_data[0]) == int: + fmt_data = ("line %d" % fmt_data[0], fmt_data[1]) + + try: + unlink(tmp_path) + except OSError as e: + _log_error("Could not unlink '%s': %s" % (tmp_path, e)) + raise BoomLegacyFormatError(err % fmt_data) + + (name, decorator, path) = find_legacy_loader(loader, cfg_path) + + if not isabs(path): + path = path_join(get_boot_path(), path) + + cfg_dir = dirname(path) + + if not exists(cfg_dir): + _log_error("Cannot clear %s configuration: '%s' does not exist'" % + (name, cfg_dir)) + return + + begin_tag = BOOM_LEGACY_BEGIN_FMT % name + end_tag = BOOM_LEGACY_END_FMT % name + + # Pre-set configuration error messages. Use a string format for + # the line number so that 'EOF' can be passed for end-of-file. + err_dupe_begin = ("Duplicate Boom begin tag at %s in legacy " + + "configuration file '%s'") + err_dupe_end = ("Duplicate Boom end tag at %s in legacy " + + "configuration file '%s'") + err_no_begin = ("Missing Boom begin tag at %s in legacy " + + "configuration file '%s'") + err_no_end = ("Missing Boom end tag at %s in legacy " + + "configuration file '%s'") + + line_nr = 1 + found_boom = False + in_boom_cfg = False + + (tmp_fd, tmp_path) = mkstemp(prefix="boom", dir=cfg_dir) + + try: + with fdopen(tmp_fd, "w") as tmp_f: + # Our original file descriptor will be closed on exit from + # the fdopen with statement: save a copy so that we can call + # fdatasync once at the end of writing rather than on each + # loop iteration. + tmp_fd = dup(tmp_fd) + with open(path, "r") as cfg_f: + for line in cfg_f: + if begin_tag in line: + if in_boom_cfg or found_boom: + _legacy_format_error(err_dupe_begin, + (line_nr, path)) + in_boom_cfg = True + continue + if end_tag in line: + if found_boom: + _legacy_format_error(err_dupe_end, (line_nr, path)) + if not in_boom_cfg: + _legacy_format_error(err_no_begin, (line_nr, path)) + in_boom_cfg = False + found_boom = True + continue + if not in_boom_cfg: + tmp_f.write(line) + line_nr += 1 + except BoomLegacyFormatError as e: + _log_error("Error parsing %s configuration: %s" % (name, e)) + found_boom = False + + if in_boom_cfg and not found_boom: + _legacy_format_error(err_no_end, ("EOF", path)) + + if not found_boom: + # No boom entries: nothing to do. + try: + unlink(tmp_path) + except OSError as e: + _log_error("Could not unlink '%s': %s" % (tmp_path, e)) + return + + try: + fdatasync(tmp_fd) + rename(tmp_path, path) + chmod(path, BOOT_ENTRY_MODE) + except Exception as e: + _log_error("Error writing legacy configuration file %s: %s" % + (path, e)) + try: + unlink(tmp_path) + except Exception: + pass + raise e + + +class Grub1BootEntry(object): + """Class transforming a Boom ``BootEntry`` into legacy Grub1 + boot entry notation. + + The Grub1BootEntry decorates the ``__str__`` method of the + BootEntry superclass by returning data formatted in Grub1 + configuration notation rather than BLS. + + Currently this uses a simple fixed format string for the + Grub1 syntax. If additional legacy formats are required it + may be better to extend the generic BootEntry.__str() + formatter to be able to accept maps of alternate format + keys. This is somewhat complicated by aspects like the + grub1 boot device key (since this has no representation or + equivalent in BLS notation). + """ + + be = None + + def __init__(self, boot_entry): + self.be = boot_entry + + def __str__(self): + grub1_tab = " " * 8 + grub1_fmt = ("title %s\n" + grub1_tab + "root %s\n" + grub1_tab + + "kernel %s %s\n" + grub1_tab + "initrd %s") + + return grub1_fmt % (self.be.title, _get_grub1_device(), + self.be.linux, self.be.options, self.be.initrd) + + +#: Map of legacy boot loader decorator classes and defaults. +#: Each entry in _loader_map is a three tuple containing the +#: format's name, decorator class and default configuration path. +_loader_map = { + BOOM_LOADER_GRUB1: ("Grub1", Grub1BootEntry, BOOM_GRUB1_CFG_PATH) +} + +__all__ = [ + # Exception class for errors in legacy format handling + 'BoomLegacyFormatError', + + # Write legacy boot configuration + 'write_legacy_loader', + 'clear_legacy_loader', + + # Lookup legacy boot loader formats + 'find_legacy_loader', + + # Legacy bootloader names + 'BOOM_LOADER_GRUB1', + + # Legacy bootloader decorator classes + 'Grub1BootEntry' +] + +# vim: set et ts=4 sw=4 : diff --git a/boom/osprofile.py b/boom/osprofile.py new file mode 100644 index 0000000..8b68f93 --- /dev/null +++ b/boom/osprofile.py @@ -0,0 +1,1720 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# osprofile.py - Boom OS profiles +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The ``boom.osprofile`` module defines the `OsProfile` class that +represents an operating system profile. An `OsProfile` defines the +identity of a profile and includes template values used to generate boot +loader entries. + +Functions are provided to read and write operating system profiles from +an on-disk store, and to retrieve ``OsProfile`` instances using various +selection criteria. + +The ``OsProfile`` class includes named properties for each profile +attribute ("profile key"). In addition, the class serves as a container +type, allowing attributes to be accessed via dictionary-style indexing. +This simplifies iteration over a profile's key / value pairs and allows +straightforward access to all members in scripts and the Python shell. + +All profile key names are made available as named members of the module: +``BOOM_OS_*``, and the ``OS_PROFILE_KEYS`` list. Human-readable names for +all the profile keys are stored in the ``OS_KEY_NAMES`` dictionary: these +are suitable for display use and are used by default by the +``OsProfile`` string formatting routines. +""" +from __future__ import print_function + +from boom import * +from hashlib import sha1 +from tempfile import mkstemp +from os.path import basename, join as path_join, exists as path_exists +from os import fdopen, rename, chmod, unlink, fdatasync +import logging +import re + +#: Boom profiles directory name. +BOOM_PROFILES = "profiles" + +#: File name format for Boom profiles. +BOOM_OS_PROFILE_FORMAT = "%s-%s%s.profile" + +#: The file mode with which to create Boom profiles. +BOOM_PROFILE_MODE = 0o644 + +# Constants for Boom profile keys + +#: Constant for the Boom OS identifier profile key. +BOOM_OS_ID = "BOOM_OS_ID" +#: Constant for the Boom OS name profile key. +BOOM_OS_NAME = "BOOM_OS_NAME" +#: Constant for the Boom OS short name profile key. +BOOM_OS_SHORT_NAME = "BOOM_OS_SHORT_NAME" +#: Constant for the Boom OS version string profile key. +BOOM_OS_VERSION = "BOOM_OS_VERSION" +#: Constant for the Boom OS version ID string profile key. +BOOM_OS_VERSION_ID = "BOOM_OS_VERSION_ID" +#: Constant for the Boom OS uname pattern profile key. +BOOM_OS_UNAME_PATTERN = "BOOM_OS_UNAME_PATTERN" +#: Constant for the Boom OS kernel pattern profile key. +BOOM_OS_KERNEL_PATTERN = "BOOM_OS_KERNEL_PATTERN" +#: Constant for the Boom OS initramfs pattern profile key. +BOOM_OS_INITRAMFS_PATTERN = "BOOM_OS_INITRAMFS_PATTERN" +#: Constant for the Boom OS LVM2 root options key. +BOOM_OS_ROOT_OPTS_LVM2 = "BOOM_OS_ROOT_OPTS_LVM2" +#: Constant for the Boom OS BTRFS root options key. +BOOM_OS_ROOT_OPTS_BTRFS = "BOOM_OS_ROOT_OPTS_BTRFS" +#: Constant for the Boom OS command line options key. +BOOM_OS_OPTIONS = "BOOM_OS_OPTIONS" +#: Constant for the Boom OS title template key. +BOOM_OS_TITLE = "BOOM_OS_TITLE" +#: Constant for the Boom OS optional keys key. +BOOM_OS_OPTIONAL_KEYS = "BOOM_OS_OPTIONAL_KEYS" + +#: Ordered list of possible profile keys, partitioned into mandatory +#: keys, root option keys, and optional keys (currently the Linux +#: kernel command line). +OS_PROFILE_KEYS = [ + # Keys 0-6 (ID to INITRAMFS_PATTERN) are mandatory. + BOOM_OS_ID, BOOM_OS_NAME, BOOM_OS_SHORT_NAME, BOOM_OS_VERSION, + BOOM_OS_VERSION_ID, + BOOM_OS_KERNEL_PATTERN, BOOM_OS_INITRAMFS_PATTERN, + # At least one of keys 7-8 (ROOT_OPTS) is required. + BOOM_OS_ROOT_OPTS_LVM2, BOOM_OS_ROOT_OPTS_BTRFS, + # The OPTIONS, TITLE and UNAME_PATTERN keys are optional. + BOOM_OS_OPTIONS, BOOM_OS_TITLE, BOOM_OS_OPTIONAL_KEYS, + BOOM_OS_UNAME_PATTERN +] + +#: A map of Boom profile keys to human readable key names suitable +#: for use in formatted output. These key names are used to format +#: a ``OsProfile`` object as a human readable string. +OS_KEY_NAMES = { + BOOM_OS_ID: "OS ID", + BOOM_OS_NAME: "Name", + BOOM_OS_SHORT_NAME: "Short name", + BOOM_OS_VERSION: "Version", + BOOM_OS_VERSION_ID: "Version ID", + BOOM_OS_UNAME_PATTERN: "UTS release pattern", + BOOM_OS_KERNEL_PATTERN: "Kernel pattern", + BOOM_OS_INITRAMFS_PATTERN: "Initramfs pattern", + BOOM_OS_ROOT_OPTS_LVM2: "Root options (LVM2)", + BOOM_OS_ROOT_OPTS_BTRFS: "Root options (BTRFS)", + BOOM_OS_OPTIONS: "Options", + BOOM_OS_TITLE: "Title", + BOOM_OS_OPTIONAL_KEYS: "Optional keys" +} + +#: Boom profile keys that must exist in a valid profile. +OS_REQUIRED_KEYS = OS_PROFILE_KEYS[0:7] + +#: Boom profile keys for different forms of root device specification. +OS_ROOT_KEYS = OS_PROFILE_KEYS[8:9] + +#: Keys with default values +_DEFAULT_KEYS = { + BOOM_OS_UNAME_PATTERN: "", + BOOM_OS_KERNEL_PATTERN: "/vmlinuz-%{version}", + BOOM_OS_INITRAMFS_PATTERN: "/initramfs-%{version}.img", + BOOM_OS_ROOT_OPTS_LVM2: "rd.lvm.lv=%{lvm_root_lv}", + BOOM_OS_ROOT_OPTS_BTRFS: "rootflags=%{btrfs_subvolume}", + BOOM_OS_OPTIONS: "root=%{root_device} ro %{root_opts}", + BOOM_OS_TITLE: "%{os_name} %{os_version_id} (%{version})" +} + +# Module logging configuration +_log = logging.getLogger(__name__) +_log.set_debug_mask(BOOM_DEBUG_PROFILE) + +_log_debug = _log.debug +_log_debug_profile = _log.debug_masked +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +#: Global profile list +_profiles = [] +_profiles_by_id = {} + +#: Whether profiles have been read from disk +_profiles_loaded = False + + +def _profile_exists(os_id): + """Test whether the specified ``os_id`` already exists. + + Used during ``OsProfile`` initialisation to test if the new + ``os_id`` is already known (and to avoid passing through + find_profiles(), which may trigger recursive profile loading). + + :param os_id: the OS identifier to check for + + :returns: ``True`` if the identifier is known or ``False`` + otherwise. + :rtype: bool + """ + global _profiles_by_id + if not _profiles_by_id: + return False + if os_id in _profiles_by_id: + return True + return False + + +def boom_profiles_path(): + """Return the path to the boom profiles directory. + + :returns: The boom profiles path. + :rtype: str + """ + return path_join(get_boom_path(), BOOM_PROFILES) + + +def _is_null_profile(osp): + """Test for the Null Profile. + + Test ``osp`` and return ``True`` if it is the Null Profile. + The Null Profile has an empty identity and defines no profile + keys: it is used in the case that no valid match is found for + an entry loaded from disk (for e.g. an entry that has been + hand-edited and matches no known version or options string, + or an entry for which the original profile has been deleted). + + :param osp: The OsProfile to test + :returns: ``True`` if ``osp`` is the Null Profile or ``False`` + otherwise + :rtype: bool + """ + global _profiles + if osp.os_id == _profiles[0].os_id: + return True + return False + + +def profiles_loaded(): + """Test whether profiles have been loaded from disk. + + :rtype: bool + :returns: ``True`` if profiles are loaded in memory or ``False`` + otherwise + """ + return _profiles_loaded + + +def drop_profiles(): + """Drop all in-memory profiles. + + Clear the list of in-memory profiles and reset the OsProfile + list to the default state. + + :returns: None + """ + global _profiles, _profiles_by_id, _profiles_loaded + nr_profiles = len(_profiles) - 1 + + _profiles = [] + _profiles_by_id = {} + + _null_profile = OsProfile(name="", short_name="", + version="", version_id="") + _profiles.append(_null_profile) + _profiles_by_id[_null_profile.os_id] = _null_profile + _log_info("Dropped %d profiles" % nr_profiles) + _profiles_loaded = False + + +def load_profiles(): + """Load OsProfile data from disk. + + Load the set of profiles found at the path + ``boom.osprofile.boom_profiles_path()`` into the global profile + list. + + This function may be called to explicitly load, or reload the + set of profiles on-disk. Profiles are also loaded implicitly + if an API function or method that requires access to profiles + is invoked (for example, ``boom.bootloader.load_entries()``. + + :returns: None + """ + global _profiles_loaded + drop_profiles() + load_profiles_for_class(OsProfile, "Os", boom_profiles_path(), "profile") + _log_info("Loaded %d profiles" % (len(_profiles) - 1)) + _profiles_loaded = True + + +def write_profiles(force=False): + """Write all OsProfile data to disk. + + Write the current list of profiles to the directory located at + ``boom.osprofile.boom_profiles_path()``. + + :rtype: None + """ + global _profiles + _log_debug("Writing profiles to %s" % boom_profiles_path()) + for osp in _profiles: + if _is_null_profile(osp): + continue + try: + osp.write_profile(force) + except Exception as e: + _log_warn("Failed to write OsProfile(os_id='%s'): %s" % + (osp.disp_os_id, e)) + + +def min_os_id_width(): + """Calculate the minimum unique width for os_id values. + + Calculate the minimum width to ensure uniqueness when displaying + os_id values. + + :returns: the minimum os_id width. + :rtype: int + """ + return min_id_width(7, _profiles, "os_id") + + +def select_profile(s, osp): + """Test the supplied profile against selection criteria. + + Test the supplied ``OsProfile`` against the selection criteria + in ``s`` and return ``True`` if it passes, or ``False`` + otherwise. + + :param s: The selection criteria + :param osp: The ``OsProfile`` to test + :rtype: bool + :returns: True if ``osp`` passes selection or ``False`` + otherwise. + """ + if _is_null_profile(osp): + return False + if s.os_id and not osp.os_id.startswith(s.os_id): + return False + if s.os_name and osp.os_name != s.os_name: + return False + if s.os_short_name and osp.os_short_name != s.os_short_name: + return False + if s.os_version and osp.os_version != s.os_version: + return False + if s.os_version_id and osp.os_version_id != s.os_version_id: + return False + if s.os_uname_pattern and osp.uname_pattern != s.os_uname_pattern: + return False + if s.os_kernel_pattern and osp.kernel_pattern != s.os_kernel_pattern: + return False + if (s.os_initramfs_pattern and + s.os_initramfs_pattern != osp.initramfs_pattern): + return False + if s.os_options and osp.options != s.os_options: + return False + return True + + +def find_profiles(selection=None, match_fn=select_profile): + """Find profiles matching selection criteria. + + Return a list of ``OsProfile`` objects matching the specified + criteria. Matching proceeds as the logical 'and' of all criteria. + Criteria that are unset (``None``) are ignored. + + If the optional ``match_fn`` parameter is specified, the match + criteria parameters are ignored and each ``OsProfile`` is tested + in turn by calling ``match_fn``. If the matching function returns + ``True`` the ``OsProfile`` will be included in the results. + + If no ``OsProfile`` matches the specified criteria the empty list + is returned. + + OS profiles will be automatically loaded from disk if they are + not already in memory. + + :param selection: A ``Selection`` object specifying the match + criteria for the operation. + :param match_fn: An optional match function to test profiles. + :returns: a list of ``OsProfile`` objects. + :rtype: list + """ + global _profiles + + # Use null search criteria if unspecified + selection = selection if selection else Selection() + + selection.check_valid_selection(profile=True) + + if not profiles_loaded(): + load_profiles() + + matches = [] + + _log_debug_profile("Finding profiles for %s" % repr(selection)) + for osp in _profiles: + if match_fn(selection, osp): + matches.append(osp) + _log_debug_profile("Found %d profiles" % len(matches)) + matches.sort(key=lambda o: (o.os_name, o.os_version)) + + return matches + + +def get_os_profile_by_id(os_id): + """Find an OsProfile by its os_id. + + Return the OsProfile object corresponding to ``os_id``, or + ``None`` if it is not found. + + :rtype: OsProfile + :returns: An OsProfile matching os_id or None if no match was + found + """ + if not profiles_loaded(): + load_profiles() + if os_id in _profiles_by_id: + return _profiles_by_id[os_id] + return None + + +def match_os_profile(entry): + """Attempt to match a BootEntry to a corresponding OsProfile. + + Probe all loaded ``OsProfile`` objects with the supplied + ``BootEntry`` in turn, until an ``OsProfile`` reports a match. + Checking terminates on the first matching ``OsProfile``. + + :param entry: A ``BootEntry`` object with no attached + ``OsProfile``. + :returns: The corresponding ``OsProfile`` for the supplied + ``BootEntry`` or ``None`` if no match is found. + :rtype: ``OsProfile`` + """ + global _profiles, _profiles_loaded + + if not _profiles_loaded: + load_profiles() + + # Do not report a boot_id: it will change if an OsProfile is + # matched to the entry. + _log_debug("Attempting to match profile for BootEntry(title='%s', " + "version='%s') with unknown os_id" % + (entry.title, entry.version)) + + # Attempt to match by uname pattern + for osp in sorted(_profiles, key=lambda o: (o.os_name, o.os_version)): + if _is_null_profile(osp): + continue + if osp.match_uname_version(entry.version): + _log_debug("Matched BootEntry(version='%s', boot_id='%s') " + "to OsProfile(name='%s', os_id='%s')" % + (entry.version, entry.disp_boot_id, osp.os_name, + osp.disp_os_id)) + return osp + + # No matching uname pattern: attempt to match options template + for osp in _profiles: + if _is_null_profile(osp): + continue + if osp.match_options(entry): + _log_debug("Matched BootEntry(version='%s', boot_id='%s') " + "to OsProfile(name='%s', os_id='%s')" % + (entry.version, entry.disp_boot_id, osp.os_name, + osp.disp_os_id)) + return osp + + _log_debug_profile("No matching profile found for boot_id=%s" % + entry.boot_id) + + # Assign the Null profile to this BootEntry: we cannot determine a + # valid OsProfile to associate with it, so it cannot be modified or + # displayed correctly by boom. Add it to the list of loaded entries, + # but do not return it as a valid entry in entry selections. + return _profiles[0] + + +def match_os_profile_by_version(version): + """Attempt to match a version string to an OsProfile. + + Attempt to find a profile with a uname pattern that matches + ``version``. The first OsProfile with a match is returned. + + :param version: A uname release version string to match. + :rtype: OsProfile + :returns: An OsProfile matching version or None if not match + was found + """ + global _profiles, _profiles_loaded + + if not _profiles_loaded: + load_profiles() + + for osp in _profiles: + if osp.match_uname_version(version): + return osp + return None + + +def key_from_key_name(key_name): + key_format = "%%{%s}" + return key_format % key_name + + +class BoomProfile(object): + """Class ``BoomProfile`` is the abstract base class for Boom template + profiles. The ``BoomProfile`` class cannot be instantiated by + itself but serves as the base class for both ``OsProfile`` and + ``HostProfile`` instances. + """ + #: Profile data dictionary + _profile_data = None + #: Dirty flag + _unwritten = False + #: Comment descriptors read from on-disk store + _comments = None + + #: Key set for this profile class + _profile_keys = None + #: Mandatory keys for this profile class + _required_keys = None + #: The identity key for this profile class + _identity_key = None + + def __str__(self): + """Format this profile as a human readable string. + + This method must be implemented by concrete profile classes. + + :returns: A human readable string representation of this + ``BoomProfile``. + + :rtype: string + """ + raise NotImplementedError + + def __repr__(self): + """Format this ``BoomProfile`` as a machine readable string. + + This method must be implemented by concrete profile classes. + + :returns: a string representation of this ``BoomProfile``. + :rtype: ``string`` + """ + raise NotImplementedError + + def _from_data(self, profile_data, dirty=True): + """ + This method must be implemented by concrete profile classes. + """ + raise NotImplementedError + + def __len__(self): + """Return the length (key count) of this profile. + + :returns: the profile length as an integer. + :rtype: ``int`` + """ + return len(self._profile_data) + + def __getitem__(self, key): + """Return an item from this profile. + + :returns: the item corresponding to the key requested. + :rtype: the corresponding type of the requested key. + :raises: TypeError if ``key`` is of an invalid type. + KeyError if ``key`` is valid but not present. + """ + if not isinstance(key, str): + raise TypeError("Profile key must be a string.") + + if key in self._profile_data: + return self._profile_data[key] + + raise KeyError("Key %s not in profile." % key) + + def __setitem__(self, key, value): + """Set the specified ``Profile`` key to the given value. + + :param key: the ``Profile`` key to be set. + :param value: the value to set for the specified key. + """ + # Name of the current profile class instance + ptype = self.__class__.__name__ + + # Map key names to a list of format keys which must not + # appear in that key's value: e.g. %{kernel} in the kernel + # pattern profile key. + bad_key_map = { + BOOM_OS_KERNEL_PATTERN: [FMT_KERNEL], + BOOM_OS_INITRAMFS_PATTERN: [FMT_INITRAMFS], + BOOM_OS_ROOT_OPTS_LVM2: [FMT_ROOT_OPTS], + BOOM_OS_ROOT_OPTS_BTRFS: [FMT_ROOT_OPTS], + } + + def _check_format_key_value(key, value, bad_keys): + for bad_key in bad_keys: + if bad_key in value: + bad_fmt = key_from_key_name(bad_key) + raise ValueError("%s.%s cannot contain %s" + % (ptype, key, bad_fmt)) + + if not isinstance(key, str): + raise TypeError("%s key must be a string." % ptype) + + if key not in self._profile_keys: + raise ValueError("Invalid %s key: %s" % (ptype, key)) + + if key in bad_key_map: + _check_format_key_value(key, value, bad_key_map[key]) + + self._profile_data[key] = value + + def keys(self): + """Return the list of keys for this ``BoomProfile``. + + :rtype: list + :returns: A list of ``BoomProfile`` key names + """ + return self._profile_data.keys() + + def values(self): + """Return the list of key values for this ``BoomProfile``. + + :rtype: list + :returns: A list of ``BoomProfile`` key values + """ + return self._profile_data.values() + + def items(self): + """Return the items list for this ``BoomProfile``. + + Return a list of ``(key, value)`` tuples representing the + key items in this ``BoomProfile``. + + :rtype: list + :returns: A list of ``BoomProfile`` key item tuples + """ + return self._profile_data.items() + + def _dirty(self): + """Mark this ``BoomProfile`` as needing to be written to disk. + + A newly created ``BoomProfile`` object is always dirty and + a call to its ``write_profile()`` method will always write + a new profile file. Writes may be avoided for profiles + that are not marked as dirty. + + A clean ``BoomProfile`` is marked as dirty if a new value + is written to any of its writable properties. + + :returns None: + """ + if self._identity_key in self._profile_data: + # The profile may not have been modified in a way that + # causes the identifier to change: clear it anyway, and + # it will be re-set to the previous value on next access. + self._profile_data.pop(self._identity_key) + self._unwritten = True + + def _generate_id(self): + """Generate a new profile identifier. + + Generate a new sha1 profile identifier for this profile. + + Subclasses of ``BoomProfile`` must override this method to + generate an appropriate hash value, using the corresponding + identity keys for the profile type. + + :returns: None + :raises: NotImplementedError + """ + raise NotImplementedError + + def _append_profile(self): + """Append a ``BoomProfile`` to the appropriate global profile list + + This method must be overridden by classes that extend + ``BoomProfile``. + + :returns: None + :raises: NotImplementedError + """ + raise NotImplementedError + + def _from_file(self, profile_file): + """Initialise a new profile from data stored in a file. + + Initialise a new profile object using the profile data + in profile_file. + + This method should not be called directly: to build a new + ``Boomprofile`` object from in-memory data, use the class + initialiser with the ``profile_file`` argument. + + :returns: None + """ + profile_data = {} + comments = {} + comment = "" + ptype = self.__class__.__name__ + + _log_debug("Loading %sProfile from '%s'" % + (ptype, basename(profile_file))) + with open(profile_file, "r") as pf: + for line in pf: + if blank_or_comment(line): + comment += line if line else "" + else: + name, value = parse_name_value(line) + profile_data[name] = value + if comment: + comments[name] = comment + comment = "" + self._comments = comments + + try: + # Call subclass _from_data() hook for initialisation + self._from_data(profile_data, dirty=False) + except ValueError as e: + raise ValueError(str(e) + "in %s" % profile_file) + + def __init__(self, profile_keys, required_keys, identity_key): + """Initialise a new ``BoomProfile`` object. + + This method should be called by all subclasses of + ``BoomProfile`` in order to initialise the base class state. + + Subclasses must provide the set of allowed keys for this + profile type, a list of keys that are mandatory for profile + creation, and the name of the identity key that will return + this profile's unique identifier. + + :param profile_keys: The set of keys used by this profile. + :param required_keys: Mandatory keys for this profile. + :param identity_key: The key containing the profile id. + :returns: A new ``BoomProfile`` object. + :rtype: class ``BoomProfile`` + """ + self._profile_keys = profile_keys + self._required_keys = required_keys + self._identity_key = identity_key + + def match_uname_version(self, version): + """Test ``BoomProfile`` for version string match. + + Test the supplied version string to determine whether it + matches the uname_pattern of this ``BoomProfile``. + + :param version: A uname release version string to match. + :returns: ``True`` if this version matches this profile, or + ``False`` otherwise. + :rtype: bool + """ + _log_debug_profile("Matching uname pattern '%s' to '%s'" % + (self.uname_pattern, version)) + if self.uname_pattern and version: + if re.search(self.uname_pattern, version): + return True + return False + + def match_options(self, entry): + """Test ``BoomProfile`` for options template match. + + Test the supplied ``BootEntry`` to determine whether it + matches the options template defined by this + ``BoomProfile``. + + Used as a match of last resort when no uname pattern match + exists. + + :param entry: A ``BootEntry`` to match against this profile. + :returns: ``True`` if this entry matches this profile, or + ``False`` otherwise. + :rtype: bool + """ + # Attempt to match a distribution-formatted options line + + if not self.options or not entry.options: + return False + + opts_regex_words = self.make_format_regexes(self.options) + _log_debug_profile("Matching options regex list with %d entries" % + len(opts_regex_words)) + + format_opts = [] + fixed_opts = [] + + for rgx_word in opts_regex_words: + for word in entry.options.split(): + (name, exp) = rgx_word + match = re.match(exp, word) + if not match: + continue + value = match.group(0) + if name: + fixed_opts.append(value) + else: + format_opts.append(value) + + fixed = [o[1] for o in opts_regex_words if not o[0]] + have_fixed = [True if f in fixed_opts else False for f in fixed] + + form = [o[1] for o in opts_regex_words if o[0]] + have_form = [True if f in format_opts else False for f in form] + + return all(have_fixed) and any(have_form) + + def make_format_regexes(self, fmt): + """Generate regexes matching format string + + Generate a list of ``(key, expr)`` tuples containing key and + regular expression pairs capturing the format key values + contained in the format string. Any non-format key words + contained in the string are returned as a ``('', expr)`` + tuple containing no capture groups. + + The resulting list may be matched against the words of a + ``BootEntry`` object's value strings in order to extract + the parameters used to create them. + + :param fmt: The format string to build a regex list from. + :returns: A list of key and word regex tuples. + :rtype: list of (str, str) + """ + key_format = "%%{%s}" + regex_all = r"\S+" + regex_num = r"\d+" + regex_words = [] + + if not fmt: + return regex_words + + _log_debug_profile("Making format regex list for '%s'" % fmt) + + # Keys captured by single regex + key_regex = { + FMT_VERSION: regex_all, + FMT_LVM_ROOT_LV: regex_all, + FMT_BTRFS_SUBVOL_ID: regex_num, + FMT_BTRFS_SUBVOL_PATH: regex_all, + FMT_ROOT_DEVICE: regex_all, + FMT_KERNEL: regex_all, + FMT_INITRAMFS: regex_all + } + + # Keys requiring expansion + key_exp = { + FMT_LVM_ROOT_OPTS: [self.root_opts_lvm2], + FMT_BTRFS_ROOT_OPTS: [self.root_opts_btrfs], + # "fix" this by adding root_opts_btrfs_{id,path}? + FMT_BTRFS_SUBVOLUME: ["subvol=%{btrfs_subvol_path}", + "subvolid=%{btrfs_subvol_id}"], + FMT_ROOT_OPTS: [self.root_opts_lvm2, self.root_opts_btrfs], + } + + def _substitute_keys(word): + """Return a list of regular expressions matching the format keys + found in ``word``, expanding and substituting format keys + as necessary until all keys have been replaced with a + regular expression. + + For keys that form part of a word that represents the + canonical source of a BootParams attribute value (for e.g. + 'root=%{root_device}') the regular expression returned will + include a capture group for the attribute value. + """ + subst = [] + did_subst = False + capture = ( + "root=%{root_device}", "rd.lvm.lv=%{lvm_root_lv}", + "subvolid=%{btrfs_subvol_id}", "subvol=%{btrfs_subvol_path}" + ) + + replace = ("rootflags=%{btrfs_subvolume}",) + + for key in FORMAT_KEYS: + k = key_format % key + if k in word and key in key_regex: + regex_fmt = "%s" + keyname = "" + if word in capture: + regex_fmt = "(%s)" + keyname = key + word = word.replace(k, regex_fmt % key_regex[key]) + subst.append((keyname, word)) + did_subst = True + elif k in word and key in key_exp: + # Recursive expansion and substitution + for e in key_exp[key]: + if word in replace: + exp = e + else: + exp = word.replace(key_format % key, e) + subst += _substitute_keys(exp) + did_subst = True + + if not did_subst: + # Non-formatted word + subst.append(("", word)) + + return subst + + for word in fmt.split(): + regex_words += _substitute_keys(word) + + return regex_words + + # We use properties for the BoomProfile attributes: this is to + # allow the values to be stored in a dictionary. Although + # properties are quite verbose this reduces the code volume + # and complexity needed to marshal and unmarshal the various + # file formats used, as well as conversion to and from string + # representations of different types of BoomProfile objects. + + # The set of keys defined as properties for the BoomProfile + # class is the set of keys exposed by every profile type. + + @property + def os_name(self): + """The ``os_name`` of this profile. + + :getter: returns the ``os_name`` as a string. + :type: string + """ + return self._profile_data[BOOM_OS_NAME] + + @property + def os_short_name(self): + """The ``os_short_name`` of this profile. + + :getter: returns the ``os_short_name`` as a string. + :type: string + """ + return self._profile_data[BOOM_OS_SHORT_NAME] + + @property + def os_version(self): + """The ``os_version`` of this profile. + + :getter: returns the ``os_version`` as a string. + :type: string + """ + return self._profile_data[BOOM_OS_VERSION] + + @property + def os_version_id(self): + """The ``version_id`` of this profile. + + :getter: returns the ``os_version_id`` as a string. + :type: string + """ + return self._profile_data[BOOM_OS_VERSION_ID] + + # Configuration keys specify values that may be modified and + # have a corresponding .setter. + + @property + def uname_pattern(self): + """The current ``uname_pattern`` setting of this profile. + + :getter: returns the ``uname_pattern`` as a string. + :setter: stores a new ``uname_pattern`` setting. + :type: string + """ + return self._profile_data[BOOM_OS_UNAME_PATTERN] + + @uname_pattern.setter + def uname_pattern(self, value): + self._profile_data[BOOM_OS_UNAME_PATTERN] = value + self._dirty() + + @property + def kernel_pattern(self): + """The current ``kernel_pattern`` setting of this profile. + + :getter: returns the ``kernel_pattern`` as a string. + :setter: stores a new ``kernel_pattern`` setting. + :type: string + """ + return self._profile_data[BOOM_OS_KERNEL_PATTERN] + + @kernel_pattern.setter + def kernel_pattern(self, value): + kernel_key = key_from_key_name(FMT_KERNEL) + + if kernel_key in value: + ptype = self.__class__.__name__ + raise ValueError("%s.kernel cannot contain %s" % + (ptype, kernel_key)) + + self._profile_data[BOOM_OS_KERNEL_PATTERN] = value + self._dirty() + + @property + def initramfs_pattern(self): + """The current ``initramfs_pattern`` setting of this profile. + + :getter: returns the ``initramfs_pattern`` as a string. + :setter: store a new ``initramfs_pattern`` setting. + :type: string + """ + return self._profile_data[BOOM_OS_INITRAMFS_PATTERN] + + @initramfs_pattern.setter + def initramfs_pattern(self, value): + initramfs_key = key_from_key_name(FMT_INITRAMFS) + if initramfs_key in value: + ptype = self.__class__.__name__ + raise ValueError("%s.initramfs cannot contain %s" % + (ptype, initramfs_key)) + self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = value + self._dirty() + + @property + def root_opts_lvm2(self): + """The current LVM2 root options setting of this profile. + + :getter: returns the ``root_opts_lvm2`` value as a string. + :setter: store a new ``root_opts_lvm2`` value. + :type: string + """ + if BOOM_OS_ROOT_OPTS_LVM2 not in self._profile_data: + return None + return self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] + + @root_opts_lvm2.setter + def root_opts_lvm2(self, value): + root_opts_key = key_from_key_name(FMT_ROOT_OPTS) + if root_opts_key in value: + ptype = self.__class__.__name__ + raise ValueError("%s.root_opts_lvm2 cannot contain %s" % + (ptype, root_opts_key)) + self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = value + self._dirty() + + @property + def root_opts_btrfs(self): + """The current BTRFS root options setting of this profile. + + :getter: returns the ``root_opts_btrfs`` value as a string. + :setter: store a new ``root_opts_btrfs`` value. + :type: string + """ + if BOOM_OS_ROOT_OPTS_BTRFS not in self._profile_data: + return None + return self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] + + @root_opts_btrfs.setter + def root_opts_btrfs(self, value): + root_opts_key = key_from_key_name(FMT_ROOT_OPTS) + if root_opts_key in value: + ptype = self.__class__.__name__ + raise ValueError("%s.root_opts_btrfs cannot contain %s" % + (ptype, root_opts_key)) + self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = value + self._dirty() + + @property + def options(self): + """The current kernel command line options setting for this + profile. + + :getter: returns the ``options`` value as a string. + :setter: store a new ``options`` value. + :type: string + """ + if BOOM_OS_OPTIONS not in self._profile_data: + return None + return self._profile_data[BOOM_OS_OPTIONS] + + @options.setter + def options(self, value): + self._profile_data[BOOM_OS_OPTIONS] = value + self._dirty() + + @property + def title(self): + """The current title template for this profile. + + :getter: returns the ``title`` value as a string. + :setter: store a new ``title`` value. + :type: string + """ + if BOOM_OS_TITLE not in self._profile_data: + return None + return self._profile_data[BOOM_OS_TITLE] + + @title.setter + def title(self, value): + self._profile_data[BOOM_OS_TITLE] = value + self._dirty() + + def _check_optional_key(self, optional_key): + """Check that they optional key ``key`` is a valid, known BLS + optional key and raise ``ValueError`` if it is not. + """ + _valid_optional_keys = [ + "grub_users", + "grub_arg", + "grub_class", + "id" + ] + if optional_key not in _valid_optional_keys: + raise ValueError("Unknown optional key: '%s'" % optional_key) + + @property + def optional_keys(self): + """The set of optional BLS keys allowed by this profile. + + :getter: returns a string containing optional BLS key names. + :setter: store a new set of optional BLS keys. + :type: string + """ + if BOOM_OS_OPTIONAL_KEYS not in self._profile_data: + return "" + return self._profile_data[BOOM_OS_OPTIONAL_KEYS] + + @optional_keys.setter + def optional_keys(self, optional_keys): + for opt_key in optional_keys.split(): + self._check_optional_key(opt_key) + self._profile_data[BOOM_OS_OPTIONAL_KEYS] = optional_keys + self._dirty() + + def add_optional_key(self, key): + """Add the BLS key ``key`` to the allowed set of optional keys + for this profile. + """ + self._check_optional_key(key) + self.optional_keys = self.optional_keys + " " + key + + def del_optional_key(self, key): + """Remove the BLS key ``key`` from the allowed set of optional + keys for this profile. + """ + self._check_optional_key(key) + spacer = " " + key_list = [k for k in self.optional_keys.split() if k != key] + self.optional_keys = spacer.join(key_list) + + def _profile_path(self): + """Return the path to this profile's on-disk data. + + Return the full path to this Profile in the appropriate + Boom profile directory. Subclasses of ``BoomProfile`` must + override this method to return the correct path for the + specific profile type. + + :rtype: str + :returns: The absolute path for this ``BoomProfile`` file + :raises: NotImplementedError + """ + raise NotImplementedError + + def _write_profile(self, profile_id, profile_dir, mode, force=False): + """Write helper for profile classes. + + Write out this profile's data to a file in Boom format to + the paths specified by the current configuration. + + The pathname to write is obtained from self._profile_path(). + + If the value of ``force`` is ``False`` and the profile + is not currently marked as dirty (either new, or modified + since the last load operation) the write will be skipped. + + :param profile_id: The os_id or host_id of this profile. + :param profile_dir: The directory containing this type. + :param mode: The mode with which files are created. + :param force: Force this profile to be written to disk even + if the entry is unmodified. + + :raises: ``OsError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. + """ + ptype = self.__class__.__name__ + if not force and not self._unwritten: + return + + profile_path = self._profile_path() + + _log_debug("Writing %s(id='%s') to '%s'" % + (ptype, profile_id, basename(profile_path))) + + # List of key names for this profile type + profile_keys = self._profile_keys + + (tmp_fd, tmp_path) = mkstemp(prefix="boom", dir=profile_dir) + with fdopen(tmp_fd, "w") as f: + for key in [k for k in profile_keys if k in self._profile_data]: + if self._comments and key in self._comments: + f.write(self._comments[key].rstrip() + '\n') + f.write('%s="%s"\n' % (key, self._profile_data[key])) + f.flush() + fdatasync(f.fileno()) + try: + rename(tmp_path, profile_path) + chmod(profile_path, mode) + except Exception as e: + _log_error("Error writing profile file '%s': %s" % + (profile_path, e)) + try: + unlink(tmp_path) + except Exception: + pass + raise e + + _log_debug("Wrote %s (id=%s)'" % (ptype, profile_id)) + + def write_profile(self, force=False): + """Write out profile data to disk. + + Write out this ``BoomProfile``'s data to a file in Boom + format to the paths specified by the current configuration. + + This method must be overridden by `BoomProfile` subclasses. + + :raises: ``OsError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. ``NotImplementedError`` if + the method is called on the ``BoomProfile`` base + class. + """ + raise NotImplementedError + + def _delete_profile(self, profile_id): + """Deletion helper for profile classes. + + Remove the on-disk profile corresponding to this + ``BoomProfile`` object. This will permanently erase the + current file (although the current data may be re-written at + any time by calling ``write_profile()`` before the object is + disposed of). + + The method will call the profile class's ``_profile_path()`` + method in order to determine the location of the on-disk + profile store. + + :rtype: ``NoneType`` + :raises: ``OsError`` if an error occurs removing the file or + ``ValueError`` if the profile does not exist. + """ + ptype = self.__class__.__name__ + profile_path = self._profile_path() + + _log_debug("Deleting %s(id='%s') from '%s'" % + (ptype, profile_id, basename(profile_path))) + + if not path_exists(profile_path): + return + try: + unlink(profile_path) + _log_debug("Deleted %sProfile(id='%s')" % (ptype, profile_id)) + except Exception as e: + _log_error("Error removing %s file '%s': %s" % + (ptype, profile_path, e)) + + def delete_profile(self): + """Delete on-disk data for this profile. + + Remove the on-disk profile corresponding to this + ``BoomProfile`` object. This will permanently erase the + current file (although the current data may be re-written at + any time by calling ``write_profile()`` before the object is + disposed of). + + Subclasses of ``BoomProfile`` that implement an on-disk store + must override this method to perform any unlinking of the + profile from in-memory data structures, and to call the + generic ``_delete_profile()`` method to remove the profile + file. + + :rtype: ``NoneType`` + :raises: ``OsError`` if an error occurs removing the file or + ``ValueError`` if the profile does not exist. + """ + raise NotImplementedError + + +class OsProfile(BoomProfile): + """ Class OsProfile implements Boom operating system profiles. + Objects of type OsProfile define the paths, kernel command line + options, root device flags and file name patterns needed to boot + an instance of that operating system. + """ + _profile_data = None + _unwritten = False + _comments = None + + _profile_keys = OS_PROFILE_KEYS + _required_keys = OS_REQUIRED_KEYS + _identity_key = BOOM_OS_ID + + def __str__(self): + """Format this OsProfile as a human readable string. + + Profile attributes are printed as "Name: value, " pairs, + with like attributes grouped together onto lines. + + :returns: A human readable string representation of this OsProfile. + + :rtype: string + """ + breaks = [ + BOOM_OS_ID, BOOM_OS_SHORT_NAME, BOOM_OS_VERSION_ID, + BOOM_OS_UNAME_PATTERN, BOOM_OS_INITRAMFS_PATTERN, + BOOM_OS_ROOT_OPTS_LVM2, BOOM_OS_ROOT_OPTS_BTRFS, + BOOM_OS_OPTIONS, BOOM_OS_TITLE + ] + + fields = [f for f in OS_PROFILE_KEYS if f in self._profile_data] + osp_str = "" + tail = "" + for f in fields: + osp_str += '%s: "%s"' % (OS_KEY_NAMES[f], self._profile_data[f]) + tail = ",\n" if f in breaks else ", " + osp_str += tail + osp_str = osp_str.rstrip(tail) + return osp_str + + def __repr__(self): + """Format this OsProfile as a machine readable string. + + Return a machine-readable representation of this ``OsProfile`` + object. The string is formatted as a call to the ``OsProfile`` + constructor, with values passed as a dictionary to the + ``profile_data`` keyword argument. + + :returns: a string representation of this ``OsProfile``. + :rtype: string + """ + osp_str = "OsProfile(profile_data={" + fields = [f for f in OS_PROFILE_KEYS if f in self._profile_data] + for f in fields: + osp_str += '%s:"%s", ' % (f, self._profile_data[f]) + osp_str = osp_str.rstrip(", ") + return osp_str + "})" + + def _generate_id(self): + """Generate a new OS identifier. + + Generate a new sha1 profile identifier for this profile, + using the os_short_name, version, and version_id values and + store it in _profile_data. + + :returns: None + """ + hashdata = (self.os_short_name + self.os_version + self.os_version_id) + + digest = sha1(hashdata.encode('utf-8')).hexdigest() + self._profile_data[BOOM_OS_ID] = digest + + def _append_profile(self): + """Append an OsProfile to the global profile list + + Check whether this ``OsProfile`` already exists, and add it + to the global profile list if not. If the profile is already + present ``ValueError`` is raised. + + :raises: ValueError + """ + if _profile_exists(self.os_id): + raise ValueError("Profile already exists (os_id=%s)" % + self.disp_os_id) + + _profiles.append(self) + _profiles_by_id[self.os_id] = self + + def _from_data(self, profile_data, dirty=True): + """Initialise an OsProfile from in-memory data. + + Initialise a new OsProfile object using the profile data + in the `profile_data` dictionary. + + This method should not be called directly: to build a new + ``Osprofile`` object from in-memory data, use the class + initialiser with the ``profile_data`` argument. + + :returns: None + """ + err_str = "Invalid profile data (missing %s)" + + _log_debug_profile("Initialising OsProfile from profile_data=%s" % + profile_data) + + # Set profile defaults + for key in _DEFAULT_KEYS: + if key not in profile_data: + profile_data[key] = _DEFAULT_KEYS[key] + + for key in self._required_keys: + if key == BOOM_OS_ID: + continue + if key not in profile_data: + raise ValueError(err_str % key) + + root_opts = [key for key in OS_ROOT_KEYS if key in profile_data] + if not any(root_opts): + root_opts_err = err_str % "ROOT_OPTS" + raise ValueError(root_opts_err) + + if BOOM_OS_OPTIONAL_KEYS in profile_data: + for opt_key in profile_data[BOOM_OS_OPTIONAL_KEYS].split(): + self._check_optional_key(opt_key) + + # Empty OPTIONS is permitted: set the corresponding + # value in the _profile_data dictionary to the empty string. + if BOOM_OS_OPTIONS not in profile_data: + profile_data[BOOM_OS_OPTIONS] = "" + self._profile_data = dict(profile_data) + + if BOOM_OS_ID not in self._profile_data: + self._generate_id() + + if dirty: + self._dirty() + + self._append_profile() + + def _from_file(self, profile_file): + """Initialise a new profile from data stored in a file. + + Initialise a new profil object using the profile data + in profile_file. + + This method should not be called directly: to build a new + ``Osprofile`` object from in-memory data, use the class + initialiser with the ``profile_file`` argument. + + :returns: None + """ + profile_data = {} + comments = {} + comment = "" + ptype = self.__class__.__name__ + + _log_debug("Loading %sProfile from '%s'" % + (ptype, basename(profile_file))) + with open(profile_file, "r") as pf: + for line in pf: + if blank_or_comment(line): + comment += line if line else "" + else: + name, value = parse_name_value(line) + profile_data[name] = value + if comment: + comments[name] = comment + comment = "" + self._comments = comments + + try: + self._from_data(profile_data, dirty=False) + except ValueError as e: + raise ValueError(str(e) + "in %s" % profile_file) + + def __init__(self, name=None, short_name=None, version=None, + version_id=None, profile_file=None, profile_data=None, + uname_pattern=None, kernel_pattern=None, + initramfs_pattern=None, root_opts_lvm2=None, + root_opts_btrfs=None, options=None, optional_keys=None): + """Initialise a new ``OsProfile`` object. + + If neither ``profile_file`` nor ``profile_data`` is given, + all of ``name``, ``short_name``, ``version``, and + ``version_id`` must be given. + + These values form the profile identity and are used to + generate the profile unique identifier. + + :param name: The name of the operating system. + :param short_name: A short name for the operating system, + suitable for use in file names. + :param version: A string describing the version of the + operating system. + :param version_id: A short alphanumeric string representing + the operating system version and suitable + for use in generating file names. + :param profile_data: An optional dictionary mapping from + ``BOOM_OS_*`` keys to profile values. + :param profile_file: An optional path to a file from which + profile data should be loaded. The file + should be in Boom profile format, with + ``BOOM_OS_*`` key=value pairs. + :param uname_pattern: Optional uname pattern. + :param kernel_pattern: Optional kernel pattern. + :param initramfs_pattern: Optional initramfs pattern. + :param root_opts_lvm2: Optional LVM2 root options template. + :param root_opts_btrfs: Optional BTRFS options template. + :param options: Optional options template. + + :returns: A new ``OsProfile`` object. + :rtype: class OsProfile + """ + global _profiles + self._profile_data = {} + + # Initialise BoomProfile base class + super(OsProfile, self).__init__(OS_PROFILE_KEYS, OS_REQUIRED_KEYS, + BOOM_OS_ID) + + if profile_data and profile_file: + raise ValueError("Only one of 'profile_data' or 'profile_file' " + "may be specified.") + + if profile_data: + self._from_data(profile_data) + return + if profile_file: + self._from_file(profile_file) + return + + self._dirty() + + self._profile_data[BOOM_OS_NAME] = name + self._profile_data[BOOM_OS_SHORT_NAME] = short_name + self._profile_data[BOOM_OS_VERSION] = version + self._profile_data[BOOM_OS_VERSION_ID] = version_id + + # Optional arguments: unset values will be replaced by defaults. + if uname_pattern: + self._profile_data[BOOM_OS_UNAME_PATTERN] = uname_pattern + self._profile_data[BOOM_OS_KERNEL_PATTERN] = kernel_pattern + self._profile_data[BOOM_OS_INITRAMFS_PATTERN] = initramfs_pattern + self._profile_data[BOOM_OS_ROOT_OPTS_LVM2] = root_opts_lvm2 + self._profile_data[BOOM_OS_ROOT_OPTS_BTRFS] = root_opts_btrfs + self._profile_data[BOOM_OS_OPTIONS] = options + + if optional_keys: + self.optional_keys = optional_keys + + required_args = [name, short_name, version, version_id] + if all([not val for val in required_args]): + # NULL profile + for key in OS_PROFILE_KEYS: + self._profile_data[key] = "" + elif any([not val for val in required_args]): + raise ValueError("Invalid profile arguments: name, " + "short_name, version, and version_id are" + "mandatory.") + + def default_if_unset(key): + if key not in self._profile_data: + return _DEFAULT_KEYS[key] + return self._profile_data[key] or _DEFAULT_KEYS[key] + + # Apply global defaults for unset keys + for key in _DEFAULT_KEYS: + self._profile_data[key] = default_if_unset(key) + + self._generate_id() + self._append_profile() + + # We use properties for the OsProfile attributes: this is to + # allow the values to be stored in a dictionary. Although + # properties are quite verbose this reduces the code volume + # and complexity needed to marshal and unmarshal the various + # file formats used, as well as conversion to and from string + # representations of OsProfile objects. + + # Keys obtained from os-release data form the profile's identity: + # the corresponding attributes are read-only. + + # OSProfile properties: + # + # disp_os_id + # os_id + + # OSProfile properties inherited from BoomProfile + # os_name + # os_short_name + # os_version + # os_version_id + # uname_pattern + # kernel_pattern + # initramfs_pattern + # root_opts_lvm2 + # root_opts_btrfs + # options + + @property + def disp_os_id(self): + """The display os_id of this profile. + + Return the shortest prefix of this OsProfile's os_id that + is unique within the current set of loaded profiles. + + :getter: return this OsProfile's os_id. + :type: str + """ + return self.os_id[:min_os_id_width()] + + @property + def os_id(self): + """The ``os_id`` of this profile. + + :getter: returns the ``os_id`` as a string. + :type: string + """ + if BOOM_OS_ID not in self._profile_data: + self._generate_id() + return self._profile_data[BOOM_OS_ID] + + # + # Class methods for building OsProfile instances from os-release + # + + @classmethod + def from_os_release(cls, os_release, profile_data=None): + """Build an OsProfile from os-release file data. + + Construct a new OsProfile object using data obtained from + a file in os-release(5) format. + + :param os_release: String data in os-release(5) format + :param profile_data: an optional dictionary of profile data + overriding default values. + :returns: A new OsProfile for the specified os-release data + :rtype: OsProfile + """ + release_data = {} + profile_data = profile_data or {} + for line in os_release: + if blank_or_comment(line): + continue + name, value = parse_name_value(line) + release_data[name] = value + + release_keys = {"NAME": BOOM_OS_NAME, + "ID": BOOM_OS_SHORT_NAME, + "VERSION": BOOM_OS_VERSION, + "VERSION_ID": BOOM_OS_VERSION_ID} + + for key in release_keys.keys(): + profile_data[release_keys[key]] = release_data[key] + + osp = OsProfile(profile_data=profile_data) + + return osp + + @classmethod + def from_os_release_file(cls, path, profile_data={}): + """Build an OsProfile from an on-disk os-release file. + + Construct a new OsProfile object using data obtained from + the file specified by 'path'. + + :param path: Path to a file in os-release(5) format + :param profile_data: an optional dictionary of profile data + overriding default values. + :returns: A new OsProfile for the specified os-release file + :rtype: OsProfile + """ + with open(path, "r") as f: + return cls.from_os_release(f, profile_data=profile_data) + + @classmethod + def from_host_os_release(cls, profile_data={}): + """Build an OsProfile from the current hosts's os-release. + + Construct a new OsProfile object using data obtained from + the running hosts's /etc/os-release file. + + :param profile_data: an optional dictionary of profile data + overriding default values. + :returns: A new OsProfile for the current host + :rtype: OsProfile + """ + return cls.from_os_release_file("/etc/os-release", + profile_data=profile_data) + + def _profile_path(self): + """Return the path to this profile's on-disk data. + + Return the full path to this OsProfile in the Boom profiles + directory (or the location to which it will be written, if + it has not yet been written). + + :rtype: str + :returns: The absolute path for this OsProfile's file + """ + profile_id = (self.os_id, self.os_short_name, self.os_version_id) + profile_path_name = BOOM_OS_PROFILE_FORMAT % (profile_id) + return path_join(boom_profiles_path(), profile_path_name) + + def write_profile(self, force=False): + """Write out profile data to disk. + + Write out this ``OsProfile``'s data to a file in Boom + format to the paths specified by the current configuration. + + Currently the ``os_id``, ``short_name`` and ``version_id`` + keys are used to construct the file name. + + If the value of ``force`` is ``False`` and the ``OsProfile`` + is not currently marked as dirty (either new, or modified + since the last load operation) the write will be skipped. + + :param force: Force this profile to be written to disk even + if the entry is unmodified. + :raises: ``OsError`` if the temporary entry file cannot be + renamed, or if setting file permissions on the + new entry file fails. + """ + path = boom_profiles_path() + mode = BOOM_PROFILE_MODE + self._write_profile(self.os_id, path, mode, force=force) + + def delete_profile(self): + """Delete on-disk data for this profile. + + Remove the on-disk profile corresponding to this + ``OsProfile`` object. This will permanently erase the + current file (although the current data may be re-written at + any time by calling ``write_profile()`` before the object is + disposed of). + + :rtype: ``NoneType`` + :raises: ``OsError`` if an error occurs removing the file or + ``ValueError`` if the profile does not exist. + """ + global _profiles + self._delete_profile(self.os_id) + if _profiles and self in _profiles: + _profiles.remove(self) + if _profiles_by_id and self.os_id in _profiles_by_id: + _profiles_by_id.pop(self.os_id) + + +__all__ = [ + 'BoomProfile', 'OsProfile', + 'profiles_loaded', 'drop_profiles', 'load_profiles', 'write_profiles', + 'find_profiles', 'get_os_profile_by_id', 'select_profile', + 'match_os_profile', 'match_os_profile_by_version', 'key_from_key_name', + + # Module constants + 'BOOM_PROFILES', 'BOOM_OS_PROFILE_FORMAT', + 'BOOM_PROFILE_MODE', + + # Exported key names + 'BOOM_OS_ID', 'BOOM_OS_NAME', 'BOOM_OS_SHORT_NAME', + 'BOOM_OS_VERSION', 'BOOM_OS_VERSION_ID', 'BOOM_OS_UNAME_PATTERN', + 'BOOM_OS_KERNEL_PATTERN', 'BOOM_OS_INITRAMFS_PATTERN', + 'BOOM_OS_ROOT_OPTS_LVM2', 'BOOM_OS_ROOT_OPTS_BTRFS', + 'BOOM_OS_OPTIONS', 'BOOM_OS_TITLE', 'BOOM_OS_OPTIONAL_KEYS', + + 'OS_PROFILE_KEYS', 'OS_KEY_NAMES', 'OS_REQUIRED_KEYS', 'OS_ROOT_KEYS', + + # Path configuration + 'boom_profiles_path', +] + +# vim: set et ts=4 sw=4 : diff --git a/boom/report.py b/boom/report.py new file mode 100644 index 0000000..0ae6072 --- /dev/null +++ b/boom/report.py @@ -0,0 +1,1096 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# boom/report.py - Text reporting +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +"""The Boom reporting module contains a set of classes for creating +simple text based tabular reports for a user-defined set of object +types and fields. No restrictions are placed on the types of object +that can be reported: users of the ``BoomReport`` classes may define +additional object types outside the ``boom`` package and include these +types in reports generated by the module. + +The fields displayed in a specific report may be selected from the +available set of fields by specifying a simple comma-separated string +list of field names (in display order). In addition, custom multi-column +sorting is possible using a similar string notation. + +The ``BoomReport`` module is closely based on the ``device-mapper`` +reporting engine and shares many features and behaviours with device +mapper reports. +""" +from __future__ import print_function + +from boom import find_minimum_sha_prefix, BOOM_DEBUG_REPORT +import logging +import sys + +_log = logging.getLogger(__name__) +_log.set_debug_mask(BOOM_DEBUG_REPORT) + +_log_debug = _log.debug +_log_debug_report = _log.debug_masked +_log_info = _log.info +_log_warn = _log.warning +_log_error = _log.error + +_default_columns = 80 + +REP_NUM = "num" +REP_STR = "str" +REP_SHA = "sha" + +_dtypes = [REP_NUM, REP_STR, REP_SHA] + + +_default_width = 8 + +ALIGN_LEFT = "left" +ALIGN_RIGHT = "right" + +_align_types = [ALIGN_LEFT, ALIGN_RIGHT] + +ASCENDING = "ascending" +DESCENDING = "descending" + +STANDARD_QUOTE = "'" +STANDARD_PAIR = "=" + +MIN_SHA_WIDTH = 7 + + +# Python2 vs. Python2 string types +try: + # Py2 + string_types = (str, unicode) +except NameError: + # Py3 + string_types = (str) + +num_types = (int, float) + + +class BoomReportOpts(object): + """BoomReportOpts() + Options controlling the formatting and output of a boom report. + """ + columns = 0 + headings = True + buffered = True + separator = None + field_name_prefix = None + unquoted = True + aligned = True + columns_as_rows = False + report_file = None + + def __init__(self, columns=_default_columns, headings=True, buffered=True, + separator=" ", field_name_prefix="", unquoted=True, + aligned=True, report_file=sys.stdout): + """Initialise BoomReportOpts object. + + Initialise a ``BoomReportOpts`` object to control output + of a ``BoomReport``. + + :param columns: the number of columns to use for output. + :param headings: a boolean indicating whether to output + column headings for this report. + :param buffered: a boolean indicating whether to buffer + output from this report. + :param report_file: a file to which output will be sent. + :returns: a new ``BoomReportOpts`` object. + :rtype: ```` + """ + self.columns = columns + self.headings = headings + self.buffered = buffered + self.separator = separator + self.field_name_prefix = field_name_prefix + self.unquoted = unquoted + self.aligned = aligned + self.report_file = report_file + + +class BoomReportObjType(object): + """BoomReportObjType() + Class representing a type of objecct to be reported on. + Instances of ``BoomReportObjType`` must specify an identifier, + a description, and a data function that will return the correct + type of object from a compound object containing data objects + of different types. For reports that use only a single object + type the ``data_fn`` member may be simply ``lambda x: x``. + """ + + objtype = -1 + desc = "" + prefix = "" + data_fn = None + + def __init__(self, objtype, desc, prefix, data_fn): + """Initialise BoomReportObjType. + + Initialise a new ``BoomReportObjType`` object with the + specified ``objtype``, ``desc``, optional ``prefix`` and + ``data_fn``. The ``objtype`` must be an integer power of two + that is unique within a given report. The ``data_fn`` should + accept an object as its only argument and return an object + of the requested type. + """ + if not objtype or objtype < 0: + raise ValueError("BoomReportObjType objtype cannot be <= 0.") + + if not desc: + raise ValueError("BoomReportObjType desc cannot be empty.") + + if not data_fn: + raise ValueError("BoomReportObjType requires data_fn.") + + self.objtype = objtype + self.desc = desc + self.prefix = prefix + self.data_fn = data_fn + + +class BoomFieldType(object): + """BoomFieldType() + The ``BoomFieldType`` class describes the properties of a field + available in a ``BoomReport`` instance. + """ + objtype = -1 + name = None + head = None + desc = None + width = _default_width + align = None + dtype = None + report_fn = None + + def __init__(self, objtype, name, head, desc, width, dtype, report_fn, + align=None): + """Initialise new BoomFieldType object. + + Initialise a new ``BoomFieldType`` object with the specified + properties. + + :param objtype: The numeric object type ID (power of two) + :param name: The field name used to select display fields + :param desc: A human-readable description of the field + :param width: The default (initial) field width + :param dtype: The BoomReport data type of the field + :param report_fn: The field reporting function + :param align: The field alignment value + :returns: A new BoomReportFieldType object + :rtype: BoomReportFieldType + """ + if not objtype: + raise ValueError("'objtype' must be non-zero") + if not name: + raise ValueError("'name' is required") + self.objtype = objtype + self.name = name + self.head = head + self.desc = desc + + if dtype not in _dtypes: + raise ValueError("Invalid field dtype: %s " % dtype) + + if align and align not in _align_types: + raise ValueError("Invalid field alignment: %s" % align) + + self.dtype = dtype + self.report_fn = report_fn + + if not align: + if dtype == REP_STR or dtype == REP_SHA: + self.align = ALIGN_LEFT + if dtype == REP_NUM: + self.align = ALIGN_RIGHT + else: + self.align = align + + if width < 0: + raise ValueError("Field width cannot be < 0") + self.width = width if width else _default_width + + +class BoomFieldProperties(object): + field_num = None + # sort_posn + initial_width = 0 + width = 0 + objtype = None + dtype = None + align = None + # + # Field flags + # + hidden = False + implicit = False + sort_key = False + sort_dir = None + compact_one = False # used for implicit fields + compacted = False + sort_posn = None + + +class BoomField(object): + """BoomField() + A ``BoomField`` represents an instance of a ``BoomFieldType`` + including its associated data values. + """ + #: reference to the containing BoomReport + _report = None + #: reference to the BoomFieldProperties describing this field + _props = None + #: The formatted string to be reported for this field. + report_string = None + #: The raw value of this field. Used for sorting. + sort_value = None + + def __init__(self, report, props): + """Initialise a new BoomField object. + + Initialise a BoomField object and configure the supplied + ``report`` and ``props`` attributes. + + :param report: The BoomReport that owns this field + :param props: The BoomFieldProperties object for this field + """ + self._report = report + self._props = props + + def report_str(self, value): + """Report a string value for this BoomField object. + + Set the value for this field to the supplied ``value``. + + :param value: The string value to set + :rtype: None + """ + if not isinstance(value, string_types): + raise TypeError("Value for report_str() must be a string type.") + self.set_value(value, sort_value=value) + + def report_sha(self, value): + """Report a SHA value for this BoomField object. + + Set the value for this field to the supplied ``value``. + + :param value: The SHA value to set + :rtype: None + """ + if not isinstance(value, string_types): + raise TypeError("Value for report_sha() must be a string type.") + self.set_value(value, sort_value=value) + + def report_num(self, value): + """Report a numeric value for this BoomField object. + + Set the value for this field to the supplied ``value``. + + :param value: The numeric value to set + :rtype: None + """ + if value is not None and not isinstance(value, num_types): + raise TypeError("Value for report_num() must be a numeric type.") + report_string = str(value) if value else "" + sort_value = value if value is not None else -1 + self.set_value(report_string, sort_value=sort_value) + + def set_value(self, report_string, sort_value=None): + """Report an arbitrary value for this BoomField object. + + Set the value for this field to the supplied ``value``, + and set the field's ``sort_value`` to the supplied + ``sort_value``. + + :param report_string: The string value to set + :param sort_value: The sort value + :rtype: None + """ + if report_string is None: + raise ValueError("No value assigned to field.") + self.report_string = report_string + self.sort_value = sort_value if sort_value else report_string + + +class BoomRow(object): + """BoomRow() + A class representing a single data row making up a report. + """ + #: the report that this BoomRow belongs to + _report = None + #: the list of report fields in display order + _fields = None + #: fields in sort order + _sort_fields = None + + def __init__(self, report): + self._report = report + self._fields = [] + + def add_field(self, field): + """Add a field to this BoomRow. + + :param field: The field to be added + :rtype: None + """ + self._fields.append(field) + + +def __none_returning_fn(obj): + """Dummy data function for special report types. + + :returns: None + """ + return None + + +# Implicit report fields and types + +BR_SPECIAL = 0x80000000 +_implicit_special_report_types = [ + BoomReportObjType( + BR_SPECIAL, "Special", "special_", __none_returning_fn + ) +] + + +def __no_report_fn(f, d): + """Dummy report function for special report types. + + :returns: None + """ + return + + +_special_field_help_name = "help" + +_implicit_special_report_fields = [ + BoomFieldType( + BR_SPECIAL, _special_field_help_name, "Help", "Show help", 8, + REP_STR, __no_report_fn) +] + + +# BoomReport class + +class BoomReport(object): + """BoomReport() + A class representing a configurable text report with multiple + caller-defined fields. An optional title may be provided and he + ``fields`` argument must contain a list of ``BoomField`` objects + describing the required report. + + """ + report_types = 0 + + _fields = None + _types = None + _data = None + _rows = None + _keys_count = 0 + _field_properties = None + _header_written = False + _field_calc_needed = True + _sort_required = False + _already_reported = False + + # Implicit field support + _implicit_types = _implicit_special_report_types + _implicit_fields = _implicit_special_report_fields + + private = None + opts = None + + def __help_requested(self): + """Check for presence of 'help' fields in output selection. + + Check the fields making up this BoomReport and return True + if any valid 'help' field synonym is present. + + :returns: True if help was requested or False otherwise + """ + for fp in self._field_properties: + if fp.implicit: + name = self._implicit_fields[fp.field_num].name + if name == _special_field_help_name: + return True + return False + + def __get_longest_field_name_len(self, fields): + """Find the longest field name length. + + :returns: the length of the longest configured field name + """ + max_len = 0 + for f in fields: + cur_len = len(f.name) + max_len = cur_len if cur_len > max_len else max_len + for t in self._types: + cur_len = len(t.prefix) + 3 + max_len = cur_len if cur_len > max_len else max_len + return max_len + + def __display_fields(self, display_field_types): + """Display report fields help message. + + Display a list of valid fields for this ``BoomReport``. + + :param fields: The list of fields to display + :param display_field_types: A boolean controling whether + field types (str, SHA, num) + are included in help output + """ + fields = self._fields + name_len = self.__get_longest_field_name_len(fields) + last_desc = "" + banner = "-" * 79 + for f in fields: + t = self.__find_type(f.objtype) + if t: + desc = t.desc + else: + desc = "" + if desc != last_desc: + if len(last_desc): + print(" ") + desc_len = len(desc) + 7 + print("%s Fields" % desc) + print("%*.*s" % (desc_len, desc_len, banner)) + print(" %-*s - %s%s%s%s" % + (name_len, f.name, f.desc, + " [" if display_field_types else "", + f.dtype if display_field_types else "", + "]" if display_field_types else "")) + last_desc = desc + + def __find_type(self, report_type): + """Resolve numeric type to corresponding BoomReportObjType. + + :param report_type: The numeric report type to look up + :returns: The requested BoomReportObjType. + :raises: ValueError if no matching type was found. + """ + for t in self._implicit_types: + if t.objtype == report_type: + return t + for t in self._types: + if t.objtype == report_type: + return t + + raise ValueError("Unknown report object type: %d" % report_type) + + def __copy_field(self, field_num, implicit): + """Copy field definition to BoomFieldProperties + + Copy values from a BoomFieldType to BoomFieldProperties. + + :param field_num: The number of this field (fields order) + :param implicit: True if this field is implicit, else False + """ + fp = BoomFieldProperties() + fp.field_num = field_num + fp.width = fp.initial_width = self._fields[field_num].width + fp.implicit = implicit + fp.objtype = self.__find_type(self._fields[field_num].objtype) + fp.dtype = self._fields[field_num].dtype + fp.align = self._fields[field_num].align + return fp + + def __add_field(self, field_num, implicit): + """Add a field to this BoomReport. + + Add the specified BoomFieldType to this BoomReport and + configure BoomFieldProperties for it. + + :param field_num: The number of this field (fields order) + :param implicit: True if this field is implicit, else False + """ + fp = self.__copy_field(field_num, implicit) + if fp.hidden: + self._field_properties.insert(0, fp) + else: + self._field_properties.append(fp) + return fp + + def __get_field(self, field_name): + """Look up a field by name. + + Attempt to find the field named in ``field_name`` in this + BoomReport's tables of implicit and user-defined fields, + returning the a ``(field, implicit)`` tuple, where field + contains the requested ``BoomFieldType``, and ``implicit`` + is a boolean indicating whether this field is implicit or + not. + + :param field_num: The number of this field (fields order) + :param implicit: True if this field is implicit, else False + """ + # FIXME implicit fields + for field in self._implicit_fields: + if field.name == field_name: + return (self._implicit_fields.index(field), True) + for field in self._fields: + if field.name == field_name: + return (self._fields.index(field), False) + raise ValueError("No matching field name: %s" % field_name) + + def __field_match(self, field_name, type_only): + """Attempt to match a field and optionally update report type. + + Look up the named field and, if ``type_only`` is True, + update this BoomReport's ``report_types`` mask to include + the field's type identifier. If ``type_only`` is False the + field is also added to this BoomReport's field list. + + :param field_name: A string identifying the field + :param type_only: True if this call should only update types + """ + try: + (f, implicit) = self.__get_field(field_name) + if (type_only): + if implicit: + self.report_types |= self._implicit_fields[f].objtype + else: + self.report_types |= self._fields[f].objtype + return + return self.__add_field(f, implicit) + except ValueError as e: + # FIXME handle '$PREFIX_all' + # re-raise 'e' if it fails. + raise e + + def __parse_fields(self, field_format, type_only): + """Parse report field list. + + Parse ``field_format`` and attempt to match the names of + field names found to registered BoomFieldType fields. + + If ``type_only`` is True only the ``report_types`` field + is updated: otherwise the parsed fields are added to the + BoomReport's field list. + + :param field_format: The list of fields to parse + :param type_only: True if this call should only update types + """ + for word in field_format.split(','): + # Allow consecutive commas + if not word: + continue + try: + self.__field_match(word, type_only) + except ValueError as e: + self.__display_fields(True) + print("Unrecognised field: %s" % word) + raise e + + def __add_sort_key(self, field_num, sort, implicit, type_only): + """Add a new sort key to this BoomReport + + Add the sort key identified by ``field_num`` to this list + of sort keys for this BoomReport. + + :param field_num: The field number of the key to add + :param sort: The sort direction for this key + :param implicit: True if field_num is implicit, else False + :param type_only: True if this call should only update types + """ + fields = self._implicit_fields if implicit else self._fields + found = None + + for fp in self._field_properties: + if fp.implicit == implicit and fp.field_num == field_num: + found = fp + + if not found: + if type_only: + self.report_types |= fields[field_num].objtype + return + else: + found = self.__add_field(field_num, implicit) + + if found.sort_key: + _log_info("Ignoring duplicate sort field: %s" % + fields[field_num].name) + found.sort_key = True + found.sort_dir = sort + found.sort_posn = self._keys_count + self._keys_count += 1 + + def __key_match(self, key_name, type_only): + """Attempt to match a sort key and update report type. + + Look up the named sort key and, if ``type_only`` is True, + update this BoomReport's ``report_types`` mask to include + the field's type identifier. If ``type_only`` is False the + field is also added to this BoomReport's field list. + + :param field_name: A string identifying the sort key + :param type_only: True if this call should only update types + """ + sort_dir = None + + if not key_name: + raise ValueError("Sort key name cannot be empty") + + if key_name.startswith('+'): + sort_dir = ASCENDING + key_name = key_name[1:] + elif key_name.startswith('-'): + sort_dir = DESCENDING + key_name = key_name[1:] + else: + sort_dir = ASCENDING + + for field in self._implicit_fields: + fields = self._implicit_fields + if field.name == key_name: + return self.__add_sort_key(fields.index(field), sort_dir, + True, type_only) + for field in self._fields: + fields = self._fields + if field.name == key_name: + return self.__add_sort_key(fields.index(field), sort_dir, + False, type_only) + + raise ValueError("Unknown sort key name: %s" % key_name) + + def __parse_keys(self, keys, type_only): + """Parse report sort key list. + + Parse ``keys`` and attempt to match the names of + sort keys found to registered BoomFieldType fields. + + If ``type_only`` is True only the ``report_types`` field + is updated: otherwise the parsed fields are added to the + BoomReport's sort key list. + + :param field_format: The list of fields to parse + :param type_only: True if this call should only update types + """ + if not keys: + return + for word in keys.split(','): + # Allow consecutive commas + if not word: + continue + try: + self.__key_match(word, type_only) + except ValueError as e: + self.__display_fields(True) + print("Unrecognised field: %s" % word) + raise e + + def __init__(self, types, fields, output_fields, opts, + sort_keys, private): + """Initialise BoomReport. + + Initialise a new ``BoomReport`` object with the specified fields + and output control options. + + :param types: List of BoomReportObjType used in this report. + :param fields: A list of ``BoomField`` field descriptions. + :param output_fields: An optional list of output fields to + be rendered by this report. + :param opts: An instance of ``BoomReportOpts`` or None. + :returns: A new report object. + :rtype: ``BoomReport``. + """ + + self._fields = fields + self._types = types + self._private = private + + if opts.buffered: + self._sort_required = True + + self.opts = opts if opts else BoomReportOpts() + + self._rows = [] + self._field_properties = [] + + # set field_prefix from type + + # canonicalize_field_ids() + + if not output_fields: + output_fields = ",".join([field.name for field in fields]) + + # First pass: set up types + self.__parse_fields(output_fields, 1) + self.__parse_keys(sort_keys, 1) + + # Second pass: initialise fields + self.__parse_fields(output_fields, 0) + self.__parse_keys(sort_keys, 0) + + if self.__help_requested(): + self._already_reported = True + self.__display_fields(display_field_types=True) + print("") + + def __recalculate_sha_width(self): + """Recalculate minimum SHA field widths. + + For each REP_SHA field present, recalculate the minimum + field width required to ensure uniqueness of the displayed + values. + + :rtype: None + """ + shas = {} + props_map = {} + for row in self._rows: + for field in row._fields: + if self._fields[field._props.field_num].dtype == REP_SHA: + # Use field_num as index to apply check across rows + num = field._props.field_num + if num not in shas: + shas[num] = set() + props_map[num] = field._props + shas[num].add(field.report_string) + for num in shas.keys(): + min_prefix = max(MIN_SHA_WIDTH, props_map[num].width) + props_map[num].width = find_minimum_sha_prefix(shas[num], + min_prefix) + + def __recalculate_fields(self): + """Recalculate field widths. + + For each field, recalculate the minimum field width by + finding the longest ``report_string`` value for that field + and updating the dynamic width stored in the corresponding + ``BoomFieldProperties`` object. + + :rtype: None + """ + for row in self._rows: + for field in row._fields: + if self._sort_required and field._props.sort_key: + row._sort_fields[field._props.sort_posn] = field + if self._fields[field._props.field_num].dtype == REP_SHA: + continue + field_len = len(field.report_string) + if field_len > field._props.width: + field._props.width = field_len + + def __report_headings(self): + """Output report headings. + + Output the column headings for this BoomReport. + + :rtype: None + """ + self._header_written = True + if not self.opts.headings: + return + + line = "" + props = self._field_properties + for fp in props: + if fp.hidden: + continue + fields = self._fields + heading = fields[fp.field_num].head + headertuple = (fp.width, fp.width, heading) + if self.opts.aligned: + heading = "%-*.*s" % headertuple + line += heading + if props.index(fp) != (len(props) - 1): + line += self.opts.separator + self.opts.report_file.write(line + "\n") + + def __row_key_fn(self): + """Return a Python key function to compare report rows. + + The ``cmp`` argument of sorting functions has been removed + in Python 3.x: to maintain similarity with the device-mapper + report library we keep a traditional "cmp"-style function + (that is structured identically to the version in the device + mapper library), and dynamically wrap it in a ``__RowKey`` + object to conform to the Python sort key model. + + :returns: A __RowKey object wrapping _row_cmp() + :rtype: __RowKey + """ + def _row_cmp(row_a, row_b): + """Compare two report rows for sorting. + + Compare the report rows ``row_a`` and ``row_b`` and + return a "cmp"-style comparison value: + + 1 if row_a > row_b + 0 if row_a == row_b + -1 if row_b < row_a + + Note that the actual comparison direction depends on the + field definitions of the fields being compared, since + each sort key defines its own sort order. + + :param row_a: The first row to compare + :param row_b: The seconf row to compare + """ + for cnt in range(0, row_a._report._keys_count): + sfa = row_a._sort_fields[cnt] + sfb = row_b._sort_fields[cnt] + if sfa._props.dtype == REP_NUM: + num_a = sfa.sort_value + num_b = sfb.sort_value + if num_a == num_b: + continue + if sfa._props.sort_dir == ASCENDING: + return 1 if num_a > num_b else -1 + else: + return 1 if num_a < num_b else -1 + else: + stra = sfa.sort_value + strb = sfb.sort_value + if stra == strb: + continue + if sfa._props.sort_dir == ASCENDING: + return 1 if stra > strb else -1 + else: + return 1 if stra < strb else -1 + return 0 + + class __RowKey(object): + """__RowKey sort wrapper. + """ + def __init__(self, obj, *args): + """Initialise a new __RowKey object. + + :param obj: The object to be compared + :returns: None + """ + self.obj = obj + + def __lt__(self, other): + """Test if less than. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) < 0 + + def __gt__(self, other): + """Test if greater than. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) > 0 + + def __eq__(self, other): + """Test if equal to. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) == 0 + + def __le__(self, other): + """Test if less than or equal to. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) <= 0 + + def __ge__(self, other): + """Test if greater than or equal to. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) >= 0 + + def __ne__(self, other): + """Test if not equal to. + + :param other: The other object to be compared + """ + return _row_cmp(self.obj, other.obj) != 0 + + return __RowKey + + def _sort_rows(self): + """Sort the rows of this BoomReport. + + Sort this report's rows, according to the configured sort + keys. + + :returns: None + """ + self._rows.sort(key=self.__row_key_fn()) + + def report_object(self, obj): + """Report data for object. + + Add a row of data to this ``BoomReport``. The ``data`` + argument should be an object of the type understood by this + report's fields. It will be passed in turn to each field to + obtain data for the current row. + + :param obj: the object to report on for this row. + """ + if obj is None: + raise ValueError("Cannot report NoneType object.") + + if self._already_reported: + return + + row = BoomRow(self) + fields = self._fields + if self._sort_required: + row._sort_fields = [-1] * self._keys_count + for fp in self._field_properties: + field = BoomField(self, fp) + data = fp.objtype.data_fn(obj) + + if data is None: + raise ValueError("No data assigned to field %s" % + fields[fp.field_num].name) + + try: + fields[fp.field_num].report_fn(field, data) + except ValueError: + raise ValueError("No value assigned to field %s" % + fields[fp.field_num].name) + row.add_field(field) + self._rows.append(row) + + if not self.opts.buffered: + return self.report_output() + + def _output_field(self, field): + """Output field data. + + Generate string data for one field in a report row. + + :field: The field to be output + :returns: The output report string for this field + :rtype: str + """ + fields = self._fields + prefix = self.opts.field_name_prefix + quote = "" if self.opts.unquoted else STANDARD_QUOTE + + if prefix: + field_name = fields[field._props.field_num].name + prefix += "%s%s%s" % (field_name.upper(), STANDARD_PAIR, + STANDARD_QUOTE) + + repstr = field.report_string + width = field._props.width + if self.opts.aligned: + align = field._props.align + if not align: + if field._props.dtype == REP_NUM: + align = ALIGN_RIGHT + else: + align = ALIGN_LEFT + reptuple = (width, width, repstr) + if align == ALIGN_LEFT: + repstr = "%-*.*s" % reptuple + else: + repstr = "%*.*s" % reptuple + + suffix = quote + return prefix + repstr + suffix + + def _output_as_rows(self): + """Output this report in column format. + + Output the data contained in this ``BoomReport`` in column + format, one row per line. If column headings have not been + printed already they will be automatically displayed by this + call. + + :returns: None + """ + for fp in self._field_properties: + if fp.hidden: + for row in self._rows: + row._fields = row._fields[1:] + + fields = self._implicit_fields if fp.implicit else self._fields + line = "" + + if self.opts.headings: + line += fields[fp.field_num].head + self.opts.separator + + for row in self._rows: + field = row._fields[0] + line += self._output_field(field) + line += self.opts.separator + row._fields = row._fields[1:] + + self.opts.report_file.write(line + "\n") + + def _output_as_columns(self): + """Output this report in column format. + + Output the data contained in this ``BoomReport`` in column + format, one row per line. If column headings have not been + printed already they will be automatically displayed by this + call. + + :returns: None + """ + if not self._header_written: + self.__report_headings() + for row in self._rows: + do_field_delim = False + line = "" + for field in row._fields: + if field._props.hidden: + continue + if do_field_delim: + line += self.opts.separator + else: + do_field_delim = True + line += self._output_field(field) + self.opts.report_file.write(line + "\n") + + def report_output(self): + """Output report data. + + Output this report's data to the configured report file, + using the configured output controls and fields. + + On success the number of rows output is returned. On + error an exception is raised. + + :returns: the number of rows of output written. + :rtype: ``int`` + """ + if self._already_reported: + return + if self._field_calc_needed: + self.__recalculate_sha_width() + self.__recalculate_fields() + if self._sort_required: + self._sort_rows() + if self.opts.columns_as_rows: + return self._output_as_rows() + else: + return self._output_as_columns() + + +__all__ = [ + # Module constants + + 'REP_NUM', 'REP_STR', 'REP_SHA', + 'ALIGN_LEFT', 'ALIGN_RIGHT', + 'ASCENDING', 'DESCENDING', + + # Report objects + 'BoomReportOpts', 'BoomReportObjType', 'BoomField', 'BoomFieldType', + 'BoomFieldProperties', 'BoomReport' +] + +# vim: set et ts=4 sw=4 : diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..6e02dee --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,225 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . +# the i18n builder cannot share the environment and doctrees with the others +I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " applehelp to make an Apple Help Book" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " epub3 to make an epub3" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " texinfo to make Texinfo files" + @echo " info to make Texinfo files and run them through makeinfo" + @echo " gettext to make PO message catalogs" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " xml to make Docutils-native XML files" + @echo " pseudoxml to make pseudoxml-XML files for display purposes" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " coverage to run coverage check of the documentation (if enabled)" + @echo " dummy to check syntax errors of document sources" + +.PHONY: clean +clean: + rm -rf $(BUILDDIR)/* + +.PHONY: html +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +.PHONY: dirhtml +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +.PHONY: singlehtml +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +.PHONY: pickle +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +.PHONY: json +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +.PHONY: htmlhelp +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +.PHONY: qthelp +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/Boom.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/Boom.qhc" + +.PHONY: applehelp +applehelp: + $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp + @echo + @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." + @echo "N.B. You won't be able to view it unless you put it in" \ + "~/Library/Documentation/Help or install it in your application" \ + "bundle." + +.PHONY: devhelp +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/Boom" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/Boom" + @echo "# devhelp" + +.PHONY: epub +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +.PHONY: epub3 +epub3: + $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 + @echo + @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." + +.PHONY: latex +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +.PHONY: latexpdf +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: latexpdfja +latexpdfja: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through platex and dvipdfmx..." + $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +.PHONY: text +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +.PHONY: man +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +.PHONY: texinfo +texinfo: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo + @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." + @echo "Run \`make' in that directory to run these through makeinfo" \ + "(use \`make info' here to do that automatically)." + +.PHONY: info +info: + $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo + @echo "Running Texinfo files through makeinfo..." + make -C $(BUILDDIR)/texinfo info + @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." + +.PHONY: gettext +gettext: + $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale + @echo + @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." + +.PHONY: changes +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +.PHONY: linkcheck +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +.PHONY: doctest +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." + +.PHONY: coverage +coverage: + $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage + @echo "Testing of coverage in the sources finished, look at the " \ + "results in $(BUILDDIR)/coverage/python.txt." + +.PHONY: xml +xml: + $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml + @echo + @echo "Build finished. The XML files are in $(BUILDDIR)/xml." + +.PHONY: pseudoxml +pseudoxml: + $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml + @echo + @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +.PHONY: dummy +dummy: + $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy + @echo + @echo "Build finished. Dummy builder generates no files." diff --git a/doc/boom.rst b/doc/boom.rst new file mode 100644 index 0000000..91c2c89 --- /dev/null +++ b/doc/boom.rst @@ -0,0 +1,75 @@ +boom package +============ + +Module contents +--------------- + +.. automodule:: boom + :members: + :special-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + +Submodules +---------- + +boom.bootloader module +---------------------- + +.. automodule:: boom.bootloader + :members: + :special-members: + :private-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + +boom.osprofile module +--------------------- + +.. automodule:: boom.osprofile + :members: + :special-members: + :private-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + + +boom.hostprofile module +----------------------- + +.. automodule:: boom.hostprofile + :members: + :special-members: + :private-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + + +boom.command module +------------------- + +.. automodule:: boom.command + :members: + :special-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + + +boom.report module +------------------ + +.. automodule:: boom.report + :members: + :special-members: + :private-members: + :exclude-members: __dict__, __module__, __weakref__ + :undoc-members: + :show-inheritance: + + + diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..f12214c --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,345 @@ +# -*- coding: utf-8 -*- +# +# Boom documentation build configuration file, created by +# sphinx-quickstart on Wed Jun 14 19:07:51 2017. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import os +import sys + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +sys.path.insert(0, os.path.abspath('..')) +import boom + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.ifconfig', + 'sphinx.ext.viewcode', + 'sphinx.ext.coverage', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The encoding of source files. +# +# source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'Boom' +copyright = u'2017, Bryn M. Reeves' +author = u'Bryn M. Reeves' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = u'0.1' +# The full version, including alpha/beta/rc tags. +release = u'0.1.0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# +# today = '' +# +# Else, today_fmt is used as the format for a strftime call. +# +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +# +# default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +# +# add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +# +# add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +# +# show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +# modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +# keep_warnings = False + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] + +# The name for this set of Sphinx documents. +# " v documentation" by default. +# +# html_title = u'Boom v0.1.0' + +# A shorter title for the navigation bar. Default is the same as html_title. +# +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +# +# html_logo = None + +# The name of an image file (relative to this directory) to use as a favicon of +# the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +# +# html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# +# html_extra_path = [] + +# If not None, a 'Last updated on:' timestamp is inserted at every page +# bottom, using the given strftime format. +# The empty string is equivalent to '%b %d, %Y'. +# +# html_last_updated_fmt = None + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# +# html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +# +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +# +# html_additional_pages = {} + +# If false, no module index is generated. +# +# html_domain_indices = True + +# If false, no index is generated. +# +# html_use_index = True + +# If true, the index is split into individual pages for each letter. +# +# html_split_index = False + +# If true, links to the reST sources are added to the pages. +# +# html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +# +# html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +# +# html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +# +# html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +# html_file_suffix = None + +# Language to be used for generating the HTML full-text search index. +# Sphinx supports the following languages: +# 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' +# 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' +# +# html_search_language = 'en' + +# A dictionary with options for the search language support, empty by default. +# 'ja' uses this config value. +# 'zh' user can custom change `jieba` dictionary path. +# +# html_search_options = {'type': 'default'} + +# The name of a javascript file (relative to the configuration directory) that +# implements a search results scorer. If empty, the default will be used. +# +# html_search_scorer = 'scorer.js' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'Boomdoc' + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'Boom.tex', u'Boom Documentation', + u'Bryn M. Reeves', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +# +# latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# +# latex_use_parts = False + +# If true, show page references after internal links. +# +# latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# +# latex_show_urls = False + +# Documents to append as an appendix to all manuals. +# +# latex_appendices = [] + +# It false, will not define \strong, \code, itleref, \crossref ... but only +# \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added +# packages. +# +# latex_keep_old_macro_names = True + +# If false, no module index is generated. +# +# latex_domain_indices = True + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'boom', u'Boom Documentation', + [author], 1) +] + +# If true, show URL addresses after external links. +# +# man_show_urls = False + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'Boom', u'Boom Documentation', + author, 'Boom', 'One line description of project.', + 'Miscellaneous'), +] + +# Documents to append as an appendix to all manuals. +# +# texinfo_appendices = [] + +# If false, no module index is generated. +# +# texinfo_domain_indices = True + +# How to display URL addresses: 'footnote', 'no', or 'inline'. +# +# texinfo_show_urls = 'footnote' + +# If true, do not generate a @detailmenu in the "Top" node's menu. +# +# texinfo_no_detailmenu = False diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..84c55a7 --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,28 @@ +Boom documentation +================== + +Boom is a boot manager for Linux systems using the +`BootLoader Specification `_. +Boom can create and remove boot entries for the system, or for +snapshots of the system using LVM2, or BTRFS. + +Boom is tested with grub2 and the Red Hat BLS patch but the boot +entries written by boom should be usable with any bootloader that +implements the BLS (for e.g. `systemd-boot `_). + +Contents: + +.. toctree:: + :maxdepth: 2 + + boom + modules + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/doc/modules.rst b/doc/modules.rst new file mode 100644 index 0000000..8ecee9d --- /dev/null +++ b/doc/modules.rst @@ -0,0 +1,7 @@ +Modules +======= + +.. toctree:: + :maxdepth: 4 + + boom diff --git a/etc/default/boom b/etc/default/boom new file mode 100755 index 0000000..cd5f772 --- /dev/null +++ b/etc/default/boom @@ -0,0 +1,3 @@ +BOOM_USE_SUBMENU="yes" +BOOM_SUBMENU_NAME="Snapshots" +BOOM_ENABLE_GRUB="yes" diff --git a/etc/grub.d/42_boom b/etc/grub.d/42_boom new file mode 100755 index 0000000..85611dd --- /dev/null +++ b/etc/grub.d/42_boom @@ -0,0 +1,35 @@ +#!/bin/sh +BOOM_CONFIG="/etc/default/boom" +. $BOOM_CONFIG + +BOOM_USE_SUBMENU="${BOOM_USE_SUBMENU:-yes}" +BOOM_SUBMENU_NAME="${BOOM_SUBMENU_NAME:-Snapshots}" +BOOM_ENABLE_GRUB="${BOOM_ENABLE_GRUB:-no}" + +# Indentation for body of submenu commands +SUBMENU_PREFIX=" " + +INSMOD_CMD="insmod blscfg" +IMPORT_CMD="bls_import" + +# Test whether boom grub menu entries are enabled +if [ "$BOOM_ENABLE_GRUB" = "no" -o "$BOOM_ENABLE_GRUB" = "n" ]; then + exit +fi + +# Do not generate grub configuration unless boom entries have +# been configured. +if [ -z "$(boom list --noheadings)" ]; then + exit +fi + +# Optional submenu support +if [ "$BOOM_USE_SUBMENU" = "yes" -o "$BOOM_SUBMENU_NAME" = "y" ]; then + echo "submenu \"$BOOM_SUBMENU_NAME\" {" + echo "${SUBMENU_PREFIX}${INSMOD_CMD}" + echo "${SUBMENU_PREFIX}${IMPORT_CMD}" + echo "}" +else + echo ${INSMOD_CMD} + echo ${IMPORT_CMD} +fi diff --git a/examples/boom.conf b/examples/boom.conf new file mode 100644 index 0000000..a96e8a0 --- /dev/null +++ b/examples/boom.conf @@ -0,0 +1,8 @@ +[global] +boot_root = /boot +boom_root = %(boot_root)s/boom + +[legacy] +enable = False +format = grub1 +sync = True diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf new file mode 120000 index 0000000..af1ed68 --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf \ No newline at end of file diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-12ce4b8-4.1.1-100.fc24.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-12ce4b8-4.1.1-100.fc24.conf new file mode 120000 index 0000000..70b5b3f --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-12ce4b8-4.1.1-100.fc24.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-12ce4b8-4.1.1-100.fc24.conf \ No newline at end of file diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf new file mode 120000 index 0000000..9e9e8fa --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf \ No newline at end of file diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf new file mode 120000 index 0000000..618fcbb --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf \ No newline at end of file diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf new file mode 120000 index 0000000..a8a13ea --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf \ No newline at end of file diff --git a/examples/entries/611f38fd887d41dea7eb3403b2730a76-f2ebf21-3.10-23.el7.conf b/examples/entries/611f38fd887d41dea7eb3403b2730a76-f2ebf21-3.10-23.el7.conf new file mode 120000 index 0000000..4a62f7d --- /dev/null +++ b/examples/entries/611f38fd887d41dea7eb3403b2730a76-f2ebf21-3.10-23.el7.conf @@ -0,0 +1 @@ +../../tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-f2ebf21-3.10-23.el7.conf \ No newline at end of file diff --git a/examples/entries/fffffffe-9591d36-3.10.1-1.el7.conf b/examples/entries/fffffffe-9591d36-3.10.1-1.el7.conf new file mode 120000 index 0000000..4bcc30e --- /dev/null +++ b/examples/entries/fffffffe-9591d36-3.10.1-1.el7.conf @@ -0,0 +1 @@ +../../tests/loader/entries/fffffffe-9591d36-3.10.1-1.el7.conf \ No newline at end of file diff --git a/examples/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/examples/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 120000 index 0000000..f2f677a --- /dev/null +++ b/examples/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile \ No newline at end of file diff --git a/examples/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/examples/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 120000 index 0000000..0fdcc07 --- /dev/null +++ b/examples/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile \ No newline at end of file diff --git a/examples/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/examples/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 120000 index 0000000..a3d3416 --- /dev/null +++ b/examples/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile \ No newline at end of file diff --git a/examples/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/examples/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 120000 index 0000000..367d689 --- /dev/null +++ b/examples/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile \ No newline at end of file diff --git a/examples/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/examples/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 120000 index 0000000..67f7f22 --- /dev/null +++ b/examples/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile \ No newline at end of file diff --git a/examples/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/examples/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 120000 index 0000000..32f9cd3 --- /dev/null +++ b/examples/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile \ No newline at end of file diff --git a/examples/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/examples/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 120000 index 0000000..b5cfc3a --- /dev/null +++ b/examples/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile \ No newline at end of file diff --git a/examples/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/examples/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 120000 index 0000000..4feaf77 --- /dev/null +++ b/examples/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1 @@ +../../tests/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile \ No newline at end of file diff --git a/man/man5/boom.5 b/man/man5/boom.5 new file mode 100644 index 0000000..d191b02 --- /dev/null +++ b/man/man5/boom.5 @@ -0,0 +1,52 @@ +.TH BOOM 5 "Mar 09 2018" "Linux" "FILE FORMATS MANUAL" + +. +.SH NAME +. +boom.conf \(em boom configuration file +. +.SH SYNOPSIS +\fB/boot/boom/boom.conf\fP +. +.SH DESCRIPTION +\fBboom.conf\fP is loaded during the initialisation of boom and contains +configuration keys affecting boom's global behaviour and the setting of +boom's legacy configuration support. + +The file is structured in \fBINI\fP format: section headers appear in +square brackets and configuration values appear as key-value pairs with +one pair per line. +. +.SH SECTIONS +.TP +.B global +The global section contains the \fBboot_path\fP and \fBboom_path\fP +keys that may be used to override the location of the \fB/boot\fP +mount point and \fB/boot/boom\fP configuration directory respectively. +.TP +.B legacy +The legacy section contains settings to enable and configure support +for non-BLS boot loader configuration formats. + +To enable legacy boot loader support set the \fBenable\fP key to +\fByes\fP or \fBtrue\fP and set the \fBformat\fP key to the required +format (currently only \fbgrub\fP syntax is supported). + +If the value of the \fBsync\fP key is true the legacy configuration +will be automatically written whenever entries are added, removed, or +modified. +. +.SH AUTHORS +. +Bryn M. Reeves +. +.SH SEE ALSO +. +Boom project page: https://github.com/bmr-cymru/boom +.br +Boot to snapshot documentation: https://github.com/bmr-cymru/snapshot-boot-docs +.br +LVM2 resource page: https://www.sourceware.org/lvm2/ +.br +Device-mapper resource page: http://sources.redhat.com/dm/ +.br diff --git a/man/man8/boom.8 b/man/man8/boom.8 new file mode 100644 index 0000000..28bad24 --- /dev/null +++ b/man/man8/boom.8 @@ -0,0 +1,1585 @@ +.TH BOOM 8 "Oct 30 2017" "Linux" "MAINTENANCE COMMANDS" + +.de ARG_CMD_TYPES +. RI [ entry | profile | host ] +.. +. +.de ARG_COMMANDS +. RI [ create | delete | clone | show | list | edit ] +.. +. +. +.de ARG_LEGACY_TYPES +. RI legacy +.. +. +.de ARG_LEGACY_COMMAND +. RI [ write | clear | show ] +.. +. +. +.SH NAME +. +Boom \(em linux boot manager +. +.SH SYNOPSIS +. +.PD 0 +.HP +.B boom +.de CMD_COMMAND +. ad l +. ARG_CMD_TYPES +. ARG_COMMANDS +. ad b +.. +.CMD_COMMAND + +. +.HP +.B boom +.de CMD_LEGACY_COMMAND +. ad l +. ARG_LEGACY_TYPES +. ARG_LEGACY_COMMAND +. ad b +.. +.CMD_LEGACY_COMMAND + +. +.HP +.B boom +.de CMD_ENTRY_CREATE +. ad l +. BR entry +. BR \fBcreate +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --linux +. IR kernel_path ] +. RB [ --initrd +. IR initrd_path ] +. RB [ --btrfs-subvol +. IR subvol ] +. RB [ --add-opts +. IR opts ] +. RB [ --del-opts +. IR opts ] +. ad b +.. +.CMD_ENTRY_CREATE +. +.HP +.B boom +.de CMD_ENTRY_DELETE +. ad l +. BR entry +. BR \fBdelete +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. ad b +.. +.CMD_ENTRY_DELETE +. +.HP +.B boom +.de CMD_ENTRY_CLONE +. ad l +. BR entry +. BR \fBclone +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --linux +. IR kernel_path ] +. RB [ --initrd +. IR initrd_path ] +. RB [ --btrfs-subvol +. IR subvol ] +. RB [ --add-opts +. IR opts ] +. RB [ --del-opts +. IR opts ] +. ad b +.. +.CMD_ENTRY_CLONE +. +.HP +.B boom +.de CMD_ENTRY_LIST +. ad l +. BR entry +. BR \fBlist +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --linux +. IR kernel_path ] +. RB [ --initrd +. IR initrd_path ] +. RB [ --btrfs-subvol +. IR subvol ] +. ad b +.. +.CMD_ENTRY_LIST +. +.HP +.B boom +.de CMD_ENTRY_SHOW +. ad l +. BR entry +. BR \fBshow +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --btrfs-subvol +. IR subvol ] +. ad b +.. +.CMD_ENTRY_SHOW + +. +.HP +.B boom +.de CMD_PROFILE_CREATE +. ad l +. BR profile +. BR \fBcreate +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR short_name ] +. RB [ --os-version +. IR version ] +. RB [ --os-version-id +. IR version_id ] +. BR [ --from-host ] +. RB [ --os-release +. IR os_release ] +. RB [ --uname-pattern +. IR uname_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_PROFILE_CREATE +. +.HP +.B boom +.de CMD_PROFILE_DELETE +. ad l +. BR profile +. BR \fBdelete +. IR [ profile_id ] +. RB [ --profile +. IR os_id ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR short_name ] +. RB [ --os-version +. IR version ] +. RB [ --os-version-id +. IR version_id ] +. BR [ --from-host ] +. RB [ --os-release +. IR os_release ] +. RB [ --uname-pattern +. IR uname_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_PROFILE_DELETE +. +.HP +.B boom +.de CMD_PROFILE_CLONE +. ad l +. BR profile +. BR \fBclone +. IR [ profile_id ] +. RB [ --profile +. IR os_id ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR short_name ] +. RB [ --os-version +. IR version ] +. RB [ --os-version-id +. IR version_id ] +. BR [ --from-host ] +. RB [ --os-release +. IR os_release ] +. RB [ --uname-pattern +. IR uname_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_PROFILE_CLONE +. +.HP +.B boom +.de CMD_PROFILE_LIST +. ad l +. BR profile +. BR \fBlist +. IR [ profile_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. ad b +.. +.CMD_PROFILE_LIST +. +.HP +.B boom +.de CMD_PROFILE_SHOW +. ad l +. BR profile +. BR \fBshow +. IR [ profile_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. ad b +.. +.CMD_PROFILE_SHOW + +. +.HP +.B boom +.de CMD_HOST_CREATE +. ad l +. BR host +. BR \fBcreate +. RB [ --name +. IR name ] +. RB [ --short-name +. IR short_name ] +. RB [ --profile +. IR os_id ] +. RB [ --machine-id +. IR machine_id ] +. RB [ --kernel-pattern +. IR kernel_pattern ] +. RB [ --initramfs-pattern +. IR initramfs_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_HOST_CREATE +. +.HP +.B boom +.de CMD_HOST_DELETE +. ad l +. BR host +. BR \fBdelete +. IR [ host_id ] +. RB [ --host-profile +. IR host_id ] +. ad b +.. +.CMD_HOST_DELETE +. +.HP +.B boom +.de CMD_HOST_CLONE +. ad l +. BR host +. BR \fBclone +. IR [ host_id ] +. RB [ --host-profile +. IR host_id ] +. RB [ --name +. IR name ] +. RB [ --short-name +. IR short_name ] +. RB [ --profile +. IR os_id ] +. RB [ --machine-id +. IR machine_id ] +. RB [ --kernel-pattern +. IR kernel_pattern ] +. RB [ --initramfs-pattern +. IR initramfs_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_HOST_CLONE +. +.HP +.B boom +.de CMD_HOST_EDIT +. ad l +. BR host +. BR \fBedit +. IR [ host_id ] +. RB [ --host-profile +. IR host_id ] +. RB [ --name +. IR name ] +. RB [ --short-name +. IR short_name ] +. RB [ --profile +. IR os_id ] +. RB [ --machine-id +. IR machine_id ] +. RB [ --kernel-pattern +. IR kernel_pattern ] +. RB [ --initramfs-pattern +. IR initramfs_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_HOST_EDIT +. +.HP +.B boom +.de CMD_HOST_LIST +. ad l +. BR host +. BR \fBlist +. IR [ host_id ] +. RB [ --host-profile +. IR host_id ] +. RB [ --name +. IR name ] +. RB [ --short-name +. IR short_name ] +. RB [ --profile +. IR os_id ] +. RB [ --machine-id +. IR machine_id ] +. RB [ --kernel-pattern +. IR kernel_pattern ] +. RB [ --initramfs-pattern +. IR initramfs_pattern ] +. RB [ --lvm-opts +. IR lvm_opts ] +. RB [ --btrfs-opts +. IR btrfs_opts ] +. RB [ --os-options +. IR os_options ] +. ad b +.. +.CMD_HOST_LIST +. +.HP +.B boom +.de CMD_HOST_SHOW +. ad l +. BR host +. BR \fBshow +. IR [ profile_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. ad b +.. +.CMD_HOST_SHOW + +. +.HP +.B boom +.de CMD_LEGACY_WRITE +. ad l +. BR legacy +. BR \fBwrite +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --linux +. IR kernel_path ] +. RB [ --initrd +. IR initrd_path ] +. RB [ --btrfs-subvol +. IR subvol ] +. ad b +.. +.CMD_LEGACY_WRITE +. +.HP +.B boom +.de CMD_LEGACY_CLEAR +. ad l +. BR legacy +. BR \fBclear +. ad b +.. +.CMD_LEGACY_CLEAR +. +.HP +.B boom +.de CMD_LEGACY_SHOW +. ad l +. BR legacy +. BR \fBshow +. IR [ boot_id ] +. RB [ --boot-id +. IR boot_id ] +. RB [ --profile +. IR os_id ] +. RB [ --version +. IR version ] +. RB [ --name +. IR osname ] +. RB [ --short-name +. IR osshortname ] +. RB [ --os-version +. IR version ] +. RB [ --root-device +. IR device ] +. RB [ --root-lv +. IR lv ] +. RB [ --linux +. IR kernel_path ] +. RB [ --initrd +. IR initrd_path ] +. RB [ --btrfs-subvol +. IR subvol ] +. ad b +.. +.CMD_LEGACY_SHOW +. +.PD +.ad b +. +.SH DESCRIPTION +. +Boom is a \fIboot manager\fP for Linux systems using boot loaders that +support the \fBBootLoader Specification\fP for boot entry configuration. + +Boom works best with a BLS compatible boot loader: either the +\fIsystemd-boot\fP project, or \fIGrub2\fP with the `bls` patch. The +\fIgrub2\fP boot loader included in \fBCentOS\fP, \fBFedora\fP and +\fBRed Hat Enterprise Linux\fP include this support. + +Boom also supports writing configuration in legacy boot loader format: +currently the syntax used by the \fBGrub1\fP configuration file is +supported. + +All long options supported by boom may be written with or without +dashes separating words. For example, \fB--boot-id\fP and \fB--bootid\fP +are synonymous. + +.SH OPTIONS +. +.HP +.BR -a | --add-opts +.IR opts +.br +Specify additional boot options for this entry. +. +.HP +.BR -d | --del-opts +.IR opts +.br +Specify boot options to exclude from this entry. +. +.HP +.BR -b | --boot-id | --bootid +.IR boot_id +.br +Specify a boot identifier to operate on. +. +.HP +.BR --boot-dir | --bootdir +.IR path +.br +Specify the location of the /boot file system. Useful for testing or +for accessing boom data from a system image. +. +.HP +.BR -B | --btrfs-subvolme | --btrfssubvolume +.RI [ subvol_path | subvol_id ] +.br +Specify a BTRFS subvolume by its path or identifier. +.br +.HP +.BR --btrfs-opts | --btrfsopts +.IR btrfs_options_template +.br +An OS profile template string for BTRFS boot options. +. +.HP +.BR --debug +.IR debug_flags +.br +A comma-separated list of subsystem names to enable debugging output +for, or 'all' to enable all debugging. The available debug classes +are: profile, entry, command, report. +. +.HP +.BR -e | --efi +.IR efi_image +.br +Specify an EFI application image for a boot entry. +. +.HP +.BR -H | --from-host | --fromhost +.br +When creating a new OS profile, use \fIos-release\fP data from the +running host. +. +.HP +.BR -P | --host-profile +.br +Use the specified host profile for search or create operations. +. +.HP +.BR -i | --initrd +.IR image_path +.br +A Linux initial ramfs image path. +. +.HP +.BR -k | --kernel-pattern | --kernelpattern +.IR pattern +.br +An OS profile template used to generate kernel image paths. +. +.HP +.BR -l | --linux +.IR image_path +.br +A Linux kernel image path. +. +.HP +.BR -L | --root-lv | --rootlv +.IR root_lv +.br +The logical volume containing the root file system for a boot entry. +If \fB--root-lv\fP is given, but \fB--root-device\fP is not, the root +device is assumed to be the specified logical volume. +. +.HP +.BR --lvm-opts +.IR lvm_opts +.br +An OS profile template used to generate LVM2 boot options. +. +.HP +.BR -m | --machine-id | --machineid +.IR machine_id +.br +. +.HP +.BR -n | --name +.IR os_name +.br +The name of a boom operating system profile. +. +.HP +.BR --name-prefixes | --nameprefixes +.br +Add a prefix to report field output names. +. +.HP +.BR --no-headings | --noheadings +.br +Suppress output of report headings. +. +.HP +.BR -o | --options +.IR field_list +.br +Specify which fields to display. +. +.HP +.BR --os-version +.br +The version string of a boom operating system profile. +. +.HP +.BR -O | --sort +.IR key_list +.br +A comma-separated list of sort keys (field names), with an optional +per-field prefix of \fB+\fP or \fB-\fP to force ascending or +descending sort order respectively for that field. +. +.HP +.BR -I | --os-version-id | --osversionid +.IR os_version_id +.br +A boom operating system profile version identifier. +. +.HP +.BR --os-options | --osoptions +.IR options_template +.br +An operating system profile template string used to generate the +kernel command line options string. +. +.HP +.BR --os-release | --osrelease +.IR os_release_path +.br +A path to a file in \fIos-release(5)\fP from which to create a new +operating system profile. +. +.HP +.BR -p | --profile +.IR os_id +.br +The operating system identifier (\fIos_id\fP) of a boom operating +system profile to use for the current operation. Defaults to the +OS profile of the running system if absent. +. +.HP +.BR -r | --root-device | --rootdevice +.IR root_dev +.br +The system root device for a new boot entry. +. +.HP +.BR -R | --initramfs-pattern | --initramfspattern +.IR initramfs_pattern +.br +An OS profile template used to generate initial ramfs image paths. +. +.HP +.BR --rows +.br +Output report columns as rows. +. +.HP +.BR --separator +.IR separator +.br +Report field separator +. +.HP +.BR -s | --short-name | --shortname +.IR short_name +The short name of a boom operating system profile. +. +.HP +.BR -t | --title +.IR entry_title +.br +The title for a new boot entry. +. +.HP +.BR -u | --uname-pattern | --unamepattern +.IR uname_pattern +.br +An uname pattern to match for an operating system profile. +. +.HP +.BR -V | --verbose +.br +Increase verbosity level. Specify multiple times, or set additional +debug classed with \fB--debug\fP to enable more verbose messages. +. +.HP +.BR -v | --version +.IR version +.br +The kernel version of a boom boot entry. +. +.SH OS Profiles and Boot Entries +. +Boom manages boot loader entries for one or more installed operating +systems. Each operating system is identified by an \fBOS Profile\fP +that provides identity information and a set of templates used to +create boot loader entries. + +An OS profile is identified by its \fBos_id\fP, an alphanumeric +string based on an SHA digest of the profile's identity fields. +Identifiers reported in boom command output are automatically +abbreviated to the minimum length required to ensure uniqueness +and this short form may be used in any place where a boom OS +identifier is expected. + +A \fBBoot Entry\fP represents one bootable instance of an installed +operating system: a kernel, optional initial ramfs image, command +line options, and other images or settings required for boot. + +Each boot entry is also identified by a SHA based unique identifier: +the \fBboot_id\fP. An entry's ID is used to select an entry for +display, modification, deletion or other operations. + +Since the boot entry's identifier is based on the boot parameters +used to create the entry, the \fBboot_id\fP will change if an +existing entry is modified (for e.g. with the \fBboom entry edit\fP +command). + +. +.P +.B Host Profiles +.P +. +Host profiles provide an additional mechanism to control boot entry +templates on a per-host basis. A host profile is bound to a specific +\fBmachine_id\fP and is used whenever new boot entries are created for +the corresponding host. + +A host profile can add and delete boot options from the set supplied by +the active \fBOS Profile\fP, or override specific BOS Profile keys +completely. Any keys not set in a host profile are mapped directly to +the original OS profile. + +. +.P +.B Boot Entry Commands +.P +. +.HP +.B boom +.CMD_ENTRY_CREATE +.br +Create a new boot entry using the specified values. + +The title of the new entry must be set with the \fB--title\fP option. + +The kernel version for the new entry is given with \fB--version\fP. +If \fB--version\fP is not present the version is assumed to be that +of the currently running kernel. + +If \fB--profile\fP is given, it specifies the OS identifier of an +existing OS profile to use for the new entry. If \fB--profile\fP is +not given, and a profile exists that matches either the supplied +or detected version then that profile will be automatically used. + +The \fImachine-id\fP of the new entry is automatically set to the +current machine-id (read from /etc/machine-id) unless this is +overridden by the \fB--machine-id\fP switch. + +A root device may be explicitly specified with the \fB--root-device\fP +option or if an LVM2 logical volume is used this may be specified +with \fB--root-lv\fP: in this case the root device is assumed to be +the normal device path of the specified logical volume. + +A BTRFS subvolume may be set by either the subvolume path or subvolume +identifier using the \fB--btrfs-subvol\fP option. + +Additional boot options not defined by the corresponding \fBOsProfile\fP +templates may be specified with \fB--add-opts\fP. Options may also be +removed from the entry using \fB--del-opts\fP (for example to disable +graphical boot or the "quiet" flag for a particular entry). + +The newly created entry and its boot identifier are printed to the +terminal on success: +.br +# +.B boom create --title 'System Snapshot' --root-lv vg00/lvol0 +.br +Created entry with boot_id 14d6b6e: +.br + title System Snapshot +.br + machine-id 611f38fd887d41dea7eb3403b2730a76 +.br + version 4.13.5-200.fc26.x86_64 +.br + linux /vmlinuz-4.13.5-200.fc26.x86_64 +.br + initrd /initramfs-4.13.5-200.fc26.x86_64.img +.br + options BOOT_IMAGE=/vmlinuz-4.13.5-200.fc26.x86_64 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet +.br +. +.HP +.B boom +.CMD_ENTRY_DELETE +.br +Delete the specified boot entry. The entry to delete may be specified +either by its \fBboot identifier\fP, in which case at most one entry +will be removed, or by specifying selection criteria which may match +(and remove) multiple entries in a single operation. + +For example, by giving \fB--version\fP, all entries matching the +specified kernel version can be removed at once. + +On success the number of entries removed is printed to the terminal. +If the \fB--verbose\fP option is given then a report of the entries +removed will also be displayed. +. +.HP +.B boom +.CMD_ENTRY_CLONE +.br +Clone an existing boot entry and modify its configuration. + +The entry to clone must be specified by its \fBboot identifier\fP. +Any remaining command line arguments are taken to be modifications +to the original entry. + +On success the new entry and its boot identifier are printed to the +terminal. +. +.HP +.B boom +.CMD_ENTRY_LIST +.br +Output a tabular report of boot entries. + +Displays a report with one boot entry per line, containing fields +describing the properties of the configured boot entries. + +The list of fields to display is given with \fB--options\fP as a +comma separated list of field names. To obtain a list of available +fields run '\fBboom list -o help\fP'. If the list of fields begins +with the '\fB+\fP' character the specified fields are appended to +the default field list. Otherwise the given list of fields replaces +the default set of report fields. + +Report output may be sorted by multiple user-defined keys using +the \fB--sort\fP option. The option expects a comma separated list +of keys, with optional '\fB+\fP' and '\fB-\fP' prefixes indicating +ascending and descending sort for that field respectively. +. +.HP +.B boom +.CMD_ENTRY_SHOW +.br +Display boot entries matching selection criteria on standard out. + +Boot entries matching the criteria given on the command line are +printed to the terminal in boot loader entry format. +. +.P +.B OS Profile Commands +.P +. +.HP +.B boom +.CMD_PROFILE_CREATE +.br +Create a new OS profile using the specified values. + +A new OS profile can be created either by specifying required values +on the \fBboom\fP command line, or by reading data from either the +hosts's \fIos-release\fP file (at /etc/os-release), or from another +file in \fIos-release\fP format specified on the command line. + +The information read from \fIos-release\fP (or equivalent command line +options) form the profile's identity and are the basis for the profile +OS identifier. + +In addition to the \fIos-release\fP data a new OS profile requires +a uname version string pattern to match, and template values used to +construct boot entries. + +The uname pattern must be given on the \fBprofile create\fP command +line and is a regular expression matching the UTS release +(\fBuname -r\fP) values reported by that distribution. The pattern is +only used to attempt to match unknown boot entries to a valid OS +profile: for example entries that have been manually edited, or that +were created by another tool. + +The \fBboom\fP command provides default templates that are suitable +for most Linux distributions. Alternately, these values may be set +on the command line at the time of profile creation, or modified using +the \fBboom\fP program at a later time. + +To create a profile for the currently running host, use the +\fB--from-host\fP switch. + +To create a profile from a saved \fIos-release\fP file use the +\fB--os-release\fP optiona and give the path to the file to be used. +. +.HP +.B boom +.CMD_PROFILE_DELETE +.br +Delete the specified Os profile or profiles. + +Delete all OS profiles matching the provided selection criteria. If +the \fB--profile\fP option is used to specify an OS identifier then +at most one profile will be removed. + +On success the number of profiles removed is printed to the terminal. +If the \fB--verbose\fP option is given then a report of the profiles +removed will also be displayed. +. +.HP +.B boom +.CMD_PROFILE_CLONE +.br +Clone an existing OS profile and modify its configuration. + +The entry to clone must be specified by its \fBOS identifier\fP. +Any remaining command line arguments are taken to be modifications +to the original entry. + +On success the new entry and its OS identifier are printed to the +terminal. +. +.HP +.B boom +.CMD_PROFILE_LIST +.br +Output a tabular report of OS profiles. + +Displays a report with one OS profile per line, containing fields +describing the properties of the configured OS profiles. + +The list of fields to display is given with \fB--options\fP as a +comma separated list of field names. To obtain a list of available +fields run '\fBboom list -o help\fP'. If the list of fields begins +with the '\fB+\fP' character the specified fields are appended to +the default field list. Otherwise the given list of fields replaces +the default set of report fields. + +Report output may be sorted by multiple user-defined keys using +the \fB--sort\fP option. The option expects a comma separated list +of keys, with optional '\fB+\fP' and '\fB-\fP' prefixes indicating +ascending and descending sort for that field respectively. +. +.HP +.B boom +.CMD_PROFILE_SHOW +.br +Display OS profiles matching selection criteria on standard out. + +OS profiles matching the criteria given on the command line are +printed to the terminal in a compact multi-line format. +.br +. +.P +.B Host Profile Commands +.P + +. +.HP +.B boom +.CMD_HOST_CREATE +.br +Create a new host profile for the specified \fBmachine_id\fP and using +the given profile option arguments. Any \fBOS Profile\fP keys that are +given values will override the values in the underlying profile. +. +.HP +.B boom +.CMD_HOST_DELETE +.br +Delete the specified host profile or profiles. + +Delete all host profiles matching the provided selection criteria. If +the \fB--host-profile\fP option is used to specify an host identifier +then at most one profile will be removed. + +On success the number of profiles removed is printed to the terminal. +If the \fB--verbose\fP option is given then a report of the profiles +removed will also be displayed. +. +.HP +.B boom +.CMD_HOST_CLONE +.br +Clone an existing host profile and modify its configuration. + +The entry to clone must be specified by its \fBhost identifier\fP. +Any remaining command line arguments are taken to be modifications +to the original entry. + +On success the new entry and its host identifier are printed to the +terminal. +. +.HP +.B boom +.CMD_HOST_EDIT +.br +Edit an existing host profile and modify its configuration. + +The entry to edit must be specified by its \fBhost identifier\fP. +Any remaining command line arguments are taken to be modifications +to the original profile. + +On success the new profile and its host identifier are printed to the +terminal. +. +.HP +.B boom +.CMD_HOST_LIST . +Output a tabular report of host profiles. + +Displays a report with one host profile per line, containing fields +describing the properties of the configured host profiles. + +The list of fields to display is given with \fB--options\fP as a comma +separated list of field names. To obtain a list of available fields run +'\fBboom host list -o help\fP'. If the list of fields begins with the +'\fB+\fP' character the specified fields are appended to the default +field list. Otherwise the given list of fields replaces the default set +of report fields. + +Report output may be sorted by multiple user-defined keys using +the \fB--sort\fP option. The option expects a comma separated list +of keys, with optional '\fB+\fP' and '\fB-\fP' prefixes indicating +ascending and descending sort for that field respectively. +.HP +.B boom +.CMD_HOST_SHOW +.br +Display host profiles matching selection criteria on standard out. + +Host profiles matching the criteria given on the command line are +printed to the terminal in a compact multi-line format. + +.SH LEGACY BOOTLOADER FORMATS +Boom is able to write the current set of boot entries into the +configuration file of a legacy boot loader installed on the +system. This may be used either on platforms that do not have +a native bootloader supporting the Boot Loader Specification, +or to allow upgrades and recovery from an installation lacking +BLS support (if the system is updated to a distribution that +does support the BLS boot loader configuration it will be used +automatically when present). + +Legacy support is enabled and configured via the \fBboom.conf(5)\fP +configuration file. +. +.HP +.B boom +.CMD_LEGACY_WRITE +.br +Write out the current set of Boom boot entries in the configured +legacy configuration file. The normal command line selection +options may be used to control the set of entries written to the +file. +. +.HP +.B boom +.CMD_LEGACY_CLEAR +.br +Remove all Boom boot entries from the configured legacy +configuration file. +. +.HP +.B boom +.CMD_LEGACY_SHOW +Display the selected boot entries as they would appear in the +configured legacy boot loader format. The normal command line +selection options may be used to control the set of entries +written to the terminal. +. +.SH REPORT FIELDS +. +The \fBboom\fP report provides several types of field that may be +added to the default field set for either Boot Entry or OS Profile +reports, or used to create custom reports. +. +.SS Boot Parameters +. +Boot parameter fields represent the properties that distinguish +boot entries: the kernel version and root device configuration. +.TP +.B version +The kernel version of this Boot Entry. +.TP +.B rootdev +The root device of this Boot Entry. +.TP +.B rootlv +The root logical volume of this Boot Entry in 'VG/LV' notation. +.TP +.B subvolpath +The BTRFS subvolume path for this Boot Entry. +.TP +.B subvolid +The BTRFS subvolume ID for this BootEntry. +. +.SS Boot Entry fields +. +Boot Entry fields provide information about an entry not specified +by its Boot Parameters, including the title, boot identifier, boot +image locations, and options required to boot the entry. +.TP +.B bootid +Boot identifier. +.TP +.B title +The entry title as displayed in the boot loader. +.TP +.B options +The kernel command line options used to boot this entry. +.TP +.B kernel +The path to the bootable kernel image, relative to the boot loader. +.TP +.B initramfs +The path to the initramfs image, relative to the boot loader. +.TP +.B machineid +The machine-id associated with this Boot Entry. +.TP +.B entrypath +The absolute path to this Boot Entry's on-disk configuration file. +. +.SS OS Profile fields +. +OS Profile fields provide access to the details of a profile's +configuration including identity fields and the template strings +used to generate entries. + +Since each Boot Entry has an attached OS Profile all profile fields +are also available to add to any Boot Entry report. +.TP +.B osid +OS profile identifier. +.TP +.B osname +The name of this OS prorile as read from \fIos-release\fP. +.TP +.B osshortname +The short name of this OS profile as read from \fIos-release\fP. +.TP +.B osversion +The OS version of this OS profile as read from \fIos-release\fP. +.TP +.B osversion_id +The OS version identifier of this OS profile as read from +\fIos-release\fP. +.TP +.B unamepattern +The configured UTS release pattern for this OS profile. +.TP +.B kernelpattern +The configured kernel image template for this OS profile. +.TP +.B initrdpattern +The configured initramfs image template for this OS profile. +.TP +.B lvm2opts +The configured LVM2 root device options template for this OS profile. +.TP +.B btrfsopts +The configured BTRFS root options template for this OS profile. +.TP +.B options +The kernel command line options template for this OS profile. +.TP +.B profilepath +The absolute path to this OS Profile's on-disk configuration file. +. +.SH REPORTING COMMANDS +Both the \fBentry list\fP and \fBprofile list\fP commands use a common +reporting system to display the results of the query. The selection of +fields, and the order in which they are displayed, may be controlled to +produce custom report formats. +.P +Displaying the available boot entry fields +.br +# +.B boom list -o help +.br +Boot loader entries Fields +.br +-------------------------- +.br + bootid - Boot identifier [sha] +.br + title - Entry title [str] +.br + options - Kernel options [str] +.br + kernel - Kernel image [str] +.br + initramfs - Initramfs image [str] +.br + machineid - Machine identifier [sha] +.br + entrypath - On-disk entry path [str] +.P +OS profiles Fields +.br +------------------ +.br + osid - OS identifier [sha] +.br + osname - OS name [str] +.br + osshortname - OS short name [str] +.br + osversion - OS version [str] +.br + osversion_id - Version identifier [str] +.br + unamepattern - UTS name pattern [str] +.br + kernelpattern - Kernel image pattern [str] +.br + initrdpattern - Initrd pattern [str] +.br + lvm2opts - LVM2 options [str] +.br + btrfsopts - BTRFS options [str] +.br + options - Kernel options [str] +.br + profilepath - On-disk profile path [str] +.P +Boot parameters Fields +.br +---------------------- +.br + version - Kernel version [str] +.br + rootdev - Root device [str] +.br + rootlv - Root logical volume [str] +.br + subvolpath - BTRFS subvolume path [str] +.br + subvolid - BTRFS subvolume ID [num] +.P +Displaying the available OS profile fields +.br +# +.B boom profile list -o help +.br +OS profiles Fields +.br +------------------ +.br + osid - OS identifier [sha] +.br + osname - OS name [str] +.br + osshortname - OS short name [str] +.br + osversion - OS version [str] +.br + osversion_id - Version identifier [str] +.br + unamepattern - UTS name pattern [str] +.br + kernelpattern - Kernel image pattern [str] +.br + initrdpattern - Initrd pattern [str] +.br + lvm2opts - LVM2 options [str] +.br + btrfsopts - BTRFS options [str] +.br + options - Kernel options [str] +.br + profilepath - On-disk profile path [str] +.P +Selecting custom fields for the \fBentry list\fP and \fBprofile list\fP +commands +.br +# +.B boom list -o bootid,osname +.br +BootID Name +.br +0d3e547 Fedora +.br +bc18de2 Fedora +.br +576fe39 Fedora +.br +1838f58 Fedora +.br +81520ca Fedora +.br +327e24a Fedora +.P +Adding additional fields to the default set +.br +# +.B boom list -o +options +.br +BootID Version Name RootDevice Options +.br +0d3e547 4.13.5-200.fc26.x86_64 Fedora /dev/mapper/vg_hex-root BOOT_IMAGE=/vmlinuz-4.11.12-100.fc24.x86_64 root=/dev/mapper/vg_hex-root ro rd.lvm.lv=vg_hex/root rhgb quiet rd.auto=1 +.br +bc18de2 4.13.5-200.fc26.x86_64 Fedora /dev/vg_hex/root-snap10 BOOT_IMAGE=/vmlinuz-4.13.5-200.fc26.x86_64 root=/dev/vg_hex/root-snap10 ro rd.lvm.lv=vg_hex/root-snap10 +.br +576fe39 4.13.5-200.fc26.x86_64 Fedora /dev/vg_hex/root BOOT_IMAGE=/vmlinuz-4.13.5-200.fc26.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root +.br +1838f58 4.13.5-200.fc26.x86_64 Fedora /dev/mapper/vg_hex-root BOOT_IMAGE=/vmlinuz-4.11.12-100.fc24.x86_64 root=/dev/mapper/vg_hex-root ro rd.lvm.lv=vg_hex/root rhgb quiet +.br +81520ca 4.13.13-200.fc26.x86_64 Fedora /dev/mapper/vg_hex-root BOOT_IMAGE=/vmlinuz-4.13.5-200.fc26.x86_64 root=/dev/mapper/vg_hex-root ro rd.lvm.lv=vg_hex/root rhgb quiet LANG=en_GB.UTF-8 +.br +327e24a 4.13.5-200.fc26.x86_64 Fedora /dev/vg_hex/root BOOT_IMAGE=%{linux} root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root +.P +Sort operating system profiles by ascending OS name and descending +OS version +.br +# +.B boom profile list -O+osname,-osversion +.br +OsID Name OsVersion +.br +d4439b7 Fedora 26 (Workstation Edition) +.br +9736c34 Fedora 25 (Server Edition) +.br +9cb53dd Fedora 24 (Workstation Edition) +.br +6bf746b Fedora 24 (Server Edition) +.br +b99ea5f Red Hat Enterprise Linux Server 8 (Server) +.br +3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) +.br +c0b921e Red Hat Enterprise Linux Server 7 (Server) +.br +98c3edb Red Hat Enterprise Linux Server 6 (Server) +.br +b730331 Red Hat Enterprise Linux Server 5 (Server) +.br +efd6d41 Red Hat Enterprise Linux Server 4 (Server) +.br +21e37c8 Ubuntu 16.04 LTS (Xenial Xerus) +.P +.SH EXAMPLES +List the available operating system profiles +.br +# +.B boom profile list +.br +OsID Name OsVersion +.br +efd6d41 Red Hat Enterprise Linux Server 4 (Server) +.br +b730331 Red Hat Enterprise Linux Server 5 (Server) +.br +98c3edb Red Hat Enterprise Linux Server 6 (Server) +.br +c0b921e Red Hat Enterprise Linux Server 7 (Server) +.br +3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) +.br +b99ea5f Red Hat Enterprise Linux Server 8 (Server) +.P +List the available boot entries +.br +# +.B boom list +.br +BootID Version Name RootDevice +.br +0d3e547 4.13.5-200.fc26.x86_64 Fedora /dev/mapper/vg00-lvol0 +.br +bc18de2 4.13.5-200.fc26.x86_64 Fedora /dev/vg00/lvol0-snap10 +.br +576fe39 4.13.5-200.fc26.x86_64 Fedora /dev/vg00/lvol0 +.br +f52ba10 4.11.12-100.fc24.x86_64 Fedora /dev/vg00/lvol0-snap +.br +1838f58 4.13.5-200.fc26.x86_64 Fedora /dev/mapper/vg00-lvol0 +.br +81520ca 4.13.13-200.fc26.x86_64 Fedora /dev/mapper/vg00-lvol0 +.br +327e24a 4.13.5-200.fc26.x86_64 Fedora /dev/vg00/lvol0 +.P +Create an OS profile for the running system (using Fedora 26 as an +example) +.br +# +.B boom profile create --from-host --uname-pattern fc26 +.br +Created profile with os_id d4439b7: +.br + OS ID: "d4439b7d2f928c39f1160c0b0291407e5990b9e0", +.br + Name: "Fedora", Short name: "fedora", +.br + Version: "26 (Workstation Edition)", Version ID: "26", +.br + UTS release pattern: "fc26", +.br + Kernel pattern: "/kernel-%{version}", Initramfs pattern: "/initramfs-%{version}.img", +.br + Root options (LVM2): "rd.lvm.lv=%{lvm_root_lv}", +.br + Root options (BTRFS): "rootflags=%{btrfs_subvolume}", +.br + Options: "root=%{root_device} ro %{root_opts}" +.P +Create a new boot entry for a specific OS profile and version +.br +# +.B boom profile list --short-name rhel +.br +OsID Name OsVersion +.br +3fc389b Red Hat Enterprise Linux Server 7.2 (Maipo) +.br +98c3edb Red Hat Enterprise Linux Server 6 (Server) +.br +c0b921e Red Hat Enterprise Linux Server 7 (Server) +.P +# +.B boom create --profile 3fc389b --title \(dqRHEL7 snapshot\(dq --version 3.10-272.el7 --root-lv vg00/lvol0-snap +.br +Created entry with boot_id a5aef11: +.br +title RHEL7 snapshot +.br +machine-id 611f38fd887d41dea7eb3403b2730a76 +.br +version 3.10-272.el7 +.br +linux /boot/vmlinuz-3.10-272.el7 +.br +initrd /boot/initramfs-3.10-272.el7.img +.br +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet +.P +Create a new boot entry for the running system, changing only the root logical volume +.br +# +.B boom create --title Snap1 --root-lv vg00/lvol0-snap1 +.br +Created entry with boot_id e077490: +.br + title Snap1 +.br + machine-id 611f38fd887d41dea7eb3403b2730a76 +.br + version 4.13.13-200.fc26.x86_64 +.br + linux /vmlinuz-4.13.13-200.fc26.x86_64 +.br + initrd /initramfs-4.13.13-200.fc26.x86_64.img +.br + options BOOT_IMAGE=/vmlinuz-4.13.13-200.fc26.x86_64 root=/dev/vg00/lvol0-snap1 ro rd.lvm.lv=vg00/lvol0-snap1 +.P +Delete an entry by its boot identifier +.br +# +.B boom delete --boot-id e077490 +.br +Deleted 1 entry +.P +Delete all entries for the Fedora 24 OS profile +.br +# boom delete --name Fedora --os-version-id 24 +Deleted 4 entries +.P +.SH AUTHORS +. +Bryn M. Reeves +. +.SH SEE ALSO +. +Boom project page: https://github.com/bmr-cymru/boom +.br +Boot to snapshot documentation: https://github.com/bmr-cymru/snapshot-boot-docs +.br +BootLoader Specification: https://systemd.io/BOOT_LOADER_SPECIFICATION +.br +LVM2 resource page: https://www.sourceware.org/lvm2/ +.br +Device-mapper resource page: http://sources.redhat.com/dm/ +.br diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..34bc04f --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +pycodestyle>=2.4.0 +nose>=1.3.7 +coverage>=4.0.3 +Sphinx>=1.3.5 diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2cda1f1 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python +from setuptools import setup + +from boom import __version__ as boom_version + +setup( + name='boom', + version=boom_version, + description=("""The Boom Boot Manager."""), + author='Bryn M. Reeves', + author_email='bmr@redhat.com', + url='https://github.com/bmr-cymru/boom', + license="GPLv2", + test_suite="tests", + scripts=['bin/boom'], + packages=['boom'], +) + + +# vim: set et ts=4 sw=4 : diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..8b56bb3 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,230 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# boom/__init__.py - Boom package initialisation +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from os.path import join, abspath +from os import geteuid, getegid, makedirs +from subprocess import Popen, PIPE +import shutil +import errno + +import boom + +# Root of the testing directory +BOOT_ROOT_TEST = abspath("./tests") + +# Location of the temporary sandbox for test data +SANDBOX_PATH = join(BOOT_ROOT_TEST, "sandbox") + +# Test sandbox functions + +def rm_sandbox(): + """Remove the test sandbox at SANDBOX_PATH. + """ + try: + shutil.rmtree(SANDBOX_PATH) + except OSError as e: + if e.errno != errno.ENOENT: + raise + + +def mk_sandbox(): + """Create a new test sandbox at SANDBOX_PATH. + """ + makedirs(SANDBOX_PATH) + + +def reset_sandbox(): + """Reset the test sandbox at SANDBOX_PATH by removing it and + re-creating the directory. + """ + rm_sandbox() + mk_sandbox() + +def reset_boom_paths(): + """Reset configurable boom module paths to the default test values. + """ + boom.set_boot_path(BOOT_ROOT_TEST) + +# Mock objects + +class MockArgs(object): + """Mock arguments class for testing boom command line infrastructure. + """ + add_opts = "" + architecture = None + boot_id = "12345678" + btrfs_opts = "" + btrfs_subvolume = "23" + command = "" + config = "" + debug = "" + del_opts = "" + efi = "" + efi = "" + expand_variables = False + from_host = "" + grub_arg = "" + grub_class = "" + grub_users = "" + host_id = None + host_name = "" + host_profile = "" + host_profile = "" + id = "" + identifier = "" + initramfs_pattern = "" + initrd = "" + kernel_pattern = "" + label = "" + linux = "" + lvm_opts = "" + machine_id = "" + name = "" + name_prefixes = False + no_dev = False + no_headings = False + options = "" + optional_keys = "" + os_id = "" + os_options = "" + os_release = "" + os_version = "" + os_version_id = "" + profile = "" + root_device = "" + root_lv = "" + rows = False + separator = "" + short_name = "" + sort = "" + title = "" + type = "" + uname_pattern = "" + verbose = 0 + version = "" + +# Cached logical volume to use for tests +_lv_cache = None + +def _root_lv_from_cmdline(): + """Return the root logical volume according to the kernel command + line, or the empty string if no rd.lvm.lv argument is found. + """ + with open("/proc/cmdline", "r") as f: + for line in f.read().splitlines(): + if isinstance(line, bytes): + line = line.decode('utf8', 'ignore') + args = line.split() + for arg in args: + if "rd.lvm.lv" in arg: + (rd, vglv) = arg.split("=") + return "/dev/%s" % vglv + return None + + +def get_logical_volume(): + """Return an extant logical volume path suitable for use for + device presence checks. + + The actual volume returned is unimportant. + + The device is not modified or written to in any way by the + the test suite. + """ + global _lv_cache + if _lv_cache: + return _lv_cache + + if not have_root() or not have_lvm(): + """The LVM2 binary is not present or not usable. Attempt to + guess a usable device name based on the content of the + system kernel command line. + """ + return _root_lv_from_cmdline() + + p = Popen(["lvs", "--noheadings", "-ovgname,name"], stdin=None, + stdout=PIPE, stderr=None, close_fds=True) + out = p.communicate()[0] + lvs = [] + for line in out.splitlines(): + if isinstance(line, bytes): + line = line.decode('utf8', 'ignore') + (vg, lv) = line.strip().split() + if "swap" in lv: + continue + if "root" in lv: + _lv_cache = "/dev/%s/%s" % (vg, lv) + return _lv_cache + lvs.append("/dev/%s/%s" % (vg, lv)) + _lv_cache = lvs[0] + return _lv_cache + + +def get_root_lv(): + """Return the logical volume found by ``get_logical_volume()`` + in LVM VG/LV notation. + """ + lv = get_logical_volume() + return lv[5:] if lv else None + + +def have_root_lv(): + """Return ``True`` if a usable root logical volume is present, + or ``False`` otherwise. + """ + return bool(get_root_lv()) + +# Test predicates + +def have_root(): + """Return ``True`` if the test suite is running as the root user, + and ``False`` otherwise. + """ + return geteuid() == 0 and getegid() == 0 + + +def have_lvm(): + """Return ``True`` if the test suite is running on a system with + at least one logical volume, or ``False`` otherwise. + """ + p = Popen(["lvs", "--noheadings", "-oname"], stdin=None, stdout=PIPE, + stderr=None, close_fds=True) + out = p.communicate()[0] + if len(out.splitlines()): + return True + return False + + +def have_grub1(): + """Return ``True`` if the grub1 bootloader commands are present, + or ``False`` otherwise. + """ + try: + p = Popen(["grub", "--help"], stdin=None, stdout=PIPE, stderr=PIPE, + close_fds=True) + out = p.communicate(input="\n")[0] + return True + except OSError: + return False + + +__all__ = [ + 'BOOT_ROOT_TEST', 'SANDBOX_PATH', + 'rm_sandbox', 'mk_sandbox', 'reset_sandbox', 'reset_boom_paths', + 'get_logical_volume', 'get_root_lv', 'have_root_lv', + 'MockArgs', + 'have_root', 'have_lvm', 'have_grub1' +] + +# vim: set et ts=4 sw=4 : diff --git a/tests/boom/boom.conf b/tests/boom/boom.conf new file mode 100644 index 0000000..10bd30b --- /dev/null +++ b/tests/boom/boom.conf @@ -0,0 +1,8 @@ +[global] +boot_root = tests +boom_root = %(boot_root)s/boom + +[legacy] +enable = False +format = grub1 +sync = True diff --git a/tests/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host b/tests/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host new file mode 100644 index 0000000..659a68e --- /dev/null +++ b/tests/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="1a979bb835db0ce920557cc4acde98e84f671e3b" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="testing" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host b/tests/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host new file mode 100644 index 0000000..c9351cf --- /dev/null +++ b/tests/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host @@ -0,0 +1,9 @@ +BOOM_HOST_ID="2b4048d37f3c42b1d5e2a9ede501b2815fac9c69" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 2b40" diff --git a/tests/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host b/tests/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host new file mode 100644 index 0000000..2ef6a9f --- /dev/null +++ b/tests/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="373ccd1b66af9058ce49f0ba735fe63c50ba1d98" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_HOST_LABEL="ALABEL" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 373c" diff --git a/tests/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host b/tests/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host new file mode 100644 index 0000000..9aae091 --- /dev/null +++ b/tests/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host @@ -0,0 +1,5 @@ +BOOM_HOST_ID="5ebcb1fa34dd831bf37c27caf168695409e2f0b4" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="" diff --git a/tests/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host b/tests/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host new file mode 100644 index 0000000..5566fe2 --- /dev/null +++ b/tests/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host @@ -0,0 +1,7 @@ +BOOM_HOST_ID="cb7b3ebbd37511ed08919f7561c5ae6663ab027f" +BOOM_HOST_NAME="qux.errorists.org" +BOOM_ENTRY_MACHINE_ID="ffffffffffffc" +BOOM_OS_ID="d4439b7" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet qux" +BOOM_HOST_ADD_OPTS="debug" +BOOM_HOST_DEL_OPTS="rhgb quiet" diff --git a/tests/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile b/tests/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile new file mode 100644 index 0000000..a106e89 --- /dev/null +++ b/tests/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="01f4a140de8c7cb9083be599e61d26bdff2c2c97" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="8 (jessie)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="deb8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/tests/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 100644 index 0000000..68b1bdc --- /dev/null +++ b/tests/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="21e37c8002f33c177524192b15d91dc9612343a3" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="16.04 LTS (Xenial Xerus)" +BOOM_OS_VERSION_ID="16.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initrd.img-%{version}" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile b/tests/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile new file mode 100644 index 0000000..d655ae8 --- /dev/null +++ b/tests/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="28851f93a99cad97a0082fcd0270da994b2bff7a" +BOOM_OS_NAME="NewOs" +BOOM_OS_SHORT_NAME="newos" +BOOM_OS_VERSION="1 (Server Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile b/tests/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/tests/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3ee2c8b6e7b5043d305bc2850ba01f4838739a1b" +BOOM_OS_NAME="NoOptions" +BOOM_OS_SHORT_NAME="nooptions" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/tests/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 100644 index 0000000..254caad --- /dev/null +++ b/tests/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7.2 (Maipo)" +BOOM_OS_VERSION_ID="7.2" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class" diff --git a/tests/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile b/tests/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile new file mode 100644 index 0000000..290eb1b --- /dev/null +++ b/tests/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="3fda8a315ae33e869ba1756cce40c7d8c4c24db9" +BOOM_OS_NAME="Blanks" +BOOM_OS_SHORT_NAME="blanks" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" + +BOOM_OS_UNAME_PATTERN="bl1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile b/tests/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile new file mode 100644 index 0000000..a3c0cdf --- /dev/null +++ b/tests/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="418203ef8b710a2dc125676933747d9c893374db" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="17.04 (Zesty Zapus)" +BOOM_OS_VERSION_ID="17.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile b/tests/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile new file mode 100644 index 0000000..53a372b --- /dev/null +++ b/tests/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="6bae4a4aea177592284ef45163428dd58023854d" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="7 (wheezy)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="deb7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile b/tests/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile new file mode 100644 index 0000000..eca86e9 --- /dev/null +++ b/tests/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="6bf746bb7231693b2903585f171e4290ff0602b5" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Server Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class" diff --git a/tests/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile b/tests/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile new file mode 100644 index 0000000..1f3daab --- /dev/null +++ b/tests/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile @@ -0,0 +1,12 @@ +# A profile with comments +BOOM_OS_ID="6cc6727da76d21db7d39e0abf8c265ffb144d6ca" +BOOM_OS_NAME="Comments" +BOOM_OS_SHORT_NAME="comments" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="co1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile b/tests/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile new file mode 100644 index 0000000..139feb7 --- /dev/null +++ b/tests/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="7dbe237be02bc028a95148659c5baaa499259bbc" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.1" +BOOM_OS_VERSION_ID="42.1" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile b/tests/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile new file mode 100644 index 0000000..20858d7 --- /dev/null +++ b/tests/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="81a78ae30161f02d8b2e6092bde6b789d9a3c21b" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="14.04.4 LTS, Trusty Tahr" +BOOM_OS_VERSION_ID="14.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/8896596a45fcc9e36e9c87aee77ab3e422da2635-fedora30.profile b/tests/boom/profiles/8896596a45fcc9e36e9c87aee77ab3e422da2635-fedora30.profile new file mode 100644 index 0000000..558a398 --- /dev/null +++ b/tests/boom/profiles/8896596a45fcc9e36e9c87aee77ab3e422da2635-fedora30.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="8896596a45fcc9e36e9c87aee77ab3e422da2635" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="30 (Workstation Edition)" +BOOM_OS_VERSION_ID="30" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="fc30" diff --git a/tests/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/tests/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 100644 index 0000000..f525c7b --- /dev/null +++ b/tests/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="9736c347ccb724368be04e51bb25687a361e535c" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="25 (Server Edition)" +BOOM_OS_VERSION_ID="25" +BOOM_OS_UNAME_PATTERN="fc25" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class" diff --git a/tests/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile b/tests/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile new file mode 100644 index 0000000..585815c --- /dev/null +++ b/tests/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="987f0daf2d004093d71ca678fea89ef1989695b3" +BOOM_OS_NAME="NoBTRFS" +BOOM_OS_SHORT_NAME="nobtrfs" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nb1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/tests/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 100644 index 0000000..604d4e4 --- /dev/null +++ b/tests/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="98c3edb94b7b3c8c95cb7d93f75693d2b25f764d" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="6 (Server)" +BOOM_OS_VERSION_ID="6" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd_LVM_LV=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="el6" diff --git a/tests/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/tests/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 100644 index 0000000..ab19588 --- /dev/null +++ b/tests/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="9cb53ddda889d6285fd9ab985a4c47025884999f" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Workstation Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="fc24" diff --git a/tests/boom/profiles/README b/tests/boom/profiles/README new file mode 100644 index 0000000..c067249 --- /dev/null +++ b/tests/boom/profiles/README @@ -0,0 +1,8 @@ +Profile directory for the boom unit test suite. + +This directory contains real and mock OsProfile data for use +with the boom test suite. + +For examples of actual Boom operating system profiles that can +be used with the tool please refer to the examples/profiles +directory in the source tree. diff --git a/tests/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile b/tests/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile new file mode 100644 index 0000000..e41e511 --- /dev/null +++ b/tests/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="af602322b29a22d002e6cb7c8762cb198a8ba49e" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.3" +BOOM_OS_VERSION_ID="42.3" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile b/tests/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile new file mode 100644 index 0000000..08a7fe0 --- /dev/null +++ b/tests/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="b730331f540c416bb914418b051e2d8d72d13b32" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="5 (Server)" +BOOM_OS_VERSION_ID="5" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="el5" diff --git a/tests/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile b/tests/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile new file mode 100644 index 0000000..0b991b2 --- /dev/null +++ b/tests/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="b99ea5fd28c19c01ce2081e9158eafa1920fa632" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="8 (Server)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="el8" diff --git a/tests/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile b/tests/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile new file mode 100644 index 0000000..cd82f8b --- /dev/null +++ b/tests/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="bab31a35db247122f05658a2721f53c3aae4a039" +BOOM_OS_NAME="Foo" +BOOM_OS_SHORT_NAME="foo" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fo1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/tests/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 100644 index 0000000..3909f24 --- /dev/null +++ b/tests/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="c0b921ea84dbc5259477cbff7a15b000dd222671" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7 (Server)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="el7" diff --git a/tests/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile b/tests/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile new file mode 100644 index 0000000..510ed50 --- /dev/null +++ b/tests/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d3e1bd82b33d0b1da75f0e893d5a30db8e308b58" +BOOM_OS_NAME="NoLVM" +BOOM_OS_SHORT_NAME="nolvm" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nl1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="None" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/tests/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 100644 index 0000000..afa3f1e --- /dev/null +++ b/tests/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="26 (Workstation Edition)" +BOOM_OS_VERSION_ID="26" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="fc26" diff --git a/tests/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/tests/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 100644 index 0000000..d94a3b6 --- /dev/null +++ b/tests/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1,13 @@ +BOOM_OS_ID="efd6d41ee868310fec02d25925688e4840a7869a" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="4 (Server)" +BOOM_OS_VERSION_ID="4" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" +BOOM_OS_OPTIONAL_KEYS="grub_users grub_arg grub_class id" +BOOM_OS_UNAME_PATTERN="el4" diff --git a/tests/boom_configs/badconfig/boot/boom/boom.conf b/tests/boom_configs/badconfig/boot/boom/boom.conf new file mode 100644 index 0000000..46d1896 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/boom.conf @@ -0,0 +1 @@ +SKZZERT. diff --git a/tests/boom_configs/badconfig/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host b/tests/boom_configs/badconfig/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host new file mode 100644 index 0000000..659a68e --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="1a979bb835db0ce920557cc4acde98e84f671e3b" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="testing" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host b/tests/boom_configs/badconfig/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host new file mode 100644 index 0000000..c9351cf --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host @@ -0,0 +1,9 @@ +BOOM_HOST_ID="2b4048d37f3c42b1d5e2a9ede501b2815fac9c69" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 2b40" diff --git a/tests/boom_configs/badconfig/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host b/tests/boom_configs/badconfig/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host new file mode 100644 index 0000000..2ef6a9f --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="373ccd1b66af9058ce49f0ba735fe63c50ba1d98" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_HOST_LABEL="ALABEL" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 373c" diff --git a/tests/boom_configs/badconfig/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host b/tests/boom_configs/badconfig/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host new file mode 100644 index 0000000..9aae091 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host @@ -0,0 +1,5 @@ +BOOM_HOST_ID="5ebcb1fa34dd831bf37c27caf168695409e2f0b4" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="" diff --git a/tests/boom_configs/badconfig/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host b/tests/boom_configs/badconfig/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host new file mode 100644 index 0000000..5566fe2 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host @@ -0,0 +1,7 @@ +BOOM_HOST_ID="cb7b3ebbd37511ed08919f7561c5ae6663ab027f" +BOOM_HOST_NAME="qux.errorists.org" +BOOM_ENTRY_MACHINE_ID="ffffffffffffc" +BOOM_OS_ID="d4439b7" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet qux" +BOOM_HOST_ADD_OPTS="debug" +BOOM_HOST_DEL_OPTS="rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile b/tests/boom_configs/badconfig/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile new file mode 100644 index 0000000..a106e89 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="01f4a140de8c7cb9083be599e61d26bdff2c2c97" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="8 (jessie)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="deb8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/tests/boom_configs/badconfig/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 100644 index 0000000..68b1bdc --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="21e37c8002f33c177524192b15d91dc9612343a3" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="16.04 LTS (Xenial Xerus)" +BOOM_OS_VERSION_ID="16.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initrd.img-%{version}" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile new file mode 100644 index 0000000..d655ae8 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="28851f93a99cad97a0082fcd0270da994b2bff7a" +BOOM_OS_NAME="NewOs" +BOOM_OS_SHORT_NAME="newos" +BOOM_OS_VERSION="1 (Server Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3ee2c8b6e7b5043d305bc2850ba01f4838739a1b" +BOOM_OS_NAME="NoOptions" +BOOM_OS_SHORT_NAME="nooptions" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/tests/boom_configs/badconfig/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 100644 index 0000000..2ce9eea --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7.2 (Maipo)" +BOOM_OS_VERSION_ID="7.2" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile new file mode 100644 index 0000000..290eb1b --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="3fda8a315ae33e869ba1756cce40c7d8c4c24db9" +BOOM_OS_NAME="Blanks" +BOOM_OS_SHORT_NAME="blanks" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" + +BOOM_OS_UNAME_PATTERN="bl1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile b/tests/boom_configs/badconfig/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile new file mode 100644 index 0000000..a3c0cdf --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="418203ef8b710a2dc125676933747d9c893374db" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="17.04 (Zesty Zapus)" +BOOM_OS_VERSION_ID="17.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile b/tests/boom_configs/badconfig/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile new file mode 100644 index 0000000..53a372b --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="6bae4a4aea177592284ef45163428dd58023854d" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="7 (wheezy)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="deb7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile b/tests/boom_configs/badconfig/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile new file mode 100644 index 0000000..bec22a9 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile @@ -0,0 +1,12 @@ +# A profile with comments and blank lines +BOOM_OS_ID="6bf746bb7231693b2903585f171e4290ff0602b5" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Server Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile new file mode 100644 index 0000000..1f3daab --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile @@ -0,0 +1,12 @@ +# A profile with comments +BOOM_OS_ID="6cc6727da76d21db7d39e0abf8c265ffb144d6ca" +BOOM_OS_NAME="Comments" +BOOM_OS_SHORT_NAME="comments" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="co1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile new file mode 100644 index 0000000..139feb7 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="7dbe237be02bc028a95148659c5baaa499259bbc" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.1" +BOOM_OS_VERSION_ID="42.1" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile b/tests/boom_configs/badconfig/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile new file mode 100644 index 0000000..20858d7 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="81a78ae30161f02d8b2e6092bde6b789d9a3c21b" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="14.04.4 LTS, Trusty Tahr" +BOOM_OS_VERSION_ID="14.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/tests/boom_configs/badconfig/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 100644 index 0000000..1cad8f6 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="9736c347ccb724368be04e51bb25687a361e535c" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="25 (Server Edition)" +BOOM_OS_VERSION_ID="25" +BOOM_OS_UNAME_PATTERN="fc25" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile new file mode 100644 index 0000000..585815c --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="987f0daf2d004093d71ca678fea89ef1989695b3" +BOOM_OS_NAME="NoBTRFS" +BOOM_OS_SHORT_NAME="nobtrfs" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nb1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/tests/boom_configs/badconfig/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 100644 index 0000000..9e1a449 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="98c3edb94b7b3c8c95cb7d93f75693d2b25f764d" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="6 (Server)" +BOOM_OS_VERSION_ID="6" +BOOM_OS_UNAME_PATTERN="el6" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd_LVM_LV=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/tests/boom_configs/badconfig/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 100644 index 0000000..af22497 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="9cb53ddda889d6285fd9ab985a4c47025884999f" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Workstation Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/README b/tests/boom_configs/badconfig/boot/boom/profiles/README new file mode 100644 index 0000000..c067249 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/README @@ -0,0 +1,8 @@ +Profile directory for the boom unit test suite. + +This directory contains real and mock OsProfile data for use +with the boom test suite. + +For examples of actual Boom operating system profiles that can +be used with the tool please refer to the examples/profiles +directory in the source tree. diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile b/tests/boom_configs/badconfig/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile new file mode 100644 index 0000000..e41e511 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="af602322b29a22d002e6cb7c8762cb198a8ba49e" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.3" +BOOM_OS_VERSION_ID="42.3" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile b/tests/boom_configs/badconfig/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile new file mode 100644 index 0000000..f3b5492 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b730331f540c416bb914418b051e2d8d72d13b32" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="5 (Server)" +BOOM_OS_VERSION_ID="5" +BOOM_OS_UNAME_PATTERN="el5" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile b/tests/boom_configs/badconfig/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile new file mode 100644 index 0000000..c63fe3d --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b99ea5fd28c19c01ce2081e9158eafa1920fa632" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="8 (Server)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="el8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile new file mode 100644 index 0000000..cd82f8b --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="bab31a35db247122f05658a2721f53c3aae4a039" +BOOM_OS_NAME="Foo" +BOOM_OS_SHORT_NAME="foo" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fo1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/tests/boom_configs/badconfig/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 100644 index 0000000..a28fbcc --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="c0b921ea84dbc5259477cbff7a15b000dd222671" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7 (Server)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile b/tests/boom_configs/badconfig/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile new file mode 100644 index 0000000..510ed50 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d3e1bd82b33d0b1da75f0e893d5a30db8e308b58" +BOOM_OS_NAME="NoLVM" +BOOM_OS_SHORT_NAME="nolvm" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nl1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="None" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/tests/boom_configs/badconfig/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 100644 index 0000000..eb17953 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="26 (Workstation Edition)" +BOOM_OS_VERSION_ID="26" +BOOM_OS_UNAME_PATTERN="fc26" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/badconfig/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/tests/boom_configs/badconfig/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 100644 index 0000000..27c2b84 --- /dev/null +++ b/tests/boom_configs/badconfig/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="efd6d41ee868310fec02d25925688e4840a7869a" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="4 (Server)" +BOOM_OS_VERSION_ID="4" +BOOM_OS_UNAME_PATTERN="el4" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/boom.conf b/tests/boom_configs/boom_other/boot/boom_other/boom.conf new file mode 100644 index 0000000..bf7d1c5 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/boom.conf @@ -0,0 +1,8 @@ +[global] +boot_root = /home/breeves/src/git/boom/tests +boom_root = %(boot_root)s/boom + +[legacy] +enable = False +format = grub1 +sync = True diff --git a/tests/boom_configs/boom_other/boot/boom_other/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host b/tests/boom_configs/boom_other/boot/boom_other/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host new file mode 100644 index 0000000..659a68e --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="1a979bb835db0ce920557cc4acde98e84f671e3b" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="testing" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host b/tests/boom_configs/boom_other/boot/boom_other/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host new file mode 100644 index 0000000..c9351cf --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host @@ -0,0 +1,9 @@ +BOOM_HOST_ID="2b4048d37f3c42b1d5e2a9ede501b2815fac9c69" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 2b40" diff --git a/tests/boom_configs/boom_other/boot/boom_other/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host b/tests/boom_configs/boom_other/boot/boom_other/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host new file mode 100644 index 0000000..2ef6a9f --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="373ccd1b66af9058ce49f0ba735fe63c50ba1d98" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_HOST_LABEL="ALABEL" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 373c" diff --git a/tests/boom_configs/boom_other/boot/boom_other/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host b/tests/boom_configs/boom_other/boot/boom_other/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host new file mode 100644 index 0000000..9aae091 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host @@ -0,0 +1,5 @@ +BOOM_HOST_ID="5ebcb1fa34dd831bf37c27caf168695409e2f0b4" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="" diff --git a/tests/boom_configs/boom_other/boot/boom_other/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host b/tests/boom_configs/boom_other/boot/boom_other/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host new file mode 100644 index 0000000..5566fe2 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host @@ -0,0 +1,7 @@ +BOOM_HOST_ID="cb7b3ebbd37511ed08919f7561c5ae6663ab027f" +BOOM_HOST_NAME="qux.errorists.org" +BOOM_ENTRY_MACHINE_ID="ffffffffffffc" +BOOM_OS_ID="d4439b7" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet qux" +BOOM_HOST_ADD_OPTS="debug" +BOOM_HOST_DEL_OPTS="rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile new file mode 100644 index 0000000..a106e89 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="01f4a140de8c7cb9083be599e61d26bdff2c2c97" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="8 (jessie)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="deb8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 100644 index 0000000..68b1bdc --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="21e37c8002f33c177524192b15d91dc9612343a3" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="16.04 LTS (Xenial Xerus)" +BOOM_OS_VERSION_ID="16.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initrd.img-%{version}" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile new file mode 100644 index 0000000..d655ae8 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="28851f93a99cad97a0082fcd0270da994b2bff7a" +BOOM_OS_NAME="NewOs" +BOOM_OS_SHORT_NAME="newos" +BOOM_OS_VERSION="1 (Server Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3ee2c8b6e7b5043d305bc2850ba01f4838739a1b" +BOOM_OS_NAME="NoOptions" +BOOM_OS_SHORT_NAME="nooptions" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 100644 index 0000000..2ce9eea --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7.2 (Maipo)" +BOOM_OS_VERSION_ID="7.2" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile new file mode 100644 index 0000000..290eb1b --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="3fda8a315ae33e869ba1756cce40c7d8c4c24db9" +BOOM_OS_NAME="Blanks" +BOOM_OS_SHORT_NAME="blanks" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" + +BOOM_OS_UNAME_PATTERN="bl1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile new file mode 100644 index 0000000..a3c0cdf --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="418203ef8b710a2dc125676933747d9c893374db" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="17.04 (Zesty Zapus)" +BOOM_OS_VERSION_ID="17.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile new file mode 100644 index 0000000..53a372b --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="6bae4a4aea177592284ef45163428dd58023854d" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="7 (wheezy)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="deb7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile new file mode 100644 index 0000000..bec22a9 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile @@ -0,0 +1,12 @@ +# A profile with comments and blank lines +BOOM_OS_ID="6bf746bb7231693b2903585f171e4290ff0602b5" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Server Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile new file mode 100644 index 0000000..1f3daab --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile @@ -0,0 +1,12 @@ +# A profile with comments +BOOM_OS_ID="6cc6727da76d21db7d39e0abf8c265ffb144d6ca" +BOOM_OS_NAME="Comments" +BOOM_OS_SHORT_NAME="comments" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="co1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile new file mode 100644 index 0000000..139feb7 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="7dbe237be02bc028a95148659c5baaa499259bbc" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.1" +BOOM_OS_VERSION_ID="42.1" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile new file mode 100644 index 0000000..20858d7 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="81a78ae30161f02d8b2e6092bde6b789d9a3c21b" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="14.04.4 LTS, Trusty Tahr" +BOOM_OS_VERSION_ID="14.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 100644 index 0000000..1cad8f6 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="9736c347ccb724368be04e51bb25687a361e535c" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="25 (Server Edition)" +BOOM_OS_VERSION_ID="25" +BOOM_OS_UNAME_PATTERN="fc25" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile new file mode 100644 index 0000000..585815c --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="987f0daf2d004093d71ca678fea89ef1989695b3" +BOOM_OS_NAME="NoBTRFS" +BOOM_OS_SHORT_NAME="nobtrfs" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nb1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 100644 index 0000000..9e1a449 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="98c3edb94b7b3c8c95cb7d93f75693d2b25f764d" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="6 (Server)" +BOOM_OS_VERSION_ID="6" +BOOM_OS_UNAME_PATTERN="el6" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd_LVM_LV=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 100644 index 0000000..af22497 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="9cb53ddda889d6285fd9ab985a4c47025884999f" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Workstation Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/README b/tests/boom_configs/boom_other/boot/boom_other/profiles/README new file mode 100644 index 0000000..c067249 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/README @@ -0,0 +1,8 @@ +Profile directory for the boom unit test suite. + +This directory contains real and mock OsProfile data for use +with the boom test suite. + +For examples of actual Boom operating system profiles that can +be used with the tool please refer to the examples/profiles +directory in the source tree. diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile new file mode 100644 index 0000000..e41e511 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="af602322b29a22d002e6cb7c8762cb198a8ba49e" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.3" +BOOM_OS_VERSION_ID="42.3" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile new file mode 100644 index 0000000..f3b5492 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b730331f540c416bb914418b051e2d8d72d13b32" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="5 (Server)" +BOOM_OS_VERSION_ID="5" +BOOM_OS_UNAME_PATTERN="el5" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile new file mode 100644 index 0000000..c63fe3d --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b99ea5fd28c19c01ce2081e9158eafa1920fa632" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="8 (Server)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="el8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile new file mode 100644 index 0000000..cd82f8b --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="bab31a35db247122f05658a2721f53c3aae4a039" +BOOM_OS_NAME="Foo" +BOOM_OS_SHORT_NAME="foo" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fo1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 100644 index 0000000..a28fbcc --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="c0b921ea84dbc5259477cbff7a15b000dd222671" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7 (Server)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile new file mode 100644 index 0000000..510ed50 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d3e1bd82b33d0b1da75f0e893d5a30db8e308b58" +BOOM_OS_NAME="NoLVM" +BOOM_OS_SHORT_NAME="nolvm" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nl1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="None" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 100644 index 0000000..eb17953 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="26 (Workstation Edition)" +BOOM_OS_VERSION_ID="26" +BOOM_OS_UNAME_PATTERN="fc26" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/boom_other/boot/boom_other/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/tests/boom_configs/boom_other/boot/boom_other/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 100644 index 0000000..27c2b84 --- /dev/null +++ b/tests/boom_configs/boom_other/boot/boom_other/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="efd6d41ee868310fec02d25925688e4840a7869a" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="4 (Server)" +BOOM_OS_VERSION_ID="4" +BOOM_OS_UNAME_PATTERN="el4" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/boom.conf b/tests/boom_configs/default/boot/boom/boom.conf new file mode 100644 index 0000000..bf7d1c5 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/boom.conf @@ -0,0 +1,8 @@ +[global] +boot_root = /home/breeves/src/git/boom/tests +boom_root = %(boot_root)s/boom + +[legacy] +enable = False +format = grub1 +sync = True diff --git a/tests/boom_configs/default/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host b/tests/boom_configs/default/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host new file mode 100644 index 0000000..659a68e --- /dev/null +++ b/tests/boom_configs/default/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="1a979bb835db0ce920557cc4acde98e84f671e3b" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="testing" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host b/tests/boom_configs/default/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host new file mode 100644 index 0000000..c9351cf --- /dev/null +++ b/tests/boom_configs/default/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host @@ -0,0 +1,9 @@ +BOOM_HOST_ID="2b4048d37f3c42b1d5e2a9ede501b2815fac9c69" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 2b40" diff --git a/tests/boom_configs/default/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host b/tests/boom_configs/default/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host new file mode 100644 index 0000000..2ef6a9f --- /dev/null +++ b/tests/boom_configs/default/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="373ccd1b66af9058ce49f0ba735fe63c50ba1d98" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_HOST_LABEL="ALABEL" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 373c" diff --git a/tests/boom_configs/default/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host b/tests/boom_configs/default/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host new file mode 100644 index 0000000..9aae091 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host @@ -0,0 +1,5 @@ +BOOM_HOST_ID="5ebcb1fa34dd831bf37c27caf168695409e2f0b4" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="" diff --git a/tests/boom_configs/default/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host b/tests/boom_configs/default/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host new file mode 100644 index 0000000..5566fe2 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host @@ -0,0 +1,7 @@ +BOOM_HOST_ID="cb7b3ebbd37511ed08919f7561c5ae6663ab027f" +BOOM_HOST_NAME="qux.errorists.org" +BOOM_ENTRY_MACHINE_ID="ffffffffffffc" +BOOM_OS_ID="d4439b7" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet qux" +BOOM_HOST_ADD_OPTS="debug" +BOOM_HOST_DEL_OPTS="rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile b/tests/boom_configs/default/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile new file mode 100644 index 0000000..a106e89 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="01f4a140de8c7cb9083be599e61d26bdff2c2c97" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="8 (jessie)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="deb8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/tests/boom_configs/default/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 100644 index 0000000..68b1bdc --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="21e37c8002f33c177524192b15d91dc9612343a3" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="16.04 LTS (Xenial Xerus)" +BOOM_OS_VERSION_ID="16.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initrd.img-%{version}" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile b/tests/boom_configs/default/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile new file mode 100644 index 0000000..d655ae8 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="28851f93a99cad97a0082fcd0270da994b2bff7a" +BOOM_OS_NAME="NewOs" +BOOM_OS_SHORT_NAME="newos" +BOOM_OS_VERSION="1 (Server Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile b/tests/boom_configs/default/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3ee2c8b6e7b5043d305bc2850ba01f4838739a1b" +BOOM_OS_NAME="NoOptions" +BOOM_OS_SHORT_NAME="nooptions" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/default/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/tests/boom_configs/default/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 100644 index 0000000..2ce9eea --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7.2 (Maipo)" +BOOM_OS_VERSION_ID="7.2" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile b/tests/boom_configs/default/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile new file mode 100644 index 0000000..290eb1b --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="3fda8a315ae33e869ba1756cce40c7d8c4c24db9" +BOOM_OS_NAME="Blanks" +BOOM_OS_SHORT_NAME="blanks" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" + +BOOM_OS_UNAME_PATTERN="bl1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile b/tests/boom_configs/default/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile new file mode 100644 index 0000000..a3c0cdf --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="418203ef8b710a2dc125676933747d9c893374db" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="17.04 (Zesty Zapus)" +BOOM_OS_VERSION_ID="17.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile b/tests/boom_configs/default/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile new file mode 100644 index 0000000..53a372b --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="6bae4a4aea177592284ef45163428dd58023854d" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="7 (wheezy)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="deb7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile b/tests/boom_configs/default/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile new file mode 100644 index 0000000..bec22a9 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile @@ -0,0 +1,12 @@ +# A profile with comments and blank lines +BOOM_OS_ID="6bf746bb7231693b2903585f171e4290ff0602b5" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Server Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile b/tests/boom_configs/default/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile new file mode 100644 index 0000000..1f3daab --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile @@ -0,0 +1,12 @@ +# A profile with comments +BOOM_OS_ID="6cc6727da76d21db7d39e0abf8c265ffb144d6ca" +BOOM_OS_NAME="Comments" +BOOM_OS_SHORT_NAME="comments" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="co1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile b/tests/boom_configs/default/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile new file mode 100644 index 0000000..139feb7 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="7dbe237be02bc028a95148659c5baaa499259bbc" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.1" +BOOM_OS_VERSION_ID="42.1" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile b/tests/boom_configs/default/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile new file mode 100644 index 0000000..20858d7 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="81a78ae30161f02d8b2e6092bde6b789d9a3c21b" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="14.04.4 LTS, Trusty Tahr" +BOOM_OS_VERSION_ID="14.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/tests/boom_configs/default/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 100644 index 0000000..1cad8f6 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="9736c347ccb724368be04e51bb25687a361e535c" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="25 (Server Edition)" +BOOM_OS_VERSION_ID="25" +BOOM_OS_UNAME_PATTERN="fc25" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile b/tests/boom_configs/default/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile new file mode 100644 index 0000000..585815c --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="987f0daf2d004093d71ca678fea89ef1989695b3" +BOOM_OS_NAME="NoBTRFS" +BOOM_OS_SHORT_NAME="nobtrfs" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nb1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/default/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/tests/boom_configs/default/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 100644 index 0000000..9e1a449 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="98c3edb94b7b3c8c95cb7d93f75693d2b25f764d" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="6 (Server)" +BOOM_OS_VERSION_ID="6" +BOOM_OS_UNAME_PATTERN="el6" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd_LVM_LV=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/tests/boom_configs/default/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 100644 index 0000000..af22497 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="9cb53ddda889d6285fd9ab985a4c47025884999f" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Workstation Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" diff --git a/tests/boom_configs/default/boot/boom/profiles/README b/tests/boom_configs/default/boot/boom/profiles/README new file mode 100644 index 0000000..c067249 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/README @@ -0,0 +1,8 @@ +Profile directory for the boom unit test suite. + +This directory contains real and mock OsProfile data for use +with the boom test suite. + +For examples of actual Boom operating system profiles that can +be used with the tool please refer to the examples/profiles +directory in the source tree. diff --git a/tests/boom_configs/default/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile b/tests/boom_configs/default/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile new file mode 100644 index 0000000..e41e511 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="af602322b29a22d002e6cb7c8762cb198a8ba49e" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.3" +BOOM_OS_VERSION_ID="42.3" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile b/tests/boom_configs/default/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile new file mode 100644 index 0000000..f3b5492 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b730331f540c416bb914418b051e2d8d72d13b32" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="5 (Server)" +BOOM_OS_VERSION_ID="5" +BOOM_OS_UNAME_PATTERN="el5" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile b/tests/boom_configs/default/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile new file mode 100644 index 0000000..c63fe3d --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b99ea5fd28c19c01ce2081e9158eafa1920fa632" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="8 (Server)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="el8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile b/tests/boom_configs/default/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile new file mode 100644 index 0000000..cd82f8b --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="bab31a35db247122f05658a2721f53c3aae4a039" +BOOM_OS_NAME="Foo" +BOOM_OS_SHORT_NAME="foo" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fo1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/boom_configs/default/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/tests/boom_configs/default/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 100644 index 0000000..a28fbcc --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="c0b921ea84dbc5259477cbff7a15b000dd222671" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7 (Server)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile b/tests/boom_configs/default/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile new file mode 100644 index 0000000..510ed50 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d3e1bd82b33d0b1da75f0e893d5a30db8e308b58" +BOOM_OS_NAME="NoLVM" +BOOM_OS_SHORT_NAME="nolvm" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nl1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="None" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/boom_configs/default/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/tests/boom_configs/default/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 100644 index 0000000..eb17953 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="26 (Workstation Edition)" +BOOM_OS_VERSION_ID="26" +BOOM_OS_UNAME_PATTERN="fc26" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_configs/default/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/tests/boom_configs/default/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 100644 index 0000000..27c2b84 --- /dev/null +++ b/tests/boom_configs/default/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="efd6d41ee868310fec02d25925688e4840a7869a" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="4 (Server)" +BOOM_OS_VERSION_ID="4" +BOOM_OS_UNAME_PATTERN="el4" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/boom_tests.py b/tests/boom_tests.py new file mode 100644 index 0000000..f2cc08b --- /dev/null +++ b/tests/boom_tests.py @@ -0,0 +1,279 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# boom_tests.py - Boom module tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import unittest +import logging +import boom +from sys import stdout +from os.path import abspath + +from tests import * + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +BOOT_ROOT_TEST = abspath("./tests") +# Override default BOOT_ROOT. +boom.set_boot_path(BOOT_ROOT_TEST) + + +class BoomTests(unittest.TestCase): + # Module tests + def test_import(self): + import boom + + # Helper routine tests + + def test_parse_name_value_default(self): + # Test each allowed quoting style + nvp = "n=v" + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = "n='v'" + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = 'n="v"' + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = 'n = "v"' + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + + # Assert that a comment following a value is permitted, with or + # without intervening whitespace. + nvp = 'n=v # Qux.' + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v ") + nvp = 'n=v#Qux.' + (name, value) = boom.parse_name_value(nvp) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + + # Assert that a malformed nvp raises ValueError + with self.assertRaises(ValueError) as cm: + nvp = "n v" + (name, value) = boom.parse_name_value(nvp) + with self.assertRaises(ValueError) as cm: + nvp = "n==v" + (name, value) = boom.parse_name_value(nvp) + with self.assertRaises(ValueError) as cm: + nvp = "n+=v" + (name, value) = boom.parse_name_value(nvp) + + # Test that values with embedded assignment are accepted + (name, value) = boom.parse_name_value('n=v=v1') + self.assertEqual(value, "v=v1") + + def test_parse_name_value_whitespace(self): + # Test each allowed quoting style + nvp = "n v" + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = "n 'v'" + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = 'n "v"' + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + nvp = 'n "v"' + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + + # Assert that a comment following a value is permitted, with or + # without intervening whitespace. Trailing whitespace is + # included in the parsed value. + nvp = 'n v # Qux.' + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v ") + nvp = 'n v#Qux.' + (name, value) = boom.parse_name_value(nvp, separator=None) + self.assertEqual(name, "n") + self.assertEqual(value, "v") + + # Assert that a malformed nvp raises ValueError + with self.assertRaises(ValueError) as cm: + nvp = "n=v" + (name, value) = boom.parse_name_value(nvp, separator=None) + with self.assertRaises(ValueError) as cm: + nvp = "n==v" + (name, value) = boom.parse_name_value(nvp, separator=None) + with self.assertRaises(ValueError) as cm: + nvp = "n+=v" + (name, value) = boom.parse_name_value(nvp, separator=None) + + # Test that values with embedded assignment are accepted + (name, value) = boom.parse_name_value('n v=v1', separator=None) + self.assertEqual(value, "v=v1") + + def test_blank_or_comment(self): + self.assertTrue(boom.blank_or_comment("")) + self.assertTrue(boom.blank_or_comment("# this is a comment")) + self.assertFalse(boom.blank_or_comment("THIS_IS_NOT=foo")) + + def test_set_debug_mask(self): + boom.set_debug_mask(boom.BOOM_DEBUG_ALL) + + def test_set_debug_mask_bad_mask(self): + with self.assertRaises(ValueError) as cm: + boom.set_debug_mask(boom.BOOM_DEBUG_ALL + 1) + + def test_BoomLogger(self): + bl = boom.BoomLogger("boom", 0) + bl.debug("debug") + + def test_BoomLogger_set_debug_mask(self): + bl = boom.BoomLogger("boom", 0) + bl.set_debug_mask(boom.BOOM_DEBUG_ALL) + + def test_BoomLogger_set_debug_mask_bad_mask(self): + bl = boom.BoomLogger("boom", 0) + with self.assertRaises(ValueError) as cm: + bl.set_debug_mask(boom.BOOM_DEBUG_ALL + 1) + + def test_BoomLogger_debug_masked(self): + bl = boom.BoomLogger("boom", 0) + boom.set_debug_mask(boom.BOOM_DEBUG_ALL) + bl.set_debug_mask(boom.BOOM_DEBUG_ENTRY) + bl.debug_masked("qux") + + def test_BoomConfig__str__(self): + bc = boom.BoomConfig(boot_path="/boot", legacy_enable=False) + xstr = ('[defaults]\nboot_path = /boot\nboom_path = /boot/boom\n\n' + '[legacy]\nenable = False\nformat = grub1\nsync = True') + self.assertEqual(str(bc), xstr) + + def test_BoomConfig__repr__(self): + bc = boom.BoomConfig(boot_path="/boot", legacy_enable=False) + xrepr = ('BoomConfig(boot_path="/boot",boom_path="/boot/boom",' + 'enable_legacy=False,legacy_format="grub1",' + 'legacy_sync=True)') + self.assertEqual(repr(bc), xrepr) + + def test_set_boom_config(self): + bc = boom.BoomConfig(boot_path="/boot", legacy_enable=False) + boom.set_boom_config(bc) + + def test_set_boom_config_bad_config(self): + class Qux(object): + pass + + with self.assertRaises(TypeError) as cm: + boom.set_boom_config(None) + + with self.assertRaises(TypeError) as cm: + boom.set_boom_config(Qux()) + + def test_parse_btrfs_subvol(self): + self.assertEqual("23", boom.parse_btrfs_subvol("23")) + self.assertEqual("/svol", boom.parse_btrfs_subvol("/svol")) + self.assertEqual(None, boom.parse_btrfs_subvol(None)) + + def test_parse_btrfs_subvol_bad_subvol(self): + with self.assertRaises(ValueError) as cm: + boom.parse_btrfs_subvol("foo23foo") + boom.set_boom_path("loader") + + def test_Selection_from_cmd_args_subvol_id(self): + cmd_args = MockArgs() + s = boom.Selection.from_cmd_args(cmd_args) + self.assertEqual(s.btrfs_subvol_id, "23") + self.assertEqual(s.boot_id, "12345678") + + def test_Selection_from_cmd_args_subvol(self): + cmd_args = MockArgs() + cmd_args.btrfs_subvolume = "/svol" + s = boom.Selection.from_cmd_args(cmd_args) + self.assertEqual(s.btrfs_subvol_path, "/svol") + + def test_Selection_from_cmd_args_root_lv(self): + cmd_args = MockArgs() + cmd_args.root_lv = "vg00/lvol0" + s = boom.Selection.from_cmd_args(cmd_args) + self.assertEqual(s.lvm_root_lv, "vg00/lvol0") + + def test_Selection_from_cmd_args_no_btrfs(self): + cmd_args = MockArgs() + cmd_args.btrfs_subvolume = "" + cmd_args.root_lv = "vg00/lvol0" + s = boom.Selection.from_cmd_args(cmd_args) + self.assertEqual(s.lvm_root_lv, "vg00/lvol0") + + def test_Selection_invalid_selection(self): + # A boot_id is invalid for an OsProfile select + s = boom.Selection(boot_id="12345678") + with self.assertRaises(ValueError) as cm: + s.check_valid_selection(profile=True) + + def test_Selection_is_null(self): + s = boom.Selection() + self.assertTrue(s.is_null()) + + def test_Selection_is_non_null(self): + s = boom.Selection(boot_id="1") + self.assertFalse(s.is_null()) + + +class BoomPathTests(unittest.TestCase): + def setUp(self): + # Set up required test paths + pass + + def tearDown(self): + # Clean up test sandbox + pass + + def test_set_boot_path(self): + boom.set_boot_path(BOOT_ROOT_TEST) + + def test_set_boot_path_bad_path(self): + with self.assertRaises(ValueError) as cm: + boom.set_boot_path("/the/wrong/path") + + def test_set_boom_path(self): + boom.set_boom_path(BOOT_ROOT_TEST + "/boom") + + def test_set_boom_path_bad_path(self): + with self.assertRaises(ValueError) as cm: + boom.set_boom_path("/the/wrong/path") + + def test_set_boom_path_non_abs(self): + boom.set_boot_path(BOOT_ROOT_TEST) + boom.set_boom_path("boom/") + + def test_set_boom_path_non_abs_bad(self): + boom.set_boot_path(BOOT_ROOT_TEST + "/boom") + with self.assertRaises(ValueError) as cm: + boom.set_boom_path("absolutely/the/wrong/path") + + def test_set_boot_path_non_abs(self): + with self.assertRaises(ValueError) as cm: + boom.set_boot_path("absolutely/the/wrong/path") + + def test_set_boom_path_no_profiles(self): + boom.set_boot_path(BOOT_ROOT_TEST) + with self.assertRaises(ValueError) as cm: + boom.set_boom_path("loader") + +# vim: set et ts=4 sw=4 : diff --git a/tests/bootloader_configs/boom_off/boot/grub2/grub.cfg b/tests/bootloader_configs/boom_off/boot/grub2/grub.cfg new file mode 100644 index 0000000..4e94cc8 --- /dev/null +++ b/tests/bootloader_configs/boom_off/boot/grub2/grub.cfg @@ -0,0 +1,10 @@ +# +# Fake grub.cfg for check_bootloader() tests. +# + +### BEGIN /etc/grub.d/42_boom ### +submenu "Snapshots" { + insmod blscfg + bls_import +} +### END /etc/grub.d/42_boom ### diff --git a/tests/bootloader_configs/boom_off/etc/default/boom b/tests/bootloader_configs/boom_off/etc/default/boom new file mode 100755 index 0000000..dfb766c --- /dev/null +++ b/tests/bootloader_configs/boom_off/etc/default/boom @@ -0,0 +1,3 @@ +BOOM_USE_SUBMENU="yes" +BOOM_SUBMENU_NAME="Snapshots" +BOOM_ENABLE_GRUB="no" diff --git a/tests/bootloader_configs/boom_off/etc/grub.d/42_boom b/tests/bootloader_configs/boom_off/etc/grub.d/42_boom new file mode 100755 index 0000000..85611dd --- /dev/null +++ b/tests/bootloader_configs/boom_off/etc/grub.d/42_boom @@ -0,0 +1,35 @@ +#!/bin/sh +BOOM_CONFIG="/etc/default/boom" +. $BOOM_CONFIG + +BOOM_USE_SUBMENU="${BOOM_USE_SUBMENU:-yes}" +BOOM_SUBMENU_NAME="${BOOM_SUBMENU_NAME:-Snapshots}" +BOOM_ENABLE_GRUB="${BOOM_ENABLE_GRUB:-no}" + +# Indentation for body of submenu commands +SUBMENU_PREFIX=" " + +INSMOD_CMD="insmod blscfg" +IMPORT_CMD="bls_import" + +# Test whether boom grub menu entries are enabled +if [ "$BOOM_ENABLE_GRUB" = "no" -o "$BOOM_ENABLE_GRUB" = "n" ]; then + exit +fi + +# Do not generate grub configuration unless boom entries have +# been configured. +if [ -z "$(boom list --noheadings)" ]; then + exit +fi + +# Optional submenu support +if [ "$BOOM_USE_SUBMENU" = "yes" -o "$BOOM_SUBMENU_NAME" = "y" ]; then + echo "submenu \"$BOOM_SUBMENU_NAME\" {" + echo "${SUBMENU_PREFIX}${INSMOD_CMD}" + echo "${SUBMENU_PREFIX}${IMPORT_CMD}" + echo "}" +else + echo ${INSMOD_CMD} + echo ${IMPORT_CMD} +fi diff --git a/tests/bootloader_configs/boom_on/boot/boom/boom.conf b/tests/bootloader_configs/boom_on/boot/boom/boom.conf new file mode 100644 index 0000000..bf7d1c5 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/boom.conf @@ -0,0 +1,8 @@ +[global] +boot_root = /home/breeves/src/git/boom/tests +boom_root = %(boot_root)s/boom + +[legacy] +enable = False +format = grub1 +sync = True diff --git a/tests/bootloader_configs/boom_on/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host b/tests/bootloader_configs/boom_on/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host new file mode 100644 index 0000000..659a68e --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/hosts/1a979bb835db0ce920557cc4acde98e84f671e3b-localhost-testing.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="1a979bb835db0ce920557cc4acde98e84f671e3b" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="testing" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host b/tests/bootloader_configs/boom_on/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host new file mode 100644 index 0000000..c9351cf --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/hosts/2b4048d37f3c42b1d5e2a9ede501b2815fac9c69-localhost.host @@ -0,0 +1,9 @@ +BOOM_HOST_ID="2b4048d37f3c42b1d5e2a9ede501b2815fac9c69" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 2b40" diff --git a/tests/bootloader_configs/boom_on/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host b/tests/bootloader_configs/boom_on/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host new file mode 100644 index 0000000..2ef6a9f --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/hosts/373ccd1b66af9058ce49f0ba735fe63c50ba1d98-localhost-ALABEL.host @@ -0,0 +1,10 @@ +BOOM_HOST_ID="373ccd1b66af9058ce49f0ba735fe63c50ba1d98" +BOOM_HOST_NAME="localhost.localdomain" +BOOM_ENTRY_MACHINE_ID="611f38fd887d41dea7ffffffffffff" +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_HOST_LABEL="ALABEL" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet 373c" diff --git a/tests/bootloader_configs/boom_on/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host b/tests/bootloader_configs/boom_on/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host new file mode 100644 index 0000000..9aae091 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/hosts/badbadbad-5ebcb1fa34dd831bf37c27caf168695409e2f0b4-localhost.host @@ -0,0 +1,5 @@ +BOOM_HOST_ID="5ebcb1fa34dd831bf37c27caf168695409e2f0b4" +BOOM_HOST_NAME="localhost" +BOOM_ENTRY_MACHINE_ID="fffffffffffffff" +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_HOST_LABEL="" diff --git a/tests/bootloader_configs/boom_on/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host b/tests/bootloader_configs/boom_on/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host new file mode 100644 index 0000000..5566fe2 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/hosts/cb7b3ebbd37511ed08919f7561c5ae6663ab027f-qux.host @@ -0,0 +1,7 @@ +BOOM_HOST_ID="cb7b3ebbd37511ed08919f7561c5ae6663ab027f" +BOOM_HOST_NAME="qux.errorists.org" +BOOM_ENTRY_MACHINE_ID="ffffffffffffc" +BOOM_OS_ID="d4439b7" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet qux" +BOOM_HOST_ADD_OPTS="debug" +BOOM_HOST_DEL_OPTS="rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile new file mode 100644 index 0000000..a106e89 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/01f4a140de8c7cb9083be599e61d26bdff2c2c97-debian8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="01f4a140de8c7cb9083be599e61d26bdff2c2c97" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="8 (jessie)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="deb8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile new file mode 100644 index 0000000..68b1bdc --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/21e37c8002f33c177524192b15d91dc9612343a3-ubuntu16.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="21e37c8002f33c177524192b15d91dc9612343a3" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="16.04 LTS (Xenial Xerus)" +BOOM_OS_VERSION_ID="16.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initrd.img-%{version}" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile new file mode 100644 index 0000000..d655ae8 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/28851f93a99cad97a0082fcd0270da994b2bff7a-newos1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="28851f93a99cad97a0082fcd0270da994b2bff7a" +BOOM_OS_NAME="NewOs" +BOOM_OS_SHORT_NAME="newos" +BOOM_OS_VERSION="1 (Server Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile new file mode 100644 index 0000000..b4976af --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/3ee2c8b6e7b5043d305bc2850ba01f4838739a1b-nooptions1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3ee2c8b6e7b5043d305bc2850ba01f4838739a1b" +BOOM_OS_NAME="NoOptions" +BOOM_OS_SHORT_NAME="nooptions" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="no1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile new file mode 100644 index 0000000..2ce9eea --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/3fc389bba581e5b20c6a46c7fc31b04be465e973-rhel7.2.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="3fc389bba581e5b20c6a46c7fc31b04be465e973" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7.2 (Maipo)" +BOOM_OS_VERSION_ID="7.2" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile new file mode 100644 index 0000000..290eb1b --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/3fda8a315ae33e869ba1756cce40c7d8c4c24db9-blanks1.profile @@ -0,0 +1,13 @@ +# A profile with comments and blank lines +BOOM_OS_ID="3fda8a315ae33e869ba1756cce40c7d8c4c24db9" +BOOM_OS_NAME="Blanks" +BOOM_OS_SHORT_NAME="blanks" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" + +BOOM_OS_UNAME_PATTERN="bl1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile new file mode 100644 index 0000000..a3c0cdf --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/418203ef8b710a2dc125676933747d9c893374db-ubuntu17.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="418203ef8b710a2dc125676933747d9c893374db" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="17.04 (Zesty Zapus)" +BOOM_OS_VERSION_ID="17.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile new file mode 100644 index 0000000..53a372b --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/6bae4a4aea177592284ef45163428dd58023854d-debian7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="6bae4a4aea177592284ef45163428dd58023854d" +BOOM_OS_NAME="Debian GNU/Linux" +BOOM_OS_SHORT_NAME="debian" +BOOM_OS_VERSION="7 (wheezy)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="deb7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile new file mode 100644 index 0000000..bec22a9 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/6bf746bb7231693b2903585f171e4290ff0602b5-fedora24.profile @@ -0,0 +1,12 @@ +# A profile with comments and blank lines +BOOM_OS_ID="6bf746bb7231693b2903585f171e4290ff0602b5" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Server Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile new file mode 100644 index 0000000..1f3daab --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/6cc6727da76d21db7d39e0abf8c265ffb144d6ca-comments1.profile @@ -0,0 +1,12 @@ +# A profile with comments +BOOM_OS_ID="6cc6727da76d21db7d39e0abf8c265ffb144d6ca" +BOOM_OS_NAME="Comments" +BOOM_OS_SHORT_NAME="comments" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="co1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile new file mode 100644 index 0000000..139feb7 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/7dbe237be02bc028a95148659c5baaa499259bbc-opensuse42.1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="7dbe237be02bc028a95148659c5baaa499259bbc" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.1" +BOOM_OS_VERSION_ID="42.1" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile new file mode 100644 index 0000000..20858d7 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/81a78ae30161f02d8b2e6092bde6b789d9a3c21b-ubuntu14.04.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="81a78ae30161f02d8b2e6092bde6b789d9a3c21b" +BOOM_OS_NAME="Ubuntu" +BOOM_OS_SHORT_NAME="ubuntu" +BOOM_OS_VERSION="14.04.4 LTS, Trusty Tahr" +BOOM_OS_VERSION_ID="14.04" +BOOM_OS_UNAME_PATTERN="generic" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile new file mode 100644 index 0000000..1cad8f6 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/9736c347ccb724368be04e51bb25687a361e535c-fedora25.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="9736c347ccb724368be04e51bb25687a361e535c" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="25 (Server Edition)" +BOOM_OS_VERSION_ID="25" +BOOM_OS_UNAME_PATTERN="fc25" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile new file mode 100644 index 0000000..585815c --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/987f0daf2d004093d71ca678fea89ef1989695b3-nobtrfs1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="987f0daf2d004093d71ca678fea89ef1989695b3" +BOOM_OS_NAME="NoBTRFS" +BOOM_OS_SHORT_NAME="nobtrfs" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nb1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile new file mode 100644 index 0000000..9e1a449 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/98c3edb94b7b3c8c95cb7d93f75693d2b25f764d-rhel6.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="98c3edb94b7b3c8c95cb7d93f75693d2b25f764d" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="6 (Server)" +BOOM_OS_VERSION_ID="6" +BOOM_OS_UNAME_PATTERN="el6" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd_LVM_LV=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile new file mode 100644 index 0000000..af22497 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/9cb53ddda889d6285fd9ab985a4c47025884999f-fedora24.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="9cb53ddda889d6285fd9ab985a4c47025884999f" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="24 (Workstation Edition)" +BOOM_OS_VERSION_ID="24" +BOOM_OS_UNAME_PATTERN="fc24" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/README b/tests/bootloader_configs/boom_on/boot/boom/profiles/README new file mode 100644 index 0000000..c067249 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/README @@ -0,0 +1,8 @@ +Profile directory for the boom unit test suite. + +This directory contains real and mock OsProfile data for use +with the boom test suite. + +For examples of actual Boom operating system profiles that can +be used with the tool please refer to the examples/profiles +directory in the source tree. diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile new file mode 100644 index 0000000..e41e511 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/af602322b29a22d002e6cb7c8762cb198a8ba49e-opensuse42.3.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="af602322b29a22d002e6cb7c8762cb198a8ba49e" +BOOM_OS_NAME="openSUSE Leap" +BOOM_OS_SHORT_NAME="opensuse" +BOOM_OS_VERSION="42.3" +BOOM_OS_VERSION_ID="42.3" +BOOM_OS_UNAME_PATTERN="default" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile new file mode 100644 index 0000000..f3b5492 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/b730331f540c416bb914418b051e2d8d72d13b32-rhel5.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b730331f540c416bb914418b051e2d8d72d13b32" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="5 (Server)" +BOOM_OS_VERSION_ID="5" +BOOM_OS_UNAME_PATTERN="el5" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile new file mode 100644 index 0000000..c63fe3d --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/b99ea5fd28c19c01ce2081e9158eafa1920fa632-rhel8.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="b99ea5fd28c19c01ce2081e9158eafa1920fa632" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="8 (Server)" +BOOM_OS_VERSION_ID="8" +BOOM_OS_UNAME_PATTERN="el8" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile new file mode 100644 index 0000000..cd82f8b --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/bab31a35db247122f05658a2721f53c3aae4a039-foo1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="bab31a35db247122f05658a2721f53c3aae4a039" +BOOM_OS_NAME="Foo" +BOOM_OS_SHORT_NAME="foo" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fo1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts}" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile new file mode 100644 index 0000000..a28fbcc --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/c0b921ea84dbc5259477cbff7a15b000dd222671-rhel7.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="c0b921ea84dbc5259477cbff7a15b000dd222671" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="7 (Server)" +BOOM_OS_VERSION_ID="7" +BOOM_OS_UNAME_PATTERN="el7" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/c48d3c2d3fe9c53384a99fbd402e2ea5bcba5a49-fedora1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/c48d3c2d3fe9c53384a99fbd402e2ea5bcba5a49-fedora1.profile new file mode 100644 index 0000000..44468bd --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/c48d3c2d3fe9c53384a99fbd402e2ea5bcba5a49-fedora1.profile @@ -0,0 +1,12 @@ +BOOM_OS_ID="c48d3c2d3fe9c53384a99fbd402e2ea5bcba5a49" +BOOM_OS_NAME="Fedora Core" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="1 (Workstation Edition)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="fc1" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" +BOOM_OS_TITLE="%{os_name} %{os_version_id} (%{version})" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile new file mode 100644 index 0000000..510ed50 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/d3e1bd82b33d0b1da75f0e893d5a30db8e308b58-nolvm1.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d3e1bd82b33d0b1da75f0e893d5a30db8e308b58" +BOOM_OS_NAME="NoLVM" +BOOM_OS_SHORT_NAME="nolvm" +BOOM_OS_VERSION="1 (Server)" +BOOM_OS_VERSION_ID="1" +BOOM_OS_UNAME_PATTERN="nl1" +BOOM_OS_KERNEL_PATTERN="/vmlinux-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="None" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile new file mode 100644 index 0000000..eb17953 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/d4439b7d2f928c39f1160c0b0291407e5990b9e0-fedora26.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="d4439b7d2f928c39f1160c0b0291407e5990b9e0" +BOOM_OS_NAME="Fedora" +BOOM_OS_SHORT_NAME="fedora" +BOOM_OS_VERSION="26 (Workstation Edition)" +BOOM_OS_VERSION_ID="26" +BOOM_OS_UNAME_PATTERN="fc26" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="BOOT_IMAGE=%{kernel} root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile b/tests/bootloader_configs/boom_on/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile new file mode 100644 index 0000000..27c2b84 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/boom/profiles/efd6d41ee868310fec02d25925688e4840a7869a-rhel4.profile @@ -0,0 +1,11 @@ +BOOM_OS_ID="efd6d41ee868310fec02d25925688e4840a7869a" +BOOM_OS_NAME="Red Hat Enterprise Linux Server" +BOOM_OS_SHORT_NAME="rhel" +BOOM_OS_VERSION="4 (Server)" +BOOM_OS_VERSION_ID="4" +BOOM_OS_UNAME_PATTERN="el4" +BOOM_OS_KERNEL_PATTERN="/vmlinuz-%{version}" +BOOM_OS_INITRAMFS_PATTERN="/initramfs-%{version}.img" +BOOM_OS_ROOT_OPTS_LVM2="rd.lvm.lv=%{lvm_root_lv}" +BOOM_OS_ROOT_OPTS_BTRFS="rootflags=%{btrfs_subvolume}" +BOOM_OS_OPTIONS="root=%{root_device} ro %{root_opts} rhgb quiet" diff --git a/tests/bootloader_configs/boom_on/boot/grub2/grub.cfg b/tests/bootloader_configs/boom_on/boot/grub2/grub.cfg new file mode 100644 index 0000000..4e94cc8 --- /dev/null +++ b/tests/bootloader_configs/boom_on/boot/grub2/grub.cfg @@ -0,0 +1,10 @@ +# +# Fake grub.cfg for check_bootloader() tests. +# + +### BEGIN /etc/grub.d/42_boom ### +submenu "Snapshots" { + insmod blscfg + bls_import +} +### END /etc/grub.d/42_boom ### diff --git a/tests/bootloader_configs/boom_on/etc/default/boom b/tests/bootloader_configs/boom_on/etc/default/boom new file mode 100755 index 0000000..cd5f772 --- /dev/null +++ b/tests/bootloader_configs/boom_on/etc/default/boom @@ -0,0 +1,3 @@ +BOOM_USE_SUBMENU="yes" +BOOM_SUBMENU_NAME="Snapshots" +BOOM_ENABLE_GRUB="yes" diff --git a/tests/bootloader_configs/boom_on/etc/grub.d/42_boom b/tests/bootloader_configs/boom_on/etc/grub.d/42_boom new file mode 100755 index 0000000..85611dd --- /dev/null +++ b/tests/bootloader_configs/boom_on/etc/grub.d/42_boom @@ -0,0 +1,35 @@ +#!/bin/sh +BOOM_CONFIG="/etc/default/boom" +. $BOOM_CONFIG + +BOOM_USE_SUBMENU="${BOOM_USE_SUBMENU:-yes}" +BOOM_SUBMENU_NAME="${BOOM_SUBMENU_NAME:-Snapshots}" +BOOM_ENABLE_GRUB="${BOOM_ENABLE_GRUB:-no}" + +# Indentation for body of submenu commands +SUBMENU_PREFIX=" " + +INSMOD_CMD="insmod blscfg" +IMPORT_CMD="bls_import" + +# Test whether boom grub menu entries are enabled +if [ "$BOOM_ENABLE_GRUB" = "no" -o "$BOOM_ENABLE_GRUB" = "n" ]; then + exit +fi + +# Do not generate grub configuration unless boom entries have +# been configured. +if [ -z "$(boom list --noheadings)" ]; then + exit +fi + +# Optional submenu support +if [ "$BOOM_USE_SUBMENU" = "yes" -o "$BOOM_SUBMENU_NAME" = "y" ]; then + echo "submenu \"$BOOM_SUBMENU_NAME\" {" + echo "${SUBMENU_PREFIX}${INSMOD_CMD}" + echo "${SUBMENU_PREFIX}${IMPORT_CMD}" + echo "}" +else + echo ${INSMOD_CMD} + echo ${IMPORT_CMD} +fi diff --git a/tests/bootloader_configs/no_boom/boot/grub2/grub.cfg b/tests/bootloader_configs/no_boom/boot/grub2/grub.cfg new file mode 100644 index 0000000..4e94cc8 --- /dev/null +++ b/tests/bootloader_configs/no_boom/boot/grub2/grub.cfg @@ -0,0 +1,10 @@ +# +# Fake grub.cfg for check_bootloader() tests. +# + +### BEGIN /etc/grub.d/42_boom ### +submenu "Snapshots" { + insmod blscfg + bls_import +} +### END /etc/grub.d/42_boom ### diff --git a/tests/bootloader_configs/no_boom/etc/grub.d/42_boom b/tests/bootloader_configs/no_boom/etc/grub.d/42_boom new file mode 100755 index 0000000..85611dd --- /dev/null +++ b/tests/bootloader_configs/no_boom/etc/grub.d/42_boom @@ -0,0 +1,35 @@ +#!/bin/sh +BOOM_CONFIG="/etc/default/boom" +. $BOOM_CONFIG + +BOOM_USE_SUBMENU="${BOOM_USE_SUBMENU:-yes}" +BOOM_SUBMENU_NAME="${BOOM_SUBMENU_NAME:-Snapshots}" +BOOM_ENABLE_GRUB="${BOOM_ENABLE_GRUB:-no}" + +# Indentation for body of submenu commands +SUBMENU_PREFIX=" " + +INSMOD_CMD="insmod blscfg" +IMPORT_CMD="bls_import" + +# Test whether boom grub menu entries are enabled +if [ "$BOOM_ENABLE_GRUB" = "no" -o "$BOOM_ENABLE_GRUB" = "n" ]; then + exit +fi + +# Do not generate grub configuration unless boom entries have +# been configured. +if [ -z "$(boom list --noheadings)" ]; then + exit +fi + +# Optional submenu support +if [ "$BOOM_USE_SUBMENU" = "yes" -o "$BOOM_SUBMENU_NAME" = "y" ]; then + echo "submenu \"$BOOM_SUBMENU_NAME\" {" + echo "${SUBMENU_PREFIX}${INSMOD_CMD}" + echo "${SUBMENU_PREFIX}${IMPORT_CMD}" + echo "}" +else + echo ${INSMOD_CMD} + echo ${IMPORT_CMD} +fi diff --git a/tests/bootloader_configs/no_grub_d/boot/grub2/grub.cfg b/tests/bootloader_configs/no_grub_d/boot/grub2/grub.cfg new file mode 100644 index 0000000..4e94cc8 --- /dev/null +++ b/tests/bootloader_configs/no_grub_d/boot/grub2/grub.cfg @@ -0,0 +1,10 @@ +# +# Fake grub.cfg for check_bootloader() tests. +# + +### BEGIN /etc/grub.d/42_boom ### +submenu "Snapshots" { + insmod blscfg + bls_import +} +### END /etc/grub.d/42_boom ### diff --git a/tests/bootloader_configs/no_grub_d/etc/default/boom b/tests/bootloader_configs/no_grub_d/etc/default/boom new file mode 100755 index 0000000..cd5f772 --- /dev/null +++ b/tests/bootloader_configs/no_grub_d/etc/default/boom @@ -0,0 +1,3 @@ +BOOM_USE_SUBMENU="yes" +BOOM_SUBMENU_NAME="Snapshots" +BOOM_ENABLE_GRUB="yes" diff --git a/tests/bootloader_tests.py b/tests/bootloader_tests.py new file mode 100644 index 0000000..98195a1 --- /dev/null +++ b/tests/bootloader_tests.py @@ -0,0 +1,1175 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# osprofile_tests.py - Boom OS profile tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from sys import stdout +from os import listdir, makedirs, mknod, unlink +from os.path import abspath, exists, join +from stat import S_IFBLK, S_IFCHR +import shutil + +# Test suite paths +from tests import * + +import boom +from boom.bootloader import * +from boom.osprofile import * +from boom.hostprofile import * +from boom import Selection + +# Override default BOOM_ROOT and BOOT_ROOT +# NOTE: with test fixtures that use the sandbox, this path is further +# overridden by the class setUp() method to point to the appropriate +# sandbox location. +boom.set_boot_path(BOOT_ROOT_TEST) + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +_test_osp = None + +class BootParamsTests(unittest.TestCase): + def test_BootParams_no_version_raises(self): + with self.assertRaises(ValueError) as cm: + # A version string is required + bp = BootParams(None) + + def test_BootParams_conflicting_btrfs_raises(self): + with self.assertRaises(ValueError) as cm: + # Only one of subvol_id or subvol_path is allowed + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5", + btrfs_subvol_path="/snapshots/snap-1", + btrfs_subvol_id="232") + + def test_BootParams_plain__str__and__repr__(self): + # Plain root_device + bp = BootParams(version="1.1.1.x86_64", root_device="/dev/sda5") + xstr = "1.1.1.x86_64, root_device=/dev/sda5" + xrepr = 'BootParams("1.1.1.x86_64", root_device="/dev/sda5")' + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + def test_BootParams_lvm__str__and__repr__(self): + # LVM logical volume and no root_device + bp = BootParams(version="1.1.1.x86_64", lvm_root_lv="vg00/lvol0") + xstr = ("1.1.1.x86_64, root_device=/dev/vg00/lvol0, " + "lvm_root_lv=vg00/lvol0") + xrepr = ('BootParams("1.1.1.x86_64", root_device="/dev/vg00/lvol0", ' + 'lvm_root_lv="vg00/lvol0")') + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + # LVM logical volume and root_device override + bp = BootParams(version="1.1.1.x86_64", + root_device="/dev/mapper/vg00-lvol0", + lvm_root_lv="vg00/lvol0") + xstr = ("1.1.1.x86_64, root_device=/dev/mapper/vg00-lvol0, " + "lvm_root_lv=vg00/lvol0") + xrepr = ('BootParams("1.1.1.x86_64", ' + 'root_device="/dev/mapper/vg00-lvol0", ' + 'lvm_root_lv="vg00/lvol0")') + + self.assertFalse(bp.has_btrfs()) + self.assertTrue(bp.has_lvm2()) + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + def test_BootParams_btrfs__str__and__repr__(self): + # BTRFS subvol path and root_device + bp = BootParams(version="1.1.1.x86_64", + root_device="/dev/sda5", + btrfs_subvol_path="/snapshots/snap-1") + xstr = ("1.1.1.x86_64, root_device=/dev/sda5, " + "btrfs_subvol_path=/snapshots/snap-1") + xrepr = ('BootParams("1.1.1.x86_64", root_device="/dev/sda5", ' + 'btrfs_subvol_path="/snapshots/snap-1")') + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + # BTRFS subvol ID and root_device + bp = BootParams(version="1.1.1.x86_64", + root_device="/dev/sda5", + btrfs_subvol_id="232") + xstr = ("1.1.1.x86_64, root_device=/dev/sda5, " + "btrfs_subvol_id=232") + xrepr = ('BootParams("1.1.1.x86_64", root_device="/dev/sda5", ' + 'btrfs_subvol_id="232")') + + self.assertTrue(bp.has_btrfs()) + self.assertFalse(bp.has_lvm2()) + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + def test_BootParams_lvm_btrfs__str__and__repr__(self): + # BTRFS subvol path and LVM root_device + bp = BootParams(version="1.1.1.x86_64", lvm_root_lv="vg00/lvol0", + btrfs_subvol_path="/snapshots/snap-1") + xstr = ("1.1.1.x86_64, root_device=/dev/vg00/lvol0, " + "lvm_root_lv=vg00/lvol0, " + "btrfs_subvol_path=/snapshots/snap-1") + xrepr = ('BootParams("1.1.1.x86_64", root_device="/dev/vg00/lvol0", ' + 'lvm_root_lv="vg00/lvol0", ' + 'btrfs_subvol_path="/snapshots/snap-1")') + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + # BTRFS subvol id and LVM root_device + bp = BootParams(version="1.1.1.x86_64", lvm_root_lv="vg00/lvol0", + btrfs_subvol_id="232") + xstr = ("1.1.1.x86_64, root_device=/dev/vg00/lvol0, " + "lvm_root_lv=vg00/lvol0, btrfs_subvol_id=232") + xrepr = ('BootParams("1.1.1.x86_64", root_device="/dev/vg00/lvol0", ' + 'lvm_root_lv="vg00/lvol0", btrfs_subvol_id="232")') + self.assertEqual(str(bp), xstr) + self.assertEqual(repr(bp), xrepr) + + +def _reset_test_osprofile(): + global _test_osp + # Some tests modify the OsProfile: recycle it each time it is used + if _test_osp: + _test_osp.delete_profile() + osp = OsProfile(name="Distribution", short_name="distro", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "di1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} %{root_opts} rhgb quiet" + _test_osp = osp + + +class MockBootEntry(object): + boot_id = "1234567890abcdef" + version = "1.1.1" + expand_options = "root=/dev/mapper/rhel-root ro rhgb quiet" + _osp = None + _entry_data = {} + + +class BootEntryBasicTests(unittest.TestCase): + """Tests for the BootEntry class that do not depend on external + test data. + """ + # DELETEME + test_version = "1.1.1-1.qux.x86_64" + test_lvm2_root_device = "/dev/vg00/lvol0" + test_lvm_root_lv = "vg00/lvol0" + test_btrfs_root_device = "/dev/sda5" + test_btrfs_subvol_path = "/snapshots/snap1" + test_btrfs_subvol_id = "232" + + # Sandbox paths + + # Master BLS loader directory for sandbox + loader_path = join(BOOT_ROOT_TEST, "loader") + + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + def setUp(self): + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + loader_sandbox = join(SANDBOX_PATH, "boot/loader") + + # Initialise sandbox from master + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + shutil.copytree(self.loader_path, loader_sandbox) + + # Set boom paths + boom.set_boot_path(boot_sandbox) + + # Reset profiles, entries, and host profiles to known state. + load_profiles() + load_entries() + load_host_profiles() + + def tearDown(self): + # Drop any in-memory entries and profiles modified by tests + drop_entries() + drop_profiles() + drop_host_profiles() + + # Clear sandbox data + rm_sandbox() + reset_boom_paths() + + # BootEntry tests + + def test_BootEntry__str__(self): + be = BootEntry(title="title", machine_id="ffffffff", osprofile=None, + allow_no_dev=True) + xstr = ('title title\nmachine-id ffffffff\n' + 'linux /vmlinuz-%{version}\n' + 'initrd /initramfs-%{version}.img') + self.assertEqual(str(be), xstr) + + def test_BootEntry__repr__(self): + be = BootEntry(title="title", machine_id="ffffffff", osprofile=None, + allow_no_dev=True) + xrepr = ('BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", ' + 'BOOM_ENTRY_MACHINE_ID: "ffffffff", ' + 'BOOM_ENTRY_LINUX: "/vmlinuz-%{version}", ' + 'BOOM_ENTRY_INITRD: "/initramfs-%{version}.img", ' + 'BOOM_ENTRY_BOOT_ID: ' + '"40c7c3158e626ed25cc2066b7c308fca0cb57be2"})') + self.assertEqual(repr(be), xrepr) + + def test_BootEntry(self): + # Test BootEntry init from kwargs + with self.assertRaises(ValueError) as cm: + be = BootEntry(title=None, machine_id="ffffffff", osprofile=None, + allow_no_dev=True) + + # Empty machine-id is now allowed for Red Hat BLS entries... + #with self.assertRaises(ValueError) as cm: + # be = BootEntry(title="title", machine_id=None, osprofile=None, + # allow_no_dev=True) + + with self.assertRaises(ValueError) as cm: + be = BootEntry(title=None, machine_id=None, osprofile=None, + allow_no_dev=True) + + be = BootEntry(title="title", machine_id="ffffffff") + + self.assertTrue(be) + + def test_BootEntry_from_entry_data(self): + # Pull in all the BOOM_ENTRY_* constants to the local namespace. + from boom.bootloader import ( + BOOM_ENTRY_TITLE, BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_VERSION, + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, BOOM_ENTRY_INITRD, + BOOM_ENTRY_OPTIONS + ) + with self.assertRaises(ValueError) as cm: + # Missing BOOM_ENTRY_TITLE + be = BootEntry(entry_data={BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_LINUX: "/vmlinuz-1.1.1", + BOOM_ENTRY_INITRD: "/initramfs-1.1.1.img", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro"}) + + # Valid entry + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_LINUX: "/vmlinuz-1.1.1", + BOOM_ENTRY_INITRD: "/initramfs-1.1.1.img", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro"}) + + with self.assertRaises(ValueError) as cm: + # Missing BOOM_ENTRY_LINUX or BOOM_ENTRY_EFI + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_INITRD: "/initramfs-1.1.1.img", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro"}) + + # Valid Linux entry + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_LINUX: "/vmlinuz", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro"}) + + # Valid EFI entry + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_EFI: "/some.efi.thing", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro"}) + + def test_BootEntry_with_boot_params(self): + from boom.bootloader import ( + BOOM_ENTRY_TITLE, BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_VERSION, + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, BOOM_ENTRY_INITRD, + BOOM_ENTRY_OPTIONS + ) + bp = BootParams(version="2.2.2", lvm_root_lv="vg00/lvol0") + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_LINUX: "/vmlinuz-1.1.1", + BOOM_ENTRY_INITRD: "/initramfs-1.1.1.img", + BOOM_ENTRY_OPTIONS: "root=/dev/vg_root/root " + "rd.lvm.lv=vg_root/root"}, + boot_params=bp, allow_no_dev=True) + # boot_params overrides BootEntry + self.assertEqual(be.version, bp.version) + self.assertNotEqual(be.version, "1.1.1") + + def test_BootEntry_empty_osprofile(self): + # Assert that key properties of a BootEntry with no attached osprofile + # return None. + from boom.bootloader import ( + BOOM_ENTRY_TITLE, BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_VERSION, + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, BOOM_ENTRY_INITRD, + BOOM_ENTRY_OPTIONS + ) + bp = BootParams(version="2.2.2", lvm_root_lv="vg00/lvol0") + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_LINUX: "/vmlinuz", + BOOM_ENTRY_VERSION: "1.1.1"}, boot_params=bp, + allow_no_dev=True) + + xoptions = "root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0" + self.assertEqual(be.options, xoptions) + + +class BootEntryTests(unittest.TestCase): + """Tests for the BootEntry class using on-disk entry and profile + data. + """ + test_version = "1.1.1-1.qux.x86_64" + test_lvm2_root_device = "/dev/vg00/lvol0" + test_lvm_root_lv = "vg00/lvol0" + test_btrfs_root_device = "/dev/sda5" + test_btrfs_subvol_path = "/snapshots/snap1" + test_btrfs_subvol_id = "232" + + # Standard test OsProfile. Tests must not modify this. + test_osp = None + + # Standard test BootParams. Tests must not modify this. + test_bp = None + + # Standard test BootEntry. Tests must not modify this. + test_be = None + + # Master BLS loader directory for sandbox + loader_path = join(BOOT_ROOT_TEST, "loader") + + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + # Test fixture init/cleanup + def setUp(self): + """Set up a test fixture for the BootEntryTests class. + + Defines standard OsProfile, BootParams, and BootEntry + objects for use in these tests. + """ + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + loader_sandbox = join(SANDBOX_PATH, "boot/loader") + + # Initialise sandbox from master + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + shutil.copytree(self.loader_path, loader_sandbox) + + # Set boom paths + boom.set_boot_path(boot_sandbox) + + # Load test OsProfile and BootEntry data + load_profiles() + load_entries() + + # Define a new, test OsProfile that is never included in the + # standard set distributed with boom. To be used only for + # formatting BootEntry objects for testing. + osp = OsProfile(name="Distribution", short_name="distro", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "di1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} %{root_opts} rhgb quiet" + self.test_osp = osp + + # Define a standard set of test BootParams + bp = BootParams("1.1.1.fc24", root_device="/dev/vg/lv", + lvm_root_lv="vg/lv") + self.test_bp = bp + + # Define a synthetic BootEntry for testing + be = BootEntry(title="title", machine_id="ffffffff", + boot_params=bp, osprofile=osp, allow_no_dev=True) + self.test_be = be + + def tearDown(self): + """Tear down the standard test profiles and entries used by the + BootEntryTests class. + """ + # Drop any in-memory entries and profiles modified by tests + drop_entries() + drop_profiles() + + # Clear sandbox data + rm_sandbox() + reset_boom_paths() + + self.test_osp = None + self.test_bp = None + + # BootParams recovery tests + def test_BootParams_from_entry_no_opts(self): + osp = self.test_osp + osp.options = "" + + be = MockBootEntry() + be.options = "" + be._osp = osp + + self.assertFalse(BootParams.from_entry(be)) + + def test_BootParams_from_entry_no_root_device(self): + osp = self.test_osp + + be = MockBootEntry() + be.options = "ro rd.lvm.lv=vg00/lvol0 rhgb quiet" + be._osp = osp + + self.assertTrue(BootParams.from_entry(be)) + + def test_BootEntry_empty_format_key(self): + # Assert that key properties of a BootEntry with empty format keys + # return the empty string. + from boom.bootloader import ( + BOOM_ENTRY_TITLE, BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_VERSION, + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, BOOM_ENTRY_INITRD, + BOOM_ENTRY_OPTIONS + ) + + osp = self.test_osp + # Clear the OsProfile.options format key + osp.options = "" + + bp = BootParams(version="2.2.2", lvm_root_lv="vg00/lvol0") + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_LINUX: "/vmlinuz-1.1.1", + BOOM_ENTRY_INITRD: "/initramfs-1.1.1.img"}, + osprofile=osp, boot_params=bp, allow_no_dev=True) + + self.assertEqual(be.options, "") + + def test_BootEntry_write(self): + # Use a real OsProfile here: the entry will be written to disk, and + # may be seen during entry loading (to avoid the entry being moved + # to the Null profile). + osp = find_profiles(Selection(os_id="d4439b7"))[0] + bp = BootParams("1.1.1-1.fc26", root_device="/dev/vg00/lvol0", + lvm_root_lv="vg00/lvol0") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + osprofile=osp, allow_no_dev=True) + + boot_id = be.boot_id + be.write_entry() + load_entries() + be2 = find_entries(Selection(boot_id=boot_id))[0] + self.assertEqual(be.title, be2.title) + self.assertEqual(be.boot_id, be2.boot_id) + self.assertEqual(be.version, be2.version) + ## Create on-disk entry and add to list of known entries + #be.write_entry() + # Profile and entry are non-persistent + be2.delete_entry() + + def test_BootEntry_profile_kernel_version(self): + osp = self.test_osp + be = BootEntry(title="title", machine_id="ffffffff", osprofile=osp) + be.version = "1.1.1-17.qux.x86_64" + self.assertEqual(be.linux, "/vmlinuz-1.1.1-17.qux.x86_64") + self.assertEqual(be.initrd, "/initramfs-1.1.1-17.qux.x86_64.img") + + def test_BootEntry_profile_root_lvm2(self): + osp = self.test_osp + bp = BootParams("1.1", lvm_root_lv="vg00/lvol0") + be = BootEntry(title="title", machine_id="ffffffff", + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(be.root_opts, "rd.lvm.lv=vg00/lvol0") + self.assertEqual(be.options, "root=/dev/vg00/lvol0 " + "rd.lvm.lv=vg00/lvol0 rhgb quiet") + + def test_BootEntry_profile_root_btrfs_id(self): + osp = self.test_osp + bp = BootParams("1.1", root_device="/dev/sda5", btrfs_subvol_id="232") + be = BootEntry(title="title", machine_id="ffffffff", + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(be.root_opts, "rootflags=subvolid=232") + self.assertEqual(be.options, "root=/dev/sda5 " + "rootflags=subvolid=232 rhgb quiet") + + def test_BootEntry_profile_root_btrfs_path(self): + osp = self.test_osp + bp = BootParams("1.1", root_device="/dev/sda5", + btrfs_subvol_path="/snapshots/20170523-1") + be = BootEntry(title="title", machine_id="ffffffff", + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(be.root_opts, + "rootflags=subvol=/snapshots/20170523-1") + self.assertEqual(be.options, "root=/dev/sda5 " + "rootflags=subvol=/snapshots/20170523-1 rhgb quiet") + + def test_BootEntry_boot_id(self): + xboot_id = 'f0a46b7a6e982cab4163af6b45087e87691a0c43' + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + self.assertEqual(xboot_id, be.boot_id) + + def test_BootEntry_root_opts_no_values(self): + from boom.bootloader import ( + BOOM_ENTRY_TITLE, BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_VERSION, + BOOM_ENTRY_LINUX, BOOM_ENTRY_EFI, BOOM_ENTRY_INITRD, + BOOM_ENTRY_OPTIONS + ) + osp = self.test_osp + xroot_opts = "" + + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_LINUX: "/vmlinuz", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_OPTIONS: "root=/dev/sda5 ro" + }, allow_no_dev=True) + + self.assertEqual(xroot_opts, be.root_opts) + + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(entry_data={BOOM_ENTRY_TITLE: "title", + BOOM_ENTRY_LINUX: "/vmlinuz", + BOOM_ENTRY_MACHINE_ID: "ffffffff", + BOOM_ENTRY_VERSION: "1.1.1", + BOOM_ENTRY_OPTIONS: "root=%{root_device} %{root_opts}"}, + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(xroot_opts, be.root_opts) + + # BootEntry properties get/set tests + # Simple properties: direct set to self._entry_data. + def test_BootEntry_options_set_get(self): + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + xoptions = "testoptions root=%{root_device}" + be.options = xoptions + self.assertEqual(xoptions, be.options) + + def test_BootEntry_linux_set_get(self): + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + xlinux = "/vmlinuz" + be.linux = xlinux + self.assertEqual(xlinux, be.linux) + + def test_BootEntry_initrd_set_get(self): + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + xinitrd = "/initrd.img" + be.initrd = xinitrd + self.assertEqual(xinitrd, be.initrd) + + def test_BootEntry_efi_set_get(self): + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + xefi = "/some.efi.img" + be.efi = xefi + self.assertEqual(xefi, be.efi) + + def test_BootEntry_devicetree_set_get(self): + bp = BootParams("1.1.1.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + xdevicetree = "/tegra20-paz00.dtb" + be.devicetree = xdevicetree + self.assertEqual(xdevicetree, be.devicetree) + + def test_BootEntry_optional_keys(self): + osp = self.test_osp + osp.optional_keys = "grub_users grub_arg grub_class" + + bp = BootParams("1.1", lvm_root_lv="vg00/lvol0") + be = BootEntry(title="title", machine_id="ffffffff", + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(be.root_opts, "rd.lvm.lv=vg00/lvol0") + self.assertEqual(be.options, "root=/dev/vg00/lvol0 " + "rd.lvm.lv=vg00/lvol0 rhgb quiet") + + be.grub_users = "test_user" + be.grub_arg = "--test-arg" + be.grub_class = "test_class" + + def test_BootEntry_optional_keys_not_set(self): + osp = self.test_osp + osp.optional_keys = "" + + bp = BootParams("1.1", lvm_root_lv="vg00/lvol0") + be = BootEntry(title="title", machine_id="ffffffff", + osprofile=osp, boot_params=bp, allow_no_dev=True) + self.assertEqual(be.root_opts, "rd.lvm.lv=vg00/lvol0") + self.assertEqual(be.options, "root=/dev/vg00/lvol0 " + "rd.lvm.lv=vg00/lvol0 rhgb quiet") + + with self.assertRaises(ValueError) as cm: + be.grub_users = "test_user" + + def test_match_OsProfile_to_BootEntry(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + xos_id = "6bf746bb7231693b2903585f171e4290ff0602b5" + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + self.assertEqual(be._osp.os_id, xos_id) + + def test_BootEntry__getitem__(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + from boom.bootloader import (BOOM_ENTRY_VERSION, BOOM_ENTRY_TITLE, + BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_LINUX, + BOOM_ENTRY_INITRD, BOOM_ENTRY_OPTIONS, + BOOM_ENTRY_DEVICETREE) + xtitle = "title" + xmachine_id = "ffffffff" + xversion = "4.11.5-100.fc24.x86_64" + xlinux = "/vmlinuz-4.11.5-100.fc24.x86_64" + xinitrd = "/initramfs-4.11.5-100.fc24.x86_64.img" + xoptions = "root=/dev/sda5 ro rhgb quiet" + xdevicetree = "device.tree" + + bp = BootParams(xversion, root_device="/dev/sda5") + be = BootEntry(title=xtitle, machine_id=xmachine_id, boot_params=bp, + allow_no_dev=True) + be.devicetree = xdevicetree + + self.assertEqual(be[BOOM_ENTRY_VERSION], "4.11.5-100.fc24.x86_64") + self.assertEqual(be[BOOM_ENTRY_TITLE], "title") + self.assertEqual(be[BOOM_ENTRY_MACHINE_ID], "ffffffff") + self.assertEqual(be[BOOM_ENTRY_LINUX], xlinux) + self.assertEqual(be[BOOM_ENTRY_INITRD], xinitrd) + self.assertEqual(be[BOOM_ENTRY_OPTIONS], xoptions) + self.assertEqual(be[BOOM_ENTRY_DEVICETREE], xdevicetree) + + def test_BootEntry__getitem__bad_key_raises(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp) + with self.assertRaises(TypeError) as cm: + be[123] + + def test_BootEntry__setitem__(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + from boom.bootloader import (BOOM_ENTRY_VERSION, BOOM_ENTRY_TITLE, + BOOM_ENTRY_MACHINE_ID, BOOM_ENTRY_LINUX, + BOOM_ENTRY_INITRD, BOOM_ENTRY_OPTIONS, + BOOM_ENTRY_DEVICETREE) + + xtitle = "title" + xmachine_id = "ffffffff" + xversion = "4.11.5-100.fc24.x86_64" + xlinux = "/vmlinuz-4.11.5-100.fc24.x86_64" + xinitrd = "/initramfs-4.11.5-100.fc24.x86_64.img" + xoptions = "root=/dev/sda5 ro rhgb quiet" + xdevicetree = "device.tree" + + bp = BootParams(xversion, root_device="/dev/sda5") + be = BootEntry(title="qux", machine_id="11111111", boot_params=bp, + allow_no_dev=True) + be.devicetree = xdevicetree + + be[BOOM_ENTRY_VERSION] = xversion + be[BOOM_ENTRY_TITLE] = xtitle + be[BOOM_ENTRY_MACHINE_ID] = xmachine_id + be[BOOM_ENTRY_LINUX] = xlinux + be[BOOM_ENTRY_INITRD] = xinitrd + be[BOOM_ENTRY_DEVICETREE] = xdevicetree + + self.assertEqual(be.version, "4.11.5-100.fc24.x86_64") + self.assertEqual(be.title, "title") + self.assertEqual(be.machine_id, "ffffffff") + self.assertEqual(be.linux, xlinux) + self.assertEqual(be.initrd, xinitrd) + self.assertEqual(be.options, xoptions) + self.assertEqual(be.devicetree, xdevicetree) + + def test_BootEntry__getitem__bad_key_raises(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + with self.assertRaises(TypeError) as cm: + be[123] = "qux" + + def test_BootEntry_keys(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + xkeys = [ + 'BOOM_ENTRY_TITLE', 'BOOM_ENTRY_MACHINE_ID', + 'BOOM_ENTRY_ARCHITECTURE', 'BOOM_ENTRY_LINUX', 'BOOM_ENTRY_INITRD', + 'BOOM_ENTRY_OPTIONS', 'BOOM_ENTRY_VERSION' + ] + + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + + self.assertEqual(be.keys(), xkeys) + + def test_BootEntry_values(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + xvalues = [ + 'title', + 'ffffffff', + 'x64', + '/vmlinuz-4.11.5-100.fc24.x86_64', + '/initramfs-4.11.5-100.fc24.x86_64.img', + 'root=/dev/sda5 ro rhgb quiet', + '4.11.5-100.fc24.x86_64' + ] + + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + architecture='x64', allow_no_dev=True) + + # Ignore ordering + be_set = set(be.values()) + xvalues_set = set(xvalues) + self.assertEqual(be_set, xvalues_set) + + def test_BootEntry_items(self): + from boom.osprofile import OsProfile, load_profiles + load_profiles() + + os_id = "9cb53ddda889d6285fd9ab985a4c47025884999f" + osp = boom.osprofile.get_os_profile_by_id(os_id) + + xkeys = [ + 'BOOM_ENTRY_TITLE', 'BOOM_ENTRY_MACHINE_ID', + 'BOOM_ENTRY_ARCHITECTURE', 'BOOM_ENTRY_LINUX', + 'BOOM_ENTRY_INITRD', 'BOOM_ENTRY_OPTIONS', 'BOOM_ENTRY_VERSION' + ] + + xvalues = [ + 'title', + 'ffffffff', + '', + '/vmlinuz-4.11.5-100.fc24.x86_64', + '/initramfs-4.11.5-100.fc24.x86_64.img', + 'root=/dev/sda5 ro rhgb quiet', + '4.11.5-100.fc24.x86_64' + ] + + xitems = list(zip(xkeys, xvalues)) + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + osprofile=osp, allow_no_dev=True) + + be_set = set(be.items()) + xitems_set = set(xitems) + self.assertEqual(be_set, xitems_set) + + def test_BootEntry_eq_no_boot_id(self): + class NotABootEntry(object): + i_have_no_boot_id = True + osp = self.test_osp + be = self.test_be + self.assertFalse(be == NotABootEntry()) + + def test__add_entry_loads_entries(self): + boom.bootloader._entries = None + osp = self.test_osp + be = self.test_be + boom.bootloader._add_entry(be) + self.assertTrue(boom.bootloader._entries) + self.assertTrue(boom.osprofile._profiles) + + def test__del_entry_deletes_entry(self): + boom.bootloader.load_entries() + be = boom.bootloader._entries[0] + self.assertTrue(be in boom.bootloader._entries) + boom.bootloader._del_entry(be) + self.assertFalse(be in boom.bootloader._entries) + + def test_load_entries_loads_profiles(self): + import boom.osprofile + boom.osprofile._profiles = [] + boom.osprofile._profiles_by_id = {} + boom.osprofile._profiles = [boom.osprofile.OsProfile("","","","","")] + boom.osprofile._profiles_loaded = False + boom.bootloader.load_entries() + self.assertTrue(boom.osprofile._profiles) + self.assertTrue(boom.bootloader._entries) + + def test_find_entries_loads_entries(self): + boom.bootloader._entries = None + boom.bootloader.find_entries() + self.assertTrue(boom.osprofile._profiles) + self.assertTrue(boom.bootloader._entries) + + def test_find_entries_by_boot_id(self): + boot_id = "12a2696bf85cc33f42f0449fab5da64dac7aa10a" + boom.bootloader._entries = None + bes = boom.bootloader.find_entries(Selection(boot_id=boot_id)) + self.assertEqual(len(bes), 1) + + def test_find_entries_by_title(self): + title = "Red Hat Enterprise Linux 7.2 (Maipo) 3.10-23.el7" + boom.bootloader._entries = None + bes = boom.bootloader.find_entries(Selection(title=title)) + self.assertEqual(len(bes), 1) + + def test_find_entries_by_version(self): + version = "4.10.17-100.fc24.x86_64" + boom.bootloader._entries = None + bes = boom.bootloader.find_entries(Selection(version=version)) + path = boom_entries_path() + nr = len([p for p in listdir(path) if version in p]) + self.assertEqual(len(bes), nr) + + def test_find_entries_by_root_device(self): + entries_path = boom_entries_path() + root_device = "/dev/vg_root/root" + boom.bootloader._entries = None + bes = boom.bootloader.find_entries(Selection(root_device=root_device)) + xentries = 0 + for e in listdir(entries_path): + if e.endswith(".conf"): + with open(join(entries_path, e)) as f: + for l in f.readlines(): + if root_device in l: + xentries +=1 + self.assertEqual(len(bes), xentries) + + def test_find_entries_by_lvm_root_lv(self): + entries_path = boom_entries_path() + boom.bootloader._entries = None + lvm_root_lv = "vg_root/root" + bes = boom.bootloader.find_entries(Selection(lvm_root_lv=lvm_root_lv)) + xentries = 0 + for e in listdir(entries_path): + if e.endswith(".conf"): + with open(join(entries_path, e)) as f: + for l in f.readlines(): + if "rd.lvm.lv=" + lvm_root_lv in l: + xentries +=1 + self.assertEqual(len(bes), xentries) + + def test_find_entries_by_btrfs_subvol_id(self): + entries_path = boom_entries_path() + boom.bootloader._entries = None + btrfs_subvol_id = "23" + nr = 0 + + # count entries + for p in listdir(entries_path): + with open(join(entries_path, p), "r") as f: + for l in f.readlines(): + if "subvolid=23" in l: + nr += 1 + + select = Selection(btrfs_subvol_id=btrfs_subvol_id) + bes = boom.bootloader.find_entries(select) + self.assertEqual(len(bes), nr) + + def test_find_entries_by_btrfs_subvol_path(self): + entries_path = boom_entries_path() + btrfs_subvol_path = "/snapshot/today" + boom.bootloader._entries = None + select = Selection(btrfs_subvol_path=btrfs_subvol_path) + bes = boom.bootloader.find_entries(select) + nr = 0 + + # count entries + for p in listdir(entries_path): + with open(join(entries_path, p), "r") as f: + for l in f.readlines(): + if "/snapshot/today" in l: + nr += 1 + + self.assertEqual(len(bes), nr) + + def test_delete_unwritten_BootEntry_raises(self): + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + with self.assertRaises(ValueError) as cm: + be.delete_entry() + + def test_delete_BootEntry_deletes(self): + bp = BootParams("4.11.5-100.fc24.x86_64", root_device="/dev/sda5") + be = BootEntry(title="title", machine_id="ffffffff", boot_params=bp, + allow_no_dev=True) + be.write_entry() + be.delete_entry() + self.assertFalse(exists(be._entry_path)) + + +class BootLoaderBasicTests(unittest.TestCase): + def test_import(self): + import boom.bootloader + + +class BootLoaderTests(unittest.TestCase): + """Class for bootloader module-level tests. + """ + + # Master BLS loader directory for sandbox + loader_path = join(BOOT_ROOT_TEST, "loader") + + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + # Test fixture init/cleanup + def setUp(self): + """setUp() + Set up a test fixture for the BootEntryTests class. + + Defines standard OsProfile, BootParams, and BootEntry + objects for use in these tests. + """ + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + loader_sandbox = join(SANDBOX_PATH, "boot/loader") + + # Initialise sandbox from master + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + shutil.copytree(self.loader_path, loader_sandbox) + + # Set boom paths + boom.set_boot_path(boot_sandbox) + + # Load test OsProfile and BootEntry data + load_profiles() + load_entries() + + def tearDown(self): + """tearDown() + Tear down the standard test profiles and entries used by the + BootEntryTests class. + """ + # Drop any in-memory entries and profiles modified by tests + drop_entries() + drop_profiles() + + # Clear sandbox data + rm_sandbox() + reset_boom_paths() + + # Module tests + + def _nr_machine_id(self, machine_id): + entries = boom.bootloader._entries + match = [e for e in entries if e.machine_id == machine_id] + return len(match) + + # Profile store tests + + def test_load_entries(self): + # Test that loading the test entries succeeds. + boom.bootloader.load_entries() + entry_count = 0 + for entry in listdir(boom_entries_path()): + if entry.endswith(".conf"): + entry_count += 1 + self.assertEqual(len(boom.bootloader._entries), entry_count) + + def test_load_entries_with_machine_id(self): + # Test that loading the test entries by machine_id succeeds, + # and returns the expected number of profiles. + machine_id = "ffffffff" + boom.bootloader.load_entries(machine_id=machine_id) + entry_count = 0 + for entry in listdir(boom_entries_path()): + if entry.startswith(machine_id) and entry.endswith(".conf"): + entry_count += 1 + self.assertEqual(len(boom.bootloader._entries), entry_count) + + def test_write_entries(self): + boom.bootloader.load_entries() + boom.bootloader.write_entries() + + def test_find_boot_entries(self): + boom.bootloader.load_entries() + + find_entries = boom.bootloader.find_entries + + entries = find_entries() + self.assertEqual(len(entries), len(boom.bootloader._entries)) + + entries = find_entries(Selection(machine_id="ffffffff")) + self.assertEqual(len(entries), self._nr_machine_id("ffffffff")) + + def test_Selection_no_osp_match(self): + s = Selection(os_id="12345") + self.assertFalse(find_entries(s)) + +@unittest.skipIf(not have_root(), "requires root privileges") +class BootLoaderTestsCheckRoot(unittest.TestCase): + """Base class for BootLoaderTests that validate a chosen root + device. + + Subclasses define the lists of devices that must be present + or absent, and the superclass initialises these in the setUp() + and tearDown() methods. + + The format of the ``add_dev`` and ``del_dev`` lists is a + tuple ``(devname, type)``, where ``type`` is a string type + containing the character 'c' (char), or 'b' (block). + + Device types in the del_devs list are currently ignored. + + Tests using this base class require root privileges in order + to manipulate device nodes in the test sanbox. These tests + are automatically skipped if the suite is run as a normal + user. + """ + + add_devs = [] + del_devs = [] + + def setUp(self): + reset_sandbox() + dev_path = join(SANDBOX_PATH, "dev") + makedirs(dev_path) + for dev in self.add_devs: + mode = 0o600 + if dev[1] == 'b': + mode |= S_IFBLK + if dev[1] == 'c': + mode |= S_IFCHR + mknod(join(dev_path, dev[0]), mode) + for dev in self.del_devs: + try: + unlink(join(dev_path, dev[0])) + except OSError as e: + if e.errno != 2: + raise + + def tearDown(self): + rm_sandbox() + + +class BootLoaderTestsCheckRootReal(BootLoaderTestsCheckRoot): + """Check root device with valid block device node. + """ + add_devs = [("sda", "b")] + def test_check_root_device_real(self): + # Real block device node + boom.bootloader.check_root_device("tests/sandbox/dev/sda") + + +class BootLoaderTestsCheckRootNonex(BootLoaderTestsCheckRoot): + """Check root device with invalid block device node. + """ + del_devs = [("sdb", "b")] + def test_check_root_device_nonex(self): + # Non-existent device node + with self.assertRaises(BoomRootDeviceError) as cm: + boom.bootloader.check_root_device("tests/dev/sdb") + + +class BootLoaderTestsCheckRootNonblock(BootLoaderTestsCheckRoot): + """Check root device with non-block device node. + """ + add_devs = [("null", "c")] + def test_check_root_device_nonblock(self): + # Non-existent device node + with self.assertRaises(BoomRootDeviceError) as cm: + boom.bootloader.check_root_device("tests/dev/null") + + +class BootLoaderTestsWithData(unittest.TestCase): + """Base class for BootLoaderTests that require specific on-disk + test fixture data. + + Each subclass sets ``bootloader_data`` to the subdirectory of + the ``bootloader_tests`` directory that contains the correct + set of configuration files. + """ + # Set by subclass + bootloader_config = None + + # Common to all subclasses + configs = join(BOOT_ROOT_TEST, "bootloader_configs") + + def setUp(self): + rm_sandbox() + if not self.bootloader_config: + raise ValueError("bootloader_config is undefined") + config_path = join(self.configs, self.bootloader_config) + shutil.copytree(config_path, join(SANDBOX_PATH)) + boom.set_boot_path(join(BOOT_ROOT_TEST, "sandbox/boot")) + + def tearDown(self): + rm_sandbox() + reset_boom_paths() + + def bootloader_config_check(self, value): + self.assertEqual(check_bootloader(), value) + + +class BootLoaderTestsBoomOn(BootLoaderTestsWithData): + # Boot config with boom disabled + bootloader_config = "boom_on" + + def test_check_bootloader(self): + self.bootloader_config_check(True) + + +class BootLoaderTestsBoomOff(BootLoaderTestsWithData): + # Boot config with boom disabled + bootloader_config = "boom_off" + + def test_check_bootloader(self): + self.bootloader_config_check(True) + + +class BootLoaderTestsNoBoom(BootLoaderTestsWithData): + # Boot config with no boom configuration + bootloader_config = "no_boom" + + def test_check_bootloader(self): + self.bootloader_config_check(False) + + +class BootLoaderTestsNoGrubD(BootLoaderTestsWithData): + # Boot config with no boom configuration + bootloader_config = "no_grub_d" + + def test_check_bootloader(self): + self.bootloader_config_check(False) + + +# vim: set et ts=4 sw=4 : diff --git a/tests/command_tests.py b/tests/command_tests.py new file mode 100644 index 0000000..f55dd78 --- /dev/null +++ b/tests/command_tests.py @@ -0,0 +1,2182 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# command_tests.py - Boom command API tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from sys import stdout +from os import listdir, makedirs +from os.path import abspath, exists, join +import shutil +import re + +# Python3 moves StringIO to io +try: + from StringIO import StringIO +except: + from io import StringIO + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +from boom import * +from boom.osprofile import * +from boom.bootloader import * +from boom.hostprofile import * +from boom.command import * +from boom.config import * +from boom.report import * + +# For access to non-exported members +import boom.command + +from tests import * + +BOOT_ROOT_TEST = abspath("./tests") +config = BoomConfig() +config.legacy_enable = False +config.legacy_sync = False +set_boom_config(config) +set_boot_path(BOOT_ROOT_TEST) + +debug_masks = ['profile', 'entry', 'report', 'command', 'all'] + + +class CommandHelperTests(unittest.TestCase): + """Test internal boom.command helpers: methods in this part of the + test suite import boom.command directly in order to access the + non-public helper routines not included in __all__. + """ + def test_int_if_val_with_val(self): + import boom.command + val = "1" + self.assertEqual(boom.command._int_if_val(val), int(val)) + + def test_int_if_val_with_none(self): + import boom.command + val = None + self.assertEqual(boom.command._int_if_val(val), None) + + def test_int_if_val_with_badint(self): + import boom.command + val = "qux" + with self.assertRaises(ValueError) as cm: + boom.command._int_if_val(val) + + def test_subvol_from_arg_subvol(self): + import boom.command + xtuple = ("/svol", None) + self.assertEqual(boom.command._subvol_from_arg("/svol"), xtuple) + + def test_subvol_from_arg_subvolid(self): + import boom.command + xtuple = (None, "23") + self.assertEqual(boom.command._subvol_from_arg("23"), xtuple) + + def test_subvol_from_arg_none(self): + import boom.command + self.assertEqual(boom.command._subvol_from_arg(None), (None, None)) + + def test_str_indent(self): + import boom.command + instr = "1\n2\n3\n4" + xstr = " 1\n 2\n 3\n 4" + indent = 4 + outstr = boom.command._str_indent(instr, indent) + self.assertEqual(outstr, xstr) + + def test_str_indent_bad_indent(self): + import boom.command + instr = "1\n2\n3\n4" + indent = "qux" + with self.assertRaises(TypeError) as cm: + outstr = boom.command._str_indent(instr, indent) + + def test_str_indent_bad_str(self): + import boom.command + instr = None + indent = 4 + with self.assertRaises(AttributeError) as cm: + outstr = boom.command._str_indent(instr, indent) + + def test_canonicalize_lv_name(self): + import boom.command + xlv = "vg/lv" + for lvstr in ["vg/lv", "/dev/vg/lv"]: + self.assertEqual(xlv, boom.command._canonicalize_lv_name(lvstr)) + + def test_canonicalize_lv_name_bad_lv(self): + import boom.command + with self.assertRaises(ValueError) as cm: + boom.command._canonicalize_lv_name("vg/lv/foo/bar/baz") + with self.assertRaises(ValueError) as cm: + boom.command._canonicalize_lv_name("vg-lv") + with self.assertRaises(ValueError) as cm: + boom.command._canonicalize_lv_name("/dev/mapper/vg-lv") + + def test_expand_fields_defaults(self): + import boom.command + default = "f1,f2,f3" + xfield = default + self.assertEqual(xfield, boom.command._expand_fields(default, "")) + + def test_expand_fields_replace(self): + import boom.command + default = "f1,f2,f3" + options = "f4,f5,f6" + xfield = options + self.assertEqual(xfield, boom.command._expand_fields(default, options)) + + def test_expand_fields_add(self): + import boom.command + default = "f1,f2,f3" + options = "+f4,f5,f6" + xfield = default + ',' + options[1:] + self.assertEqual(xfield, boom.command._expand_fields(default, options)) + + def test_set_debug_no_debug_arg(self): + """Test set_debug() with an empty debug mask argument. + """ + import boom.command + boom.command.set_debug(None) + + def test_set_debug_args_one(self): + """Test set_debug() with a single debug mask argument. + """ + import boom.command + for mask in debug_masks: + boom.command.set_debug(mask) + + def test_set_debug_args_all(self): + """Test set_debug() with a list of debug mask arguments. + """ + import boom.command + all_masks = ",".join(debug_masks[:-1]) + boom.command.set_debug(all_masks) + + def test_set_debug_no_debug_arg(self): + """Test set_debug() with a bad debug mask argument. + """ + import boom.command + with self.assertRaises(ValueError) as cm: + boom.command.set_debug("nosuchmask") + + def test_setup_logging(self): + """Test the setup_logging() command helper. + """ + import boom.command + args = MockArgs() + boom.command.setup_logging(args) + + @unittest.skipIf(not have_grub1(), "requires grub1") + def test_show_legacy_default(self): + """Test the show_legacy() command helper. + """ + import boom.command + boom.command.show_legacy() + + def test__get_machine_id(self): + # FIXME: does not cover _DBUS_MACHINE_ID hosts or exceptions + # reading /etc/machine-id. + machine_id = boom.command._get_machine_id() + self.assertTrue(machine_id) + + +# Default test OsProfile identifiers +test_os_id = "9cb53ddda889d6285fd9ab985a4c47025884999f" +test_os_disp_id = test_os_id[0:6] + +test_lv = get_logical_volume() +test_root_lv = get_root_lv() + +def get_create_cmd_args(): + """Return a correct MockArgs object for a call to the _create_cmd() + helper. Tests that should fail modify the fields returned to + generate the required error. + """ + args = MockArgs() + args.profile = test_os_disp_id + args.title = "ATITLE" + args.version = "2.6.0" + args.machine_id = "ffffffff" + args.root_device = get_logical_volume() + args.root_lv = get_root_lv() + return args + + +class CommandTests(unittest.TestCase): + """Test boom.command APIs + """ + + # Master BLS loader directory for sandbox + loader_path = join(BOOT_ROOT_TEST, "loader") + + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + # Master grub configuration path for sandbox + grub_path = join(BOOT_ROOT_TEST, "grub") + + # Test fixture init/cleanup + def setUp(self): + """Set up a test fixture for the CommandTests class. + + Defines standard objects for use in these tests. + """ + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + grub_sandbox = join(SANDBOX_PATH, "boot/grub") + loader_sandbox = join(SANDBOX_PATH, "boot/loader") + + # Initialise sandbox from master + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + shutil.copytree(self.loader_path, loader_sandbox) + shutil.copytree(self.grub_path, grub_sandbox) + + # Set boom paths + set_boot_path(boot_sandbox) + + # Tests that deal with legacy configs will enable this. + config = BoomConfig() + config.legacy_enable = False + config.legacy_sync = False + + # Reset profiles, entries, and host profiles to known state. + load_profiles() + load_entries() + load_host_profiles() + + def tearDown(self): + # Drop any in-memory entries and profiles modified by tests + drop_entries() + drop_profiles() + drop_host_profiles() + + # Clear sandbox data + rm_sandbox() + reset_boom_paths() + + def test_command_find_profile_with_profile_arg(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "d4439b7d2f928c39f1160c0b0291407e5990b9e0" # F26 + cmd_args.machine_id = "12345" # No HostProfile + osp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertEqual(osp.os_id, cmd_args.profile) + + def test_command_find_profile_with_version_arg(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = None + cmd_args.version = "4.16.11-100.fc26.x86_64" # F26 + cmd_args.machine_id = "12345" # No HostProfile + xprofile = "d4439b7d2f928c39f1160c0b0291407e5990b9e0" + osp = _find_profile(cmd_args, cmd_args.version, + cmd_args.machine_id, "test") + self.assertEqual(osp.os_id, xprofile) + + def test_command_find_profile_with_bad_version_arg(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = None + cmd_args.version = "4.16.11-100.x86_64" # no match + cmd_args.machine_id = "12345" # No HostProfile + xprofile = "d4439b7d2f928c39f1160c0b0291407e5990b9e0" + osp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertEqual(osp, None) + + def test_command_find_profile_bad_profile(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "quxquxquxquxquxquxquxqux" # nonexistent + cmd_args.machine_id = "12345" # No HostProfile + osp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertEqual(osp, None) + + def test_command_find_profile_ambiguous_profile(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "9" # ambiguous + cmd_args.machine_id = "12345" # No HostProfile + osp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertEqual(osp, None) + + def test_command_find_profile_ambiguous_host(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "" + cmd_args.machine_id = "fffffffffff" # Ambiguous HostProfile + osp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertEqual(osp, None) + + def test_command_find_profile_host(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "" + cmd_args.machine_id = "ffffffffffffc" + cmd_args.label = "" + hp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertTrue(hp) + self.assertTrue(hasattr(hp, "add_opts")) + + def test_command_find_profile_host_os_mismatch(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "3fc389bba581e5b20c6a46c7fc31b04be465e973" + cmd_args.machine_id = "ffffffffffffc" + cmd_args.label = "" + hp = _find_profile(cmd_args, "", cmd_args.machine_id, "test") + self.assertFalse(hp) + + def test_command_find_profile_no_matching(self): + import boom.command + _find_profile = boom.command._find_profile + cmd_args = MockArgs() + cmd_args.profile = "" + cmd_args.machine_id = "1111111111111111" # no matching + hp = _find_profile(cmd_args, "", cmd_args.machine_id, + "test", optional=False) + self.assertFalse(hp) + + # + # API call tests + # + # BootEntry tests + # + + def test_list_entries(self): + path = boom_entries_path() + nr = len([p for p in listdir(path) if p.endswith(".conf")]) + bes = list_entries() + self.assertTrue(len(bes), nr) + + def test_list_entries_match_machine_id(self): + machine_id = "611f38fd887d41dea7eb3403b2730a76" + path = boom_entries_path() + nr = len([p for p in listdir(path) if p.startswith(machine_id)]) + bes = list_entries(Selection(machine_id=machine_id)) + self.assertTrue(len(bes), nr) + + def test_list_entries_match_version(self): + version = "4.10.17-100.fc24.x86_64" + path = boom_entries_path() + nr = len([p for p in listdir(path) if version in p]) + bes = list_entries(Selection(version=version)) + self.assertEqual(len(bes), nr) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_entry_notitle(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + osp.title = None + with self.assertRaises(ValueError) as cm: + be = create_entry(None, "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_entry_noversion(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + with self.assertRaises(ValueError) as cm: + be = create_entry("ATITLE", None, "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_entry_nomachineid(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + with self.assertRaises(ValueError) as cm: + be = create_entry("ATITLE", "2.6.0", "", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_entry_norootdevice(self): + # FIXME: should this default from the lvm_root_lv? + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + with self.assertRaises(ValueError) as cm: + be = create_entry("ATITLE", "2.6.0", "ffffffff", None, + lvm_root_lv=test_root_lv, profile=osp) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_entry_noosprofile(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + with self.assertRaises(ValueError) as cm: + be = create_entry("ATITLE", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv) + + def test_create_dupe(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + + title = "Fedora (4.1.1-100.fc24.x86_64) 24 (Workstation Edition)" + machine_id = "611f38fd887d41dea7eb3403b2730a76" + version = "4.1.1-100.fc24" + root_device = "/dev/sda5" + btrfs_subvol_id = "23" + + with self.assertRaises(ValueError) as cm: + create_entry(title, version, machine_id, root_device, + btrfs_subvol_id=btrfs_subvol_id, profile=osp, + allow_no_dev=True) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_create_delete_entry(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("ATITLE", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + delete_entries(Selection(boot_id=be.boot_id)) + self.assertFalse(exists(be._entry_path)) + + @unittest.skipIf(not have_grub1() or not have_root_lv(), "requires " + "grub1 and LVM") + def test_create_delete_entry_with_legacy(self): + config = BoomConfig() + config.legacy_enable = True + config.legacy_sync = True + set_boom_config(config) + set_boot_path(BOOT_ROOT_TEST) + + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("ATITLE", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + delete_entries(Selection(boot_id=be.boot_id)) + self.assertFalse(exists(be._entry_path)) + + + def test_delete_entries_no_matching_raises(self): + with self.assertRaises(IndexError) as cm: + delete_entries(Selection(boot_id="thereisnospoon")) + + def test_clone_entry_no_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = clone_entry(Selection()) + + def test_clone_entry_no_matching_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = clone_entry(Selection(boot_id="qqqqqqq"), title="FAIL") + + def test_clone_entry_ambiguous_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = clone_entry(Selection(boot_id="6"), title="NEWTITLE") + + + def test_clone_entry_add_opts(self): + be = clone_entry(Selection(boot_id="9591d36"), title="NEWNEWTITLE", + add_opts="foo", allow_no_dev=True) + self.assertTrue(exists(be._entry_path)) + be.delete_entry() + self.assertFalse(exists(be._entry_path)) + + def test_clone_entry_del_opts(self): + be = clone_entry(Selection(boot_id="9591d36"), title="NEWNEWTITLE", + del_opts="rhgb quiet", allow_no_dev=True) + self.assertTrue(exists(be._entry_path)) + be.delete_entry() + self.assertFalse(exists(be._entry_path)) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_clone_delete_entry(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("ATITLE", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + be2 = clone_entry(Selection(boot_id=be.boot_id), title="ANEWTITLE", + version="2.6.1") + + self.assertTrue(exists(be2._entry_path)) + + be.delete_entry() + be2.delete_entry() + + self.assertFalse(exists(be._entry_path)) + self.assertFalse(exists(be2._entry_path)) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_clone_entry_no_args(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("ATITLE", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + with self.assertRaises(ValueError) as cm: + be2 = clone_entry(Selection(boot_id=be.boot_id)) + + be.delete_entry() + + def test_clone_entry_with_add_del_opts(self): + # Entry with options +"debug" -"rhgb quiet" + orig_boot_id = "78861b7" + # Use allow_no_dev=True here since we are cloning an existing + # entry on a system with unknown devices. + be = clone_entry(Selection(boot_id=orig_boot_id), + title="clone with addopts", allow_no_dev=True) + orig_be = find_entries(Selection(boot_id=orig_boot_id))[0] + self.assertTrue(orig_be) + self.assertTrue(be) + self.assertEqual(orig_be.options, be.options) + be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_clone_dupe(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("CLONE_TEST", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + be2 = clone_entry(Selection(boot_id=be.boot_id), title="ANEWTITLE", + version="2.6.1") + + with self.assertRaises(ValueError) as cm: + be3 = clone_entry(Selection(boot_id=be.boot_id), title="ANEWTITLE", + version="2.6.1") + + be.delete_entry() + be2.delete_entry() + + def test_edit_entry_no_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = edit_entry(Selection()) + + def test_edit_entry_no_matching_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = edit_entry(Selection(boot_id="qqqqqqq"), title="FAIL") + + def test_edit_entry_ambiguous_boot_id(self): + with self.assertRaises(ValueError) as cm: + bad_be = edit_entry(Selection(boot_id="6"), title="NEWTITLE") + + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_add_opts(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + profile=osp) + + # Confirm original entry has been written + self.assertTrue(exists(orig_be._entry_path)) + + # Save these - they will be overwritten by the edit operation + orig_id = orig_be.boot_id + orig_entry_path = orig_be._entry_path + + edit_title = "EDITED_TITLE" + edit_add_opts = "foo" + + # FIXME: restore allow_no_dev + edit_be = edit_entry(Selection(boot_id=orig_id), title=edit_title, + add_opts=edit_add_opts) + + # Confirm edited entry has been written + self.assertTrue(exists(edit_be._entry_path)) + + # Confirm original entry has been removed + self.assertFalse(exists(orig_entry_path)) + + # Verify new boot_id + self.assertFalse(orig_id == edit_be.boot_id) + + # Verify edited title and options + self.assertEqual(edit_title, edit_be.title) + self.assertEqual(edit_be.bp.add_opts, [edit_add_opts]) + self.assertTrue(edit_add_opts in edit_be.options) + + # Clean up entries + edit_be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_add_opts_with_add_opts(self): + edit_title = "EDITED_TITLE" + edit_add_opts = "foo" + orig_add_opts = "bar" + + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + add_opts="bar", profile=osp) + + # Confirm original entry has been written + self.assertTrue(exists(orig_be._entry_path)) + + # Save these - they will be overwritten by the edit operation + orig_id = orig_be.boot_id + orig_entry_path = orig_be._entry_path + + # FIXME: restore allow_no_dev + edit_be = edit_entry(Selection(boot_id=orig_id), title=edit_title, + add_opts=edit_add_opts) + + # Confirm edited entry has been written + self.assertTrue(exists(edit_be._entry_path)) + + # Confirm original entry has been removed + self.assertFalse(exists(orig_entry_path)) + + # Verify new boot_id + self.assertFalse(orig_id == edit_be.boot_id) + + # Verify edited title and options + self.assertEqual(edit_title, edit_be.title) + + # Sort the opts lists as Python3 does not guarantee ordering + sorted_bp_add_opts = sorted(edit_be.bp.add_opts) + sorted_edit_and_orig_opts = sorted([edit_add_opts, orig_add_opts]) + self.assertEqual(sorted_bp_add_opts, sorted_edit_and_orig_opts) + + # Verify original added opts + self.assertTrue(orig_add_opts in edit_be.options) + # Verify edit added opts + self.assertTrue(edit_add_opts in edit_be.options) + + # Clean up entries + edit_be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_del_opts(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + profile=osp) + + # Confirm original entry has been written + self.assertTrue(exists(orig_be._entry_path)) + + # Save these - they will be overwritten by the edit operation + orig_id = orig_be.boot_id + orig_entry_path = orig_be._entry_path + + edit_title = "EDITED_TITLE" + edit_del_opts = "rhgb" + + # FIXME: restore allow_no_dev + edit_be = edit_entry(Selection(boot_id=orig_id), title=edit_title, + del_opts=edit_del_opts) + + # Confirm edited entry has been written + self.assertTrue(exists(edit_be._entry_path)) + + # Confirm original entry has been removed + self.assertFalse(exists(orig_entry_path)) + + # Verify new boot_id + self.assertFalse(orig_id == edit_be.boot_id) + + # Verify edited title and options + self.assertEqual(edit_title, edit_be.title) + self.assertEqual(edit_be.bp.del_opts, [edit_del_opts]) + self.assertTrue(edit_del_opts not in edit_be.options) + + # Clean up entries + edit_be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_del_opts_with_del_opts(self): + edit_title = "EDITED_TITLE" + edit_del_opts = "rhgb" + orig_del_opts = "quiet" + + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + del_opts="quiet", profile=osp) + + # Confirm original entry has been written + self.assertTrue(exists(orig_be._entry_path)) + + # Save these - they will be overwritten by the edit operation + orig_id = orig_be.boot_id + orig_entry_path = orig_be._entry_path + + # Verify original deled opts + self.assertTrue(orig_del_opts not in orig_be.options) + self.assertEqual(orig_be.bp.del_opts, [orig_del_opts]) + + # FIXME: restore allow_no_dev + edit_be = edit_entry(Selection(boot_id=orig_id), title=edit_title, + del_opts=edit_del_opts) + + # Confirm edited entry has been written + self.assertTrue(exists(edit_be._entry_path)) + + # Confirm original entry has been removed + self.assertFalse(exists(orig_entry_path)) + + # Verify new boot_id + self.assertFalse(orig_id == edit_be.boot_id) + + # Verify edited title and options + self.assertEqual(edit_title, edit_be.title) + + # Sort the opts lists as Python3 does not guarantee ordering + sorted_bp_del_opts = sorted(edit_be.bp.del_opts) + sorted_edit_and_orig_opts = sorted([edit_del_opts, orig_del_opts]) + self.assertEqual(sorted_bp_del_opts, sorted_edit_and_orig_opts) + + # Verify original deleted opts + self.assertTrue(orig_del_opts not in edit_be.options) + # Verify edit deleted opts + self.assertTrue(edit_del_opts not in edit_be.options) + + # Clean up entries + edit_be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_del_opts(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + profile=osp) + + be = edit_entry(Selection(boot_id=orig_be.boot_id), + title="NEWNEWTITLE", del_opts="rhgb quiet") + + self.assertTrue(exists(be._entry_path)) + be.delete_entry() + self.assertFalse(exists(be._entry_path)) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_delete_entry(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("ATITLE", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + profile=osp) + orig_path = orig_be._entry_path + self.assertTrue(exists(orig_path)) + + edit_be = edit_entry(Selection(boot_id=orig_be.boot_id), + title="ANEWTITLE", version="2.6.1") + + self.assertTrue(exists(edit_be._entry_path)) + self.assertFalse(exists(orig_path)) + + edit_be.delete_entry() + + self.assertFalse(exists(edit_be._entry_path)) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_no_args(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + be = create_entry("ATITLE", "2.6.0", "ffffffff", test_lv, + lvm_root_lv=test_root_lv, profile=osp) + self.assertTrue(exists(be._entry_path)) + + with self.assertRaises(ValueError) as cm: + be2 = edit_entry(Selection(boot_id=be.boot_id)) + + be.delete_entry() + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test_edit_entry_with_add_del_opts(self): + # Fedora 24 (Workstation Edition) + osp = get_os_profile_by_id(test_os_id) + orig_be = create_entry("EDIT_TEST", "2.6.0", "ffffffff", + test_lv, lvm_root_lv=test_root_lv, + profile=osp) + orig_path = orig_be._entry_path + + add_opts = "debug" + del_opts = "rhgb quiet" + + # Entry with options +"debug" -"rhgb quiet" + orig_boot_id = orig_be.boot_id + edit_be = edit_entry(Selection(boot_id=orig_boot_id), + title="edit with addopts", add_opts=add_opts, + del_opts=del_opts) + + self.assertTrue(edit_be) + + self.assertTrue(exists(edit_be._entry_path)) + self.assertFalse(exists(orig_path)) + + self.assertTrue(add_opts in edit_be.options) + self.assertTrue(del_opts not in edit_be.options) + + edit_be.delete_entry() + + def test_print_entries_no_matching(self): + xoutput = r"BootID.*Version.*Name.*RootDevice" + output = StringIO() + opts = BoomReportOpts(report_file=output) + print_entries(selection=Selection(boot_id="thereisnoboot"), opts=opts) + self.assertTrue(re.match(xoutput, output.getvalue())) + + def test_print_entries_default_stdout(self): + print_entries() + + def test_print_entries_boot_id_filter(self): + xoutput = [r"BootID.*Version.*Name.*RootDevice", + r"debfd7f.*4.11.12-100.fc24.x86_64.*Fedora.*" + r"/dev/vg00/lvol0-snapshot"] + output = StringIO() + opts = BoomReportOpts(report_file=output) + print_entries(selection=Selection(boot_id="debfd7f"), opts=opts) + for pair in zip(xoutput, output.getvalue().splitlines()): + self.assertTrue(re.match(pair[0], pair[1])) + + # + # API call tests + # + # OsProfile tests + # + + def test_command_create_delete_profile(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + # Use the OsProfile.delete_profile() method + osp.delete_profile() + + def test_command_create_delete_profiles(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + # Use the command.delete_profiles() API call + delete_profiles(selection=Selection(os_id=osp.os_id)) + + def test_command_delete_profiles_no_match(self): + with self.assertRaises(IndexError) as cm: + delete_profiles(selection=Selection(os_id="XyZZy")) + + def test_command_create_delete_profile_from_file(self): + os_release_path = "tests/os-release/fedora26-test-os-release" + osp = create_profile(None, None, None, None, + profile_file=os_release_path, uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Fedora") + self.assertEqual(osp.os_version, "26 (Testing Edition)") + osp.delete_profile() + + def test_command_create_delete_profile_from_data(self): + profile_data = { + BOOM_OS_NAME: "Some Distro", BOOM_OS_SHORT_NAME: "somedist", + BOOM_OS_VERSION: "1 (Qunk)", BOOM_OS_VERSION_ID: "1", + BOOM_OS_UNAME_PATTERN: "sd1", + BOOM_OS_KERNEL_PATTERN: "/vmlinuz-%{version}", + BOOM_OS_INITRAMFS_PATTERN: "/initramfs-%{version}.img", + BOOM_OS_ROOT_OPTS_LVM2: "rd.lvm.lv=%{lvm_root_lv}", + BOOM_OS_ROOT_OPTS_BTRFS: "rootflags=%{btrfs_subvolume}", + BOOM_OS_OPTIONS: "root=%{root_device} %{root_opts}", + BOOM_OS_TITLE: "This is a title (%{version})" + } + + # All fields: success + osp = create_profile(None, None, None, None, profile_data=profile_data) + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + self.assertEqual(osp.os_version, "1 (Qunk)") + osp.delete_profile() + + # Pop identity fields in reverse checking order: + # OS_VERSION_ID, OS_VERSION, OS_SHORT_NAME, OS_NAME + + profile_data.pop(BOOM_OS_VERSION_ID) + with self.assertRaises(ValueError) as cm: + bad_osp = create_profile(None, None, None, None, + profile_data=profile_data) + + profile_data.pop(BOOM_OS_VERSION) + with self.assertRaises(ValueError) as cm: + bad_osp = create_profile(None, None, None, None, + profile_data=profile_data) + + profile_data.pop(BOOM_OS_SHORT_NAME) + with self.assertRaises(ValueError) as cm: + bad_osp = create_profile(None, None, None, None, + profile_data=profile_data) + + profile_data.pop(BOOM_OS_NAME) + with self.assertRaises(ValueError) as cm: + bad_osp = create_profile(None, None, None, None, + profile_data=profile_data) + + def test_clone_profile_no_os_id(self): + with self.assertRaises(ValueError) as cm: + bad_osp = clone_profile(Selection()) + + def test_clone_profile_no_args(self): + with self.assertRaises(ValueError) as cm: + bad_osp = clone_profile(Selection(os_id="d4439b7")) + + def test_clone_profile_no_matching_os_id(self): + with self.assertRaises(ValueError) as cm: + bad_osp = clone_profile(Selection(os_id="fffffff"), name="NEW") + + def test_clone_profile_ambiguous_os_id(self): + with self.assertRaises(ValueError) as cm: + bad_osp = clone_profile(Selection(os_id="d"), name="NEW") + + def test_clone_profile_new_name(self): + osp = clone_profile(Selection(os_id="d4439b7"), + name="NEW", short_name="new", version="26 (Not)", + version_id="~26") + self.assertTrue(osp) + self.assertEqual("NEW", osp.os_name) + self.assertEqual("new", osp.os_short_name) + osp.delete_profile() + + def test_create_edit_profile(self): + osp = create_profile("Test1", "test", "1 (Test)", "1", + uname_pattern="t1") + + self.assertTrue(osp) + + edit_osp = edit_profile(Selection(os_id=osp.os_id), + uname_pattern="t2") + + self.assertTrue(edit_osp) + self.assertEqual(osp.uname_pattern, "t2") + osp.delete_profile() + edit_osp.delete_profile() + + def test_edit_no_matching_os_id(self): + with self.assertRaises(ValueError) as cm: + edit_osp = edit_profile(Selection(os_id="notfound"), + uname_pattern="nf2") + + def test_edit_ambiguous_os_id(self): + with self.assertRaises(ValueError) as cm: + edit_osp = edit_profile(Selection(os_id="d"), + uname_pattern="d2") + + def test_list_profiles(self): + profiles = list_profiles() + self.assertTrue(profiles) + + def test_print_profiles(self): + repstr = print_profiles() + + # + # API call tests + # + # HostProfile tests + # + + def test_create_delete_host(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "ffffffffffffffff1234567890" + host_name = "somehost.somedomain" + host_opts = osp.options + " hostoptions" + + hp = create_host(machine_id=host_machine_id, host_name=host_name, + os_id=osp.os_id, label="", options=host_opts) + + self.assertEqual(host_machine_id, hp.machine_id) + self.assertEqual(host_name, hp.host_name) + self.assertEqual(host_opts, hp.options) + + # Use the command.delete_hosts() API call + delete_hosts(Selection(host_id=hp.host_id)) + + # Clean up osp + osp.delete_profile() + + def test_create_host_no_os_id(self): + os_id = None + host_machine_id = "ffffffffffffffff1234567890" + host_name = "somehost.somedomain" + host_opts = "hostoptions" + + with self.assertRaises(ValueError) as cm: + bad_hp = create_host(machine_id=host_machine_id, + host_name=host_name, os_id=os_id, + label="", options=host_opts) + + def test_create_host_no_os_id_match(self): + os_id = "notfound" + host_machine_id = "ffffffffffffffff1234567890" + host_name = "somehost.somedomain" + host_opts = "hostoptions" + + with self.assertRaises(ValueError) as cm: + bad_hp = create_host(machine_id=host_machine_id, + host_name=host_name, os_id=os_id, + label="", options=host_opts) + + def test_create_host_no_host_name(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "ffffffffffffffff1234567890" + host_name = "" + host_opts = "hostoptions" + + with self.assertRaises(ValueError) as cm: + bad_hp = create_host(machine_id=host_machine_id, + host_name=host_name, os_id=osp.os_id, + label="", options=host_opts) + + osp.delete_profile() + + def test_create_host_no_machine_id(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "" + host_name = "somehost.somedomain" + host_opts = "hostoptions" + + with self.assertRaises(ValueError) as cm: + bad_hp = create_host(machine_id=host_machine_id, + host_name=host_name, os_id=osp.os_id, + label="", options=host_opts) + + osp.delete_profile() + + def test_create_host_all_args(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "ffffffffffffffff1234567890" + host_name = "somehost.somedomain" + + hp = create_host(machine_id=host_machine_id, host_name=host_name, + os_id=osp.os_id, label="label", + kernel_pattern="/vmlinuz", + initramfs_pattern="/initramfs.img", + root_opts_lvm2="rd.lvm.lv=vg/lv", + root_opts_btrfs="rootflags=subvolid=1", + options=osp.options, add_opts="debug", + del_opts="rhgb quiet") + + self.assertEqual(host_machine_id, hp.machine_id) + self.assertEqual(host_name, hp.host_name) + + hp.delete_profile() + + # Clean up osp + osp.delete_profile() + + def test_delete_hosts_no_match(self): + with self.assertRaises(IndexError) as cm: + delete_hosts(Selection(host_id="nomatch")) + + def test_clone_host(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "ffffffffffffffff1234567890" + clone_machine_id = "ffffffffffffffff0987654321" + host_name = "somehost.somedomain" + host_opts = osp.options + " hostoptions" + + hp = create_host(machine_id=host_machine_id, host_name=host_name, + os_id=osp.os_id, label="", options=host_opts) + + self.assertEqual(host_machine_id, hp.machine_id) + self.assertEqual(host_name, hp.host_name) + self.assertEqual(host_opts, hp.options) + + clone_hp = clone_host(Selection(host_id=hp.host_id), + machine_id=clone_machine_id) + + self.assertEqual(clone_machine_id, clone_hp.machine_id) + self.assertNotEqual(hp.host_id, clone_hp.host_id) + + hp.delete_profile() + clone_hp.delete_profile() + + # Clean up osp + osp.delete_profile() + + def test_clone_host_no_host_id(self): + with self.assertRaises(ValueError) as cm: + bad_hp = clone_host(Selection(host_id=None)) + + def test_clone_host_no_host_id_match(self): + host_id = "notfound" + + with self.assertRaises(ValueError) as cm: + bad_hp = clone_host(Selection(host_id=host_id), + machine_id="ffffffff") + + def test_clone_host_no_args(self): + host_id = "5ebcb1f" + + with self.assertRaises(ValueError) as cm: + bad_hp = clone_host(Selection(host_id=host_id)) + + def test_create_edit_host(self): + osp = create_profile("Some Distro", "somedist", "1 (Qunk)", "1", + uname_pattern="sd1", + kernel_pattern="/vmlinuz-%{version}", + initramfs_pattern="/initramfs-%{version}.img", + root_opts_lvm2="rd.lvm.lv=%{lvm_root_lv}", + root_opts_btrfs="rootflags=%{btrfs_subvolume}", + options="root=%{root_device} %{root_opts}") + + self.assertTrue(osp) + self.assertEqual(osp.os_name, "Some Distro") + + host_machine_id = "ffffffffffffffff1234567890" + host_name = "somehost.somedomain" + host_opts = osp.options + " hostoptions" + + hp = create_host(machine_id=host_machine_id, host_name=host_name, + os_id=osp.os_id, label="", options=host_opts) + + self.assertEqual(host_machine_id, hp.machine_id) + self.assertEqual(host_name, hp.host_name) + self.assertEqual(host_opts, hp.options) + + edit_name = "someother.host" + edit_opts = osp.options + + edit_hp = edit_host(Selection(host_id=hp.host_id), + machine_id=host_machine_id, host_name=edit_name, + os_id=osp.os_id, label="", options=edit_opts) + + self.assertEqual(host_machine_id, edit_hp.machine_id) + self.assertEqual(edit_name, edit_hp.host_name) + self.assertEqual(osp.options, edit_hp.options) + + edit_hp.delete_profile() + + # Clean up osp + osp.delete_profile() + + def test_list_hosts_default(self): + """Test the list_hosts() API call with no selection. + """ + hps = list_hosts() + self.assertTrue(len(hps) >= 1) + + def test_print_hosts_default(self): + """Test the list_hosts() API call with no selection. + """ + print_hosts() + + # + # Command handler tests + # + + def test__create_cmd(self): + """Test the _create_cmd() handler with correct arguments. + """ + args = get_create_cmd_args() + opts = boom.command._report_opts_from_args(args) + boom.command._create_cmd(args, None, opts, None) + + def test__create_cmd_bad_identity(self): + """Test the _create_cmd() handler with an invalid identity + function argument. + """ + args = get_create_cmd_args() + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, "badident") + self.assertEqual(r, 1) + + @unittest.skip("Requires boom.command.get_uts_release() override") + def test__create_cmd_no_version(self): + """Test the _create_cmd() handler with missing version. + """ + args = get_create_cmd_args() + args.version = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test__create_cmd_version_from_uts(self): + """Test the _create_cmd() handler with missing version, and the + default version obtained from the system UTS data. + """ + args = get_create_cmd_args() + args.version = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__create_cmd_no_root_device(self): + """Test the _create_cmd() handler with missing root device. + """ + args = get_create_cmd_args() + args.root_device = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test__create_cmd_auto_machine_id(self): + """Test the _create_cmd() handler with automatic machine_id. + """ + args = get_create_cmd_args() + args.machine_id = None + args.profile = None + args.version = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__create_cmd_no_profile(self): + """Test the _create_cmd() handler with missing profile. + """ + args = get_create_cmd_args() + args.profile = None + # Avoid HostProfile match + args.machine_id = "quxquxquxqux" + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__create_cmd_no_title(self): + """Test the _create_cmd() handler with missing title. + """ + args = get_create_cmd_args() + args.title = None + + # Avoid OsProfile auto-title + osp = get_os_profile_by_id(test_os_id) + osp.title = None + + opts = boom.command._report_opts_from_args(args) + r = boom.command._create_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__delete_cmd_no_selection(self): + """Test that _delete_cmd() rejects a call with no valid + selection. + """ + args = MockArgs() + args.boot_id = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + @unittest.skipIf(not have_root_lv(), "requires root LV") + def test__create_cmd_with_override(self): + args = get_create_cmd_args() + args.title = "override test" + # Use a profile that includes BOOT_IMAGE=%{kernel} in BOOM_OS_OPTIONS + args.profile = "d4439b7" + # Use an image string ("vmlinux") that does not match the OsProfile + # template pattern for a Linux bzImage ("vmlinu*z*"). + args.linux = "/vmzlinux-test" + args.initrd = "/initrd-test.img" + opts = boom.command._report_opts_from_args(args) + boom.command._create_cmd(args, None, opts, None) + + # Find entry and verify --linux and --initrd override + be = find_entries(Selection(title=args.title))[0] + boot_id = be.boot_id + self.assertEqual(be.linux, args.linux) + self.assertEqual(be.initrd, args.initrd) + + # Reload entry and verify boot_id and overrides + drop_entries() + load_entries() + self.assertEqual(be.boot_id, boot_id) + self.assertEqual(be.linux, args.linux) + self.assertEqual(be.initrd, args.initrd) + + def test__delete_cmd(self): + """Test the _delete_cmd() handler with a valid entry. + """ + args = MockArgs() + args.boot_id = "61bcc49" + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__delete_cmd_no_selection(self): + """Test the _delete_cmd() handler with no valid entry selection. + """ + args = MockArgs() + args.boot_id = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__delete_cmd_verbose(self): + """Test the _delete_cmd() handler with a valid entry. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.verbose = 1 # enable reporting + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__delete_cmd_with_options(self): + """Test the _delete_cmd() handler with a valid entry and report + options object setting columns-as-rows mode. + """ + args = MockArgs() + args.boot_id = "61bcc49" + opts = boom.command._report_opts_from_args(args) + opts.columns_as_rows = True + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__delete_cmd_with_fields(self): + """Test the _delete_cmd() handler with a valid entry and report + field options string. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.options = "title,bootid" + args.verbose = 1 # enable reporting + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__delete_cmd_with_bad_fields(self): + """Test the _delete_cmd() handler with a valid entry and invalid + report field options string. + """ + args = MockArgs() + args.boot_id = "61bcc49" + opts = boom.command._report_opts_from_args(args) + args.options = "I,wish,I,knew,how,it,would,feel,to,be,free" + args.verbose = 1 # enable reporting + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__delete_cmd_verbose(self): + """Test the _delete_cmd() handler with a valid entry and + verbose output. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.verbose = 1 + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__delete_cmd_identity(self): + """Test the _delete_cmd() handler with a valid entry that + is passed via the 'identiry' handler argument. + """ + args = MockArgs() + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, "61bcc49") + self.assertNotEqual(r, 1) + + def test__delete_cmd_no_criteria(self): + """Test the _delete_cmd() handler with no valid selection. + """ + args = MockArgs() + args.boot_id = None + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__delete_cmd_multi(self): + """Test the _delete_cmd() handler with multiple valid entries. + """ + args = MockArgs() + args.boot_id = "6" # Matches four entries + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__delete_cmd_no_matching(self): + """Test the _delete_cmd() handler with no matching entries. + """ + args = MockArgs() + args.boot_id = "qux" # Matches no entries + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__clone_cmd(self): + """Test the _clone_cmd() handler with a valid entry and new + title. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.title = "Something New" + # Disable device presence checks + args.no_dev = True + opts = boom.command._report_opts_from_args(args) + r = boom.command._clone_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__clone_cmd_no_criteria(self): + """Test the _clone_cmd() handler with no valid selection. + """ + args = MockArgs() + args.boot_id = None + args.title = "Something New" + opts = boom.command._report_opts_from_args(args) + r = boom.command._clone_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__clone_cmd_no_matching(self): + """Test the _clone_cmd() handler with no matching entries. + """ + args = MockArgs() + args.boot_id = "qux" + args.title = "Something New" + opts = boom.command._report_opts_from_args(args) + r = boom.command._clone_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__show_cmd(self): + """Test the _show_cmd() handler. + """ + args = MockArgs() + r = boom.command._show_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_cmd_single(self): + """Test the _show_cmd() handler with a single selected entry. + """ + args = MockArgs() + args.boot_id = "61bcc49" + r = boom.command._show_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_cmd_single_identifier(self): + """Test the _show_cmd() handler with a single identifier. + """ + args = MockArgs() + r = boom.command._show_cmd(args, None, None, "61bcc49") + self.assertEqual(r, 0) + + def test__show_cmd_selection(self): + """Test the _show_cmd() handler with multiple selected entries. + """ + args = MockArgs() + args.boot_id = "6" # Matches four entries + r = boom.command._show_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_cmd_invalid_selection(self): + """Test the _show_cmd() handler with an invalid selection. + """ + args = MockArgs() + # Clear boot_id + args.boot_id = None + # Invalid selection criteria for BootEntry type + select = Selection(host_add_opts="qux") + r = boom.command._show_cmd(args, select, None, None) + self.assertEqual(r, 1) + + def test__list_cmd(self): + args = MockArgs() + r = boom.command._list_cmd(args, None, None, None) + self.assertNotEqual(r, 1) + + def test__list_cmd_single(self): + args = MockArgs() + args.boot_id = "61bcc49" + r = boom.command._list_cmd(args, None, None, None) + self.assertNotEqual(r, 1) + + def test__list_cmd_single_identifier(self): + """Test the _list_cmd() handler with a single identifier. + """ + args = MockArgs() + r = boom.command._list_cmd(args, None, None, "61bcc49") + self.assertEqual(r, 0) + + def test__list_cmd_selection(self): + """Test the _list_cmd() handler with multiple selected entries. + """ + args = MockArgs() + args.boot_id = "6" # Matches four entries + r = boom.command._list_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__list_cmd_invalid_selection(self): + """Test the _list_cmd() handler with an invalid selection. + """ + args = MockArgs() + # Clear boot_id + args.boot_id = None + # Invalid selection criteria for BootEntry type + select = Selection(host_add_opts="qux") + r = boom.command._list_cmd(args, select, None, None) + self.assertEqual(r, 1) + + def test__list_cmd_with_options(self): + """Test the _list_cmd() handler with report field options + string. + """ + args = MockArgs() + args.options = "title" + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_cmd_verbose(self): + """Test the _list_cmd() handler with a valid entry and + verbose output. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.verbose = 1 + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__edit_cmd(self): + """Test the _edit_cmd() handler with a valid entry and new + title. + """ + args = MockArgs() + args.boot_id = "61bcc49" + args.title = "Something New" + # Disable device presence checks + args.no_dev = True + opts = boom.command._report_opts_from_args(args) + r = boom.command._edit_cmd(args, None, opts, None) + self.assertNotEqual(r, 1) + + def test__edit_cmd_no_criteria(self): + """Test the _edit_cmd() handler with no valid selection. + """ + args = MockArgs() + args.boot_id = None + args.title = "Something New" + opts = boom.command._report_opts_from_args(args) + r = boom.command._edit_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__edit_cmd_no_matching(self): + """Test the _edit_cmd() handler with no matching entries. + """ + args = MockArgs() + args.boot_id = "qux" + args.title = "Something New" + opts = boom.command._report_opts_from_args(args) + r = boom.command._edit_cmd(args, None, opts, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_bad_identity(self): + """Test the _create_profile_cmd() handler with a non-None + identity argument. + """ + args = MockArgs() + r = boom.command._create_profile_cmd(args, None, None, "12345") + self.assertEqual(r, 1) + + def test__create_profile_cmd(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = "Test OS" + args.short_name = "testos" + args.os_version = "1 (Workstation)" + args.os_version_id = "1" + args.uname_pattern = "to1" + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__create_profile_cmd_no_name(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = None + args.short_name = "testos" + args.os_version = "1 (Workstation)" + args.os_version_id = "1" + args.uname_pattern = "to1" + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_no_short_name(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = "Test OS" + args.short_name = None + args.os_version = "1 (Workstation)" + args.os_version_id = "1" + args.uname_pattern = "to1" + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_no_version(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = "Test OS" + args.short_name = "testos" + args.os_version = None + args.os_version_id = "1" + args.uname_pattern = "to1" + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_no_version_id(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = "Test OS" + args.short_name = "testos" + args.os_version = "1 (Workstation)" + args.os_version_id = None + args.uname_pattern = "to1" + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_no_uname_pattern(self): + """Test the _create_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.name = "Test OS" + args.short_name = "testos" + args.os_version = "1 (Workstation)" + args.os_version_id = "1" + args.uname_pattern = None + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_profile_cmd_from_host(self): + """Test that creation of an OsProfile from /etc/os-release on + the running host succeeds. + """ + # Depending on the machine the test suite is running on it is + # possible that an OsProfile already exists for the system. To + # avoid a collision between an existing host OsProfile and the + # newly created test profile, attempt to delete any existing + # profile from the test sandbox first. + drop_profiles() + host_os_id = OsProfile.from_host_os_release().os_id + load_profiles() + if host_os_id: + try: + delete_profiles(selection=Selection(os_id=host_os_id)) + except Exception: + pass + + args = MockArgs() + args.uname_pattern = "test1" + args.from_host = True + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__create_profile_cmd_from_os_release(self): + """Test creation of an OsProfile from an os-release file. + """ + test_os_release = "tests/os-release/test-os-release" + args = MockArgs() + args.uname_pattern = "test1" + args.os_release = test_os_release + r = boom.command._create_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__create_profile_cmd_invalid_identifier(self): + """Test that _create_profile_cmd() rejects an identifier arg. + """ + args = MockArgs() + identifier = "d4439b7" + r = boom.command._create_profile_cmd(args, None, None, identifier) + self.assertEqual(r, 1) + + def test__delete_profile_cmd_valid_identifier(self): + """Test that _delete_profile_cmd() deletes a profile via a + valid identifier arg. + """ + args = MockArgs() + identifier = "d4439b7" + r = boom.command._delete_profile_cmd(args, None, None, identifier) + self.assertEqual(r, 0) + + def test__delete_profile_cmd_no_selection(self): + """Test that _delete_profile_cmd() returns an error with no + profile selection. + """ + args = MockArgs() + args.profile = None + r = boom.command._delete_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__delete_profiles_cmd_verbose(self): + """Test the _delete_profile_cmd() handler with reporting. + """ + args = MockArgs() + args.profile = "d4439b7" + args.verbose = 1 # enable reporting + r = boom.command._delete_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__delete_profiles_cmd_with_fields(self): + """Test the _delete_profile_cmd() handler with reporting. + """ + args = MockArgs() + args.profile = "d4439b7" + args.options = "osid,osname" + args.verbose = 1 # enable reporting + r = boom.command._delete_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__delete_profiles_cmd_with_bad_fields(self): + """Test the _delete_profile_cmd() handler with reporting. + """ + args = MockArgs() + args.profile = "d4439b7" + args.options = "There,is,water,at,the,bottom,of,the,ocean" + args.verbose = 1 # enable reporting + r = boom.command._delete_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__clone_profile_cmd(self): + """Test the _clone_profile_cmd() handler with a valid os_id and + new name. + """ + args = MockArgs() + args.profile = "d4439b7" + args.short_name = "somethingsomethingsomething profile side" + r = boom.command._clone_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__clone_profile_cmd_no_criteria(self): + """Test the _clone_profile_cmd() handler with no valid selection. + """ + args = MockArgs() + args.profile = None + args.name = "Something Something Something, Profile Side" + r = boom.command._clone_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__clone_profile_cmd_no_matching(self): + """Test the _clone_profile_cmd() handler with no matching entries. + """ + args = MockArgs() + args.profile = "thisisnottheprofileyouarelookingfor" + args.name = "Something Something Something, Profile Side" + r = boom.command._clone_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__show_profile_cmd(self): + """Test the _show_profile() command handler with defaults args. + """ + args = MockArgs() + r = boom.command._show_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_profile_cmd_with_identifier(self): + """Test the _show_profile() command handler with defaults args. + """ + args = MockArgs() + os_id = "d4439b7" + r = boom.command._show_profile_cmd(args, None, None, os_id) + self.assertEqual(r, 0) + + def test__show_profile_cmd_with_profile_arg(self): + """Test the _show_profile() command handler with defaults args. + """ + args = MockArgs() + args.profile = "d4439b7" + r = boom.command._show_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__list_profile_cmd(self): + args = MockArgs() + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_profile_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_profile_cmd_with_identifier(self): + args = MockArgs() + os_id = "d4439b7" + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_profile_cmd(args, None, opts, os_id) + self.assertEqual(r, 0) + + def test__list_profile_cmd_with_profile_arg(self): + args = MockArgs() + args.profile = "d4439b7" + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_profile_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_profile_cmd_with_options(self): + """Test the _list_cmd() handler with report field options + string. + """ + args = MockArgs() + args.options = "osname,osversion" + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_profile_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_profile_cmd_with_verbose(self): + """Test the _list_cmd() handler with report field options + string. + """ + args = MockArgs() + args.verbose = 1 + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_profile_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__edit_profile_cmd(self): + """Test the _edit_profile_cmd() hander with default args. + """ + args = MockArgs() + args.profile = "d4439b7" + args.uname_pattern = "nf26" + args.os_options = "boot and stuff" + r = boom.command._edit_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__edit_profile_cmd_with_identifier(self): + """Test the _edit_profile_cmd() handler with an identifier. + """ + args = MockArgs() + os_id = "d4439b7" + args.uname_pattern = "nf26" + args.os_options = "boot and stuff" + r = boom.command._edit_profile_cmd(args, None, None, os_id) + self.assertEqual(r, 0) + + def test__edit_profile_cmd_ambiguous_identifier(self): + """Test the _edit_profile_cmd() handler with an ambiguous + identifier argument. + """ + args = MockArgs() + os_id = "d" + args.uname_pattern = "nf26" + args.os_options = "boot and stuff" + r = boom.command._edit_profile_cmd(args, None, None, os_id) + self.assertEqual(r, 1) + + def test__edit_profile_cmd_with_options(self): + """Test the _edit_profile_cmd() handler with report control + options. + """ + args = MockArgs() + args.profile = "d4439b7" + args.options = "badoptions" + r = boom.command._edit_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__edit_profile_cmd_edits_identity_keys(self): + """Test the _edit_profile_cmd() handler with invalid profile + key modifications. + """ + args = MockArgs() + args.profile = "d4439b7" + # Can only change via clone + args.name = "Bad Fedora" + r = boom.command._edit_profile_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__clone_profile_cmd(self): + """Test the _clone_profile_cmd() handler with valid args. + """ + args = MockArgs() + args.profile = "d4439b7" + args.name = "NotFedora" + args.short_name = "notfedora" + r = boom.command._clone_profile_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__create_host_cmd_with_identifier(self): + """Test _create_host_cmd() with an invalid identifier arg. + """ + args = MockArgs() + identifier = "badidentity" + r = boom.command._create_host_cmd(args, None, None, identifier) + self.assertEqual(r, 1) + + def test__create_host_cmd_no_name(self): + """Test the _create_host_cmd() handler with no name argument. + """ + args = MockArgs() + args.host_name = None + r = boom.command._create_host_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_host_cmd_no_profile(self): + """Test the _clone_profile_cmd() handler with missing profile + argument. + """ + args = MockArgs() + args.name = "NotFedora" + args.short_name = "notfedora" + r = boom.command._create_host_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__create_host_cmd(self): + """Test the _create_host_cmd() handler with valid args. + """ + args = MockArgs() + args.machine_id = "611f38fd887d41fffffffffffffff000" + args.host_name = "newhost" + args.profile = "d4439b7" + r = boom.command._create_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__delete_host_cmd(self): + """Test the _delete_host_cmd() handler with valid --host-id + argument. + """ + args = MockArgs() + args.host_id = "5ebcb1f" + r = boom.command._delete_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__delete_host_cmd_with_options(self): + """Test the _delete_host_cmd() handler with valid --host-id + argument and report control options. + """ + args = MockArgs() + args.host_id = "5ebcb1f" + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__delete_host_cmd_with_verbose(self): + """Test the _delete_host_cmd() handler with valid --host-id + argument and verbosity. + """ + args = MockArgs() + args.host_id = "5ebcb1f" + args.verbose = 1 + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__delete_host_cmd_with_fields(self): + """Test the _delete_host_cmd() handler with valid --host-id + argument and custom report field options. + """ + args = MockArgs() + args.host_id = "5ebcb1f" + args.options = "hostid,hostname" + opts = boom.command._report_opts_from_args(args) + r = boom.command._delete_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__delete_host_cmd_with_identifier(self): + """Test the _delete_host_cmd() handler with valid identifier + argument. + """ + args = MockArgs() + host_id = "5ebcb1f" + r = boom.command._delete_host_cmd(args, None, None, host_id) + self.assertEqual(r, 0) + + def test__delete_host_cmd_no_selection(self): + """Test the _delete_host_cmd() handler with no valid selection. + """ + args = MockArgs() + r = boom.command._delete_host_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__clone_host_cmd(self): + """Test the _clone_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.host_id = "5ebcb1f" + args.host_name = "new_host" + r = boom.command._clone_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_host_cmd(self): + """Test the _show_host_cmd() handler with valid arguments. + """ + args = MockArgs() + r = boom.command._show_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__show_host_cmd_with_identifier(self): + """Test the _show_host_cmd() handler with valid arguments. + """ + args = MockArgs() + host_id = "1a979bb" + r = boom.command._show_host_cmd(args, None, None, host_id) + self.assertEqual(r, 0) + + def test__show_host_cmd_with_host_id(self): + """Test the _show_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.host_id = "1a979bb" + r = boom.command._show_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__list_host_cmd(self): + """Test the _list_host_cmd() handler with valid arguments. + """ + args = MockArgs() + r = boom.command._list_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__list_host_cmd_with_identifier(self): + """Test the _list_host_cmd() handler with valid arguments. + """ + args = MockArgs() + host_id = "1a979bb" + r = boom.command._list_host_cmd(args, None, None, host_id) + self.assertEqual(r, 0) + + def test__list_host_cmd_with_host_id(self): + """Test the _list_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.host_id = "1a979bb" + r = boom.command._list_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__list_host_cmd_with_options(self): + """Test the _list_host_cmd() handler with valid --host-id + argument and report control options. + """ + args = MockArgs() + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_host_cmd_with_verbose(self): + """Test the _list_host_cmd() handler with valid --host-id + argument and verbosity. + """ + args = MockArgs() + args.verbose = 1 + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__list_host_cmd_with_fields(self): + """Test the _list_host_cmd() handler with valid --host-id + argument and custom report field options. + """ + args = MockArgs() + args.options = "hostid,hostname" + opts = boom.command._report_opts_from_args(args) + r = boom.command._list_host_cmd(args, None, opts, None) + self.assertEqual(r, 0) + + def test__edit_host_cmd(self): + """Test the _edit_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.host_id = "1a979bb" + args.host_name = "notlocalhost" + r = boom.command._edit_host_cmd(args, None, None, None) + self.assertEqual(r, 0) + + def test__edit_host_cmd_with_invalid_options(self): + """Test the _edit_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.options = "bad,touch,ricky,bad,touch" + r = boom.command._edit_host_cmd(args, None, None, None) + self.assertEqual(r, 1) + + def test__edit_host_cmd_with_identifier(self): + """Test the _edit_host_cmd() handler with valid arguments. + """ + args = MockArgs() + args.host_name = "notlocalhost" + host_id = "1a979bb" + r = boom.command._edit_host_cmd(args, None, None, host_id) + self.assertEqual(r, 0) + + def test_boom_main_noargs(self): + args = ['bin/boom', '--help'] + boom.command.main(args) + + def test_boom_main_list(self): + args = ['bin/boom', 'entry', 'list'] + boom.command.main(args) + +# vim: set et ts=4 sw=4 : diff --git a/tests/config_tests.py b/tests/config_tests.py new file mode 100644 index 0000000..ffefb04 --- /dev/null +++ b/tests/config_tests.py @@ -0,0 +1,130 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# config_tests.py - Boom report API tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from os.path import abspath, join +from sys import stdout +import shutil + +try: + # Python2 + from ConfigParser import SafeConfigParser as ConfigParser, ParsingError +except: + # Python3 + from configparser import ConfigParser, ParsingError + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +# Test suite paths +from tests import * + +from boom import * +from boom.config import * +BOOT_ROOT_TEST = abspath("./tests") +set_boot_path(BOOT_ROOT_TEST) + +class ConfigBasicTests(unittest.TestCase): + """Basic tests for the boom.config sub-module. + """ + + def test_sync_config(self): + """Test that the internal _sync_config() helper works. + """ + import boom.config # for _sync_config() + cfg = ConfigParser() + bc = BoomConfig() + + cfg.add_section("global") + cfg.add_section("legacy") + + boot_path = "/boot" + boom_path = "/boot/boom" + legacy_format = "grub1" + + bc.legacy_enabled = False + bc.legacy_sync = False + bc.legacy_format = legacy_format + + bc.boot_path = boot_path + bc.boom_path = boom_path + + boom.config._sync_config(bc, cfg) + self.assertEqual(cfg.get("legacy", "enable"), "no") + self.assertEqual(cfg.get("legacy", "sync"), "no") + self.assertEqual(cfg.get("legacy", "format"), legacy_format) + self.assertEqual(cfg.get("global", "boot_root"), boot_path) + self.assertEqual(cfg.get("global", "boom_root"), boom_path) + + +class ConfigTests(unittest.TestCase): + # The set of configuration files to use for this test class + conf_path = join(BOOT_ROOT_TEST, "boom_configs/default/boot") + + # The path to the boot directory in the test sandbox + boot_path = join(SANDBOX_PATH, "boot") + + # The path to the sandbox boom.conf configuration file + boom_conf = join(boot_path, "boom/boom.conf") + + def setUp(self): + """Set up a test fixture for the ConfigTests class. + """ + reset_sandbox() + + # Sandbox paths + shutil.copytree(self.conf_path, join(SANDBOX_PATH, "boot")) + # Set boom paths + set_boot_path(self.boot_path) + + def tearDown(self): + rm_sandbox() + reset_boom_paths() + + def test_get_boom_config_path(self): + """Test that the correct boom.conf path is returned from a call + to the `get_boom_config_path()` function. + """ + conf_path = self.boom_conf + self.assertEqual(get_boom_config_path(), conf_path) + + def test_set_boom_config_path_abs(self): + """Test that the correct boom.conf path is returned from a call + to the `get_boom_config_path()` function when an absolute + path is given. + """ + conf_dir = join(SANDBOX_PATH, "boot/boom") + conf_path = join(conf_dir, "boom.conf") + set_boom_config_path(conf_dir) + self.assertEqual(get_boom_config_path(), conf_path) + + def test_load_boom_config_default(self): + """Test the `load_boom_config()` function with the default + configuration file. + """ + load_boom_config() + +class BadConfigTests(ConfigTests): + # The set of configuration files to use for this test class + conf_path = join(BOOT_ROOT_TEST, "boom_configs/badconfig/boot") + + def test_load_boom_config_default(self): + """Test the `load_boom_config()` function with the default + configuration file. + """ + with self.assertRaises(ValueError) as cm: + load_boom_config() + +# vim: set et ts=4 sw=4 : diff --git a/tests/grub/grub.conf b/tests/grub/grub.conf new file mode 100644 index 0000000..96dbc19 --- /dev/null +++ b/tests/grub/grub.conf @@ -0,0 +1,159 @@ +# grub.conf generated by anaconda +# +# Note that you do not have to rerun grub after making changes to this file +# NOTICE: You have a /boot partition. This means that +# all kernel and initrd paths are relative to /boot/, eg. +# root (hd0,0) +# kernel /vmlinuz-version ro root=/dev/mapper/vg_mother-lv_root +# initrd /initrd-[generic-]version.img +#boot=/dev/sdb +default=0 +timeout=5 +splashimage=(hd0,0)/grub/splash.xpm.gz +hiddenmenu +title Red Hat Enterprise Linux Server (2.6.32-621.el6.x86_64) + root (hd0,0) + kernel /vmlinuz-2.6.32-621.el6.x86_64 ro root=/dev/vg_mother/lv_root rd_NO_LUKS KEYBOARDTYPE=pc KEYTABLE=uk LANG=en_US.UTF-8 rd_NO_MD quiet SYSFONT=latarcyrheb-sun16 rhgb crashkernel=auto rd_LVM_LV=vg_mother/lv_root rd_LVM_LV=vg_mother/lv_swap0 rd_NO_DM + initrd /initramfs-2.6.32-621.el6.x86_64.img +title Red Hat Enterprise Linux Server (2.6.32-573.3.1.el6.x86_64) + root (hd0,0) + kernel /vmlinuz-2.6.32-573.3.1.el6.x86_64 ro root=/dev/vg_mother/lv_root rd_NO_LUKS KEYBOARDTYPE=pc KEYTABLE=uk LANG=en_US.UTF-8 rd_NO_MD quiet SYSFONT=latarcyrheb-sun16 rhgb crashkernel=auto rd_LVM_LV=vg_mother/lv_root rd_LVM_LV=vg_mother/lv_swap0 rd_NO_DM + initrd /initramfs-2.6.32-573.3.1.el6.x86_64.img +title Red Hat Enterprise Linux Server (2.6.32-504.16.2.el6.x86_64) + root (hd0,0) + kernel /vmlinuz-2.6.32-504.16.2.el6.x86_64 ro root=/dev/vg_mother/lv_root rd_NO_LUKS KEYBOARDTYPE=pc KEYTABLE=uk LANG=en_US.UTF-8 rd_NO_MD quiet SYSFONT=latarcyrheb-sun16 rhgb crashkernel=auto rd_LVM_LV=vg_mother/lv_root rd_LVM_LV=vg_mother/lv_swap0 rd_NO_DM + initrd /initramfs-2.6.32-504.16.2.el6.x86_64.img +#--- BOOM_Grub1_BEGIN --- +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.10 root=/dev/vg00/lvol0-snap9 ro rd.lvm.lv=vg00/lvol0-snap9 rootflags=subvolid=23 rhgb quiet + initrd /initramfs-3.3.10.img +title ANEWTITLE + root (hd0,0) + kernel /vmlinuz-3.3.30 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 + initrd /initrd.img-3.3.30 +title add_del_opts + root (hd0,0) + kernel /vmlinuz-3.10-1.el7.fc24.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root debug + initrd /initramfs-3.10-1.el7.fc24.x86_64.img +title ANOTHERTITLE2 + root (hd0,0) + kernel /vmlinuz-3.10-23.el7 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet + initrd /initramfs-3.10-23.el7.img +title ANEWTITLE + root (hd0,0) + kernel /vmlinuz-3.3.40 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 + initrd /initrd.img-3.3.40 +title ANOTHERTITLE + root (hd0,0) + kernel /vmlinuz-3.3.60-12.fc24.x86_64 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet + initrd /initramfs-3.3.60-12.fc24.x86_64.img +title Red Hat Enterprise Linux 7.2 (Maipo) 3.10-23.el7 + root (hd0,0) + kernel /vmlinuz-3.10-23.el7 root=/dev/sda5 ro rhgb quiet + initrd /initramfs-3.10-23.el7.img +title Some other snapshot + root (hd0,0) + kernel /vmlinuz-4.11.12-100.fc24.x86_64 root=/dev/vg00/lvol0-snapshot2 ro rd.lvm.lv=vg00/lvol0-snapshot2 rhgb quiet + initrd /initramfs-4.11.12-100.fc24.x86_64.img +title qux + root (hd0,0) + kernel /vmlinuz-3.3.4 root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rhgb quiet + initrd /initramfs-3.3.4.img +title Some snapshot + root (hd0,0) + kernel /vmlinuz-4.11.12-100.fc24.x86_64 root=/dev/vg00/lvol0-snapshot ro rd.lvm.lv=vg00/lvol0-snapshot rhgb quiet + initrd /initramfs-4.11.12-100.fc24.x86_64.img +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.4 root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rhgb quiet + initrd /initramfs-3.3.4.img +title title + root (hd0,0) + kernel /vmlinuz-1.1.1-1.fc24.x86_64 root=/dev/vg_root/root ro rd.lvm.lv=vg_root/root rhgb quiet + initrd /initramfs-1.1.1-1.fc24.x86_64.img +title ANEWTITLE + root (hd0,0) + kernel /vmlinuz-3.10.1-1.el7 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet + initrd /initramfs-3.10.1-1.el7.img +title A NEWER TITLE + root (hd0,0) + kernel /vmlinuz-7.7.7 root=/dev/vg_qux/lv_qux ro rd.lvm.lv=vg_qux/lv_qux rhgb quiet + initrd /initramfs-7.7.7.img +title Fedora (4.1.1-100.fc24.x86_64) 24 (Workstation Edition) + root (hd0,0) + kernel /vmlinuz-4.1.1-100.fc24 root=/dev/sda5 ro rootflags=subvolid=23 rhgb quiet + initrd /initramfs-4.1.1-100.fc24.img +title ANEWTITLE + root (hd0,0) + kernel /vmlinuz-3.3.50 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 + initrd /initrd.img-3.3.50 +title title + root (hd0,0) + kernel /vmlinuz-2.2.2-2.fc24.x86_64 root=/dev/vg_root/root ro rootflags=subvol=/snapshot/today rhgb quiet + initrd /initramfs-2.2.2-2.fc24.x86_64.img +title Clone test1 + root (hd0,0) + kernel /vmlinuz-4.16.11-100.fc26.x86_64 BOOT_IMAGE=/vmlinuz-4.16.11-100.fc26.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet debug + initrd /initramfs-4.16.11-100.fc26.x86_64.img +title ANEWTITLE + root (hd0,0) + kernel /vmlinuz-3.3.30 root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap + initrd /initrd.img-3.3.30 +title RHEL7 snapshot + root (hd0,0) + kernel /vmlinuz-3.10-272.el7 root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet + initrd /initramfs-3.10-272.el7.img +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.5 root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet + initrd /initramfs-3.3.5.img +title ANEWERTITLE3 + root (hd0,0) + kernel /vmlinuz-3.3.30 root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 + initrd /initrd.img-3.3.30 +title A NEW TEST TITLE + root (hd0,0) + kernel /vmlinuz-4.14.14-200.fc26.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root qux debug + initrd /initramfs-4.14.14-200.fc26.x86_64.img +title ANOTHERTITLE3 + root (hd0,0) + kernel /vmlinuz-3.3.10 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 + initrd /initrd.img-3.3.10 +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.10 root=/dev/vg00/lvol0-snap2 ro rootflags=subvolid=23 rhgb quiet + initrd /initramfs-3.3.10.img +title ANEWERTITLE2 + root (hd0,0) + kernel /vmlinuz-3.3.30 root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap + initrd /initrd.img-3.3.30 +title title + root (hd0,0) + kernel vmlinuz-2.2.2-2.fc24.x86_64 root=/dev/vg_root/root ro rootflags=subvol=/snapshot/today rhgb quiet + initrd initramfs-2.2.2-2.fc24.x86_64.img +title title + root (hd0,0) + kernel vmlinuz-1.1.1-1.fc24.x86_64 root=/dev/vg_root/root ro rd.lvm.lv=vg_root/root rhgb quiet + initrd initramfs-1.1.1-1.fc24.x86_64.img +title Red Hat Enterprise Linux Server (3.10-1.el7.fc24.x86_64) 7.2 (Maipo) + root (hd0,0) + kernel /vmlinuz-3.10-1.el7.fc24.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet + initrd /initramfs-3.10-1.el7.fc24.x86_64.img +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.10 root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rootflags=subvolid=23 rhgb quiet + initrd /initramfs-3.3.10.img +title ANOTHERTITLE + root (hd0,0) + kernel /vmlinuz-3.3.60 root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 + initrd /initrd.img-3.3.60 +title clone with addopts + root (hd0,0) + kernel /vmlinuz-3.10-1.el7.fc24.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet debug + initrd /initramfs-3.10-1.el7.fc24.x86_64.img +title ATITLE + root (hd0,0) + kernel /vmlinuz-3.3.9 root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet + initrd /initramfs-3.3.9.img +#--- BOOM_Grub1_END --- diff --git a/tests/grub2/grub.cfg b/tests/grub2/grub.cfg new file mode 100644 index 0000000..4e94cc8 --- /dev/null +++ b/tests/grub2/grub.cfg @@ -0,0 +1,10 @@ +# +# Fake grub.cfg for check_bootloader() tests. +# + +### BEGIN /etc/grub.d/42_boom ### +submenu "Snapshots" { + insmod blscfg + bls_import +} +### END /etc/grub.d/42_boom ### diff --git a/tests/hostprofile_tests.py b/tests/hostprofile_tests.py new file mode 100644 index 0000000..aa8b537 --- /dev/null +++ b/tests/hostprofile_tests.py @@ -0,0 +1,572 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# osprofile_tests.py - Boom OS profile tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from sys import stdout +from os import listdir, makedirs +from os.path import abspath, exists, join +import shutil + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +from boom.osprofile import * +from boom.hostprofile import * +from boom.bootloader import * +from boom import * + +# For private member validation checks +import boom.hostprofile + +from tests import * + +BOOT_ROOT_TEST = abspath("./tests") +set_boot_path(BOOT_ROOT_TEST) + +BOOM_ENTRY_MACHINE_ID="BOOM_ENTRY_MACHINE_ID" + +def _count_value_in_key(dir_path, ext, key_name, xvalue, + exact=False, default=False): + """Helper function to count the number of times ``xvalue`` appears + as the value of key ``key_name`` in the set of files found in + ``dir_path`` having file extension ``ext`. + + If ``exact`` is true the value is compared for equality. + Otherwise the value is counted if the string ``value`` appears + anywhere within the named key value. + + If ``default`` is ``True``, and the key is not found in a + profile, it is assumed that ``xvalue`` is the default value, + and that the relevant profile has inherited this value from + the embedded ``OsProfile`` defaults. + + This is used to count profiles by property independently of + the ``boom`` library modules for comparison of results + returned from the API. + """ + count = 0 + filecount = 1 + for f_name in listdir(dir_path): + if not f_name.endswith(ext): + continue + f_path = join(dir_path, f_name) + found_in_file = False + with open(f_path, "r") as f: + for line in f.readlines(): + (key, value) = parse_name_value(line) + if key not in HOST_PROFILE_KEYS: + continue + if key == key_name: + found_in_file = True + if not exact: + count += 1 if xvalue in value else 0 + else: + count += 1 if xvalue == value else 0 + if default and not found_in_file: + count += 1 + filecount += 1 + return count + + +class HostProfileTests(unittest.TestCase): + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + # Master BLS loader directory for sandbox + loader_path = join(BOOT_ROOT_TEST, "loader") + + def setUp(self): + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + loader_sandbox = join(SANDBOX_PATH, "boot/loader") + + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + shutil.copytree(self.loader_path, loader_sandbox) + + set_boot_path(boot_sandbox) + + drop_host_profiles() + drop_profiles() + + def tearDown(self): + drop_host_profiles() + drop_profiles() + rm_sandbox() + reset_boom_paths() + + # Module tests + def test_import(self): + import boom.hostprofile + + # Profile store tests + + def test_load_profiles(self): + # Test that loading the test profiles succeeds. + load_host_profiles() + + # HostProfile tests + + def test_HostProfile__str__(self): + load_profiles() + load_host_profiles() + hp = HostProfile(machine_id="ffffffffffffffff", host_name="localhost", + label='', os_id="3fc389b") + + xstr = ( + 'Host ID: "83fb23b393d6460e18e3694a8766b06ade021c3f",\n' + 'Host name: "localhost",\nMachine ID: "ffffffffffffffff",\n' + 'OS ID: "3fc389bba581e5b20c6a46c7fc31b04be465e973",\n' + 'Host label: "",\nName: "Red Hat Enterprise Linux Server", ' + 'Short name: "rhel", Version: "7.2 (Maipo)",\n' + 'Version ID: "7.2", UTS release pattern: "el7",\n' + 'Kernel pattern: "/vmlinuz-%{version}", ' + 'Initramfs pattern: "/initramfs-%{version}.img",\n' + 'Root options (LVM2): "rd.lvm.lv=%{lvm_root_lv}",\n' + 'Root options (BTRFS): "rootflags=%{btrfs_subvolume}",\n' + 'Options: "root=%{root_device} ro %{root_opts} rhgb quiet"' + ) + + self.assertEqual(str(hp), xstr) + hp.delete_profile() + + def test_HostProfile__repr__(self): + load_profiles() + load_host_profiles() + hp = HostProfile(machine_id="ffffffffffffffff", host_name="localhost", + label='', os_id="3fc389b") + + xrepr = ('HostProfile(profile_data={' + 'BOOM_HOST_ID:"83fb23b393d6460e18e3694a8766b06ade021c3f", ' + 'BOOM_HOST_NAME:"localhost", ' + 'BOOM_ENTRY_MACHINE_ID:"ffffffffffffffff", ' + 'BOOM_OS_ID:"3fc389bba581e5b20c6a46c7fc31b04be465e973", ' + 'BOOM_HOST_LABEL:"", ' + 'BOOM_OS_NAME:"Red Hat Enterprise Linux Server", ' + 'BOOM_OS_SHORT_NAME:"rhel", BOOM_OS_VERSION:"7.2 (Maipo)", ' + 'BOOM_OS_VERSION_ID:"7.2", BOOM_OS_UNAME_PATTERN:"el7", ' + 'BOOM_OS_KERNEL_PATTERN:"/vmlinuz-%{version}", ' + 'BOOM_OS_INITRAMFS_PATTERN:"/initramfs-%{version}.img", ' + 'BOOM_OS_ROOT_OPTS_LVM2:"rd.lvm.lv=%{lvm_root_lv}", ' + 'BOOM_OS_ROOT_OPTS_BTRFS:"rootflags=%{btrfs_subvolume}", ' + 'BOOM_OS_OPTIONS:"root=%{root_device} ro %{root_opts} rhgb ' + 'quiet"})' + ) + + self.assertEqual(repr(hp), xrepr) + hp.delete_profile() + + def test_HostProfile(self): + # Test HostProfile init from kwargs + with self.assertRaises(ValueError) as cm: + hp = HostProfile(host_name="localhost", os_id="3fc389b") + with self.assertRaises(ValueError) as cm: + hp = HostProfile(machine_id="ffffffffffffffff", host_name="localhost") + with self.assertRaises(ValueError) as cm: + hp = HostProfile(machine_id="ffffffffffffffff", os_id="3fc389b") + + hp = HostProfile(machine_id="ffffffffffffffff", host_name="localhost", + os_id="3fc389b", label='') + + self.assertTrue(hp) + + # os_id for RHEL-7.2 + self.assertEqual(hp.os_id, "3fc389bba581e5b20c6a46c7fc31b04be465e973") + + hp.delete_profile() + + def test_HostProfile_from_profile_data(self): + profile_data = { + BOOM_ENTRY_MACHINE_ID: "fffffffffffffff", + BOOM_HOST_NAME: "localhost", + BOOM_OS_ID: "3fc389bba581e5b20c6a46c7fc31b04be465e973", + BOOM_OS_KERNEL_PATTERN: "/vmlinuz-%{version}", + BOOM_OS_INITRAMFS_PATTERN: "/initramfs-%{version}.img", + BOOM_OS_ROOT_OPTS_LVM2: "rd.lvm.lv=%{lvm_root_lv} rh", + BOOM_OS_ROOT_OPTS_BTRFS: "rootflags=%{btrfs_subvolume} rh", + BOOM_OS_OPTIONS: "root=%{root_device} %{root_opts} rhgb quiet" + } + + hp = HostProfile(profile_data=profile_data) + self.assertTrue(hp) + + # Assert that overrides are present + self.assertEqual(hp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv} rh") + self.assertEqual(hp.root_opts_btrfs, "rootflags=%{btrfs_subvolume} rh") + + hp.delete_profile() + + # Remove the root options keys. + profile_data.pop(BOOM_OS_ROOT_OPTS_LVM2, None) + profile_data.pop(BOOM_OS_ROOT_OPTS_BTRFS, None) + hp = HostProfile(profile_data=profile_data) + + # Assert that defaults are restored + self.assertEqual(hp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv}") + self.assertEqual(hp.root_opts_btrfs, "rootflags=%{btrfs_subvolume}") + + hp.delete_profile() + + # Remove the name key. + profile_data.pop(BOOM_HOST_NAME, None) + with self.assertRaises(ValueError) as cm: + hp = HostProfile(profile_data=profile_data) + + def test_HostProfile_properties(self): + hp = HostProfile(machine_id="fffffffffffffff", host_name="localhost", + os_id="3fc389b", label="") + hp.kernel_pattern = "/vmlinuz-%{version}" + hp.initramfs_pattern = "/initramfs-%{version}.img" + hp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + hp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + hp.options = "root=%{root_device} %{root_opts} rhgb quiet" + + self.assertEqual(hp.host_name, "localhost") + self.assertEqual(hp.machine_id, "fffffffffffffff") + self.assertEqual(hp.kernel_pattern, "/vmlinuz-%{version}") + self.assertEqual(hp.initramfs_pattern, + "/initramfs-%{version}.img") + self.assertEqual(hp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv}") + self.assertEqual(hp.root_opts_btrfs, + "rootflags=%{btrfs_subvolume}") + self.assertEqual(hp.options, + "root=%{root_device} %{root_opts} rhgb quiet") + hp.delete_profile() + + def test_HostProfile_write(self): + hp = HostProfile(machine_id="fffffffffffffff", host_name="localhost", + os_id="3fc389b", label="") + hp.write_profile() + profile_path = join(boom_host_profiles_path(), + "%s-%s.host" % (hp.host_id, hp.host_name)) + self.assertTrue(exists(profile_path)) + hp.delete_profile() + + @unittest.skipIf(have_root(), "DAC controls do not apply to root") + def test_load_host_profiles_no_read(self): + # Set the /boot path to a non-writable path for the test user. + set_boot_path("/boot") + with self.assertRaises(OSError) as cm: + load_host_profiles() + # Re-set test /boot + set_boot_path(BOOT_ROOT_TEST) + + def test_write_host_profiles_fail(self): + load_host_profiles() + # Set the /boot path to a non-writable path for the test user. + set_boot_path("/boot") + # Create a dirty profile to write + hp = HostProfile(machine_id="ffffffffffffff1", host_name="localhost", + os_id="3fc389b", label="") + write_host_profiles() + + # Clean up dummy profile + hp.delete_profile() + # Re-set test /boot + set_boot_path(BOOT_ROOT_TEST) + + def test_osprofile_write_profiles(self): + load_host_profiles() + write_host_profiles() + + def test_hostprofile_find_profiles_by_id(self): + # Reload host profiles from disk: a failure to clean up in an + # earlier test may have left the profile list in an inconsistent + # state. + host_id = "6af0980a6607b20cda34a45d2869c9be020914b4" + load_host_profiles() + hp = HostProfile(machine_id="fffffffffffffff", host_name="localhost", + os_id="3fc389b", label="testhp") + hp.write_profile() + hp_list = find_host_profiles(selection=Selection(host_id=host_id)) + self.assertEqual(len(hp_list), 1) + self.assertEqual(hp_list[0].host_id, host_id) + hp.delete_profile() + + def test_hostprofile_find_profiles_by_host_name(self): + host_name = "localhost" + hp_list = find_host_profiles(selection=Selection(host_name=host_name)) + nr_profiles = 0 + for f in listdir(boom_profiles_path()): + if f.endswith(host_name + ".host"): + nr_profiles += 1 + self.assertTrue(len(hp_list), nr_profiles) + + def test_hostprofile_find_profiles_by_add_opts(self): + add_opts = "debug" + select = Selection(host_add_opts=add_opts) + hp_list = find_host_profiles(selection=select) + # Adjusted to current test data + self.assertEqual(1, len(hp_list)) + self.assertEqual(add_opts, hp_list[0].add_opts) + + def test_hostprofile_find_profiles_by_del_opts(self): + del_opts = "rhgb quiet" + select = Selection(host_del_opts=del_opts) + hp_list = find_host_profiles(selection=select) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.del_opts == del_opts: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(del_opts, hp_list[0].del_opts) + + def test_hostprofile_find_profiles_by_os_id(self): + os_id = "3fc389b" + hp_list = find_host_profiles(selection=Selection(os_id=os_id)) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.os_id.startswith(os_id): + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertTrue(hp_list[0].os_id.startswith(os_id)) + + def test_hostprofile_find_profiles_by_os_name(self): + os_name = "Fedora" + hp_list = find_host_profiles(selection=Selection(os_name=os_name)) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.os_name == os_name: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(os_name, hp_list[0].os_name) + + def test_hostprofile_find_profiles_by_os_short_name(self): + os_short_name = "fedora" + select = Selection(os_short_name=os_short_name) + hp_list = find_host_profiles(selection=select) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.os_short_name == os_short_name: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(os_short_name, hp_list[0].os_short_name) + + def test_hostprofile_find_profiles_by_os_version(self): + os_version = "7.2 (Maipo)" + select = Selection(os_version=os_version) + hp_list = find_host_profiles(selection=select) + # Adjusted to current test data + self.assertEqual(2, len(hp_list)) + self.assertEqual(os_version, hp_list[0].os_version) + + def test_hostprofile_find_profiles_by_os_version_id(self): + os_version_id = "7.2" + select = Selection(os_version_id=os_version_id) + hp_list = find_host_profiles(selection=select) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.os_version_id == os_version_id: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(os_version_id, hp_list[0].os_version_id) + + def test_hostprofile_find_profiles_by_uname_pattern(self): + uname_pattern = "el7" + select = Selection(os_uname_pattern=uname_pattern) + hp_list = find_host_profiles(selection=select) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.uname_pattern == uname_pattern: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(uname_pattern, hp_list[0].uname_pattern) + + def test_hostprofile_find_profiles_by_kernel_pattern(self): + kernel_pattern = "/vmlinuz-%{version}" + select = Selection(os_kernel_pattern=kernel_pattern) + hp_list = find_host_profiles(selection=select) + + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.kernel_pattern == kernel_pattern: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(kernel_pattern, hp_list[0].kernel_pattern) + + # Non-matching + kernel_pattern = "NOTAREALKERNELPATTERN" + select = Selection(os_kernel_pattern=kernel_pattern) + hp_list = find_host_profiles(selection=select) + self.assertFalse(hp_list) + + def test_hostprofile_find_profiles_by_initramfs_pattern(self): + initramfs_pattern = "/initramfs-%{version}.img" + select = Selection(os_initramfs_pattern=initramfs_pattern) + hp_list = find_host_profiles(selection=select) + + host_path = boom_host_profiles_path() + profile_count = _count_value_in_key(host_path, ".host", + BOOM_OS_INITRAMFS_PATTERN, + initramfs_pattern, + exact=True, default=True) + + self.assertEqual(profile_count, len(hp_list)) + self.assertEqual(initramfs_pattern, hp_list[0].initramfs_pattern) + + # Non-matching + initramfs_pattern = "NOTAREALINITRAMFSPATTERN" + select = Selection(os_initramfs_pattern=initramfs_pattern) + hp_list = find_host_profiles(selection=select) + # Adjusted to current test data + self.assertFalse(hp_list) + + def test_hostprofile_find_profiles_by_options(self): + options = "root=%{root_device} ro %{root_opts} rhgb quiet" + select = Selection(os_options=options) + hp_list = find_host_profiles(selection=select) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.options == options: + nr_hps += 1 + self.assertEqual(nr_hps, len(hp_list)) + self.assertEqual(options, hp_list[0].options) + + # Non-matching + options = "root=/dev/nodev ro rhgb quiet" + select = Selection(os_options=options) + hp_list = find_host_profiles(selection=select) + # Adjusted to current test data + self.assertFalse(hp_list) + + def test_hostprofile_find_host_profiles_not_loaded(self): + # Find with automatic load + hps = find_host_profiles() + self.assertTrue(hps) + + def test_host_min_id_width(self): + import boom.hostprofile + xwidth = 7 # Adjusted to current test data + load_host_profiles() + width = boom.hostprofile.min_host_id_width() + self.assertEqual(xwidth, width) + + def test_machine_min_id_width(self): + import boom.hostprofile + xwidth = 13 # Adjusted to current test data + load_host_profiles() + width = boom.hostprofile.min_machine_id_width() + self.assertEqual(xwidth, width) + + def test_find_host_host_id(self): + # Non-existent host_id + hps = find_host_profiles(Selection(host_id="fffffff")) + self.assertFalse(hps) + + host_id1 = "373ccd1" + # Valid single host_id + hps = find_host_profiles(Selection(host_id=host_id1)) + self.assertTrue(hps) + + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.host_id.startswith(host_id1): + nr_hps += 1 + self.assertEqual(nr_hps, len(hps)) + + # Two host profiles exist for this host_id (and with no label). + # This is because this host is used for "hand edited host" + # testing. Although this is not a valid configuratio that can + # be reached using the Boom CLI it is still expected to work + # and to produce consistent API behaviour. + host_id2 = "5ebcb1f" + hps = find_host_profiles(Selection(host_id=host_id2)) + self.assertTrue(hps) + + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.host_id.startswith(host_id2): + nr_hps += 1 + self.assertEqual(nr_hps, len(hps)) + + # Valid host_id scoped by label + host_label = "ALABEL" + hps = find_host_profiles(Selection(host_id=host_id1, + host_label=host_label)) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.host_id.startswith(host_id1): + if hp.label == host_label: + nr_hps += 1 + self.assertTrue(hps) + self.assertEqual(nr_hps, len(hps)) + + def test_find_host_host_name(self): + host_name = "localhost.localdomain" + hps = find_host_profiles(Selection(host_name=host_name)) + self.assertTrue(hps) + nr_hps = 0 + for hp in boom.hostprofile._host_profiles: + if hp.host_name == host_name: + nr_hps += 1 + self.assertEqual(nr_hps, len(hps)) + + def test_find_host_host_short_name(self): + load_host_profiles() + host_name = "localhost" + hps = find_host_profiles(Selection(host_short_name=host_name)) + self.assertTrue(hps) + + host_path = boom_host_profiles_path() + profile_count = _count_value_in_key(host_path, ".host", + BOOM_HOST_NAME, host_name) + # Adjusted to current test data + self.assertEqual(profile_count, len(hps)) + + def test_find_host_host_label(self): + hps = find_host_profiles(Selection(host_label="ALABEL")) + self.assertTrue(hps) + # Adjusted to current test data + self.assertEqual(1, len(hps)) + + def test_get_host_profile_by_id(self): + load_host_profiles() + m_id1 = "fffffffffffffff" + hp = get_host_profile_by_id(m_id1) + self.assertEqual(m_id1, hp.machine_id) + + def test_get_host_profile_by_id_not_loaded(self): + m_id1 = "fffffffffffffff" + + + hp = get_host_profile_by_id(m_id1) + self.assertEqual(m_id1, hp.machine_id) + + def test_get_host_profile_by_id_and_label(self): + m_id1 = "611f38fd887d41dea7ffffffffffff" + label = "ALABEL" + hp = get_host_profile_by_id(m_id1, label=label) + self.assertEqual(m_id1, hp.machine_id) + self.assertEqual(label, hp.label) + + def test_get_host_profile_by_id_no_match(self): + m_id1 = "bazquxfoo" + hp = get_host_profile_by_id(m_id1) + self.assertFalse(hp) + + def test_match_host_profile(self): + bes = find_entries(Selection(boot_id="dc5f44d")) + self.assertTrue(bes) + be = bes[0] + self.assertTrue(be) + machine_id = be.machine_id + hp = match_host_profile(be) + self.assertTrue(hp) + self.assertEqual(be.machine_id, hp.machine_id) + +# vim: set et ts=4 sw=4 : diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf new file mode 100644 index 0000000..5d2fc26 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-12a2696-4.11.12-100.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title Some other snapshot +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 4.11.12-100.fc24.x86_64 +linux /vmlinuz-4.11.12-100.fc24.x86_64 +initrd /initramfs-4.11.12-100.fc24.x86_64.img +options root=/dev/vg00/lvol0-snapshot2 ro rd.lvm.lv=vg00/lvol0-snapshot2 rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-463ae3c-2.2.2-2.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-463ae3c-2.2.2-2.fc24.x86_64.conf new file mode 100644 index 0000000..08f86b5 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-463ae3c-2.2.2-2.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 9cb53ddda889d6285fd9ab985a4c47025884999f +title title +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 2.2.2-2.fc24.x86_64 +linux /vmlinuz-2.2.2-2.fc24.x86_64 +initrd /initramfs-2.2.2-2.fc24.x86_64.img +options root=/dev/vg_root/root ro rootflags=subvol=/snapshot/today rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-676709f-3.3.10.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-676709f-3.3.10.conf new file mode 100644 index 0000000..7c8435e --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-676709f-3.3.10.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANOTHERTITLE3 +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.3.10 +linux /vmlinuz-3.3.10 +initrd /initrd.img-3.3.10 +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-78861b7-3.10-1.el7.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-78861b7-3.10-1.el7.fc24.x86_64.conf new file mode 100644 index 0000000..62c1327 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-78861b7-3.10-1.el7.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title add_del_opts +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-1.el7.fc24.x86_64 +linux /vmlinuz-3.10-1.el7.fc24.x86_64 +initrd /initramfs-3.10-1.el7.fc24.x86_64.img +options root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root debug diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf new file mode 100644 index 0000000..46abc86 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-881f6e0-3.10-23.el7.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title ANOTHERTITLE2 +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-23.el7 +linux /vmlinuz-3.10-23.el7 +initrd /initramfs-3.10-23.el7.img +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-89b01a8-1.1.1-1.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-89b01a8-1.1.1-1.fc24.x86_64.conf new file mode 100644 index 0000000..c99f693 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-89b01a8-1.1.1-1.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 9cb53ddda889d6285fd9ab985a4c47025884999f +title title +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 1.1.1-1.fc24.x86_64 +linux /vmlinuz-1.1.1-1.fc24.x86_64 +initrd /initramfs-1.1.1-1.fc24.x86_64.img +options root=/dev/vg_root/root ro rd.lvm.lv=vg_root/root rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-92761c2-3.10-1.el7.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-92761c2-3.10-1.el7.fc24.x86_64.conf new file mode 100644 index 0000000..5b21f47 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-92761c2-3.10-1.el7.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title clone with addopts +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-1.el7.fc24.x86_64 +linux /vmlinuz-3.10-1.el7.fc24.x86_64 +initrd /initramfs-3.10-1.el7.fc24.x86_64.img +options root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet debug diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-943778d-3.10-1.el7.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-943778d-3.10-1.el7.fc24.x86_64.conf new file mode 100644 index 0000000..dbfd7e7 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-943778d-3.10-1.el7.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title Red Hat Enterprise Linux Server (3.10-1.el7.fc24.x86_64) 7.2 (Maipo) +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-1.el7.fc24.x86_64 +linux /vmlinuz-3.10-1.el7.fc24.x86_64 +initrd /initramfs-3.10-1.el7.fc24.x86_64.img +options root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-a16356e-4.16.11-100.fc26.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-a16356e-4.16.11-100.fc26.x86_64.conf new file mode 100644 index 0000000..a73f087 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-a16356e-4.16.11-100.fc26.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: d4439b7d2f928c39f1160c0b0291407e5990b9e0 +title Clone test1 +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 4.16.11-100.fc26.x86_64 +linux /vmlinuz-4.16.11-100.fc26.x86_64 +initrd /initramfs-4.16.11-100.fc26.x86_64.img +options BOOT_IMAGE=/vmlinuz-4.16.11-100.fc26.x86_64 root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root rhgb quiet debug diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bc0ea6d-3.10-23.el7.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bc0ea6d-3.10-23.el7.conf new file mode 100644 index 0000000..0c9ce41 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bc0ea6d-3.10-23.el7.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title Red Hat Enterprise Linux 7.2 (Maipo) 3.10-23.el7 +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-23.el7 +linux /vmlinuz-3.10-23.el7 +initrd /initramfs-3.10-23.el7.img +options root=/dev/sda5 ro rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bca58f1-4.1.1-100.fc24.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bca58f1-4.1.1-100.fc24.conf new file mode 100644 index 0000000..b53a3b4 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-bca58f1-4.1.1-100.fc24.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 9cb53ddda889d6285fd9ab985a4c47025884999f +title Fedora (4.1.1-100.fc24.x86_64) 24 (Workstation Edition) +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 4.1.1-100.fc24 +linux /vmlinuz-4.1.1-100.fc24 +initrd /initramfs-4.1.1-100.fc24.img +options root=/dev/sda5 ro rootflags=subvolid=23 rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf new file mode 100644 index 0000000..ea01823 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-c751c79-3.10-272.el7.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title RHEL7 snapshot +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 3.10-272.el7 +linux /vmlinuz-3.10-272.el7 +initrd /initramfs-3.10-272.el7.img +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-db02de8-1.1.1-1.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-db02de8-1.1.1-1.fc24.x86_64.conf new file mode 100644 index 0000000..6669b47 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-db02de8-1.1.1-1.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 9cb53ddda889d6285fd9ab985a4c47025884999f +title title +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 1.1.1-1.fc24.x86_64 +linux vmlinuz-1.1.1-1.fc24.x86_64 +initrd initramfs-1.1.1-1.fc24.x86_64.img +options root=/dev/vg_root/root ro rd.lvm.lv=vg_root/root rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf new file mode 100644 index 0000000..180f032 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-debfd7f-4.11.12-100.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title Some snapshot +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 4.11.12-100.fc24.x86_64 +linux /vmlinuz-4.11.12-100.fc24.x86_64 +initrd /initramfs-4.11.12-100.fc24.x86_64.img +options root=/dev/vg00/lvol0-snapshot ro rd.lvm.lv=vg00/lvol0-snapshot rhgb quiet diff --git a/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-feb2d5c-2.2.2-2.fc24.x86_64.conf b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-feb2d5c-2.2.2-2.fc24.x86_64.conf new file mode 100644 index 0000000..bab0aa6 --- /dev/null +++ b/tests/loader/entries/611f38fd887d41dea7eb3403b2730a76-feb2d5c-2.2.2-2.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 9cb53ddda889d6285fd9ab985a4c47025884999f +title title +machine-id 611f38fd887d41dea7eb3403b2730a76 +version 2.2.2-2.fc24.x86_64 +linux vmlinuz-2.2.2-2.fc24.x86_64 +initrd initramfs-2.2.2-2.fc24.x86_64.img +options root=/dev/vg_root/root ro rootflags=subvol=/snapshot/today rhgb quiet diff --git a/tests/loader/entries/README b/tests/loader/entries/README new file mode 100644 index 0000000..20ee65b --- /dev/null +++ b/tests/loader/entries/README @@ -0,0 +1,7 @@ +Boot loader entry directory for the boom unit test suite. + +The entries in this directory may contan synthetic test data: +non-existent kernel versions, and mock OsProfile data. + +For examples of actual boom boot entries please refer to the +files in the examples/entries directory of the source tree. diff --git a/tests/loader/entries/fffffffe-08fe046-3.3.40.conf b/tests/loader/entries/fffffffe-08fe046-3.3.40.conf new file mode 100644 index 0000000..b15cdc3 --- /dev/null +++ b/tests/loader/entries/fffffffe-08fe046-3.3.40.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWTITLE +machine-id fffffffe +version 3.3.40 +linux /vmlinuz-3.3.40 +initrd /initrd.img-3.3.40 +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 diff --git a/tests/loader/entries/fffffffe-167c7fe-3.3.30.conf b/tests/loader/entries/fffffffe-167c7fe-3.3.30.conf new file mode 100644 index 0000000..6c22c52 --- /dev/null +++ b/tests/loader/entries/fffffffe-167c7fe-3.3.30.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWERTITLE3 +machine-id fffffffe +version 3.3.30 +linux /vmlinuz-3.3.30 +initrd /initrd.img-3.3.30 +options root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 diff --git a/tests/loader/entries/fffffffe-2b0452c-3.3.30.conf b/tests/loader/entries/fffffffe-2b0452c-3.3.30.conf new file mode 100644 index 0000000..c0957b1 --- /dev/null +++ b/tests/loader/entries/fffffffe-2b0452c-3.3.30.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWERTITLE2 +machine-id fffffffe +version 3.3.30 +linux /vmlinuz-3.3.30 +initrd /initrd.img-3.3.30 +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap diff --git a/tests/loader/entries/fffffffe-2cf414e-3.3.30.conf b/tests/loader/entries/fffffffe-2cf414e-3.3.30.conf new file mode 100644 index 0000000..e27234e --- /dev/null +++ b/tests/loader/entries/fffffffe-2cf414e-3.3.30.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWTITLE +machine-id fffffffe +version 3.3.30 +linux /vmlinuz-3.3.30 +initrd /initrd.img-3.3.30 +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap diff --git a/tests/loader/entries/fffffffe-61bcc49-3.3.10.conf b/tests/loader/entries/fffffffe-61bcc49-3.3.10.conf new file mode 100644 index 0000000..b74d6c7 --- /dev/null +++ b/tests/loader/entries/fffffffe-61bcc49-3.3.10.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.10 +linux /vmlinuz-3.3.10 +initrd /initramfs-3.3.10.img +options root=/dev/vg00/lvol0-snap9 ro rd.lvm.lv=vg00/lvol0-snap9 rootflags=subvolid=23 rhgb quiet diff --git a/tests/loader/entries/fffffffe-67431f2-3.3.30.conf b/tests/loader/entries/fffffffe-67431f2-3.3.30.conf new file mode 100644 index 0000000..d14ee46 --- /dev/null +++ b/tests/loader/entries/fffffffe-67431f2-3.3.30.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWTITLE +machine-id fffffffe +version 3.3.30 +linux /vmlinuz-3.3.30 +initrd /initrd.img-3.3.30 +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 diff --git a/tests/loader/entries/fffffffe-6de124e-3.3.50.conf b/tests/loader/entries/fffffffe-6de124e-3.3.50.conf new file mode 100644 index 0000000..ebb5d76 --- /dev/null +++ b/tests/loader/entries/fffffffe-6de124e-3.3.50.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANEWTITLE +machine-id fffffffe +version 3.3.50 +linux /vmlinuz-3.3.50 +initrd /initrd.img-3.3.50 +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 diff --git a/tests/loader/entries/fffffffe-758fa8d-3.3.10.conf b/tests/loader/entries/fffffffe-758fa8d-3.3.10.conf new file mode 100644 index 0000000..1c6f659 --- /dev/null +++ b/tests/loader/entries/fffffffe-758fa8d-3.3.10.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.10 +linux /vmlinuz-3.3.10 +initrd /initramfs-3.3.10.img +options root=/dev/vg00/lvol0-snap2 ro rootflags=subvolid=23 rhgb quiet diff --git a/tests/loader/entries/fffffffe-7f3fb73-7.7.7.conf b/tests/loader/entries/fffffffe-7f3fb73-7.7.7.conf new file mode 100644 index 0000000..6bbca68 --- /dev/null +++ b/tests/loader/entries/fffffffe-7f3fb73-7.7.7.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title A NEWER TITLE +machine-id fffffffe +version 7.7.7 +linux /vmlinuz-7.7.7 +initrd /initramfs-7.7.7.img +options root=/dev/vg_qux/lv_qux ro rd.lvm.lv=vg_qux/lv_qux rhgb quiet diff --git a/tests/loader/entries/fffffffe-9591d36-3.10.1-1.el7.conf b/tests/loader/entries/fffffffe-9591d36-3.10.1-1.el7.conf new file mode 100644 index 0000000..99009fa --- /dev/null +++ b/tests/loader/entries/fffffffe-9591d36-3.10.1-1.el7.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 3fc389bba581e5b20c6a46c7fc31b04be465e973 +title ANEWTITLE +machine-id fffffffe +version 3.10.1-1.el7 +linux /vmlinuz-3.10.1-1.el7 +initrd /initramfs-3.10.1-1.el7.img +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet diff --git a/tests/loader/entries/fffffffe-a948ec1-3.3.4.conf b/tests/loader/entries/fffffffe-a948ec1-3.3.4.conf new file mode 100644 index 0000000..67aebb2 --- /dev/null +++ b/tests/loader/entries/fffffffe-a948ec1-3.3.4.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.4 +linux /vmlinuz-3.3.4 +initrd /initramfs-3.3.4.img +options root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rhgb quiet diff --git a/tests/loader/entries/fffffffe-aa9c868-3.3.4.conf b/tests/loader/entries/fffffffe-aa9c868-3.3.4.conf new file mode 100644 index 0000000..0a999a2 --- /dev/null +++ b/tests/loader/entries/fffffffe-aa9c868-3.3.4.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title qux +machine-id fffffffe +version 3.3.4 +linux /vmlinuz-3.3.4 +initrd /initramfs-3.3.4.img +options root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rhgb quiet diff --git a/tests/loader/entries/fffffffe-b3389d2-3.3.9.conf b/tests/loader/entries/fffffffe-b3389d2-3.3.9.conf new file mode 100644 index 0000000..e43bb96 --- /dev/null +++ b/tests/loader/entries/fffffffe-b3389d2-3.3.9.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.9 +linux /vmlinuz-3.3.9 +initrd /initramfs-3.3.9.img +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet diff --git a/tests/loader/entries/fffffffe-bca4f34-3.3.5.conf b/tests/loader/entries/fffffffe-bca4f34-3.3.5.conf new file mode 100644 index 0000000..1314e42 --- /dev/null +++ b/tests/loader/entries/fffffffe-bca4f34-3.3.5.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.5 +linux /vmlinuz-3.3.5 +initrd /initramfs-3.3.5.img +options root=/dev/vg00/lvol0-snap ro rd.lvm.lv=vg00/lvol0-snap rhgb quiet diff --git a/tests/loader/entries/fffffffe-d76ed3d-3.3.10.conf b/tests/loader/entries/fffffffe-d76ed3d-3.3.10.conf new file mode 100644 index 0000000..1a9119f --- /dev/null +++ b/tests/loader/entries/fffffffe-d76ed3d-3.3.10.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ATITLE +machine-id fffffffe +version 3.3.10 +linux /vmlinuz-3.3.10 +initrd /initramfs-3.3.10.img +options root=/dev/vg00/lvol0-snap2 ro rd.lvm.lv=vg00/lvol0-snap2 rootflags=subvolid=23 rhgb quiet diff --git a/tests/loader/entries/ffffffff-5a19e74-3.3.60-12.fc24.x86_64.conf b/tests/loader/entries/ffffffff-5a19e74-3.3.60-12.fc24.x86_64.conf new file mode 100644 index 0000000..be2c243 --- /dev/null +++ b/tests/loader/entries/ffffffff-5a19e74-3.3.60-12.fc24.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 6bf746bb7231693b2903585f171e4290ff0602b5 +title ANOTHERTITLE +machine-id ffffffff +version 3.3.60-12.fc24.x86_64 +linux /vmlinuz-3.3.60-12.fc24.x86_64 +initrd /initramfs-3.3.60-12.fc24.x86_64.img +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 rhgb quiet diff --git a/tests/loader/entries/ffffffff-f21f2e2-3.3.60.conf b/tests/loader/entries/ffffffff-f21f2e2-3.3.60.conf new file mode 100644 index 0000000..814df7b --- /dev/null +++ b/tests/loader/entries/ffffffff-f21f2e2-3.3.60.conf @@ -0,0 +1,7 @@ +#OsIdentifier: 21e37c8002f33c177524192b15d91dc9612343a3 +title ANOTHERTITLE +machine-id ffffffff +version 3.3.60 +linux /vmlinuz-3.3.60 +initrd /initrd.img-3.3.60 +options root=/dev/vg00/lvol0 ro rd.lvm.lv=vg00/lvol0 diff --git a/tests/loader/entries/ffffffffffffc-dc5f44d-4.14.14-200.fc26.x86_64.conf b/tests/loader/entries/ffffffffffffc-dc5f44d-4.14.14-200.fc26.x86_64.conf new file mode 100644 index 0000000..5f4fe2d --- /dev/null +++ b/tests/loader/entries/ffffffffffffc-dc5f44d-4.14.14-200.fc26.x86_64.conf @@ -0,0 +1,7 @@ +#OsIdentifier: d4439b7 +title A NEW TEST TITLE +machine-id ffffffffffffc +version 4.14.14-200.fc26.x86_64 +linux /vmlinuz-4.14.14-200.fc26.x86_64 +initrd /initramfs-4.14.14-200.fc26.x86_64.img +options root=/dev/vg_hex/root ro rd.lvm.lv=vg_hex/root qux debug diff --git a/tests/os-release/fedora26-os-release b/tests/os-release/fedora26-os-release new file mode 100644 index 0000000..a270edb --- /dev/null +++ b/tests/os-release/fedora26-os-release @@ -0,0 +1,16 @@ +NAME=Fedora +VERSION="26 (Workstation Edition)" +ID=fedora +VERSION_ID=26 +PRETTY_NAME="Fedora 26 (Workstation Edition)" +ANSI_COLOR="0;34" +CPE_NAME="cpe:/o:fedoraproject:fedora:26" +HOME_URL="https://fedoraproject.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +REDHAT_BUGZILLA_PRODUCT="Fedora" +REDHAT_BUGZILLA_PRODUCT_VERSION=26 +REDHAT_SUPPORT_PRODUCT="Fedora" +REDHAT_SUPPORT_PRODUCT_VERSION=26 +PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy +VARIANT="Workstation Edition" +VARIANT_ID=workstation diff --git a/tests/os-release/fedora26-test-os-release b/tests/os-release/fedora26-test-os-release new file mode 100644 index 0000000..3babe53 --- /dev/null +++ b/tests/os-release/fedora26-test-os-release @@ -0,0 +1,16 @@ +NAME=Fedora +VERSION="26 (Testing Edition)" +ID=fedora +VERSION_ID=26 +PRETTY_NAME="Fedora 26 (Workstation Edition)" +ANSI_COLOR="0;34" +CPE_NAME="cpe:/o:fedoraproject:fedora:26" +HOME_URL="https://fedoraproject.org/" +BUG_REPORT_URL="https://bugzilla.redhat.com/" +REDHAT_BUGZILLA_PRODUCT="Fedora" +REDHAT_BUGZILLA_PRODUCT_VERSION=26 +REDHAT_SUPPORT_PRODUCT="Fedora" +REDHAT_SUPPORT_PRODUCT_VERSION=26 +PRIVACY_POLICY_URL=https://fedoraproject.org/wiki/Legal:PrivacyPolicy +VARIANT="Workstation Edition" +VARIANT_ID=workstation diff --git a/tests/os-release/test-os-release b/tests/os-release/test-os-release new file mode 100644 index 0000000..36075c4 --- /dev/null +++ b/tests/os-release/test-os-release @@ -0,0 +1,7 @@ +NAME=Test OS +VERSION="1 (Testing Edition)" +ID=testos +VERSION_ID=1 +PRETTY_NAME="Test OS 1 (Testing Edition)" +VARIANT="Testing Edition" +VARIANT_ID=testing diff --git a/tests/osprofile_tests.py b/tests/osprofile_tests.py new file mode 100644 index 0000000..df75136 --- /dev/null +++ b/tests/osprofile_tests.py @@ -0,0 +1,390 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# osprofile_tests.py - Boom OS profile tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from sys import stdout +from os import listdir, makedirs +from os.path import abspath, join +import shutil + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +from boom.osprofile import * +from boom import * + +BOOT_ROOT_TEST = abspath("./tests") +set_boot_path(BOOT_ROOT_TEST) + +from tests import * + +class OsProfileTests(unittest.TestCase): + """Test OsProfile basic methods + """ + + # Master boom configuration path for sandbox + boom_path = join(BOOT_ROOT_TEST, "boom") + + def setUp(self): + reset_sandbox() + + # Sandbox paths + boot_sandbox = join(SANDBOX_PATH, "boot") + boom_sandbox = join(SANDBOX_PATH, "boot/boom") + + makedirs(boot_sandbox) + shutil.copytree(self.boom_path, boom_sandbox) + + set_boot_path(boot_sandbox) + + drop_profiles() + + def tearDown(self): + drop_profiles() + rm_sandbox() + reset_boom_paths() + + # Module tests + def test_import(self): + import boom.osprofile + + # Profile store tests + + def test_load_profiles(self): + # Test that loading the test profiles succeeds. + load_profiles() + + # Add profile content tests + + # OsProfile tests + + def test_OsProfile__str__(self): + osp = OsProfile(name="Distribution", short_name="distro", + version="1 (Workstation)", version_id="1") + + xstr = ('OS ID: "d279248249d12dd3d115e77e81afac1cb6a00ebd",\n' + 'Name: "Distribution", Short name: "distro",\n' + 'Version: "1 (Workstation)", Version ID: "1",\n' + 'Kernel pattern: "/vmlinuz-%{version}", ' + 'Initramfs pattern: "/initramfs-%{version}.img",\n' + 'Root options (LVM2): "rd.lvm.lv=%{lvm_root_lv}",\n' + 'Root options (BTRFS): "rootflags=%{btrfs_subvolume}",\n' + 'Options: "root=%{root_device} ro %{root_opts}",\n' + 'Title: "%{os_name} %{os_version_id} (%{version})",\n' + 'UTS release pattern: ""') + + self.assertEqual(str(osp), xstr) + osp.delete_profile() + + def test_OsProfile__repr__(self): + osp = OsProfile(name="Distribution", short_name="distro", + version="1 (Workstation)", version_id="1") + + xrepr = ('OsProfile(profile_data={' + 'BOOM_OS_ID:"d279248249d12dd3d115e77e81afac1cb6a00ebd", ' + 'BOOM_OS_NAME:"Distribution", BOOM_OS_SHORT_NAME:"distro", ' + 'BOOM_OS_VERSION:"1 (Workstation)", BOOM_OS_VERSION_ID:"1", ' + 'BOOM_OS_KERNEL_PATTERN:"/vmlinuz-%{version}", ' + 'BOOM_OS_INITRAMFS_PATTERN:"/initramfs-%{version}.img", ' + 'BOOM_OS_ROOT_OPTS_LVM2:"rd.lvm.lv=%{lvm_root_lv}", ' + 'BOOM_OS_ROOT_OPTS_BTRFS:"rootflags=%{btrfs_subvolume}", ' + 'BOOM_OS_OPTIONS:"root=%{root_device} ro %{root_opts}", ' + 'BOOM_OS_TITLE:"%{os_name} %{os_version_id} (%{version})", ' + 'BOOM_OS_UNAME_PATTERN:""})') + + self.assertEqual(repr(osp), xrepr) + osp.delete_profile() + + def test_OsProfile(self): + # Test OsProfile init from kwargs + with self.assertRaises(ValueError) as cm: + osp = OsProfile(name="Fedora", short_name="fedora", + version="24 (Workstation Edition)") + with self.assertRaises(ValueError) as cm: + osp = OsProfile(name="Fedora", short_name="fedora", + version_id="24") + with self.assertRaises(ValueError) as cm: + osp = OsProfile(name="Fedora", version="24 (Workstation Edition)", + version_id="24") + + osp = OsProfile(name="Fedora", short_name="fedora", + version="24 (Workstation Edition)", version_id="24") + + self.assertTrue(osp) + + # os_id for fedora24 + self.assertEqual(osp.os_id, "9cb53ddda889d6285fd9ab985a4c47025884999f") + + def test_OsProfile__profile_exists(self): + import boom + osp = OsProfile(name="Fedora", short_name="fedora", + version="24 (Workstation Edition)", version_id="24") + + self.assertTrue(osp) + + # os_id for fedora24 + self.assertEqual(osp.os_id, "9cb53ddda889d6285fd9ab985a4c47025884999f") + self.assertTrue(boom.osprofile._profile_exists(osp.os_id)) + + def test_OsProfile_from_profile_data(self): + # Pull in all the BOOM_OS_* constants to the local namespace. + from boom.osprofile import ( + BOOM_OS_ID, BOOM_OS_NAME, BOOM_OS_SHORT_NAME, + BOOM_OS_VERSION, BOOM_OS_VERSION_ID, + BOOM_OS_UNAME_PATTERN, BOOM_OS_KERNEL_PATTERN, + BOOM_OS_INITRAMFS_PATTERN, BOOM_OS_ROOT_OPTS_LVM2, + BOOM_OS_ROOT_OPTS_BTRFS, BOOM_OS_OPTIONS + ) + profile_data = { + BOOM_OS_ID: "3fc389bba581e5b20c6a46c7fc31b04be465e973", + BOOM_OS_NAME: "Red Hat Enterprise Linux Server", + BOOM_OS_SHORT_NAME: "rhel", + BOOM_OS_VERSION: "7.2 (Maipo)", + BOOM_OS_VERSION_ID: "7.2", + BOOM_OS_UNAME_PATTERN: "el7", + BOOM_OS_KERNEL_PATTERN: "/vmlinuz-%{version}", + BOOM_OS_INITRAMFS_PATTERN: "/initramfs-%{version}.img", + BOOM_OS_ROOT_OPTS_LVM2: "rd.lvm.lv=%{lvm_root_lv} rh", + BOOM_OS_ROOT_OPTS_BTRFS: "rootflags=%{btrfs_subvolume} rh", + BOOM_OS_OPTIONS: "root=%{root_device} %{root_opts} rhgb quiet" + } + + osp = OsProfile(profile_data=profile_data) + self.assertTrue(osp) + + # Cleanup + osp.delete_profile() + + # Remove the root options keys. + profile_data.pop(BOOM_OS_ROOT_OPTS_LVM2, None) + profile_data.pop(BOOM_OS_ROOT_OPTS_BTRFS, None) + osp = OsProfile(profile_data=profile_data) + + # Assert that defaults are restored + self.assertEqual(osp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv}") + self.assertEqual(osp.root_opts_btrfs, "rootflags=%{btrfs_subvolume}") + + # Cleanup + osp.delete_profile() + + # Remove the name key. + profile_data.pop(BOOM_OS_NAME, None) + with self.assertRaises(ValueError) as cm: + osp = OsProfile(profile_data=profile_data) + + def test_OsProfile_properties(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} %{root_opts} rhgb quiet" + self.assertEqual(osp.os_name, "Fedora Core") + self.assertEqual(osp.os_short_name, "fedora") + self.assertEqual(osp.os_version, "1 (Workstation Edition)") + self.assertEqual(osp.os_version_id, "1") + self.assertEqual(osp.kernel_pattern, "/vmlinuz-%{version}") + self.assertEqual(osp.initramfs_pattern, + "/initramfs-%{version}.img") + self.assertEqual(osp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv}") + self.assertEqual(osp.root_opts_btrfs, + "rootflags=%{btrfs_subvolume}") + self.assertEqual(osp.options, + "root=%{root_device} %{root_opts} rhgb quiet") + osp.delete_profile() + + def test_OsProfile_no_lvm(self): + osp = OsProfile(name="NoLVM", short_name="nolvm", + version="1 (Server)", version_id="1") + osp.kernel_pattern = "/vmlinux-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + + self.assertEqual(osp.root_opts_lvm2, "rd.lvm.lv=%{lvm_root_lv}") + + def test_OsProfile_no_btrfs(self): + osp = OsProfile(name="NoBTRFS", short_name="nobtrfs", + version="1 (Server)", version_id="1") + osp.kernel_pattern = "/" + osp.kernel_pattern = "/vmlinux-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + + self.assertEqual(osp.root_opts_btrfs, "rootflags=%{btrfs_subvolume}") + + def test_OsProfile_from_os_release(self): + osp = OsProfile.from_os_release([ + '# Fedora 24 Workstation Edition\n', + 'NAME=Fedora\n', 'VERSION="24 (Workstation Edition)\n', + 'ID=fedora\n', 'VERSION_ID=24\n', + 'PRETTY_NAME="Fedora 24 (Workstation Edition)"\n', + 'ANSI_COLOR="0;34"\n', + 'CPE_NAME="cpe:/o:fedoraproject:fedora:24"\n', + 'HOME_URL="https://fedoraproject.org/"\n', + 'BUG_REPORT_URL="https://bugzilla.redhat.com/"\n', + 'VARIANT="Workstation Edition"\n', + 'VARIANT_ID=workstation\n' + ]) + + def test_OsProfile_from_file(self): + osp = OsProfile.from_os_release_file("/etc/os-release") + self.assertTrue(osp) + + def test_OsProfile_from_host(self): + osp = OsProfile.from_host_os_release() + self.assertTrue(osp) + + def test_OsProfile_write(self): + from os.path import exists, join + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.write_profile() + profile_path = join(boom_profiles_path(), + "%s-fedora1.profile" % osp.os_id) + self.assertTrue(exists(profile_path)) + + def test_OsProfile_set_optional_keys(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.optional_keys = "grub_users grub_arg" + + def test_OsProfile_bad_optional_key_raises(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + with self.assertRaises(ValueError) as cm: + osp.optional_keys = "no_such_option" + + def test_OsProfile_add_optional_keys(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.add_optional_key("grub_class") + osp.optional_keys = "grub_users grub_arg" + osp.add_optional_key("grub_class") + + def test_OsProfile_add_bad_optional_keys(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.optional_keys = "grub_users grub_arg" + with self.assertRaises(ValueError) as cm: + osp.add_optional_key("no_such_key") + + def test_OsProfile_del_optional_keys(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.optional_keys = "grub_users grub_arg" + osp.del_optional_key("grub_arg") + + def test_OsProfile_del_bad_optional_keys(self): + osp = OsProfile(name="Fedora Core", short_name="fedora", + version="1 (Workstation Edition)", version_id="1") + osp.uname_pattern = "fc1" + osp.kernel_pattern = "/vmlinuz-%{version}" + osp.initramfs_pattern = "/initramfs-%{version}.img" + osp.root_opts_lvm2 = "rd.lvm.lv=%{lvm_root_lv}" + osp.root_opts_btrfs = "rootflags=%{btrfs_subvolume}" + osp.options = "root=%{root_device} ro %{root_opts} rhgb quiet" + osp.optional_keys = "grub_users grub_arg" + with self.assertRaises(ValueError) as cm: + osp.del_optional_key("no_such_key") + + def test_osprofile_write_profiles(self): + import boom + boom.osprofile.load_profiles() + boom.osprofile.write_profiles() + + def test_osprofile_find_profiles_by_id(self): + rhel72_os_id = "9736c347ccb724368be04e51bb25687a361e535c" + osp_list = find_profiles(selection=Selection(os_id=rhel72_os_id)) + self.assertEqual(len(osp_list), 1) + self.assertEqual(osp_list[0].os_id, rhel72_os_id) + + def test_osprofile_find_profiles_by_name(self): + os_name = "Fedora" + os_short_name = "fedora" + osp_list = find_profiles(selection=Selection(os_name=os_name)) + nr_profiles = 0 + for f in listdir(boom_profiles_path()): + if os_short_name in f: + nr_profiles += 1 + self.assertTrue(len(osp_list), nr_profiles) + + def test_no_select_null_profile(self): + import boom + osps = find_profiles(Selection(os_id=boom.osprofile._profiles[0].os_id)) + self.assertFalse(osps) + + def test_find_os_short_name(self): + osps = find_profiles(Selection(os_short_name="fedora")) + self.assertTrue(osps) + + def test_find_os_version(self): + osps = find_profiles(Selection(os_version="26 (Workstation Edition)")) + self.assertTrue(osps) + + def test_find_os_version_id(self): + osps = find_profiles(Selection(os_version_id="26")) + self.assertTrue(osps) + + def test_find_os_uname_pattern(self): + osps = find_profiles(Selection(os_uname_pattern="el7")) + self.assertTrue(osps) + + def test_find_os_kernel_pattern(self): + pattern = "/vmlinuz-%{version}" + osps = find_profiles(Selection(os_kernel_pattern=pattern)) + self.assertTrue(osps) + + def test_find_os_initramfs_pattern(self): + osps = find_profiles(Selection(os_initramfs_pattern="/initramfs-%{version}.img")) + self.assertTrue(osps) + +# vim: set et ts=4 sw=4 : diff --git a/tests/report_tests.py b/tests/report_tests.py new file mode 100644 index 0000000..5d4552a --- /dev/null +++ b/tests/report_tests.py @@ -0,0 +1,121 @@ +# Copyright (C) 2017 Red Hat, Inc., Bryn M. Reeves +# +# report_tests.py - Boom report API tests. +# +# This file is part of the boom project. +# +# This copyrighted material is made available to anyone wishing to use, +# modify, copy, or redistribute it subject to the terms and conditions +# of the GNU General Public License v.2. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program; if not, write to the Free Software Foundation, +# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +import unittest +import logging +from sys import stdout +from os import listdir +from os.path import exists, abspath + +# Python3 moves StringIO to io +try: + from StringIO import StringIO +except: + from io import StringIO + +log = logging.getLogger() +log.level = logging.DEBUG +log.addHandler(logging.FileHandler("test.log")) + +import boom +BOOT_ROOT_TEST = abspath("./tests") +boom.set_boot_path(BOOT_ROOT_TEST) + +import boom.report + +from boom.report import * + +_report_objs = [ + (1, "foo", "ffffffffffffffffffffffffffffffffffffffff"), + (2, "bar", "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"), + (3, "baz", "1111111111111111111111111111111111111111"), + (4, "qux", "2222222222222222222222222222222222222222") +] + +BR_NUM = 1 +BR_STR = 2 +BR_SHA = 4 + +_test_obj_types = [ + BoomReportObjType(BR_NUM, "Num", "num_", lambda o: o[0]), + BoomReportObjType(BR_STR, "Str", "str_", lambda o: o[1]), + BoomReportObjType(BR_SHA, "Sha", "sha_", lambda o: o[2]) +] + + +class ReportTests(unittest.TestCase): + def test_BoomFieldType_no_name(self): + with self.assertRaises(ValueError): + bf = BoomFieldType(BR_NUM, None, "None", "Nothing", 0, + REP_NUM, lambda x: x) + + def test_BoomFieldType_bogus_dtype_raises(self): + with self.assertRaises(ValueError): + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", 0, + "fzzrt", lambda x: x) + + def test_BoomFieldType_dtype_NUM(self): + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", 0, + REP_NUM, lambda x: x) + self.assertEqual(bf.dtype, REP_NUM) + + def test_BoomFieldType_dtype_STR(self): + bf = BoomFieldType(BR_STR, "none", "None", "Nothing", 0, + REP_STR, lambda x: x) + self.assertEqual(bf.dtype, REP_STR) + + def test_BoomFieldType_dtype_SHA(self): + bf = BoomFieldType(BR_SHA, "none", "None", "Nothing", 0, + REP_SHA, lambda x: x) + self.assertEqual(bf.dtype, REP_SHA) + + def test_BoomFieldType_bogus_align_raises(self): + with self.assertRaises(ValueError): + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", 0, + REP_NUM, lambda x: x, align="qux") + + def test_BoomFieldType_with_align_l(self): + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", 0, + REP_NUM, lambda x: x, align=ALIGN_LEFT) + + def test_BoomFieldType_with_align_r(self): + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", 0, + REP_NUM, lambda x: x, align=ALIGN_RIGHT) + + def test_BoomFieldType_negative_width_raises(self): + with self.assertRaises(ValueError) as cm: + bf = BoomFieldType(BR_NUM, "none", "None", "Nothing", -1, + REP_NUM, lambda x: x) + + def test_BoomFieldType_simple_str_int_report(self): + bf_name = BoomFieldType(BR_STR, "name", "Name", "Nothing", 8, + REP_STR, lambda f, d: f.report_str(d)) + bf_num = BoomFieldType(BR_NUM, "number", "Number", "Nothing", 8, + REP_NUM, lambda f, d: f.report_num(d)) + + output = StringIO() + opts = BoomReportOpts(report_file=output) + + xoutput = ("Name Number \nfoo 1\n" + + "bar 2\nbaz 3\nqux 4\n") + + br = BoomReport(_test_obj_types, [bf_name, bf_num], "name,number", + opts, None, None) + + for obj in _report_objs: + br.report_object(obj) + br.report_output() + + self.assertEqual(output.getvalue(), xoutput) + +# vim: set et ts=4 sw=4 :