././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.6973155 kiwi-11.1.8/.bumpversion.cfg0000644000000000000000000000025615273675667012636 0ustar00[bumpversion] current_version = 11.1.8 commit = True tag = True [bumpversion:file:pyproject.toml] [bumpversion:file:kiwi/version.py] [bumpversion:file:doc/source/conf.py] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.6988065 kiwi-11.1.8/LICENSE0000644000000000000000000010451315273675667010534 0ustar00 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. 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 them 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 prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. 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. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey 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; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If 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 convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU 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 that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. 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. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 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. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. 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 state 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 3 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, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program 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, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU 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 Lesser General Public License instead of this License. But first, please read . ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.6988065 kiwi-11.1.8/Makefile0000644000000000000000000001370115273675667011165 0ustar00buildroot = / docdir = /usr/share/doc/packages python_version = 3 python_lookup_name = python$(python_version) python = $(shell which $(python_lookup_name)) sc_disable = SC1091,SC1090,SC2001,SC2174,SC1117,SC2048,SC2004,SC2317,SC2329 LC = LC_MESSAGES version := $(shell \ $(python) -c \ 'from kiwi.version import __version__; print(__version__)'\ ) install_dracut: for dracut in dracut/modules.d/*; do \ ${MAKE} -C $$dracut install ;\ done install_package_docs: install -d -m 755 ${buildroot}${docdir}/python-kiwi install -m 644 LICENSE \ ${buildroot}${docdir}/python-kiwi/LICENSE install -m 644 README.rst \ ${buildroot}${docdir}/python-kiwi/README install: # apart from python sources there are also # the manual pages and the completion # Note: These information will be missing when installed from pip # manual pages install -d -m 755 ${buildroot}usr/share/man/man8 for man in doc/build/man/*.8; do \ install -m 644 $$man ${buildroot}usr/share/man/man8 ;\ done # completion install -d -m 755 ${buildroot}usr/share/bash-completion/completions install -m 644 completions/bash-completion \ ${buildroot}usr/share/bash-completion/completions/kiwi-ng # kiwi default configuration install -d -m 755 ${buildroot}usr/share/kiwi install -d -m 755 ${buildroot}usr/share/kiwi/kiwi.yml.d install -d -m 755 ${buildroot}etc/kiwi.yml.d install -m 644 kiwi.yml ${buildroot}usr/share/kiwi/kiwi.yml.example # kiwi old XSL stylesheets for upgrade cp -a helper/xsl_to_v74 ${buildroot}usr/share/kiwi/ kiwi/schema/kiwi.rng: kiwi/schema/kiwi.rnc # whenever the schema is changed this target will convert # the short form of the RelaxNG schema to the format used # in code and auto generates the python data structures @type -p trang &>/dev/null || \ (echo "ERROR: trang not found in path: $(PATH)"; exit 1) trang -I rnc -O rng kiwi/schema/kiwi.rnc kiwi/schema/kiwi.rng # XML parser code is auto generated from schema using generateDS # http://pythonhosted.org/generateDS # --- # a) modify arch-name xsd:token pattern to be generic because # generateDS translates the regular expression into another # expression which is different and wrong compared to the # expression in the schema cat kiwi/schema/kiwi.rnc | sed -e \ s'@arch-name = xsd:token.*@arch-name = xsd:token {pattern = ".*"}@' >\ kiwi/schema/kiwi_modified_for_generateDS.rnc # convert schema rnc format into xsd format and call generateDS trang -I rnc -O xsd kiwi/schema/kiwi_modified_for_generateDS.rnc \ kiwi/schema/kiwi_for_generateDS.xsd generateDS.py -f --external-encoding='utf-8' --no-dates --no-warnings \ -o kiwi/xml_parse.py kiwi/schema/kiwi_for_generateDS.xsd rm kiwi/schema/kiwi_for_generateDS.xsd rm kiwi/schema/kiwi_modified_for_generateDS.rnc obs_test_status: ./.obs_test_status valid: for i in `find build-tests test kiwi -name *.xml -o -name *.kiwi`; do \ if [ ! -L $$i ];then \ xsltproc -o $$i.converted kiwi/xsl/master.xsl $$i && \ mv $$i.converted $$i ;\ fi \ done setup: poetry install --all-extras docs: setup poetry run make -C doc man html ghpages: setup poetry run sphinx-build doc/source doc/build docs_suse: setup sed -ie "s@:hidden:@@" doc/source/index.rst rm -f doc/source/index.rste poetry run make -C doc xml_suse git checkout doc/source/index.rst rm -rf doc/build/restxml mv doc/build/xml doc/build/restxml poetry run pip uninstall --yes rstxml2docbook || true poetry run pip install \ git+https://github.com/openSUSE/rstxml2docbook.git@feature/kiwi poetry run bash -c 'pushd doc && rstxml2docbook \ -v --no-split -o build/xml/book.xml build/restxml/index.xml' bash -c 'mkdir -p doc/build/images/src/png && \ cp -a doc/source/.images/* doc/build/images/src/png' cp doc/DC-kiwi doc/build/ bash -c 'pushd doc/build && daps -d DC-kiwi html' test_scripts: setup poetry run bash -c 'pushd test/scripts && sudo pytest -s -vv' check: setup # shell code checks find build-tests -name config.sh | xargs shellcheck bash -c 'shellcheck -e ${sc_disable} dracut/modules.d/*/*.sh -s bash' bash -c 'shellcheck -e ${sc_disable} kiwi/config/functions.sh -s bash' bash -c 'shellcheck -e ${sc_disable} boxes/*/*.sh -s bash' bash -c 'shellcheck -e ${sc_disable} boxes/images.sh -s bash' bash -c 'shellcheck build-tests.sh' # python flake tests poetry run flake8 --statistics -j auto --count kiwi poetry run flake8 --statistics -j auto --count test/unit poetry run flake8 --statistics -j auto --count test/scripts test: setup # python static code checks poetry run mypy kiwi # unit tests poetry run bash -c 'pushd test/unit && pytest -n auto \ --doctest-modules --no-cov-on-fail --cov=kiwi \ --cov-report=term-missing --cov-fail-under=100 \ --cov-config .coveragerc' black: setup poetry run black \ --skip-string-normalization \ --line-length 80 kiwi test/unit/ build: clean check test # build the sdist source tarball poetry build --format=sdist # provide rpm source tarball mv dist/kiwi-${version}.tar.gz dist/python-kiwi.tar.gz # update rpm changelog using reference file helper/update_changelog.py --since package/python-kiwi.changes --fix > \ dist/python-kiwi.changes helper/update_changelog.py --file package/python-kiwi.changes >> \ dist/python-kiwi.changes # update package version in spec file cat package/python-kiwi-spec-template | sed -e s'@%%VERSION@${version}@' \ > dist/python-kiwi.spec # update package version in PKGBUILD file md5sums=$$(md5sum dist/python-kiwi.tar.gz | cut -d" " -f1); \ cat package/python-kiwi-pkgbuild-template | sed \ -e s'@%%VERSION@${version}@' \ -e s"@%%MD5SUM@$${md5sums}@" > dist/PKGBUILD # provide rpm rpmlintrc cp package/python-kiwi-rpmlintrc dist # provide additional source files cp package/systemd-tmpfiles-for-suse.conf dist prepare_for_pypi: clean setup # documentation render and tests poetry run make -C doc man # sdist tarball, the actual publishing happens via the # ci-publish-to-pypi.yml github action poetry build --format=sdist clean: rm -rf dist rm -rf doc/build rm -rf doc/dist ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.6988065 kiwi-11.1.8/README.rst0000644000000000000000000000442415273675667011216 0ustar00KIWI - Next Generation ====================== .. |GitHub Action Code Style| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-code-style.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |GitHub Action Config Functions| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-config-functions.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |GitHub Action Documentation| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-documentation.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |GitHub Action Publish Pages| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-publish-pages.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |GitHub Action Publish PyPi| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-publish-to-pypi.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |GitHub Action Unit Types| image:: https://github.com/OSInside/kiwi/actions/workflows/ci-units-types.yml/badge.svg :target: https://github.com/OSInside/kiwi/actions .. |Health| image:: https://app.codacy.com/project/badge/Grade/228f7e8cd15d448688a590c272ec3789 :target: https://www.codacy.com/gh/OSInside/kiwi/dashboard?utm_source=github.com&utm_medium=referral&utm_content=OSInside/kiwi&utm_campaign=Badge_Grade .. |Doc| replace:: `Documentation `__ .. |Installation| replace:: `Installation `__ .. |Contributing| replace:: `Contributing `__ .. |IntegrationTesting| replace:: `Integration Testing `__ .. |Donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif :target: https://www.paypal.com/donate/?hosted_button_id=CYZY57A7Q4TCC |GitHub Action Code Style| |GitHub Action Config Functions| |GitHub Action Documentation| |GitHub Action Publish Pages| |GitHub Action Publish PyPi| |GitHub Action Unit Types| |Health| **KIWI, the OS image and appliance builder.** * |Installation| * |IntegrationTesting| * |Contributing| * |Doc| KIWI has helped you in your work ? Even the smallest gift is a way to help that we don't run out of coffee :) |Donate| ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7051604 kiwi-11.1.8/build-tests.sh0000755000000000000000000001054415273675667012325 0ustar00#!/bin/bash # git clone https://github.com/OSInside/kiwi.git # Simple build test script to build the integration test # images from a given test directory. The host to run this # command requires the following tools: # # - tree # - git # - xmllint # - podman # - pip # # And requires the installation of kiwi which provides # the boxbuild command # # $ pip install --upgrade kiwi # set -e ARGUMENT_LIST=( "test-dir:" "test-name:" "box-name:" "vm" ) function usage() { echo "usage: build-tests --test-dir " echo " --test-dir " echo " Some test dir name, e.g. build-tests/x86/tumbleweed/" echo " --test-name " echo " some test name, e.g. test-image-disk" echo " --box-name " echo " name of the box to use for the build, default: universal" echo " --vm" echo " build in a virtual machine instead of a container" } if ! opts=$(getopt \ --longoptions "$(printf "%s," "${ARGUMENT_LIST[@]}")" \ --name "$(basename "$0")" \ --options "" \ -- "$@" ); then usage exit 0 fi eval set --"${opts}" while [[ $# -gt 0 ]]; do case "$1" in --test-dir) argTestDir=$2 shift 2 ;; --test-name) argTestName=$2 shift 2 ;; --box-name) argBoxName=$2 shift 2 ;; --vm) argVM=1 shift ;; *) break ;; esac done if [ ! "${argTestDir}" ];then usage exit 1 fi if [ ! -e "${argTestDir}"/.repos ];then echo "No .repos information for specified test dir" exit 1 fi boxname=universal if [ "${argBoxName}" ];then boxname="${argBoxName}" fi function create_repo_list() { local build_dir=$1 if [ -s "${build_dir}"/.repos ];then local repo_options="--ignore-repos" while read -r repo;do repo_options="${repo_options} --add-repo ${repo}" done < "${build_dir}"/.repos echo "${repo_options}" fi } function create_build_commands() { local build_dir=$1 local test_name=$2 build_commands=() for image in "${build_dir}"/*;do test -e "${image}/appliance.kiwi" || continue test -e "${image}/.skip_boxbuild_container" && continue base_image=$(basename "${image}") if [ -n "${test_name}" ] && [ ! "${test_name}" = "${base_image}" ];then continue fi build_command="kiwi-ng --debug" has_profiles=false repo_options=$(create_repo_list "${build_dir}") box_options="system boxbuild --box ${boxname}" if [ ! "${argVM}" = 1 ];then box_options="${box_options} --container" fi for profile in $( xmllint --xpath "//image/profiles/profile/@name" \ "${image}/appliance.kiwi" 2>/dev/null | cut -f2 -d\" );do has_profiles=true target_dir="build_results/${base_image}/${profile}" build_command="${build_command} --profile ${profile}" build_command="${build_command} ${box_options} kiwi" build_command="${build_command} --description $image" build_command="${build_command} ${repo_options}" build_command="${build_command} --target-dir ${target_dir}" echo "${build_command}" \ > "build_results/${base_image}-${profile}.build" build_commands+=( "${build_command}" ) build_command="kiwi-ng --debug" done if [ "${has_profiles}" = "false" ];then target_dir="build_results/${base_image}" build_command="${build_command} ${box_options} kiwi" build_command="${build_command} --description $image" build_command="${build_command} ${repo_options}" build_command="${build_command} --target-dir ${target_dir}" echo "${build_command}" \ > "build_results/${base_image}.build" build_commands+=( "${build_command}" ) fi done } # create results directory mkdir -p build_results # build command list create_build_commands "${argTestDir}" "${argTestName}" # build them in a row for build in "${build_commands[@]}";do ${build} sudo rm -rf build_results/*/*/build/ sudo rm -rf build_results/*/build/ done # show result tree test -d build_results && tree -L 3 build_results ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7331226 kiwi-11.1.8/completions/bash-completion0000644000000000000000000000051315273675667015065 0ustar00# NOTE: this file was generated from kiwi-ng --show-completion _kiwi_ng_completion() { local IFS=$' ' COMPREPLY=( $( env COMP_WORDS="${COMP_WORDS[*]}" \ COMP_CWORD=$COMP_CWORD \ _KIWI_NG_COMPLETE=complete_bash $1 ) ) return 0 } complete -o default -F _kiwi_ng_completion kiwi-ng ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.733253 kiwi-11.1.8/doc/Makefile0000644000000000000000000001765015273675667011741 0ustar00# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -W -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source .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 " 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 " xml_suse to make DocBook 5 output for SUSE documentation" @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 " spell to run spell-checking" .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/kiwi.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/kiwi.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/kiwi" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/kiwi" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .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: xml_suse xml_suse: $(SPHINXBUILD) -b xml -t suse $(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: spell spell: $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling @echo @echo "Spell-checking complete. Find results in $(BUILDDIR)/spelling." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333384 kiwi-11.1.8/doc/source/.images/architecture.svg0000644000000000000000000000545515273675667016327 0ustar00 KIWI NG Architecture Image description config.xml or *.kiwi Repositories Packages used for the build Prepare Create a new root Prepared root tree Create One or more images disk, ISO, container ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333865 kiwi-11.1.8/doc/source/.images/flow-diagram.png0000644000000000000000000026721315273675667016205 0ustar00‰PNG  IHDR âB÷‡Y cHRMz&€„ú€èu0ê`:˜pœºQ<bKGDÿÿÿ ½§“ pHYsÃÃÇo¨d€IDATxÚìÝwtUÇñoz'¡%¡„Þ{ï ¢ØAEQ± |Pi¢XQl(6@T:Ò;„z é½÷MöýcaȺ°Aø}Îñ˜Ý¹3ûÌÝewž¹ÍÎl6›±ûâ@DDDDDnJ@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±Çâ@Däj,Ù¶�;Bèݦ!=ZÖ³Úž’‘Åû?-!7/€¾íÓ­YŽ„ÇðÀد¸¯Cުᅫ%=+‡¶/M MݪÌzcà%Ë¿1s¾×оwP·²‘Øßùn!ÞžnŒ1¿ €%Ûö±,ð'"ãÉÊÉÅ·¤ÍkUæÑ.-¨Z®Ìeã>�Àôy«pwqfÒ³ý¬¶ÿ°|+»Žžà¡ÎÍ騰†±-&)• s–àà`Ï´çÀÁþúÝË*\ ª–çÙ>/»O|J:¿¬ÞAd|2ÎN޼÷Ô=D'¦2ñgKœ-kWfà]m®[Œ""òï(‘ÿ” #§ù~ù|KZ% 9y&}ÿ6í;@ÏVõ¹£QMróò9�X.X¯‡‚‚ã˜UüK_¶üOÿl'3'€c±,�ô²ÕöÌœ\ãÜÊ•ò.’€:Í“~ $4ªÈ±WìaÊo+xãá»5 'vvv�ïT ~[³ƒ¬Ü<^y°+åJyÛg.ÚÀ�S‘ØÛÛY% �‡N1¶¬Såº&ç×A¯Ö .›€ddåÐé•i„Å%Ͻ÷Ô=¤ddÇÉÊÉU""rQ,¹%˜Í<ûá#ùhß :?Ž| 'Gœ�¨â_š*þ¥)ãíy]^ÓÞÞÞ8¦ÉWµïÆ}ÇøsÓž+.<2Žî#fɇƒ½= «V M½jÆù˜ò ˜ôër>øeù%�åæìDÇ3‰À®#§�¿3³s9t:Úx¼óp¨Õ¾{Ž…wkVçºÔ㿱íàI#ù¨V¾ ÃîïRÜ!‰ˆÈe¨DDn £fýÅ‚�»h\½"sÇ ÁÍÙÉØ^«¢û¾s]_ÓÓÍå_óíoÿ¦g«úVq^ÌÓS"9=€Ž kðÕë�P¶$`I<¾[¶™7¿Z@~AÓæ®äÉží(_Úû¢ÇëÖ¼ÿì´te :ršÞm°çxù¸:;‘�›Ç�S‘dæäâîâlÙ~,¬Ð1ê^×ú¼…[³÷lÇ+ÜYÜ!‰ˆÈe(‘ÿ¼ Öðùßë¨QÁ—?ß{žnVeœŠdèG¿pǦ ðNžŸþ³Ñ¢ð×ø¡”òòàõ/ç³ãÐ)>ö(MªW`Ì÷‹X·ç0_¿þ8e¼=¹ÌLÚÕ¯^d,Å圎Mäãù«Ù¿ç%Ë­Ýs˜ #––ˆŠe}˜;öY<Ý\ŒíŽö<Û§#‡ÃbøzñFòLù,ݶ�gzw¸è1»JÎŽ÷°ümI0:5ªÉîcaÄ%§±çXíêW`÷™ÄÇÓ�æµ*û…Ç%óñ«Xt�èÄTÜœ�¨_¥<�woÍ£]ZXu 2í'‡Å°îã×™øó2f¯ØÊ”ç M½ªŒ7<.™§¦Ì&'7O7¾{ó f.Ú`¼O ·ìåxd<ï¾ôøž\S>ß.Ýļõ»8C~A•|KÑ»MC^î×…’^îüïël=p€Ùo=iŒ¯yïÇ%¬ :ÀûƒûrGãZLŸ·Š¿Î´jM}þZ×­Šˆˆ¥DDþÓæ­2-W(ãÃßã‡RÖÇ«H¹Ìì\ãâ¹UÝ*ø–,ÁÏ«Øqè=ZÖÇl6ódž]$¤f°yß1#ù{ËNDÆãíáF� ¾Ä¥¤Çô¿DkÃ…Üß±) 6îæãù«y¼{k£5ãB–0þÜ«½UòQؤ!ýxgàݸ\¦U¥F_ªø—æTt»ŽžÆl6cgggtÇjV³ö,Û¾Ÿ‡CiW¿:áqÉÄ%§йI-cüÇžãáôý9Ii™Võ½aïQ6ì=ʪ ƒ|óÆ@# 9cÔÛ{?.áù+((0_0ÖŒ¬yïköžˆÀÙÑ�yãžÃ¿T æ­ âtl¢Q.ðÐ)�b䀋'tÙ¹yô3ÓèªwVHh!¡Qü¾n'Ë'# lIÊ—ñ1âÜrÂH@n 6¨õÁG�dñ¶}ì>†³£õ«”¿ªÏƒˆÈíDc@Dä?kóþãß}ò<Ý\ÈÌÉåíoÿ¾dÙ}'"Œ¿ÛÔ«vÑrNŽøxºããé~EݺîjaÀŸ”–i ¦:j9ßæµ*ÑâL ÇÙ:(ÜýêΦ–ñù<5y¶‘|ôhYŸïÞÄ„§ïÃÇÓÒ’0w]?¯ ¼` Ÿý¹–nÔ«\O7ç"Û Ìfžžö{OD`ogÇ—¯ kÓÚ|óÆ@^{¨›Qöé»Û³p FKÖ…Lúu…‘|Tò-ÅG/<ÄW¯=N«:U8“ȰO~¬[‰¶…XZBR2²8k<¶nròLì=n¼GKEDD- "ò¶z×!ãïü‚æ­bì >W¼ëºUñts!=+‡í!–dÇáS”õñ".9��gº>í<| ³Ùr‡þ®ë0öÁ·¤ÿ{´ï|¿�w3¤wªW({Á²ÉéYÆß¥Ît:k}ð^:sÁ\Øà^íxõÁn\J·fuøzñF‚ŽžÆÇÓÝH²šÕª„³“£qîp®ûÀ�Í- Ⱥ=G8a¹ oß :óÆ1Z:ZÔ®D�7?à›¥›y¼{ë"1<Ø©3^~—3¯•˜bµ}Ìw Y²mž¹�‡;77¶µ­_�ÐB- 5*øÒ¥I틞o�ÙÌ÷Ë,3c9;:°lÒËTò³$¬tjJóç&“Àª]‡�I ^årT(ãCD|2ÛÏ$¨;‡b6›�ÏGÐÑÓ˜Íì;ANž °N\DD¤(µ€ˆÈZ¹RÞxŸïñáÜU,Û¾ÿŠ÷uvt0ºÏ9Mž)߸£=è®68:Øs*:�ø”tvœyÞÎÎŽ;¯Óæ ÷u¦zyKÒñæW ÈÏ¿p$7—s­Ù¹VÛ2³s9�Pä¿ÄBÝ¡.æŽÆµŒ ÿ]GN³ûXf³™J¾¥ðõñ¢y­JØÛÙ—LTb ÁÇ- Hí?£ËXáY²ÝÕÖj¬G»úÕ�ó >Fž)¿H oìmÄp¾­N0cÁüK•¸¢5A.åDd‰i–Ö­.MkÉ€‹“#�tia<>û~ßyf¦¯Ã§cHÉÈ2Î÷�ŽMñöp#5#‹£á±Fâ 7Çà|‘›™ùÏ*ëãÅ¢‰/òÅðÇ0›Í ùp'£â¯øgïVgæä²ïd„q!Ù¥I-êU.X.²Ïv®_¥Ü%g—ºÎŽLb¸¾ïd?¬ØrÁr…×Ù}4Ìj›ioúuhB¿MèÛ®ñU½¾»«³1¸<èÈésã?Ît½òöp£fE_£ξö�…¦ß-<î#À·è8–³]âLù¤ddÙ^¡ÌÅëòì¬_щ©|¹pý¿ªï±V¼À˜›ÂãpÏtÃ;ûù(0›Ù~ð¤ñùhS¯š1çáSì8dILÊ—ö¦~•rÿ*N‘[�ùÏzþžŽÔð£o»ÆÅ3½Û_õëŸ=Ÿàáž¹¸nQhv«µ*ð÷æ`bÏ @/œ€xëpv€zaq)i,{Ö¥Lpg+c<Åä_W“”zÍu]øõ/´¥U¬®–ñ(…Çûl 9i´€´ª[…ægêfç‘ÓFbrgóº—='‘Û�ùÏ*|¡7áéûh|f¶ª½'"xõóyWtŒJ~¥¨UѰŒSÈ5åÓ jy<Ü\Œ ñß×í4îÞ߈þý“ží‡“£ÃE“¦:65Vï?ÉÛßþmŒG9+.9�×¾˜Õ¯}ö|2²rX}fjÙfg.¬ZÔ¶ü}vzYWg':Z½aÕs³=-Ú²×êØGÃc9jYÔ°Fß+_XóZ•ùâÕþ »¿+i™ÙŒýañ5×sõòe�Öí9Bj¡³ÙlƒªK+PË3Ôç® "!5ƒ e|([’–gêæŸ!Æ þë1>HDäV§DDn .NŽÌ~ëIãnùÏ«¶óýò-W´ïÙ‹ðÐËEäÙ‘šŸ¹À<cèìéærÉY¨®U­Š~yùì1›ÍLùmMŸ�@ŸQŸsè´¥õ£EíʼܯË5×�‡›‹±X£ÙlfÄWP`6_Ó±ÞÜ×—²qß1º¼6�V/|Àœ•Û�×úì•G­Z×Îïvwöóáëãe5ísË:UŒ DDä─ˆÈ-åáÎÍy¢G[Àr×~àÄï�™�.¦CÃVÙg/0ìíiVóÜxˆ9½j 7Æê}É2ÿ{¬K>x‰ k‹‚e0ûCw4gÕ´áVãE®TáójV³²Õ6'GjéÖ¬N‘ýïmߘE^¤mýjغp/]ƒáÞÉ’^Âõ*»_�ï‰m�µ‚Žœ6†«åëãÅÚ�^ãñî­­Æ„8;:ЫuÖ~øšÕ{ФF€UrXx…ó«Ákú]‘+cg6_ãm$)6É陜ŽMÂÞÎŽJ¾%)q“ÜyOËÌ&:17gÊ—ñ¶JHn6y¦|"’É3åS¡lÉ«£"""×F ˆˆˆˆˆˆØŒº`‰ˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6ãXÜHQÑ1q˜L&*V(WÜ¡\Ö‘£')0�Ý\] ¨X{{å¶çËËË#.>‘òåünèë˜ÍfŽ;EéÒ%)]ʧ¸O€§Â([¦^ž-K™Ò%qrrºh™´ô ââ©V%àºÇ˜•�Mjj:~¾eŠ»ºD®YXx™™ÆcÊùûâéáþ¯��œœJjz:•*–/îÓ$"2š´ô «ç<ÜÝñ÷/‹“£.mDnvúWzúáç?ˆOHdÚ„QÅÊe=÷Êh�1�ž‘IéR>¼öÒÓÜÙ¹½ÍãycôD*–÷gø‹ƒ/[6äÐQ\œ�©^­2ÉÉ©<ýâÿ6ôIîèÐú†ÄöÝOó©Y½Ê O@&Nû‚u›¶óÊÐ'±¾ûi¿Ïþ GG‡úº—2rì†yœÎ—¨Û÷&ʧSÇ]ò8»ƒðÙW?2÷ÇÏŠnÛÂøÉŸ2}ÒÛT¨pEq¥gdòij¯óÂ�ÇéÔ¡5ÎG“ÞÆÃýß_¬‰‡iŸÌbWð\]]ÈÉÉÁ”g¢oïî¼ùêsØÛÙ]ó±W­ÛÌÚ [ù|ú{Å}š|:s6›·áêæj<—––NéR>¼óæË´iÕ´Xã{õ­ñT­R‘aÏ?ItLaáQ´lÞ蚎õËÜ…,_µž¿þ°XÏIäzR"ÿÚÈ7†rW׎€åÙÂe«xgüG”ôñ¦Y“6�¥UóÆ”ôñ¾¢²óþ\ŠoÙ2 ­Vgg'ºvnG9?ßWtl<›·ïäÙ§½áu°}çÆüïe:¶kÉÞý‡èÚ¹öö×~Ña Q1øù–ùWIRéR>tíÜO�+ÞÇÉÑÑò¾—óÃÉÑ‘žÝï`öÏ xaÈãÅ]%"×ì¾>ÝyýågK‹è¦­;9n MÖ¥g÷;Š;¼ë¦G·N¼ýæKÆãÌÌ,&Lû‚w'}ÂÒ?¾Ãî_$[ÿVë–M([º$»‚0ïÏ%|ß|ê5«ZÕJtjߪØÎEäFPr“ËÎÉaÛŽ=´hÚ�ÅË×�’’F—Nm¨Q­2‹—¯!2:–†õjÓ¾MscŸØ¸ÖnØJRr ~¾eéÖ¥½U×—£ÇO±~Óv<<ÜéÓ£ ÁûѸa]£Ìñ¡îÚKvv6íZ7§vÍjW¯�O =v?ûCŽðÝOó¬� =ûÙwà0ÎN–‹}ÿB]]N‡G²as ÙÙ94nP×êNQFf&ë7CÕ*téÔ{;;b㈈Š! B9þ^²’{{w§n�FWƒ =û©P�cÇO±÷À!*V(Ç�w´ÃÅÅ™�ÃÇˆŽ‰#3+›Ý{ChÚ¨õëÖÂß¿¬ñº!‡Ž´g?ööö4kÜ€ºµ«`2å³~óvîhßšÖlätx$µkT¥sÇ6ýÁûã¯etïÒÁjûñ¡“g2ѾM ªW­dl;q*ŒAÁdçäа^m«z\¿i;­š7fÛŽÝ>v’JËsW׎8::°y[YÙ9:rœ€ å(_Î�¦�ê]âL¦|ÖlØÂ©Ðpš4ªG¥Šå‰‰�§qúVñšÍfÖo¤N­êÆû—@Èáctjߊ°°H²²³qttd톭x¸»Ó·w7²²²Y²| fÌÜÕµ#Êû_ÑçfýÆí—l¹˜Ô´tvìÚK“†õ([ºujU§dIo6o ¢JåŠT8ÓÚ´c×^iÚ¨¡aDEÅÒ¦USÖ«MyKâÙ½K{jO |7W׫ŽGäfcggGÇv-©[»&û¡g÷;È3™X³~+¡§Ãquu¡m«fÔ¬^ÅØ'33‹u›¶[¾s+W´|ç^ [m\|"{¢u‹&xz¸“™™Åªµ›‰ŒŽÁ»„wthmÕâŸ�ĺ�ÛHMK§Ç�ILN¡TIãßi\|"›·í$.>‘õjѦeÓ«J"ÜÝݸ·w7V¯ÛLbRŠÑu_Èaö‡Ð¹c*Zº7‡†E�••�»»«×mÆÝÍ�Ní[QÎÿÜ�¨¤ä6nÞAl|ÕªT¢cû–F/³ÙÌÖÀÝ„:Š››+];µ5ö­_§^žžDÇÄqðÐ1RÓ2X½~ wÞÑ€ƒ‡�´gÍ×§^�š°vã6:µoÅß‹WR­J•Êãàp®þ ؼ-ˆÃGOPª¤7ݺt „—§±=äÐQƒö‚ÙLë–M�ß.‘›‰:êßäRRÒ;á#^5�¸¸9γ/�ⵑãÙ»ÿÉÉ©Œ=‘m�»�ˆâ±§^!0(˜œÜ<þ\´‚g^z‹‚Ë8��[v0䥑„…Grè(ÏÍ{“>!:&€•k6ñäÐ79tä8§Ã"y楑,X¸üªãîÑ­Áû’g20ã‹ï9v 1±qìܽ—ÇžF𾃀¥ëÌÀ!¯qìD(‰‰É¼5v ¿ÿ±8ÓEæ¹Ìÿs))©i|:s6ï¼gi†Þr„Ï¿þ‘×F�gó¶ rssùmÞ"Ö¬ß ÀWßýÂ;ïÈÇ_~OBb2ßÎþ�g‡�"''—“'OŸ˜DTt,‡�žàÝf À‚E+xnØhŽŸ<ÍÑã§òò[ü½d%`+0úÝi¼3~:A»÷‘““Ë{“?eö/ .Z+×n¢]ësIâº�Ûxrè›>v’§Âxjèã=Ü´u'‡¼ÆîàDDÆðúè‰Ìüîcß÷'ʸfðÏšMäççóÉ̘öÉ,K�çæqøè ¢b,çvv[AAÃÿ÷ßüð;)©i|øé7¼3þ#fÿòG‘xíìì8|䣯M¥  €³™·ßÿ�ÃGN`ogdžÍ�L˜ú9“§Ï$¿ €y-åå7Æ2âíÈÈÊbÛŽ=<÷Êhòòò®èó²mÇnZ·lrUŸ±Ô´t^~cÛwì¡dIo“ýî4 ò X¸t¿Í[dœ÷¨qS3~º±ïìŸÿ`é?ë˜0õsBOGàåéAÕÊî ¾êϼÈÍ*77�è˜X¼Kx0bôD¾�ý;™™Y>r‚§†Ž`ïþC€%ùxòùÌûs )©i|>k£ß+Úõ'&6žç_Íñ¡xz¸“››ÇÓ/¾Å_‹ÿ!'7�í;÷ðø�׌ߖа=û:6ÏÐWßaÒ‡_ÿÖŽŸåÑ'‡±nã6’’Rx÷ƒO?õó«>×Óa‘øx—0Îõ‡Ÿÿ`؈w ‹ŒâÀ¡£ xf8[¶ï`Ãæ@&4“^CDd ë6ncà�×9| °´\÷<œ…ËV‘˜”Âg_ÿÈK¯�¥Àl`ú§ß2iú—¤¦¥³;øýŸNDT ?ÿþ7ë6m'!1‰§N“‘™ÉŽ ½,\ºŠ!/¿ÅÑc'9~"”ç†�æ�¿-¿³¹yyŒ~w~ò ¿/XLbR A{ö3ó[Ëo€Ùlæ�ÑùðÓoHJNaÙÊ ô|îu/_ÃKoŒ³tûŠˆâÙ—G¿-"7µ€üäääòDÿh׺&S>=û=AÍêUyñÙ�DÇÆ±÷À!Ú´jÊ®à´iÕ” c^,­wöyœ¨èXÊùû2ý³oyñ¹�|Ë–áÑúXí“••Í—ßüÌÈ7^àî»:мI>ûêGztëd”«]³Ox€^lß±Ûx\XDT )©iT=3p:??Ÿ�¿øžW_|Šûûö B9?~žû7mZ5åã/¾gðÀ‡xzÐÃtïÚ�×ÞÏý÷ôÀ·li|¼K0òõ¡T«R‰¯Î$(/ yœ¥ÿ¬åÑï¡EÓ†lÞdırÍ&N�Ž`îìOqww#++›‡Ÿx /Ï*¬»Áƒbó¶�üüûß8:9’——gÄ–;ƒóç|�«‹ õj×àÍw&ñÓ¬éÔ¬^År‡ó¾'ˆˆŒ¡J劗ü¬$&%ãîæzU-g“�ÚµªñÖkC‹ôko׺¿Î·$ ÇŽ‡âîîFRr aáQT,GОý }zÀ�]¯NMv¸ac�Dn´CGNðÓ¯–Vô�[w�••Í=ww#%5�°ð(¾üx¼ñ}2âíؾs�Ôá·?ãîîÆ¬O'áèèÀã�ÜG¿þÏe?&6ž^}‡žÝï`È“–n¥'N�ÆÞÁž¯>™`L$ñØS¯¼ï þ~eùú»_iÙ¼ïŽÀ®=ûyáµ1Æ1?ýj6}zuåÕ3ã÷‘ïçÌã‹éïQ¿n-~žû7Ó>™Å‚Ÿ¿4êêÛ/&S¯v Ìf3ã&~Ì—ßÌáÓ©ãøþ§yÔ¨V™SÇbogGJj |‘5ë¶Ð­K{–ü³–‰cÞ0Æ›¼?ùSN…†-:õëÖ¢wÏ®Ìûs o½ö<99¹|1ko¾ò}{w u‹&|øé7ôê~ögZ:˜;û3ìììX¼|�q¼�[vpøè ~ýþJxyb6›ykìfýðãF¾ÂÒÖ1èÑ~<ùøƒT,Ï©°ˆb#r>% ÿ�X¾€(QÂ‹Æ Ï}!—.éƒÉ”@ß»»Ñ·Ð�Ìö�{ÈË3GTt,=»�ëÜãÎNFr*4œÄ¤¼<=Œ;5®..ä™L;jÄp%òò,-ìÞ‚¿_Ybãˆ�KÀ¯l9N^^Êóýœy|?g�Ú·¦g·Nô:ÓO9hÏ~ºu逃ƒel€¿_Yü2“’>ÞÊ”.yÑäà®®qu± Æôòô [çvï?ÈCýî¾è>'O‡“••E�;;Ïõì~|ø%§OGR®œ¥‰ýŽçúä–.UÒxÎC™Ò%�‹äȨX¢c⬒™��õ#9%�¤äÂ#¢èÝ£‹±­e³Føø” äð1ã‚¡sÇsÇe.ñÚ…ï§SûVF"éææJ§ö­ˆŠŽ½`y'GGƾ5Œç^�ƒƒ_2ÑjŒF­U�º-YÒww7£G /Oì1å_>® ›éxý›3³²yù�q=~Š c^¿à Ú¶­š1iúLâÙµ÷-š5"*:– =û°³³tõ¸Ø²oÙÒl ÜuÅñˆÜl’’S8tä¸ñ¸u‹&ôës—Ñ�ò�Ÿ¿$//�“¡a>r‚c'B©VÅÒtמýÜÙ¹�ñoÝÏ· ~™‰�·¥E!1)…^}‡¤”T¾¿·ñujUçço>"+;›“¡á<|Œ¸ø£|Wðƾ5Ì(߬IÊ–)@�ÙÌ®à´kÝÜøí(ïïǾG.š€˜òL¤¥�› +;;‡ôô 2²²Ø{à^ždffÇ-U҇Ȩ“¨Q­2õj×,-¿÷öîΫ#Çc6›Ùwà¾×øŽñ.áEû6Í Þ�n]ÚP¡3¿û…”Ô4Z4kÄ;ÿ{ù²ïMhX©©iôºëÜïp÷®?õ3N††Q½ZeúöêvÁîgA{öS½je9aó8wßÕ™¤ä«Á’„œååéyÉ8||JX=.YÒ›£ÇO]rŸÔ”4<ÜÝ�¤,ãîn$¥¤ È¥¦”-,#3 ·B3µ$%§œ9Þ¹÷ÁÕÅ_N…†XõçK‹GrrŠñØó _»°¤¤«q&`ùA½XP¥rE|Ë–Æl¦ÈxŽÂõE?£WjÓÖ ÞùßKW\>>!‘ûút§dIo¦}2‹�'�)RÆ·lijT«ÌŽ]ûØ|€ŽíZSÎ�]{`ggGƒzµðñ.qÁ㻹¹’žžy¹0DnZm[55¡Ÿ///�IÓg²~ÓvªV ~ÝšVw$%§ù·Qx¼Þ©Óá¼1l/]Ég_ýÈè/�’šÆ»Ì`ß�ÃÔ¬Q•ukQâL7¨‚‚RRR)QÂú{íl7©ŒŒLrsóظÛèš P­J~¾¥/zž ë׿­×ž·zî‹Ysøñ—tîК¤¤rsóŠüöÜyG;ÌgºQ]è7"''—̬,RRÓŒs(sBR2Mz›_æ.äÛç2îƒ4jP‡Ñ#^¼ä4Å)©i¸º¹ZM7îàà€§§IÉ©Æsç×ÕY‰IÉDÇÄ9§ægÆ ¾þÒ3Ìýs KW¬å£Ï¾# b9Þ6„–Í®m.‘E È-æ£Ï¿£~�š¼1üYìíìÈÎÉá«ï~,îDEÅ“áÑÆ¾åüÊ’ŸŸÏè/wÉM¦|Ž�8Eµó.\/¥ÀlæÏ…+hߦ9övv”ó+‹w /&Œ}Ã(“–žAllpvvvœ�¤N-ËÀÁ¼¼<£ Ý¿áïW¶H}~ÿ/dö/ ð.áEFV?þº€ÁúW1œ/#3“üü|ãBäJT(ïÏ3Oü’/¿ù™Æ�¸h|åüÊ’™™E|B’CJj))iWôïæwºðïiBb2™™–VŸ}!‡¹·ww>Ú�Ô´t¾úî&Nû‚?™yÝ?K"ÿ†¡ßbrrr)UÊ{;;òóóùaÎ|ÀÒ¸Lé’4kÒ€/¾ù™<“‰ÜÜ®..”ó+‹££ŽNŽtïÒ�¿ÿcülÛ±‡Y?üŽ§ç•­Ó°tÅZ£+B`P0«×n¦{—s«g¿´ «\©"5«WáÓ¯~ÄdÊ'Ïdâ“™³©[»:×°8d•€ŠÄÅ'’›k�íç[†F êðÅ7s0™ò1™òùæ‡ß9|änn®´oÓ‚™ßþLVV6f3_ÿðîîn4iXï_}6#¶ì`Ï>Ël0ûCްas ±=-=ƒŸ~ýӨ냇�óËÜ¿=âEÞ~ã%~üeÇN„þ«ηeÛ.ÚµivÉ2KV¬µŠólKK…r~ ôñ]‘ÉÀ2dõú-¸ººP¾œõêÔ$#3‹ ›/Ùm/<2ú†,t(r3ÈÍÍÃÅÕŘ®z_Èa¶ïÜCVv6`™ îïÅ+�‰víåë~3f<;V½:5¹ÿžLþh¦ñ{âá鎋³3kÖoåÄÉÓdgç–î°?þö'IÉ)˜Íffÿò™ÆM¤»ºväÛÙsIKK`ó¶ ž:‚œœœ«:?G{’S,¯Ñ¼©¥Uà§ßþÄl6“g2ñá§ßðÅ7?壢cùí�ŘÍfbãøföoÜuæ†Æ]wvdî‚%Dœ™œdÓÖ�Ó­K{ xåï±jí&Ê–)�§§;n®î-�™•�Ùl¦ByêÕ®Á§_Í&ÏdÂdÊç“™³©^µ’1NðRºwéÀÆ-;Ù±ËÒ¥,5-�WG¾ÏÊ3qL�ñ5ßÏ™�ÙlÆËÓƒÒ¥J^4&‘â¤�[ÌS�?À˜ ³bõ23³¸·ww:¶kÉÐWßaÙ‚ïýÆ Œxûºß3{{ž{ê1‚÷ÄÓÓ{;;Þ5œÑïM£{ß�899RÒÇ›‰cG\r*Ä�>ÿŽ�>ÿ°LƒØ¬q}¾úd¢±’»�O &Œy�w'}Âç_ÿD^ž‰úuk2ê�ò䣌xûîºon®.äå™õÆ‹89:Ò«û=v’GŸ†�w rrrõÆ ”)]êŠê£K§¶¼6r¹¹¹deçðÔãÒæÌlKõëÖâÃO¿!:6Ž©ãGûØÛÙñö›/1rÜTºÝcY¢B&Žyãšæ•÷ñ)AÕ*9tä¸1ŽfÌÿ^6Žïèè@…òþL~ÿ-Þö o¾3‰»î„³³3^^Ló:..Îÿê³Ñ°^mžü/¾6/OJ•ô¦ÿÃ}�»�©ié|>ë'*–ïlÞ�4ƒgž|Ôx}°ïMþ„ï>Ÿò¯â(lýæí {þÉK–™ÿ×R*”÷¿à<øýº—«6ðŬŸðð½VÛÔ«…‹³3M×,ã§7¬Ë‰“§©q¦Ÿõ…ì?p˜—Ÿâº�£Èͤ{×ü¹h}zwÊù•eðÀ‡øê»_©[»=ºuâð±“<6øã;÷­×ž¿`KÄóÏôgíÛøé×?yàÞž¼üÆ8ú>2{{{֯̀GîcÆ—?P«FUžð'N…Ñç¡gpws¥S‡Ö´jÙÄèÊ:ô™¼7ùSz=ðÞ%¼ÈÏ/`ÜÈáWü]V…òþg¦J¤s‡ÖLóc'|Ä�¿, Àl¦jå«Іõë°as ³~ø�ì¬lZ6oÌsƒûð@ß8x„‡½„—§99¹ŒxeµjTàåç1ñÃ/øìëÉÈÌ" b9¦¼÷V‘˜jT«Lll]îîϺe¿2zÄ‹¼5n Ýïˆ��þ~e‘ñlç«U£*#† áÍ·'áêêBFf&]:µeàcýxeèS¼ûÁ –®X €›« cÏ ü¹™Ø™Ïv„”[FVV6Q1qT,ï�³³¥ŸivN®..DDÅà[¦1±ñxzxàèäÈ]÷bý²_­ú¤ÆÄÆc2™(çï{Áù߯E�ÙLxDn®®Æàóòó󉊎ÅdʧBcžõÂçG…rçÎérž6Š>=ºÒ§WWÂ"¢ð-[ºÈLKYYÙ8::X�{aÑ1q`ggÕúZÌ]°„¨˜8^ú¤õñcã1àïW¶Hr“�˜Lvv6åËù]—µÒÒ3ÈÉÉÅÃÝ�¨˜8ªTªÀÌo!??¿Ø.¸>~SÍQ˰7ßå÷ÙŸý«£Enfg¿‹½<=Œñg#Î:û�[ÞßïŠo~˜Lù„EDZZδ˜œ=n|B"nn®dee“—gùmyì©WøßkÏYµî¦¦¥“�˜DÅòþý^¾Zf³™È¨ñ+[Úø>ýé·? ÞwˆiF�ƒƒƒÑ-ª°ŒÌLââ©X¡\‘ÅR33³ˆŠŽÅÓÓÃèR{!yyy˜òó­~ƒbbã1›ÍVã¯ÔÙº.éã]dÌN^^‘1899âïW¶Èx=‘›�Z@nAnn®Eº�œýa9v ­š7fè30™òùð³ohÖ¸~‘/z¿yÁ}!övvœçàà`Üi¿Ø9U­|mÝbìíí©Pá¢Ç½”kùa¸�>=»2äå‘äääZý˜_*±9»ˆÖõ²?ä0ã&ÎàëO&R­J‡ŽgáÒU¼ÿök×õu®ÆÍ”|,\ºšÇê«äCniú.>¢‘kùÎutt(²ÏÙã~ûã\¢¢c™8v®%]X°p9‰IÉÔ­Uê| /Ï"“pü[vvv—]õR¿yîîxT¾p·_ww7cæªKqrrº®¿³ªë¯u¹éÏEŠ›�Û̸QÙ4ýKºÝ3;{;êÔªnÌb"7Ž»»íÇÜ?—0ðÑ~ÅCÛVÍèÿp_ž>šì¬¼¼Ä"""ç‹OHdûÎ`¶íÜÃþ�Ãä噌mûR"rJ@ä–ò÷’˜ÿ÷rªW­D«æ�iÕ¼1Õ«Vº.³H‰ˆˆœ‹$pW0�;ƒ9x丱ªº��5ªU¦UóÆthÛâ’+¢‹ÜÈ-%++'GGŽ�娉P~™·ß²¥iÙ¬-›5¢Qƒ:8kLˆˆˆ\¡üü=Φ­;Ù¸›ø„Dc›‹‹3�Ô¡Uó&´nÑØ˜ÖXD.Mc@ä–“››KÈácî fKà.â’ŒmÎÎÎÔ«]ƒVÍÑ®u³«^äJDDn}iéï³ŒçØ¾s™YÆ6o/š5n@‡¶-hÚ¨>NNº—+rµ”€È-ïbÍå•*–?ÓU«uk×PW-‘ÛTtL�AÁíeÈaLùùÆ6ýVˆ\_J@ä¶’’šFÐî}îÚË®=ÈÌ:wW«„—'�Ô¡U³F´nÙw·âWDDn³ÙÌñ“§Ï$Á;jl³··§NÍj´jјv­šQ¾œ_q‡+rKQ"·­³ýzƒö²mçnÂ#¢�m…|Z·hBÀ%Vi‘ÿ†ÜÜ\öœéZLbRбÍËÓƒÆ ëê&”ˆ (9ãRÍïþ~eiÕ¬­Z4¦AÝZ8:ªÏ¯ˆÈAá–ï�»ö‘�“clóó-CÓFõhÕ¼1Íš4Àñ&X Qäv DäÎ@ܳ/„m;ö�œ’jlsuq¡QƒÚšõDDä&u©©r OÓ^£ZåâU䶤Dä2 8q*ì‚ý„Ïÿ1Óš#""¶WPPÀ¡#–.µ[wcl+<ûa‡¶-(UÒ§¸Ã¹í)¹J1qñì!0h»÷†X­|ë[¶4Í×§IÃz´hÚWW—âWDä–”ž‘Éž½!–©rƒ‚ÉÈÈ4¶y—ð¢y“´jÞXßÅ"7!% "ÿBNN.�XÖÙ¼} ‰[s¤9eJ—,îpEDþÓ ßÚ‚ÉtîÐÙ±zÚ¶ÐT¹"79% "בÖ¹~®tªÜ6-›R±¼q‡+"WH ˆÈ ¢5GDD®^nn.!‡--Ë›¶‘˜”ll;; H‡6-hÝ¢ îÅ®ˆ\% "6p©5Gì©]CkŽˆÈí+5-��»ö¸k/A{ö“••ml;;¶®UóÆ4k\_Ó ‹Ü”€ˆ­9""·»³ßƒ›¶î¼`—Õm[hvA‘[”‘b¦5GDävPxªÜó[‚��œ¨W§&­š7¢}›æ”.¥I;DneJ@Dn"—ZsÄÞÞžjU´æˆˆügdçä°wÿ!ƒ‚Ù¶c7É)iÆ6//OZ6mH«æ�iÞ¤nn®Å®ˆØˆ‘›Ø¯9Ò¬!®.šç^DŠ_l\»‚\rªÜV-Ó°^ì‹;\)J@Dþ#rrr Þ�À `vìÚ§5GDä¦q±)È O•«I6Dä,% "ÿQZsä¿mÅŽ–lÛwÁm%<ܨQ¾,}Ú6¤Œ·gq‡*RDn^!‡Ž¸3˜-�»ˆO8wCÄÅÅ™Æ êЪyÚ´l‚�w‰âWDn2J@Dn—ZsÄ»„Í›4 UóÆ4m\_kŽÜ$¦þþïý¸ä’eÜ]�™ùêúuhRÜáŠ�––N°1žc�Õ÷LÙ2¥Î}Ï4ª�““fï‘‹S"r‹9{grÏÞƒlÛ±›ðÈ ¯9Ò¦ES*VÐÊÁÅ¥pâêì„«³yùùddåå\œÙõõh*ù–*î�å6TxÊð}!‡ÈÏ/0¶©¥UD®•‘[ܯ9R¯6ŽÅîm£p2â‘»3¨·±íHx ½û5'"ã÷D^¸{q‡,·�Â3ñmÚº“Óá‘Æ¶³70:´mA»ÖÍ(SZI±ˆ\µ‘ŠÜâüýÊÒ÷înô½»›±æÈÙi~£câX¸l5 —­ÆÓÃ�&�êѤa=­9RÌjUôã¹>�øß× 8À¢­{™úÛ?Œxô.Jyy0꛿(ëãÉüqÏ�”–Éôù«X¼uQ‰)”ps¥CüñHwêU>7øûå[ø~Ù&>s;ŸâÛ¥[HHM§v€CûÞÁ#]ZXÅ•’‘Å´¹+Y»û0áq–>ÿåJ{Ó¹q-#€Œ¬z½õ)ÍjUâ��½yýËy¬ :ľoß¡”—f³™_Öìà‡å[ �¨_¥üóy|·l³Õ±¶8ΖÇÙy8”)ÏÝ_ÜoÁ--9%�]{ö±iÛÎ"S|«eTDn4% "·){{{jT«L�j•éÿP_bbãÙ½÷Üš#ÇN„rìD(¿Ì[ˆ_Ù24m\OLm$;7�y낌ǵüŠ”ùfé&ò ¨QÁ— e,Ó.�œõ§‘|¼ú`7îmߘý'#xkÖŸ¤gåðì‡sþöÜœ�¬ŽµýàIÚ7¨N¿M �âûå[0›Í|ðËrîëЄZý˜»v§‘|tiR›Á½Úa>Y°†�‡C �b}ðîjQ¯È±sòLø•,Aé8ØÛ±<ð€‘|´©W�±ƒz“—ŸÏØï±ûX_.\O�–õ¸³Y�â~+n)›9ÏÎÎŽÕ*7jT«\Ü¡ŠÈ-NW"€ŸozvëDÏn�Ь9ÏòUX¾j..ÎÔ­¥5G®§…[‚9 @Zf6ûNF“” €³£tlVdOw–O~™|�ýþظ°\Ô¿÷Ô=4¯U‰„Ô Æþ°ˆ¨ÄoÝËCw4·:VêX4áEœ-wºK•pgÚï+1åð뚌ԇŒœ\º4© ÀÃûS±¬Iiì< XZUΗ“gâ³a�òD�¶Æs?¬Ø X.|¿ó ãXUG–¡Ñ3ïc6›™µd“�)?¿€ÃG�³iëN¶î&>!ÑØæììL“†–©r[5oL©’êr)"¶£DDŠpqq6î†BÑ;§{ö…°g__ÿð›f¹‡Åp8,¦Èóövv|8ô!ã½°'z´5’€½'"È3Y&È3™óý"c[Xì¹ Ï­NI@»³¥‘| êÞ†i¿¯`בÓ<O'ž¿§¦ü‚�‡±*è ûOEðצ`c¿‚ ÌiR½|Y«ä0wWg¾^¼Ñj››³™9¹l 9QÜoËRáq^Ûwî!#óÂSr·hÖW—âWDnSJ@Dä²*”§R@y¼·—Ñwüìš#§Ã#9Éü¿—Y]à4kR_V¯�©”+uî´“£5+úñt¯v´¬Så‚ûT(c}Ç:!5Ýø;èÈi‚Î$ç‹J,ÚJQ±lÉ"�íìì0›Í$¥e�kÊgœ¥|³t3©ç.jì/ynç 1-°ŒOùhþª î—�šA®)gG�?¸œ˜Øx¶ïÜsÁ™îtƒ@DnFJ@Däªøx{ÑõŽvt½£Ý×Y³a+k6lÕš#Wa`÷6VÓð^‰ó/$]œÎ�븷}cèÔì‚ûù—,º*uBJºõãÔ c|€§›å.ùGóV1}ž%YhS¯÷wlBóZ•9ÃÐ�¹DœEŸsqr$Ï”O¹RÞL¾Ä`s{],_�ÙlæøÉÓVHœeooO½Ú5hÕ¢1m[5£B9¿ñJ""7†¹fÎNN4ih™º÷ÉYs$äð1B㇟ÿÐÌ:7X�Jç.4ì­VOÏ/( -3ç L °hë^žéÝÁêñY�«W`Yà~À²hâ’^2Z&þ<3îäjÔð'èH(Éé™ôjUßX„,cYò °³³»lëÊí$77—=…¦ÐNL:×’uv íVÍѺeS<ÜÕò("77% "rÝ\éš#g×hÒ°mZ6ÁǻĿñÛ\e¿Ò4­Àîca,ܲ—;Эy]Ò³rûÃ"¾]j™uê§‘Oq_¡ä`ÍîüöÅ<éÒ‚}'"xû»¿K+Ë#][PP`iÉÉ3±dÛ>ZԮ̆½GùîÌZ"W£_‡Æ %+7�ÑßþÍûOõÅÅÙ‘%ÛöñääÙä™òéס ?Ž|ª¸«µX¥¤¦´ÛÒÝqçî}dgçÛ ÏL׬q}õs."ÿúÆ‘âÊ×™cµæˆ¦½vÓ†>H‘�›Çƒã¾ÆÞÎÎj`xï6 ¹·}ã"û•ôrgÖ’MÌZ²Éêù!½;ÐäL Hç&µÙ}, ³ÙÌ ¾7Ê”.áAfN.™Ù¹Wç³÷tâ÷µAì;Á׋7òõâ�V±–+å͸'î)îê,š*WDnJ@Dä†Óš#¶ÑªN–~ð£¾ý›íOôeK2¤O^î×õ‚ƒ�Ǿ—ß×îdÃÞ£€eñÀïíÌ[ý{eF èIdB2s×a6›qwqfp¯vÜß±)]_ÿ€o—mæ¹{:]6N7g'–Nz‰ÑßþÍüõ»ÈÌÉ¥ÀlÆÍÙ‰îhƨ½({{Lï\PPÀ¡#Ç ÚËÖÀ]DD�› ÍÙÙÙ2ž£y#:´mA©’>Å®ˆÈuag6_`ÞD)¼æH`Ð^“’�m…×iߦ9¥KÝ¥×Crz&ñ)é”.áII/÷"Û'þ¼Œ~YÀÜ1CèÕº1I©¤efSɯôEgŸÊÈÊ!:)• e|¬Æn\«\S>a±‰¸:;á_ªö·þ¸�ìœöî?Ħ­;ÙLFF¦±­„—'-š6´L•Û´!®®š*WDn=J@Dä¦qvvŸ=ûBŠtA�;¥h^žé¶jm¹P"礤¦á]Âëº/&.žÝÁ–V¿]Á!˜L&cÛÙ :´m¡©rEä¶pûüÚŠÈMïl?÷Õ*Ys$hÏþºæÈÜ?—Ï C×m·1S~>s,aÏÞ¦¼ÿÖ¿:Öé°H6mÛI`P0ÇOž6’éÂSå¶iÙ”Šå5EµˆÜ^”€ˆÈMëBkŽî fëŽÝÄÅ'kŽ8;9Q¯NMš4ª{Ít�A{9~2”#ÇOòæ+ÏQ­J@qŸ¾ØXL\F`P0µkV+îS¹e¨«ˆÜ¶víýWûÇÅ'2êÝiüðóW}g]n¼Ý{CxyÄ»ÿ*ùؾ3¸¸OEDä–¢¹míØõï°Lû;ÿïeì 9̈aCð÷+[ܧuÛ3åç3wÁ~�¿ˆë1ÌñÔépbbãñó-Sܧ&"rKP""·¥´´t=Qäyggg\]œqwwÃÍÕWWg\]\ððpÇÕÅWg\]]ñpwÃÅÕòØÝÍ 77W²²³¯!¹ÞÒÒÒ©W§o�x‘¼<™™ä™òÉÎÎ&;;“)ŸôŒ L¦|²srÈÎÎ!Ïd"##“¼<9¹¹deec2™ÈÈÌ`{P0}{ÝYܧ&"rKÐ,X"r[JÏÈ$:&Î’T¸žK*´rµœ/7/s�çâEDä– DDDDDDlFƒÐEDDDDÄf”€ˆˆˆˆˆˆÍhº›œ3ë£óX‘ÞD§ÓóIÉ+ ¯ ¸#¹u8Ùƒ·“=•<hRÊ‘Þ]èèç„‹½Æ¼Iñб¹ÈÌ&ïÏ`~h6éyúø‰ˆˆØš§“Uvå =(ç¦1b[J@Äf²òÍLÙŸÁ—‡³È2™±ãNL?gª{9PÒÙ'}Šˆˆ\7y�”[Àñ´|6Æä²$<‡àDfÀÍÑŽj»ófw\Ô""¶¡Dl"2³€þ’Ù�h¸¿²+o7ò š—Cq‡&""rÛ9ž–Ïø½üš�h^Ú‰Ÿ;y«5DlB ˆÜp{“L<¼.™¨¬ª{9ð]{oš”Òð#‘â¶+!�§·¤r"-ŸrnöÌëìCÃ’ú�–K ˆÜPÑYtY‘Hdfí}�ø±£7e\twEDDäf‘”[À“›RY�‹¿›=k{”¢¼»~«åÆÑ§Kn˜¬|3�®O&2³€ÎþÎüݵ¤’‘›LIg{æwöág¢³ x|c Yùº?-7Ž®写²?ƒÝ‰&ª{90»ƒ·—‹ˆˆÜ¤œìav‡Tór (!�dwHr Ó%¡Ü‘™|y8 ;àÛöÞø8kf ‘›YIg{¾iW;àóC™Deia.¹1”€È 1yY&3÷Wv¥©œ‹ˆˆü'4/íÄ}•\È4Y¦Î¹”€Èu—S`f~h6vÀÛ�<Š;¹ o7öĘw*›œ�‘ëO ˆ\wë£óHÏ3Ó´´“Öùù�©áå@£Rޤå™Ù“WÜáÈ-H ˆ\wË"r¸»‚sq‡""""× wE–†çw(r R"×ÝžDý”€ˆˆˆüuôµü†ŸùM¹ž”€Èušž îW"""ÿQgÃC3ò‹;¹)‘ë.-Ï2`MSˆü7•t±ü†§æjº\J@äºË=3c†³½‘ÿ"—3¿ášKn% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ8w"7+ï_b‹;ùKéï[Ü!ˆÜ”Ô""""""6£‘ËØ{OÙâADDþC-Š+îDnjj›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ8w"rû '7;›ê5k\Óþ,]´„eK–�››+U«UåáG¦i‹fÅ}zWlæg3‰ŠŒ¼d™‡}ˆ�w¨W}öøúúÒ®c;7i\,qM7žÜÜ\ã±�½=¥K—¦SçŽ4nÚĪìö­Ûppp¤E«Å«ˆÈíH- "b3_ÌøŒåËV\Ó¾ù&o¿9š§L§jµª yþî½ÿ^bbbyõåW ܶÝ&çp`ß~Æ�{]�¹aÝzN?i“ø¯Ôç3>gÝšµ—,³cûöïÛoõ\nNëÖ¬ååç^b_÷¸¦LœÂŽí�—,³qÝŽ?a<ÎÏÏgÛÖm¼üüËLý`*ƶ�§~ÌWŸÏ¼áõùéôOذvý ‘ÿµ€ˆÈ ••�Í‘C‡Y³j ›7næ‘�^Óq~þñg¶nÝÆGŸL§Ió¦Æó=ö¯¾8œIã'1ïï?pp¸±÷UbbbY·fÝ¿:Æó/=oõ¸[§;¹£Ë<óü374ö«¸m;îîntîÚå’åê֯ǻß³z.ßdâÕ—_åûo~à¡GÆÎÎîºÅµyãfjÖªIËÖ—.×±Sž{Ѻžÿœÿ'M�NÍZ5¸ï�~ 1‡ÿS¸mÛv¼KzÓ‰;nøk‰ˆÜ씀ˆÈ µuãf>œü!öö×–äæä2÷×¹ôéÛÇ*ùptpàñ'ç½·ßåÈáCÔ­W��!dddP³fMæþ6—F�Ò¦][ ظn»wíÁÑÑ‘æ-›Ñ¦][« ää¤dV,]NdTNŽN4lÔ€wtÂÁÁžcG޲kgf³™….¤i‹fT¼¢ã^‹�ýؼq ))É”.SšÎ]»PµZUcûÒÅKhÙª¡§BÙ°n/¼ò®.®„ž:Åò¥+ÈÌÈ U›ÖÔ­W—M6Ñ·__cßø¸x–/YFtt4~þ~ôèÕ_?_–,\LZZGaÕ?«èvW·«ŠÛÁÑ‘V­[±g×R’“ñ)Y°tÁûgÙ ’ñõõ¥gïžøúùYí{ìÈQV¯ZCZj**V Wï»ñ)éC¾ÉÄ’EKÉÉÉaßÞ}”)SšŽ�;]U\ýìGà¶íü:çW#ÁÎGÒÓÓY³r ½z÷bÕ?+‰�4’ÂÐS§X¹|©©©T çÝ=ñòò²:þÖÍ[ܺG'G7kL‡ŽXü÷"ÒÓÒ9r˜5+WÓµû�–ú=Í?+VZêÃÏ—žw÷2Þ€–¯ QãFÄDǰfÕžy~[6m¡r•ÊÔ©[Ç(w6î6íÚXí/"r³R,¹¡ºv¿“%«–²dÕRÊW(MÇ8Bjj*ïèpÁí­Û´fɪ¥Ô­W°tÁ™ûóï ñ.ø›¸Ø8 3j ïŽy�è¨(N�<Ũ#™øîã8‰ ‰<õø“ÌŸû™é„ž<Å»o�ãÓ�> ôT(ö`ÝšµDGE]Ñq¯Åò%ËúÌPöíÝKNvë׬gðÀÁ„ì?`”ùdÚ ~ÿåwÞ|íM·mÇœ_ÀÎÀ� 8˜-7“”˜Ìä “˜ùð“3Ÿ•õdedròø ¶mµtܶe+O x’5+W“žžÁ¢¿3è±Aš�‡‡û¿ú̉ˆØŠZ@Dä¦@¹ ®xŸ�;xâé'xbð“ØÛÛ³bÙ ¶lØÄŒ/?¥acËàî­›·ðÖëoÑùÎ.´ïØžukÖ’“�ÃOÏÁÓÓ€¿›Íü¹0ü�áÜyW7ììíyï�w™þéGWtÜkñÇÜ?èÕûnÞzç-Lùù<ñè@6¬ÛH½õ�r+W¬dÖ³¨^£:ùùL™8…ÖmÚðþ¤ñ88ØË3ƒ[{ò„ÉÔ¬]‹)Ó§àìâŒ)/�QoŽfòøIüòǯLùh O<6ˆÎ];óÔ�Á—Œ3.&޵«Ï�ÉÍÍåà�ƒ¬]¹š‘cFáèà@~~“'L¡I“&Lž>G''²²³yõ…W˜6ùC¾ûé;232˜>e:ÝîêΨ±£°··'%%…¡O?ϧÆ”�¦0ýÓ�¸·×½<ðÐô{°ß5Õ«_9b£c.šïÚÃü¿çSªt)2³²˜3”Ï?þœ‰S'²7x/ æýÁØ÷Çrç™Ö¢e‹—2iü$øÓfLcÀÃèÑë.=õ¦ü|¦~0•æ-š3iÚ88:’™•Åð¡Ãøhòt¾þa–Ëò¥ËùbÖ—Ô®S€®wvaüØ÷INJƧ¤Ï™ÏÀ*:têˆÇ™Ï¬ˆÈÍN- "rÓËÉÉÀËÓãŠ÷ñòòbàSOݾ¶oÙFóV-�$ mûvÔ¬U“m[,wù{ßÓ‡ù çÉG~~99¹dgf]ôu®ä¸×â£Ï?æõÿ½~®²²ÀÎŽ¬lëXz÷íMõÕ8JtT�?1À ãëçË]½zå9røž€³‹3ŽNNôØŸˆˆ#Ù»RCB;jŒñß„qãY0ïê7l@ó33KE†‡À'àè䀛«+÷„cGŽ’ššÊ¡ƒ‡ÈÊÌä‰ÁƒŒ÷ÌÛÛ›~öc÷®]˜Íæk®ËÂ\\\È3å]´Ìcƒ£TéR€eÒ�´´4ž|úI£K�——Úth‡££×o 6&†½{‚éÑë®ëR?""¶ ¹éy{{mŒ'8ßž Ý”*]ŠJU*PÆ·¬Ñ· ",‚ÐÓ§yôþG¬öKŒO4¸º°eS›6l"ìt'Ž''7ûKÜ«¹’ã^ ;`Áü?8°/„ððpNž8‰½}Ñ1%å ½ÆÙ.A+X•©TùÜãðpK‚1yü$Ïýœ½PNLH à¼ý/¥S—;?i¼ñØl6sèàA^ùu¾úü+^ÿßëDFEP©’Õ¾•+[ޫȈH¢"£pp°§|ÅŠÖ±W©LNv ñ ”)[æšëó¬Ø˜XJ•*uÑ2å µ´E„…cggNjϾ`U&++›ìœl233ˆˆˆ b@€Õ˜W pÁãGEF�y_¬ëãìg72"’µjà_ÞzŒŒ»›mÚ·eÝšõÜs__V®X…OÉ’´ls™Qù""7% "rÓ«[ß2¶ãÐþƒÔ­[·ÈöؘX†½0Œa¯3.âœ�¬Ê8»ºÐ¼E3 ~¢ÈþgZV¾Ÿõs~œÃ}ýîãÁ‡ jµª„aúäé�íJŽ{µòróxîéç)ÈÏçÞ~÷rß÷R­zuƽ=®HÙÂIÄÙ»ûÙYÙ”83ö §Ðš®®®¼0ìE*T,Ú¥-  "ÿ†��uëգà ـ›››%®3-YF\9ÙFÜnnnäç�›—‹›ƒë¹2Y–2®n®Wòò—upnîîT©Zå¢e %®®®®8ØÛ3vüXì튔uwsÃÙÙ™ÜÜ®”ûÙúȶÞ';ûL}¸ž;WG§¢?Ó]ºuá½1šÊª+éÞ£»U²-"r³S,¹é•¯Pž&Í›òËœ_IOO/²}É¢Å4oqñÅä*W©Dddµj×¢v�ÚÔ®S›Zµk±uÓX¶d9÷?ø/¿6Œ;ºv¦R•ÊÄÇÅ_2¶+9îÕ:°o?§O…òþ¤ñ<2àQšµhŽOIâbc/KåÊØÙÙ±/x¯Õóûƒ÷W¬X{²³²ŒxÏvñÙ°v.®n×å=óòô$%%°$5vvvïÚmUfwÐn©P¡‚Ñê²w÷žóÊì¡TéRF·¸#ôÔ)ÿ½ˆž½{]Ÿ.§RåJ˜òóÁÎΪ¾âØ»'GG*U àøÑãdf�ë—‘žNÿcGàŽ"Ç<ÛBµç¼úØ´'g'Ê—/wɘÚvh‹³£#³¿�ÍñcÇÕýJDþs”€ˆH±›óÃOL7þ’e^}ãUÒÓÓyù¹—ؾuéééDGEñëœ_øé‡9t¹ÓzŠÚóõîÛ›S'NðùŒÏINJ&==�¯¿øŠ9?Î1úû—ôñáÄñdçd“Ÿ_ÀÖÍ[øå§_0™òŒ®;vvvpêä)²s²¯è¸W˧”¥›Ù¡ËŒHééé|öñg„�#.:–ü Œ+Ëx�.wváËÏf²/x™,üs!Û·l3ÆU¸{xе[7¾þòkvî /7�ãG�ñþØ÷ ;fŒ±³³#*2ÊH"®–ƒ£#II‰˜ÍfJ–*Eûíùæ«oظ“¬ìl6mØÈÏ?þLÏ»{âìâL�Z5©S·3¦Í ä@Y™™,_ºœE Ò·ß½Æqíì "<‚´´´K¾~è©Ó¬]½–µ«×²úŸUÌúrCŸJ?_ž~öé+>�:õêR½f ¦L˜Ì‘ÃGÈËÍ#hÇN&¾;�ŒtËX�î=»cÊËcÊ„ÉÄÅÆ‘––ƧFjjª‘ÜÙÙÙa©Ï2eËЦ][¾þòkví "3+‹�ë6ðËœ_éÕûîË&G®.®´íØž?æÎ§z�êÔ¬]ëšÞ#‘â¢.X"Rìöï+rçû|U«Uå³™Ÿòá”éŒ>ÂxÞÑÁ�ž}îæå׆]rÿºõê1rÌ(>ùðc…n___Þ›ø>ÏŒ;:ìÆŒz‡»»ô{{*U `ä;#™:iýzŒUVS§n¼½½ôè@F�MÏ»{^ö¸W«JÕ*<2àQ¦~0•Óf�gÊå®^=yqØ‹|ùÙ—|üá «ê… c8£ÿ7Ú³P­z5^þ³fž›Yéõÿ½Æ„÷&òư×�ÁÝ:uä£ß4Ê´lÓŠßþ�à=ÁüþçÜk8‡Êäåæ±|É2zõ¹›ÿ½óï½ó¯½ü*`¹ ¿£Kgã}³³³ãýIï3vÔXžü`Y7¦Ï½÷ðÄSƒŒã¶jÓš¹¿þξ={ùꇯ/úú›6ldÓ†�ÆqÊ”-CÏ»{ñÄ3OXuO»{{{>˜:‘q£ÇñÌ KââàèHß~}ø”¥Û�¯ŸïMzŸIã'ñÀ=÷ŸyΗw'¾k¼VëÖ­˜÷û<öî æ—ù¿2jÌ(Þó.Ã_nœ—n]yqøKWW×;»°fåjzÜÝãªß‘âfg¾^S‹ˆœáý‹åNqJÿÿö‚XgÏcï=e‹;9OLt4Q‘ÑØ;ØS½zµ«š~Ô”—ÇÉ'quu¥|…òEî6çd熳³3g çåæ‘—o2úî›òòHOÏÀÛÇÛx|¹ã^‹ä¤$¢£cð/WË@üôôtÜÜÜ/¸â{AA¸»¹InnU«Weé¢%ü1÷¾ÿù«ò ñ ÄÅÅáïïwÁÁý©©©¸8»àâêrÝ޻ؘXð-ëKé2¥/X&""‚ÔÔTÊ—/oL@PXJJ n®nÆ,^¶IFz:å*”¿`—0S~>¡'NâììL¹ŠŠŒË¸P}ÆÆÄ��˜ˆŸ¯ßUµ˜­_³Žqo�eÞß\—Áùr}5ZÜ:¿ƒÿõó�›�Z@Dä?ÇÏß?ÿk›aÊÑÉé’]V\\]¨^³†ÕsNÎN8áduŒ³k0\éq¯…OÉ’EƒK�…0›¡ÿ�qïý÷+xgfeñçü¿hظQ‘ò¥Ë”¾h\UKÁ•òõó½ìjÝ*T Â%Ö|¹PRb —[HÓÑÁ¡Èg§° Õ§¯Ÿ_‘Õà/Çl63ÿ÷ù´hÙRɇˆü')¹E88Øóâ+/2僩ìÙµoo†ÄÞÞ�ÇŸx¼¸Ã“ë`ÉÂÅÌš9‹ÔÔ4>›ùiq‡#"rM”€ˆˆÜBzöîE“fMÚDFF½úô¢YËF÷1ùokÔ´   i³¦ÆZ!""ÿ5J@DDn1þåÊÑ»oŸâCn€€€Š<úpq‡!"ò¯h^±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6ãX܈Üì-Š+îDDDDnj‘«Öñ¯QtükTq‡!""ÿAj¹ˆ”þ¾Å‚ÈM«Ï_–ÿë߉ˆˆ\-µ€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6ãX܈ܬ¼‰-îDn^÷MôïDäRRúûw"7%µ€ˆˆˆˆˆˆÍ¨Dä2öÞS¶¸C‘ÿ�F‹âŠ;‘›šZ@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3ŽÅ€ˆüwå›L?~‚ØØXüýü¨V£:ööWw_câ»ÈÉɱzÎ×Ï�æ-›Ñ¦]Ûâ>ÅbuüØq~ünö%Ë”ðöæõÿ½^Ü¡^Óy¸{xP¥JezßÛOOO›Çµ#p‹ÿZdõœ‹«+Õ«W£Gï^øøxÏÇÅÆ±ß~ê7¨�¯Ÿ¯í+QD䢹&a§ÃøÈ@žô4“ߟÄà�ƒ:øy¢"£®ê8Öo䨱ãÆã³™�;vðæ«oòÙÇŸ]÷¸ÿY¾‚Y_κâò±11Œ5†ø¸øW™W(<,‚µ«×b2™Š;ÃÑÃG;j y¹y-“”˜ÄÚÕkIIIµz>&:š¯¾üš��¿-ø�òÊv:Œ7‡�`ü¸÷ùüë/®êX:v`èËC­žûô£O™÷Û\ êOÉR¥®[ÜÇ�ž x÷† rEåÓÒÒY»z-Ï<÷Ì�¯ÔóT¯Q�w'¾g<þcÞ|fL;ÂÈ1£Š¥ÅàBX»z-#ÇŽÂ §K–òü3ÔoØÀê¹ûö3ô™¡ü½àožxúÉëWTT4Ö®‡wß¾lÙ7ß~˪µ#9)‰ÿ½öïŒ|‡ŸçþŒ»‡uë×ã݉ïQ¿AýZŸñññ¬]½–wÞ}nèk‰ˆ% "rÕrsrÙ¿w/¯¼>œòÊP)€AƒñÁû�œœbuAw-Zµmż߿c$ Ù9Ùü³tG�ÃÅÅ…6íÚТU «ýÒÓÓY¶d¡'OáááÉ]:QïÌEãŽÀ;zŒ¤¤$þ¹�®Ý»âééILt4+–­ .6ž²eËгwO|ýüˆŽŠbýÚõ¬Yµ†Vm[Q·^=–,\L›vm9~ì›6læåá/ãäìÄ©“§X»z ñ x{ûоc;ãµMyy,]¼Œ®Ý»²{ç.vî ÂÅÙ…;ﺓÚujÿ«º²ÔË?œ<~;;;ª×¬Æ�=ºáêâ @è©S„ž ¥eÛÖÌûåwüÊùÓ£Wòó XõÏJì;€·�7}ïëË©“'ñðô¤^ýzÆñ·oÛNà–í˜ÍаIC:vî„£ƒ'Oœdû–@–.\B³Í©RµÊUÅ^¿a¼½½‰ˆˆ4ž+((`Ýšuï  qãFÜѵ çíMyyü³â…ÆÑÁ‘­[ж}[ììì8|è0{‚ö`*Èg០iѪ…ñ9½>%K2jìH=:ˆËþ¡ßƒýpsw#-5 7w7ÀÒÂP¾Bæÿ>ŸvÚÒ¸iLùù¬]µšý{÷ãìâB›¶­iÞÒú3šœœÂÒE‹‰ŒˆÂׯ,½zßMYß²?vœÛ,õ¹èïÅ4kÑ”ÊUª�Ÿ_ÀºÕkØ{¦E¦IÓ&ÜÑ壻cXX8G¦C§ŽÌûm.Þ>Þ4nÚ„ÝA»é}ÏÝ88žû©Þ½‡¤¤$:wíò¯>s""ÿ†¹jéôîÛ‡æ-š[=ïàh¹c[pº�n ÄÛÛ›ªÕªZ^3=�—ž{‰¨¨hš·lNjJ*sý�þðüKÏ–~ú/ y�œì,š4kÆÑCGùuÎ/¼üêË<ôèÃìÞÇéÐÓ¤§§³nÍZÚ´kMxX8/?ÿ•*U¦jµª¬X¶‚_æüÊ÷s¾'!>žÀ3„Û·R¦lêÖ«ÇGÓ>¢ß¡ÌŸ;??_òÍ/°oç^Þö:5jÕ¤r•ÊÙÉœÙ?ñîÄwéܵ Ù99L›4•íÛ¶³oO0 7â䉓ÌýõwÞ÷6Ýzt¿¦zÊËÍãåg_"*2ŠVmZ‘›—Ç’…‹øgÅJf|>€=»öðׂ¿ù{Áßì߀Gú?‚Ùlæ�ÿ�&00�–­ZròÄIýµ�²eËÒ¬Es#ùhÚGüýÇ_4oÑggþþë/š4mÊ”é“  '8Ø’$¬_·?ÿ«N@’“SHOO# REËg§ €ÑoŽbÛÖí´8sá¾ðÏ…¬XöLû{{{rsryåÅW8~ä-Zµ +;‹?æÍçî{zó¿Ñÿã䉓 Á\�Ϻ5k©T¹ÒU% •«T¡b@Eví¢ßƒýHˆ�gÚ¤©4lÜ///V,û‡´´4Ž:LVf&uë×!ßdâ�WÞ dßZ¶iIFF&sù�z�a¯ 2"’Ÿ}G6nÈŠe»ùùÇ_øúû¯?ÆÞ`K’±~ízüËùQ1 #ßx‹�;vÿÞþ¹�¶íÚ2~òxìíí9xà?Ìú�Õ+W¸“{î¿—¦-š1mÒTüÊùѺMkã¼&O˜L›¶m•€ˆH±R""W­d©R¼ñÖ«çRSS™óÃ7iL©ÒW×ejÓÆMDGYÆŽ˜Ívš„¸x&NýÀ¸‹?çÇŸ‰‹à›¿¡R•Êü:çf~6“î=ºS½fufÍœEnn.ßþô=e}Ë–®\_~6“®Ýîä©!ƒÉÎÎ!x÷¦úßó•*UfÖìYØÛÛ“™•Å ‡gëæ-ÜÿÐŒ9‚§<ÉÈ·ß" r%#æUÿ¬ä»Ÿ¾3¤¿þø›M1ãóØÙÙðêKÃY»j­ÕÅÞá�C|ÿólJ—)M¾ÉÄ;o½ÃGS?¢CçŽÆ¹^�ý{÷qøÐafÿú£ËÆuý¿Ñ$'%ãSÒ€“ÇOP§ÏÝ,™>G''Ö¬\ÍæM›™ñù š4o À�ßÍæ›¯¾¡Ù™ Ý];ƒøsÞ&Lž@ÇÎ�8‹ϾÄâ…‹¹÷þûprräÍWßdòôÉ—�?hg±±qÆã¤ÄD–,\Jíºuyà¡,qmÜÌäé“iÛ¾Û·ncÄð¬^¹Šî=îbÁü?8r�/¾�I�ºuX²p1“'Læ®ÝéywO¦OúÐxŸ¯…¹rÄDE_tûÆuxqøK<ôÈCÌûm.öïgæw_Q½Fu–/]ÎÄw'й[g5nÄg}Š——3¿�‰»‡¦¼<žü?~÷ï¼÷NŽŒzc$Ó>žŠ£“Ë—.gÛ–­L�1ÍH$6oÜÌÈ7ÞbÝêµtí~'‘‘‘Ô©W‡¿—/ÂÅÕ€Úuj³nõ:c¿ƒ!!„‡…s×ûw]s�ˆˆ\„."ÿŠÙlfõ?«<à)òróxç½wþÕñìì T©Òädçðëœ_ÉÌÊ`÷Î]téÞÕH>|ø!ÜÜÝÙX.˜ûôíc$ƒ"ßd2ºóœÏÞÞžÄÄBöÀÝÍ�ù‹þàþ3ÄÓçÞ{Œ ~€‘ï¼ÅÔéS�ä#';³²²²­öë{_J—) €ƒ£#ƒŸLZZ‡¾¦újظ!KV-µŠ%+ÛRgYgê,- C_Š£“eœÆöm�4lÔÐH>êÿNÎçÆqlÛ²�ê5kÉ@ÝzõhÕº%Û·l¿êX¿™ù cG�1þûxÚÇœ8q‚~wË{´›ÚujÉ@ë¶m¨S·A;v�yŸwÓ¦][#ùèÕçn|}} ÚuMõx!...ä]bÀ¿9|øA«úº£sg#ùèywOüüý ÜHAA���ô{ð~ã|�œöê0š·lvÁר´›zõëYµb´ïØžµj²«Ð¹ðÜ‹ÏÉ@—n]ؼa£1 }å²UT©ZŪÞDDŠƒZ@DäšEDD0yü$ì?Àý>ÀSÏÆÝÍíª�s¡Aè¡§Nñô §ùåÇŸyæ¹gˆŒŒ¢C§Veœœ�(W¾‘áQ˜òòˆ�‹' r€Uooo¼}¼/:;×ãO<ÎÁ�ƒ¼0äÊ•/GÓfÍèÒ½‹Õß…”+ïoõØÁÁ‘åK—³{×n"ÂÂ9qâÍš[wS«Z­šÕã*U«`ooOTD$�›4¾êºstr"ìðV._Å©S' =Jjjj‘ržž”(QÂxAÅJÖuåæêŠoÙsSÌF„‡Σ÷?bU.))™*Uª\u¬_~ó¥Õ ôìœl¾þìkÆ�Oízu ¨HTd$çÅP©re"#-ãD¢"£hÛÞzŠf{{{*V *âêfa»”ؘJ•,yÑíþåÊ çÙú:r°H}%&&���@|\<¹9¹EίI³&4iÖ䂯q±ú¨\¥2‘…ÆÍ8:8PÖ×zzà.wvå«Ï¿bOÐnš4oÊêU«yø±‡¯[ýˆˆ\+% "rM:Ì+/¼B�Õ™óûÏ”+_r•*Ôª]›C!‡pww#û¼õBr²³qusÁÁÑ'g'² Ýõ?+7;W7— ¾N¹òåøöÇo9|è0�[·³eóVF¼òO>óƒ‡ ¾h|ŽŽÖ_Ÿo¾ö&¡§Nqßý÷Ñ«wOªT­Êw³¾#1!Ñ:–óÎ!''‡‚‚œ]\¸kW¯eÜè±ôèÕƒ»ï¹›ªU«’››ËóO?oUÎÉÑzF%ggç"ë¯äæå»¸¸P½VM^yý•"å\®1ÞÂ\]\4x óçÎãðÁCTÄÍÍ�ìì¢qeçd]¼ÜÝÜÈÉÉ.Z&;W·«ïÆv!™„††òHÿG/ZÆé¼Ï€‹‹ �:6â�G,R¶D‰8;;[ê87—+åææ~áÏ}V¶U—7{G‡"kð”+_Ž:uê°nÍ: òIIN¦û5Ž5¹žÔKD®ZAAãF�¥]û¶|üå'×=ùËàêè¨(cÑ·€€‚w[w£Š�‰!:2ŠJ•+aggGÅ€ŠìÝc½NჇÈÌÊ¢RåÊ|�ß~þ•Í7Q§n ~‚™ßΤϽ÷°nõÚ+Ž5&:š];ƒxã­<ñô“´jÓ_?_â �w8+ø¼øvî òE⻜e‹—Ѳu+F�M÷wQ£VM¯`M�*U«p`ß«õ&"#"IˆO0T :*ŠjÕªQ»Nmã¿àÝ{Œ1;ÿÖÙé„SR’�× 9pÀjm޼ܡ^ýzT¯Y�kQ²TI"#"HNN ä@_Ìø°tß)|‘^X¿‡ï'..ŽO§Ï 9)‰ÈˆH&M˜„“³“qÎ=îîIZZS&N&.6ެìl~ÿå7¾üô ¼¼¼¬êç䉓Æû~5ìpp°'1Þ’4õìÝ“””T¦~0…ø¸xâãâ™úÁ’’’éÕ§—å}îÛ›ÇOðŧ_�œ”LtTÆM{{cP¶��y¦\­êÿB¶lÜd|ö–,\̈WÞà�yóòü3T¬XñŠÏåž{û°'h7ßÏúŽÔÔTRRRøøÃÌŸ;Ÿ²eËŸÑ¿ÏgÝšµdçdr „9³çS1Ûa]Ÿ½z÷$)!‘i“>$!ÞÒ�kÊÄ)¤¥¥]QrÔåÎ.¤$'³fåjzÜ}åÉ”ˆÈ�¤.X"rÕÂBOðóì9ÜÞ¾c{\]\Ù¼�½gÖK¸”M7±iã&㱫‹+5j×dêŒiÆ€Ù;ºv湟ç‡o¿çÛ¯¿ B… Lš6ɸ‹Þï�û‰‰Šå“�>ᣩÓ¨^³:“¦M2ÖBhÚ¼ ó~ŸK¿Þ÷ñÓï?ñ°xûÍÑ<5àIãõ7m°×,Ó¦–¯XžŠýæ(r Ï}¶Hünîî¼ðÊK|ùé—ü:çòM&Z·mÃÿÞþÓ&}ÈÛÿÍØ ãxø±Gøò³™|ðþÔ©[‡÷>x�k5ð©�¼ùÊîíyÎ.Δ(Q‚_y‰ÙßþÀð‡óë‚ß.¸_ÅŠyëí·˜>e:Îÿ{ ˆ‹“3Þ>–±"å+”çýÞgÊÄ)ÎÆu¹÷×§dI|.2ÃÖ…êÓÑÉé²õq!!û²ÿÏbóz¹% ""·¡;{tcÉ¢¥<Ùÿ 5mLzz:Á»öðè€Ç¨P¡Bq‡'×Á C^àð¡C4oÙÜX\RDäf DDÄ\œ]xîÅç©ps\Ü»º¸òÙWŸ±3p¡§BqwwçÅa/^×–)^>ü™™™të©©wE俢DDÄœœ�0h@q‡aÅÞÞžVmZÓê2‹.ÊÓÙÁDDn6š†WDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ8w"7»F‹âŠ;‘[†Z@DDäªuükÿUÜaˆˆÈ�Z@D."¥¿oq‡ rÓêó—åÿúw"""WK- """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q"""RH~~>‡ŽÇdÊ/îPn*ÇOž&=#³¸Ã‘[€¹åÅÅ'²-p÷•ÍÈÌâÉçG�’švCb �ˆbwð�+*{èÈqŽ?e«jº¤7ßþ€à}‹; ¹(‘[^È¡£LÿìÛ‭�»ùî§yWTö¯Å+Yºbmq‡,"r]9w"""É)©:r‚ºµ«³låzrrrhÙ¼1õj×0Êäåå±as 'CÃñó-CÇv-ññ.al?tä8Ûwî¡  €VÍS¿n-âÙwà0™YY¬^¿…;Ú·ÆÑÑá²ñäääðç¢HHL¢ní´oÓÜØV`6³-p7‡ŽÇÇÛ‹Ö-›R¡œŸ±=;'‡ ›9I9¿²tjß //OÂ#¢8rì$‰Iɬߴ�;:´¦Àlfãæ@Ž;‰§§Ý:·§l™R:rœˆ¨hœ�œÚ³ŸæM°qËš7iÀÚ�Ûððp§s‡Ö¤¦¥³jí&bb(]ʇ®w´¥LéRìÚ³ŸJå9vâ4{÷¤ByîìÜW— žsžÉÄšu[8u:‚2¥KrW×xyyZ•Y¿i;!‡ŽQ¡œ=ºuÂÅŹ¸?:"ò£¹)œ‹dâ´ÏyúÅ·Øwà0‡�žàÙ—G²pé*À26ãùáïðŬ9$&¥ðç¢ðôpâøgÍF†‡ˆ¨""cx~ø;¬Û´�¤äTŽ?Evv;‚öRPPpEñŒxg›·ÃÛï}È”�¾2¶M˜òï~0ƒ¸ø6lÙAÿÁ¯pààÀr?䥑|óÃï$%§ðûŸKx|Èk$§¤—@hX©iéì:Ó ëýIŸ0ãËÈÈÈdëö]gÎ)‰ÐÓÄÅ'ÏÁÃÇ,¯;õs>™9›YßÿJll#®¤äæÏùGGƒ‚)[¶ _|ôööö˜Íôyði>N›–MŒ:�óÍG¸¹¹’‘™IÿÁÃùsÑ?<öà=VçËî½!,ž÷-eJ—$''—7ß™DttU*W ByÆ¿ó:õëÖäýÉŸ÷GFDþƒÔ"""7•¾ww+ô÷�¤¥gp*4œ½û]™ìííéÑ­Á,£+VðgñòÕüþÇbN‡G2àá{øh¿kŽãî»:7oÒ€Jå9pð(û¦níêFòq¶ìþ�ØÍï?D—NmqwwÀÑÑ�»ºvdïþCE^ÃÉÑ‘rþ¾|òålV¯ÛLJJƼN³& .×Ù.d­š7fÖ§ÉÊÎæðÑüñ×2ÒÒ30™òŒòÝ»vÀÍÍwwºunÁX|¼½ðòò䃿`ÓÖ˜òM̘2ÆH>:¶keü]¦L)Í&"×D ˆˆˆÜ4\\œ�‹e'''<=ÜINN%%5�žÖã|¼½HI±t7öüô¨/+VoäÑ'‡ñàÀ¯xæ« ñòô°z\ÒÛ›ôŒLRRÓð:/oo/rsóÈÌÌ:³Ýã¼í%HNI½àë|ùÑûÔ«Sƒ/¾ù™»xŠ—ßGtLÜEã*QÂËø;6.�^Ã}�=Ïç_ÿDbR nÖuäãm}%½IJJ)r\ww¾ùôJ—.ɤé_Ñã¾'xkìÒÒÒ�2žî×\Ÿ""g)‘›FNN.±q Æãø„$RÓÒñ÷+‹¿oN‡GZ• ‹Ä߯,û¦W÷Î|÷ÅdVü5›Žm[ðþ”kï"aü�g2q*,œ åýð÷+KXDf³ÙØ~:,//O<=Üñ÷-KØyqž‹ Ü™8 KÏÈäTh8Ã_xŠ?æ|Áï³?#'7—oœ{E1~3ûw\]œY:ÿ[>™:–ç?†£ƒõûóc9Ž¿ÑX“INIeÔë/°xÞ7|÷ÅNž cîŸK¯Ï›+"r†¹©|öÕ�äåå‘““ËGŸ}K­U©T©Ý»v`Ëö ƒ‚K‚0ÿ¯¥ÜÕµ#ñ=³fÿF�ÙŒ—§¥K—ÄÕõ\kJVv6…’†Ëùúû_INI%??Ÿ™ßüŒ£ƒmZ6¥}›æ$&%3ÿ¯e€eö®oœK÷.¸«kÖlØÆî½!?Ê_‹WÒýLœ`Yk ;;›ao¾Ëæm;ð÷+ƒ»›+®®çf©ÊÈʺhŒ9¹¹”(á…““f³™¿—¬$>!‰ìì\£Ì²•ë 93ˆ}çî}¬\³Ñˆu÷Þ~ÿc1‘QÑ þ¶±îH…ò~8:9â^¨EJDäzÐ t¹ixz¸ãååA·¾)(0Sί,¼û&övvÔ«S“gŸzŒ×GMÄÃÝ�Ô´túÝs÷öîÀËÏ=Á;¦³rÍ&ìííqrrdÌ[èV¥™™Ùt½»?Kþø÷Kw%rtt m«fÜóÐ38::âêêÂ{o¿Š‹‹3..ÎŒñ"“¦Ïäë~#33‹¶­š2ôéþ4kÒ€��Þǰ7ÆáááNjZ:�<Ї^gƔԪQ•O¿šMŸ‡žfñ¼oyæ‰G9v*>>%HKË fõ*<õøƒÔ«SƒI~ILL<3¦Œ)çcÞëo�§_ÿç1™L´kÝœûûöä½ÉŸP½Z%ºtjË›o@vv™YÙ<9àcJáí;vóÇÂ<ò@Ô«Í=wwãÉçGPº”IÉ©´nјû¸?"r‹±3›¯âv�Èðþ%€”þ¾ÅŠˆÜ }~€Ås¿¹nÇÜ»ÿ¯�žÈÊ¿$--�”Ô4*”÷ÇÎÎΪ\žÉDxDþ¾e­Æ‹œÝƒƒƒ=åü}q(Ô)Ïd”g*²Ï¥$%§��˜LåJpr´¾gWPP@XD>Þ%ð.4.Ãx½¼<Â#£ñ÷+‹›«õkæææQ`.0ÖãHÏÈ$:&o/c �³²²³q°wÀÙÙé‚1æåå�¿ocà{vN®.. }õºwíÀ}½»…o™Ò—=ÿä”Tâ’(Sª$>>%�Û—~ÏåFQ ˆˆˆÜt¼¼<‹,€w–“££Õ Tço«\©ÂE·99:rôø)òó/>{S™Ò¥(Sº$%}¼)yÞ î³ìíí©Pá¢Çqrrºhœç'žîÔ¨Vù‚eÏO^.ô:ÕªX¿Îù ^.ÖÂ|¼KX-î("r½)‘ÛÊž½!dgç\t{“ÆõŒDDD®?% ""rS¨_·& ~þò†¿ÎCýî.îS<¹‡“€IDATµ™'ŒÂÉI?õ"rsÑ·’ÈEœíû*"pßDàFý;É(î³¹.4vBäÂ4 ¯ˆˆˆˆˆØŒZ@D.cï=eÿýADDä¶ÑhQÜ¿?ˆÈ-L- """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDDDDlF ˆˆˆˆˆˆØŒ±% """""b3J@DDDDDÄf”€ˆˆˆˆˆˆÍ(›Q""""""6£DDDþÏÞ]ÇWUÿqÝõÆŠ ØèT ¥KØè›F@EET°PºEEŒŸ�Ò±ÑÝÝcÉXwßûûcpõºÑpG¼Ÿ�½'¾çsνÛîû~¿ßsEDD¬FDDDDDD¬FDDDDDD¬Æ®  )hiééœ;}†Ø¸8|}|ð-Wö†ÛøëÏÅœ=s–7ß~«@Ïeýšu?v‚W^€ £'й['jÕ©�ïöK/a÷ÎÝËœœ�ñ-ëK§®�ððð¸mµM3�Ž�;Rû±:z�DD¤`©DDh‡â™.OñÚ‹¯2yì$z=Û‹÷†½KVfÖ µsâøI¶nÞv[kûû¯¥üðíœÚçܹ víÜe~¼1`®¸ýɧغe«Å²˜èæ|û=½žîIpÐùÛv>›7q[¯‘ˆˆÜ{Ô"",“ÉÄ”‰S©P¹"“¦N¤�«+ûvïeØÃY0o>½úõ.ÐúN�<Åù à;~wwwÆNg±ìÌé3¼ô‹,œ¿�áï½] ×ADDî/ "òÀŠºx‘à ó¼þæ ¹ºP§î£Ô¬U“CßRÛûvïÅÆÎ–2eʰtñâââ¨X©"í;vÀÞÁÈ @Û¶lãÀ¾defQµFUüÛ´ÂÎÖ–Û¶sîì9bbbY²h ­ÚµÆÅÙ™Ô”V._Iðùlml¨Z£*~þ~ØÙÛßÖkS±RE*U®ÌÙ3çÌËÎ�=Gàúb¢cððð¤Ió&Ôx¨†Å~gN�fí굤¥¥S¡by‹óý¯è¨h¶nÞÊC× båJ—ö?ÆÀ ÄÆÄàéY˜¦�7¥Zõjû>xˆ€u�`2á×Ú777NŸ>ƒ_+?ó6ÁAçY½j ‰ ‰øø–¡m‡v¸»»›×§¦¥±jÙJÎ�ÇÙÅ™&MópÍGnë5‘üi–ˆ<°�œ;i=ü�y™Ñh$>.÷[h–-]΂_æóÚ‹¯JRR23>žÉÄqÍÛ|úɧŒ|ÿCB‚CˆŽŽæã)Ó?r,ö$<4œ„¸x×�‘–FZj*ƒú¿È�ßÿDRbá|4é#Æ�Û¯MVf‘.àéé À®�»x¡W?¶oÙAFz»wîfð€—Ù´a“yŸõkÖ1°ïöîÚK||<_}þ¯½ø*ÙYy‡³EGE3ô•×Ù¸__¶oÝÆ€>/°sûN2Ò3ع}/õ‘mÿ"¶dÑ_¼úâ«=r” .0ü�·™5cs¾ùg¨Úæ�›èÛ³[6n&1!�y?Ï£Ï~„„„�š’ ¾ùù‡ŸHNJæð�C¼2èÖ¯]Û¯£ˆˆä¥y`yxxÐÒ¿%G�ecÀFöïÝGfF&}ô½åö7oÚÌÌÙ3y´îc”+_–¿ÿ‘œ#ÙYYüµè/†¾õ:Ýž|€åK—ñÅg_�–šÊ‹ƒ_$%9™óAÁLŸ5€ÀõD„†1ÑB¼Jx¹óD¦Oý„¬Ì¬+ö4\KVf¦ÅDô¤¤$þ^¼”˜èÚwjÀâßQç±G͵¼>xk×Óìñf¤¥¦2mÊÇtêÚ…aï $8„>Ï÷!0`­Ú´2ïw9|”*]š‰S'âàèÀŸ¿-¢~Ã|4ã# ·‡èÕ_%`] �š4&>>�Y3gñ\Ïç|„”ädzõíiÞÆÇׇ·ßN±bÅÌË¢£sÃGjjªEø3aôŽ‘‘{Ž—ŽqàR8ìÕ¯—y›jÕ«Q·^=.\ˆàØ¡£$$$Ðo@?llr;ù]]]yò™'ùtú§æko4Ù±m­Ú´ÂÎÞžIÓ&Ýòó-""×GDDðkí�_kÒÒÓyç�·ùÞ‡ÌýmÞ-µY²d ‹ÇƒÁüÿŽN޼õö›|>ck׬£ÆCÕ©[¯.»tÄ«D‰|Ûsptàȡìäüùóœ;sŽÔÔ”[>÷¢ÅŠòÓüŸÍ�íìíqvr²ØÆÎΞ•ËW°oï~ÂBB9{ö,õë× <4 G‡<µÿ7P}ÿ¿ïxø‘G aÓ†�øÿ«gÄÆÎ–/gÿ¾æc Š/@XXž… ãææfѦoY_s Å`0ðúà!Û¤¥¥“•™ERR2-üZ°iãf&�›Äç3?§fíZ4nÚˆ¶íÛÝtˆ‘ë§""¬3§ÏpâØq‹7ÉÎNN´iךi“§‘œœŒë¥Þ†›qùø+éÜ­ þm[³sÛvöîÞÇ¢?1Þ¾ýñ[||}òl¿dÑfLû„�;ѵ[ÊU(OÔÅ‹¼ýÆ­Ý¥ÊÆÆ&Ï›ú3™L :Œð°0º>Ñ•�ÚS®|y¾ùò$'%ààà@Nv69F#v¶¶Wl˯U+>ó£FŒbÖÌY4hÜWWWL&o¾ú&Q/Òµ{W:véHùòå™ýÙlr²sr�áè@zzzž632ÿé­rrvÂ`00jühló©ÃÕµ¶vvŒ™0†—_}‰m›·±kçn>ùh:+–­àó¯g[E¹ý4 ]DX".0eÂ.F^´X�ƒ£NŽŽwìØ¡¡¡|=û+ÒSÓhá×’·Þy‹ù‹b4æ°cûö|÷Yþ÷rZ¶òçí÷߯¯µ?*V &&æŽ_§ððpìÛÏ;#Þ¡Ï }©× >ŽŠsñb”yß²¾ää9vø¨Å¾ï {—Ÿ¾ÿÑü¸nýǰ±±aè[CIOÏàëÙ_|þ¾¾�FLF#U«U5ÿ‹�‹cÿÞ}ØÙÛ³)p# æÎ§¤·7O<Ý�IÓ&1ṙ:pˆ˜è;=EDt "òÀª]§EŠaêÄ©„„„’š’Âæ�›ømþo´jÝÚ|kÛ_~ø™‰c&ÜÖc;:8²`î|¾ÿæ{’““ÉHÏ`ãú@2Ò3(_¾|îF1Ñ1ÄDÇ��“C‘Â… :OJr2F£‘ý{÷óÝ×ß�Édº#×ÉÃÃ[[Ž>ŠÑh$-=�¹?þ¾={‰‰Ž!#=ƒêÕ b¥ŠLÿèΜ:MFzý¹˜m[¶Q³v­tlÕ�£GŽÐ»oo<.Ý.Ø`00ñ£‰8»¸0¨ï@ü›ù1ìõ·háïGßýèÜ­3Mš7cäû#ñkêG×ö]9rø0ã&�ÃÖVEDî4ƒéN}d&,�y¹C&zxt)·å<v.^ХȖ“cäbd$©))”ò)“gö�”–žNDX8&“‰ReJç9vZz:9ÙÙæ¹(9ÙÙ�ÇÖÖß²¾ØØØ�“�MFF.… ÝÑZ“’’ §X±b-VÈýN GGGlíþ™RJfz:¥}Ëàäxc×211‘ˆðŠ/N‘¢E,Ža2HKMÅÙÅ…ógƒpp°Ç§¬/³?�MÔÅ‹Œ™8Ö¢­ˆð’’’(UºT¾syâã∌¼ˆ«›+%Kz+|ÈmSsiîðÄûåïà½~r÷Ñ$tyàÙÚÚà]Ê»@ŽíìäD…Š®ºÞ¢V;;*Vª˜g™‹Ý�ÿuîææFÕjU-–åz||ÊÜô1ÜÝÝ-¾±üßLj‰Ž¡{§'øp쇴nÛÈNµjÅJúæó½-Þ¥¼ñæÊÏ«gáÂx.|ǯ›ˆˆXR‘{BÑbEéݯ“ÇOaå²U88Øs`ßÊø”¡c—N]žˆˆ\'¹g |y -ý[rèàArŒ9<ñÔ(“É„oY_Ú¶oK»Ž°µ½óŸ_}ûÕ·”ñ-C»ín{Û9ÙÙŒ5îšÛ½ùö[xö¼ãç*"ò QˆˆÜ³bcbyóµ7ùcÁŸ7ÝÆ©'Y²è/Ö°âïw¬Ö£GŽqäÐÑ›ÞóÆMÌüx¦ùqdäE×Þ‘Z332yøûL7 wwwúôïK¿ý(Z¬Ó&OcøÐ·HMK»c×ê²=»÷pæô™;~€ä¤dÖc•㉈<ÈÔ""÷$“ÉÄı‰�‹»¥vV¯Zƒ««+uë×cýÚu 2›»ï³™óAÁìØºÝ*ÇúrÖ—ìܱ“)ŸL¡aãFæåÝž|‚N]:òî°w™5ý3ÞýàÝ‚¾,7ÍÖÎŽ±“þé9uâ$»úì¢GŸž4nÚ¸ Ë¹¯)€ˆÈ=iἄ‡†Q±rÅ›nÃh4²nõZZ¶ò£QÓF®àÀÞýÔ©û¨y›=»vãääDá"EX¾t9‰‰‰T«^•¶íÛbk—û+tó¦Íx—,‰ÁƆÕ+V“––J­Úµháß2ß0³fÕjÊø”±6––šÊšUkiШ>%J–´ØþÀþ;zŒ”Ô–,ZBÓæMÍë²³²Xú×ßœ9}š¢E‹Ò±K'¼Jxý³>'‡Àµë9tðŽŽ4lÔ€ÇêÕ½â5‰ºÅâE‹y¾×óáã²ÇêÕ¥WŸ^üøýôé߇¸ØX‚‚‚èЩ£ÅvG&ø|0í;u 8è<«W­!1!ß2´íÐwww²2³X±lþ­ýعc‡bÈ[¯ç9vvVë×­çÄÑä�ø”-C»öí(äêÊÙ3g9zø»tÂ`0˜÷Ùµs4mÖ”›±jÅ*jשEDøÖ­gÐàquu%>>�•Ë–J±bÅhݾ ¥K—¶Ø÷bäEV._ÁÅÈ(¼KyÓ¶}[Š/vÓ¯W‘ûÅÝ÷1ŸˆÈ5œÃßx›M6bccË¢?óò€—HINrç9¼=tMüˆè¨hN�8Å[CÞâ³O>»â5Ù¾u9ÙÙtéÖùŠÛty¢+F£‰­[¶Ï”ñS8sê´Å63¦Í`Û–Ü›Í7Ñ·g?¶lÜLbBó~žGÿžý  -=��§LãÇïbܨqìßw Ï1M&ï¿=‚éS>!**šøøx¾ÿú{^ô ÙYYdeeñѤ�8väŸanÙ99L=�“ÇOÞôkä‹Ï¾`á¼… úÛ¶l';+‹�óÁô}¾¿Ïÿ�ääÖ­YOßçû°mËVó~G¡÷s½Yù÷JRRRX²è/ú<×›£‡�Üt-""÷ õ€ˆÈ=%5-�±Ž¡WÿÞÔxèæ'ž¬Y¹–’ÞÞÔ¬UƒÁ@³Ç›¸.€7‡¿��½½y»=»öðöûoÓ¹[ ·ÇcÄð÷Y½|%:ç~ò¿}ë6¦NŸJ£&¹Ãwÿ±ˆéM§]ÇvÔoØÀâ¸~þ-ùþHb¢c(Z¬hn-«ÖШI#<<<òÔùôsÏ�™™Åß‹—2}Ö Î�Çh4R¹je^}ýU:véÈ ¾9røõ6àÏßÿäð¡Ã|õý×T¬”ÛS´rùJ&��H‹V-¨Y«fžcŸÁÍÍ�’ÞÞW¼nEŠ¡h±¢œ?L×n]ñðð `} +W ä|0'OœdÐàA¤g¤3uÂTÚwlÏð÷†cccCrr2¯z…ÏgÌbêô©æv·nÙÊü?~ÍÓÆŽmÛùøÓ�Í×óرc¼ÔïEÎ�=GÕjU)]º4�ë7Pãá‡Ø»k7ñqq´ißæ–^'«V¬âËo¿¢JÕ*Œ9–’%J0ãËOqqv&'ÇÈ„1ã™:q*¿ÿõ;¶vvL?…Zµk1飉ØÙÛ“™‘É;o½Ã´ÉÓ˜3÷‡[ªGDä^§¹§|öɧ-V”½{ÝR;™™lÜ@›vmÌCvüZµ$))‰íÛ,çZx•(AÇ.�Ì�›6kJ�‡b÷ÎÝæe5~È> ·— DÉ’ìÞ¹'ϱë7l€£“6þ={i×¾í ŸÇÓÏ>eþÿJ•+�ž–Àö­;x¼E søhס%J–dç¶�ù_—Ì ®y\'''ÒRS±³·çq¿¬ 0¯[»f-^^^Ô­_Ÿc‡Ž’��@¿ýÌÃÑ\]]yò™'Ù½k9ÙÙç’_øð.åͲµË©× ¾yYFjîy^žß²µ�ë1™L¬Y¹†‡y˜2eÊÜðuý·¶íÛšÃGFzûöìãÙ^Ïár©·ÊÖÖ†^}zË©S§ˆ�à|P½ûõ6YGžïõgNŸábäÅ[ªGDä^§¹glXȦÀ�Ì™ûÃ-ßvËæ-¤$'³uËVŽÉ“}éÍðÚÕëhÚ¼™yÛòåËå™ËQ¡bΞ9k~\®|9‹õ666”+_Žˆˆˆ<Çvtr¤I³&òÄÓÝY·f-®®n4j’wÎÅÕ Š/þ¯Ç–ëÃBC9~ôÏuÖbyll 11ùßí©páÂÄÇ'�™‘yÅ ’“c$6:†"Es{oZµiÅ’Eqæô*VªÈÚÕëh×±=¶¶6„††b0x}ð‹6ÒÒÒÉÊÌ"))›KÏ¥w©+÷ºØÙÚr1ò"?Ïù™³gÎr>è|„¤¤$žêò¤ÅòC±zå*&L™@ó–�_W[kV®ÁËË ÿÖþËM&غq ©iiæO¸3²2ó쟞žŽ£“£ùqVfVžm22ÒóR¹½-#ßû�¸ØXÖ¬\ƒ_‹a_×Ã`0\õŽ]ŽŽŽÔlV“'ÿÕKrÙå àÿU³vMr²³Ù¹c‡Eû·}{ö’š–FÍÚ5Íûx•ðbCÀrrr ¡]§ÜïîprvÂ`00jühlmmó´åêZÈ܃agwå?Idè+CiÔ¸þ­ý(_±žž<Óíó6•ªTÆÇׇ  %;;‹–­ünèšæÇÎöŸºœœœxaÐ TªR9϶¥J—âÜÙs¼ùö›ùN:÷)ë{Ë5‰ˆÜË@DäžÑ剮4øÏ|ŠY3>£HÑ"ôìÓ‹J×yG¬ÄÄD¶oÛÎs=Ÿ£gŸžë*U©ÄÛC‡³yÃFÚ´ËuêÄ)RSRp)TÈ ÷ Ió&æý:DvNv—Þd'&&râøIêÖÏÿŽSõ4ÀÑÉ™_~œË‰ã'xó�·nûõò-W– ‘‘T­VÕ¼,';›9ßþ@ó–Íñ&o�CíGëP¡b¾žý?êæ|ó=‰‰‰$$$0ó“Où}áï¿Âí`mlløṗDGG3xà`6l >>�„„6oÚÌ+_&,4Œ‘ãFšÃ@«6þ� bé_K-®KÕjU©Z­*Ó&OãÄñdef±o÷^&Ž™@rrÊu?÷…‹&.6–È 8{æ,Ó§|b¾ö—ç}´ôoIhh(Û·n§m‡ŸSs-¶¶6tì܉ŸçüÌ–M[ÈÌÈ$è\FOàÄñ8:9âY¸0MoƳ¾`ßî½defqâø Æ�Ï…ˆ · y©DDî;âà¾ýW\¿få*Vªh19û2[;;š·lÁ²¥Ëˆ�‹ ^ƒúd¤¥ót×§pvqáÝÞ³øtÛ¯µ?§Ožâ‰ŽÝð,ìɘ c¯z7©þ-Yµbí®ã�rÍZ¹C£žìÜ�Y_~]ס~üùö[|5û+æ|;È"4eÚd )rÅý*U©Ì—ß~ɬ³5bFcîüƒÁÀ£uåÃ1#ó\»ÊU«P¶\9¢¢¢háß¼Ü`00ñ£‰Œþ` ƒúrçstêÖ…¾ú]÷súä3O±uÓVžîú4NŽN8:92xÈ`œV:3iìDJ–,IíGkS±rE|Ë•%1!�†�\wû7bðkƒINJæƒwF˜¯M½õ9öCó6ï}øFO`è«CÍס…_ ÞxûÍ;R“ˆÈ½Ä`ºü±‘Èmâ1/÷/ =îíI–—Ïã`çâ·Ø’ÜËÆŽKNvã&�#,,Œä¤dÊ–/‹“£“y›!/½F•ªUòÖë�'##ƒòÊ_sNÇ–M[øà�,\¼¯%®YKfF&©iixzz\sÛËHÏàüùó8»¸à]ÊÛ¢çâZâbc ÀÇLJ"E‹\÷¾ÿARR¥J—Âõ?C»®‡Ñh$,4ŒŒôtÊ•/‡�½=F£‘””‹¡V½žíEýúõy}Øë7|ŒÇ… ‘/^Ü|;åÿŠºELL %½½oøy“{WÍ¥QÀýówð^?¹û¨DDä:ý÷›®óã[®ìu·÷û‚ߨýh�ë �;lëzn‘û_ŽNŽæÛÈÞ¨ÂEŠ\µ·äFx—òÎwÞÉõ²±±ÁÇ×'ϲ‡�ÛwtžQãFÝ–š¯Æ³paó°¿+)îUœâ^úCDäß@DD¬lõÊU|9ëKâã˜9{fA—sßèû|‚ƒChÞòñ›\""rç)€ˆˆ\…LÆ«�TíÒ½ En —àáG¡GïÔ¬]Ëb‰Üšç{÷“ ÿÖ­ º¹ ‘«hÚ¬é5·iݶÍu´ô�R¥KñôsÏÜÐ>rmí:´+èDDä:è6¼"""""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5 """""b5v]€ÈÝ®æÒ¨‚.ADDD価¹aÍ� Ùâ]†ˆˆÜƒÔ"r =¼ º‘»V§Å¹ÿÕωˆˆÜ(õ€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆÈç|HXA— "òÀR‘ûÖ®½‰¸pÑbÙŨ¾ùa~A—v]öî?LXø…+®ظ�¤ä”‚.SDä†(€ˆˆÈ}ëËoç²÷À‹e6ï EÓ†]Úuùæ‡ùìØsàŠëGMœIDÄÅhQD¤àÙt"""F“‰€�ÛhÞ¸K–¯Ã×§õ­IBb·ì$òb4å|ËмI}ìÍû…„F°c÷~’“S¨Vµ êÖÂ`0°kïA“’9vâ4•+”£Jåòlݱ— £ÞÊ·†´´t7ï ,ü¥¼KТi\\œ8}ö<ƒ£ÑHà¦í ê÷\žý×oØF“†�±bM %½ŠÓ°~’’’Ù¸e'‘Qøú”¢y“ú89:š÷ ‹ˆdûÎ}$$&Q­JEÖ¯ƒ�ÁÀÞý‡‰KHäÄɳ;q†êU+Z+pÓvL&#;öì§�« ¥½K�”œÂÚ€-D^ŒÂÓÓƒ–ÍR«gƒBÈÉÉÁÖÖ–Õë7ñRÿç1 mnÙ¾‡CGŽSÖ· Í×cïþÃ4oRŸ°ˆHŽŸ<“ç|›7©�½�ÞJˆÈ�Ño ¹+sŒ|0öcºujÃî}‡Ô÷YbãâéóâpŠ+L�j•Y¹v#óþÅ7ŸOÆÖÖ–‡ŽñÚð1Ô{ô¼ŠcÞoKðoјwß|™c'N“’’ÊÙ ·„Q¥ry“’±µµ¥�‹Kžã§¥§Óïå·±±±á±:°i;ßý´�Ÿþ÷1…\\Xµv#ᑈ¿–­å…^OS¥ry:†»[!¾œ1;;[L&Oôx™ÃÇNÒ¢i¶íÚÇOÿû„²>¥-Ú  gÞoñÃWS©BYL&o¾?Á¼¾Þc5©÷XM¢câxá•wxëµù9‘kѹ«téàotðÈ ÚµjŽ­­-®…\x¼i}>NvvG�Ÿ¦cÛ–æ}«T*O¥ e9|ôD¾moÚº‹æMêç»îàáãø·h‚££ööö´jÙ”‡�›·©[ç‘«†€Îíý±¹Tÿ¡#Çiã× ;»Üú���hÙ¬!Ãh2qèè :¶ù§þreËP½jE9Î�ªõHuæ|9�ÌÌLNž:Ç¢¥«ˆ�O$;+Û¼MíGjä £B9_*U( €Á` ­ó<ÛeddòÎÈ)4mT�î]ÚÝž'\D8ê‘»Š»›«ùÿ-xx¸sæìy’“S0�y×»»Ÿ˜§Ý´ôtRSÓ(RØ3ßã&$&áîš·­øµåîîvCõÇ'$áîþßúÝ8BFz™™YyÖ{z¸ç[ÿµÄÄÆ3vò§;y†j•+ðPõʸ¹²Øæ¿�/‹‹OÀÍÕr�ÇÎÕd21qÚlœ�yëµ7\ŸˆÈe ""r×*Y¢8Á¡áË‚CÂð.é…‡‡NŽŽ‡FPêR¯„Ñd"$4œNíýó´µc÷Ö«sCÇ:éX—¸1%K'8ä¿m†ã]Ò gg'ÜÝ\  §|Y ÷M~pHø{i®æ‡¹¿“““ò߾3OÒ_°Åb›ÿN:ÿw�aá0™Læmþ{ûßæþÎ‘ã§øþ‹©æ‘›¡!X""r×jí×”?—¬ä|pîîÚ{�M[vÑÚ¯)ƒ�Ö~MùöÇù$%%c2™˜·ð/RÒÒiTÿŸ ‘––À†MÛy¼Yƒ+«�_3V­ÛÈÑã§8~ò ËVÐæÒü‘›ÑÆ¿K–¯å̹`ö<ʺÀ­´ñof>¿ïþ�ø„Ü�߯ *&–¦�êæ©`Éòµlݱ×â©éiddfâîîf+×l <"’ôŒŒ|kÛ±{?.]@£zuHIKgÁŸËˆŠŽeá¢eæm6ncî‚¿˜6áý<=#""7J= ""r×êÜÞŸCGNðü Cqws%5-�!/÷å‘UxeP/Þýíº÷ÇÅÙ {{{&Œ|Ë< êáU˜>ë[BÂ"ˆŒŠ¹êü�& £{ç¶ 27·B$%¥Ðûù'hÑìæ¿3¤m«æ8tŒÞƒrkJIIå¥=x¬öüôBÞýŸ€‹‹3¶66Œÿð-ó0±‡kTeö×?qáBÃ^ȯ¿-å¡ê•iÜàQóù½6l4ï¿5˜gŸèÈÐwÇÑí¹1šLÔ­óÏtïÈÔ_S¹b¹<µmܲ“m;÷ѽs[ÜÜ\÷ÁŒš8“ï~Zˆ­­ /ö{޹ þ`ùê@’SRéñÂP‹6>›6šú�Õ*è—‰ˆÜc &“ÉTÐEÈýÅc^î—b%ôðºÅ–DänÕé™Ü»Ný½ð[«/--� £(SÚ;ßÛ¾Æ'$’˜”L™ÒÞæ à—¥¤¦b2ATtŒy¨ÓÕddd~!ï’^ß×qKõ§§s!2Š2¥JbooŸg}Bbñ ‰ø”öÆÆÆrpBJj*ŽŽù{2™L$'§àêZƒÁ@Vv6!¡”ð*j¾CUzFÆ5Ï#33‹è˜X¼Š#84ï’ÅÙ½÷?Ï_Äÿ>›t[®�Ü{ô÷\î Á‘»ž³³åËú\ñ;'<=Üñ-S*Oø(äâ‚k!—ë ŽŽ”/ësÛ€³Ó¥úó �;ỬOé<áãrýWšsa0pss5ÏÛ°·³£B9‹Ûã^Ïy¤¤¦òlß!lݱ‡ å|HIIeÎÏ¿]uÎŒˆÈÍÒ,‘\aOÆ�Ƭ¯dÔ„88ØÓ´Q]z>Óµ K‘û�ˆˆˆˆÐ¢iZ4m@NNŽù{WDDî‘+¸<öUDòÑ-w^€~ND®Ls'Dò§9 """""b5깆ƒ�‹t ""r©¹4ª K¹«©DDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬Æ®  )H™™DGGçYîââ‚gaÏ›jóã)“”˜Hßý¨P±Â©û·ù 1�<Ûã¹[nkýÚõlXÈ#µá©gž¾-õ}óå7”-ïK›vmó]Ÿ�“æÀ�x—ò¦Zõjw䉈ÈÝIDDhÛ¶leäû#ó,ïй#ï}øÞ ·wêÄI–,ú ¯%xuè«w¤î£GŽ‘“�s[Úšûã\N�<ɾ½ûx¢ûØÚÝúŸ†=»v“�� íò_Ÿ™™Éè£nú:‹ˆÈ½KDDh¡¡ax•ðâ½ß·X^¬x±›joõª5¸ººR·~=Ö¯]Çà!ƒ±±¹{G»� âÔÉ“têÚ™¿ÿZÊ®�»hظÑ?®ƒƒc'�û”wA_±2y …†„R¡bEêÖ¯{Ëm�FÖ­^KËV~4jÚˆÀõØ»Ÿ:u5o³g×nœœœ(\¤Ë—.'11‘jÕ«Ò¶}[sÏÃæM›ñ.Yƒ� «W¬&--•ZµkÑ¿e¾afͪՔñ)Cõ5ÌËÒRSY³j- Õ§DÉ’W¬yíªµ8»¸0ä�ר²i kW­³ 99F–-ù›ÇýçÐ�ƒìÚ±‹7ß~ €óAA¬^±†ä”dÊ–õ¥C—Ž89:Y´îì9V.[IFF?ò~­ý±±±ÁÎÖ–œìlìíí‰�‰eóÆÍ4jÒˆâ^ÅÍûÆDǰeÓ7mL±âÅHMIaù²å�=�gaOü[ûS¾Byóö&“‰­›·r`ß²³²©öPUüZ·ÂÎÖÖj¯'¹¶»÷c9+ Åǧ ;¶mç÷…¿±yÓfÒ3Òoª­}{öM›ömhР…\]Y»f�Å6/YÆ·_Ë€>8tè¡!!|�™ŸÌÄÉÙùŠõšL&Ö¬ZM ¿8»¸ÐÂïq6oØhqþ9ÙÙ|x˜ˆðÞñ-ýýXôû"¶nÜ‚_k‹m7näç?S¦L²³²˜:q*-ýZòáØ1 \Œ¼Hïgz²zùJºvïÀ‰'øaÞ�”.]€q£Æ±)p#^`Ѷ“£ÍoƆ€ ôô‚yùÚÕëðo퇓£ÍšŠ­­-?/øwwwL&Ÿ~ò)Ó?šN£&�°³·çï¿–ðÖ;ÃÌ×uÉ¢%|óåÿHÏHÏÓ3#""G= "òÀÊÎÊ¢X±bt}²KW/ã‡y?2ï·¹00eüäj+3#“ �hÓ® ƒ¿V-IJJbû¶íÛz•(AÇ.�Ì�›6kJ�‡b÷ÎÝæe5~È>º<Ñ•%K²{çž<Ǯ߰ŽNNl Øä]Ú·g/íÚçªËÖ¬ZCq¯âÔy,wˆXÍÚ5)V¼kW¯Ë³íO=A™2e8uê±1±ôèÓÃ|®^%¼>âmJ•.eÞ§q“ÆæðP¹J%ÒÒÒò­¥UÛVœ=s–à óœ>yŠóAAæ@¶mëvžzö)ÜÝÝ0 ôé߇´ÔT8”» ‡!>.þÒ5ëÂÒÕ+|ˆˆÜeÔ"",;{{þ÷Ã7ËJz{Ó£O¦MžF||žž×ÕÖ–Í[HINfë–­=r ÷.Pä~’ß´y3ó¶åË—Ë3—£BÅ œ=sÖüø¿½/666”+_Žˆˆˆ<Çvtr¤I³&òÄÓÝY·f-®®n4jråÉäÙYY¬]�ÁƆáC‡ý³<;›íÛ¶“””„›››yy©Rÿ‹°Ðp >>>m¶nÛÆâ±W‰×ý\¬[³Ž§ž}'G Ïöx–Gë>–gß—F×îÝhÓ® Û·í`ïî½üùÛÌŸ·€oúÖÜ{#""OC°Dä�µkç.:¶ê@XX˜Åò3§Ncï`O©2¥¯«�ÄÄD¶oÛN›méÙ§§Å¿^ýz‘ž‘Îæ ÍÛŸ:qŠÔ”óã¬Ì,î;@¹reÍË:dž”~ù'ŽŸÄ·œO¾5ÔoÐG'g~ùq.'ŽŸ íu ¿*V¼¯ }Í¢ÞÁ¯ ÆËË+Ïäùó)ë{©ÆÃËÇ�Ë׳¿ºéç£UëVœ9u†Ë–m>ggŠ/NbB"U«U5ÿóôô p]` $8„¯gEFF&-ý[2ìÝaÌÿs™™™ìÚ¾ë¦k‘ÛODDXuêÔÆ¥P!&��Èù  RSRr'[Ïù™.]»âì”;wà—~fâU.€ì¬¬<½uë>†§§kWýó†>%9™Iã'yá##™�ì¬,‹[õŠˆHÁÓ,y`ÙÙÛ3åãÉL3�ÞÏöÎ]fkKÇ®�yåõWÌÛ €³‹ ï~ðU«U5ïç×ÚŸÓ'OñDÇnxödÌ„±”ô¾ò÷µðoɪ«h×áê½6�‘ž�[ÿ|×ûµnÉ‚yóY¿fO<Õ=ßmÆLØÇðR¿�Ü4à¥4iÖ䦟ƒÁ@«¶­øå‡Ÿéø¯0Ыââã™4f‚¹gèáš�0é£É †Üo�ã5¾™ý?ýö'�Ûs2hð j?Zû¦k‘ÛÏ`2™L]„Ü_<æ] ¡Ç½=ñóòyì\ü[’»�Ñh$òB$ñ ñ”)SÆbòõí6väXr²s7yaaa$'%S¶|Y‹;5 yé5ªT­Â�·^'8è<”¯Pþªs:¶lÚÂïŒ`áâ…74üV„‡…“š’B)Ÿ2æ9.wRRRa¡a)R8ßsLMK#",€Ò>¥u,)5—æöÞ/ïõó�»�z@Dä�gccƒw)o¼Kyßzc7àß·¨½ßÍ ¹–ßüFíGëX-|·Ýµ777ªU¯vÅõ.ÎÎùöD‰ˆÈÝCs@DDîq«W®â‰ŽÝØ¿ïýõ/èrDDD®J= ""Vä߯“ñê#_»tïB‘"E®»Í‡y„½{P³v-‹y$"""w#+jÚ¬é5·ùïú]K©Ò¥xú¹g úÔDDD®‹†`‰ˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕ(€ˆˆˆˆˆˆÕØt"w»šK£ º‘û†z@DDä†5[<‚f‹Gt""rRˆÈ$ôð*èDîZ�çþW?'""r£Ô""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""VcWЈܭ<æ],èDî^Ý&ú9¹š„^]‚È]I= """""b5깆ƒ�‹t ""r©¹4ª K¹«©DDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬FDDDDDD¬Æ®  ¹“"Â#8sú4EŠ¥ZõjØØÜØç.ß~ý-!烯¸þ¹ÞÏS½zõ‚>Í{’Ñhdᯠ8|è_ˆÉd"è\ ÖÇ¥P¡;rÌ�Ûw¸~ïŒx§ O_Dä�¥¹/™L&>Ÿù9Ïu–‰c'1xÀË ~áe’’’n¨�}{ör`ÿ�‚>�ë6óã™lÞ¸© Ë¸.›7òÅg_P¢D \\\\Àè£ˆŠŠºcÇ c˦-}ê""4õ€ˆÈ}iùÒeüñÛŒŸ<žæ-'$$”×_~�¯gÅð÷Þ¾¡¶*W®ÌØIã ú”®ËŽ­Û)îUœ¦Í º’k Å»”7CÞ@ ¿–”+_žâÅ‹ti""r)€ˆÈ}iþÜ´ëÐŽæ-Àǧ ¯¿5”S'OÝöcåäYµ|å*”§ÆC5ÌËwíÜEJr2-üZ¹ÃÁV.[All¥Ë”¢]‡vx.lÑÖþ=ûØ´q3˜L<ôÈC´ðo‰�� ™™¬\¾’&ÍšP´XQóöÛ¶lÅ¥P!jծŒEKHIMáØÑc¬_»¿V~×}ÜÿŠ�O`ùÒ¿ ‹ DI/ÚuhOq¯‚Abb"+þ^NHp®®n<îÿ¸ÅP´=»vãààˆw)o–.^B||<+U¢C§öØÙÛs`ÿŽ=FZZ:K-¡I³&88:�’œl1üê̩Ӭ^¹†ôŒtš6kBÙr娳k7í;u¸bí!çƒY·f11±”,Y‚�;P¸H‹mÒÒÓYòçbB‚C(Y²$�ºvƳ°çmmˆˆH^‚%"÷�ø¸xÎѤy²³²héß’¿xÛ�gkkCdd$ï {—ø¸x‚ÎñÞ°w1M@n°èû\Ö®YGRR¿/øƒ>Ïõæô¿Ñ�ßýÀÐW‡ræô"/^d⸉Œ•Ûó’š–ÆÇS¦bqìßæÿÆÚUk\@ZZgO�açö�×}Üÿ £_�>,ú}1)))¬\¶’^Ïö"8è<‘.пg?æþ4�Ä„$vïÜÅËý_â¯?›Ûø{É2æÏý•×^z•°Ðp“˜9m:“ÇOà䉓œ;}–ôÔ4×Ïñ#ǘ>õs›6lbPß�ìÚ¹‹Ø˜XÆ~8–�§|̧Ó?»bí{ví¦ïó}ؾmi©i,þó/úõìO|\œy›¬ÌL†¾<„½»÷ðë/¿òꋯ˜_'""rg©DDî;1Ñ1\¼Iÿ^ý8sê ž…=iÓ®-ÄÉÑé†Ú;wîO™–ïºWß‚³“}úõaËÆ-̘6�QãÇ0yÜdZ´l‰_+?²sr˜8n ›4bÔ„ÑØÙÚ’–žÎ[¯½É'MçËo¿$8è=¯ë¸ù™5ý3<==ùòÛ/qvq!;+‹Aýñó?óÁ˜ùrÖ—0ðÃÜ(R´&“‰éMgÖÌY<î×OO6oÜÌg_~F­:µ˜ãëü_æa2}ÈÓÏ>MFZ:Ëÿ^ÎôY3:wÎ\CVfÓ&Oïu+>óƒ�ð°pú÷ê�Á`¸â5øcáŸÔz´63>Ÿ @|\½ŸíÍî�»iÕ¶5ÉÉÉ<îׂž}zЬEsÞ:œ³gÏQ¥j•;÷Â@= "rЉ‰à›¯¾¥mû¶|þ¿ÙôÐ�¥-eÖôY7Ü^vv6I‰Iùþ3™r{8líìø`̇lÞ´™÷†½KLL4o¾ó&çÏyá}_èƒ�­-ÎNN<óü3=|„¤¤$vî܉‹‹ Otïf>nƒF yåõW°¹´Ï�ºžãþWNŽ‘];wñÄSOàìâ€�½=CßJ�ºuØ»{�»u¦HÑÜaMƒ�>ýû�™‘ÉáC‡ÌmU©ZÅ>*U©LFz9Fã5k?yâñqqôê×Ë8J•.EKÿ–WÝÏ`€È ‘œi~\¹reÒÒÒnê:‹ˆÈ�QˆˆÜwèÓ¿7Ïõ|€šµj’–šÆ7_}Ã�·†ÜP/ÈõNB/_¡<=z÷äÇï~`Üäq¸ºº Àûo�ÀÆæŸOï3331™LÄÅÄAÉRÞØÙÛ›× sýññ 7|®ç¸nnnûDG]$+3 _‹åµ«Cmê�–šJ||>e}-Ö÷*޳‹ áæe^%KÜôs†Á`ÀǧŒÅrß²>WÝoÀKùÞ‡ ì3_êLvV¾åÊÞ–kY®|y8h±üÐÁÃWÝoÎ7ß³w÷^©õý¾Àœ_æÐìñf®Ûp[ê‘[§""÷{{ÚwêÈ�ßþÈ–M[HMIaÛ–­üüý�tìÒ www~ùág&Ž™pÍöbbc X�ï¿ÇO¹o”Ì�Ï»#Þ¥ÆÃÑ»o/>ûä3b¢cð.åM½úõøì“Ï8tàY™Y=|„ £Ç›8šµhŽk!&Ž�HDx©iiüoöׄ…„ñp͇qssÃÙɉ¥‹ÿ&=#�Ô”f|4�¬lË;7]ž¬�˜˜x]ÇÍO§®�ùmÁol Ø@zF:GaîO¿P­zµKë;±jå*–.^BjJ 'ŽŸàãÉÓ(_¡<?òÈmyË•/Gý† ølúg;vŒÔ”~[ðÀæ_c¦–.^Âè£H½4cÏ®=|úɧ„‡�“cäÔ‰“œyŠÑ#Få»®Ë]ð}ýU&��H§n]¨S÷Qz÷ëÍÆÀ�|‹~û“ƒûò󂟯yÜü |i q±±Œ1 㥠ã­Ú´¢ÿÀx¶ÇóDEF3ã£éL›œ{w°*U«0fÒXlmoßçZïŒx‡ÞÁKýro�\­z5^~íe~ÿõwó6'Oœ"`]ÃÞÎμ1ü F�Eϧ{˜·iШ!/¾rûo¿,""7Ç`º| ‘ÛÄcÞEz\û¶¡w³Ëçq°³¾•ù^Mll,eÊ”¶ø‚»‚r1ò"qqq”ô.‰‡‡GžõF£‘ó—¾oÃǧŒÅ¤tȽ…lXh… Æ«Dþ?c 89:áèäxÝÇÍO\l,‘‘)éím¾µî¿%%%Š««[žÉâ·Êh4’’’‚³‹ a!¹½å+”gá¼lܰ‘ÙÿûâŠûæä‰ˆ'%9…%¼®ù¥‹"·[Í¥QÀýówð^?¹û¨DDîkÅŠ»cs>n†W ¯+Èí¡(_¡ü×»ººRµZÕ«#¿€q­ãæ§p‘"y¾AüßÜÜÜòLô¿]²³²y²cwú¿ØŸç{åöf$''³dñš·h~Õ}mmm(Sæö"¹}@DDä®ãàèÀËC^fÖÌÏÙ¶u;®…\9|è0®®…x¶Çs]žˆˆÜ¹+uúIê5lÀþ={IOO§S×N¿DDD –nÃ+"""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""VcWЈÜíj.�*èDDDDîê‘Ölñš-QÐeˆˆÈ=H= "W�Ðë K¹kuZœû_ýœˆˆÈ�RˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆ<ÒÒÓ9rì$™wìçƒÃˆOH,èS¹«)€ˆˆÈ}/=#ƒ/¼Á‡ã§s1*†qS>cÜ”Ïnûq&|ô9·ì,èÓ¹«Ùt"""wZHhQÑ1¬ÿ{öÔ®Y£ Ky`)€ˆˆÈ]Ãd2±c×~Ž?…‡»mü›áîæúϺÝ8~â4®®…hP·6>e¼ˆOHäø‰3Ô|¸ËVŸHÝ:�P§ÖC$§¤²}ç> 6mÛEƒºµ©Z¹ æãFÇıbM YYÙ´ñkJ|B…=Ý)]ªdž�F#6ïàÔ™ <ÜÝhÕ²)E‹xZl³sÏöî?L±¢Ehßæq ¹¸ô¥¹kh–ˆˆÜ5&Oÿ’ Ó>'&6žu¶ÒsÀÄÇçΩøø³o9þ""£Ø¾k=¼Á¾G€Ü¹Sf|Å›ïO¸ÔÛË«ÃF³yÛ.ÒÒÒ9rüF£‘]{’––Î_ËÖ²tå: ·w¤ßËÃÙ¹û‘£yå­QLžþ%ÛwíÏ·Æ‘ã§3û›_HMMcÓÖ]ôø¦Å¼�EKWñãÜ?À`à×ß—2|Ĥ‚¾¬""wõ€ˆˆÈ]áÄ©³¬XÈügQÚ»F“‰¯¼ÃеhX¯6‹–®æû/¦R­JEfÎþžY_ýÈ÷_~À…È(† H³ÆõHMKcû®ý4mT�~=ŸdûÎ}¼÷ÖËyŽû¿~¥N­‡ÿá[ì?t”—‡~˜o�iié¬ß¸�¿ú˜*•Ëc4™1ú#BBÃñôpÀÙÙ™Ï?‹Á` Y£º |í}23³pp°/èK,"rWPˆˆˆÜö8ÂÃ5ªPÚ»63¦Œ¤}ëÇ9|ô$åËù˜Ã@‡¶-9~ò YYYØÛÙѤácæõÅ‹!;;çšÇÝwàíZ=n~\û‘”ô*–ï¶ŽNŽx/ÊŒÙß°qIIÉL÷.�nßç»sö\0�Ÿ„‹³Í×£aý:¸^òuððqfó3Ï=Õ‰Ò¥JÒ·Ç“¼ùþxŠö$!1‰êU*Ñë¹n}ùDDî“é_ýÙ"·�Ǽ‹$ôð*èRDäéôÌ@þ^øímo;''‡Ð° xzºç™b4™ »€›k!<=ÝoËñ¢cbqrr"==ƒÌÌLJy— ç€76d �Ö~8ß}’’’‰ŒŠÁÓÃ�bEo~˜ÈÝLÏåNQˆˆˆÜUlmm)ë[:ßu6ƒùËo—9¿üNph8SƾC‘Â,þ{5QѱԨ^ùŠû¸¹¹âææzG‘Ë@DDä�6x`/&ò%�ž(UÒ‹‰£†ãäèXÐ¥‰ˆÜ—@DDä�æZÈ…‰£†¹Ã¿lmm º$‘ûšˆÈ\û*"ùè6 ÐωÈÕhî„Hþt^±õ€ˆ\ÃÁÎÅ º¹‡Ô\uë�ˆÜÇÔ""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""V£""""""VcWЈÈ�—‘žALLÌ×.Zg'§jó÷…¿Q§N*V®d^vêÄIÌ[ÈÁIMI¡Xñb4l܈çz>‡gaÏ[:‡ÄÄDöìÚC¥Jñ)ëkÕë7kÆ,jÖz„ÇýZ°tñ*V®D�‡j\uŸÝ;w³tñ||}øòÀ|·Y²h {ví¦A£tèÜñ–jüæËo([Þ—6íÚröÌYÎ�§Aㆸ8;“�“ï?ÏåÄñ“¼2äJ•.uË×dõÊUœ?Ì Áƒnûõž<~2éiiËììì)á]ÿVþT¬\ñ¶ój.F^dö§Ÿ_u[[F�mÕºDDîUêy:xˆçº?{Å»¶í¸¡öN�8ÉŠ¥+)_±‚yÙâ?1¨ß Μ9C×î]>!Ïz“ÉÄ/?þBÀºN�8}Ë5îÙµ›3§Î°~ÍzF�ELtnè\»r5ß}ý¥J•Âéƒæe æÍç·ù Í�Ïœ:Ëž]»ïÈõÞ¸‘Ó§,¯IZj*Ë—.c@ŸØ°á¶/+3‹Ñ#FqêÄÉëÚþâÅ(Ö�’’zGÎ_Dä~§‘@•jU˜>kFžå;·ïdåò•<\ó‘jï_ü�§Ÿ›ÜÏ0ìÛÏÌ�gÒ¹[Þþ&¶¶ÿ|¶ñ|¯çysÈ›|ðö~üõG ¹º^×1âãâXÀ°÷†ƒ³3¥K—fì¤qTªdÝO¿ÿ«|…ò”,Y‚•ËWÒ±K§kn_¸°'6Ðå‰.Ë�;FÔÅH )rÛkôkíGÅÊ)Z¬(¡!a”+_ŽW^å¦Û€QïfÈ›¯Q¶\9«]‘û…ˆÈÀÝÝ�ºõëZ,‹�‹câØ |8抽þ7ÁÁAç9tà c§Œ7/ûîßQÆ×‡7† µž…==~4ýzôeé_Ky®çó=|„ÔÔ4ÊW(Ïß-%**šŠ+оKœ�:϶-ÛX¹l�Õ­K¹òeIJLÂñ_Ÿà§¦¤°|Ùr‚ΞÇÙÅ™Ç[4·SöÀ˜�CùŠX²ø/¢£b(_¡<:uÀÑÉÑâœÖ­YGLL,îî4nÒ誡¬m‡¶üøý�×@Z´lÉúµëó�€µ�ÔªS‡¸ØX‹å&“‰­›·²{ÇnŒ¦ªU¯Fë¶­±³·ÿç¼ÓÒXñ÷2Î�9GÑ¢EóÔaïà@jJ .ÎÎìÛ½—“'N’œœÌ’EKhظÛ·î ^ƒzx—ò6ïÆž�{hÓ¡ NŽ–½$6†­�-K-±8^||Kþ\LTT4>¾eèÔ­ .ÎÎÏÑŠå+8w&Ïžøµò£Â¿zÎn„³“u«Ãš«-–ŸöìÞËÃßàBD+—­$::†ÒeJѶ}»‹™ÓfШiã<½ÿr>˜€uæÇééìÞ¹‡S'N2å“©$%%ñéÇ3iß±ï~ø.ƒ�ø¸x^ìÿ"_|ö§N$,,Œïÿ÷}ú÷5ß àBDz`Îwsxsø›L�>•6Í[óâàA4lÜèºò³yã&æ.œK©Ò¥ÈÉ12yÜ$ê6¨Ç¸Iã°µ³#=#�aCÞâ“Éóõÿ#5-�©§Ò©K'Þzç- III 8˜Ù3g3iÚ¤[ªGDän¤‘ŒÉdâËÏ¿äù^7wgª ³A”*óÏ]”2Ó3ptt¼ê~ÎÎΤ¥ÿ"œ�œx¾wóã?DãæMÙ}�›÷îÚCûNÌA w¿^M&öïÛo^VƧŒ9|T¬T‘œ#™¹u¿óþÛLûìcsøÈHÏÀd4’ž–~Õã—.S†s—&¦_�Á~­üX¿v½yYÀÚ@j?V'Ïõß·g?^%JоSó²êÕ«S¿aCöîÎýD}ïî}´ðka½úõÊ·çâJüü[yáÇ�`Çö�$&&Ѧ}›ën p‘"´låg~\ùR½|í¶oÝF÷g»›ƒ†Á` Ï ½IMKãÐ�ƒWm{ëæ­Œ1ÊüoÒ¸I¬^¹ŠFMS½F5 ·w'-=�¾úšŸ?ÏžtíÞÕ|½ìÝ�ÁÆ–}{šÛ.éíMÛöíØ»kï �ïõèþLwó]ÆB‚ƒ  §Ï }ͽƒNŽN<×ã9Ž?N|\g1 =;'‡=»vóþ[ïR¡RyzôîIDX8öö”ô.i±oÙr¾¤$'“��@xxÅ‹³˜—’{M}Xò×_˜L¦:çkñöþ'œ‡††0ò½‘ØØüsm2331™LÄÆÆŠÁ`àÕ-o�––NzF:©©)š ""÷‘Ìâ?Ó¬yS<==nx_6¶¶dýë´>¾¾xödëæ­´ºÂ�¥ì¬,víÜÅcuÿ™Ü›‘‘‘g»ô´t‹ÉáWbkkƒ££#iéé×lÃÆæêwnzoØ»œ9s†nݻѶc;Ê—/ÏOs~$"üÂU÷ËÎÎÆþ_ï¦Zõjx{{¸>€Gj=BôÅ‹4k‘÷.N..΄‡å=§Œô óís] ¹’–OïLÚ5zlþ«¥¿kW¯¡ï }زa3o½ûÖ íŸ{m¯Ü‰îàèˆÁ`àéçžÉwRw‰^7t,;[[4lÀC5áè‘Üžgr²sÈÎÊÆÞáŸç"==ƒÁ€ƒ£cnÏ[Zþ×ÔÑÁ1OØ»UvöÿüY½°‡¿7 OÏ<Û–*SŠ“ÇO`kcÃè £ó}­þ78‰ˆÜ4KäǶM[hÕ¶õMío0(R¤‰ ÿ|¯…�� O<ù�k׳ïþ|÷›óíÄÅÆñä³ÝÍË.„GXôš˜L&vïÚMÙ²e¯«__ì;`±ìÌ©3$%%á{�mD]Œb×Î] {gýö§AÃx•ðâbdÔ5÷Mˆ�§X±b×}íZ¶öcýÚÖR§îcù@‚Μ%11ÑâºØ·ß܉×e|}سÃr˜ZXX##¯»€–þ-  çÛ¯¿Ã`€Çoãmm!wˆ]q//©Z­ªù_áÂ… ü×ÜŽåæêJÂ¥ïUññõÁh4rð€åë`ÿÞ}÷òÂÙÉ ß²¾$ÄÇç¹aÀ¾½ûð-w}¯“›å{i²|ff¦Å5HOOgÛæ­89æÖ—�“ƒÅ61Ñ1ÜàŠ7v¹—)€ˆ<@vlßIŽÑHÍÚ5ó]ÿË?3qÌ„«¶Qý¡êœ=}ÎbY¯¾½©þp Þ}ómæþ4—ˆðRSR8qü“ÆMâç~¢ï }©^ãŸoÏÎÉa˜ñ„œ&&:†YÓgqúä):uͽū�ÜO¦ƒÎåÛÓÑ©k'6oØÄo ~#%9™3§Ï0iÜ$Jz{S¯~½ëºnnØÚÙqìØqŒF#é,˜7Ÿ�ÛwCzÆ•{Μ>KµKs®‡ŸKŽ9ÊÊeËiéß"ßm.ϧ˜4v"""ˆ�‹çó™Ÿt.ˆÎ—®Kç®�8vì³?›MlL,烂˜0z6¶7ö+WÄ·\YþXø;ÍZ<޳‹ËU·7r‡ÑÅÆÄ^÷°¥.Ý:óçï°vÕÒÒÓ  gÒ¸IlÛ²-ß�ëagoOì¥[W«^�Š•+1}Ú Ž;–{[æ¥ËXþ÷ º<Ñ€ú êSÜ«8“ÆNæÜÙs¤$'3î¯lÛ²�.Ýr·1˜r_kÁçƒIIN¾©ºòãU‹�2kÆ,8HVfG�eü¨ñæ»–U«Q�Š•+ñÑÄ©œU«U¥xq/z>“;9ØÁÑ�ÁC›'Œ—¯T‘â^ÅyíÅWyõõWéܽ«Åþ�»u!"â_Îú’YÓ?ËݧBy¦|2Åb8ÎÕ89:1ä�טýÙlþº€œìlê5hÀ{¾ÇG“§1bøûù~�ctT4Q/òXÝG¯ë8�;¯áaá4kÑ<ßm¼Jx1~ê¦L˜Â3Ýžr‡ {w8µ«@³Í4x?|÷ æÎ M»¶yæA\¿V~üðíÚvl{Ím«W—é}B·]Y»qÝuµß£o/âbã™4vbî§üÀC�<̤i“ozè“o9_6²o÷^êÔ}”‰S'2æƒÑ¼ÔïE wx^—îÝèÑ'wŽ�£“#S>žÂ˜‘céû| ÷õÚo`?ÚuloÞ¦V�Ú|>ósŽ>ʘ‰coª¶ü|0æCÆ�Çk/æÎg±±±Á¯µ?CÞb~ê‘âîîN›íX¹|}òù„ÖÁÑ�êÕ«_w{^%J\ñM©�­í5o\ÈÕõ†Ž—WWWªV«j±ìjo²W,]ÉkC_½V³7ÍÖֆЕ*^u—B…né¼ÓRSY¶äoÚ¶o{]áãò1o†››Õª_ÿpµ›Q¦L™«®7 W�óa0nê¶Ô×Ë«„W¾�ýß.ß¾WDä~§9 "rÃ^xq«W®!5åÁ£¾eÓJ—)M�~u·ùâ³/xºëÓdggóÔ³Ot9""ò€QˆˆÜ0OOÞÿð=bãânêSñF�R©r¥‚>�›âèèÈÃß(è2nI ÿ÷*Æã-§h±¢]Žˆˆ<`@Dä¦<ôÈÃ7½ï£u+èòoZ~ßiq¯©ñP jy)€ˆ<`Ò²M|~<•O�¥’”¥À!"Ö‘šmâTb§sXšÁû{ ¼QÃ…W«¹àlk(èòDÄŠ@D a©9<·!�ƒqÙøy;ÐÍבÆÅ(íbƒ‹�ÞˆÈ�‘šm",ÕÈÖ¨L�Ï àB&㤰$$ƒùÍ=)å¢Þ‘…ˆÈ",5ÿUqD¤©ânËg ÜiTܾ Ë‘„‹��Êî¶Tvw¦oEg¶^Ìâõ�‰ˆÍÆoU,ëÛQy@è']ä�–mâ¹ D¤iZÂ�µmŠ(|ˆH�jìeϺ6EhêeODš‘ç7Æ“–£a¡"‘ÀçÇS9—Mw[æ5óÀÃAC­D¤ày8˜ÛÜ“Jn¶ì�ÍæËãi]’ˆX�ˆÈ}..ÓȧÇRø¬�»Â‡ˆÜU< |ÚÀ€GSˆÏT/ˆÈýNDä>÷wH&IY&ü¼4ìJDîJM½ìiQÒ�Ä,‡ft9"r‡)€ˆÜçV„åþ1Â×± K¹¢Ë¿£V†)€ˆÜï@Dîs'ro¹Û¨¸CA—""rE�½rG¿ô;KDî_ "÷¹ˆ4#€no)"wµÒ—~G…§ º¹ÃôŽDä>—’�;¡³�¾dPDîb—G]þ�%"÷/±±±±±±±±±±±±»‚.@D¤ ý0÷w·1úý¡yÖ…_¸È„©³xñ…ç©ýH�‚.ÕÂŒÙßSÚ»Ïtïx[Û]ºb烷_Íwý…È(ÆMùŒAýž£N­‡ ú2ÜÔù­XxÕmê×­E¿žOt©""÷-õ€ˆÈmÿÁcnÞ‘ïºää–­ àÂ…¨‚.3�í;÷r䨩ÛÞîÉÓçØ´u××'§ä^“ˆÈ‹} n‹½û�\õ|EDäöSˆˆˆ<0:·÷§s{óãÁoŒäÜù¾˜1¾ Ky`(€ˆˆ\§Ógƒ8yúþ�7aþïK9qê,¥K•ä¹§:S´ˆ'›¶îÂÕµ…=ÝùíÏå$$&Që‘ê<Ñ¥-v¶¶æ¶.FÅðÇ_+ ¿€“³# ëÖÁïñFØØüÓ1qá"¿/^Á…È(|ÊxóL÷N)ì‘om™,þ{5eJ—¤Iú„†EðÇ_+‰¼MYßÒtïÒŽâÅŠ˜÷1™L¬\³�­;÷âèà@Çv~7|MâY¾*�'»¶ã÷¿VpäèI¼½½x¡×Ódfeñ󯋈¼Mµªyþ©ÎØÙýóggïþì ÜB||"^ŋҩ½•*”³hÃæ¬ß° {;žêÚž¬¬l¢¢cð{¼±y›ÃGOò÷Êu¤¤¦Q­JEºujC!ç›~žÏœ=ωÓgiÙ¼ßý´�Å‹ñôز}7ë·’“cäÑÚÓ±m ‹s2™L¬ ØÌ–m{°µµ¡aý:´jÑÄâyyÐé7¢ˆÈuÚ¾k?Ÿ|ö}^Æú�Û0aâs~¥{�—ÉÈÈ`ÞÂ%|òÙ7t|j{álP0B¿—†““c $4‚Ö]z³à�¥¤¤¦rüÄ yŸ³¿7ëȱ“´éÚ‡?þZARr2?Îû“V�{q!2ïp°ŒŒL yŸ¯¾›k~¿sÏZwéÍâ¿W“”œbÞÿðÑ“æýÞþp2/¿ñ!G�ŸâÔ™ žë7„µ[nèš\ˆŒâý11ðµ÷X³~3YYÙ|öåôyiÝž{‰“§ÏŸÀ¨ 3˜öé7æýüñ7Ý{fÏþ䥧³lUížèÏ�ÃÇÌÛLÿü;ú¾4œ§Ïrö\0Oöz…)ӿ仟š·ùõ·%tzzÛvì#::Ž©Ó¿¢ó3‰Ž‰½éçy×ރ̜=‡—_ÿ€/¾ù… ó¡Lúø zx“c'Îq!’wFN¦ç€7ÌϽÉdâ�wÇ3x臜 ãlPƒ‡~È+o�Âh4ÞÙ§ˆÈ=D= ""7àôÙ ºujÍ�—ûеCkží7„£ÇO™'eoÚº‹ißç™'r'ˆ¯\³�—†~Àâ¿Wód×v,þ{5NNެXôÎNNLøèsþ^¹žaC0bÌÇT­\�¹ßÍÄÑÑ�””TZuéÍ·?.àÃw^3×s9|„„E0ÿ‡Y”ð*FNŽ‘a#&Ò¬q=¾˜1;;;ÒÒÓé5ðMFŒ™Æ’…ß°eûn~[´œ‘ïa`ßg�Ü€õ|ÿשZ¹Â _—ªU*ðáÛ¹uù”ñæ“Yß2yÌ;ôx¦ CßǦ­»xØ`æüò;Ï=Õ™)cß ;;¿Ž=Y½vµ®Î™³ç™õÕ�Œ|wú<Àú [é?øš6Êíá¹è‰3xí¥> }�yY§§˜�³N� ¢æÃÕØ¿eöì;p„¯¿ŸÇÌ©#y¢s[Ž�8M·ç_â§_ÿdP¿çX¹fK–¯å—o§›{¡¶lßM¯�oñ÷ÊõtéÐÊZ/S‘»šz@DDn€Á`à…ÞO›W¯V €”Ô4ó2_ŸR<Ý­ƒùq»Ö�SóájlÞ–;Ùy`¿gYÿ÷\søÈÎÉ!#3“ÔKmÄÅ'°ÿÐQö}GG raÒèáÔz¤º¹ÝôŒ ¾ö›¶îbΗQ«gƒ‚  gÈËýÌÜ�œx±ßóm´lÞÈâño‹–ñPõ*88Ø3wÁ_æ[çççå7>ÄÆæŸ:2221™LDÇÄN…ò¾ØÚZ~U¥Ry¢¢o|øÒ¿ç\\>õkÍÃøqîì9p˜ ó¡œ:dqÍ·ãS¦”Åü € å|ILJàÜùLF#­»ö¶Ø&99…Ĥ”›z~/sqv¢h‘ÂæÇAÁ¡DDFѬí3ÛÅÅ'Rª¤×¥šC9tôdžm¢£cqvvº¥zDDî' "ò@³µµ1�áÿ¯ËËmíl-¶¿–üÚKKKÇÉÉ€ñSgñëoKèõܼؿU*•'`ã6¾þ~ŽŽWlçߪ^…ùs>cÎÜß™1ë;ºwiKõª•Ì=+“F§HaÏ<ûùú”ÂÃÍ•  Ð<ë.÷@ÜIéétyf z<Ý…>Ï?AÕÊyyèæm,zLÌ×63ÃüÿÎÎN¸¹¹òÅôüï`e2™ò ˆ×ã¿ÁÇÉÑ‘JÊ2eì»y¶½ÜKâäèH½:�0|è‹y¶¹•Iñ""÷ Á‘šo™R„GD~!ï÷Zlßµ€ e}n¨ÍÃGOZ ÉÊÈÈdçžæ â¿-ZÎ+ƒzóÁÛ¯ÒÖ¿åË–±˜\^®liìlmÙ³ÿ�E»Íüš×†�6?~´ÖÃ*äÂKýŸ§By_ÞýF£‘ å}s�›™É#U5ÿKKOgý†­8;9Q¾œ/ÇO�±èí0�lßµÿŽ_óÝûqî|(ß̚Ġ~ÏѸÁc-âÉ…‹ÿ\ƒÊËnq]23³8xø¸ùq…ò¾Ä'$R¤°‡Åy‡räØÉ›ù©XÞ—°ðH*U,kq¬½û›{¬*V(KpX5ªU¶ØfÖD^Œ¾ã×UDä^¡""´§»w¤P!^ü›·í&.>�°ð ü0÷w>ùì[ü[46Ïi¸^ñ ‰¼ýÁ$Â#" ¿À°‰‰�ã™î¹óBŠ-Ì‘ã§ÈÌÌ";'‡e«øù×?IJN!6.g''ºvjͬ¯~dÃæ¤§g°eûnæüüµkæýFv{{{&�y›ý‡ŽñË‚¿(UÒ‹Í2nÊgìÚ{�ÌÌ,ö<ÊÐwÆ™ßwjç‡��¯ Åé³ADEÇ2fò§„„FÜñk~yhÓþƒ¹w¼JHLb̤O  ',"’ììl:µó£XÑ 1‘à�pbbã5qIÉ)æ`Ѽq}J—*ÉÐwÆqêL™™Y¬Z»‘·?˜LÎm¾ëT—­ÈÌÊbØû �ˆ$-=�ŸæýÉØ)ŸâV¨Pîk鉎‡„3zâ ¢cbINNaÆìï™9{E‹¾Å DDî "ò@+UÒ‹¹ßÍLôðµw¤q«§?õs:·÷gÖ´17Üf‹f ILJ¦‘ÿ“4nõ�›wðñĨQ­2#ßÂÖí{¨^·5Uëøóå·¿0cÊHìlmiÛ-wBõ¸Þ¤n�Gèóâ0ª>êO¯�oѽK;‹IãÿVïÑš<ûd'>šù5£b˜1e$åËúðT¯W¨\»%Ý{¼L½Çj2ê½×�ÜÉÚ_ÍœÀ™sÁøwêEÝæ]عûo¼ÚÿŽ_óêU+òBï§yûÃÉT{´ušt$5-�÷‡ fõºMŒŸú9NNŽÌþd§Ïž§YÛgx´i'“èùLWów¡8::0çË�HLJ¦Uç^T®Ý’!o�a`¿géñt—ÛZ³wI/¾™5‰}ŽÐÈÿIª=ÚŠ�?û†‰£†Ó¨Á£æóúlÚh–¯à±f]x¨~[æ-ü‹Ï?C•Jåïøu¹WL&“© ‹�û‹Ç¼Ü¡, =¼ ºAÏÇ�¸CPp(ƒ�j•+àææzÃm ~c$vv¶Ìúx AÁ¡$%¥P¹b9óü�ËRÓÒ :‚‹‹3å|˹s#L˜Ìs8 ÷{6¢câð)ã�‡»Û ×qá"Ñ1q”)]Ò<Éýß²³³9yúvvvùNž¿“¢cb �¸ˆOosm‰IÉrqÁhÌ!5-�B.Μ8uw77|ÊxóÚ°Ñ”ð*ÆÈw‡X´u6(„ôôtÊú”¶¸IÀí–“cäôÙ L&åËú˜ïRößkzêLööv”õ)�½½½Õ®é½N¿¯î.z>äNÑ$t‘K¼ŠÅ«xÑÛÖÞå`‘g's�Èeÿ )%K§d‰â7]ƒwI/¼K^ù̓��]ž:¬¥XÑ"+ZÄb™û¥Ðwôø):==€?Ì¢aý:@î7”¯ ØÌ§SGåi«B¹›§s³lmm®ù=)vvvT¯ZÉ*õˆˆÜ‹@DDä®óp�*<Ó½#}_Nã�a4Ù¹{? ëÖ¦�³‚.ODDn�ˆˆÈmÔ­s‹ïÞ�›c0˜6á}z<Ó•ƒ‡Ža°±áåzР^mll4}QDä^¦""rµÕ§ó·U�š5¨“Ï�¿DDäÞ¥�‘DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@DDDDDÄj@Dîs…ì ¤d› º‘+JÊÊýåfo(èRDäS¹Ïy;çþ˜‡§ º‘+ŠHËýUÒYoMDîwú)¹ÏUõ°`kTfA—""rE[/æþŽªvéw–ˆÜ¿@DîsíK;°è|FA—""rEçþŽºü;KDî_ "÷¹N>¸Ù¸�ÉÖ‹Y]ŽˆH›#3Ùp!w{Ë(€ˆÜï@ä¶s°É�@˜iÔ¤ç»AaÞ¨áÀë;IÈÔó""w�øL¯ïLà­‡ áé IèwƒŒKÃmô|Èí§"·Ýå;˜Äë�î]ãÕj.Ô*bÇ©ÄzlŒ×s#"w…øL=7Æs&)‡:Eì\Õ¹ K’Kâ2rÿN¸+Ê "·]9W[Î$åt)r‰³­�_›{PÊņͳð_Ëf Ç‘´92¿K¿‹J¹Ø0¯¹'N¶z³{·¸ü7üòßt‘ÛIDn»ZErï`²)Rw]º›”v±e]›"Ô.bÇé¤:®�£ëúx~<�ƉÄl}OˆˆÜQ)Ù&N$fóÃé4º¬�§ãºz>Öµ)B)½%¹›\þ^»ˆîJ&·ŸÁd2é]‡ÜVkÂ3y*0žÚEìØÐ®HA—#ÿ‘–câËãiÌ8šBb–~üE¤`¸Ûxë¡B ®ê¬ž�»PÓ±ŠËf±Ÿ'-K:t9rŸQ‘Û.Ãh¢ÂѤd™ØÓ¹(ÝÔ}{7ŠÏ4ñwh+B38™˜MXªQ½ "rDz3PÚņ*îvt(ãHÇ2Žšp~—:•˜C½¿cpµ7pöÉbæ›ËˆÜ. rG¼±3‰9§Óè^Ö‰9MÜ º¹N}7'°88ƒ•�™^Ï­ Ë‘û�\Êñî#…p¶3°è|:{c4ÙYDDä^°+:‹¿‚3p±3ðÎÃ… º¹O)€ÈáílÃ+U]0¶&—i,è’DDDä*b3Œ Úšˆ x­š %�õ6Qî ½²äŽyûa-jÏÙ¤únN$KDDDä®”e„¾›9—œC½bö {È¥ K’û˜ˆÜ1ÿþî‰ 2é²>Žè ¥‘»I\¦‘'ãÙ™IIg~jê¡;“É¥"wTIg>³ [/fÑjU»5'DDDä®°+:‹–+ãØp!“R.6üÞÂSßÉ"wœî‚%V‘f¤ÇÆöÆdaºù:òa-W*é½"""Vw*1‡ “ù+8P·¨=s›{hÞ‡X…ˆXMZމ�§2ûD*i—¾o¢V;:–q¤©—•Ümñt0à¨û�‹ˆˆÜ6Fñ™&N%æ°92“åaˆÍÀÅÎÀkÕ\ö�‹†]‰Õ(€ˆÕE¤ùèp ¿¥“¤oâ±:7{Ï”sâ�‡ ©×C¬ND L¦ÑĦÈ,–‡f°?6› ä³Ldõ’¹]l x8([È–Ú—G”°×7œK�Q«QŸ›ˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�ˆˆˆˆˆˆX�]A ""·.3;‡-‡O³÷d0 )é8;ÚS³Biü­†³ƒ}A—'""b¦""r�[¾ã0oõÁcó¬+âVˆI»Ò³Uƒ‚.€“¡‘¤¤gP»b CA—$""Vf0™L¦‚.BDDnÎü€Ý¼ôÉ/¯ñ«ü“ÁOñb§f].-ßšÎîçˆ_:Ûÿ·wßÑQT}Ç¿›ž�„�RHHè½Jï½wi"" HQQ¤7¥IUD± ¢ ¢JWzï½÷„JHïåý#¸’¡íRžÏ9³;3w~3»&ûì�{ÇBW‹ˆ¼hô›_Däx-œ÷¿øÍ>šT*ÁŽYƒ¹¹|'æŽå�Öµ�ëŽønWãÌ]²ˆˆˆ.ÁyV}ó×bâ¨^²¿ŒènìQðqwabÏ6\¾z“åÛ—˜Ä[Ò«E N^aÁ?{¨[¶0e ú2ë��\�ŒfFßöÆö·;Ç¢Mû ¸vG;[*ñ絕pr°ËPGjZK·dçñ \ ÃÎÚšÜ9]h\±8ÕK *6ž¿÷ŸäfT¬q»¥[Q ·;¥óûÛùsûaVí>ÆõˆhÜœ¨S¶íj–ÃÊRß—‰ˆ…¢~ž¤¥¥1`ö"æ¬Øš¡­E›÷ó颸ããÞ÷÷Ò¿·ó5뜺cߟ-YÏ0æõæ^ §ëÄ2,ïöÉ\z·¨Iéü>ÄÄ%Ðá£oÙ|øL†uü³‡ÙË6±ä£ÞÆã‘g›¾Ry%$%sòr¨ñqùÂ~w]¯\Á<Ì|·#3ßíH»šåîX>oíNŸ¿’á¹Vo7†�Òù}øø�–¼Ñ¸*V–‡EÐyüwÄ'&·ÿ7|ôÉEßVµx½Q²ÙÛ0ã÷¿ º�[ölôoW/×ìÆý¼ß®uÊ`蜥ÆðѰ|1ÆwoMÓJ%Øæ2ïÌüÅܧ\DDõ€ˆˆ<ƒnFÅòï".Ž8ÞúÀÿ ®GD3©gZV+�ƒ­ �Þs�3»#«&½k¼äÊ;gvÆÏ_Åù ë,ÛvˆŽuʳnß , VÒ�\.N@úì[3ýMjZgC©Uº½Ñ‚-GÎÀ˜×›ciaÁ�È~þ{•Šæeј·0 ôk[‡}˪]GùkÇN„RØ×Ãܧ^DD‘ˆˆÈ3èöÙke&ÛæUJñömƒÕ¯\ç|Ðu ½GcïéKÆeÿT²m,IDAT† €õNÒ±NyŽìa|.9%•—B8zñ í<œáù{Ùyü<‰É)òõ`ã¡ÓÆe¼Ý3ìSDDäÙ§""ò Êá” K RRS¹Kl|"v6Üοã>þ|#ÂøóŽcçi9üË»nw1ä¿{Žü¶q?­ÝÉ®Œ—f=ˆ Ûö9oíNæ­Ýy×õ.…„eµIyŠ)€ˆˆ<ƒl¬,)æçÅ‘ éã7ö�¹L�[3NÝnDZóL\°€:e Ñ¿]ý Ëÿv©Û{SŠäñ¤J±|wÝ¿o®ÌXô7£~øH¿d«yå’¼TØ�Ó¡ü°z{–Žåöœê% P0w®»®W¦ ¯™Î¶ˆˆw;oîS.""��z@DDžQ¹\œøªg^›øIÉ)¬Üu”•»ŽÞuÝQ¯7ÇßÓ-KíŽ|­)íÇ~K\bÕûM¡lA_â“9x6€”ÔTr¹8Ѭrú¹¥óû°ãØy®†GQ¤Û\²Ùx=<ÃeU·Bw¿m {ëQ³éR¿]V¦ÿËõ7%烮Sºç8Šùyq-"Š—B€ôi†+ËkîS.""��z@DDžaÍ*—dÙ¸¾Éãy×åÞnÙùz@Þk[7Ëm6ªPœ¯ú¿Š‹£± ‰l;zŽ}§/‘’šJÕâùùgZc�˜ÒëeãÏQ±ñ\»I�2éSîþëö±z4­†��5�>>åÈ­{� êÔ�AbceÉõˆh6>ÉK!XYZЮf9–�ë‹Å£L÷%""O CÚ¿É‹ˆÈ3+--�g8t.�ðèXœìí(îïM…"þw 4¿m;’Ï+'~wK`ëѳ݈ÀÅÑ�b~^w�7*6ž�‡NC‰¼¹y©PbâØ}ê"6VVT+‘߸þµð(ž $)9…‚¹sQÐ'W†e[�ž#<:�Δ)à‹·[vDDäù¡""""""&£K°DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd¬Ì]€¼¸S’Ør�µWvsèÆYbB‰LŒ!)5ÙÜ¥‰ˆˆ<7¬-¬p¶É†o6J» ±Oeª{”ÂÆÒÚÜ¥É Ê�–––fî"äÅ{ƒiGðÇÅMD'Å™»‘Ž£µ=mýkóA©ÎxÚ»š»yÁ(€ˆÉħ$2ýÈ/|srqÉ 0¿‰©êQ’|ιq±qÄÚBs"""�KRj2á‰Ñœ�¼Â¶ÐìØÉá°s¤‘†½•-oiÍ%;akicîRå¡"&{ƒn›>æà�30ÐÊ¿CJ¿F^'os—&""òÂ9ĤƒóX~i i¤QÖ­sk�Toˆ˜„ˆæ/øÔØõ~¿±¬Y½™V­’ŸÀÇcgÒ¼E}>úxƒ�K—‚èЮ66Ö@zÏÒˆS©R¥S§ÇÊÊŠøøxz½5”�?šÉ‚…39xðG�žâ�¥ß�¿€cFÊßë¶Ð¹s«»ËüùKiÙ²�À©“çèþæ‡;v†r/• 0 ˜I“‡Ò´imJ•.ÂGc?ãæÍHräp6çÛQDä™§‘ûHNN&2"ê®ÿÒÒ;8°²²b„Ù¸q'ï¾=Šk×n0lÄ;œ?{™à Pzõ¥%ööv¼Öµ-‡� 22ší;ö‘-›=:63î·zõ |0°'–·¶yPYÙoV5j\Ó$òæõ i³ÿšøçó!9%…ä¤$†íË÷s'·IHH$%5•ظ8Ž;Cxx$Ý{tÂ`H¿ÜËÏÏ›ú ªÛ¼x1�À€`ÞêÕ+«ôïËìììxýõv;vš°° ¤o»mÛ^cƘ±ïóýS3=ƒÎ_¸ÌÅ[½I…‹ägëöÅÆðàêêB“&µŒ� IŸA-þVý""òðÔ""rY„ž¿€o¼Ù�¯¿ú™i3FÇCЯßX,n[‘��HZZ7®ß$ðr0Þ¹=±¶¶6.7 ¼Þ­7oF>pÝYÙ¯³³c–Ú²wøoìÿ�ÁÁÁîŽçþemcÅŠëٵ㠗/qæÌEÒÒR)^¢�±6KK |}½2lçŸ×‡Í›vÝZ'= 0î®õ‡Ý¸Iù ¥èС9Ó§ÍáË/çñR¹’T¯Q�ÖmbooÇÝ Ô›!C&ѪEw ô§R¥2´hQŸ¢Å ×ñðÌ™á˜þÿøDDäá)€ˆˆ<&IIIlذ[[þ\ö�qð¹­½-�þͼ³Ó�ø½¼Ý±±µ!11ñ�÷wÛ¸…ÿ—•ý>)ú�ãÈátìÔ’f-êR ¿?þ¸˜'ÒÇnØØÚ�’’Jbbb† �˜�düÙÎ.ýù‘£Þ%‡Kö;öáãë‰Á``ĨwéÕ»3›7ïb×΃LŸö-¿ýö¿þö…ñr®Û•¯P’Õk~dïÞ#ìܱŸþÞÊ‚Ÿ—1}Æã½O,-®×IDDîO—`‰ˆ<&_Ìú‰ØØ8¾û~2Û¶í寿þ ¯Ÿ‰‰‰+^Ðø/66ž­[÷`ggGÞ¼>„pýúMc{É))¼Ü¶7«Vm4>—˜ô_H‰‹‹çü¹K™Ö“•ý> áá‘lÚ¸“÷¡Oß.T¯^O¯\„†^7®“?_úx�ŽeØöÀþÿûûß½þøø6oÞ…��;w`ö—ópÏåÆËíš2yê0¾œ=Žsg/qáüå;jKHHäÓßqêäy*W.Ëûý»³ü¯ï)R4?¯ÛöD·ˆˆd¤""/¬9ß.dxn¸wíÆMÖ®Ýr×Ç��Ò?8ÿ4w1cÇö§Té¢ôèÙ‘O&ÎæÚµ0rûxR¥J9>™8›û�“˜˜ÄáC':ä®_ n½j8:Ú3bØ®†Ïg3¾'àr0eJÅÙÙ{{;ÿ¾Šøøxb¢c?n‰IÉj5 •¥ý> ŽŽØÚÚpìØiRRRIHHä—ËÙ°~;a7nO¡Ây©T© “&~ÉñcgˆŽŠá§sèð ãåVžžîT¯^�ÉŸ|Í�ýÇÒë?|’!C>áÚÕôú£¢¢™ýå|–,^Ebbáá‘üó÷6lmmðññºãu¶µµaåŠ L›ö !!×HNNfÿ¾£±‹ˆÈ“¥K°Dä…u`ÿ1öí;Ìøû¬wêä¹ SÌÞ®]û¦ ô‹áçðr»&T¨XH¿™ßßo㣱Ÿñù¬±Lüd0C>œÄë]ûé÷&iÖ¬ | ggG>›9†¡C'Ó¤ñ븺fgÒ'âåíÀà!½™8q6U+µ E«4i\;C=U«½ÄÂ_þäÀþ£,]þí}÷û$XYY1xH>ùä+~]ø')))TªT–�ÇdìèOéÝk(?þ4ƒ�>@¿~céÔ1}°~É’Exÿý7ùõ×?�m�Ÿø!COâõ®Œõ7nR‹÷ ^½j´iÛ˜�ÆÎdÌèOpÏåÆ'S†ïëñÿ¯óGã>`ø°)4¬ßHŸF¹I“:¼Öµí;'""òCZÚ¿s¸ˆ<óÓgñ í²ÂÜ¥<–ã8Ýù7s—"Ï‘��k„ÝÇ;·..wNçšššÊ…óäñóÎ0( **†€ËA¸¹¹àáy÷1áá‘ØÛÙbkg›åý> ÑQ1“Ã5;ž·j��‰ÃÚÆ KbbbqÈfŸ~o“´4òåÏÃßÿÎÖ­»ï˜Å*4ä7îQxx$!ÁW±³·ÇÇÇÃ8kVf’““ ¸LBB^ÞdÏîd’s"/†B :ÏÏßÁgý8äé£òôt7~¿ ‹{^ ää”�bÅ Þswû€~¿ý> ŽNÙ2Ì,ï$O½:¯Ðï½7Œ=Q,^´’&MêÜÑ–‡§{¦�ëßc~�`eeeEÞ|¾&=""’NDDDLÎÎÎŽ÷ûwgÚÔoÙ¸q'ŽÙ²qèÐ1œ³;Ñ¥ks—'""O�ˆˆˆ˜Å«]ZS­Zyöî=L||/·oL¥Je±µµ1wi""ò)€ˆˆˆÙøçõÁ?¯�¹ËÒ4¼"""""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2 """""b2Væ.@äiWhAs— """òÜPˆˆˆ<° ˽¨°ÜËÜeˆˆÈ3H= "™í²ÂÜ%ˆ<µš/ïèÿypê“Q“Q“Q“Q“Q“Q“Q“Q“Q“Q“Q“Q1‘“§Ï‘˜˜dî2DDÌJDDDž W‚BxùÕ>ì=pÄÜ¥<©iitë=ˆà�«æ.EDĬ@DDä©àà`OÝÚUqws5w)""òY™»€ìÙ�)R(?¹ÜÝØºc/¥Kaóö=\¢L©¢T©XŽí»ösèÈ òøxѸA-,--ˆ��ãï Û %»³µªWÂÛËÃØþõa¬Z»‰¤ädÕ«AØÍp\]s�ûÖ:×®‡±mç^®]£D±BT®PƒÁ@RRoÜÎå€ ÜÝ]iX·ŽÙîz±±q¬Û°•�Ðëäöö A�êØÚÚ—§¤¦²jÝ&.]$_Þ<Ô¯] }("/ýÆ‘§BjJ*ÃÇNåFØM&MÿŠCÇsèè nFD0`èx�˜Èo¬ %%…é³¾ç‡ù‹HHH¤ûÛCXºb ‰IìÚ{�.=z €KWèúÖ@öì?LèÕëôé?’IÓ¿bϾCœ;‰NÝú±që.nÞŒ`ìÄ™ŒŸòEz]©©ôí?Š_-'..Ž¿VþC÷·“”tçXޏ¸xºõÄŠ5ˆ‹‹ãç_—ñÞ‡eXgÜ'Ÿ³sÏ’’’™<ãk¾ún�¹O½ˆˆI©DDDžZ^*Å[o¼À•+!DÇÄ2{ÆÇØÙÙräØ)Î_ ÀÂÒ‚¯?‡µµ5�ÞèÇ¡#'ðôpç›ï¡bùÒŒúû¥ï€QÆý|þõ�4oR—þo¿ À«[ѹûû´jV'GGŽ?źe?áääHllCFOæêµäööÌPï‘㧸v“…sgbaaAPp(§Ï&:&{*–/Mïî¯�3§+kÿÙ t1÷©1õ€ˆˆÈS«T‰¢ÆŸ]]](]¢ˆñq—ì$''P´p~~ž3ƒä”NŸ½ÀÒ¿ÖríZI·–ï?tŒÆõk·-W¦î9ÓÇš¤¦¥±ÿÐ1r{y°gßaöì;LPðU¼dsÈ8Ä×NjٟŽã÷%+>v*ñ 4®_‹�ïõ4Öî˜-""/2yæÍÿu)QQ1üùûìlmضkäÊ•“€À`Š)@rr ¡W¯àä˜ ÇltîÐ’ åJÛ¡\éâ4¨WÃܧIDä© K°DDä™—˜˜„£c6lmÒg›Z¿i.\&>>€†ukðÓÂ?¸AZZ?.XLLL¬q–«†ukðÝ�¿ À¶�ûx£Ï ¸xù }ú�äÂ¥|r{bee…ƒ}ú˜ŽÍÛv³bÍ=I¿Ac¹v= ƒÁ€onO,,,°··3÷)yj¨DDDžy/·jÌ»ÇЪÓ[ J•(«[óÙì¹*�—n¯¾Ìù‹4oß{;jU¯DÅò¥qtL¿ªO�Wùè“Ïiòòdwv"%%•±ÃÞ'§›+n®9hX·]z ÀÍÕ…›á‘T«TŽMê°vý®…ЬQê×®ÊڶЦs/\s¤¯Û¤a-ªW)oîS$"òÔ0¤¥¥¥™»y¾xÌo@h—æ.EDž�æzð×osÌ]ŠQrr W‚pÏéfôŸ�€�­-×o„aooG\\ÙïKBB¯³nÝÖGng×®$%'Có'Z®Q«V x­kÛ Ï………Ó¦UOæ|û+³¾üÈ4…žõú¸ººP¼xaƒ2<æôV­ÚHtT4ùòûÓ¢E=²9:—§¤¤²rå<Ž£S6š7¯G||‘QÑT©RŽë×o²qÃ𷍋���q»•+6P° ? å5>·mÛ^¶nÙiP¦\qêÕ¯†•¥¥qù•ÀV¬XÏÕÐäòt£UËxxºÌú¶§·»jÕª–§Xñ‚ìÜy€Í›v‘š’Jé²ÅhР:VVÿý‹ŒŒfé’5\¼ˆ··m_nòÀçîÈ‘“DEÅ�/_ý¶’ððH*V.CÆ58rä$+Wl ^½ê”¯PÒ¸]bbkVoâøñ3¤¥¦áŸ/-š×Íp~ãããùcÉZΜ¹€¯¯:4gë¶½.”ÿ¼>Æ×`ÍšÍùd6‹~[™~—ƒùhìgDEÅfØß§3¾cçÎÆÇ“&|ÉÛ}FrþÜe®\ eİ)¼Ý{ÉÉÉ>t‚V-{°~ývbcãX¾tmÛô"$ø*!!×Ù±c?[·ìæÜ¹KL�ú ½zåÌé _cÔÈéôzk‰‰I\»Fûv}™ýÕ|®]½ÁÚµ[èÔñbbbyÖïàÓßÓý�A\»Æù —8`C>œÈ€÷Ç‘˜�ľ}GéÑýCN?k4‰ØØxãs�Á ò áá‘܋ࣱŸ1øÃIL›ö-W¯†±jÕ&Ú·ëË–-»�Û¬^½‰íú²eëÂÂ"øæ«´k×—à Ð‡®MD^\ê‘ææ–ƒQ£ßËð\DDß}»�r/• gÎYjçòåôñ >>ÞYÞ÷öûéÕûUzõ…þù›6ì仦R¶\16oÞÅ»o�¦a£šÔª]™uk·ŸÀêåspºõ�í7_/`ÁÏË:ìmš4©�……ƒMä›9“²Ôîÿ›>c$]:¿G¹—J2àƒÆçO�<ÇܧÛùü³¹ÌŸÿ‡qù¸�gâãëÍ·s&aooGrJ ƒNd̨¬^ûS†žƒÿwêÔyÖ®Ýb|Ãúõ;ˆ‰‰£ÿ€î@zÏÀGc?¥ó«­8è- 7oFÒù•wùtÆwLúd+Wn`Ë–ÝÌœ5†ÚµÓáÒ¥k5b: ÔÈòë³w÷a,XÆô#©ß :G�žâõ×>`Ñ¢ÕtêÔœ_þ™Þ‹µàS 1ѱ´lÑ�-[÷о}3†�x‡Nßa„Áäöñäà�cü4w1“>BÓfuŒç´Ë«ïóÛ¯Ñåµ6ÌúüG¢¢¢ùeá,üýs“––Æ„q³øõ׿è½ púÔy~^ðÅK"--�¶­ßbó–=,]ú ¹�Þ½‡óñGŸ³jõ�ÄÅÆ1v̧tèØœÁCû`0�¤Kç÷˜>mS¦ àºDäŦy$iii¬Zµ‘vmû�˜˜Ä¤Iƒ³¼m||"NNYÞÆÙÙ‘ž½^1ζµmËn*W)güpP³f%ŠÍÏ–Í{hÛ¶k×Í7†�””Tâ3|+üÿ²ÒnV+^0C;…‹æ'..}ß±±ñìÛ{”7ßl�½}úeMV––tïÑ‘«¡×9sêÂ=Û^¾lŒ3þ=j›6î¤I“ÚÆËzöï?B||"½ût1^>–#‡3;6O¿L ؾueÊ3†€V­>ðLZ[¶î¡`¡¼ÆðP¢DaªU+϶[ߨ,,¸zõGŽœ ›£ÿlø…öí›ÝµÍ­[÷âŸ×Ç> ÉO­Z•ŒõïÙsˆV­àïŸ;}}Þîšéår÷’'�ÅK2¶ãçïËK/•$—GNìðòt'66?ÿÜlݾ˜òþ»Œ0.>}YÌ­u¶mÝC“&uð½u> Ý{tÌxœ[öP·nUcøhÚ,}›­[wó0\]³S¾B)Ö­Ùl|nåŠ 4iZ;Ãåk]^kƒ³³#¶v¶ôêý*!ÁW¹t)�ýŽGï¾ÿ½\\œéØ©[¶î!--í¡j‘—z@Dä¡3zÔ4:I§WZÒ÷í®88Øey{—[ãD‚‚Bquu¹ë:{wÆ-gòæó —GÎ =—/sábM›t˰Ýõk7ñòòÒ?PíÜ´‹ ëwpéR gN_ !!ƒ!óï`²ÒnVyx¸gx|ûgâÀ€ ôoë'|ÉԩߟONN¿tçú�°{¶=èÃ^¡'''³rÅF ŸJÑ¢hÚ¬—/¦�éØáí ÛÆÆÄMbb�Á(àÿuÈŸ/ÏëåËW¸|Çy 'oÞô×ð­^�9qâ,]:¿‡�¯*”¦q“Z™Žu¸DHðõ;Ú¼y3‚ܹ=HII%$è*þoÌ‚«köLßW÷â�-c 6 [6{îxò+++B‚¯òÍ7söôEÎ_àæÿÍàDƒ†{’òøåÎ�.\áÆõ›w絫7¸~Ÿ©vï¥QãšLþäkâãã¹p>�óç/3ñÿ¾(ÈŸß?Ããß W®„p9ƒÁ@çWúeX'6&ŽØ˜8âãŒáYD$+@Dä¡?v†îÝ?¤P¡¼,[>ç¡L—,Q€£GNSâÖÏ· ¹Æ›obðÐ>Æbcm�a[;*U,Ã[½:ß±ý¿dg9�ïæüJûÍxõÕÖäÏïÇ‘#'÷qæ×Ög¥Ý¬²¼Ç½Qlo æîׯÛxà+++Z¶jÀìÙó9rø$M›ÕÁÖÞ¦N½û¥2–––X[[wDz¸¸lllî¹Ï{sìlm)\$C‡ö½Ë±Ú�'�7‹ÏæèÑSlÛº—Í›vóÇ’Õ¼ÓïuÞzëÎómkkKþy9²ß]–Ù`ii�½ƒÝ]{´ââ3ïåz\öì>D¯žC©U§2M›Õ¡@²9f£E³7ŒëØØØf8OI‰Izlmm©Y³Ò³šÆÞ‰‡Q·^uÆ<‹m[÷qàÀ1òð£h±Œ÷ILJÈxÞnÕjgk‹�� VV–L™2ì®=JÖ6Öˆˆ<]‚%",55•AÇS«fE¾ûaÊCÏÖäãëEù ¥øá‡ß‰ŠŠ¹cùKÖP¹RÙLÛÈ›×—À+!-V€bÅ R¬xAŠ+ÀæÍ»Œ7h[¶ìo:½Ò’ÁCúP¿AuòæóåêÕ÷¬-+í>¹sçÂÆÆš¨èã~Š/ˆµµëÖmÁîÖ‡öåääÈÍðˆ[ÇâC||<¶v¶ör•ÃGNaii�ŸŸ/ûö5ꈎŠáø‰3w´�”˜hü90 ˜ˆˆ(ãcÿ¼>] !¿ ûÚ¿ÿ(W®ðÝœ_Ù¼y7%K¡wŸ.,X8“-ë³níݧAöÏëCpP(ùòûfhóà¡ãÆ™¾üý}Ù¹ã@†íŽ=ElLOÚŸËÿ¡@¡¼Ìøt-[5 Xñ‚„ß Ï°Nþüy8°?ã}Y:žáq¾¼¾‡f8ÆBEò±~ýö çøAåÈáL¥ÊeY½j#«Vm¤åm³³ýkßÞ£ïܹƒÁ€¿¿þþ¾$&&amm�¡¶Ð«×8tèÄ=Ç(‰ˆÜ�ˆˆ<°ƒŽŒ—·K—¬aÑï+3ü‹¿õ­óœoÞõnÝ·>ü¢¢¢éöúlݺ‡¨¨‚®„0÷‡ß™óíB6¬qÇt ·kÓ¶çÎ^dê”o ‹ **†Ï>ýžïæüŠÛ­Áð®9²söÌEâããIIIeóæ]üðýo$'%zk(ƒÁ@jj*çÎ]&>>>KíÞ•ÁÀ•ÀÂÂ"²t.­¬¬hݺ_Ïþ™Í›w‘��ȹ³—:t2§NœË0U±¶2¥r/•$o>_F ŸÂéSHLLbëÖ= >•Äøôo¾[¶¬ÏÕ«×5b*!ÁW  eø°©ÄDÿ7‹”§§ þErJ ׯßdÌèO�=Í›×#**–1£fr�¸¸xæý´„iS¿ÁÉÑ €ýûŽ2q—œ;{‰””TΞ¹ÈÑ£§ÈŸ?�ñµ8á2±±ñ4mV‡øøF�˜NHÈ5âããùeÁr¦|òŽé½Qm_nÄ–-»ùæë„…Epêä9F�š�µõ“ÿvÞÕ-;×®Þ0Î(vúÔÆ�ÿH¿„)--�.¯·eÇŽýÌýáw¢£b8yâ,S?ùkãñ¾Ü¾ ç‹Y?Exx$“'Ífþü?È•Ëí‘jlÔ¸&ëÖm%ìÆMš5«{ÇòŸ~Z̺u[ˆŠŠaÓÆ�|þÙ\jÔ¨ˆ{.7Ê”-Nþ~Œ9•ӧΓ˜˜Ä¶m{>lê½:""Y¡K°Dä�]¼¤“}7µjWÆÎÎŽû�±oßaîuñüü˜;w*ãÆ}NßÞ#ŒÏ[YZÒ²uCé}ÏZJ–,ÂÇãòÉÄÙÌûi �>æbêôäÉ“>»Ö€z2ðƒ�©Z©- üüsóñ¸øhìLš7{“=ûþ¤x±‚¸¸8Ó¦UOÆOD‹–õïÛîÝT«VžÙ_ÎcïÞClÚò{–ÎçÀ{Í»o�6^’S½ZyÆ?À€þÿçŸ×—5«7qîì%òðcæçñá ñ´{9ý|Z[[óÊ«-éÒµ eËgè°·™>m+Wn4KÍZ•Œmzy{гç+|ÿý¯,øy)©©iôîû*É)ÉÆurûx2ý³‘Œ9�õ»é–ÇŒíoˆ?èÃ^ô,mZ¿eÜ®BÅÒ ú°~~¹ññõâí>#éÓ÷5úôí§3Ç0rø4ÞjÓÙÙ‘‘£úïÓöå&\ºÄW_ÎgÖç?baaAÇNÍÉåþhܳ⵮mÙºu ëwÁÎÎ{{êÅ¢E+m_õ³ñ|Îü| î�@êÔ­ÊGcgR±biã`úÛ½þz;F�šAô­žÈòJòѸXZZ0óó±|8hí^îc|ÿtìÔœno´{âçWDž?†4M_!�™Çüô™lB»¬0w)�å8NwþÍÜ¥¼0‚ƒB¹rå*––,èÿ@c-’’’8wövvvøøzf˜á !>�K—®`cccœ*11‰ääãÀù¤¤$¢¢bÉ‘ÃÙø­ôýÚ½›ÈÈhll¬2ܬ/+nܸIHð5<<ݳ<•ñƒº|9ˆØ˜X|}½3Ü$ï_qqñœ?wçìNøúz1xÐD’“S˜6ã¿pNpÐU¼s{fzÃÉ””TΟ»„Á` �_î;zrRSS ºJDDÔ]�7%%•ˆˆ(²gwÂÒÒÂøÜ…ó—IKKÃÏß箽CQá‘+ç#h©©©\¾LB|<ù øaeeEjj*Ñѱ8;;�Á�~óÍ+�!øù寯ֆÊ[3oþ ã¬[�¾Îùs—qÈæ@n�Çr‰ÓÍ›‘4¨×™±ÈÐréR-š½Á¢Å³Éãç͹³ÿ½öwLLt >>^º á=Z�~'úçåïà³~òôQˆˆ<5¼¼=ðò~°¦þemmM‘¢2]nkgK¡Âù2¶¶6�¾ƒL\¼x…Þ½†J¿÷ÞÐt¹"òTP‘L5iV›ÔT]©û¨Üs¹±tÙ·lÞ´‹ +¡Ô®[…R¥Šâççýè�ßC®\ntëÖ//÷ ãyþ•#‡3ï÷ïNNwWsŸ"y�(€ˆˆH¦n¿3º<ë wˆ7;:½Ò"ÓåÎÎŽ¼Ù½ƒ¹O�ˆ¼`4 ¯ˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œ•¹ yÚZÐÁÜ%ˆˆˆˆ<7Ô"""¬Âr/*,÷2w""ò RˆH&B»¬0w "O­æË{úÿDDDœz@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDž©©©œ<}ŽððHs—""òÄ(€ˆˆÈSc÷¾C„\½nî2LfÉŸkhßõmããac¦ðþ��Ùè¿-YAç7ß3k}‡OàÓ/¾ÏÒºÇOžáÜùKf­WDž ""òÔ˜õÍO:rÂÜe˜Œ¿onêÖ¬j|¼kß!&�ý�ºµª�×ß—Ú5*›µ¾Š/•¦xÑBYZ÷÷?V²výV³Ö+"Ï+s ""°gßa¢£c9vâ4ùü}‰ˆŒÂÝÍ¿<¹�ë8|g'G³9roOVÿ½ ƒÁ@ÕJ/‘Ïß׸nRr2[·ïáìùKärw£~�jdspÈtÿIIIü½q;—‚pww¥aÝ8fs !!‘{P¹BV®ÝHxx$¥Kå¥2%2l¿ïàQŽ;…�µ5ukWÅ3WN㲘ØX6mÝMPp(yý}©S³ >¹½HLJ`ãÖ]$%%qäØ)ÜÝ\ñÍíiÿµŸ˜˜Ä¦­»¸po/Ô­ŽµÕÝÿŒ_ bó¶ÝÄÇ'PºDQ*¼Tê¾µ\½vƒ+Á¡øæöbÙŠu´jÖ€¢E à˜ÍÁx|~¾¹9{î"‡��Ä'·õjUÅÖÖ†ã§Îz�ظx>NÙRňOHàï Û¸ЧGNÖ­�½½�¹ßf"òPˆˆˆ<Ž�8Mllç/\ær`ÛwícÖ7?—'&&1hø®]ãȱS|2ã+º¿=˜ó.³ÿàQ^ï5�­;ö�’’Bßþ£˜=çg""£X²| ]ßÈ�°ð»î;55•¾ýGñË¢åÄÅÅñ×Êèþö`’’’ˆˆŒbÔ¸éôí?Š­Û÷Dÿ!3çÇ_�Ûöå =™Ð«×Ø{à0¯¼ÑÏØ“Ëë½±è�•DDFñùW?2ò£i=~š™³`ÿÁ£¤¦¦qìÄ®^»ÁÁÃÇ�ÇŸœœBï÷†óý¼ß‰ˆˆbþÂ?è7p ©©©wË�ÃÇy­çΞ¿DXX8CFOæ×Åe©–/¾ù‰ÃƱmç>YøûŸ¬ß´€¯¿_ÀÈ�§ñéì¸Îw?þÊ[ý†‘��È… —¹v“à�«œ:sž¤¤$z¼=„%Ë×Çâe«éõþpRÓÒQˆˆˆ<ºuiÇú-;hÑ´>õjUÅËÃ�Þï� 66{¶îØ‹��_*ÅúM;¸x)� £Q·VfÏù™ÏfÏ¥Zåò¬\»‘›áÌŸ3;[[RSSyûƒÑüôËú¿ýæû¾Ä‘ã§X·ì'œœ‰��cÈèÉ\½vkkk“(U¼0ïßÚ¶~�ê õ -›Ö'&6–%®á§¯§{k¾øvŸ}ùßÏžÌÂEâì˜�ogMÄÒÒ’W;´¢mçÞ\ ÍPÀwº³ô¯µ¼Ù¥=… æeõºMÆe+Ö¬çfD$¿üðv¶¶DEÇкS/;I™’Å2´³öŸÍÔ­Y…ÑCÓÇ�”,Q„Óg/Ü·–£ÇO3mÂpªU~鮯Ñõ°›Ìûvº±†WÞx�å+ÿ¦}›¦ì=p„\î9éôrsŽ?Í…K�l\¹kkkÂn†3z§„……“Ó-‡¹ßj"bf ""òT*Z¸9Ý\Ùºs/ ëÖ`Í?›i\¿&é�÷NŽÙ¨Só¿1­›7àÇ‹ »Á¾ƒG)˜ßŸ#GO—çõóåȱôÇkþÞL\|<%‹ÆÝÝ�l|GRRû¥~�êXZZàéáÎ’_‘Ã%;§Èšý�R»Feìlm�ǾpîL²9Øß±®ono~˜ÿ;?Ìÿ�šÕ*Ѹ~Mš4¨pßZrºåÈ4|4¬[#C õkWåÐÑ´oÓ4Ãz¹Üݰ´´`ÌÄ™´jVŸ’Å óù”1æx‰ÈSHDDDžJƒ�zµª²~ÓªT(Ëö]û™;{²qyöìÎ ãã9²AXX87Ân²ô¯µÚ,U¼0�'22€\î9ÉŸÏ�o>ŸÀÂE2aêDDDQ³z%† ìcÜÖõVûÆý¹dçfD$aaáÄÄÄÞ±¯:5«�œœÂÍð²gwʰÌÓÃý�ÎEXxyýódxÎ=§ë]×íÔ®9ŽŽ¬þ{3ßÿô;..ÎôéÑ…¦ kß·'GÇ{ÖáââœñäÈΙsïX/—»ßÌœÀ¯‹ÿbôøO‰‰�¥^­ª ÐÛ`DäÅ¥"""O­zµ«Òû½¬X»‘¼þ>äÏçg\víú âŒh/\ ÀÃÃOOwr{{0¸ïÛÖ#)) Ãó×o„ËðAéSâž8uŽMeÉò54ªWH¿L+‡KzILL"(8/wl­­ÉîìÄøÑ�íEEÇpõêuìííðôp'èÿ.·úé—%Ô¬Z1ËçÁË#×m,^¶šÂóQ¢XÆYªŽŸñ ,ZºŠñS¾ N�Ê÷­å¶`[DDžnê‘§Rí•Ùºî÷;.u¨U£U*•5I9sº²nÙO¤¿ˆ>�4kk}l‘G§ß$""òÔº[ø€ôK«þ\ëÁ`ÈÒø‘ç�ƒÃ‹ÀDäñÑ%X"""""b2 """""b2 """""b2 """""b2„.""™7å ®ß»ãyß[÷˜xÈÇw,ËéæÊˆAo›»tyJ)€ˆˆH¦r{y°sÏ�Ú¦tÉ¢æ.[DDžbºKDD2õ07¬U­’¹Ë‘§˜ˆˆˆd*Þ<äññÎòú>¹=Éçïkî²EDä)¦"""÷T£j…,¯[»zes—+""O9¹§Ú5*c0²´îƒ„y1)€ˆˆÈ=yy¸S Ÿß}×+T /¹½<Ì]®ˆˆ<å@DDä¾jU¿ÿÀòZ1`]DD^< ""r_5«VÄÂ"ó?ƒ�êUÊ›»Ly(€ˆˆÈ}¹æÈNÉb…2]^ªxaÜ\s˜»Ly(€ˆˆH–Ô¼Çý=jêÞ""’E ""’%Õ*¿„µµÕÏ[YYQµR9s—'""ÏÉÇl”+]âŽçË—-�“c6s—'""Ïɲ»ÍtUK—_‰ˆÈP‘,«T¾ vv¶ÆÇv¶¶Tx©”¹Ë‘gˆˆˆˆd™­­ •+”5>®R±,v¶¶�Тˆˆ¼h@DDä�Ü~VVnP(""r;«GoBDD^$åJ—À%»©©i”)UÌÜ刈È3FDDDˆ¥¥U*¾„ÁV––æ.GDDž1 ""òÀjU¯ˆƒ¹Ë‘g�ˆˆˆ<°âE š»yF)€ˆˆÈ3Ôû!""G³`‰ˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉh ˆÈ *<1Š•—w°&pg# нNlr¼¹Ë‘甃•Þ9)àìC#ßJ4õ­‚‹�“¹Ë3PyÁÄ%'ðÕ‰?øâøb¢’bÍ]Žˆ¼ b“ã9ÈÙÈ@VîdÔÞoy§x;zmƒ�¥�¹ËRy�\‰½Æë?æHØ9j{•£¥_u*å*Ž·CN¬ìÌ]¢ˆ<§b“ã Š½Î®«ÇXvi ›‚0ñàO¬¸¼�k�ÂÛ!§¹KQyA\‰½FÓU‰ £ ³Ó*÷£R®âæ.KD^Vvpö¡€³¯hÄÎ«Çø`çL‡�£Éªþ¬j2C!Dä¡Aè"/€¸ä^ßø1!qaTõ(ÉŠÆÓ>DĬ*ç*ÎÊÆÓ©êQ’�¸0ºmü˜ø”Ds—%"& "òøêÄ ;GAgæÖIv›læ.ID„ì6Ùø¡Öò;çæPØY¾>±ÔÜ%‰ˆ (€ˆ<ç£øâøb¦Uî§ð!"OG¦Vz€YÇžmî’Dä Syέ ØATR,µ½Êé²+y*Uõ(IM¯2D&Ű*`‡¹Ë‘'LDä9·&`­üj˜»‘LµÌ“þ;jmà.s—""O˜ˆÈsîLds3w)""™ªìQ€Óæ.EDž0‘ç\Hl^næ.ED$SÞ·~GÇÞ0w)"ò„)€ˆ<çb’ãÈfeoîRDD2õïï¨g‰ÈóKDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLFDDDDDDLÆÊ܈ˆ< ‹–®bý¦í™.ï×§9Ýr°kï!J+L_ïǶïOf|Eá‚ùhݼá]—?y†Y_ÿÄà½ñóÍmîSõH¶îØËÍðZ4©gîRî)**š§ÏCá‚ùÈííiî’2µpÑŸ„^½Î{}ß`ÏþÃÌûe 3§Œ1wYlÚº‹è˜Xš5ªcîRDä¤y.�8u–µë·Þs�“§ÏÓ·ÿH¶îØûX÷½mÇ>Nž>—éò«×ÃX±fá‘æ>M�$&&–‘ã¦S¡\)s—rOËWþMÅÚ­yå�÷xwÐXª5hÏ INI1wiwuìÄivî9@PP(+×n2É>ûöIbbR–Ö/Wº8“g|Íõaæbcãðôt§Mó†./JKKcåÚ�ìܽ''G^i×’+Á¡T®PƸÞÎ=Y·~ ÉÉ)”)U”æ�ëbm�ùÑ䔿ýòŸM•áù�Ðkü¶d!¡×(T0/_nÎêu›¨T¡,Þž¹ˆŽŽá·?VræÜ<ÜsÒ¦e£ —¢ÅÄÆ±xé*Nž>‡ƒƒ= êV§RùôZ7nÝERR êT 55•_ÿ…——µ«W 11‰EKWñRÙ\¼HBB"#‡¼‹½]úkؤamÖ«ÉÚõ[²@‚B®²qóNÕ¯�›kŽ ¯ÛÆ-»hT¿&‘QÑ;qššÕ*2ï—? ¦xÑB¼Ò®%—ƒX¸èOââã©\¡ MþwÙRJJ*®ú›}Ž`ooO› ï[϶�{ ¼Bëæ ±µµÉòûlãÖ]lÚ²“´4¨ðR)Õ¯‰•¥%§Ï^`ã–]ü¶xU+¿DÁüþlؼƒM[wceeIÅò¥iX·†±½¶-ѲÓ[ ìדlÙð]/"/2]‚%"/¬ÈÈ(†Ž™Ìå€ V¬ÝÀ—ßΧ]—¾9v’„ÄD>™ñoôdÜæÐÑ4hù+×n$.>ž}ŽÒ±Û»,\ôçC×qâÔ9FOø”W»¿ÏÁ#Ç‰ŽŽeü”Yôx{m;÷!ôÚu._¾Âûƒ?fþ¯ËŒÛMŸõoôÄÉÓ爉�eá¢?iܦ�W‚�ë 1‘~ƒÆp%„ÃGOÒ¼}w¦|ö5‹—­2®3íó9têö.GOœ&8ô*ÃÆL¡c·~ÄÅÇgZóÞýGˆŽ�ÍpùÕ©3çiت+?ý²„ëa7ùþ§ßióJ/F�ÿ”Óg/é=M_~“Ùsæͪu›¨ß¢  ìfÍ۽ɔϾáÆÍpö:F‡®ï0sö\:Æ€!ãHII5îsÈèÉ õ‰±Žý‡Ž1tÌdbcãpp°çõW_6†ŸYYY’œœœ¥×Ç1›£'Ì`ɲ5žÿqÁ¦Îü–ìÎŽì?x”qŸÌ¢C×w8}ö7ÂÂñÑ4z¿7œ]ßæfx§Ï^ Ïû#Y¾òo =öê7Œ÷Ìés9~ò ­_éÅî}‡3­eÉò5t}k 66Ö>FŽ›Î½qæÜE®ñþà�x£÷ ’““¹p)�Ýû°êïM\º|€1>£ÇÛC¸pñ2.Ð罌?ÃØfþ|~¸åpaãÖ]󶑘z@Dä¹ÇÐ1“ïx¾~íêÔ«]õ®Ûlßµ�Ï§Ž¡eÓú¼T¦CFO&ìf®9²óËïR0¿?Ë�ƒ¥eúw8}ÞÉ_«×Ó©]‹‡®5**šVÍêÓíÕv88سpÑŸüôÍ4jÝúf¿]—¾lݾ›®¯´!--�^Ìû}ߤ_Ÿn@zÏA…Z­Ø´m7¯vhÅÖ{ùý�•|3s�ê×`ÞÂ?ñÑ4òùçÒ¯ýÿü«™4öCcýgÏ_¤e‡ž|÷Óo¼óV×»Ö»mç^Ê”,š¡7fÔ¸é¸çtcÑü/Èá’�ÄÄ$^ï5�§þóÉŒ¯IJJbõ?âæêBjj*¯¼ùÓg}Ǽo§óÙ—?píÆMV.úÞ81ÀôYß1ã‹ïiÙ¬>ujUaÆßsøØIÊ–*ÆÎ=psu!(ä*/âŸÇ‡»÷ãæêBé[õÕ¨Z!Cí{÷aõß›øàÝYzmœ�©S³ +Ön ç�€ôð°|åß´mÕ+«ô?¥Á¡WòAoãä½ÞÎÚ¶°dÁW”-UŒ´´4¶êÊ–í{hÙ´>+×nd݆­L›0œv­›°æŸ-¼õîPªVzéŽ:–,_Ç#'1m°L'8Èì=ýÓ‚%Þ�§]—·Y¸ø/ºtl��µÝzâ‡Ù“±·³cß�#ü0ÿwfNM«æ X¼l5†Ž£O÷WñºèÊ–.ζ{5]Dˆz@D乕–QwüKHLÈt›\înÆðP´Hâââ9ø]~Ÿ÷¥1|$&&‘’’LLlÜ#×Û²iãÏòú‘Ã%»1|ÈïO\üµo^ók†Kˆbbb±°° 6&½–�[wR Ÿ¿ñC'À+í[’Ã%»ññÆ-»ðÌåž!<ÈçO“†µÙ¸yg¦µž9w‘<>ÿÍŸÀ®½‡èõæ+Æöml¬y§×k¶Û´u�ÚµÀÍÕ † ìKýÚÕ€ôËm[6Ê0+Yïî¯biiÉÎÝ(Y¬09Ý\Ù¾s»ö¢]릸¹º°s÷vìÚOí•3„#HhSg~K§7úÑ Nuztë”åצU³<|œ �«ì=p„ àP:´if\ÇÊÒ2ÃñyýÈããMÙRÅ0 äÏçGü­×pÇ®ýäÏçg �êÕ dñ;Ç$-Y¾†�ÃÇÓãõ>6lÞIÑÂ2¼Ê”,FÍj2}�7nÝ…On/Z6ûïÿ…æ�ë2d@’’þë9ò÷ó1ön‰ˆd•z@Dä¹u¿Aèwãíé‘á±Á`ÈðØÖÆš?þ\˶�û¸x)€“§Ï“š–Jñ¢…K½·sø¿Ç·—b0HHHäÇŸsäØ)Î_ àRÀÒRS�ë\¾|…|þ¾Ú°²´Ì0Þââå@ÂÂéѨC†õÂÃ#qÍá’i­×¯‡‘ÿV/ ÀåÀ ÒÒÒ(T0o†õ Ègü9.>ž«×nàï—±¦Ò%ŠRºDQƒéÒ1O†åövx{æ" 0 jרÄÖ{éÛ³ »÷¢cÛf\ aÇî´jÞ€‡�ñÚ+m2´±~Óv†Ž™BZZÇ ¢]ë&w¼¶÷R¯VU²es`ÕÚ�tïÚ�å+ÖQ¶T1 øïx­­­3Œ›1 wŒ�0ðß>/ÇZd;;[êԬ€¡ã8vâ á”/[’+Á¡Ìœ=—½û��šš–¡çhÎ�¿2nò,Þ|­=ƒÞË8ýAØÙÙÒ¸~MV®ÝÀ믾̊5Øï­Gz ��‰ŒŠ¾ãùÿ?~ƒÁÀw_|B_oê·è”Ͼḛ¬Ï&>ÞžX[[³eûž ëíØ½ßø³••yòäfï�#ÖY´tM_~“´´4òùû²kïÁ ËÏ�¿ÄµëaäÏ—Þ3R³ZER’S˜õÍO+R''GªVz‰Ð«×ùù·e”/Wg'G }�ú¸É³˜:~£†ô{¨ðñ¯–Më³ÿà1/]Ell<-›>ÚÍóúû¦Ï¼c|.11‰}ÿw~,--©R©¹½=ðN~úe1‡�žÌò~òùç!ðJ0… æËðïÚ{0Ä·ËŸ7'NŸ#&&Öø\LL,µ›tbÓmƒÎo†Gà‘+ç#�yñ(€ˆˆd‘ƒƒ=övv9vŠ””T™·ðV®ÝÀ�7Ë8�¬rsÍAjj*Gn}½zí O\|<�A!郻۷$!!�!£&zõ:Á!WùpäD ƒñÒšæ�ëba0ÐÈ8¯ŸÀ/¿/gÔ¸éØÛgþa½LÉbœ:sÞøØÊÊŠÖÍðý¼ßY¼l5‘QlÙ¾‡ÉŸ~“a»WÚµ`ñ²Õ,ZºŠ˜Ø8Nœ:ǧ_~Oñ"1 tjׂ�[vòͿ͙s0t<¹rR§f ½çॲ%Y½n•+¤OœÏß/�\¬^·‰º·Öƒô±öv$&&±à·åþ­^—~ƒ¿´´4úöÉ·sÞóœW«R×.|!�~ŽåRÀBB¯1xÔ$n†g~ƒÊ7º¶§P�| 3Ù8Øð±SùdÆW÷ÜODdƒGM"$ô±qñ|;w!¦~Avg'à¿K OŸ½@\|<-›5 %9™A#&r•ȨhÆNšIØÍJ•(blûäés”.Yô‘΃ˆ¼xÔo*"’EV––|4¢?#ÇMççß–’’’Bå e™1q$ŽšÄ«o¾ÇÒ…ß<ú޲ NÍ*4kT‡ŽÝÒïo‘’šB÷®èÙ­ßÎ]ˆ·W.z½Ù™Ï¦Œfà° ,ýk-¼öJÖ­�kŽô�â9Ý\™óÅ$ Oµí�ô›Ì�ú>µkTÎtÿÕ«”gü”/ˆ‹�7ö*ŒÒ�k×Ã0t�>f`Ð{o1nò,ãvÝ»v$8ä*ƒGNâƒaã¨[«*#‡¼ ÀË­s90ˆ)Ÿ}Ãø)_éÓ½þ;;ÓíÇ¿kïA*Ýv/“ª•˱xÙjêÖúo†³ /{÷ÙÐJ/LãµHKKcÅš ¤¥ÝÿõoÞ¸.s^D‡¶Íù5ÌëçÃÌÉ£2z25u \™ô~³3ûÞý’>+KK&Žý�¶�{óÃüßéñzGÖoÚŽ{N7è÷ýäñõæ«OÇ1xÔ'Tª“>6Æ5Gv¦ŒF¹2%(V¤ î9]iÙ¡'�ü€®¯´á«ÏÆ3pø*×m €—G.¾œñ±q’�ÔÔT:F�×;>ò¹‘‹!-í~¿rEŒÇüôYaB»¬0w)‚^�'!**šKA¸ººï/‡�µÕ=oÞ÷$…\Mž7�q`rxD$Ù��HLL"11kkkΜ»ˆ{NW<=ÜiýJ/Z4©G÷®ÿ JNMMåÜ…Ë$'§�×Ï'Ó±·kÕñ-º¿Þ!ìaW‚B¸v“|~¾XXZR¬|~üfšñF�ÿÖx9 /Ï\¸çt½£í˜˜XÎ^¸„£ƒùòæy ã#2*šQãfðé'#Ÿè~î&11‰Óg/`ogKþ|~ÕF·Þƒ˜ûÕ”{®“’’Ê™s0 äõó5ŽÛùWrr2‘Ñäpq6Î –œ’™³°±¶ÆÏÏ+KKãú;víçÃQ“ظr¡qV¸G¥ßWO½ò¤¨DDä999R¢XÆY¯þ+SñöÌuÇMö\²;°qËNúôÁú ŒS»îÜs�ƒ‡�óÑðŒ_—[XXÜuF¦{éþz-]uGÉííi¼{f—¥¹dw6Öy7Ù²9gÆzÒÒÒÒøaÞïTy„»Ù? ë;ÞOâ÷?Vf¸,*3––)”?ÓåVVVÆé‘�ÏYZR´p�»ïwéJÞ|­ýc "òâPyNÕ«]•U*м}ªT(K\|<;v CÛfYúÀz?-šÔãçß–qàÐ1Ê–.nîÃ}$U+¿”á®îÏ’¼~¾î%b .râÔ9&<Ä܇/"Ï ‘甕•s¿žÊÎÝ8qúv¶¶|ðn�Ç ÓÆ#84óYÀl¬­2 O†{†Ê—+iò}ÆÄÄ2sÊhã]àED„~sˆˆ<Ç U*•£J¥rO¤}ŸÜ^øäöÊt¹µµõ7¯“gߣ\2&"¢ 7EDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DžsÙ¬ìˆIŽ3w)""™ŠJŠÀÉÚÁÜ¥ˆÈ¦"òœótp 8ö†¹KÉTH\ö®æ.EDž0‘ç\Ag_v]=fîRDD2µ3ô(…²ûš»yÂ@Džs�|+°ìÒs—""’©G5ò©lîRDä SyÎ5õ­‚“µ›‚°S½ "òÚz„-!q¶ÎFc_‘ç�ˆÈsÎÅÆ‰wŠ·àƒ�3‰HŒ1wI""Fá‰Ñ|°s&ï–h�‹�£¹K‘'LDäлhJ¹æçld Ý6}Lxb´¹K!<1š76�ã|TeÜ Ò«H+s—$"& "ò°³´aní‘x9¸±=ôMW`{ès—%"/°í¡Gh²ª?ÛC�àåàÆµF`kicî²DĬÌ]€ˆ˜FnwV6žN·�s(ì,mÖ ¡¦WZùÕ ’{q¼³å4Þ3DDäq‹IŽ#(æ:;¯cÙ¥-l 9@·‚üPkÞ9Í]¢ˆ˜ˆˆÈ ÄÛ!'ËMáëK™ul›ƒ²9ø ¹Ë‘�³u6Þ-Ñž^EZ©çCä£"ò‚±³´á½x½PSVì`Mà.ÎF{CwK—,«°Ü €=-ƒÍ]Š<#²YÙãíàF�ì¾4ö©LcßÊp.ò‚RyA¹Ø8òJþ¼’¿�¹K‘gPóå=í²ÂÜ¥ˆˆÈ3FƒÐEDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@DDDDDÄd@ä±³¶° )5ÙÜ¥ˆˆˆÈCHLIÀÆÒÚÜ¥ÈsHD;'k£Í]Šˆˆˆ<„›‰Q8[g3w)òR‘ÇÎÏÑ€ó‘WÌ]Šˆˆˆ<„óQAÀÓE'yìJ¹`[èas—""""a[HúßðÒ·þ¦‹94�4Òx9oãÌ–"�“ˆ<”ꌽ•-Ë/máà�Óæ.GDDD²`ßõ“üyiVv (ÙÉÜåÈsJDžO{WÞ*Òš4Òè½u á·¦ó‘§ÓÍ„(ún›šþ·»h<ì]Í]’<§@ä‰P²eÜ q!*ˆ›'鯄"""O©¤Ôdzl™ÈŨ`^ÊY„÷Kt0wIòS‘'ÆÎÒ†Ÿj�ÄÛ!'[BÒîïa܈�0wY"""r›ðÄ(^Y?š­!‡ð°w廚ðµ´1wYòS‘'ÊÃÞ•yuFãiïÊΫÇhºæö_?eî²DDD„ô1�VõgKÈA¼ÜXPw,^næ.Kžs òĕȑ�5M?£Œ[!.FÓtõ¼µeçt§t³8HÏ-i¶z £‚)—³0kš|J‰ùÌ]š¼ iiiiæ.B^ ñ)‰Ì8²�¯O.%.9€R®ùiì[…ª%Éï”Gl,­Í]ªˆÜGó=øë·9æ.EDî#1%‰ðÄhÎF²=ôkwr8ìVvô.Ú†÷KtÐeWb2 br!qaL?ò K.l$*)ÖÜåˆÈC¨°Ü €=-ƒÍ]Šˆ<'k^Î[‡%;i¶+191›¤Ôd¶†fMàN…�åRt‘‰1š-Kä "òì°¶°"»�#y=(íZ€Æ¾•©æQJ7³Q‘¦K°DDäaiºˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œˆˆˆˆˆˆ˜Œ•¹ ɪã'ϰïÀQãc++K<=Ü©\±,övv�e‰‰Iüºø/ÚµiòØÚ¼ÝÙó—(�ÏïŽeá‘,Y¾† —°±¶¦P�¼´lVÿ‰Ô!""b.ê‘gÆ�ÃÇùáçEœ<}Ž“§Ï±}×~&L›M‡®ïpú̅Dz�ø„¾øvqqñOä~]üÖÖw~÷z�NÝúqàð1J+ŒV¬Ù@·^ƒˆ��{¢çUDDÄ”Ô""Ïw7WÆ�h|œ””ÄØI32f2¿Î�‰µµµ¹KÌTjj*�W‚ñóÍ}Dzù¿.Å?�3'�Æ`0Сm3:¿ù+Ön }ë¦æ._DDä±P‘gšµµ5ï÷}“6�{³uÇ^êÔ¬Àžý‡Y¹fÁ!×(R(];·Å5‡ IÉÉL›9‡ÎZòÃ¼ß ½vƒâE òækí±·¿óR§¸øxü¶œÃGObggK¥ò¥iÝ¢)ÉÉ̘õ=¯½Ò/Ï\ÄÆÆ1ó«éÞµî9]ïhëàáã”.Yô®Çq%(”ÜÞÆð`kkÃ{}ßÈP×…Kü¾d%.àíåÁ«[“Ï߀¿V¯ÇÓÃ�3g/pèèIzvëÄ⥫ðn¬¬,¸Á×ßÿB�×;âæêÂ_«×³mç>¢¢¢©X¾4�Û·|ªCœˆˆ<ût –ˆ<órºå @¾<=~€‡Ž1høDr{{Ò¾M.\á­~ÉOH %%…¥­¥ÿà�ñóÍM³†µÙ±{?ï þˆ´´´;ÚþhÒç¬]¿…FõkR¾\I¾þþ~üyÖÖÖœ½p‰u¶×Ý´m7»ö$§[޻ֹiÛnjU¯|×eåÊgõß›™öù=IJJ 5«U¤B¹R\¿FÏw†’FíÛ4ÃÊÊŠï ! 0€ý�òõw X¿y'eJÃÛ3+×ndïþÃÆýü½qûÁÍÕ…¿/ç«ï~¦B¹R4k\—Uë61rÜ s¿œ""òœSˆˆ<rº¹À·?þJ—N­éñzGjV¯D‡®ïð÷†mÔ¯S €æMêÒ­K;*U(CÛWûpàÐ1 ä÷7¶yñR ›¶îbáÜ™äññÀ-GÆM™Åk�ÚR¿v5V®ÝH×WÚ°æïM4®_+C/Æ¿ÒÒÒ8yúï÷}ã®õ¿Ú±5v¶¶ü¹ê-]…ƒ½=åË•¤yãºÔ¨Z€Ÿ[NÙÒÅÜ¿7ukUáFØM~[²‚úõ !1‘¹_OÅâV Õ«–gýæT®X€uë·Ò¼q]’““ùaÞïŒ5�ÊÊP®tqÚvîÍÅK�øûù˜û%‘甈ˆ<¢¢bðÌåÀé3çqÍ‘�/¾�g\neeÉÅK�ÆÇ•+”3þœÓÍ•‚ùórîÂå äÜÅËärw3†€ åJGHèUê֪§_þ@Pp(vv¶ìÙ„ïö¸k}§Îœ§P�¼w 'íÛ4¥}›¦Ü �`ÏþÃü³q;ŽœÄ»½^§s‡–œ>sž4Èp\QÑ1$$üw\¥K5†€†uk0~Ê îß‹k×Ã8~ò FäJP(Ñ1±ìÚs€}�×·µµåâeyr@Dä™—˜˜Ä…K4nP“Ô´4“’ðòÈEn/ã:¯vhE>ÿ<ÆÇ66Ç9ØX[Ÿ��á¹ä䬬2þš´º5ƒUBb>¹½(]¢ë7ïÀÆÚš¢…óg+·Û´u7µkÜýò«Ç¥t}¥ Þ^äpÉNú5hX·³¾þ‰ßþXAç-IHLÄË3ãqåöòÀ5‡‹ñ±ƒ½}†¶«T*GjZûåÔéóT*_†œn®Ü Ç`0àëã……ÅWã¾×·ùóú!""ò¤(€ˆÈ3oÑÒU$&&Q»ze, |}¼ñòt§uó†Æu–þµg'Gããc'N“?oz ‰‰�å̹‹tîÐ*C»¾>^„„^åFX8n®.=~++K|¼=¨wë2,ƒÁ@³Fµ3­ñàácôx½Ã]—Y[Y±ië.ü|½y¥}Ë ËÜ\]°³³ÀÏ77VVVŽkÓÖ]8fsÈt¿ÖVVÔ­Y…õ›vpìÄiºwM¯Á'·'”(Z˜BóéSÿ¾d%9rd7÷K*""Ï1y¦DEǰô¯µÄÆÆsäø)6mÝŰ�}qqqà•v-øâ›ydÏîL�¼~¬ß¼�…‹þâ×¹ŸÛù懅dwvÂ=§+s~ü—ìT©XŽØ¸ÿî¹Q¬pŠ.Ș ŸÒ§Ç«ÄÄÄ1ù³¯iÓ¼¶¶6Ô©Y…_|�……3&�¸kÍ—ƒðööÄÒÒ2Óãzµc+¾ú~qñ Tx©6ÖÖ;y†ïç/2††Žm›Óý�!ÈïGÅr¥9vò Sg~ËçSFßóœ5¬Wƒ�Ã&`ggKµ*åÈæà@‹&õ7eï÷}ƒìÎNÌ]°˜+A¡¼öJs¿Ì""òS‘g†§‡;ùóåáŸMÛ0`ÀÛ˃ofN D±BÆõš7®KJJ ?-Xµëa*˜—ϧŽÁÅÅÙx™UÿwÞäçß–J±"™5u VV–XYZRá¥RXߺôjâ˜�|þÕ� ;[[êÕªJ÷[ƒÛ!½‡¢D±BäpÉž¡‡åv›¶ì¢võJ÷<¶.[ãìèÈÒëøå÷å·.�ò¦ÿÛoÒ´am ÌËô ÙóÓ¯ü0oÞ^¹øxÄŠM?ö¼þ¾dwvº£í²¥‹S±|iÊ”*f<.€Þíηså“_Où²%™2n¨¹_fyÎÒî6龜Ès*>!�ÚM^á�_ïßñ(’“ShÙ±'Cô¦fµŠæ><“iÞ!}°ý_¿Í1w)""òŒQˆˆÈCÚð(«ÖmÂÆÚŠ*•Ê=zƒ"""/݈PD^( ŠÊ�µõ£ÿrâô9®^»Áıƒ3\Ú$"""™Ó%X""òÀt –ˆˆ<,õ€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉ(€ˆˆˆˆˆˆÉX™»yzý³i;IIÉw<_¼HAVÿ½ùŽeÖÖVÔ«UÕÜ¥‹ˆÈSJDDD2µc÷vî9�éòc'ÏÜñ\•Šå@DD$SºKDD2U«zÅߦڃo#""/ÉT¥òep°·ÏòúöövT|©”¹Ë‘§˜ˆˆˆdÊÆÚšÊÊdyý*Ëacccî²EDä)¦"""÷Tó.©ÒåW""r? ""rOeKÇ%»Ó}×ËîìDé’EÍ]®ˆˆ<å@DDäž,--¨V¹ü}׫Q¥_òøzßñ¼on/òúùš»<yF(€ˆˆH–Õ¬zgOGí•Í]–ˆˆ¼½<Ì]–ˆˆ image/svg+xml Kiwi NG Workflow Image Description (config.xml / .kiwi) Package Repositories Prepare 1. Create Target Root Directory 2. Bootstrap Target Root Directory post_bootstrap.sh 3. Install Packages 4. Apply the Overlay Tree root/ or root.tar.gz 5. Apply Archives 6. Execute config.sh config.sh 7. (Optional) Modify the Root Tree Unpacked Image Tree Create 1. Execute images.sh images.sh 2. Create the Requested Image Type type-specific scripts One or More Images Deploy / Serve ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333865 kiwi-11.1.8/doc/source/.images/intro.png0000644000000000000000000004454115273675667014764 0ustar00‰PNG  IHDR ¤R/“™sBITUìF pHYsTTàš(tEXtSoftwarewww.inkscape.org›î<HàIDATxÚìÝg€TåÁGñCGEQQ,`רkì¢Fco±ÄD4šh�ÆÄ^°w�{ÁEÅX°#ÒDQ“¨ivca±½1Æ;ÿ÷Ã^Æ–e¶²»sø}Ù½;3;ûÌ}îaæÞ¹C�¤ÖÂ|,Ì,Ãr ¤?}é阴ãGÓ!�Ô‚›˜~ìÀaœÏíŒe<㸇۸‘k¸ŠaÜÂf<ãÍeìÃÒŽ—‘Tí–.¬Í¡ g,·r»².‹Ð¹žËwc%à:Æs;3—#h@$Uß&åÁHÆr!»²H#®¿ G2šÛÙα4 ’ªec²'1‘aìÂM¾­%8›Ñlâ¨Iý«]y�;Ø�îÍx«Kp÷²„ãk@$uÌMHog'°p‹Üú:<Åj޲‘ÔÑ6Ës)ãØ·9ŸwÔñ<ä ¶t¬ ˆ¤Ž³éX–aÜÍÆ­ð›æg :âDRÇØ]~=w³N«ý¾¾<Á@ÇÝ€Hjß›Œ%¸ŒûX·Õ_,›@/G߀Hj¿û<®å!߽͑?GúIíqS±·q̱ßß•'éíã`@$µ¯ÍÄšüŽ;X}ß‹9ÈÇ€Hj/ˆÎìÀ gÕ6p_–æ&"©=lzñ3&q ‹µ™{ô¸�Š‘ÔÖ7 s*Oðã¶õiŒõ‘1 ’ÚòFaq.b,;Õwúõ9tÏÆøèImuƒ°Wó([·Ñ{7Š®>FDRÛÛ¬ÇÜæmøŽ÷Q2 ’ÚÚ†à[<Ìõ¬ÜÆïå)"©íl:±�q!‹·ù{º$·ùxImcúwåûŒãú¶‹{»??ö1kàæ¡?+²60ˆ­ùßá;lņ¬Ê@úI�›ú½8„§8®96#­t�GxFÞÙŽQqg2œ1Œg,÷ñ[.ç|Îà$ŽáhŽæhŽå .ázîaãË-Ë,C'"iöÓ~>Žçi~Ê\íè>/ÆH¹:Gf^6ä'\Âh&r;§²/Ó¿Òƒ°éľˑÜÈ“<ÄÉlÃ<DRÝS~Na?¤K;»ß§°­�^ÙxôỜÂÝ<ÉCœÇ¾¬Ùô7}2?ßádÆqÑ߀Hšþÿª§ó${¶½·Îöžwæ‰öw¯[`º±?áj&ò'²U }ý"ÀÝ<Â�³{�ÓI%UǦ§;‡1‘ÝóJw¸÷ß⬪~ô–ä{œÏ(å"öe•ÖxþÈü„QÜÌ DªæÍO'öáIn¿ïãævVªÊ]âÛr#à ÌZt›÷a-îáz–2 RuæccÆqs·ã¿`9—©Öängž¬=‰]Õ½\2�¹*ºä8×’VzL†²–‘ÚÓ¤=’ïWé_¾+7W²ç‡<ŸU+="èb@¤ö3eçáéê=¾ˆ_ò[ºÎöRǰ�kJ+<}x0©ýLÚSØ»ªÿþ1»Ã—Ù€s]SZá±ø ?6 Rû™²ý[õc°+O°q½—èË�®+­ðHŒ­=´Ú¡�ÚÇ”½€ï: ,Ì- e±z~~¯£Ôâ�Âê\_|å`Hí`Ê.ÄG¡‹­¸›»Ø®®ãÑ8ªö¥µè#pû´Ãª ©=LÙcøž£P6K0„ Œä06£ +}Ùšk¸Æ(¶øØ¯Í�¥¯©ÍOÙ.<9û#�ªò­=9•áŒáQîâäê8?ñõûX΀HígÊîÂqŽ‚Ú帗”}ç€Hm~Ò>ÄÂŽ‚ÚÀš87O—$°C"µõI» 78 jëâ%ì5Ý÷‰ÔÆ'íP6tÔÖÄosË K©�ï@ºáê*5ûš¸0“èc@¤ö4m·âlGAs|=ìÅØ™?(Ø�‘ÚöĽª¡Ÿ .µÀóà»Ø¾Žå�Ô†'n'žp4Ç׫ê~‡¿ƒ#µå©ûM.t4‡×ÂK9b?qp¤6Æá’ÚîT~Ü1P+¯s ñûW|iLj£SypÔªëÜZ<>»çDj“y®uÔŠkÜÞËžá1.c_:»f4ãØvá|.©ì=DjO“û0vjÑÛÿþL‹^d™ÞƳ„[øå•N·ài–sÝh¦±��‘Ô¤[p¥ªÈ›Œe,¯ Gµ­€0“ SÏ©ÜÂû„0Ñu£YÆv9&±EoÃa”ª: §ß­É„0ñ}W°,½fºÖb,A§ºBwzÒsÚϘ‹åXbÆاö}ô¤gù ',4í÷Îpù� áÖâ»^¼KË.±¾¾?Ž(F¥3=éIç@§¯#‹•]v9æ�éw.Á\U±ãü)Vhò­8M%£aýÀ‚\ÇçÅâGX©øy7Îæ#Bø˜ãjÓðu@ØŒÏ �Ñ#°&�×þ”k™§¸~?Fñ%á÷ „sŠå?¦†ð%ùUy м! )}¿ WpËy8œÉÅý¹�EŠK¼Dø[ñõ΄ðÃÀ@B¸�yƒC`YÆÉ|žÍJÑ<› Ÿð {vèuk7F³`3ÜŽÓT2 !ðBX:ðH±èKBx¥öS¹‡¾*~vay@X–žcÞÀ‚¼Svíp]ÌÍë¥_ýÒ×áÔ"4µÁ:j†û¸)!|Äi¬3S\n›î^~Èâ³ È=ü‹ð``þ]vÝÏX3FRüd*ÛtØ5ëÜAÏf¹%§©ÔF§ùÏZø(¬Ú€œGú°2'Â?èÌ2„0‚™‡ë aÕÀwaó±O¦ÒZ@˜Ÿ ¯×> àB8’^ôã/„ÿ �£Šç3kq O Kò)ánb ¿#|ÉÆÓÝÇNŒ.}ó/Æp8½‹Ÿ|—ž`Izq!Ü<Û€„.aïÀ�„ÏØ‘îlÇW„û[ãÑ‹uxšð�¯_ìê@kU'ÎchsËæ4•ÚêTß—Á­�éý<°ƒÌ€À¼Åÿò·\G+†À·xŽçزÈkŒ&üsÚÑQ¬Ë`ö¥G`q^#„n� ŸÔžžž#Kù!lBñÂÖ…3ÜËÎìÈ£e >â,ºn!„Í‹KM!|A·Ùäãâ÷wã+2í\Ç å9Æn&|V»/†a߸V å„f¼5§©ÔF§úŽ-{*“™ò9GMûŸ)«q%Ï•^®Ú:0–ðïÿçʳe×^±ì¹ÛrGÙKVÝèR»‡¤øùꥀ\EÏŠçuÜ×¥Ù›‹™R|{pà„ÿ”v—#„åg�á¥ã�BøÍ ¿ãiÂ'Å=ù!œÑáÖ© 9©YoÏi*µÑɾvË~"z�;؉�؉ ¿>âŠ;Šÿë?ȈR@þDxu¦Ûx¶øŸ}ËáÏ„ð6·òT�¾„L;»0s3µÈ=ÅoúÚ“õÜãΜHw^#¼RúIí‹XÌn'z±|=B8z†[“ðåt÷åê¶F�ÓܧçtšJmuº/É�­¹½´| B8…¥›¶ÜKHí‘;ÌÏNìÄ’E@>aCn'„Š�ÿ!µ» y°ôÖÿ/¿a»Ò3� a�YÞÇ“y—wko7b*ááÀý„°d±|!ô-òZ±ôG³H?B¸«ô÷îÄ�1„·:ðútÆ´ãÞ ˆÔñÒ‹‡çH@Ž%„õBàñR@Î ¤öôŽœIkLwÖç„×èQüOºzóI) ÏÂÆ�¹ù]) ûÂö!°·0œý¦»/;ÂóÓ>Ÿ»x¦qZé«#B`e¾˜– &Ââ�.ŽŸ) �÷ °` 5„ß.ø:IlÀp†OÛÃÒ!Ö¦S[â”3NS©íNú‰s$ [ œÏ Å¢íýŠÃ[ïç>%ü‘NÓ½äB8¶t`îh.åƒâúóv#„©ÜÅ{ÅÒsÝy•ðŽçH^ |ÎjÓÝ—üž¾ä)Fû@>d©À|¼Oø’aü¶ØW³G\Npki�I]ùEñÒÛÐâöXœÿ^âpNá=Âû3¿É°Ý®I'·ÌËœ¤RÛ�ö�Ô5ÔÊéR;þŹ„p{±GáÃÒ…Þ*ÞÌ÷u@âcÂXŒoÏ;ÂKÜTÚé݉+K×¾iZ@ëóViùÿ±óL÷f@ñ‡ïƒƒÔ6&q�s§2˜-Ä 6gÎåQîb¯æ8F‡^<íËXmê1?‘Cçø}ða�:Ì&e6áœÀYœÅIìÅZôhÆ[߉¡Žq›y¬×åÁ9ÿ¢¢„¤J7µîÉÂUÏóÁI,Ö¤ 7»s´£Ð&‰+ø^›¸>’*Ü\tg’Çbµ�Ça;nh#÷ÄCRÅŒ‹gu_µÚcЗ‰må3[|8$U¾ÁØŠ³…9üŒh;§–ñá�Ô�±žpæè#p §·¡{ã"©›Œû™ÏQ˜c£¿8’°É¸‚•…VñÎŒaY"©ýoÎÆ4çgŒ¨‚ÿ%G´ÙûæÃ#©â F/uZuÄ—æ1ºIís¶5C…VïN<Ü–_2ô!’Tùã÷€´êxÞ¶?¡HR¥›‹nŒwZq¼WbLÛ}ùÊ€HjÈæbo~í(´âËWcÚÖ© ˆ¤Æo.&x*÷VírB›¿�>L’*ÚXlÇyŽB«�ö<<É\DRÇØ¤�¥Ÿ£Ðj£}*{µƒ{é%©‚MÅö~z+ŽöâŒn÷Ó‡JÒl7]˜ÀüŽC«�÷M¬o@$uŒ Ú`�¿jÅÑ^š;ÚÉ=õÁ’4Ûçéå8´ÚxŸÌVDRÇØ íÃ1ŽB+Ž÷t6 ’:Æó�ÇÛÞ‡©vàñ^•«ÛÍ}õá’TïFb?ŽrZq¼wá"©#lÎ:µ‡7´u¨ß­m~|­‘ÔÐMÄ–œë(´êˆ÷ãI0 ’Úÿælx[ý8Õ<æ3žU ˆ¤ö½)[ˆ…90îËð;IíyCvD{8#S‡ùyc@$µçÍØ#ôtæÈȯÆp"©ýnÄ:µ‡ÿwÈ‘_š§h@$µßÍØ†ÍÑ|-ÈÊlΩ\ÅÜÇhÆVä1îâzÎãpvã›ôoÛ ;Ó_¾O²’;Ñ%µç€lÄé­˜‹þlÊþœÆµÜÏ8Æ1† å$fW6fybžJC@gæg)Öa'áne ãÏ�œÊ¬Ü–s ãJz·“5ÄI"i›‡=øi ÿ†.¬ÎAÜÈDçvNg?6eYæi¡ß¶0[p(W3�qœÏætks#¾O²e;ZCœ$’f±y8€ï·Øm¯ÂáÜÏD®d0Ë·ú_Öƒ 9� ÜÁ,ÖFö7íÄXÎj_ïúw’HšÕæah� åæ c"W²Ûœÿˆ*úóCFò;¶¤Ó¼]Ù›‰œÁBín q’HšÅæamÖÛëÍ1Lâ|Vicç ü†I:'>ó„>É$Žd¾v¹†8I$Íbó°C›í¶æâH&²wÛÛïPzQëÇ<ÁQ­ùër�³o["©)»¸›é} ìÂDöoëÓÒ•xšCZþÍ“ôã<ÎU¬×Î×'‰¤Yn F5ý€Rzr+g·Ô‘U-ðLäg<Åm¡Û_†_ñ(÷2˜¹;Àúá‘4Ë Ä/Ø·‰·07£Ø½�ýÕ�Ù‰¹•èÑl·9�ïs㹉ÝÚKL ˆ¤¦l â�&nŠÇ6íôo_‘!L`8{7þÙØ‘!üŽñÜÌOY³}½#Þ€HjÚ&âVoµOçíüïÀO¹�qü–cØžÕYdÖ  ³*[q §ó[F1šaΖ,Øa×'ˆ¤z_zÕØ÷H°#:Ì8,·ø%s�Íâü[ãxŒÛ¹”ãÙ‡ YtN¾³Ä€Hj‰cÙ¿Q×ëÊ,ìøIÕ»‘èÎhÖnÄõoÞ·!Ê€Hj›‰~<Å€¿ô5®£í2–‘Ôð Ū<Í ¸|mk',‘‘Ôœ€A_r9ž¬ü€\ÎçG×€Hê¸ÓÿÇ„k*þ�¦y¹“ é[Á%Ê•Ž®‘Ôq'ÿ|E'6à:ßaGÓ§žKô殥³ãk@$uÔ©ÿ>'„gv¶+:ó}ä³äL?[–Sy’]"©ãNü�ø/!ü­qï’faâVçnâ*.æîg¿eWŸ{IyÚ¯Î? á oâ-ÍÅ,Ϫ ì8'”‘4«I¿,ïÂ{,çhÈ€HªtÊ/Æ„ðk82 ’*�ð}ù!ü�� I•N÷ÞÉ-¾á<É€Hª|Šÿ€©„ðÝ I•Nðø‚ždnGCDR¥Ó{ŸŸ™ßÑ�‘Téä^‡� á5u4d@$U:µWâB˜ÂÒŽ† ˆ¤J'ö&‡¬âhÈ€HªtZ÷ãeBøë;2 ’*�Ô}xŽ>c+GCDR¥Sº�—ìêhÈ€HªtBwãÁâË92 ’*�Î�^|ù+GCDRåÓùŠâ‹3 I•Oæ3‹/.w,d@$U>•]|q+� I•NäŠ/ ›£!"©Òi¼;_Âær4d@$U:‰·æ3Bx–ù I•Ná ø/!¼ÌÂŽ† ˆ¤J'ðjü“Þf€£!"©Òé» ï¬ähÈ€HªtòöçuBø˜u I•NÝø !|ÂæŽ† ˆ¤J'î<+ûö–tLd@$U2!GøÜCDRçã&>÷�‘ÔðÉØ‰gá_èhÈ€Hª|2þ�ö¹‡ ˆ¤†LÅ^Lö¹‡ ˆ¤†OÅ“|î!"©á±7{; 2 ’$"I’‘$I’‘$I’‘$É€H’ ˆ$É€H’ ˆ$É€H’d@$ID’d@$ID’d@$I2 ’$"I2 ’$"I2 �$É€H’ ˆª~Õ\‚�ú]ÒÿK¼²Ä«ÒŒyñQ}ŽaSz;W ˆ4]<úMÚ|Êñÿ¹3/¥FªÓ3¹nêAÿXá�…Σ›sÆ€H„0ÿÏ–~÷!7�ªÐ©ÿë÷«9o ˆÄ"#÷û×ßÝ,ªžÈ*Ìû=çŽQ•›oðàÝ ª¡^ËÀÉôsþUóê¸èRS^ws¨F¸ç«~c�ADÕüòÕ¸û¦º)Tã þgŸ}�CDÕº2ö\yŠ›A5ÖsY|‚³È€¨ZWÆoþà7ƒj¼%ßpU©yú•A5ÞzSèë<2 ªJ‹�|Ü� šàÐ�ØÊyd@T�yê7‚j‚3¾`Oç‘‘‘ ˆ‘ ˆZÇé_2Øyd@d@¤;-|ÄÜEg“‘‘�¦RÃ~Î'""54 !|ÈcÌãœ2 2 RC>çv甑‘�ðßuV©á oÓÇye@d@¤†äKŽr^©á �8¯ ˆ ˆÔ˜€¼î¼2 2 Rc2…ÞÎ,""5< ﳚ3ˀȀH ȬáÌ2 2 ’1 ’‘1 ’‘1 ’‘‘‘‘1 DDj;a6goNà~Åîl\Ýï3q³%"RÉÆrNçEÞå#¾(}ʇLá÷ü‚~D2 2 um(—fï1u?þ‚÷¸ªÏüëfKD¤¾�dOnâýÙ^ìK¦p´‘ ˆ È´MdþÂç^ø#FÒÓ€HDÖâ­Y¾pU—Où É€¨Ê¼Ýà+}Åèn@$¢*½x¡QWüwÉ€¨šrŸ5òªr¨‘ ˆª4 ¬ÍMX™'WÇ ÝlɀȀ̼i|¶I+óç\h@$¢* ›óaWçw˜ß€HDÕ�Ûš¼:Ê`òõc0ƒYÜÅo1¸ðC¶`I:·ØoÌ`6­øÒßf0{¸Ù7 ꀙ\ß�;§g*8Rw‚ùú‚ÛÂesà.Þ?—ùf Æ­õ^bvbëâëÇï¹Ù7 êha-þ¯¾ œ•äÅÙßÌz�¶�ðeåÏš9  o¢�ƒë{÷ù¦ùOeyŸU H[ ÈÒ�άÈ„ðÄ®²€¨íä×Ù'¬cÙŸZ|“uyöÉÓ�¼îÙ'÷Ï´tLöÉIÆ`Îäüº÷Ì9=£óYRY@>žöj…) ݘÄ$ŽbÆð&÷°�9�§y‹{Y­¸t/Ž`"oóø ]K·r(x™Ë˜›G˜Ä‰ÅÒeÆóü…sÙ¬¾€ß?H[ß}Ÿ‘¼ÁCü‚%J—XƒÛx�)<Ã9_Ÿ§Ÿ¸™ç˜Ì8ö£SÀ$&±{3Ž]“˜Ä)!0’I\˲ çï<Â!07¿a áßü†µ—2‰J·¾wð/1‚]Êîé$®d)nåUþÆÅÌg$f±vumZ@Vy¬ŽeO´ø&k¿�yÝSC.�ié-!ëƒ9�;ëZ¼zÊÿU�¯:þnôƤ!¼Î—Å�þÅC¥‹}È"�N<0Ý•Ï/nãÚÒ’Ñ|L¸!¶à_e—Ýg¶Ù�Ž�˦{ëÎB!°ÁtçÎ|›…‹]Þÿ)[z] !„_3•°GùKXL!¼ÆßK—¿<°PÙõ÷žþ%,.Ÿî.ß\,ý€ð*o––ßg*fxl`nàÈÚsÒF2¡®Å«ä£|”�òßJŽ3 ³ H¸‡ŸðdñõmÄ aÿÀ¦„ðßeg^'ü‡Î�A„ðÇ”ú~C +/^gvâyÂYe6Y�n,Ýâí¬ÏÉ|Lx”Î�Q„ÿ±r&!œèÁë„0˜Š•cóR@>çSưÅL _pçó)!¬GÖà9»¬Áüåa{Bø;³#ÏÂ÷J /p$gò!_?Kªòptg—ñ ÿâ lúKXDÍ�»êûñ•d*?2 ³ ÈÛ!°!ü%”²qR`k†1ŒeBànBX,p/!µyà¸R@v$„ƒB`qB¸f6YŒÆ·ø ó†À „°aàï„�Ù2Љ“8‹Á¥g-¿ �™Zd¥6 ×ÞBÙ»ì/¿nÖ;ÑyœV�eáéR@¾bɸ”¾UõéX™cùïòá ^gõæØR@ÆçþÔä�Ü•r{Þ*~þzFæ÷©Éã97'ç¾ÒòZä‚‘Óò»nõÑœ“3"¯Õ�W32#óFñÝk‘siþ<Ã-<–ssRîΛäþŒOM&伜”{S“šü-׿˜Ü“Ée×x3·çŒ‘³3nº[z%·ä˜\�¿¤&”ýlrÎ995 ȬÖÒ ›% ó2«€ü6úÂÐbCCŠËÏÇ�\Éߦm¯jŠŸ~£�c a¿á7ü†ÿÆÏ& «LÛÐó*¡¦¸fíÿ†|‰ Ùš.!p ! *na^úЫ�ïÏ|S_Õ¾“”¹øŒðL=ùGù®u^%|\ È3ÅÒÂöUŽEØ—™Â|UzésÜôÚRÙ*såÏY®ø5}345©É„�Ãsxé×oœ¿×üS¶*»[æï¥@ì\ZºL±Aþ: ¯fƒ�ÊË>œ�¥÷ lRÊÍùAé6ÉQ³ H¯|+g¦Sq�C3:ý‹¯ûgtq™QY±t›�r`évÆf@±t¾ÜŸy3¨Xþ×|ý¿˜¥[1 Ó­­?o–€4ãç±w¼€ + Èo¦]8»Ø¯ñ¯ÕnèÄ—„{K·÷Q�«gúUïÌ& ÛÂÉt*}´ý×N ôãò²½ Ï;¼CXd†[® Èê³ÈË¥åÏN»Wu„¹ùúÎü–æ/2±ÚBã5þW¼øõ!ŽgÒ©¹ŽÂš]@ºg•|/£3&†ôÊE@MçžsCÖ Y«¸æ/C6ÎÐ<’ó³|È�bùwC¾•á¹-{…,’Éey5†ü²¸äÓé™î9*åöl²sÙó•%rYFå¬ôN· 2_ÊÑy"§¦{HÏl™¹;›„l_\f³�½rcîÏ�ô™šÔä¥,�NùiîÏðlšÞéVdrVÙ5#ò@M× Èß ÈÌëìõ�Ȥâ€L¡—iL@¶¬ÝñÌ7èÄÅ¥g ïÞ(nm™Ò3�SỬQ2»} à a§@ áé²k®Á¢Åeú2˜»ù©}FÃñ„°e�Ï@VŸå3�>!ГOOÕó ä½éž�¼Dø˜N¤4"�Ù˜y‹”ÕÔõä¾åBV/-_6dtrb±ôïY5ä–Ô¤&Ë¥Gé àá!;/j‘õJ/tmr) ¯e£�_•~ÇîeÙ©Éš!—§&“Ò-óæÙbéïB!¿)¾Þ2dÉ⥫BVLMjòçtÍry»tè29-5©ÉQe÷çͬŠ€ -}U“𲇩k��Ü yª æv‹ä*—ÌUÚœÖd"!¬¿,d¯i9tf?bÛY„¹9š©„?Ó9ð(á¥C�â –a †0¤ö”+ ämÂgtfϯ_^ce¦Nžm@R{¢’âÏ5¥€¼=S@F—íY–ž,½„e@jÿþEù ï3•ð ©Ý/Ôš9²´üÀbÓ?!¤w^--¿*dŸÔ¤&Ï–^̪É]!Û”®weiùý9 wù]6Ù·ô³·Ò==ÊþgVÈÏS“3C)»‡ß¬ ½òJñõ±eÏpjÒ#‹8<›—JK� 9!5©Éò鑯GóìR6¶ ¹¶´ü‰tÊ*¤®õõÞ&äs~j@�Úg³·—íÙ—þÁ¯^¼Ïó†@/j/²k‡ùþ¨Î€Læ Þ*½~¾]ìR<ÓY…ïò2á_ôa~¦^a_Öe?þ]»¢ø-Ÿsßca*kV�ϸ�óø„ðUí;\Eø‚5è5]@¶)ŽÂÚ�£‹ÿeïj@fˆÇ8¶åmV'xhÉ€Œ*-?¸, ë–]þé��Kßý=æÜìž¾¥€lYçQ]û…b_Ãòy³´Y&K嘒ŸÏb VvÝW�KËO¹ªô]¯" µ^Ì�’o§g�Éé‘åÊ~~) Ó%G•Ý·2Ÿ©k�ݶ8n²ñy·ö ¤áY¥tµ/Š}÷tá±Òò×ø¤ô>��ù¤ì ›ñd‰3�Êäv(ý쎲åŸÖ¾,ÂÉÓ]úËÚ7'²sñ‚V­3*Øò÷²ÏC>£¸Ä/fñ>�˦û�×–½¤Ê¢\Ä8¾X‰ò?i‰w¢Ï> ª3 Û•]þ��eR“šLÊúéÒ=kg¯R@V eÏWÊÒ%e³�£Še#êøãÖNMv y°ìºÇ7K@Fd™t é�M³u�çC6˜!޵©ë€/�™×ÛN¼Ø¤€|ÉõU1¿+¾à² aÛº0„!Å‹Ns3„!ņ»/CR{¤;ð8oò;6dCž!Ð�Sy†W¹‘ùç´VæFþÂ9…õfܱØ›!…ÙŸM§?r‡ñopNÛÿ؆ßñgÞåÏÜÂZ¥¥ËsÏò&SLà †0¤ì½êCRýÍ�½BΙnÉ_Ó) _Ï: óäåÒò+CöJM®Ù¥ìÌTÓ²OÈõ¥å·g™5Ýa¼G…lRì—èšÞeï+y"gedjrnÈA Ü2»€rFié¹¥} Ò=--?§�ÍCî*{ÖuvÙ~2Ã:<¶ô’yCòñ´·3�æûU×ÂE,ÏŽ7 0�ñíÆ\·)§sÿvH�lžSr}NÈò!ýKÇRÍ: ]C~”ÛrqV ùfjR“‘!}sQžÊ=Ù=ÝB–+®½qȹ)7d»��yu†w¢ß•NY0MMO×tÉÏ3"C³UÈZÅöŸ…,œ3sg†dîôo†€ Y-Ãòt~›o¦kÈŽy55ù[ú„ü(wæ†l��!ß.Ž[>d›\›[ó£tͼ­pº—ö�ÀJÅá4 óóTÍ|oÕ_Ö‰í9™ë¸�C¿ÞóÐ&‡år†sš9hT<} ë¦ä嚯ߵÕ);å©ÒÏf�]spé:[”¼=$Óþû¸rMí§—½‘š¼˜¯��X1�Öq.¬½‹—Ájrwé=ãd“<_z6ò³²ßwK3äÍl^ºÅmòdz‡¬–šÔä‘,U,_%w”Q®É³Ù¨t�3�ªÅ€Ì°NoÕÀ„|Á«u¤n@¤Y¯H‹q £Ùª)·ÑÔ”ú[îÈÙ’3©‚KOÙ-5—ßä¬<2ÝÙ¥ÆæÜœ–‘y35y>çäÜÒOÇå795wÌp欺¼’99çMwÜUMjòd†æô<\z1¬éîÊ)¹ 8)ÉøœœJgãž!‘7röt/˽�‡r^NÉ­¥� H=kö ¼V:óøì|ÌÄÙíé3 R³Ç£9Ò0ÓÒ1•Aeï}Ù+äjÏÆÛØõ»gÖ®ÏTÞáâÚsï©Òx\Ê£Ís–aÒ|ÎY7#óvÞÊUé��ÍøŒ§Úëòl}çÇâ}—® ˆš'£¦�aÌ€´%“³ké“!K'ƒ7 MZß7åfÞâÿ¦;‰ë§¼Ë+\ÀŠUº p3¨F­8‹s)£Ø¢9o³uòlv({EGtOÏ9$—½ÛÅ€4yÍ_�½8Žë¸‡Ë8‚ݪñy‡QÓâ1”Gš7­͉xlÞ·m@d@ ˆ:r<žö¹ŽDÄ€H•¬(KpÕžÙØ€È€È€¨ÍÄÀȀu¼x\΃lڿˀȀu¤x<À&­õû ˆ H» s³¢4ƒ5Z7D¤}ä\!Í`(·öÊh@d@| K2 2 D2 2 2 2 2 2 2 ’1 ’‘1 ’‘1 ’‘‘‘‘‘‘‘ ˆ‘ ˆ ˆ‘ ˆ ˆ‘ ˆ ˆ ˆ ˆ ˆsˀȀHÄ€HDÄ€HÍ� Ï»Tg@ ˆªÕÂw#¨&ø–1 ªÚ•qû#þíFP�·¤1 ªÚ•±ÿïºTc½š… ˆQõêÿÖ«nÕHÃ2Ÿ1 ª^½wÛýC7„jŒ—²X]«”1 ª‹ÜÿÛ/ݪá¶IWb@Tå+äü‹Õ<áæP tnúÔ½B¢ªZ%WYäÅÓþç&Q•z!›×µ÷ÀUåJÙuás×{÷®¸C]³ól.΢é<ë•É€UኹZÿëýë’_n™¤:웵²hJ¯úW$b@Tµ«ç«‚šÄ€É€©!«ç+‚šä}Vw ˆªsõ|ÆAP“¼Ã‚‚Qu®ž8j’·¢j]=·ç߃šàq‡À€¨ZWÏEyÇaPœîUï :ž©ƒ½dIÁ€¨zWÐ~>Q#ý‹C¢ê^E÷å#‡A�0É!0 r%½™ÿsÔ Ÿð ‹; D‚y›/Uè}®¢§Ã`@¤Úu~îa2ïò©ƒ¡YšÊ?x—ÙØ¡0 Òô+k'Vàû ã¡ëÏŒéÀ]K»›_³!½œ)DªÆéx+: 2 ’ ˆ ˆ$"É€HD2 ’‘d@d@$ID2 ’‘ ˆd@$"ɀȀH2 2 ’ ˆd@$"É€HD’‘‘d@d@$É€HD2 ’‘ ˆ$""ɀȀH2 ’‘ ˆd@$"IDD’‘É€HD2 ’‘ ˆd@$ID2 ’‘ ˆd@$"É€H2 2 ’ ˆd@$"É€HDr:IDD’‘ ˆd@$"É€H2 2 ’ ˆ ˆ$"É€HD2 ’‘4m"ÎSævDd@$U6òÓÂ9,íˆÈ€Hªl"žÎ¿Xž+X‘«ø¯ã!"©Ò‰¸%ÿåm†só¼ã!"©Ò‰Ø‡wŸðoÂ5އ ˆ¤Ê§âÅŸ°—£!"©ò©8¶øâ–w4d@$U>O,¾˜L'GCDRåSq3>"„¿82 ’2{ó!ÜèXÈ€HjønôÏØ×‘�‘Ô°Éøá]¾áHÈ€HjØd<Š©ÔÐÅ‘�‘԰ɸò‚ã "©¡“qnþÇpÇADRçãtd@¤æX={òMö`Ϫ1‘£ªè¯mš]XÖ·\©Ý7]|äâ/¯R³Ï'üï„OªÅñŸWÏßÚTGÿg›w–}k±çû]ÎBÎ"•öô»qÐS#ÍÆ¹eê€)óýÀYc@$B÷M}ë²ÏÜ4ªR¯fïöË΢j_!û |çy7Šj ¦.ò˜³Ç€¨Ê-2þž©nÕpûü³Ï`ç�Qësà~¹)Tc¼–�SXÄ9d@TµúOþ»›B5ÒíSæ2 ªÖ•qÉAï¸Tc½™Å<ŒQÕ®Œ;û_7ƒj¼å<¥QµZôÒ;ݪ v~�UœGDUiñÇÿìFPMpò§ìæ<2 ªÎ€<õ‚A5Á_°§óȀȀHÄ€HD­ã´©<Æ p.©a áÞårz8Ÿ ˆ ˆÔ°€ÔFäÖvF©¡ á=¶vN©á o2¯³Ê€È€H ÈÜé¬2 2 RÃþÁÎ+""5< ájç•‘‘�?9¯ ˆ ˆÔ˜€L¦“3ˀȀH È;,íÌ2 2 RÃòk8³ ˆ ˆd@ ˆd@d@ ˆd@d@ ˆd@d@d@d@d@ ˆ‘‘ ˆ‘ ˆ ˆ‘ ˆ ˆ‘ ˆ ˆ ˆ ˆ ˆ1 2 R› «°+¿æîæ~Îö,a@$"RÿÆrnàMÞãÓ²…ÿå^æ\V4 ’‘©kC¹.äƒYþx*ïr/KɀȀ”o${óP=ñ(�Èt6 ’‘©ÝD.Í«|Yá…ÿÍ8zɀȀÀ ¦4è _ð ‹É€¨ÊÂò¼Óˆ«½@/"µ»€ŒÍ>eιy¢Q·óBöÉÙ�¾?Ê‘u,=*ûäI7òí( ÌÏë�ºâçe;ÿÿš¹³HjR“¿¥w΋Åò‹B�73°ì¹ÈßÒ=ÝŠçL¤QÙ îè½36åÿF•žõÕ©¦ã¿ÄÍ–:ôa¼}sUñ³—òlÞ(]rÝ�ÇS“çCV+»…Ãsh) W§kæÉK?;´,5y3 ¤kÞJMz§w^.{A¬’€[zIªsæÍëÅw¿ Åýýkž-]óåôÎü¥dQv¦=¹+d­²ßµ}È-¤ñùYÝ?¸ Ç”¼W|5´¾›yŸÕ ˆÔ®²\Î*œ“ÛKÿ[ÿúøªa9M’l5ë›ùŠý ˆÔ!rhÈŠ9¹Ø7°[�[C~Xg@VÌ+Ååö+–~7dôL—}q†—�þÜ,Ï@6Ù¬t¸ñÀ" ?¹®ì67*rNÈ ¾¤ù2¾®Å{û=¦}ÿ÷$É.õÝÐqDêY*]Kïó¨É·Š€L Y»ìR[f¹¼ZvÖŸÒ'�‹Ýð‡„œVvÙ¡9;o§&ógî²—Ênj†€ü)�²diwý[Y°ȹÓ=_z3 ò­²ßuWÎ*0 � È�u-î–>éSÚm¾J’ä«,Pß3�ÁDꙜ™·ô¦»'Ò¥Èä¬ro±üŽ�ågxÈy!+ç­ÔäÁtÊZ¥Û¸6䛩IM‡ü¼´Q_¿ò`È¥¥——ö�<“雿ËÏ/…õzúgžL(–?›yÓ=/�ÆäüÙ]¤S®J’fk"uˆg „ì™ûóxÎO¿t ¹0o‡îöÎé¹7¿Î¢3½¤&“óÍ�S“šì²f.Î]9, ¤Kq¤Ó3WÈž¹)×f“ôÈMÈkérlÆeTŽLÏtM×<–·‹gAËehFæ˜ôÈ’!ãR“š\Ò7§äÞœšo„*}+a³ä`¦Öw�¹sg’佬^ÿQX«©CdL+½qt®!¿.ÞÑ1í�èÝ‹ƒd§?•ÉØtK¯<•š¼–½Jwwþ\TºÄCY¼XÚ+·e�fØru饒¾¹:‡†�áÓ½¾K.Éæ¡ô|äìbç.éšýó¶iJ@Ö©ï#l—ÊŸ’$og…úof2= ˆÔnR¿×rmNÈ5y65™œÛsRi—ø‹¹5'把Þ|7>—æÄ\?ïfD†dØ g³jŠçriNÌ­y)5y-Wå”<_üdBÎËEù}jÒ'ݦ»Æ 9!CËöô�F¿ý­Yýpí|�$y!fw3�wü9ëfKU�öob•žûxÈŽJËœ 뎺°A>J’ Ïl?úüÓŽ¯‘iGÞÊÂé”só—Ôä�Ù"ä G¥e²ÿœyñfùw’äØJnâæ7 ’iCîJï�ÎY(¤SÌ[ŽIK}Ès3/|.Iòrv*³XÝWÿœKªaκْiW^ÌéÙ?ûdHr4Z2 ëòÓ/Z S3ó¿½ê¾úæ5 ’QU$ð_”/Ø1©4 ñ«ê˜³n¶d@d@êÚ8ÎÃËÓïR‡UêÚ}~oµÌY7[2 2 uoPÓà+Må9zÉ€¨ªX›É ºÂ§ü……ªgκْ‘™õ&²?åó /ü÷uüwŸ�J7’=¹•÷f{±/x§ãŸ¾Ý€È€È€4tC¹"cyw–§Xüœw¸�«oκْ‘©dc¹çñ2ïð_•Âñ!SxŽ#Y¼:笛-�Ê7™‹³%ûr×p<{²I5œ°Ä€È€È€È€È€HÄ€HDÄ€HDÄ€HDDDDDDD2 D2 2 D2 2 D2 2 2 2 2 ĀȀHÄ€HDÄ€HDÄ€HDDDDĀɀɀȀɀȀȀȀȀȀ¨½ä®1nÕû¢j5×O/üÒ� ob@Tµ+ãÚ»àFP�·ˆ1 ªÚ•±ûrï¸TcMʂz-rß_ÄR#í�.Ä€¨ŠWÇ–œü’›B5ÂMéS×*e@ ˆªÇ<»ìø¡C5Ô uíÿ0 DU÷2ÖÍ»~øŠ›D5Àè,]×ËWÄ€¨ Ÿ…ì9 æî¯Ü,ªoçðô�õÊd@ ˆªnµ\p‘GV}çG^�gÜDªNofTÎÊw²XæªoU2 DU¹jÎË žÿX0 IuX8 ¤óìW#b@Tµ«ç«‚šÄ€É€©!«çK‚šä]Vt ˆªsõ¼ËAP“L¡«ƒ`@T�«çÏùÒaP¼èUëê¹6ï; j‚[¢j]=»3ÅaP£}Ì÷¢ê]Aó‘àFšäU÷*:–©ƒá]–p ˆª{]˜w5ØGüÌA0 r%]�?óB›Êt ˆD  §òž¡ Ÿ{<Ébƒ‘ÊŸ‡Ïcü�)¼ÛA½Ï|À{öïkiïPÃó\ζÎ"Õ½ÊÎËÀj!ìÛaÿ¾–ÖßÙa@¤j�ŒÂw IDD’‘ ˆd@$"É€HNF""ɀȀH2 ’‘ ˆd@$"IDD’‘‘d@$"É€HD2 ’ ˆ ˆ$""É€HD2 ’‘ ˆd@$IDD’‘ ˆd@$"É€H2 2 ’ ˆ ˆ$"É€HD2 ’‘d@d@$É€HD2 ’‘ ˆd@$"ɀȀH2 ’‘ ˆd@$"É€IDD’‘ ˆd@$"É€H2 2 ’2—dýºB/öt|d@$Íjvá}öž9 ôçüÌñ‘‘Tÿ W§Ð©< ¬ÁdÂjŽŽ ˆ¤YOÁãázM ÛóÂ?éìèÈ€Hšõܤøâ�Œ%„+øŠîwld@$Õ7{ðIñågÓýàhÇFDRý“pl�‹7rdd@$Õ? O©cá'twdd@$Õ? ·¬cáXÇEDÒì&a/>Ÿiᩎ‹ ˆ¤ÙOÃ'fZ´µ£""iöÓð¬|IoGEDÒì§á63,ø½c""©’i8/_N·àÇDDReñ÷Ó}»³#""©²‰xÁtß.äˆÈ€Hªl"îXöÍ Ž‡ ˆ¤J'âL-}s•ã!"©ò©ø§Ò—û82 ’*ŸŠ—–¾àhÈ€Hª|*î^|ñ–c!"©!S±_ñÅo I ›Œ/ÂAŽ„ ˆ¤†Mƫᎄ ˆ¤†MÆ> “#!"©a“qá.ÇAD’d@$I2 ’$"I2 ’$"I2 ’$I’‘$I’‘$I’ ˆ$É€H’ ˆ$É€H’ ˆ$ID’d@$ID’d@¤jŸ>‹³ Û³Ý[áwÍÍ@2—£."µ÷©³Jß|É•ÿßÎ݆ú=Æq†ƒ³í4s¶4r·œ5)œ'›ñÄi%·“mÅLrr3«=[Ér7YhžH E"ø3Ô)š›‰å&736»aÎßÏg‘šÎ9}{?øÿ¯ïïºþ׃oßß»ëý®Ÿî}<ß•"Î�Ð¥Û¤ÊAQ)бX8³üÜÈcèóó}{KßC ëÄ•…¢Rc±pq‡£êëE¬Ø§3NÓ§Ï”HQ)б]8‹oMh·ÎñL»u°Eîs�y:Û‘Ù :Åt7ºÔbƒîøc7Ãõ­²_„“,÷˜õŽ0ã© :Á�ú½/¾ÓoFe¢(�ÅX+œ�DÌiÚ=¦·W3½×tûÑìóE,µIÜf™ˆ "b»x©­’�ÍȧuŒü–Ư¨L%�¢k…sûË&w»ÄÑî¼!~r¯ìšØä+±Ë*GÙ-VG8_Ä¢pºˆw-ó–ˆ[÷"�N>› Œ´R)ŠHQŒî™l�mMs·çõE˜#âæçŠXÙä{;TG{ã³w‹ºÃsâ—¡�xO‰_M«Mô¢Rã³x¦¸Åk¶4�Õa…ˆëôë7ßvñt#�Gš‘KEœ6Šg#|-Þѯ_¿E†”T)J E1^K¨Ã9Ö´×!=Öþ­ÃÆF W5c·K,7CÄeaò?}u ¤(�Åx+›I–Xâ¼a‘Eœeµˆå7Y0¬ÿ b�…b›®p�ßĆaãœV)J E1ÞÊf‚-b럻^q„DœÙޝב# d�øÕKâÉv䱡ý}¢^½ºþ�@¨sèqatê´_èЩÓ•©¢R£¯pžñºkÍ6Ï=">ÇÙ)žp�ý-Z�Œ(�‰Íüüvä.±Ó¼p˜7ÅVS÷*�WÅÖ¡S(ÞqHX)â¢æ\ÊÚÊSQ)ŠÑW8S}ö—ÐV'G¸SÄ/6‹X¯k$�„'ElivûRÄ7~qÍ_f.�U"¾ua ¤(�ÅXTÈíí[~üàaÇ6W–úBìòŠ›Ú�åž­¥eîãçhiY9,2Ý3¶‰¯=:|¥}u®––3"Ì´ÞnqyxHKKgX¨¥¥/ÌÒÒ²¼²T”@ŠbôP—Fz¯žÿöv^û;fèµ&ÿسgÏs"Eñÿó;?‘–U˜³BIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333865 kiwi-11.1.8/doc/source/.images/kiwi-logo.png0000644000000000000000000004165215273675667015532 0ustar00‰PNG  IHDRaaÆÏ£bKGDÿÿÿ ½§“ pHYs.#.#x¥?vtIMEà ƒ§Œ IDATxÚܽy”\×}ßù¹ËÛªª«÷h @‚àjq%“E²IÛ²lɱ=>>™“±�dìIÚ‰sΜ±23ÉØ“ˆr<öÌØ“±Û‰=É8–lŠM-ÔB-÷±/½wWu­o»÷ίºÑ B‹m�ãäá¼S@u½ê÷îïþ¶ïïûûAðŸèxò±iùøÌÆ¿gOkÀ<À!'Ø l° ˜"@ôN,ó›^OG�6�Ùôã3æòïy˜éÇŸâoÒ!þSþò'~iZ+Ã58ö㸠ç®Gp½ÕbÜhr«…u!­’ -�S¢¸kãÖ!,‹ÆI2+Eî”ÈœÁºsÀq„8 ¼œEäç§ÿ¼áoØñ×&„™�>Ìô'ŠøÔcÓ“Îñ8¾OX·ÇI±Óö)múµ1‘4™vÎHG®Ài��©$žVH!·~±sà@¹Cd‘;db¥l¥¹”ÓÅqÑIqÁ래þøÌ¹õ¯øÌc�ðèÇŸü/WOžæáÇgÖ±!>Šã~áœo*ZçcfÈsΓ ÁXGfr2c°Öâ!@ ‰VO{HñÝݶ³€µ¸Ä¢ë™ðVs¼†1NˆÁk ~wúñ'¿ö_¤&<ùKÓ¨Žâ¡ßz‚§OK`�ƒ_~Ài‘›mÒ©€<�¸Ü‚³Xg±Ö‘ÛŒÜäkqÎB@"…D)…§5¾öQR"„@ Xœ»B@n Ö… “Jà%‚ðRŠªeRça¹|x¦�Ÿ¸¼a64÷?;!üm[’ý¦_Çé˜&+K\n°&Ç:‹sç,ÎYŒ5Xë°8Œu8Wøa!Zj<íú>žÖH©ÐÊ'7 n“�ç†ÌæXëpÎ!… µÄó=T j%C¯fJ­å¹{ É_›~|fà‰_~”øÕÏüç%„ÿ±�ðcûëÑÎOãø1$×%Ûý4V.ÀXÖ"¤(Ïf8\o‹EÏ XƯY˜#­ñ=�Ðó<%5�_"ÍbŒÍ6\„±†$ÏÉó¤YÜ&“¦ñµF+U<½ÕÌÑ ©ò3ápG‘âO§Ÿù7½ç`úñ™ÿ<„°r~æõ¤µ¿Šå!W’²¹×wY`±Î`­rŽÀ°.#IÚ �Cb¬ÀX0¹#w…6 @J…§AO¡çá{¥°JšÅ$Ykiž‘d9™É±Æa�C�_kOã{þÖ‡w c‹¦+t-·NrRÀ/=üøÌÛ~ø!~ðñÏþÍÂæ�òäcÓSÂñ ÄþtÜKš“csœÍq.Çapäà aP&ô«tã6iž!…¡ȭ#7—�sÞ‹~¤H))‡A¡ ¾GÉ�¨Dƒä&£“4pÎ’C’å¤YF–rkp¶Ð2-5Z üÞõZz\éã�5Ÿ /t™ó„¿ƒs¿ýƒ�¶ð™_z„GÿÅ“35aæðôƒ8~ -JÝ)/oLžâœ)t^ØÂVà@H|]¢¿2Žu–v·8¬…V’b{¾!Í A€C �s‚JR }BϧDôWFqš�ŒÉH²Þ™çdyFn /()Ð=�òµ‡§uáö…@H Α[C†…¶Á›OÖlèçâ9-ÔãÓ�ϼx¥ßû^ò¯ª½×áøç(t÷…y£š‘¥Ý^”SD:Æ!C´,!ð(*¥!ª•A Ä÷=º‰¡ÑNÈ�‡q'¹±Äi†sÑ”³!ð¼­4‡Åb(ÌŸ±ã Æïõ¶ÎßiLŽ1™ÉHÒ„vÒ¥Ý�é´cÚ"£±]Óœòâ$·;ç~}æðô/<òø“Ïþ½8ô_ÅL|fCΗ.¾¡ìš¢�ÉóžcuXWì0×ÛqZ)”¯ÑÚC¨Â7h=D³m°Äø¾¢Þʉ ”üÂsZ犗’nšÑï �ÃöÂÖ­}lÚ*ÂZWø‹í…º ¥Äáz‰Ÿ@0X$k ÈsKnmÏ„~ˆÈGìSï¼ôkægfO›~|¦õŸLf>ú0�||†'›~ðëÎ6¾¾ä:2.l\Ͼ[+É� Î!ÉkqÎJ3&1–G–ÇXgýQ؇”‚Jä�€v;£ÑÎ0αþ­nF’Å{RxxÚÇó‚ž&¸^~ÑDO?AàJËï@�u9Î9´çcmŠ1Ögž§¤6§} Ý(ï~kæð#Ÿÿ‡óÙÇüëÂz3sxúnáø‚V:’†„DKO(é�P ³–nžalN­Õáâb�NÓM»ÄI )5¥°ßñ<…R’ÜÚqN§k6bÿV'£ÓÍhvÒžGsÈž&ø:DˆâºuZQäO+ sÔMrúÊe´ö@h°°ä&Cˆ% JÖÆ¤qLkBй6L�'Þƒs¿3sxú–üúS¬v'ÿJ‚PI_pøX7�M†¤“>PR£¤B+…C`�ÃXKn{‘ŽÍÉ­ce-¡gH Q¨¨DU βX[a~µE’Œ-BT¥ZIZíœ,·$©a /$ úK#DA¹ÉXk×XkµQž ·c‹è@éÂ!{Z“¤Ok*Q€sàù%´ ©·ê$YŠÀN\�R)ŠàÀC8lUç~Ó�‰œ÷þÔ=û.~è7þÃé?|öÔ_�&Ìžfæðt<†c§ô\º;D ðµ‡V )%RJ|O÷br¯HŽ´*0hYØÞÅZ‹#g–yñØ9.-Ï#…¤b­_à=òÜ’¦†¸kq_!„ gœ™­;\‚Ï/¡”¦�¤k‹ˆ§z !PBଣÕMYYëP 4àJÓWÂ�4%É-©ãd/[! Ðź¹éÐò»¬�¹ ä0Ž�Íž¾ ೇüÿWö�d=ø ð0 “ì/vBâyRjà)Eø„¾G”|�(ð <'Ö&áÒb“¯¾ú§fgñu‰±¡ ¢0Ö9òÜÑM y.|�k+õçæÖ�¡„"PZ,­tX\él8aO+<-É­cv©Í¹¹5@ày ! Z¢TH²‹‡Â9‰uœÂ9ÕˆÁ‘àl—D´X>`0¡>ñäᇯèñÏáþö¯ðóÿãÞ~|lšúµÿÈÌáéƒÀÿ€£•îqúrša­A ‰‚2�½ì´ú”ŸrRŽBBÏÇת÷;Œ5¬6Z|ó�×™]YaÛÐ6Ƈñ´ÄYÈ�%M-Ý8§RòÐZ"$$™áÂB�v·‹P­œÑ´º)Ý$+BW­ð´B)E«�‘æc,A QJz¹J’uð´"ô=<ÏCi­}¤ôøX§±¶ÈU ““ÒayOj:‘Îpÿú?>öÀ­â“ÿ”[ºé{/„G>>ÃoþÖ´~ ¶¬\>ê_ë;²< Šª„a)%žÒDAH) (!ÕRH_ÉGiÂ9'œ3ÖºÅú*/{‹vœ±grå0B*‰uŽ,7ÄINh¢H#{‰U³“2»¼ÚCX5ÝD’åcA©uè[‘fŽ8±d¹%7­¾ç3Ø7IàGÄi °„¾G)(6Mú„AHDø:DÉ�_h…•+1•RÛ‘g]?þå~`ï{ÿ·?å3�=ü½ÂzR²÷?L!°ù¨‡ ÆXòuÜß9œ3¤Y”£AÊÑJ»NÉÂöWÜöѪªF.R£¥d­: ²þ²Šó ³îع³”Ã*ãCÃøžB 0º‰Á:A_)@+B�¥†ù•U$œD �43à@IµQJCšÒÌ�‡’’ÁÊ8åhk IÖÄÓŠÀ÷‰‚€( ŠŒ< (E%‚0$ð#þ�{2çñï�fO ààgú=rLlHMŽ1„pZ „§ÜâjKÖ–ƒòþ[;“£´:çÈ2C’䬬uÙ³c€¾’O«“’å‚nœÐívú"¢ ÄZ‡”…ãEB’Æ=-°dÖR <Ƈ·UŒÍéfu´t_czZ¢Dáa‹¸CJRЫ ’šBι"0� !$iY�MzypÑü7Ö¹£<Ο}7P¸þöxpðÀÎåÙd€tÎ) Ú ‡À�<¹�z»�8ºZÞáiUÖµÕ9]¯·e–¥ä¹¥R)£µ"IòÞnÈzU*•èt:ÌÍ.é?ÿ“W&vL�?týuùÂê�ómš'›Ì× öàƒÕˆz3&Í ižÑŽ» ö <ÏC+�çIlæ8úõYºÊá†4Y^‹«U¦Æw#¥¦“¬§kxž¦Ýˆ©–à TÓ¹ž±v—K®¹- EI^€„Öº¼®ð¤D)�|Üw¦“åj5ûØÌáéW¦Ÿ¹ðäáiù6‚ÐßÞ=ÉÌáéÝÀ=Ú¶ßsôiæ/Õƒ#§*ç›Ñ±c+c�ÌbCÜÉ„•]®Ù¹“´$hÚ�…¸ \ǹ"ñ¹YÙ-¿÷Ì©…ð•×NEÛ· 5o¼ö&.¾úyÚ³ÚsŽï亻·³¸Ú"N Æ:iŒpÑXR®ø}fŽ7¿>‹¬hÆïƘI½uÿªåAr“ÓìÎa]ÊÅÅÃÕ2R p…®„€ÜõM““ä…â4Ã×>ˆ¢Ìª¤ÄSå)’k´�Zy(R÷›Àù«h.¦ÉÏZåÔµúȲq¥óOµ+•á€hÔ£2VáŽ[ú <¼­z:àЮkè6SVÖ˜?·Âì©eV´ÖDY‡sÉ–Å7Æ µ&Ïs¬Í9}j¹|òÜ™æû©ý“\|e•ÖrÌÛÏ/p˽;©ö…´»9if‰“.ð=Ÿ¾R i`ál“Úb›0 HÚ9Ö‡£Cܺï&pŠv²ÈR}ŽÓ³5–kmûJµ!ÀZ�EÆnLN»›²Üèây µÇ@PA8Gž§(U Ji¤�¸@�ì-¹ðh{ïÌáé0ýøÌ¿ú¶B«q#[ÿw°ëºï¿fp²|@Hwï§åât]ä~ÿþ­ãôõÊÅ–±vk…Ýæ1³KçØ9º¿¤ÙUæÀ»Çh5[ÌŸ¯1z•…suÎÖX™ol\—$ Õj•F£x/‰3yqq–F³ÃÍï:È¥·ç9ûÂRabÃð@‰úZLýl�ùsËܰÇài�Rà‰WÒžÄ/kD ñ<|ϻѺDšwyþ…—øÆWÞ–»®Fé¢ÞÐMrÖš1If(I–³\oa¬at°OE¡httã6¡¯ t�xª·§­#öÈ·Ò»ÿè“�Mî‘�Ïý–Bè `tß=£ÿËÎn)ÃB‰Q©E9(Ùï(þFf¼¾Sø6´“…Æú†©FƒŠi,a±ýÁÈT‰k:#4V[Ì�®qìë³,Ÿ{'*ì)M’¥œ¹tž{¿ï&Þ{’¾íeœp$Î2Øaê9s/,ñõó/rûÍ·UB|-©úLÝ4B‚¥4U"÷ààî)&Gv‗^|•Ïüþs4æ[ OUÙûÀµxJqêâ ‹«j͘$Í ãIöïalh_—É�a¹¾@nºD¾·Í„ž‡’…qä´tGàd#ŸPMó‘/<öè?¯›,ÿð¯îªæ(ÚyãÀï¼ÛG}Þ·®µýjpÇñK¯rhêJA±è±æ8 A� *ûnde¶É ÿñ,ùÚåïð=OTËkKhÝÏ�}{9ÿúqnXmv)!:–îZÊÉå ¬.­±«¿Dà{ø¾fÛ�.œ«“4s¢��÷^�Ë«K<óå©Ï7IZ9Õˆ°äóò±9j�6Ý8'Í-ÖZ´’¼÷Öë܆’!Ý´C­9Kšw=¯Gt€�‚4Ï6¨;‡D�mSyÔÉþ«®±ÿîÿþ¹“÷oæÿü—¯mMÖöÞ1ºãÀ½ã?\ò²Øàß‹áxãÜ7©µð=ŸÐ�ŠÓ‹ü_ûxžGøŒOUùà?¹‘¾Ñ`ãÆZ+ãÑî6™[œã¶C°z"æí¯\by¶E»›24¡<‰hr—"„" Lè�/\àÌs ¬­1^dÛÈvrcùúë/2¿ºLXòÙ¶k€©w�ðê‘9–¬5:qFšåäÆrÏ�9°ë}ѹM©µæÉòQP$u¡_Â9K7MiÇ1­n—vÓIRºiB+4®SEXmþéÔc£êJ¨ëÞ3ö÷wÝ<ô>gè‘©¾ËSöj³ßá3+­y2“ø%"¿¼ Ùdƒ]W€mpáµ5ê‹…iê“~ì·ºÍÁUbÓbjr/&u\8wš‡ÈÈÙ¾£J»ž2º{˜ƒ·]ÏÐàs«çIÓǾ6ÇÒ…cy÷ý·sͶ=¼|ü5¾òêó �W¸áÐ6Æ °&3Vë]ºI±ð¦Ç̸qï~¼óÝXcivV¨µæÈó­4ZêÿB`œ-¢¥$#ÉÓ‚d�ç=ßá8OXoÍî}Ÿ=ò'Ïž}æ­÷Þ5²Í™"ù^SŠõV¬´æiÄ5ÊAý¥B¿„V!%2—2ª2´w¹¼1|S_9²žò;^Þì4Å[GÞâ�|�C·\‹*9j­%:fžû~òZju~Á´ªÒˆ;ì½{œê¾ “û'¸nêZÎÌžç™W¿ÉÈ@™;nÚÁÜr“Ók´[)Y~¹ êk�®¹–,Ohtçé¤ËHa (I–[í%ŠZwœf¤YÖƒEzЈ„��À‘–}²>Ù‰êü}à¼Ý©å×”½-Ž÷{O¬ä6e­»ÂZw)¡WF ‰q†Ü¦HaÙÌõ ÇBïÚŸžÚû>�®³Z–XZYdj÷œ³Œí&ËÛtòN{G°.ØŒ¡þ>Ðe†÷$蟃»¯¡S�yæÕoFŠ÷¾kŠ“çkys�N'C„E&,„` ¯�÷Þr·ìßGš¯±Ò¼H³Sgn©ÅJ½Cf bÁ@%`°/$39i–“æÝ4£'´Ú9Q (G~QÕë=O¾#pºžì|ê£�¼çáO<ùÕ§>ú¢�‡ö#Hq5ü%Ø0â»wÜݼÙã™j!Q› qgÆ·ú}dσƒœËº"Ÿ4ÑDa %3|]"4U*S4ã:R ª•€åf¡T‚�¯ÿÑ뜟ýûÞÆîÙÃÉó5^yök³„/ÆB†«Ütíu¼÷–Û©”<–ç˜_½ÀùÙ%.-5é&9ÖÀ@_Àþ©QŒ5Ì­4iuºiáC’,G)è+÷~R"¥BI‰ù0LÀRö#ÀW× ½HûذFâÛ-Þæõ.*WBŠ µ‹]�¼å¬+Î-ß°5ìUZnÊ¢¨›µà¹î ïëø’´e'¤BJç<¨„Pxøå2¹í�¥>J;¢’æØ3‹¼öͳ\÷À6î|×$§/Ôxõ�yÚ+1ÆJ}%®Ù½“GÞ}?S4ãY^?{’Ó/qq¾A»[È|­˜«29VåÒÒËõv¡™%Ë JÂð@Hµì‘d%¾ö µ‡¯{a¬Tä;Cá­ä7Ï|túÀô'fŽ_B¨Ê&µßÕ.–‹®¼ÂñÖçc·6ߥÛHé62òÄl¥*IÔïQôÅÐÎ2ÕÑ›;Lnß¡zB¤·Iy��à\z¾úV÷Íî½Ã-AˆöØ6C_°›©a�§N0ôþ)öß¾›¾m9§/­qòì*V8ÂɈo�];&ù¡{ Ò—srá9^=qš‹skE²–„Œ•¬–@À±sË…Ns’¬ÎÄp™áþc-«�„�J@äûh¥záë:oVb#œÒ;Ôrv°I�,™Äû�L�‚ù“Mwú›KÌ]sY\`÷ŽË;\\¡NEŠ œuŽ ¢Å¾w�Škï#ð{ôÄÞçd…¯yf·èÌËñ7ÇÎüY,_~îttìÄIoh ß>pß=�ýàƒõJ©” <áœAÚ˜áò^J;èŽ-1[ïò手\Zhl<c£ìݹ‡{oº™N~šWß8Í‘“K4Ú ¾VŒUè+˜~µF—z³K’šùÀ22XbçD?ÒÁÊZ‡‹‹-Ò̲jˆ²_�ÚT÷=ïòj0Cz9ïÌ?œ~rú×gb­}U’ÂlÕ„M¦IA¼–qᵚ{óó³¶µœ =‰”—?'z¦Åm­ÞÞc@KAÖÎÝ«Ÿ¾ÄOÍŠëîŸ�Þ;Ne((L–/¼™Ô½Ã»=~rôßÿák„AHœÄÌ|î‹Ñ?ýÕßÿ•_þó?ñ‘Gjžr¹i€ËFPÖ۰ɵµSäÆÒW*sËÞ=ìß¹L“—�|Ž3‹ V åÈãý÷\ÃøP™Åå'/¬pqa�n’“¤9yn(qç�;ivÞz{�Z3�§˜Ú6ÈXµ‚Cl@á²ÇmÚ|˜ŠÊmI¾Wtm(„ < É–ÈĹž­wpî…{üK nù|ÛI_ˆ ¤‹•nã³[$èØ _­÷ 8×óRRêSp'¾8ïfߪËCn×Ü1‚”?Ú$„l+²jrËÁ{'¹ñK¼ñùY‚ ÀZK£Ñàÿû6ÑMùwÿë�,Yk"ùé»F2¿}�$¯sÃÞdÝ._zâi^üâ ’Ô±ó®1½{;7^;ÆZ3áËO½Íìù&r@!*Šn’šS£LmäÈéEÎÎÖ°Ö¡µ¤/ ؽ}�£ýi™¢¶àœg :ç&Ûk#ål$GTbïžÐR‰‚#¾ÉI)0¹å…ß?cf߬»<±�Ä)éÖÍÏ&5À9ÛCFÙdbÄÆ$”ØXn,Î abåB‡¯ÿÛÓtÖRnžžÄ�ô&!¼³¿Ïd–;l7o|~–$Ièëë#Ë2œµüÏ¿öc?üèc£•ÔZ‰s}8×¥8î:x7FÔ9véÞ>r‘¾p‚úbÌ®[GxÏ{ع³Ÿ³—ÖøæŸžäâ[5òÌз«ÌÎ{ƸùÀÃ}äÆò•ϲÚè „À÷»· °gû0}¥Zùá!œÀZK–Ť¦¨9H!z�Œ%0:Q�üaà‰/}ì>´”B )¶$kYlxþ÷Nç Ç®ˆcåzãØ–4Œ¢›F”C1q]UTG#Qò‘º0OÝzæO6Ýù×WI3+-N)@9¤'ñƒ¢`ÿÊg.Qòñ£ò&ŸpõÄe`[ÄžÛ†9óò ­V‹(Šèv»Äq̧>ótÿ÷³?¹Ø‰ÛBkk5ÎB/BÈ_Uè yèÞÂà@Äöñ>pŽçka�‹o®ÐX‰‘ZpóM{xøþ+õ_zþ ínFè{ìœææ½S ô÷£„_4¦˜œ$Khw´“f¯pU,¾²¨ØI�R ѯ3OˆwÜÿ±/£Åºzë›§Ö¾øÇçš o5íI¹™·ïz²8¡ËRî¸yPî»wLŒì*as‡³li[B±ÿ¾qâfÆŸ�å«´VÓ èbÃs;xîÿ9ËmìÝÔôwu!”}®¹c„3/¯ôÚ .ož×Þ8Zò|ßÉ48�áS�3r®Ÿ¼éµ)EOªÓÍ8y¶F½ã„¥4 ø¸w;ïŸÞG–Y.Ì79zz•‰¡q&ÇÆÙ96J)ŒˆÓg “wé&-ÖZ5ÝqœbŒ# ||-Áõ|„REÑÇXl¤]¨èŸ9<½gúñ™3Z¨AQò$/þé…¥£_›ïŽV£ qEÈä¤ uFNÜЯnžžC;ËØÜ‘Åöê9†s˜Üá—wþ­Ýxϯv–3/­ å&IHÙ—V·ä ß껦� ¬IÚù–÷›Í–’B!P8aAxxzˆÜÔ1¦ƒ'#¦†oáÜê‹Ô×b–k:qAqÙ[eçþ†‡ªì˜Áåͦc 4ÎGî#Í2jK¼|üsËKÄiŒs¦ý¥@š¾’On3LÚkxENá4`IUæáRþ^àŒF !¥@ŠÓÏ/Õ_yâÂb UI)i…bݧHp­<÷&néW÷þÔµh_bs·áC6GVBˆ‚¿iMj©Ž†Üù‘Ýø¡âäsKl¹Ld¯-í¯Wm‹uŒí©0:Uáâ‘ú–ŸåÆl˜J�BH�C!]©ð[¶C¤&°ÉÍÎ9¤”KšrPe×Äå`-ʘ\ã%öŒ–©5ë¼tôMNÏ^`i­Ö«ü™�°¼“€’’�Û"‚P¡zPŒ5%, R!­+¨øVàŒ#ïWÆ-æwÿFK�R�trû•Ož:_”#\"Á¨Âœ!€Ž3ªº3R÷ÿ�ý…©èuD^Ñmå´–c¢~Ÿò€ÿÎbM(¹ñÁí¬\lÓXŒ·$‚›…iÍÕ5Á9è yàç¯ãìË+¼ö©E:�žò|kŽìÅBd‘¡%ì9@œ5h7,¯ÎœaîíS¼çÑ;¹ïÑ[°Î”4‹µEž|ö‹?†<7fVôK�@Ê➪•²WDÖ[¥B IDAT•n�ûZÔž¥,>·(¸ÞCØ~Ï)—\  …RjÉñ§çæâf÷6›RdZÄ› +eUéü·×_"¶îü<±œ{e…£_^`m¡‹T‚<³ l¹ýCSŒ][Ý(¢ô�GLÝ:Ä‘/Î_ö!”'¾£O(Ö-5;²z:¥öŵ !ˆõØy“3“RcŒFHkJܶï>þío|šÓoÍSˆœà­sGhwj¼uü4§Î,€x�‡ð{ÚÝ«M )P<­(Eší£eJ‘Æ™^Üè.;d!%ʰ¿EM"TÎiÙ÷éŸÒH!²nnν¶ºl�‹{ö8“�ëÞÎì8+~`‡ðKj«ÝwÅŽ}å‰ œ{y!QÕ‚¤exæ“§¸õ'9ðîñË©œ±l?ÐÏñg7ÛBmÅŽ6GÂWfïÚóPÒ"7]“¹T(¡¯À»,Ö¦H©((RÎehWå=?r€Êõ‚ZÚåÙÓ/�$ +ç[tRò$Çdްä±íÆ!Jƒ~�VYP+K¡& =úÊ>Z òÜn&E ^ é5¼ÛÙÃè "�Á”¥ F¼íZ Dk5i­^êÔ�´¨S;'…H´’ëþˆ¯·íïßjû)¼ó¯Õ¸ðzªw¬˜g-^«1uó0QÕÛØI~¤Ðžd�páœíÕh/£¨ßn\‚ÑWoýÒ°©Ÿ^þ“±;ƒ‡—ËâémÁž ÁØ5Œíà\ÆÐÐ$]ù"ív‚3†þJÄÐTÈùù%ÚÃä5ƒºg;¯Â"pÖ⬥ÙIY©wHRC·ÛÁ¾’„¾¦x„�_¬•+ ¢1k –¼GŽ0H#1‘Pb»Bˆµ…x­µœ¬mj±¹µMOÉþTX]óEiÀGáG*ÁÅ7jhOææ*GŽ 4è”ôF8)•`m!.zÇäúΩ¿;sd2C7i��jS6êŒx¡ñå1áyöÖê½5ëL8/ò!Ê•±Â€3Tü &ö2»ð:ý%J‘†~Áõ?õ¨.ŽóI‹S/4Šv\ã 6`f6P^z“¤,thJ¾ÇpµL%DËBx�#…EiQôÉYEIéœíÓÆX³t¶µ $›Â6Îì²–r{,­(÷ùø=æÅ•šP_ˆÑ�Üâ&\oƒŠfxg™[™Ä Õå…u°x²q¹~ Il¨Œß9D�%½¶X)PꊄËÅ×3eÑ—•›£q½ÑTYž‰N·­”�6ˆB71Z·†üÜæ–[¯½•f¼@³ÝÂ:ð€Räã‡)ŠF•$µÄIN£Þ%u’ÈÓH-{ ŠëŒ=×sØ*°¼Ö¦›¦l®¢zá¸=ÀÓ œ°8-BøÚYLs)®õÀ¤u!¸F7�÷”8$•^¤ð‚Ë!éfø9ˆY² …Î3yh€ýw�1¼³BžÙB¢èœ\<Ýdu¶‹Ðb£Î $ì¿w„Wž>ù5!íæ…ë�à¹Q÷d\&¾ÜøóíO}ìtúò³'Kiž‘çyÑQä)nºñ@÷oýèûë?ñ‘{WûËCâæ½7siõ4�v—o|ê8Ës-vTº®Šq©$ÙBJó\›¬�áEš�É £ú‰J¥Ð# <¢§!E—jµb­%IÒ‚†é,ÓËcVœÀ×ÖÚ¬>×]½RInšBˆv¤ThŒÃæWI«ã!µÙ¢QQ,^Tö8xß}Ã&7œ­Š¢ÍÊù6ŸÿÍc®µœô@>+ZÜúC“ ï¬\¢¾Ó3ÛÜ·ò[z« [k’X´õ�Ÿõ3_hnlíy8à¹^‹^yíhxôÄÙèýŸ~úbföJç@K*�Žð�™×¨-­qÍýÃ,7Û¬6:4³”•…�Ù6ybÊ>oW{÷¡R TBƇ+ôWÂ"ÿqÐM“Kr—A<�É ®¥�Y»–¬Ý+ž8o%ù¹Á(lå“[¼@½#f¿éáI¾ñïN_[¤�ŒÕ *Cáe8$1œ|v‰/þö —´óË´ÉP±çöa®ÿþm¨\~Çd-K q+ß6WãÌ&Z¥30qm?ïùé½|õ÷Oáœ#MS‚ Àó<Ò4ÿ÷'?5pÛ-×v?øÈ­«fQŠÕN�êÄ(?þ ¢ 3²¼ÍÊZ�K‹‹œÛ¿ÌìéjZ˜Øn‹ÈSCËYÚqÆr½M_9`t¨D_䣄ê!«ëŒ+ÕóPá.—#µÃe :W‚»TkÙ9R¹±YÏL·‘)o\½£"V ¹á}Û9ñln®Ãßøã3¼ôé T†šË‰»øfþí5r;:Pì½{DÜúÈʃ>É’ûÖÉZ/yùÓØv Šiœs[„à®rÍmÜÉ›OÏQ»ÔYÏ%PJm‘ÿð�>7ðÃ�Þ±:92EØÔ$Y“¥µYêíˆåó-O­0<Þϵ×_Ç­BjßWcnu™…•Ëõ5Ú�.Y–÷ºEõfL³•àŠjɧúD¾Æ×_�’BH„‡“V44‚LºW+­ä<Î-F ç7–ã¨"ºjn×-ƒè@pú…Öæ»nùl›æbâ¬)bgE·Ž*z¾°ŽÒ�'=´MÜøÀvÆKï`f_邲æ™ß;Î3Ÿ<É5ïæÎ�ì¦4ào�¨Œ{GðÐ?qóÓ|åwßîÑg݆yrÎ1;·¨.^ªå i»›1Pct@ÐÍ×8ûú)žùìó4j1#ー÷ƒwrèî}ì�`ß6KœÅ¬6ê,®®°ÒXc­Õ¤'a©”$C’§k´ÄúŠÀÓxEÃ*qV8×ÔR!EèlÂú ØåFüÒäxebîåšÙyhP]�Ÿ$€7 24YáÅ?9Ëég—­5©ÄFèj{˜Iiлo7>8)Æöôá…ªˆ„®�îæèÈ�/ú"/ýÙyÂ>�KG×xúwNðàÏ_‡ò·:æ«ÝÛþw�ñüÿ{ŽÎZеÙ›VPô­ˆ“Xôõ{œ¸ô‘dtp‚]ãã¼çþ»ñU‰?ù¿žæâÙžøw_àÄÊF'‡dl`€¡j?cÃ$YÂZ»A­Y§ÑY£ÙiÒ‰»8kQ²˜ÙdäÆ!„+²þÜYM� µ)FX¾c…O/6^»iÇÐýËóM=b-ÚqÃà·�™=îû™}âÎߣN?¿ìæŽ5\§ž"„ 2â‹íûÙv}¿è ÞA0r«¥særíùø×ùòïžÀY‡T€4–º|î7�qÛC{¶ø„+ÀZÇŽCì»g”ןšÝЂËq/y’šþ¾�ÙÅY.-Ïqfv€ë¦öpÏûoãà­×òÂW^ãì¹Y²Ðpvî"çç/ú>}eFúûÙ62ÄPµŠ§†é+UhuZÔZk´ºMLnÈ1(!H„phë ±K]ó~¤s uE#¡\+Í“vš¿~cyàæ×Ÿ^T•±Ðš(}K€  ÔïqÓô¤¸åvl<«ëAÖ¸^¸úÎHëjš°6ßå™ß;IÒÊ·ð’”4—bN|uñò çW¿§<1<ôó7°÷öŽ|y�ù#Ò4Ý@^;Ý­ú¸~Ç-HwŒ3s³\X¸Äù…Y¶�Œrã¾k¹ï‡ï@JG½¾Ê©K—¸¸¸D'ÎÈmÂÂê"�v�]øž‡1Å$ƒÈ÷Q¢JœÄ[ ]±›fnäÝ,3¯Ô/h¥ÅiyOäô"×MóöB£óÜä@iÿ­®??1³0Ìôx0¼½\,è·à­/øÕˆKâ*ŽElô(]¾¾µZPiÞ?ÁKŸ:_Ô-6]êIh-§ß1Ásà`u°Ì?º‹ãC+¼øçí Gýæ™#bhìzFvphO™Ð¯pôì)–Vê;užoœc÷ä8‡nÜͶ‰îÞõ;&hwštÓ.Yž'†<7¤iVL¦1¶€¼]ÁKRR£=Æ�éâ÷™bjÀÅ…Y÷ç_?ÏÃwÞÏ®±)rP嵓oòöÑ‹\|~…³ñ/î·¼o7·Þµ‡PGhQ–š<�‰|S`D¶ˆº²Ü1•“+Û×ërÕ Ý²Ò9{@ÿîÏ~ó ÀM½.w5ÂÝl½³ÒÉòoW‚í¾ÃcÚ^q´wÇR‘Pý“•huq1ùý™Oñž›o玃ßÇÞÉý”Ã>Bù ­³fß\¡±ØamµÍ\}�ý7Œ11Ø�ïùEÔ× Ï‹Ú�Bzï(0¦G86Š.µ$ìdÊC¾²>åÅQÌ”zøQÜCS“MgÖJ²3w^3v¿ªÒêNª^¿/O=±�½þ© ¦±a7¨*Bꢠ!½×MÎÐ-OÝFJ}®Ë©ç–yö�Îpâé5ÖjE»”–·~h{O÷®«Œœ~nÏ—½bI1ª³S/²ïr¥Ä�?¹“Ú«˜;A«–�¦)õÙ.çß\ÅKiÀ7|d¢¯Éê¥F|ìÔi±¸²ÌøÈ(ÃŒ�PÙÐÉÚ¤I†5`#IÓe¬4Û䦘 ³Q?ØL|ë½§•*ú¢uïÕÓ”.d*ŒÅ?ùƒçNÆëàû*�}øÃÞgŒÑ_þò——kµZ²Ù��­/7ºé¿ÞÖ_ù• ,é4Zç;¹‹-g>·dN}y¿O‹Ò '¢!_ˆ’ônšžë4Ùã¬Zc©Ïv9þµE 1µÙ&µÃc[8¬Bn­¶ í(!Ô:TÑû¾Ô ”Âæ“ÛoÛh”ç)Æå½ªÊ¦šˆ³è}zÇH< N~æ\íùK¯Ësgf¹ÿûïæ¶‡¸ãúwQíëã•£G¸x~çKâ4Ǭ9’4§ÙNª0PŽð´‡êÙVwÅ&� »V«Ø¾õ࿚©}ê}p£{Ó�w½ë]G’$Ñ÷ÜsÏT’$cÍf3ŠãX¾ùæ›'Ÿ~úéÖ¯|ê¥ßÿä?¸ÿ�¨*~ôÒñºñ…²ž/L¨0¹¡³”RŸí*ªÊ;øþm—cÍõòžqœyi•‹¯×‹‰½¡Æ ‹zÂf0îroÜfÛáù²GÁ)Þ‹S4~Y–#¾•ãŘOÙýÄ&a»^{SéºhÛàÛéâË�…‹Ëâ3O‰ÙåyÞ÷®wspçAÊA…—£×977W˜S >Y±mrkqÆÑ_‰´WÔ…¯æ.…@­dž³|`GýâÖÚ_þå_îöþúæú{¿ýÛ¿-'''o|à�¤Yæ-ù+_½S?Ÿ¿”JÛ ˆ`Ä'#¬˜*‰¨ê÷`\¶pšºk)^te BbìeöD©ZLòZ„Ò’Ó/,Ó®¥8í üÈÛ^–~k!Xã°™Ej�RlÁºÖq*¡…œ¸d{s®u6me&nƼ~êqšòè=÷15¾‹@û„Þœºtž4+œq7uÐèU3)è‹�çwr¥¬C5r)3ûy€wýÎKßy¬ÂÏýÜÏYàµÞÉ?zó®ª<6ö‹U96ª§­³–4Ӷ¦¬ÉÉÉ›-Q Pš«Ìu˜Ì¡}µõg‚-�ƒ7Ü¿�Þh‰bšpjøú¿=ãš+Ééx`2#SešóÙeÖž»zøÛmffHj¹Y[¸RÁ�ìxßÄÐÅ/-, O�e9ÇΞ"3)ÓwÞ˶á1ôõ·!'Ο¦þ0Érê͸àI‰„¢ÙüÊúxËhÙµ_űðÙ�NóÐ'fþâÓ ÃÆ€×�zmébµ}©›#aT–•JE %yY ƒCåTs©V§¸¨[̬Óà÷IäšØ°÷ÖZ|YæÌ…"ñÚ65ÂÁ‡F.û!øÚœ¦±ØÝ(�ø%EÿDÈ�nçÔ³µ�†tgÄU�rÚ1½JÞeZæ:Fµ9ŸqÎÑ¿¯o¨}©»ÖZí¦ëcN_¼Èù¹ïûngbpœ÷$ Ž�;E'n÷†­[­„È�ñ¤BÊ¢Yds¨j9²m>£´Œ?Л­úD¨´tibV–É›�§] µsYlâÆj×–óly%ï&u³º¶Œ+·q¥.Â)Ôêbvœ 1ÎÄð$ž¯�¨@P*•Y9×&Žcö\·“øÇ‡(�+”_„zÏýû³î­ÏÏY¥¤J8å WñÄ?²›RÕߨկœ[üˆ�‚å mžþíãÅD)zƒ«Äd¾%©t <ÔØMÃcÒ‰u´c—øâKßäÜâB_r`×îºáûv�±m´D_ÉÃ:G£ÓMŠé÷W˜"¡’óNðÒÒ}×S^Þ‘oIé°t/4Ú'ü< û‚ÝÎm2V�!òæjÇ¢s‰àå¸¾ÎÆÚ\SîÇsé®êó]æÎ-¢œÇõ·íá vÔg;¼ýì’;þÌ«:Nª^#‰€¾Ñ@ÜñáÝL ~1ÞÒ†u%œí¬ãÈççìɯ/¹ù·›êÑ_¼n2Gëšp™Û�³àMèêĶa=?·lloœPž[æ–k<ÿæëÜ~pÕò0#Õa”€KËý6iêÈ2K'É©” xBöè2Þbæd×¾öðo<õö_hÔÎUΩ¯•yþí¥Ï¾ÿ†É•}=æŠÿ߀A+²åù¦í63Ù7¼ƒ£ïgŒÜóæç/qnqÕ­Ìu¬‚•e8ö÷Îlø2Õ«MxžDGR(O‰Ò€æÐCÛÅþ{Æ/3�ߺX<ÙtǾ0o´’®½»/üïÇô­ïÙÄwWÅÁœu ÞÐ?Ò®µçši,�-=K-—VÖ(Ÿ=ÇõSž.ú!レ¶VH’.Ö:å­;œ¢˜:üóÝŽSò“3}„éOü%Í8'„H¥]ë\ëÕsËî`9ô´´²�V¶‚2ã:èžz~ ôªK›NŒ`p¢Ì­�N±íš§¤rÎà”.(iFÚE%í¼@¡%G÷õɉë«òÀ}£â½çZqÓƒ;н„펋}‡gÿàtÞ]ˬÔÂI)L}¶kŽuaãs…9²WÍâ퀘ڵM†žB©^œ/ij¸0_§ÖX#It’:žÖŒô S�ª”üßó6ú¼‚ðt×#çóÓŸxòM`CI!€¤J‰è,5ãÙókOyR´"OÛÈSÖ÷”Ý£Êíåãk¶¾ÐAx¢ÈÍ7�Ö9Jƒ>ßÿ³ûä‡íVuÃÂyÇ`K¡KJí½gX\sÇ�¸ëÇvsçGö°óÆ!„º uÞÑp¨Åÿì�ìÂkuDªGK—(!ÌÚ\×e‰Ý”'\½éÑJ'Çw …#CUÇi�gÛŽSj­¢[À$dÙÿ×ÞÕGu�ççœsϽw¿%$!É| °I� Ž�¸®™º©Y»Ó8;žéLû«�i 6 PÏ´?ˆ!CÛqfâü¨ãiÚÒv3. áñئ©=`SlŒâà !!#iW»Úû}ÎÛû!i‘° 6tš¾3wv´£Ý{÷<çóyŸ÷}‹EHÄHÆåH†&8ÄPÀD> ¹ÁûÏ⚦#­ˆøBr¯Â¼âDî½Y ±Ìü–ô½(3þ`Sz¤ûÀÅæ2ŠßÈ*1¼m·¤ÙM_ʶ(\òPÊ`‚!ž6�h´ËÂ^š†(¬WŽW}¿òUÙóK>IÙç�•e¼&c2m6„’“!11®Ü¦º8ŠË$6QÇ/Á4,!7 ´ÌH`É‚6,šÕ Çà¸a]«2#!¬á'Ù��§¯ÒÍ2Ý¢ü=Ûß?8Q?€Bý*�ÿù�ü§‘’ÿ¯1)¸””BgZlJ5Åð«ŸœŽŽîí#¯Ž�ˆO™¿gbla´ß¥÷^<Nì…~©œ½ú1n0H‹“,OE$$'.&0ÎTâU¯{ BUtGÁ™€åˆQ]‰ÏhmLaÎÌFD¤Ç¿ƒ¼¤`�r¿ÈîìÜýÒ¾uÅ6½Ú" ¯â�3êœ@žz娳�goïëhJþ™$ÞK|4ÐÅwK~÷ §÷ ò ¢yaЧZmo”LØœ§šíí=ÓFpr�ö‹!¹¹ýÇóQÿûy5:àia‚ &Q錆YNã\íë:Ôðó‘† QNéÏ”`¼Š•…ÛÓ7PH�ç�•G—Ð,,ð*Ó`Â2'Ü—4ìÓŽÉBú÷ìÎÎ-�½-¹ž/T¦¡žµk×.PJÅÇ1ƒ 0÷îÝÛ322â`ßyô…d—vßÖ2ãñx’ÏvOúʶ¹æ’3*15zÂU¹n‡ˆÚÔFh#vßwo_m+¬$ãÀ»ûüK§ Qè*€—Eš,øÁäŽÌx-ÌÅOù¹PÉŠ4&Ôš[iƒsÉÊÓ}ºÎó¹€y� SZL\ V�‚øXë”+YIí£¿-“Š Ëöß_Ý}÷Ýg …d¹\6l¸-‚”çy2 Cyâä©£sþíÍ?¥ï,í¢Qµ8aš¢ó PŒ1' õ˜ãÂJ™}Uî§•*$‡edÛRÛ¶Q«âE‘䦋ÂÑ % †°¦‹íyí’cHA„òÞÕ µœ·´�õŸÉÁ Ê·˜f$p �öeIšQÔª\±ºEØ a�r8 ©‹8þfõŽ.·\µð“‹å]+xðÁk«ÒÓO?�5kÖL‚}Ë–-ËÇn¿'m¸ïñ$GÁo†ôÇ µ“$Eò,.U.tAJò'§#à’#Ýn³±^§v¬bœQnÌO§l›Uý�Mdæqø~y#ß½«¿H¾V–%p„J ³Qš_iÆ;¿Œ,Û` "?RbŒ©äòì0úÎùœûëåÍw5§ÂÛ”F”ájìÜÙÞ ~/fW‚í\ h‚ò,‹ÄM·§Sá�;ðá(÷hw¼±1�%KÚÆ¹ ûÄXqY‹_-y†ß€ôŠûˆt‹ ­tY|Ŧ>1Ò®1šôŒ±Iéƒ*§}ããò‚ϸ«â ¼J‚6f·w]€}�^9'ö�Ð\°*ÕÁˆ€‚Œ½v¢ÿ¥›[ÓïÜ>§éÞ—MÉ‚ö.œ±g/®h^¥dÜÚƒÆ ‘p­d{kFÏ^pG!n^„Dº\Z¬æ*ešûn"óÕD|é²Ék¹¯©îÁ&¹7#îÏ*Å/൰RF€1@ž÷Á<p6à‡Ù�û ó±UÈnïÂê_]¡Ôë!Ü«SÎ Nœ,üzyGË’™-ñÇó_n[˜2 SÔ‰+‘T˜ýU Œ[\…”öú+Ž]Ò: 4”¡ ²!íavÌkƒi ¨°¬¦¨àô*¤¶Kã€T<èÈÅû÷)RŒ; "Aä"Á-ÀÙGàì€g³;: Õ´üÙí×V·ùú� “…Ó¨¿ Ÿ»tÐèåGæG ¿Ÿn�}gñ=톚"L‹T¹ágˆÍ±¬ø<Ûš¬i­ ½J&x5¶òõ�£ 8!•‡;¯›÷§Š�G‘f,ÐྖĘàö"€·²•ª´ûÖÙ)ȸÿÕ ¶è%MA…Þ˜®¾EZ{§ßyq³âÆ·}­Q𠌟žB»9�ð¸–Û‰Æ?:ã¤Î/vÙ ()¦8+qrâì»ÀðV(yé�í%Ø·.‹ÕŸ×„ü@éåSo^jšÀW]QF|úÍá§uD=�ëY£éIDATýÕ¢¯µÚâ®Åª‰'ꑌí˜3ᨲ:}Àn¦õK«žÚ_*7ú7`ã$èê�Ÿo‘ìëÂ_þã]ØùGo0À¬)Ψõrjb ìÃC#ÿ2Ôãü*Ñùó›W̼#9Þk'$ ³z˜£©UnÓû|@9aN�ªPÏ 8c8—+½nÆs¥ <‘ÝÑ9Týß®µ÷aÕSû±zç["žáúšà–Ê«¾Â¨ ºÉ†Ú%çߺ²í–æyÉ¥é{¡�’†É;¬¸œ”À°ÎUJ9”Ô‘Â%ïˆsÁë9ÈR¯¿~ls†�RJÄ“éGGsÃÏÖBöÞëa×ضm<ôÐC‹J¥Rl÷î݃õ³õÏFÁ0cB6Ίˆö˜L4š2Ùd5eÚìöxÆœ!¤ˆ&·¤%œÀ‰>8Y8_ ¯†CçJîùc9@´dÉÒ/Ÿ8qüŸ£(j–R"“ɬz 7ÈŒë}CÏó¢^xáø®]»šV¯^=/ŸÏÛ…B!‘Ïç‹{÷îýH©²À7ÝÛÛÖ>pÏ1x¦Xè£ÊëÄ´‹l Pk.۶܉£í½wÿ×@€çŸ?üð0€áê{Ï=÷\¢££ãæ*ûª”RQ *¥8EQ¸mÛ¶Þz¢“s^ ¿ý´ñBAåc:�ço$7¶ Tlûöíxì±Ç&½·uëVIDíJ)Q©FesΛµÖJ©`óæÍ®æ~ëûúú~EK&“=+W®\¹gÏžs¿Ñ ÔÛæÍ›ñÄOÔ“�̲,‹ˆ˜Öš¤”ñ ¾Î‹‘¥µö“Éäî�7Nlذ[·n­ýýÈ#�dºººv�ŒŒü�Ë–-û‹·ß~ûg¿ñ#áó‰ˆØ¦M›âœg¢(²PÎPðõ*¥Ä… ØÁƒm˲֞={ö Ã0œ;ï¼óÑüÃòåËqøðáÿáZlÛ¶mX¿~ýeï¯[·î)e£Rʈuww÷âÅ‹c¦iþç<ðÓgžyf`ÅŠìСCt#Ÿÿ]ì$Žæ@ÜÉIEND®B`‚././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333865 kiwi-11.1.8/doc/source/build_methods/build_based_on_containers.rst0000644000000000000000000002011315273675667022317 0ustar00.. _stackbuild: Building based on Containers ============================ .. note:: **Abstract** When building images, the {kiwi} `stash` and `stackbuild` commands provide an opportunity to address the following requirements: * Preserve the image rootfs for a later rebuild without requiring the original software repositories. * Build an image based on an existing container. * Build an image based on a container stack. * Transform a container into a {kiwi} image type. Requirements ------------ The `stash` and `stackbuild` commands are part of {kiwi}. Add the {kiwi} repo from the Open Build Service. For details, see :ref:`installation-from-obs`. The following package pulls in the host requirements for the `stash` and `stackbuild` commands, like `podman` and the OCI container tools: .. code:: bash $ sudo zypper in kiwi-systemdeps-stackbuild .. note:: The `stash` and `stackbuild` commands were formerly provided by the `kiwi-stackbuild-plugin`. The plugin is no longer needed and is replaced by the `kiwi-systemdeps-stackbuild` package. Concept ------- The design of the stackbuild concept is twofold: First, {kiwi} comes with a command called `stash` that allows you to store a kiwi-built root tree as an OCI container. OCI stands for *Open Container Initiative* and is a de facto standard format in the container world. Once the container has been created, it can be managed using the preferred container toolchain. The `stash` and `stackbuild` commands use `podman` to work with containers. As a next step, and with the root tree as a container, {kiwi} offers the opportunity to build images based on one or more containers. That's also the reason why the other command is called *stackbuild*, as it allows you to stack different root containers together. The `stash` and `stackbuild` commands can be used independently from each other. If there is already a registry with containers that should be used to build images from, `stackbuild` can directly consume them. This concept leads to a number of use cases, and a few of them were picked and put into the abstract of this article. For the purpose of documenting the functionality of the commands, only a part of the possibilities are taken into account as follows: .. _stash: Create a stash -------------- The `stash` command creates an OCI-compliant container from a given {kiwi-product} image root tree and registers it in the local container registry. From there, a user can push it to any registry of choice. The following example creates a stash of a Tumbleweed build and illustrates how to register it in a foreign container registry: .. code:: bash # Build some image... $ git clone https://github.com/OSInside/kiwi.git $ sudo kiwi-ng system build \ --description kiwi/build-tests/x86/tumbleweed/test-image-MicroOS/ \ --set-repo http://download.opensuse.org/tumbleweed/repo/oss \ --target-dir /tmp/myTWToday # Stash the image root into a container $ sudo kiwi-ng system stash \ --root /tmp/myTWToday/build/image-root \ --container-name twmos-snapshot # Register the stash in a registry $ podman login $ podman push twmos-snapshot \ docker://docker.io/.../twmos-snapshot:2021-10-08 If the `stash` command is called multiple times with the same container name, this leads to a new layer in the container for each call. To inspect the number of layers added to the container, the following command can be used: .. code:: bash $ podman inspect twmos-snapshot To list all stashes created by the `stash` command, the following command can be used: .. code:: bash $ kiwi-ng system stash --list Rebuild from a stash -------------------- The `stackbuild` command takes the given container(s) from the local or remote registry and uses it/them to either rebuild an image from that data or build a new image on top of that data. If multiple containers are given, the `stackbuild` command stacks them together in the order in which they were provided. .. note:: When using multiple containers, the result stack root tree is created from a sequence of rsync commands into the same target directory. The `stackbuild` command does this with any container content given and does not check, validate, or guarantee that the selection of containers is actually stackable or leads to a usable root tree. This means it's the caller's responsibility to make sure the provided containers can actually be stacked together in the given order. To simply rebuild the image from the stash created in :ref:`stash`, call `stackbuild` as follows: .. code:: bash # Delete the image $ sudo rm -rf /tmp/myTWToday # Rebuild image from stash $ sudo kiwi-ng system stackbuild \ --stash twmos-snapshot \ --target-dir /tmp/myTWToday This rebuilds the image from the stash and the {kiwi} configuration inside of the stash. As all rootfs data is already in the stash, the command will not need external resources to rebuild the image. Additional arguments for the `kiwi-ng system create` command, or for the `kiwi-ng system build` command if `--description` is given, can be passed after the `kiwi` subcommand. For compatibility with the former `kiwi-stackbuild-plugin`, the `--` separator is accepted as an alias for `kiwi`. For all options, see :ref:`kiwi_system_stackbuild` and :ref:`kiwi_system_stash`. Turn a container into a VM image -------------------------------- Another use case for the `stackbuild` command is the transformation of container images into another image type that is supported by {kiwi}. The following example demonstrates how an existing container image from the openSUSE registry can be turned into a virtual machine image. When moving a container into a virtual machine image, the following aspects have to be taken into account: 1. A container image usually has no kernel installed. 2. A container image usually has no bootloader installed. 3. A container image usually has no user configured. For a VM image, the mentioned aspects are mandatory. Therefore, the following {kiwi} image description contains this additional information that the container cannot provide: Create the {kiwi} description as follows: .. code:: bash $ mkdir container_to_VM_layer $ vi container_to_VM_layer/config.kiwi And place the following content: .. code:: xml The Author user@example.org Leap Container as VM false 1.99.1 zypper en_US us UTC To build the virtual machine image from the currently hosted Leap {exc_os_version} container at SUSE, call the following `stackbuild` command: .. code:: bash $ sudo kiwi-ng system stackbuild \ --stash leap:{exc_os_version} \ --from-registry registry.opensuse.org/opensuse \ --target-dir /tmp/myLeap \ --description container_to_VM_layer The resulting virtual machine image can be booted as follows: .. code:: bash $ qemu-kvm Leap-VM.x86_64-1.99.1.raw ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1794079670.7341 kiwi-11.1.8/doc/source/build_methods/build_self_contained.rst0000644000000000000000000002057715273675667021313 0ustar00.. _self_contained: Building in a Self-Contained Environment ======================================== .. note:: **Abstract** Users building images with {kiwi} face problems if they want to build an image matching one of the following criteria: * The build should happen as a non-root user. * The build should happen on a host system distribution for which no {kiwi} packages exist. * The build happens on an incompatible host system distribution compared to the target image distribution. For example, building an apt/dpkg-based system on an rpm-based system. * Run more than one build process at the same time on the same host. * Run a build process for a different target architecture compared to the host architecture (Cross-Arch Image Build). This document describes how to perform the build process in a self-contained environment using fast-booting virtual machines to address the issues listed above. The changes on the machine to become a build host will be reduced to the requirements of the {kiwi} `boxbuild` command. Requirements ------------ The `boxbuild` command is part of {kiwi}. Add the {kiwi} repo from the Open Build Service. For details, see :ref:`installation-from-obs`. The following package pulls in the host requirements for the `boxbuild` command, like QEMU and the tools for the supported sharing backends: .. code:: bash $ sudo zypper in kiwi-systemdeps-boxbuild .. note:: The `boxbuild` command was formerly provided by the `kiwi-boxed-plugin`. The plugin is no longer needed and is replaced by the `kiwi-systemdeps-boxbuild` package. Building with the boxbuild command ---------------------------------- The {kiwi} command named `boxbuild` uses KVM as a virtualization technology and runs the {kiwi} `build` command inside a KVM-controlled virtual machine. For running the build process in a virtual machine, it's required to provide VM images that are suitable to perform this job. We call the VM images `boxes`, and they contain kiwi itself as well as all other components needed to build appliances. Those boxes are hosted in the Open Build Service and are publicly available at the `Subprojects` tab in the: `Virtualization:Appliances:SelfContained `__ project. As a user, you don't need to work with the boxes because this is all done by the `boxbuild` command and provided as a service by the {kiwi} team. The `boxbuild` command knows where to fetch the box and also cares for an update of the box when it has changed. Building an image with the `boxbuild` command is similar to building with the `build` command. One part of the `boxbuild` command is exactly the same as with the `build` command. The separation between `boxbuild` and `build` options is done using the `kiwi` subcommand. Everything after `kiwi` is passed along to the `build` command running in the box. The following example shows how to build an example from the `kiwi-descriptions` repo: .. code:: bash $ git clone https://github.com/OSInside/kiwi-descriptions.git $ kiwi-ng --profile Virtual system boxbuild --box leap kiwi \ --description kiwi-descriptions/suse/x86_64/suse-leap-15.6 \ --target-dir /tmp/myimage For compatibility with the former `kiwi-boxed-plugin`, the `--` separator is accepted as an alias for `kiwi`. For all options of the `boxbuild` command, see :ref:`kiwi_system_boxbuild`. .. note:: The provided `--description` and `--target-dir` options are set up as shared folders between the host and the box. No other data will be shared with the host. Sharing Backends ---------------- As mentioned above, the `boxbuild` call shares the two host directories provided in `--description` and `--target-dir` with the box. To do this, the following sharing backends are supported: ``--9p-sharing`` With QEMU's `9pfs`, you can create virtual filesystem devices (virtio-9p-device) and expose them to the box. For more information, see `9pfs `__. Using this sharing backend does not require any setup procedure from the user and is also the default for `boxbuild`. ``--sshfs-sharing`` SSHFS is a FUSE-based filesystem client for mounting remote directories over a Secure Shell connection (SSH). In `boxbuild`, this is used to mount directories from the host into the box. Because this runs through an SSH connection, the host must allow connections from the box. If you plan to use `sshfs`, add the following SSH public key to the :file:`~/.ssh/authorized_keys` file of the user who is expected to call `boxbuild`: .. code:: bash echo "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCtiqDaYgEMkr7za7qc4iPXftgu/j3sodPOtpoG8PinwRX6/3xZteOJzCH2qCZjEgA5zsP9lxy/119cWXvdxFUvyEINjH77unzRnaHj/yTXPhHuhHgAiEubuHer2gZoOs+UH4cGJLKCrabjTjZdeK9KvL+hoAgJaWxDUvGsXYDQTBHXlKjniOL1MGbltDBHnYhu4k+PjjJ+UEBN+8+F74Y5fYgIovXXY88WQrybuEr1eAYjhvk/ln6TKw1P6uvVMuIbAGUgnZFntDCI91Qw8ps1j+lX3vNc8ZBoOwM6nHZqq4FAqbXuH+NvQFS/xDM6wwZQhAe+14dTQBA5F1mgCVf+fSbteb0/CraSGmgKIM8aPnK8rfF+BY6Jar3AJFKVRPshRzrQj6CWYu3BfmOLupCpqOK2XFyoU2lEpaZDejgPSJq/IBGZdjKplWJFF8ZRQ01a8eX8K2fjrQt/4k9c7Pjlg1aDH8Sf+5+vcehlSNs1d50wnFoaIPrgDdy04omiaJ8= kiwi@boxbuild" >> ~/.ssh/authorized_keys The public key mentioned here is associated with an SSH key pair we provide in the pre-built box images. .. warning:: If the `sshfs` backend is used without the host trusting the box, the `boxbuild` call will become interactive at the time of the sshfs mount. In this case, the user might be asked for a passphrase, or depending on the host's `sshd` setup, the request will be declined and the boxbuild will fail. ``--virtiofs-sharing`` QEMU virtio-fs shared filesystem daemon. Share a host directory tree with a box through a virtio-fs device. For more information, see `virtiofs `__. Using this sharing backend does not require any setup procedure from the user. .. warning:: virtiofs support was added but is considered experimental and not yet stable across the distributions. Feedback is welcome. Building in Container --------------------- By default, and also as the preferred method, boxbuild runs in a KVM virtual machine. However, support for building in container instances via `podman` is also implemented. The boxes are built as virtual machine images and also as OCI containers hosted on the openSUSE registry. In container mode, boxbuild pulls the requested box from the remote registry to the local registry and starts a container instance. The following example shows how to build the mentioned {kiwi} integration test image in a container: .. code:: bash $ kiwi-ng --profile Virtual system boxbuild --container --box leap kiwi \ --description kiwi-descriptions/suse/x86_64/suse-leap-15.6 \ --target-dir /tmp/myimage .. note:: The provided `--description` and `--target-dir` options are set up as shared volumes between the host and the box container. In addition, containers also share the kiwi cache from `/var/cache/kiwi` with the host for better performance. .. warning:: For building in a container, several runtime constraints exist, and the isolation model is not as strict as it is when building in a VM. Please read the following information to get clarity on the existing constraints. ``loop devices`` As kiwi requires loop devices and calls other operations that require root privileges, `podman` is started through `sudo`. As podman runs daemonless, the calling user must have the privileges to perform the needed kiwi actions. Non- privileged builds are therefore not possible in container mode with kiwi. ``linux capabilities`` Several Linux capabilities, as they can be found in https://man7.org/linux/man-pages/man7/capabilities.7.html, are set when `boxbuild` starts the container: * AUDIT_WRITE * AUDIT_CONTROL * CAP_MKNOD * CAP_SYS_ADMIN ``host device nodes`` The host device nodes from `/dev` are shared with the container instance. This is required to work with loop devices inside the container. Device isolation is therefore not possible in container mode with kiwi. The loop device handling for container-based builds restricts the number of simultaneous kiwi build processes on this host to the number of available loop device nodes exposed from the host kernel. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7333865 kiwi-11.1.8/doc/source/build_methods.rst0000644000000000000000000000216515273675667015150 0ustar00.. _build_methods: Alternative Build Methods ========================= .. note:: **Abstract** Besides building an image directly on the build host with the `kiwi-ng system build` command, {kiwi} provides alternative methods to build images. The following sections describe these methods and the use cases they address. .. toctree:: :maxdepth: 1 build_methods/build_self_contained build_methods/build_based_on_containers The `boxbuild` command runs the {kiwi} build inside of a self-contained virtual machine or container, called a box. This keeps the build host free from the build tool chain, allows building as a non-root user, and allows building images for other distributions than the one running on the build host. See :ref:`self_contained` for details. The `stash` and `stackbuild` commands store {kiwi} built root trees as OCI containers and build images on top of one or more of those containers. This allows rebuilding an image without access to the original software repositories, as well as turning existing containers into any other image type supported by {kiwi}. See :ref:`stackbuild` for details. ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1794079670.7341 kiwi-11.1.8/doc/source/commands/image_info.rst0000644000000000000000000001411315273675667016220 0ustar00kiwi-ng image info ================== .. _db_image_info_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng image info --help kiwi-ng image info --description= [--resolve-package-list] [--list-profiles] [--print-kiwi-env] [--ignore-repos] [--add-repo=...] [--add-repo-credentials=...] [--print-xml|--print-yaml|--print-toml] .. _db_image_info_desc: DESCRIPTION ----------- Provides information about the specified image description. If no specific info option is provided, the command lists basic information about the image. This information is also available in the image XML description file. Specifying an extension option like `resolve-package-list` makes a dependency resolver run through the list of packages, providing more detailed information about the image description. .. _db_image_info_opts: OPTIONS ------- --add-repo= Adds a new repository to the existing repository setup in the XML description. This option can be specified multiple times. - **source** Source URL pointing to a package repository that must be in a format supported by the selected package manager. See the URI_TYPES section of :ref:`kiwi_system_build` for details about the supported source locators. - **type** Repository type: `rpm-md`, `apt-deb`. - **alias** An alias name for the repository. If not specified, {kiwi} generates an alias name as a result of a hex representation from uuid4. While the hex is used to uniquely identify the repository, it is not descriptive. We recommend using descriptive aliases. - **priority** A number indicating the repository priority. How the value is evaluated depends on the selected package manager. Refer to the package manager documentation for details about the supported priority ranges and their meaning. - **imageinclude** Set to either **true** or **false** to indicate if the repository is part of the system image repository setup or not. - **package_gpgcheck** Set to either **true** or **false** to indicate if the repository should validate the package signatures. - **{signing_keys}** A list of `signing_keys` enclosed in curly brackets and delimited by a semicolon. The reference to a signing key must be provided in the URI format. - **components** A component list for Debian-based repos as a space-delimited string. - **distribution** The main distribution name for Debian-based repos. - **repo_gpgcheck** Set to either **true** or **false** to indicate if the repository should validate the repository signature. - **repo_sourcetype** Specifies the source type of the repository path. Supported values are `baseurl`, `metalink`, or `mirrorlist`. With `baseurl`, the source path is interpreted as a simple URI. If `metalink` is set, the source path is resolved as a metalink URI, and if `mirrorlist` is set, the source path is resolved as a mirrorlist file. If not specified, `baseurl` is the default. --add-repo-credentials= For **uri://user:pass@location** repositories, set the user and password associated with an add-repo specification. If the provided value describes a filename in the filesystem, the first line of that file is used as credentials. --description= The description must be a directory containing a kiwi XML description and optional metadata files. --ignore-repos Ignores all repository configurations from the XML description. This option is usually used together with the `--add-repo` option. Otherwise, there are no repositories available for processing the requested image information, which could lead to an error. --list-profiles Lists profiles available for the selected/default type. NOTE: If the image description is designed in a way that there is no default build type configured and/or the main build type is also profiled, it's required to provide this information to kiwi to list further profiles for this type. For example: kiwi-ng --profile top_level_entry_profile image info ... --print-kiwi-env Prints kiwi profile environment variables. The listed variables are available in the shell environment of the kiwi hook scripts. NOTE: The kiwi profile environment grows during the build process. When used in early stages, e.g., in a `post_bootstrap.sh` script, it can happen that not all variables have a value. The setup of the kiwi profile environment in the image info output can therefore also only list the static configuration values that are known at the beginning of a build process. --resolve-package-list Solves package dependencies and returns a list of all packages, including their attributes, for example, size, shasum, and more. --print-xml Prints the image description in the XML format. The specified image description is converted to XML and sent to the XSLT stylesheet processor. The result is then validated using the RelaxNG schema and the schematron rules. The command is normally used to convert an old image description to the latest schema. --print-yaml Behaves similarly to `--print-xml`, but after validation, the result is converted to the YAML format. The command can be used for different operations: * Conversion of the specified image description from or into different formats. This requires the `anymarkup` Python module to be installed. The module is not a hard requirement and is loaded on demand. If the module is missing, requests to convert to a format other than XML will fail. * Updates an old image description to the latest schema. --print-toml Behaves like `--print-yaml`, but the validated result is converted into TOML. This also requires the `anymarkup` Python module. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/image_resize.rst0000644000000000000000000000217215273675667016570 0ustar00.. _db_kiwi_image_resize: kiwi-ng image resize ==================== .. _db_kiwi_image_resize_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng image resize --help kiwi-ng image resize --target-dir= --size= [--root=] .. _db_kiwi_image_resize_desc: DESCRIPTION ----------- For disk-based images, this allows you to resize the image to a new disk geometry. The additional space is free and not in use by the image. The OEM boot code in {kiwi} offers a resizing procedure that can be used to make use of the additional free space. For OEM image builds, it is advisable to run the resizing operation. .. _db_kiwi_image_resize_opts: OPTIONS ------- --root= The path to the root directory. If not specified, kiwi searches for the root directory in `build/image-root` under the specified target directory. --size= The new size of the image. The value is either a size in bytes, or it can be specified with m (MB) or g (GB). Example: 20g --target-dir= The directory containing the kiwi build results. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/kiwi.rst0000644000000000000000000001353015273675667015070 0ustar00kiwi-ng ======= .. _db_commands_kiwi_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng --help | --version kiwi-ng [global options] image [...] kiwi-ng [global options] result [...] kiwi-ng [global options] system [...] .. _db_commands_kiwi_desc: DESCRIPTION ----------- {kiwi} is an imaging solution that is based on an image XML description. A description can consist of a single :file:`config.xml` or :file:`.kiwi` file. It may also include additional files, such as scripts or configuration data. A collection of example image descriptions can be found in the following GitHub repository: https://github.com/OSInside/kiwi-descriptions. Most of the descriptions provide a so-called appliance image. An appliance is a small, text-based image, including a predefined remote source setup to allow the installation of missing software components. Although {kiwi} operates in two steps, the system build command combines both steps into one to make it easier to start with {kiwi}. The first step is to prepare a directory that includes the contents of a new filesystem based on one or more software package sources. The second step uses the prepared contents of the new image root tree to create an output image. {kiwi} supports the creation of the following image types: - ISO Live Systems - virtual disk for, e.g., cloud frameworks - OEM expandable disk for system deployment from ISO or the network - filesystem images for deployment in a PXE boot environment Depending on the image type, different disk formats and architectures are supported. .. _db_commands_kiwi_opts: GLOBAL OPTIONS -------------- --color-output Uses escape sequences to print different types of information in colored output. For this option to work, the underlying terminal must support those escape characters. Error messages appear in red, warning messages in yellow, and debugging information is printed in light grey. --config= Use the specified runtime configuration file. This does not replace the standard lookup: the file's settings take the highest precedence. A path that does not exist is an error. Independent of `--config`, {kiwi} always reads and merges every one of the following files that exist, with later entries overriding earlier ones: :file:`/usr/share/kiwi/kiwi.yml`, every :file:`*.yml` in :file:`/usr/share/kiwi/kiwi.yml.d/` (alphabetically), :file:`/etc/kiwi.yml`, every :file:`*.yml` in :file:`/etc/kiwi.yml.d/` (alphabetically), and :file:`~/.config/kiwi/config.yml`. Both drop-in directories are scanned whether or not the corresponding main file exists. Merging is recursive: keys inside a section are merged with the same section from earlier files, values from later files override earlier ones, and lists are combined. See :ref:`runtime_config` for further details. --debug Prints debug information on the command line. Same as: `--loglevel 10`. --debug-run-scripts-in-screen Runs scripts called by {kiwi} in a screen session. --logfile= Specifies the log file. The logfile contains detailed information about the process. The special call `--logfile stdout` sends all information to standard out instead of writing to a file. --logsocket= Sends log data to the specified Unix Domain socket in the same format as with `--logfile`. --loglevel= Specifies the logging level as a number. Further info about the available log levels can be found at: https://docs.python.org/3/library/logging.html#logging-levels. Setting a log level displays all messages at or above the specified level. .. code:: bash ---------------------------- | Level | Numeric value | ---------------------------- | CRITICAL | 50 | | ERROR | 40 | | WARNING | 30 | | INFO | 20 | | DEBUG | 10 | | NOTSET | 0 | ---------------------------- --profile= Selects a profile to use. The specified profile must be part of the XML description. The option can be specified multiple times to allow a combination of profiles. --setenv= exports an environment variable and its value into the caller's environment. This option can be specified multiple times. --shared-cache-dir= Specifies an alternative shared cache directory. The directory is shared via bind mount between the build host and image root system, and it contains information about package repositories and their cache and metadata. The default location is `/var/cache/kiwi`. --temp-dir= Specifies an alternative base temporary directory. The provided path is used as a base directory to store temporary files and directories. The default is `/var/tmp`. --target-arch= Specifies an image architecture. By default, the host architecture is used as the image architecture. If the specified architecture name does not match the host architecture (thus requesting a cross-architecture image build), you must configure the support for the image architecture and binary format on the building host. This must be done during the preparation stage, and it is beyond the scope of {kiwi}. --type= Selects an image build type. The specified build type must be configured as part of the XML description. --kiwi-file= The basename of the kiwi file that contains the main image configuration elements. If not specified, kiwi uses a file named `config.xml` or a file matching `*.kiwi`. --version Show the program version. .. _db_commands_kiwi_example: EXAMPLE ------- .. code:: bash $ git clone https://github.com/OSInside/kiwi $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_disk} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/result_bundle.rst0000644000000000000000000000431315273675667016773 0ustar00kiwi-ng result bundle ===================== .. _db_kiwi_result_bundle_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng result bundle --help kiwi-ng result bundle --target-dir= --id= --bundle-dir= [--bundle-format=] [--zsync-source=] [--package-as-rpm] [--no-compress] .. _db_kiwi_result_bundle_desc: DESCRIPTION ----------- Creates a result bundle from the image build in the specified target directory. Each resulting image contains the specified bundle identifier as part of its filename. Uncompressed image files are also compressed as an XZ archive. An SHA checksum is generated for each resulting image. .. _db_kiwi_result_bundle_opts: OPTIONS ------- --bundle-dir= The directory containing the bundle results, compressed versions of image results, and SHA checksum files. --bundle-format= Specifies the bundle format to create the bundle. If provided, this setting will overwrite an eventually provided `bundle_format` attribute from the main image description. The format string can contain placeholders for the following elements: * %N : Image name * %P : Concatenated profile name (_) * %A : Architecture name * %I : Bundle ID * %T : Image build type name * %M : Image Major version number * %m : Image Minor version number * %p : Image Patch version number * %v : Image Version string --id= Bundle ID. It is a free-form text appended to the image version information as part of the resulting image filename. --target-dir= The directory containing the {kiwi} build results. --zsync-source= The download location of the bundle file or files. Only relevant if `zsync` is used to sync the bundle. * The zsync control file is created for the bundle files marked for compression. * All files in a bundle must be stored in the same download location. --package-as-rpm Create an RPM package containing the result files. --no-compress Do not compress the result image file(s). Note: Image files that were already produced as compressed variants stay compressed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/result_list.rst0000644000000000000000000000126715273675667016502 0ustar00kiwi-ng result list =================== .. _db_kiwi_result_list_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng result list --help kiwi-ng result list --target-dir= .. _db_kiwi_result_list_desc: DESCRIPTION ----------- Lists build results from a previous build or create command. During multiple image builds with the same target directory, the build result information is overwritten every time you build an image. This means that the build result list is valid for the last build only. .. _db_kiwi_result_list_opts: OPTIONS ------- --target-dir= Directory containing the {kiwi} build results. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_boxbuild.rst0000644000000000000000000001635215273675667017346 0ustar00.. _kiwi_system_boxbuild: kiwi-ng system boxbuild ======================= .. _db_kiwi_system_boxbuild_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system boxbuild --help kiwi-ng system boxbuild --box= [--box-memory=] [--box-console=] [--box-smp-cpus=] [--box-debug] [--container] [--kiwi-version=] [--shared-path=] [--no-update-check] [--no-snapshot] [--no-accel] [--9p-sharing | --virtiofs-sharing | --sshfs-sharing] [--ssh-key=] [--ssh-port=] [--x86_64 | --aarch64] [--machine=] [--cpu=] kiwi ... kiwi-ng system boxbuild --list-boxes .. _db_kiwi_system_boxbuild_desc: DESCRIPTION ----------- Build an image in a self-contained environment. The `boxbuild` command uses KVM to start a virtual machine and run the kiwi build command inside of that virtual machine. The provided `--description` and `--target-dir` options are set up as shared folders between the host and the guest. No other data will be shared with the host, which also allows for cross distribution builds. The boxbuild command provides the following additional features over the standard build command: * Build images independent of the host. * Build images as normal user not as root. * Build cross distribution images on one host. * Build in predefined build VMs called boxes which includes all components needed to build appliances. For running the build process in a virtual machine, it's required to provide VM images that are suitable to perform this job. We call the VM images `boxes`, and they contain kiwi itself as well as all other components needed to build appliances. Those boxes are hosted in the Open Build Service and are publicly available on the `Subprojects` tab at: https://build.opensuse.org/project/show/Virtualization:Appliances:SelfContained As a user, you don't need to work with the boxes because this is all done by the `boxbuild` command. It knows where to fetch the box and also cares for an update of the box when it has changed. For a detailed introduction, see :ref:`self_contained`. .. _db_kiwi_system_boxbuild_opts: OPTIONS ------- --box= Name of the virtual machine that should be used for the build process. --list-boxes Show available build boxes. --box-memory= Amount of main memory to reserve for the virtual machine. The value is passed as is to the QEMU `-m` option, so add a unit suffix, e.g. `8G`. By default, the memory setting of the selected box is used, which is 8096M for the boxes provided with {kiwi}. --box-console= Name of console in the kernel settings for the virtual machine. By default, set to hvc0. --box-smp-cpus= Number of CPUs to use in the SMP setup. By default, 4 CPUs will be used. --box-debug In debug mode, the started virtual machine will be kept open. --container Build in container instead of a VM. Options related to building in a VM will have no effect. --kiwi-version= Specify a KIWI version to use for the build. The referenced KIWI will be fetched from pip and replaces the box installed KIWI version. Note: If --no-snapshot is used in combination with this option, the change of the KIWI version will be permanently stored in the used box. --shared-path= Optional host path to share with the box. The same path as it is present on the host will also be available inside of the box during build time. --no-update-check Skip check for available box update. The option has no effect if the selected box does not yet exist on the host. --no-snapshot Run box with snapshot mode switched off. This causes the box disk file to be modified by the build process and allows to keep a persistent package cache as part of the box. The option can be used to increase the build performance due to data stored in the box which doesn't have to be reloaded from the network. On the contrary, this option invalidates the immutable box attribute and should be used with care. On update of the box, all data stored will be wiped. To prevent this, combine the option with the --no-update-check option. --no-accel Run box without hardware acceleration. By default, KVM acceleration is activated. --9p-sharing|--virtiofs-sharing|--sshfs-sharing Select sharing backend to use for sharing data between the host and the box. This can be either 9p, virtiofs or sshfs. By default, 9p is used. --ssh-key= Name of ssh key to authorize for connection. By default, 'id_rsa' is used. --ssh-port= Port number to use to forward the guest's SSH port to the host. By default, '10022' is used. --x86_64|--aarch64 Select box for the given architecture. If no architecture is selected, the host architecture is used for selecting the box. The selected box architecture also specifies the target architecture for the image build with that box. --machine= Optional machine name used by QEMU. By default, no specific value is used here and QEMU selects its default machine type. For cross arch builds or for system architectures for which QEMU defines no default like for Arm, it's required to specify a machine name. If you don't care about reproducing the idiosyncrasies of a particular bit of hardware, the best option is to use the 'virt' machine type. --cpu= Optional CPU type used by QEMU. By default, the host CPU type is used, which is only a good selection if the host and the selected box are from the same architecture. On cross arch builds, it's required to specify the CPU emulation the box should use. kiwi ... List of command parameters as supported by the kiwi-ng system build command. The information given here is passed along to the kiwi-ng system build command running in the virtual machine or container. The `--description` and `--target-dir` options are mandatory. Global options like `--type` or `--profile` must be passed as global options to the `boxbuild` call and are forwarded to the build command. See the example below how to provide options to the build command correctly. For compatibility with the former kiwi-boxed-plugin, the `--` separator is accepted as an alias for `kiwi`. .. _db_kiwi_system_boxbuild_env: ENVIRONMENT ----------- KIWI_BOXED_CACHE_DIR By default, VM disk images used as build environment are stored in the $HOME/.kiwi_boxes directory. To override this location, set the KIWI_BOXED_CACHE_DIR environment variable to a different absolute path. KIWI_BOXED_PLUGIN_CFG Path to a custom boxes configuration file. If not set, the configuration is searched in the following order: $PWD/kiwi_boxed_plugin.yml, $HOME/.config/kiwi/kiwi_boxed_plugin.yml, /etc/kiwi_boxed_plugin.yml and the default configuration provided with {kiwi}. .. _db_kiwi_system_boxbuild_example: EXAMPLE ------- .. code:: bash $ git clone https://github.com/OSInside/kiwi-descriptions $ kiwi-ng --profile Virtual system boxbuild --box leap kiwi \ --description kiwi-descriptions/suse/x86_64/suse-leap-15.6 \ --target-dir /tmp/myimage ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_build.rst0000644000000000000000000002205415273675667016631 0ustar00.. _kiwi_system_build: kiwi-ng system build ==================== .. _db_kiwi_system_build_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system build --help kiwi-ng system build --description= --target-dir= [--allow-existing-root] [--clear-cache] [--ignore-repos] [--ignore-repos-used-for-build] [--set-repo=] [--set-repo-credentials=] [--add-repo=...] [--add-repo-credentials=...] [--add-package=...] [--add-bootstrap-package=...] [--ca-cert=...] [--ca-target-distribution=] [--delete-package=...] [--set-container-derived-from=] [--set-container-tag=] [--add-container-label=...] [--set-type-attr=...] [--set-release-version=] [--signing-key=...] .. _db_kiwi_system_build_desc: DESCRIPTION ----------- Build an image in one step. The build command combines preparation and building steps, which makes it possible to create an image with a single command. The build command creates the root directory of the image under `/build/image-root` and writes a log file, `/build/image-root.log`. The resulting image files are created in the specified target directory. .. _db_kiwi_system_build_opts: OPTIONS ------- --add-bootstrap-package= Specifies a package to install as part of the early {kiwi} bootstrap phase. The option can be specified multiple times. --add-container-label= Adds a container label in the container configuration metadata. It overwrites the label with the provided key-value pair if it is already defined in the XML description. --add-package= Specifies a package to add (install). The option can be specified multiple times. --add-repo= Adds a new repository to the existing repository setup in the XML description. This option can be specified multiple times. For details about the possible option values, see the **--set-repo** information below. --add-repo-credentials= For **uri://user:pass@location** repositories, set the user and password associated with an add-repo specification. If the provided value describes a filename in the filesystem, the first line of that file is used as credentials. --allow-existing-root Uses an existing root directory from a previous build attempt. Use with caution, because this can cause an inconsistent root tree if the existing contents do not fit the previous image type setup. --clear-cache Deletes the repository cache for each of the used repositories before installing any package. This is useful when an image build validates the signature of the package from the original repository source for any build. Some package managers unconditionally trust the contents of the cache, which works for cache data dedicated to one build. In the case of {kiwi}, the cache is shared between multiple image builds on that host for performance reasons. --ca-cert= Adds a cert file to the directory storing additional local CA certificates. The import will occur immediately after the bootstrap process, where the required CA update tooling is expected to be installed. This option is useful for situations where certificates are not packaged, or the certificates are required during the build process, e.g., due to proxy servers in the build environment that need certificates in chroot. The option can be specified multiple times. --ca-target-distribution= Specifies the target distribution for the import of certificates via the `--ca-cert` option(s) and/or the provided `` from the image description. The selected distribution is used in KIWI to map the distribution-specific CA storage path and update tool for the import process. --delete-package= Specifies a package to delete. The option can be specified multiple times. --description= The path to an XML description. This is a directory containing at least one `config.xml` or `*.kiwi` XML file. --ignore-repos Ignores all repository configurations from the XML description. This option is used in combination with the `--add-repo` option. Otherwise, there are no repositories available for an image build, which leads to an error. --ignore-repos-used-for-build Works the same way as `--ignore-repos`, except that repository configuration with the `imageonly` attribute set to **true** is not ignored. --set-repo= Overwrites the first repository entry in the XML description with the provided information: - **source** Source URL pointing to a package repository that must be in a format supported by the selected package manager. See the URI_TYPES section for details about the supported source locators. - **type** Repository type: `rpm-md`, `apt-deb`. - **alias** An alias name for the repository. If not specified, {kiwi} generates an alias name as a result of a hex representation from uuid4. While the hex is used to uniquely identify the repository, it is not descriptive. We recommend using descriptive aliases. - **priority** A number indicating the repository priority. How the value is evaluated depends on the selected package manager. Refer to the package manager documentation for details about the supported priority ranges and their meaning. - **imageinclude** Set to either **true** or **false** to indicate if the repository is part of the system image repository setup or not. - **package_gpgcheck** Set to either **true** or **false** to indicate if the repository should validate the package signatures. - **{signing_keys}** A list of `signing_keys` enclosed in curly brackets and delimited by a semicolon. The reference to a signing key must be provided in the URI format. - **components** A component list for Debian-based repos as a space-delimited string. - **distribution** The main distribution name for Debian-based repos. - **repo_gpgcheck** Set to either **true** or **false** to indicate if the repository should validate the repository signature. - **repo_sourcetype** Specifies the source type of the repository path. Supported values are `baseurl`, `metalink`, or `mirrorlist`. With `baseurl`, the source path is interpreted as a simple URI. If `metalink` is set, the source path is resolved as a metalink URI, and if `mirrorlist` is set, the source path is resolved as a mirrorlist file. If not specified, `baseurl` is the default. --set-repo-credentials= For **uri://user:pass@location**-type repositories, set the user and password connected to the set-repo specification. If the provided value describes a filename in the filesystem, the first line of that file is used as credentials. --set-container-derived-from= Overwrites the source location of the base container for the selected image type. The setting applies only if the configured image type is set up with an initial `derived_from` reference. --set-container-tag= Overwrites the container tag in the container configuration. The setting is only effective if the container configuration provides the initial tag value. --set-type-attr= Overwrites/sets the attribute with the provided value in the selected build type section. Example: `--set-type-attr volid=some`. --set-release-version= Overwrites/sets the release-version element in the selected build type preferences section. --signing-key= Sets the key file to be trusted and imported into the package manager's database before performing any operation. This is useful when an image build validates repository and package signatures during build time. This option can be specified multiple times. --target-dir= The path to store the build results. .. _db_kiwi_system_build_uri: URI_TYPES --------- - **http://** | **https://** | **ftp://** A remote repository delivered via the HTTP or FTP protocol. - **obs://** An Open Buildservice repository. The source data is translated into an HTTP URL pointing to http://download.opensuse.org. - **ibs://** An internal Open Buildservice repository. The source data is translated into an HTTP URL pointing to download.suse.de. - **dir://** A local directory. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_create.rst0000644000000000000000000000272115273675667016774 0ustar00.. _kiwi_system_create: kiwi-ng system create ===================== .. _db_kiwi_system_create_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system create --help kiwi-ng system create --root= --target-dir= [--signing-key=...] .. _db_kiwi_system_create_desc: DESCRIPTION ----------- Create an image from the previously prepared image root directory. The `kiwi-ng system create` command is normally issued after the `kiwi-ng system prepare` command, and it builds the requested image type in the specified target directory. .. _db_kiwi_system_create_opts: OPTIONS ------- --root= The path to the image root directory. This directory is normally created by the `kiwi-ng system prepare` command. Keep in mind that if the specified directory is not created using the `kiwi-ng system prepare` command, {kiwi} stores image build metadata in the `image/` directory. This directory must exist for the `kiwi-ng system create` command to work correctly. --target-dir= The path to store the build results. --signing-key= A trusted key file to be imported into the package manager's database before performing any operation. This is useful if an image build validates repository and package signatures during build time. In the create step, this option only affects the boot image. This option can be specified multiple times. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_prepare.rst0000644000000000000000000002053315273675667017170 0ustar00kiwi-ng system prepare ====================== .. _db_kiwi_system_prepare_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system prepare --help kiwi-ng system prepare --description= --root= [--allow-existing-root] [--clear-cache] [--ignore-repos] [--ignore-repos-used-for-build] [--set-repo=] [--set-repo-credentials=] [--add-repo=...] [--add-repo-credentials=...] [--add-package=...] [--add-bootstrap-package=...] [--ca-cert=...] [--ca-target-distribution=] [--delete-package=...] [--set-container-derived-from=] [--set-container-tag=] [--add-container-label=...] [--set-type-attr=...] [--set-release-version=] [--signing-key=...] .. _db_kiwi_system_prepare_desc: DESCRIPTION ----------- Create a new image root directory. The prepare step sets up a new image root directory from the specified XML description. The specified directory acts as the root directory of the new image root system. You can enter the system as root via chroot using the following command: .. code:: bash $ chroot bash .. _db_kiwi_system_prepare_opts: OPTIONS ------- --add-bootstrap-package= Specify a package to install as part of the early {kiwi} bootstrap phase. The option can be specified multiple times. --add-container-label= Add a container label to the container configuration metadata. It overwrites the label with the provided key-value pair if it was already defined in the XML description. --add-package= Specify a package to add (install). The option can be specified multiple times. --add-repo= Adds a new repository to the existing repository setup in the XML description. This option can be specified multiple times. For details about the supported option values, see the **--set-repo** information below. --add-repo-credentials= For **uri://user:pass@location**-type repositories, set the user and password connected with an add-repo specification. If the provided value is a filename in the filesystem, the first line of the file is used as credentials. --allow-existing-root Allows you to re-use an existing image root directory. --clear-cache Deletes the repository cache for each of the used repositories before installing any package. This is useful if an image build validates the signature of the package from the original repository source for any build. Some package managers unconditionally trust the contents of the cache, which works for cache data dedicated to one build. In the case of {kiwi}, the cache is shared between multiple image builds on the host for performance reasons. --ca-cert= Adds a cert file to the directory storing additional local CA certificates. The import will occur immediately after the bootstrap process, where the required CA update tooling is expected to be installed. This option is useful for situations where certificates are not packaged, or the certificates are required during the build process, e.g., due to proxy servers in the build environment that need certificates in chroot. The option can be specified multiple times. --ca-target-distribution= Specifies the target distribution for the import of certificates via the `--ca-cert` option(s) and/or the provided `` from the image description. The selected distribution is used in KIWI to map the distribution-specific CA storage path and update tool for the import process. --delete-package= Specify a package to delete. The option can be specified multiple times. --description= The path to the {kiwi} XML description. The directory must contain at least a `config.xml` or `*.kiwi` XML description. --ignore-repos Ignores all repository configurations in the XML description. This option is normally used in combination with the `--add-repo` option. Otherwise, an image build operation results in an error. --ignore-repos-used-for-build Works the same way as `--ignore-repos`, but the repository configurations that have the `imageonly` attribute set to **true** are not ignored. --root= The path to the new root system. --set-repo= Overwrites the first repository entry in the XML description with the provided information: - **source** Source URL pointing to a package repository that must be in a format supported by the selected package manager. See the URI_TYPES section of :ref:`kiwi_system_build` for details about the supported source locators. - **type** Repository type: `rpm-md`, `apt-deb`. - **alias** An alias name for the repository. If not specified, {kiwi} generates an alias name based on a hex representation of uuid4. While the hex is used to uniquely identify the repository, it is not descriptive. We recommend using a descriptive and unique alias name. - **priority** A number indicating the repository priority. How the value is evaluated depends on the selected package manager. Refer to the package manager documentation for details about the supported priority ranges and their meaning. - **imageinclude** Set to either **true** or **false** to specify if the repository is part of the system image repository setup or not. - **package_gpgcheck** Set to either **true** or **false** to specify if the repository must validate the package signatures. - **{signing_keys}** A list of signing_keys enclosed in curly brackets and delimited by a semicolon. The reference to a signing key must be provided in the URI format. - **components** A component list for Debian-based repos as a space-delimited string. - **distribution** The main distribution name for Debian-based repos. - **repo_gpgcheck** Set to either **true** or **false** to specify if the repository must validate the repository signature. - **repo_sourcetype** Specifies the source type of the repository path. Supported values are `baseurl`, `metalink`, or `mirrorlist`. With `baseurl`, the source path is interpreted as a simple URI. If `metalink` is set, the source path is resolved as a metalink URI, and if `mirrorlist` is set, the source path is resolved as a mirrorlist file. If not specified, `baseurl` is the default. --set-repo-credentials= For **uri://user:pass@location**-type repositories, sets the user and password connected to the set-repo specification. If the provided value is a filename in the filesystem, the first line of that file is used as credentials. --set-container-derived-from= Overwrites the source location of the base container for the selected image type. The setting is only effective if the configured image type is set up with an initial derived_from reference. --set-container-tag= Overwrites the container tag in the container configuration. The setting applies only if the container configuration provides an initial tag value. --set-type-attr= Overwrites/sets the attribute with the provided value in the selected build type section. Example: `--set-type-attr volid=some`. --set-release-version= Overwrites/sets the release-version element in the selected build type preferences section. --signing-key= Sets the key file to be trusted and imported into the package manager database before performing any operation. This is useful if an image build validates repository and package signatures during build time. This option can be specified multiple times. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_stackbuild.rst0000644000000000000000000000712215273675667017656 0ustar00.. _kiwi_system_stackbuild: kiwi-ng system stackbuild ========================= .. _db_kiwi_system_stackbuild_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system stackbuild --help kiwi-ng system stackbuild --stash=... --description= --target-dir= [--from-registry=] [kiwi ...] kiwi-ng system stackbuild --stash=... --target-dir= [--from-registry=] [kiwi ...] .. _db_kiwi_system_stackbuild_desc: DESCRIPTION ----------- Build an image based on one or more stash containers. A stash is a {kiwi} built root tree stored as an OCI container, see :ref:`kiwi_system_stash`. Using OCI images as the portable format to store and distribute a root tree allows them to be pushed to and pulled from OCI registries. The `stackbuild` command operates in two modes: 1. Rebuild an image from a stash In this mode, `stackbuild` takes a stash, makes it available to the local registry, and builds the image using the stash rootfs and the image description stored in the stash. The given `kiwi` subcommand arguments are passed along to the `kiwi-ng system create` command. .. code:: bash $ kiwi-ng system stackbuild --stash NAME \ --target-dir /target/rebuild 2. Build an image based on a stash In this mode, `stackbuild` takes a stash, makes it available to the local registry, and uses the stash rootfs as the base for an image build of another image description. The given `kiwi` subcommand arguments are passed along to the `kiwi-ng system build` command. .. code:: bash $ kiwi-ng system stackbuild --stash NAME \ --description /some/image-description \ --target-dir /target/rebuild .. note:: The `stackbuild` command does not perform any consistency check to determine if the used stash rootfs is compatible with the provided image description. With that in mind, nothing prevents you from using a Leap stash and trying to build a Fedora image on top of it. It is the user's responsibility to combine only compatible stashes and image descriptions. For a detailed introduction, see :ref:`stackbuild`. .. _db_kiwi_system_stackbuild_opts: OPTIONS ------- --stash= Name of the stash. See `kiwi-ng system stash --list` for available stashes. Multiple stashes will be stacked together in the given order. --from-registry= Pull the given stash container name from the provided registry URI. --description= Path to the XML description. This is a directory containing at least one `config.xml` or `*.kiwi` XML file. --target-dir= Path to store the build results. kiwi ... List of command parameters as supported by the `kiwi-ng system build` command. The information given here is passed along to the `kiwi-ng system build` command if the `--description` option is set. kiwi ... List of command parameters as supported by the `kiwi-ng system create` command. The information given here is passed along to the `kiwi-ng system create` command if the `--description` option is not set. .. note:: For compatibility with the former `kiwi-stackbuild-plugin`, the `--` separator is accepted as an alias for the `kiwi` subcommand. .. _db_kiwi_system_stackbuild_example: EXAMPLE ------- .. code:: bash $ kiwi-ng system stackbuild --stash NAME --target-dir /target/rebuild \ kiwi --signing-key /path/to/key ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_stash.rst0000644000000000000000000000265215273675667016656 0ustar00.. _kiwi_system_stash: kiwi-ng system stash ==================== .. _db_kiwi_system_stash_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system stash --help kiwi-ng system stash --root= [--tag=] [--container-name=] kiwi-ng system stash --list .. _db_kiwi_system_stash_desc: DESCRIPTION ----------- Create a container from the given root directory. The command takes the contents of the given root directory at call time and creates an OCI container from it, called a stash. The stash is stored below `/var/tmp/kiwi-stash` and imported into the local container registry using `podman`. If a stash with the same container name already exists, a new layer is added to it. A stash can be used as the image root for the :ref:`kiwi_system_stackbuild` command. For a detailed introduction, see :ref:`stackbuild`. .. _db_kiwi_system_stash_opts: OPTIONS ------- --root= The path to the root directory, usually the result of a former system prepare or build call. --tag= The tag name for the container. By default, `latest` is used. --container-name= The name of the container. By default, it is set to the image name of the stash. --list List the available stashes. .. _db_kiwi_system_stash_example: EXAMPLE ------- .. code:: bash $ kiwi-ng system stash --root /tmp/mytest/build/image-root ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/commands/system_update.rst0000644000000000000000000000170315273675667017012 0ustar00kiwi-ng system update ===================== .. _db_kiwi_system_update_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng system update --help kiwi-ng system update --root= [--add-package=...] [--delete-package=...] .. _db_kiwi_system_update_desc: DESCRIPTION ----------- Update a previously prepared image root tree. The update command refreshes the contents of the root directory according to the repository setup of the image XML description. The update command can also be used to add or remove packages from the image root tree. .. _db_kiwi_system_update_opts: OPTIONS ------- --add-package= Specify a package to add (install). The option can be specified multiple times. --delete-package= Specify a package to delete. The option can be specified multiple times. --root= Path to the root directory of the image. ././@PaxHeader0000000000000000000000000000003100000000000010207 xustar0025 mtime=1794079670.7341 kiwi-11.1.8/doc/source/commands.rst0000644000000000000000000000077015273675667014127 0ustar00Working from the Command Line ============================= .. note:: This document provides a list of the existing {kiwi-product} commands for version |version|. .. toctree:: :maxdepth: 1 commands/kiwi commands/result_list commands/result_bundle commands/system_prepare commands/system_update commands/system_build commands/system_boxbuild commands/system_create commands/system_stackbuild commands/system_stash commands/image_resize commands/image_info ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/concept_and_workflow/customize_the_boot_process.rst0000644000000000000000000005417615273675667024211 0ustar00.. _working-with-kiwi-customizing-the-boot-process: Customizing the Boot Process ---------------------------- Most Linux systems use a special boot image to control the system boot process after BIOS or UEFI hands over control of the hardware to the operating system. This boot image is a compressed cpio initial RAM disk, and it's called the :file:`initrd`. The Linux kernel loads the :file:`initrd` into the RAM and executes :command:`init` or, if present, :command:`linuxrc`. Depending on the image type, {kiwi} creates the boot image automatically during the `create` step. To create the :file:`initrd`, {kiwi} uses a tool called `dracut`. dracut-generated initrd archives can be extended with custom modules to add functionality that is not natively provided by dracut itself. In the scope of {kiwi}, the following dracut modules are used: ``kiwi-dump`` Serves as an image installer. It provides the required implementation to install a {kiwi} image on a selectable target. This module is required if one of the attributes in the image type definition, `installiso`, `installstick`, or `installpxe`, is set to `true`. Shipped in the `dracut-kiwi-oem-dump` package. ``kiwi-dump-reboot`` Serves to boot the system into the installed image after installation is completed. Also shipped in the `dracut-kiwi-oem-dump` package, together with `kiwi-dump`. ``kiwi-live`` Boots up a {kiwi} live image. This module is required if the `iso` image type is selected. Shipped in the `dracut-kiwi-live` package. ``kiwi-verity`` Sets up dm-verity for images built with the `verity_blocks` or `embed_verity_metadata` type attributes, verifying the integrity of the root device at boot time. Shipped in the `dracut-kiwi-verity` package. ``kiwi-overlay`` Allows you to boot disk images with the attribute `overlayroot` set to `true`. A disk like that has its root partition compressed and read-only. The disk boots up using overlayfs for the root filesystem with a separate partition on the same disk for persistent data. Shipped in the `dracut-kiwi-overlay` package. ``kiwi-repart`` Resizes an OEM disk image after installation on the target disk to meet the size limits configured in the `oemconfig` section of the image description. The module takes over the tasks of repartitioning the disk, resizing RAID, LVM, LUKS, and other layers, as well as resizing the system filesystems. Shipped in the `dracut-kiwi-oem-repart` package. ``kiwi-lib`` Provides common functions used by dracut modules. Shipped in the `dracut-kiwi-lib` package, which the other modules depend on. .. note:: Using Custom Boot Image Support In addition to the standard dracut-based creation of the boot image, {kiwi} supports the use of custom boot images for the image types `oem` and `pxe`. The use of a custom boot image is enabled by setting the following attribute in the image description: .. code:: none Along with this setting, you must provide a reference to a boot image description in the `boot` attribute as follows: .. code:: none While {kiwi} supports this approach, it is recommended to use dracut instead. Keep also in mind that although {kiwi} supports the creation of custom boot images, {kiwi} does not include any official boot image descriptions. You can find an OEM boot description example at https://build.opensuse.org/package/show/Virtualization:Appliances:Images:Testing_x86:tumbleweed/custom-oem-boot-description and a PXE boot description example at https://build.opensuse.org/package/show/Virtualization:Appliances:Images:Testing_x86:tumbleweed/custom-pxe-boot-description. The custom boot image descriptions make it possible to completely customize the behavior of the initrd. This concept is mostly used in PXE environments that are usually heavily customized and require a specific boot and deployment workflow. Boot Image Hook-Scripts ....................... The dracut initrd system uses `systemd` to implement a predefined workflow of services documented in the bootup man page: http://man7.org/linux/man-pages/man7/dracut.bootup.7.html To hook in a custom boot script to this workflow, it is necessary to provide a dracut module that dracut picks when {kiwi} calls it. The module files can be provided either as a package or as part of the overlay directory in the image description. The following example shows how to include a custom hook script before the system rootfs is mounted. 1. Create a subdirectory for the dracut module: .. code:: bash $ mkdir -p root/usr/lib/dracut/modules.d/90my-module 2. Register the dracut module in the configuration file: .. code:: bash $ vi root/etc/dracut.conf.d/90-my-module.conf add_dracutmodules+=" my-module " 3. Create the hook script: .. code:: bash $ touch root/usr/lib/dracut/modules.d/90my-module/my-script.sh 4. Create a module setup file in :file:`root/usr/lib/dracut/modules.d/90my-module/module-setup.sh` containing the following: .. code:: bash #!/bin/bash # called by dracut check() { # check module integrity } # called by dracut depends() { # return list of modules depending on this one } # called by dracut installkernel() { # load required kernel modules when needed instmods _kernel_module_list_ } # called by dracut install() { declare moddir=${moddir} inst_multiple _tools_my_module_script_needs_ inst_hook pre-mount 30 "${moddir}/my-script.sh" } .. note:: Declaring Extra Tools for Hook Scripts The `install()` function called by dracut can define extra tools required by the specified hook script. The `inst_multiple` command and its parameters instruct dracut to include these extra tools and items into the initrd. The specified tools and items can be files. Normally, they are executables and libraries required by the hook script. * Each file must be included in the {kiwi} description either in a package, an archive, or in the root tree of the image description directory. * The parameters of the `inst_multiple` command are space-separated. * Each parameter can be a single executable name if it exists in the `/bin`, `/sbin`, `/usr/bin`, or `/usr/sbin` directories. * Otherwise, a full path to the file is required. This normally applies for libraries and other special files. When {kiwi} calls dracut, the :file:`90my-module` is installed into the generated initrd. At boot time, systemd calls the scripts as part of the :file:`dracut-pre-mount.service`. The dracut system offers many other possibilities to customize the initrd than shown in the example above. For more information, visit the `dracut project page `_. Boot Image Parameters ..................... A dracut-generated initrd in a {kiwi} image build process includes one or more of the {kiwi}-provided dracut modules. The following non-exhaustive list documents the most commonly used kernel boot parameters for these modules: ``rd.kiwi.term`` Exports the `TERM` variable into the initrd environment. If the default value for the terminal emulation is not correct, `rd.kiwi.term` can be used to overwrite the default. The environment is also passed to the systemd unit that calls dialog-based programs in {kiwi} dracut code, which means that the `TERM` setting applies there too. ``rd.kiwi.debug`` Activates the debug log file for the {kiwi} part of the boot process in :file:`/run/initramfs/log/boot.kiwi`. ``rd.kiwi.dialog.timeout=seconds|off`` Sets a timeout value for dialogs invoked by kiwi dracut modules. By default, the timeout is set to 60 seconds. If set to the special value `off`, the dialog will never time out. ``rd.kiwi.install.devicepersistency=by-*`` Instructs an OEM installation image to present the list of available disk devices using the device names as provided by the specified `by-*` representation. For example, `rd.kiwi.install.devicepersistency=by-path` would show the by-path device names in the list of available disks. ``rd.kiwi.install.efi_display_label=name`` Instructs an OEM installation image on EFI systems to create an EFI boot entry with the given name after the image was dumped to the target disk. Any existing EFI boot entry with the same name is removed first. The entry points to the partition labeled `EFI` on the target disk and to the first EFI binary found on it. Vendor specific loaders, e.g. `\EFI\opensuse\shim.efi`, are preferred over the removable media path `\EFI\BOOT`, and shim is preferred over any other loader. Requires `efibootmgr` to be present in the installation initrd. ``rd.kiwi.install.pxe`` Instructs an OEM installation image to look up the system image on a remote location specified in `rd.kiwi.install.image`. ``rd.kiwi.install.image=URI`` Specifies the remote location of the system image in a PXE-based OEM installation. ``rd.kiwi.install.pass.bootparam`` Instructs an OEM installation image to pass additional boot parameters to the kernel used to boot the installed image. This can be used, for example, to pass on first-boot configuration for a PXE image. Note that options starting with `rd.kiwi` are not passed to avoid side effects. ``rd.kiwi.install.partition_based`` Instructs an OEM installation image to deploy the disk image partition by partition instead of dumping the entire disk image as one blob to the target disk. In this mode {kiwi} clones the partition table and the MBR boot code of the image to the target disk, maps the partitions of the image and transfers each partition individually. Partitions containing a `btrfs` filesystem are transferred using the btrfs seed/sprout process as described in https://btrfs.readthedocs.io/en/latest/Seeding-device.html. All other partitions, e.g. the EFI partition, are copied block by block. After the transfer, the filesystem UUID of the original btrfs filesystem is restored on the target partition. The main purpose of this mode is to make the btrfs seed/sprout process available for the deployment of a system. The seed/sprout process requires the btrfs root filesystem of the disk image to be flagged as a seeding device. This flag is set by {kiwi} at build time and not by the installation system. Therefore, `rd.kiwi.install.partition_based` must be activated as part of the image description via the `kernelcmdline` attribute of the `` element. Passing the option only at boot time of the installation image is not sufficient. {kiwi} flags the root filesystem as a seeding device only for install images (`installiso`, `installstick` or `installpxe`) using the `btrfs` volume manager that set `rd.kiwi.install.partition_based` in `kernelcmdline`, for example: .. code:: xml ... The btrfs tools `btrfs` and `btrfstune` must be present in the installation initrd. If used in combination with `rd.kiwi.install.pxe`, the disk image is first fetched from the remote location into the main memory of the installation system as :file:`/image.raw`. From there the partition based deployment is performed. Make sure the system has enough main memory to store the complete uncompressed disk image. .. note:: Partition Based Mode Limitations * The result of the seed/sprout process is not bit identical with the disk image. Therefore, the verification of the installed image against the image checksum is skipped in this mode, regardless of the `oem-skip-verify` setting. * All partitions of the disk image are always deployed. This mode can't be combined with `rd.kiwi.install.retain_last` or `rd.kiwi.install.retain_past`. If one of them is set, the installation is aborted before any data is written to the target disk. * Compared to the default mode of dumping the entire disk image, the partition based deployment does not provide a significant performance gain. In combination with `rd.kiwi.install.pxe`, the deployment is even slower because of the additional transfer of the image into main memory. * Messages from the btrfs seed/sprout process are logged to :file:`/seed.log` inside the installation initrd. ``rd.kiwi.install.clone_local`` For OEM installation images in combination with `rd.kiwi.install.partition_based`. Instructs the installation to first copy the disk image from the install media into the main memory of the installation system as :file:`/image.raw` and to use this copy as the source for the partition based deployment. On this copy, the btrfs filesystems are explicitly marked as seeding devices prior to the seed/sprout process. This allows the partition based deployment from install media of images which have not been flagged as a seeding device at build time. Make sure the system has enough main memory to store the complete uncompressed disk image. This option has no effect for PXE-based installations, as these always use an in-memory copy of the disk image in partition based mode. ``rd.kiwi.install.retain_last`` Instructs an OEM installation to retain the contents of the last partition on the target disk. This setting is only useful if the last partition does not belong to the main OS, e.g., an extra data partition added via the `spare_part` attribute in the type setup of the image description. The implementation also checks if the start address of the last partition on the target disk matches the start address of the image to be deployed. Only if they match can the data on the last partition be retained. ``rd.kiwi.install.retain_past`` Instructs an OEM installation to retain the contents of the last partition on the target disk in a less strict mode than it is the case with `rd.kiwi.install.retain_last`. In the retain_past mode the partition to preserve must not be part of the initial image. It can retain the content of any last partition if its start address is past the OS image. .. note:: Retain Past Mode Limitations There are no sanity checks for the partition to retain in this mode. From an implementation perspective {kiwi} simply takes the last partition prior to the dump as reported by sfdisk and rebuilds this partition after the OS image is deployed. This can lead to a gap in the partition table between the OS end and the start of the retained partition. There is also no sanity checking when there are multiple partitions after the OS image. In this case, the last partition is retained, and all other partitions are removed. In contrast to `rd.kiwi.install.retain_last`, there is no tight coupling to the OS image partition table layout and as such `rd.kiwi.install.retain_past` should be used with caution. ``rd.kiwi.oem.luks.reencrypt`` For OEM LUKS2-encrypted disk images. If set, it re-encrypts the disk prior to an eventual resize and therefore creates a new key pool and master key. The re-encryption is advisable if the image binary is not protected. With access to the image binary, it's possible to extract the LUKS header, which then allows you to decrypt the data unless it was re-encrypted. The re-encryption process only runs if the checksum of the LUKS header still matches the one from the original disk image. Be aware that the re-encryption will ask for the passphrase if the image has been built with an initial LUKS passphrase. ``rd.kiwi.oem.luks.reencrypt_randompass`` For OEM LUKS2-encrypted disk images in combination with `rd.kiwi.oem.luks.reencrypt`. Resets the insecure built-time passphrase, set via the `luks=` attribute, with a random one-time passphrase that will be stored in memory at `/run/.kiwi_reencrypt.keyfile`. .. warning:: The passphrase will only persist as long as the system does not reboot. Using this option usually requires that the boot process implements code to set retrievable keyfile information for subsequent boot processes of this system, e.g., TPM setup or similar. ``rd.kiwi.oem.disk.consistency`` For OEM disk images providing an installation image. If set, the installation image will check against all disks that are not the selected target disk to see if there is any disk in the system that has the same PTUUID compared to the image that is about to be installed. If such a disk is found, this indicates that the same image was already installed to another storage disk on the same system, which will cause device ID inconsistencies for the entire system. In such a case, an error message is displayed, providing information about the conflicting device, and the installation will be canceled. ``rd.kiwi.oem.maxdisk=size[KMGT]`` Specifies the maximum disk size an unattended OEM installation uses for image deployment. Unattended OEM deployments default to deploying on `/dev/sda` (or more precisely, the first device that is not filtered out by `oem-device-filter`). With RAID controllers, you may have big JBOD disks along with a 480G RAID1 configured for OS deployment. With `rd.kiwi.oem.maxdisk=500G`, the deployment is performed on the RAID disk. ``rd.kiwi.oem.force_resize`` Forces the disk resize process on an OEM disk image. If set, no sanity check for unpartitioned/free space is performed, and also an eventually configured `` configuration from the image description will not be taken into account. The disk resize will be started, which includes re-partitioning as well as all steps to resize the block layers up to the filesystem holding the data. As `rd.kiwi.oem.force_resize` bypasses all sanity checks to detect if such a resize process is needed or not, it can happen that all program calls of the resize process end without any effect if the disk is already properly resized. It's also important to understand that the partition UUIDs will change on every resize, which might be an unwanted side effect of a forced resize. ``rd.kiwi.oem.installdevice`` Configures the disk device to use in an OEM installation. This overwrites or resets any other OEM device-specific settings, such as `oem-device-filter`, `oem-unattended-id`, or `rd.kiwi.oem.maxdisk`, and continues the installation on the given device. The device must exist and must be a block special. .. note:: Non-interactive mode activated by rd.kiwi.oem.installdevice When setting `rd.kiwi.oem.installdevice` explicitly through the kernel command line, {kiwi} uses the device without prompting for confirmation. ``rd.kiwi.oem.sha512`` Expect sha512 checksums and use the respective `sha512sum` tool and the `.sha512` extension for filename match and checksum calculation. By default sha256 is used. ``rd.kiwi.oem.smallest_disk`` Alters the logic that chooses the destination device during non-interactive mode deployments. The device with the smallest capacity gets selected instead of the drive with the alphabetical first device name. USB flash drives are filtered out. For big JBOD setups this parameter is an alternative to `rd.kiwi.oem.maxdisk`. ``rd.live.overlay.size`` Specifies the size for the `tmpfs` filesystem of a live ISO image that is used for the `overlayfs` mount process. If the write area of the overlayfs mount uses this `tmpfs`, any new data written during the runtime of the system is written in this space. The default value is `50%`, meaning half of the available RAM space can be used for writing new data. ``rd.live.overlay.persistent`` Instructs a live ISO image to prepare a persistent write partition. ``rd.live.encrypt`` Instructs a live ISO image to encrypt the persistent write partition. The boot process becomes interactive in this case, and the user has to specify a passphrase at boot time. ``rd.live.overlay.cowfs`` Specifies which filesystem of a live ISO image to use for storing data on the persistent write partition. ``rd.live.cowfile.mbsize`` Specifies the size of the COW file in MB. When using tools like `live-grub-stick`, the live ISO image is copied as a file on the target device, and a GRUB loopback setup is created there to boot the live system from the file. In this case, the persistent write setup that normally creates an extra write partition on the target will fail in most situations because the target has no free and unpartitioned space available. To prevent this from happening, a COW file (live_system.cow) of a partition is created alongside the live ISO image file. The default size of the COW file is 500MB. ``rd.live.cowfile.path`` Effectively used in isoscan loop-mounted live systems. For details on this type of live system, refer to :ref:`iso_as_file_to_usb_stick`. Specifies the path of the COW file below the `/run/initramfs/isoscan` loop mount point. If not specified, the cowfile is placed at `/run/initramfs/isoscan/live_system.cow`. ``rd.live.dir`` Specifies a directory that contains the live OS root directory. The default is `LiveOS`. ``rd.live.squashimg`` Specifies the name of the squashfs image file that contains the OS root. The default is `squashfs.img`. ``rd.kiwi.allow_plymouth`` By default, kiwi stops plymouth if it is present and active in the initrd. Setting `rd.kiwi.allow_plymouth` will keep plymouth active in the initrd, including all effects that might have on the available consoles. Boot Debugging '''''''''''''' If the boot process encounters a fatal error, the default behavior is to stop the boot process without any possibility to interact with the system. To prevent this, activate dracut's built-in debug mode in combination with the {kiwi} debug mode as follows: .. code:: bash rd.debug rd.kiwi.debug This must be set at the kernel command line. With these parameters activated, the system enters a limited shell environment when a fatal error occurs during boot. The shell provides a basic set of tools, and it can be used for inspection using the following command: .. code:: bash less /run/initramfs/log/boot.kiwi ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/packages.rst0000644000000000000000000002507515273675667020320 0ustar00.. _packages: Adding and Removing Packages ============================ In addition to the :ref:`repositories` setup, the package setup is required. {kiwi} allows the end user to completely customize the selection of packages via the `packages` element. .. code:: xml The `packages` element provides a collection of different child elements that instruct {kiwi} when and how to perform package installation or removal. Each `packages` element acts as a group whose behavior can be configured via the following attributes: - `type`: either `bootstrap`, `image`, `delete`, `uninstall`, `systemfiles` or one of the following build types: `docker`, `iso`, `oem`, `pxe`, `kis`, `oci`. Packages for `type="bootstrap"` are pre-installed to populate the image's root filesystem before chrooting into it. Packages in `type="image"` are installed immediately after the initial chroot into the new root filesystem. Packages in `type="delete"` and `type="uninstall"` are removed from the image. For details, see :ref:`uninstall-system-packages`. And packages that belong to a build type are only installed when that specific build type is currently processed by {kiwi}. - `profiles`: a list of profiles to which this package selection applies (see :ref:`image-profiles`). - `patternType`: selection type for patterns; supported values are: `onlyRequired`, `plusRecommended`; see: :ref:`product-and-namedCollection-element`. The following sections describe the different child elements of a `packages` group. .. _package-element: The `package` element ^^^^^^^^^^^^^^^^^^^^^ The `package` element represents a single package to be installed (or removed), whose name is specified via the mandatory `name` attribute: .. code:: xml This adds the package `udev` to the list of packages to be added to the initial filesystem. Note that the value that you pass via the `name` attribute is passed directly to the used package manager. Thus, if the package manager supports other means of how packages can be specified, you may pass them in this context too. For example, RPM-based package managers (like :command:`dnf` or :command:`zypper`) can install packages via their `Provides:`. This can be used to add a package that provides a certain capability (for example, `Provides: /usr/bin/my-binary`) via: .. code:: xml Whether this works depends on the package manager and on the environment that is being used. In the Open Build Service, certain `Provides` are either not visible or cannot be properly extracted from the {kiwi} description. Therefore, relying on `Provides` is not recommended. Packages can also be included only on specific host architectures via the `arch` attribute. {kiwi} compares the `arch` attribute's value with the host architecture that builds the image according to the output of `uname -m`. .. code:: xml This results in `grub2-x86_64-efi` and `shim` being only installed if the build host is a 64-bit x86 machine, but `grub2` will be installed independent of the architecture. .. _archive-element: The `archive` element ^^^^^^^^^^^^^^^^^^^^^ In certain situations, it is necessary to include additional packages into the image that are not available in the package manager's native format. {kiwi} supports the inclusion of regular tar archives via the `archive` element, whose `name` attribute specifies the filename of the archive ({kiwi} looks for the archive in the image description folder). .. code:: xml {kiwi} extracts the archive into the root directory of the image using `GNU tar `_. This means that only archives supported by it can be included. When multiple `archive` elements are specified, then they are applied in a top-to-bottom order. If a file is already present in the image, then the file from the archive overwrites it (same as with the image overlay). .. _uninstall-system-packages: Uninstall System Packages ^^^^^^^^^^^^^^^^^^^^^^^^^ {kiwi} supports two different methods for removing packages from the appliance: 1. Packages present as a child element of `` are gracefully uninstalled by the package manager, together with dependent packages and orphaned dependencies. 2. Packages present as a child element of `` are removed by RPM/DPKG without any dependency check, potentially breaking dependencies and compromising the underlying package database. Both types of removals take place after :file:`config.sh` is run in the :ref:`prepare step ` (see also :ref:`working-with-kiwi-user-defined-scripts`). .. warning:: An `uninstall` packages request deletes: * the listed packages * the packages dependent on the listed ones * any orphaned dependency of the listed packages Use this feature with caution, as it can cause the removal of required tools, leading to failures in later build stages. Removing packages via `type="uninstall"` can be used to completely remove a build-time tool (for example, a compiler) without having to specify all dependencies of that tool (as opposed to when using `type="delete"`). Consider the following example, where we want to compile a custom program in :file:`config.sh`. We ship its source code via an `archive` element and add the build tools (`ninja`, `meson`, and `clang`) to `` and ``: .. code:: xml The tools `meson`, `clang`, and `ninja` are then available during the :ref:`prepare step `, and they can be used in :file:`config.sh` (for further details, see :ref:`working-with-kiwi-user-defined-scripts`), for example, to build `foo_app`: .. code:: bash pushd /opt/src/foo_app mkdir build export CC=clang meson build cd build && ninja && ninja install popd The `` element ensures that the final appliance no longer contains the tools required to build `foo_app`, thus making the image smaller. There are also other use cases for `type="uninstall"`, especially for specialized appliances. For containers, you can remove the package `shadow` (it is required to set up new user accounts) or any remaining partitioning tools (`parted` or `fdisk`). All networking tools can be safely uninstalled in images for embedded devices without a network connection. .. _product-and-namedCollection-element: The `product` and `namedCollection` element ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ {kiwi} supports the inclusion of openSUSE products or of named collections (*patterns* in SUSE-based distributions or *groups* for RedHat-based distributions). These can be added via the `product` and `namedCollection` child elements, which both take the mandatory `name` attribute and the optional `arch` attribute. `product` and `namedCollection` can be used to shorten the list of packages that need to be added to the image description. A named pattern, specified with the `namedCollection` element, is a representation of a predefined list of packages. Specifying a pattern installs all packages listed in the named pattern. Support for patterns is distribution-specific and available in SLES, openSUSE, CentOS, RHEL, and Fedora. The optional `patternType` attribute on the packages element allows you to control the installation of dependent packages. You may assign one of the following values to the `patternType` attribute: - `onlyRequired`: Incorporates only patterns and packages that the specified patterns and packages require. This is a "hard dependency" only resolution. - `plusRecommended`: Incorporates patterns and packages that are required and recommended by the specified patterns and packages. The `ignore` element ^^^^^^^^^^^^^^^^^^^^ Packages can be explicitly marked to be ignored for installation inside a `packages` collection. This can be used to exclude certain packages from being installed when using patterns with `patternType="plusRecommended"`, as shown in the following example: .. code:: xml Packages can be marked as ignored during the installation by adding an `ignore` child element with the mandatory `name` attribute set to the name of the package. Optionally, you can specify the architecture via `arch`, similarly to :ref:`package-element`. .. warning:: Adding `ignore` elements as children of a `` or a `` element has no effect! The packages will still be deleted. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/profiles.rst0000644000000000000000000000466115273675667020363 0ustar00.. _image-profiles: Image Profiles ============== A *profile* is a namespace for additional settings that can be applied by {kiwi} in addition to the default settings (or other profiles), making it possible to build multiple appliances with the same build type but with different configurations. The use of profiles is advisable to distinguish image builds of the same type but with different settings. In the following example, two virtual machine images of the `oem` type are configured: one for QEMU (using the `qcow2` format) and one for VMWare (using the `vmdk` format). .. code:: xml 15.0 zypper Each profile is declared via the element `profile` that must be a child of `profiles`, and it must contain the `name` and `description` attributes. The `description` is only present for documentation purposes; `name`, on the other hand, is used to instruct {kiwi} which profile to build via the command line. Additionally, you can provide the boolean attribute `import`, which defines whether this profile should be used by default when {kiwi} is invoked via the command line. A profile inherits the default settings that do not belong to any profile. It applies only to elements that contain the profile in their `profiles` attribute. The attribute `profiles` expects a comma-separated list of profiles for which the settings of this element apply. Profiles can furthermore inherit settings from another profile via the `requires` sub-element: .. code:: xml In the above example, the profile `QEMU` inherits the settings from `VM`. For further details on the usage of *profiles*, see :ref:`building-build-with-profiles`. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/repository_setup.rst0000644000000000000000000001222415273675667022171 0ustar00.. _repositories: Setting up Repositories ======================= The repository selection is a crucial part of an appliance. {kiwi} allows the end user to customize the selection of repositories and packages via the `repository` element. Adding repositories ------------------- {kiwi} installs packages into an appliance from the repositories defined in the image description. This means that at least one repository **must** be defined. Otherwise, {kiwi} cannot fetch any packages. A repository is added to the description via the `repository` element, which is a child of the top-level `image` element: .. code:: xml The above example specifies two repositories: 1. The repository belonging to the {kiwi} project: *{exc_kiwi_repo}* at the Open Build Service (OBS). 2. The RPM repository belonging to the OS project: *{exc_repo_obs}* at the Open Build Service (OBS). The translated HTTP URL is also included in the final appliance. The `repository` element accepts one `source` child element that contains the URL of the repository in a correct format, along with the following optional attributes: - `imageinclude`: Specifies whether the repository should be added to the resulting image. The default is false. - `imageonly`: A repository with `imageonly="true"` is not available during the image build but is present in the resulting appliance. The default is false. - `priority`: An integer value for all packages in this repository. If the same package is available in more than one repository, the priority decides which one is used. How the value is evaluated depends on the package manager: zypper and dnf prefer the repository with the *lower* value, while apt prefers the repository with the *higher* value and follows the `Pin-Priority` semantics of `apt_preferences(5)`. - `alias`: Name to use for the repository. It appears as the repository's name in the image, visible via `zypper repos` or `dnf repolist`. If `alias` is not specified, {kiwi} generates an alias name using a hex representation from uuid4. - `repository_gpgcheck`: Specifies whether or not this specific repository values the result of the repository signature validation. The default value is `false`. - `package_gpgcheck`: Specifies whether or not this specific repository values the result of the package signature validation for each package taken from this repository. The default value is `false`. - `components`: Distribution components used for `deb` repositories. The default is `main`. - `distribution`: Distribution name information, used for `deb` repositories. - `profiles`: List of profiles to which this repository applies. - `customize`: A script to run custom modifications to the repo file or files. Repo files allow for several customization options, but not all of them are supported to be set by kiwi through the current repository schema. As the used options do not follow any standard, and they are not compatible between package managers and distributions, the only way to handle this is through a script hook that is invoked with the repo file as a parameter for each file created by {kiwi}. An example for a script call to add the `module_hotfixes` option for a `dnf`-compatible repository configuration could look as follows: .. code:: bash repo_file=$1 echo 'module_hotfixes = 1' >> ${repo_file} .. note:: If the script is provided as a relative path, it is expected to be found in the image description directory. .. _supported-repository-paths: Supported repository paths ^^^^^^^^^^^^^^^^^^^^^^^^^^ The actual location of a repository is specified in the `source` child element of `repository` via its only attribute, `path`. {kiwi} supports the following path types: - `http://URL` or `https://URL` or `ftp://URL`: a URL to the repository available via HTTP(S) or FTP. - `obs://$PROJECT/$REPOSITORY`: checks whether the repository `$REPOSITORY` of the project `$PROJECT` is available on the Open Build Service (OBS). By default, {kiwi} looks for projects on `build.opensuse.org `_, but this can be overridden using the runtime configuration file (see :ref:`runtime_config`). Note that it is not possible to add repositories using the `obs://` path from **different** OBS instances (use direct URLs to the :file:`.repo` file instead in this case). - `obsrepositories:/`: a special path only available for builds using the Open Build Service. The repositories configured for the OBS project where the {kiwi} image resides are made available inside the appliance. This allows you to configure the repositories of your image from OBS itself, without modifying the image description. - `dir:///path/to/directory` or `file:///path/to/file`: an absolute path to a local directory or file available on the host building the appliance. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/runtime_configuration.rst0000644000000000000000000001064315273675667023147 0ustar00.. _runtime_config: The Runtime Configuration File ------------------------------ {kiwi} supports an additional configuration file for runtime-specific settings that do not belong in the image description but which are persistent and are unsuitable for command-line parameters. The runtime configuration file must adhere to the `YAML `_ format. {kiwi} reads **every** one of the following locations that exists and merges them, with later entries overriding earlier ones: 1. :file:`/usr/share/kiwi/kiwi.yml` (vendor) 2. every :file:`*.yml` in :file:`/usr/share/kiwi/kiwi.yml.d/`, alphabetically 3. :file:`/etc/kiwi.yml` (administrator) 4. every :file:`*.yml` in :file:`/etc/kiwi.yml.d/`, alphabetically 5. :file:`~/.config/kiwi/config.yml` 6. the file passed via the global `--config` option `--config` does not replace the standard lookup, it is merged last and has the highest precedence. Both drop-in directories are always scanned, whether or not the corresponding main file exists. A system with no :file:`/etc/kiwi.yml` still loads :file:`/etc/kiwi.yml.d/*.yml`. This allows for modular configuration management, where different aspects of the configuration can be separated into different files. .. note:: Sections are merged key by key. A later file only needs to contain the keys it wants to change; all other keys of the same section from earlier files are kept. Lists, such as the `disable` list in the `runtime_checks` section, are combined from all files with duplicates removed. For example, if :file:`/usr/share/kiwi/kiwi.yml` contains .. code:: yaml bundle: compress: true shasum_size: "512" and :file:`/etc/kiwi.yml.d/10-local.yml` contains .. code:: yaml bundle: shasum_size: "256" then the effective `shasum_size` is ``"256"`` and `compress` stays ``true``. A default runtime config file in :file:`/usr/share/kiwi/kiwi.yml.example` is provided with the `python3-kiwi` main package. The file contains the available settings as comments, including a short description of each setting. .. _runtime_config_sparse_sync: The sparse data sync ^^^^^^^^^^^^^^^^^^^^ The `sparse` setting of the `sync` section asks the sync step that installs the image payload to turn sequences of null bytes in the source into holes on the target. It is off by default. Its effect depends on the presence of null runs in the source material and the target filesystem. The setting applies to every data sync {kiwi} performs, not only to the one that installs the payload. One typical system setup with many null runs is a Linux distribution compiled for a larger page size (e.g., for ARM 64K Kernels). This leads to many zero tails between elf binary segments. Filesystems differ in what is needed to make the holes stick. On a filesystem that performs speculative preallocation, syncing sparsely is not enough on its own, because the skipped range is eaten by speculative preallocation and is recorded as an allocated-but-unwritten extent. Such an extent reads as zero and is reported as a hole, yet it still occupies space. {kiwi} therefore lets a filesystem implementation declare the mount options it needs to prevent this, and applies them to the install time mount only, so that they do not become mount options of the running system. Of the filesystems {kiwi} supports, only `xfs` needs this. `ext4` and `btrfs` keep the holes without help. An encrypted image keeps the gain inside the filesystem, but by default not in the image file. The sync sees the null runs in the source and writes the payload sparsely either way, so the filesystem in the image accounts less and the running system has that space available. The image file does not become smaller by itself, because {kiwi} overwrites the whole device with random data before the filesystem is created, which allocates every block of the image whatever the data sync does. With ``luks_randomize="false"`` the image file accounts less as well. It is the randomization that limits the gain. However, if the sparseness savings allow for it, you can always make the overall image smaller in the build recipe. A compressing read-only filesystem, such as `squashfs` or `erofs`, typically gains nothing in addition, because the compression already removes the null runs. Sparse allocation can sometimes lead to additional fragmentation in the target filesystem and underlying image file. Do not blindly enable it, but check if your use case gains. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/shell_scripts.rst0000644000000000000000000003563215273675667021420 0ustar00.. _working-with-kiwi-user-defined-scripts: User-Defined Scripts ==================== .. note:: **Abstract** This chapter describes the purpose of the user-defined scripts :file:`config.sh`, :file:`images.sh`, :file:`pre_disk_sync.sh`, and :file:`disk.sh`. These scripts can be used to further customize an image in ways that are not possible via the image description alone. {kiwi} supports the following optional scripts that it runs in a root environment (chroot) containing an appliance: post_bootstrap.sh Runs at the end of the `bootstrap` phase as part of the :ref:`prepare step `. The script can be used to configure the package manager with additional settings that apply in the following chroot-based installation step, which completes the installation. The script can also be used for other tasks. config.sh Runs at the end of the :ref:`prepare step ` and after users have been set and the *overlay tree directory* has been applied. It is usually used to apply a permanent and final change of data in the root tree, such as modifying a package-specific config file. config-overlay.sh Available only if `delta_root="true"` is set on a `docker` or `oci` build type that also uses `derived_from`. In this case, the script runs at the end of the :ref:`prepare step ` prior to the `umount` of the overlay root tree. It runs after `config.sh` (if specified), and it is the last entry point to change the delta root tree. config-host-overlay.sh Available only if `delta_root="true"` is set on a `docker` or `oci` build type that also uses `derived_from`. In this case, the script runs at the end of the :ref:`prepare step ` prior to the `umount` of the overlay root tree. The script is called **NOT CHROOTED** from the host with the image root directory as its working directory. It runs after `config.sh` and `config-overlay.sh` (if any or both are specified), and it is the last entry point to change the delta root tree. images.sh Executed at the beginning of the :ref:`image creation process `. It runs in the same image root tree created by the prepare step, but it is invoked whenever an image needs to be created from that root tree. It is normally used to apply image-type-specific changes to the root tree, such as a modification to a config file that must be done when building a live ISO but not when building a virtual disk image. pre_disk_sync.sh Executed for the disk image type `oem` only, and it runs right before the synchronization of the root tree into the disk image loop file. The :file:`pre_disk_sync.sh` can be used to change the content of the root tree as a last action before the sync to the disk image is performed. This is useful, for example, for deleting components from the system that were needed earlier or cannot be modified afterward when syncing into a read-only filesystem. disk.sh Executed for the disk image type `oem` only, and it runs after the synchronization of the root tree to the disk image loop file. The `chroot` environment for this script call is the virtual disk itself and not the root tree. The script :file:`disk.sh` is normally used to apply changes to parts of the system that are not an element of the file-based root tree, such as the partition table, the contents of the final initrd, the bootloader, filesystem attributes, etc. Two further hooks have variable file names. Their paths come from the `editbootconfig` and `editbootinstall` attributes of the `type` element, and both are called **NOT CHROOTED** from the host with the image root directory as the working directory: editbootconfig Called after the bootloader configuration has been written into the system image and before the root tree is synced into the disk. Two arguments are passed: the boot filesystem name and the boot partition number. This hook is also invoked for the live ISO build type. editbootinstall Called as the very last step of the disk build, after the bootloader has been installed. Two arguments are passed: the disk image file name and the device node of the boot partition. Only invoked for the `oem` disk build type. {kiwi} executes scripts via the operating system if their executable bit is set (in that case, a shebang is mandatory); otherwise, they are invoked via the Bash shell. Scripts that are called **NOT CHROOTED** are always invoked via the Bash shell. If a script exits with a non-zero exit code, {kiwi} reports the failure and aborts the image creation. Developing/Debugging Scripts ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Creating a custom script may require some experimenting and testing. To help developers with this task, {kiwi} calls scripts associated with a `screen` session. The connection to `screen` is only done if {kiwi} is called with the `--debug-run-scripts-in-screen` option. In this mode, a script can be started using the following template: .. code:: bash # The magic bits are still not set echo "break" /bin/bash Calling the script executes a `screen` session, which gives you access to the break-in shell. You can then implement the desired script code in this environment. Once the shell is closed, the {kiwi} process continues. In addition to providing a fully featured terminal throughout the execution of the script code, you also have control of the session during the process of the image creation. Listing the active sessions for script execution can be done as follows: .. code:: bash $ sudo screen -list There is a screen on: 19699.pts-4.asterix (Attached) 1 Socket in /run/screens/S-root. .. note:: As shown above, the screen session for executing the script code provides extended control, which can be considered a security risk. Because of that, {kiwi} only runs scripts through `screen` when explicitly enabled via the `--debug-run-scripts-in-screen` switch. In production, all scripts must run natively and must not require a terminal to operate correctly. Script Template for config.sh / images.sh ----------------------------------------- {kiwi} provides a collection of methods and variables that offer custom actions. For details, see :ref:`image-customization-methods`. The following template shows how to import this information into a script: .. code:: bash #====================================== # Include functions #-------------------------------------- test -f /.kconfig && . /.kconfig ... .. warning:: Modifications of the unpacked root tree Keep in mind that there is only one unpacked root tree the script operates in. This means that all changes are permanent and are not automatically restored. .. _image-customization-methods: Functions and Variables Provided by {kiwi} ------------------------------------------- {kiwi} creates the :file:`.kconfig` file to be sourced by shell scripts. :file:`.kconfig` contains several helper functions that can be used to simplify image configuration. Functions ^^^^^^^^^ The :file:`.kconfig` file provides a common set of functions. Functions specific to SUSE Linux Enterprise and openSUSE start with `suse`; functions applicable to all Linux distributions start with `base`. The following list describes the functions provided by :file:`.kconfig` that are supported for use in image scripts. Additional functions are defined in :file:`.kconfig`, these are {kiwi} internals and may change without notice. baseSetRunlevel {value} Set the default run level. baseStripAndKeep {list of info-files to keep} A helper function for the `baseStrip*` functions that reads a list of files to check from stdin for removing params: files that should be kept. baseStripLocales {list of locales} Removes all locales except for the ones given as the parameter. baseStripTranslations {list of translations} Removes all translations except those given as the parameter. baseStripUnusedLibs Removes libraries that are not directly linked against applications in the bin directories. baseUpdateSysConfig {filename} {variable} {value} Updates the contents of a sysconfig variable. baseSystemdServiceInstalled {service} Prints the path of the first found systemd unit or mount with the name passed as the first parameter. baseSysVServiceInstalled {service} Prints the name `${service}` if a SysV init service with the same name is found; otherwise, it prints nothing. baseSystemdCall {service_name} {args} Calls `systemctl ${args} ${service_name}` if a systemd unit, a systemd mount, or a SysV init service with the `${service_name}` exists. baseInsertService {servicename} Activates the specified service via :command:`systemctl`. baseRemoveService {servicename} Deactivates the specified service via :command:`systemctl`. baseService {servicename} {on|off} Activates or deactivates a service via :command:`systemctl`. The function requires the service name and the value `on` or `off` as parameters. The following example enables the sshd service on boot: .. code:: bash baseService sshd on suseInsertService {servicename} Calls `baseInsertService`. It exists only for compatibility reasons. suseRemoveService {servicename} Calls `baseRemoveService`. It exists only for compatibility reasons. suseService {servicename} {on|off} Calls `baseService`. It exists only for compatibility reasons. suseSetupProduct Creates the :file:`/etc/products.d/baseproduct` link pointing to the product referenced either by :file:`/etc/SuSE-brand` or :file:`/etc/os-release` or the latest `.prod` file available in :file:`/etc/products.d`. baseVagrantSetup Configures the image to work as a vagrant box by performing the following changes: - add the `vagrant` user to :file:`/etc/sudoers` or :file:`/etc/sudoers.d/vagrant`. - insert the insecure vagrant ssh key, apply recommended ssh settings, and start the ssh daemon. - create the default shared folder :file:`/vagrant`. Debug {message} A helper function to print the supplied message if the variable DEBUG is set to 1 (disabled by default). Echo {echo commandline} A helper function to print a message to the controlling terminal. Rm {list of files} A helper function to delete files and log the deletion. Profile Environment Variables ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ kiwi exports its environment file for each shell script call. The environment contains the variables listed below, among others. The full list can be obtained by running: .. code:: shell-session $ kiwi-ng image info --description= --print-kiwi-env $kiwi_compressed A value of the `compressed` attribute set in the `type` element in :file:`config.xml`. $kiwi_delete A list of all packages which are children of the `packages` element with `type="delete"` in :file:`config.xml`. $kiwi_drivers A comma-separated list of driver entries as listed in the `drivers` section of :file:`config.xml`. $kiwi_iname The name of the image as listed in :file:`config.xml`. $kiwi_iversion The image version as a string. $kiwi_keytable The contents of the keytable setup as specified in :file:`config.xml`. $kiwi_language The contents of the locale setup as specified in :file:`config.xml`. $kiwi_profiles A comma-separated list of profiles used to build this image. $kiwi_timezone The contents of the timezone setup as specified in :file:`config.xml`. $kiwi_type The image type as extracted from the `type` element in :file:`config.xml`. Configuration Tips ------------------ #. **Locale configuration:** To set the locale, {kiwi} relies on `systemd-firstboot`, which writes the locale configuration file, :file:`/etc/locale.conf`. The values for the locale settings are taken from the description XML file in the `` element under ``. Keep in mind that if the build distribution does not use `/etc/locale.conf`, `systemd-firstboot` does not have any effect on the locale settings. For example, in the SLE12 distribution, `systemd-firstboot` is only effective when locales in :file:`/etc/sysconfig/language` are not set or when the file does not exist at all. For compatibility reasons, the file :file:`/etc/sysconfig/language` in SLE12 has precedence over :file:`/etc/locale.conf`, and management tools can still use `sysconfig` files for locale settings. In any case, it is possible to configure the locale setting inside the :file:`config.sh` script in {kiwi} in a distribution-specific way or by adding any additional configuration file as part of the overlay root tree. #. **Stateless systemd UUIDs:** Machine ID files (:file:`/etc/machine-id`, :file:`/var/lib/dbus/machine-id`) may be created and set during the image package installation, depending on the distribution. Those UUIDs must be unique and must be set only once in each deployment. If :file:`/etc/machine-id` does not exist or contains the string `uninitialized` (systemd v249 and later), this triggers a firstboot action in systemd, and the services are run with `ConditionFirstBoot=yes`. Unless the file already contains a valid machine ID, systemd generates a machine ID and writes it into the file, creating it if necessary. See the `machine-id man page `_ for more details. Depending on whether the firstboot action should be triggered or not, :file:`/etc/machine-id` can be created, removed, or set to `uninitialized` by :file:`config.sh`. To prevent images from including a generated machine ID, {kiwi} clears :file:`/etc/machine-id` if it exists and does not contain the string `uninitialized`. This only applies to images based on a dracut initrd. .. note:: `rw` is necessary if :file:`/etc/machine-id` does not exist. For systemd to be able to write :file:`/etc/machine-id` on boot, either the file must exist (so that a bind mount can be created) or :file:`/etc` must be writable. By default, the root filesystem is mounted read-only by dracut/systemd, so a missing :file:`/etc/machine-id` will lead to an error on boot. To force the initial mount to be read-write, add the `rw` option to the kernel command line. .. note:: Avoid inconsistent :file:`/var/lib/dbus/machine-id`. :file:`/etc/machine-id` and :file:`/var/lib/dbus/machine-id` **must** contain the same unique ID. On modern systems, :file:`/var/lib/dbus/machine-id` is already a symlink to :file:`/etc/machine-id`. However, on older systems, there might be two different files. This is the case for SLE-12-based images. If you are targeting older operating systems, it is recommended to add the symlink creation into :file:`config.sh`: .. code:: bash #====================================== # Make machine-id consistent with dbus #-------------------------------------- if [ -e /var/lib/dbus/machine-id ]; then rm /var/lib/dbus/machine-id fi ln -s /etc/machine-id /var/lib/dbus/machine-id ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/systemdeps.rst0000644000000000000000000001104015273675667020725 0ustar00.. _systemdeps: Host Requirements To Build Images --------------------------------- Building OS images requires several tools and subsystems to be present on the {kiwi} host. For example, to build a virtual disk image, tools for partition table setup or tools to create filesystems must be available on the host that builds the image. The number of required components depends on the selected image type and the features used with the image. It's unreasonable to expect {kiwi} users to know which exact components are needed to build the image. A mechanism called `kiwi-systemdeps` is designed to handle the host requirements. `kiwi-systemdeps` consists of a collection of sub-packages provided with the `python-kiwi` main package. Each individual package requires a number of tools and subsystem packages that belong to the package category. There are the following systemdeps packages: `kiwi-systemdeps-core`: * Supports building the simple root archive `tbz` image type. * Installs the package managers that are supported by the target distribution, as well as the `tar` archiving tool. `kiwi-systemdeps-containers`: * Supports building `OCI` image types used with `docker` and `podman`. * Installs the distribution-specific toolchain to build OCI- compliant container images. `kiwi-systemdeps-containers-wsl`: * Supports building `appx` and `wsl` image types. The `wsl` type is the successor to the former `appx` type. * Installs the distribution-specific toolchain to build WSL-compliant container images on Windows systems. `kiwi-systemdeps-iso-media`: * Supports building `iso` image types and `oem` install media. * Installs all tools required to build ISO filesystems. * Depends on the `-core`, `-filesystems`, and `-bootloaders` kiwi-systemdeps packages. `kiwi-systemdeps-bootloaders`: * Supports building bootable `oem` and `iso` image types. * Installs all bootloader tools depending on the host architecture to allow setup and install of the bootloader. The pulled-in components are required for any image that is able to boot through some BIOS or firmware. * Depends on the `-core` kiwi-systemdeps packages. `kiwi-systemdeps-filesystems`: * Supports building `fs-type`, `oem`, `pxe`, `kis`, and live `iso` image types. * Installs all tools to create filesystems supported by {kiwi}. The pulled-in components are needed for any image type that needs to create a filesystem. This excludes the archive-based image types like `docker`, `appx`, or `tbz`. The package also installs tools one level below the actual filesystem creation toolkit. These are components to manage loop devices, as well as partition table setup and subsystem support, like LVM and LUKS. * Depends on the `-core` kiwi-systemdeps packages. `kiwi-systemdeps-disk-images`: * Supports building the `oem` image type. * Installs all tools to create virtual disks. Virtual disks in {kiwi} are created using the QEMU toolchain. * Depends on the `-filesystems`, `-bootloaders` and `-iso-media` kiwi-systemdeps packages. `kiwi-systemdeps-image-validation`: * Installs the `jing` tool to validate the image description. This is useful for detailed error reports from {kiwi} if an image description validation error occurs. In addition, the `anymarkup` Python module is installed if the option to install recommended packages is set. With `anymarkup` available, {kiwi} can also handle image descriptions in a format other than the XML markup (for example, YAML). `kiwi-systemdeps-boxbuild`: * Supports building images in a self-contained virtual machine or container via the `kiwi-ng system boxbuild` command. * Installs the QEMU/KVM toolchain required to run the build box. `kiwi-systemdeps-stackbuild`: * Supports storing image root trees as containers and building images based on containers via the `kiwi-ng system stash` and `kiwi-ng system stackbuild` commands. * Installs `podman` and `rsync`. * Depends on the `-containers` kiwi-systemdeps package. Depending on the image type, the `kiwi-systemdeps` packages can help to quickly set up the host system for building images. In case the host must support everything, there is also the main `kiwi-systemdeps` package that has all other existing `systemdeps` packages, except for `-boxbuild` and `-stackbuild`, as its dependency. .. note:: Pulling in all `kiwi-systemdeps` packages can result in a large number of packages installed on the host because the required packages themselves have other dependencies (for example, java for jing). ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/concept_and_workflow/users.rst0000644000000000000000000000344615273675667017701 0ustar00.. _adding-users: Adding Users ============ User accounts can be added or modified via the `users` element that supports a list of multiple `user` child elements: .. code:: xml Each `user` element represents a specific user to be added or modified. The following attributes are mandatory: - `name`: the UNIX username. - `password`: The password for the user account. It can be provided either in clear-text form (`pwdformat="plain"`) or in encrypted form (`pwdformat="encrypted"`). Using plain passwords is not advisable, as anyone with access to the image description can see the password. It is recommended to generate a hash of your password using `openssl` as follows: .. code:: bash $ openssl passwd -1 -salt 'xyz' YOUR_PASSWORD Additionally, the following optional attributes can be specified: - `home`: The path to the user's home directory. - `groups`: A comma-separated list of UNIX groups. The first element of the list is used as the user's primary group. The remaining elements are appended to the user's supplementary groups. When no groups are assigned, the system's default primary group will be used. - `id`: The numeric user ID of the account. - `realname`: The full name of the user. - `shell`: The login shell of the user. - `pwdformat`: The format in which `password` is provided, either `plain` or `encrypted` (the latter is the default). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7342527 kiwi-11.1.8/doc/source/concept_and_workflow.rst0000644000000000000000000002341615273675667016537 0ustar00Concept and Workflow ==================== .. note:: **Abstract** The following sections describe the concept and general workflow of building appliances with {kiwi} |version|. .. toctree:: :maxdepth: 1 concept_and_workflow/systemdeps.rst concept_and_workflow/repository_setup concept_and_workflow/packages concept_and_workflow/profiles concept_and_workflow/users concept_and_workflow/shell_scripts concept_and_workflow/runtime_configuration concept_and_workflow/customize_the_boot_process {kiwi} builds so-called *system images* (a fully installed and optionally configured system in a single file) of a Linux distribution in two steps (for further details, see :ref:`working-with-kiwi-image-building-process`): 1. *Prepare operation*: generate an *unpacked image tree* of your image. The unpacked tree is a directory containing the future filesystem of your image, generated from your image description. 2. *Create operation*: the unpacked tree generated in step 1 is packaged into the format required for the final usage (e.g., a `qcow2` disk image to launch the image with QEMU). {kiwi} executes these steps using the following components, which it expects to find in the *description directory*: :ref:`image-description`: The :file:`config.xml` file contains the image description, which is a collection of general settings of the final image, like the image layout, installed packages, present users, etc. .. note:: The filename :file:`config.xml` is not mandatory; the image description file can also have an arbitrary name plus the :file:`*.kiwi` extension. {kiwi} first looks for a :file:`config.xml` file. If it cannot be found, it picks the first :file:`*.kiwi` file. When using a file with the :file:`.kiwi` extension, multiple markup options are supported, including `XML`, `YAML`, and `TOML`. :ref:`working-with-kiwi-user-defined-scripts`: If present, custom configuration shell scripts run at different stages of the build process. They can be used to fine-tune the image in ways that are not possible via the settings provided in :file:`config.xml`. Overlay tree directory: The *overlay tree* is a folder (called :file:`root`) or a tarball (called :file:`root.tar.gz`) that contains files and directories that will be copied into the *unpacked image tree* during the *Prepare operation*. The copying is executed after all the packages included in :file:`config.xml` have been installed. Any files that are already present will be overwritten. CD root user data: For live ISO images and install ISO images, an optional archive is supported. This is a tar archive matching the name :file:`config-cdroot.tar[.compression_postfix]`. If present, the archive will be unpacked as user data on the ISO image. For example, this is used to add license files or user documentation. The documentation can then be read directly from the CD/DVD without booting from the media. .. _working-with-kiwi-image-building-process: Image Building Process ~~~~~~~~~~~~~~~~~~~~~~ .. figure:: .images/flow-diagram.png :align: center :alt: {kiwi} Build Process Flow Diagram {kiwi} Build Process Flow Diagram {kiwi} creates images in a two-step process: The first step, the *prepare* operation, generates a so-called *unpacked image tree* (directory) using the information provided in the :file:`config.xml` configuration file (see :ref:`image-description`). The second step, the *create* operation, creates the *packed image* or *image* in the specified format based on the unpacked image tree and the information provided in the :file:`config.xml` configuration file. .. _prepare-step: The Prepare Step ^^^^^^^^^^^^^^^^ As the first step, {kiwi} creates an *unpackaged image tree*, also called a "root tree". This directory will be the installation target for software packages to be installed during the image creation process. For the package installation, {kiwi} relies on the package manager specified in the `packagemanager` element in :file:`config.xml`. {kiwi} supports the following package managers: `apk`, `apt`, `dnf4` (default), `dnf5`, `microdnf`, `pacman`, and `zypper`. The prepare step consists of the following substeps: #. **Create Target Root Directory** By default, {kiwi} aborts with an error if the target root tree already exists to avoid accidental deletion of an existing unpacked image. The option `--allow-existing-root` can be used to work based on an existing root tree. #. **Bootstrap Target Root Directory** First, {kiwi} configures the package manager to use the repositories specified in the configuration file, via the command line, or both. After the repository setup, the packages specified in the `bootstrap` section of the image description are installed into the target root tree using the package manager of the build host (not chrooted). This establishes the initial environment to support the completion of the process in a chroot setting. At the end of the `bootstrap` phase, the script :file:`post_bootstrap.sh` is executed, if present. .. note:: The essential bootstrap packages are usually `filesystem` and `glibc-locale` to specify as part of the bootstrap. The dependency chain of these two packages is usually sufficient to populate the bootstrap environment with all required software to support the installation of packages into the new root tree. #. **Install Packages** After the `bootstrap` phase, all other `` sections are used to complete the installation as a chroot operation. {kiwi} uses the package manager as installed in the `bootstrap` phase and installs all other packages as configured. .. note:: The installation of software packages through the selected package manager may install unwanted packages. Removing these packages can be accomplished by marking them for deletion in the image description; see :ref:`uninstall-system-packages`. #. **Apply Archives** All archives specified in the `archive` element of the :file:`config.xml` file are applied in the specified order (top to bottom) after the package installation is complete (see :ref:`archive-element`). Files and directories are extracted relative to the top level of the new root tree. It is possible to overwrite files already existing in the target root tree. #. **Apply the Overlay Tree** Next, {kiwi} applies all files and directories present in the overlay directory named :file:`root` or in the compressed overlay, :file:`root.tar.gz`, to the target root tree. Files already present in the target root directory are overwritten. This allows you to overwrite any file that was installed by one of the packages or archives during the installation phase. #. **Execute the user-defined script** :file:`config.sh`. At the end of the preparation stage, the script :file:`config.sh` is executed (if present). It is run in the top-level directory of the target root tree. The script's primary function is to complete the system configuration. For more details about custom scripts, see :ref:`working-with-kiwi-user-defined-scripts`. #. **Modify the Root Tree** The unpacked image tree is now finished and can be converted into the final image in the *create step*. It is possible to make manual modifications to the unpacked tree before it is converted into the final image. Since the unpacked image tree is just a directory, it can be modified using the standard tools. Optionally, it is also possible to 'change root (`chroot`)' into it, for instance, to invoke the package manager. Besides the standard filesystem layout, the unpacked image tree contains an additional directory named :file:`/image` that is not present in a regular system. It contains information {kiwi} requires during the create step, including a copy of the :file:`config.xml` file. By default, {kiwi} will not stop after the *prepare step* and will proceed directly with the *create step*. Therefore, to perform manual modifications, proceed as follows: .. code:: shell-session $ kiwi-ng system prepare $ARGS $ # make your changes $ kiwi-ng system create $ARGS .. warning:: Modifications of the unpacked root tree When performing manual modifications of the unpacked image tree, be aware that these changes might be lost when calling the prepare step again. Therefore, avoid making changes that should be persistent across multiple runs of the `prepare` step. The recommended way to apply persistent changes to the unpacked image directory is to change the original image description and re-run the `prepare` step. .. _create-step: The Create Step ^^^^^^^^^^^^^^^ {kiwi} creates the final image during the *create step*: it converts the unpacked root tree into one or multiple output files appropriate for the respective build type. It is possible to create multiple images from the same unpacked root tree, for example, a self-installing OEM image and a virtual machine image from the same image description. The only prerequisite is that both image types are specified in :file:`config.xml`. During the *create step*, the following operations are performed by {kiwi}: #. **Execute the User-defined Script** :file:`images.sh`. At the beginning of the image creation process, the script named :file:`images.sh` is executed (if present). For more details about custom scripts, see :ref:`working-with-kiwi-user-defined-scripts`. #. **Create the Requested Image Type** {kiwi} converts the unpacked root into an output format appropriate for the requested build type. During this process further image type specific user defined scripts may be executed (see :ref:`working-with-kiwi-user-defined-scripts`). ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/conf.py0000644000000000000000000002335215273675667013074 0ustar00#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # KIWI NG documentation build configuration file # from datetime import datetime import sys from os.path import abspath, dirname, join, normpath import shlex from sphinx.transforms import SphinxTransform # 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. _path = normpath(join(dirname(__file__), "../..")) sys.path.insert(0, _path) # -- 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.autosummary', 'sphinx.ext.extlinks', 'sphinx.ext.todo', 'sphinx.ext.ifconfig', 'sphinx.ext.viewcode', # 'sphinxcontrib.spelling', 'sphinx_rtd_theme' ] def prologReplace(app, docname, source): result = source[0] for key in app.config.prolog_replacements: result = result.replace(key, app.config.prolog_replacements[key]) source[0] = result class AddRootRole(SphinxTransform): # Runs after the document is parsed and the source path is set default_priority = 500 def apply(self): master_doc = self.config.master_doc # Get the current document name relative to the source directory # e.g., 'index', 'folder/subpage', etc. docname = self.env.docname # Only apply the attribute if this is the main entry point if docname == master_doc: self.document['role'] = 'main' def setup(app): app.add_config_value('prolog_replacements', {}, True) app.add_transform(AddRootRole) app.connect('source-read', prologReplace) prolog_replacements = { '{exc_image_base_name}': 'kiwi-test-image-some', '{exc_image_base_name_pxe}': 'kiwi-test-image-pxe', '{exc_image_base_name_kis}': 'kiwi-test-image-kis', '{exc_image_base_name_vagrant}': 'kiwi-test-image-vagrant', '{exc_image_base_name_disk}': 'kiwi-test-image-disk', '{exc_image_base_name_disk_simple}': 'kiwi-test-image-disk-simple', '{exc_image_base_name_live}': 'kiwi-test-image-live', '{exc_image_base_name_docker}': 'kiwi-test-image-docker', '{exc_image_base_name_enclave}': 'kiwi-test-image-nitro-enclave', '{exc_netboot}': 'netboot/suse-tumbleweed', '{exc_description_pxe}': 'x86/tumbleweed/test-image-pxe', '{exc_description_kis}': 'x86/tumbleweed/test-image-kis', '{exc_description_vagrant}': 'x86/leap/test-image-vagrant', '{exc_description_disk}': 'x86/leap/test-image-disk', '{exc_description_disk_simple}': 'x86/leap/test-image-disk-simple', '{exc_description_live}': 'x86/leap/test-image-live', '{exc_description_wsl}': 'x86/tumbleweed/test-image-wsl', '{exc_description_docker}': 'x86/leap/test-image-docker', '{exc_description_enclave}': 'x86/rawhide/test-image-nitro-enclave', '{exc_os_version}': '16.0', '{exc_image_version}': '1.15.3', '{exc_image_version_pxe}': '1.42.1', '{exc_image_version_kis}': '1.42.1', '{exc_image_version_enclave}': '1.1.1', '{exc_repo_leap}': 'https://download.opensuse.org/distribution/leap/16.0/repo/oss', '{exc_repo_tumbleweed}': 'https://download.opensuse.org/tumbleweed/repo/oss', '{exc_repo_rawhide}': 'https://mirrors.fedoraproject.org/metalink?repo=rawhide&arch=x86_64', '{exc_kiwi_repo}': 'obs://Virtualization:Appliances:Builder/openSUSE_Leap_16.0', '{exc_repo_obs}': 'obs://openSUSE:Leap:16.0/standard', '{schema_version}': '8.5', '{kiwi}': 'KIWI NG', '{kiwi-product}': 'KIWI Next Generation (KIWI NG)', '{kiwi-legacy}': 'KIWI Legacy' } latex_documents = [ ('index', 'kiwi.tex', 'KIWI NG Documentation', 'Marcus Schäfer', 'manual') ] latex_elements = { 'papersize': 'a4paper', 'pointsize':'12pt', 'classoptions': ',openany', 'babel': '\\usepackage[english]{babel}', 'preamble': r''' \makeatletter \fancypagestyle{normal}{ \fancyhf{} \fancyfoot[LE,RO]{{\py@HeaderFamily\thepage}} \fancyfoot[LO]{{\py@HeaderFamily\nouppercase{\rightmark}}} \fancyfoot[RE]{{\py@HeaderFamily\nouppercase{\leftmark}}} \fancyhead[LE,RO]{{\py@HeaderFamily \@title, \py@release}} \renewcommand{\headrulewidth}{0.4pt} \renewcommand{\footrulewidth}{0.4pt} } \makeatother ''' } # 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' default_role="py:obj" # General information about the project. project = 'KIWI NG' author = 'Marcus Schäfer' copyright = f'2020-{datetime.now().year}, {author}' # 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 = '11.1.8' # The full version, including alpha/beta/rc tags. release = version # 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 = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # 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 = True autosummary_generate = True #spelling_lang = 'en_US' #spelling_show_suggestions = True #spelling_word_list_filename = ['spelling_wordlist.txt'] # -- Options for HTML output ---------------------------------------------- #html_short_title = '%s-%s' % (project, version) #html_last_updated_fmt = '%b %d, %Y' #html_split_index = True html_logo = '.images/kiwi-logo.png' html_sidebars = { '**': [ 'localtoc.html', 'relations.html', 'about.html', 'searchbox.html', ] } html_theme = "sphinx_rtd_theme" html_theme_options = { 'collapse_navigation': False, 'sticky_navigation': True, 'navigation_depth': 3, 'includehidden': True, 'titles_only': False } # -- Options for manual page output --------------------------------------- # The man page toctree documents. kiwi_doc = 'commands/kiwi' result_list_doc = 'commands/result_list' result_bundle_doc = 'commands/result_bundle' system_prepare_doc = 'commands/system_prepare' system_update_doc = 'commands/system_update' system_build_doc = 'commands/system_build' system_boxbuild_doc = 'commands/system_boxbuild' system_create_doc = 'commands/system_create' system_stackbuild_doc = 'commands/system_stackbuild' system_stash_doc = 'commands/system_stash' image_resize_doc = 'commands/image_resize' image_info_doc = 'commands/image_info' # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ( kiwi_doc, 'kiwi', 'Creating Operating System Images', [author], 8 ), ( result_list_doc, 'kiwi::result::list', 'List build results', [author], 8 ), ( result_bundle_doc, 'kiwi::result::bundle', 'Bundle build results', [author], 8 ), ( system_prepare_doc, 'kiwi::system::prepare', 'Prepare image root system', [author], 8 ), ( system_create_doc, 'kiwi::system::create', 'Create image from prepared root system', [author], 8 ), ( system_update_doc, 'kiwi::system::update', 'Update/Upgrade image root system', [author], 8 ), ( system_build_doc, 'kiwi::system::build', 'Build image in combined prepare and create step', [author], 8 ), ( system_boxbuild_doc, 'kiwi::system::boxbuild', 'Build image in a self contained VM or container', [author], 8 ), ( system_stackbuild_doc, 'kiwi::system::stackbuild', 'Build image based on stash containers', [author], 8 ), ( system_stash_doc, 'kiwi::system::stash', 'Create a stash container from an image root directory', [author], 8 ), ( image_resize_doc, 'kiwi::image::resize', 'Resize disk images to new geometry', [author], 8 ), ( image_info_doc, 'kiwi::image::info', 'Provide detailed information about an image description', [author], 8 ) ] # If true, show URL addresses after external links. #man_show_urls = False ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/contributing/kiwi_from_python.rst0000644000000000000000000000377715273675667020436 0ustar00Using {kiwi} in a Python Project ================================= .. note:: **Abstract** {kiwi} is provided as a Python module under the **kiwi** namespace. It is available for the Python 3 version. The following description applies to {kiwi} version |version|. {kiwi} can also function as a module for other Python projects. The following example demonstrates how to read an existing image description, add a new repository definition, and export the modified description on stdout: .. code:: python import sys import logging from kiwi.xml_description import XMLDescription from kiwi.xml_state import XMLState description = XMLDescription('path/to/kiwi/XML/config.xml') xml_data = description.load() xml_state = XMLState( xml_data=xml_data, profiles=[], build_type='iso' ) xml_state.add_repository( repo_source='http://repo', repo_type='rpm-md', repo_alias='myrepo', repo_prio=99 ) xml_data.export( outfile=sys.stdout, level=0 ) Each class in the example is responsible for a single task, so they can be reused in other use cases. Therefore, it is possible to use {kiwi} beyond the main image building scope, for example, to manage the setup of loop devices, filesystems, partitions, etc. This means {kiwi} offers a way to describe a system, but you can choose whether you want to use the {kiwi} description format or not. The following example shows how to use {kiwi} to create a simple filesystem image that contains your host's `tmp` directory. .. code:: python import logging from kiwi.storage.loop_device import LoopDevice from kiwi.filesystem import FileSystem with LoopDevice( filename='my_tmp.ext4', filesize_mbytes=100 ) as loop_provider: loop_provider.create() with FileSystem.new( 'ext4', loop_provider, '/tmp/' ) as filesystem: filesystem.create_on_device( label='TMP' ) filesystem.sync_data() ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.735329 kiwi-11.1.8/doc/source/contributing/kiwi_plugin_architecture.rst0000644000000000000000000001131115273675667022111 0ustar00Plugin Architecture =================== Each command provided by {kiwi} is written as a task plugin under the **kiwi.tasks** namespace. As a developer, you can extend the {kiwi} system command space with custom task plugins, following the conventions below. Naming conventions ------------------ Task plugin file name The file name of a task plugin must follow the pattern :file:`system_.py`. This allows you to invoke the task with :command:`kiwi-ng system ...` Task plugin option handling {kiwi} uses the typer module to handle options. Each task plugin must use typer to allow option handling. The typer definition must be provided in a file named :file:`cli.py` and must live in the toplevel of the plugin python namespace. The :file:`cli.py` file must provide a dictionary named `typers` that maps the command name to its `typer.Typer` instance. Task plugin class The implementation of the plugin must be a class that matches the naming convention ``SystemTask``. The class must inherit from the ``CliTask`` base class. On the plugin startup, {kiwi} expects an implementation of the `process` method. Task plugin entry point Registration of the plugin must be done in :file:`pyproject.toml` using the `tool.poetry.plugins` concept. The entry point group must be `kiwi.tasks`, and the name of the toplevel plugin python namespace must contain the `_plugin` substring. Otherwise, {kiwi} does not load the :file:`cli.py` of the plugin. The namespaces `kiwi_boxed_plugin` and `kiwi_stackbuild_plugin` are ignored because their commands are now part of {kiwi} itself. .. code:: [tool.poetry] name = "kiwi__plugin" packages = [ { include = "kiwi__plugin"}, ] [tool.poetry.plugins] [tool.poetry.plugins."kiwi.tasks"] system_ = "kiwi__plugin.tasks.system_" Example plugin -------------- .. note:: The following example assumes an existing Python project which was set up using poetry and pyproject.toml. 1. Assuming the project namespace is **kiwi_relax_plugin**, create the task plugin directory :file:`kiwi_relax_plugin/tasks`. 2. Create the entry point in :file:`pyproject.toml`. Assuming we want to create the system command **justdoit**, this is the following entry point definition in :file:`pyproject.toml`: .. code:: toml [tool.poetry] name = "kiwi_relax_plugin" packages = [ { include = "kiwi_relax_plugin"}, ] [tool.poetry.plugins] [tool.poetry.plugins."kiwi.tasks"] system_justdoit = "kiwi_relax_plugin.tasks.system_justdoit" 3. Create the typer cli interface in the file :file:`kiwi_relax_plugin/cli.py` with the following content: .. code:: python import typer from typing import Annotated # typers variable must be provided for kiwi plugins typers = { 'justdoit': typer.Typer(add_completion=False) } system = typers['justdoit'] @system.callback( help='What is it good for', invoke_without_command=True, subcommand_metavar='' ) def justdoit( ctx: typer.Context, now: Annotated[bool, typer.Option(help='For --now option')] = False ): Cli=ctx.obj Cli.subcommand_args['justdoit'] = { '--now': now, 'help': False } Cli.global_args['command'] = 'justdoit' Cli.global_args['system'] = True Cli.cli_ok = True 4. Create the plugin code in the file :file:`kiwi_relax_plugin/tasks/system_justdoit.py` with the following content: .. code:: python # These imports requires kiwi to be part of your environment # It can be either installed from pip into a virtual development # environment or from the distribution package manager. from kiwi.tasks.base import CliTask from kiwi.help import Help class SystemJustdoitTask(CliTask): def process(self): self.manual = Help() if self.command_args.get('help') is True: # The following will invoke man to show the man page # for the requested command. Thus, for the call to # succeed, a man page needs to be written and # installed by the plugin. return self.manual.show('kiwi::relax::justdoit') if self.command_args.get('--now'): print( 'https://genius.com/Frankie-goes-to-hollywood-relax-lyrics' ) 5. Test the plugin .. code:: bash $ poetry run kiwi-ng system justdoit --now ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.735329 kiwi-11.1.8/doc/source/contributing/schema_extensions.rst0000644000000000000000000001041015273675667020544 0ustar00Extending {kiwi} with Custom Operations ======================================== .. note:: **Abstract** Extension plugins in {kiwi} offer a mechanism for adding information outside the standard {kiwi} schema. This document describes how to create an extension plugin for the {kiwi} schema as well as how to add and validate additional information in the {kiwi} image description. The described schema extension can be used in an additional {kiwi} task plugin to provide a new subcommand for {kiwi}. At the present moment, there is no other plugin interface except for providing additional {kiwi} commands. Depending on the demand for custom plugins, the interface to hook into code in other parts of the {kiwi} processing needs to be extended. This description applies to version |version|. The Section ----------------------- The main {kiwi} schema supports an extension section that allows you to specify any XML structure and attributes, as long as they are attached to a namespace. This means that any custom XML structure can be implemented similar to the example below: .. code:: xml ... * Any top-level namespace must be unique. * Multiple different top-level namespaces are allowed, for example, `my_plugin_a`, `my_plugin_b`. RELAX NG Schema for the Extension --------------------------------- If an extension section is found, {kiwi} looks up its namespace and uses the main XML catalog for the schema file to validate the extension data. The schema file must be a RELAX NG schema in the `.rng` format. We recommend saving the schema as :file:`/usr/share/xml/kiwi/my_plugin.rng`. For the example above, the RELAX NG Schema in the compact format, :file:`my_plugin.rnc`, looks as follows: .. there is no rnc syntax highlighting, try cpp .. code:: cpp namespace my_plugin = "http://www.my_plugin.com" start = k.my_feature div { k.my_feature.attlist = empty k.my_feature = element my_plugin:my_feature { k.my_feature.attlist & k.title } } div { k.title.name.attribute = attribute name { text } k.title.attlist = k.title.name.attribute k.title = element my_plugin:title { k.title.attlist } } To convert this schema to the `.rng` format, just call: .. code:: bash $ trang -I rnc -O rng my_plugin.rnc /usr/share/xml/kiwi/my_plugin.rng Extension schema in XML catalog ------------------------------- As mentioned above, the mapping from the extension namespace to the correct RELAX NG schema file is handled by an XML catalog file. The XML catalog for the example is as follows: .. code:: xml For resolving the catalog, {kiwi} uses the `xmlcatalog` command and the main XML catalog from the system, :file:`/etc/xml/catalog`. .. note:: How the main catalog is informed about the existence of the {kiwi} extension catalog file depends on the distribution and its version. Refer to the distribution documentation for information on adding XML catalogs. If the following command provides the information for the correct RELAX NG schema file, you are ready for a first test: .. code:: bash $ xmlcatalog /etc/xml/catalog http://www.my_plugin.com Using the extension ------------------- To test the extension, insert the example extension into one of your image description's :file:`config.xml` file. The following example reads the name attribute from the title section of the `my_feature` root element and prints it: .. code:: python import logging from kiwi.xml_description import XMLDescription description = XMLDescription('path/to/kiwi/XML/config.xml') description.load() my_plugin = description.get_extension_xml_data('my_plugin') print(my_plugin.getroot()[0].get('name')) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.735329 kiwi-11.1.8/doc/source/contributing/scripts_testing.rst0000644000000000000000000000541515273675667020262 0ustar00Write Integration Tests for the Scripts --------------------------------------- {kiwi} comes with a set of helper functions that can be used in :file:`config.sh` (see also: :ref:`working-with-kiwi-user-defined-scripts`). These functions utilize containers to run the individual tasks and verify the final result. Ensure that you have either :command:`podman` or :command:`docker` installed and configured on your system. Podman is used by default. You can select :command:`docker` instead by setting the environment variable ``CONTAINER_RUNTIME`` to ``docker``. Then, you can run the integration tests via the `test_scripts` Makefile target: .. code:: shell-session $ make test_scripts The tests are written using the `pytest-container `__ plugin. If applicable, use the utility functions and fixtures of the plugin. For example, the `auto_container` and `auto_container_per_test` fixtures in conjunction with `testinfra `__. Test setup ~~~~~~~~~~ The script tests can be run inside different containers specified in :file:`test/scripts/conftest.py`. This file contains the `CONTAINERS` list with all currently present images. These images are pulled and built as needed, and the :file:`functions.sh` is copied into `/bin/`, so it is available in `PATH`. To use any of these containers, you can either define the global variable `CONTAINER_IMAGES` in a test module and use the `auto_container` fixture, or `parametrize `__ the `container` fixture indirectly: .. code:: python @pytest.mark.parametrize("container_per_test", (TUMBLEWEED, LEAP_15_5), indirect=True) def test_RmWorks(container_per_test): # create the file /root/foobar container_per_test.connection.run_expect([0], "touch /root/foobar") assert container_per_test.connection.file("/root/foobar").exists # source the functions and execute our function under test container_per_test.connection.run_expect([0], ". /bin/functions.sh && Rm /root/foobar") # verify the result assert not container_per_test.connection.file("/root/foobar").exists The example above uses the `_per_test` variant of the `container` fixture. It ensures that the container is used only in a single test function. Use this variant for tests that mutate the system under test because otherwise, it may lead to race conditions that are difficult to debug. For tests that only perform reads, you can omit the `_per_test` suffix so that the container environment can be shared with other tests. This improves execution speed but comes at the expense of safety in case of mutation. For further information, refer to `pytest-container `__. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.734833 kiwi-11.1.8/doc/source/contributing.rst0000644000000000000000000001241215273675667015031 0ustar00Contributing ============ .. note:: **Abstract** This document describes the development process of {kiwi} and how you can be part of it. This description applies to version |version|. .. toctree:: :maxdepth: 1 contributing/kiwi_from_python contributing/kiwi_plugin_architecture contributing/scripts_testing contributing/schema_extensions.rst The core appliance builder is developed in Python and follows the test-driven development rules. If you want to implement a bigger feature, consider opening an issue on GitHub first to discuss the changes. Or, join the discussion in the `#kiwi` channel on `Matrix `_. Fork the upstream repository ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 1. On GitHub, navigate to: https://github.com/OSInside/kiwi. 2. In the top-right corner of the page, click :command:`Fork`. Create a local clone of the forked repository ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: shell-session $ git clone https://github.com/YOUR-USERNAME/kiwi $ git remote add upstream https://github.com/OSInside/kiwi.git Install Required Operating System Packages ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ {kiwi} requires additional packages at runtime that are not provided by `pip`. Those will be pulled in by installing the following package: * kiwi-systemdeps The package is provided on the Open Build Service in the `Virtualization:Appliances:Builder `__ project. For manual inspection of the packages that are pulled in from the above `kiwi-systemdeps` package, please refer to the `package/python-kiwi-spec-template` spec file from the checked-out Git repository. Create a Python Virtual Development Environment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The following commands initialize and activate a development environment for Python 3: .. code:: shell-session $ poetry install --all-extras .. note:: To create the python virtual env for another version of Python, e.g. 3.11, call the following prior the poetry install :: $ poetry env use python3.11 The command above automatically creates the application script called :command:`kiwi-ng`, which allows you to run {kiwi} from the Python sources inside the virtual environment using Poetry: .. code:: shell-session $ poetry run kiwi-ng --help Running the Unit Tests ~~~~~~~~~~~~~~~~~~~~~~ Before submitting your changes via a pull request, ensure that all tests pass and that the code has the required test coverage via the command: .. code:: shell-session $ make check $ make test Coding Style ~~~~~~~~~~~~ {kiwi} follows the general PEP8 guidelines. Documentation ~~~~~~~~~~~~~ {kiwi} uses `Sphinx `_ for the user documentation and man pages. .. code:: shell-session $ make docs Bumping the Version ~~~~~~~~~~~~~~~~~~~ The {kiwi} project follows the `Semantic Versioning `_ scheme. We use the :command:`bumpversion` tool for consistent versioning. AI policy ~~~~~~~~~ {kiwi} has a policy of human-centric development. Only humans can be authors and are ultimately responsible for the quality of their contributions. That said, assisted development with "AI" tools (particularly contemporary LLM-based tools) is permitted with the restriction that such usage is clearly labeled with the `Assisted-by:` trailer in the pull request and commit message. The format of the trailer is as such: `Assisted-by: :`. Examples of such trailers: ================= ======================================= Tool Trailer ================= ======================================= Claude `Assisted-by: Claude:claude-opus-4-6` Gemini `Assisted-by: Gemini:gemini-2.5-pro` GitHub Copilot `Assisted-by: Copilot:gpt-4o` ChatGPT `Assisted-by: ChatGPT:gpt-4o` ================= ======================================= The trailers are required for the following scenarios: - AI wrote or generated code that ended up in the commit - AI substantially modified or refactored existing code - AI generated an implementation that the contributor then adapted It is **not** required for the following scenarios: - AI was used for **research only** - asking questions, understanding APIs, exploring approaches - but the human wrote all the code - AI performed purely **mechanical tasks** like formatting, running commands, or committing All submitted changes must be at least self-reviewed and all other guidelines continue to apply (follow code style, pass tests, make tests for new functionality, etc.) Intentional obfuscation of AI tooling usage is grounds for rejection and may result in being blocked from future contributions. Creating an RPM Package ~~~~~~~~~~~~~~~~~~~~~~~ We provide a template for an RPM spec file in :file:`package/python-kiwi-spec-template` alongside a `rpmlint` configuration file and an automatically updated :file:`python-kiwi.changes`. To create the necessary files to build an RPM package via `rpmbuild`, run: .. code:: shell-session $ make build The sources are collected in the :file:`dist/` directory. These can be directly built with :command:`rpmbuild`, :command:`fedpkg`, or submitted to the Open Build Service using :command:`osc`. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.735329 kiwi-11.1.8/doc/source/example_use_cases/build_container_image.rst0000644000000000000000000001002715273675667022312 0ustar00.. _building_container_build: Build a Container Image ======================= .. sidebar:: Abstract This page explains how to build a Container Image. It covers the following topics: * a basic configuration explanation * how to build a Container Image * how to run it with a Container Runtime {kiwi} can build native container images from scratch or using existing images. {kiwi} container images are considered native because a {kiwi} tarball image can be loaded directly into container runtimes like Podman, Docker, or Containerd, including common container configurations. The container configuration metadata is supplied to {kiwi} as part of the :ref:`XML description file ` using the `` tag. The following configuration metadata can be specified. `containerconfig` attributes: * ``name``: Specifies the repository name of the container image. * ``tag``: Sets the tag of the container image. * ``additionalnames``: Additional fully qualified names for the image, given as a comma-separated list of ``repository:tag`` entries. An entry that starts with ``:`` adds another tag for the primary repository name, for example ``:latest,:stable``. This attribute is only applied to Docker archive output (`image="docker"` or an OEM `format="docker:..."`); OCI archive output ignores it. * ``maintainer``: Specifies the author of the container. Equivalent to the `MAINTAINER` directive in a :file:`Dockerfile`. * ``user``: Sets the user name or user id (UID) to be used when running `entrypoint` and `subcommand`. Equivalent to the `USER` directive of a :file:`Dockerfile`. * ``workingdir``: Sets the working directory to be used when running `cmd` and `entrypoint`. Equivalent to the `WORKDIR` directive in a :file:`Dockerfile`. `containerconfig` child tags: * ``subcommand``: Provides the default execution parameters of the container. Equivalent to the `CMD` directive in a :file:`Dockerfile`. * ``labels``: Adds custom metadata to an image using key-value pairs. Equivalent to one or more `LABEL` directives in a :file:`Dockerfile`. * ``expose``: Defines which ports can be exposed to the outside when running this container image. Equivalent to one or more `EXPOSE` directives in a :file:`Dockerfile`. * ``environment``: Sets environment variables using key-value pairs. Equivalent to one or multiple `ENV` directives in a :file:`Dockerfile`. * ``entrypoint``: Sets the binary to use for executing all commands inside the container. Equivalent to the `ENTRYPOINT` directive of a :file:`Dockerfile`. * ``volumes``: Creates mountpoints with the given name and marks them to hold external volumes from the host or from other containers. Equivalent to one or more `VOLUME` directives in a :file:`Dockerfile`. * ``stopsignal``: The `stopsignal` element sets the system call signal that will be sent to the container to exit. This signal can be a signal name in the format `SIG[NAME]`, for instance, `SIGKILL`, or an unsigned number that matches a position in the kernel's syscall table, for instance, 9. The default is `SIGTERM` if not defined. Other :file:`Dockerfile` directives such as `RUN`, `COPY`, or `ADD`, can be mapped to {kiwi} using the :ref:`config.sh ` script file to run Bash commands, or the :ref:`overlay tree ` to include additional files. The following example illustrates how to build a container image based on openSUSE Leap: 1. Install `umoci` and `skopeo` tools. .. code:: bash $ zypper in umoci skopeo #. Build an image with {kiwi}: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_docker} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage #. Test the container image. First, load the new image into your container runtime: .. code:: bash $ podman load -i {exc_image_base_name_docker}.x86_64-{exc_image_version}.docker.tar.xz Then, run the image: .. code:: bash $ podman run --rm -it buildsystem /bin/bash ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_enclave.rst0000644000000000000000000000650315273675667020607 0ustar00.. _eif: Build an AWS Nitro Enclave ============================== .. sidebar:: Abstract This page explains how to build AWS Nitro Enclaves. It covers the following topics: * how to build an AWS Nitro Enclave * how to test the enclave via QEMU AWS Nitro Enclaves enables customers to create isolated compute environments to further protect and securely process highly sensitive data such as personally identifiable information (PII), healthcare, financial, and intellectual property data within their Amazon EC2 instances. Nitro Enclaves uses the same Nitro Hypervisor technology that provides CPU and memory isolation for EC2 instances. For further details, please visit https://aws.amazon.com/ec2/nitro/nitro-enclaves. To add an enclave build to your appliance, create a `type` element with `image` set to `enclave` in the :file:`config.xml` file, as shown below: .. code:: xml The following attributes of the `type` element are relevant: - `enclave_format`: Specifies the enclave target. As of today, only the `aws-nitro` enclave target is supported. - `kernelcmdline`: Specifies the kernel commandline suitable for the enclave. An enclave is a system that runs completely in RAM, loaded from an enclave binary format that includes the kernel, initrd, and the kernel command line suitable for the target system. With the appropriate settings specified in :file:`config.xml`, you can build an image using {kiwi}: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_enclave} \ --set-repo {exc_repo_rawhide} \ --target-dir /tmp/myimage The resulting image is saved in :file:`/tmp/myimage`, and the image can be tested with QEMU: .. code:: bash $ sudo qemu-system-x86_64 \ -M nitro-enclave,vsock=c \ -m 4G \ -nographic \ -chardev socket,id=c,path=/tmp/vhost4.socket \ -kernel {exc_image_base_name_enclave}.x86_64-{exc_image_version_enclave}.eif The image is now complete and ready to use. Access to the system is possible via ssh through a vsock connection into the guest. To establish a vsock connection, it's required to forward the connection through the guest's `AF_VSOCK` socket. This can be done via a `ProxyCommand` setup of the host's ssh as follows: .. code:: bash $ vi ~/bin/vsock-ssh.sh #!/bin/bash CID=$(echo "$1" | cut -d . -f 1) socat - VSOCK-CONNECT:$CID:22 .. code:: bash $ vi ~/.ssh/config host *.vsock ProxyCommand ~/bin/vsock-ssh.sh %h After the ssh proxy setup, log in to the enclave with a custom vsock CID as follows: .. code:: bash $ ssh root@21.vsock ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_expandable_disk.rst0000644000000000000000000004004315273675667022304 0ustar00.. _expandable_disk: Build an Expandable Disk Image ============================== .. sidebar:: Abstract This page explains how to build an expandable disk image. It covers the following topics: * builds an expandable disk image * deploys an expandable disk image * runs the deployed system An expandable disk represents the system disk with the capability to automatically expand the disk and its filesystem to a custom disk geometry. This allows deploying the same disk image on target systems with different hardware setups. The following example shows how to build and deploy an expandable disk image based on openSUSE Leap using a QEMU virtual machine as a target system: 1. Build an image with {kiwi}: .. code:: bash $ sudo kiwi-ng --type oem system build \ --description kiwi/build-tests/{exc_description_disk} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage The resulting image is saved in :file:`/tmp/myimage`. * The disk image with the suffix :file:`.raw` is an expandable virtual disk. It can expand itself to a custom disk geometry. * The installation image with the suffix :file:`.install.iso` is a hybrid installation system that contains the disk image and is capable of installing this image on any target disk. .. _deployment_methods: Deployment Methods ------------------ The goal of an expandable disk image is to provide the virtual disk data for OEM vendors to support easy deployment of the system to physical storage media. Basic deployment strategies are as follows: 1. :ref:`deploy_manually` Manually deploy the disk image onto the target disk. 2. :ref:`deploy_from_iso` Boot the installation image and let {kiwi}'s installer deploy the disk image from a CD/DVD or USB stick onto the target disk. 3. :ref:`deploy_from_network` PXE boot the target system and let {kiwi}'s installer deploy the disk image from the network onto the target disk. .. _deploy_manually: Manual Deployment ----------------- The manual deployment method can be tested using virtualization software like QEMU and an additional virtual large-size target disk. To do this, follow the steps below. 1. Create a target disk: .. code:: bash $ qemu-img create target_disk 20g .. note:: Retaining the Disk Geometry If the target disk geometry is less than or equal to the geometry of the disk image itself, the disk expansion that is performed on a physical disk install during the boot workflow is skipped, and the original disk geometry stays unchanged. 2. Dump the disk image on the target disk: .. code:: bash $ dd if={exc_image_base_name_disk}.x86_64-{exc_image_version}.raw of=target_disk conv=notrunc 3. Boot the target disk: .. code:: bash $ sudo qemu -hda target_disk -m 4096 -serial stdio On the first boot of the `target_disk`, the system is expanded to the configured storage layout. By default, the system root partition and filesystem are resized to the maximum free space available. .. _deploy_from_iso: CD/DVD Deployment ----------------- The deployment from a CD/DVD via an installation image can also be tested using virtualization software such as QEMU. To do this, follow the steps below. 1. Create a target disk: Follow the steps above to create a virtual target disk. 2. Boot the installation image as a CD/DVD with the target disk attached. .. code:: bash $ sudo qemu -cdrom \ {exc_image_base_name_disk}.x86_64-{exc_image_version}.install.iso -hda target_disk \ -boot d -m 4096 -serial stdio .. note:: USB Stick Deployment Like any other ISO image built with {kiwi}, the installation image is also a hybrid image. Thus, it can also be used on a USB stick and serve as installation media, as explained in :ref:`hybrid_iso`. .. _deploy_from_network: Network Deployment ------------------ The process of deployment from the network downloads the disk image from a PXE boot server. This requires a PXE network boot server to be set up as described in :ref:`network-boot-server`. If the PXE server is running, the following steps show how to test the deployment process over the network using a QEMU virtual machine as a target system: 1. Create an installation PXE TAR archive along with your disk image by replacing the following configuration in `kiwi/build-tests/{exc_description_disk}/appliance.kiwi`: Find the line below: .. code:: xml Modify the line as follows: .. code:: xml 2. Rebuild the image, unpack the resulting :file:`{exc_image_base_name_disk}.x86_64-{exc_image_version}.install.tar` file to a temporary directory, and copy the initrd and kernel images to the PXE server. a) Unpack the installation tarball: .. code:: bash mkdir /tmp/pxe && cd /tmp/pxe tar -xf {exc_image_base_name_disk}.x86_64-{exc_image_version}.install.tar b) Copy the kernel and initrd used for PXE boot: .. code:: bash scp pxeboot.{exc_image_base_name_disk}.x86_64-{exc_image_version}.initrd PXE_SERVER_IP:/srv/tftpboot/boot/initrd scp pxeboot.{exc_image_base_name_disk}.x86_64-{exc_image_version}.kernel PXE_SERVER_IP:/srv/tftpboot/boot/linux 3. Copy the disk image, SHA256 file, system kernel, initrd, and bootoptions to the PXE boot server. Activation of the deployed system is done via `kexec` of the kernel and initrd provided here. a) Copy the system image and SHA256 checksum: .. code:: bash scp {exc_image_base_name_disk}.x86_64-{exc_image_version}.xz PXE_SERVER_IP:/srv/tftpboot/image/ scp {exc_image_base_name_disk}.x86_64-{exc_image_version}.sha256 PXE_SERVER_IP:/srv/tftpboot/image/ b) Copy the kernel, initrd, and bootoptions used for booting the system via kexec: .. code:: bash scp {exc_image_base_name_disk}.x86_64-{exc_image_version}.initrd PXE_SERVER_IP:/srv/tftpboot/image/ scp {exc_image_base_name_disk}.x86_64-{exc_image_version}.kernel PXE_SERVER_IP:/srv/tftpboot/image/ scp {exc_image_base_name_disk}.x86_64-{exc_image_version}.config.bootoptions PXE_SERVER_IP:/srv/tftpboot/image/ .. note:: The `config.bootoptions` file is used with `kexec` to boot the previously dumped image. This file specifies the root of the dumped image, and the file can include other boot options. The file provided with the {kiwi}-built image is connected to the image present in the PXE TAR archive. If other images are deployed, the file must be modified to match the correct root reference. .. note:: If the image is deployed into a ramdisk that is configured by passing `rd.kiwi.ramdisk` as part of the append setup below, it is not required to copy the above-mentioned kernel and initrd file to boot the system because for ramdisk deployments, the currently active kernel and initrd will be used, as kexec cannot be called with a system in memory. 4. Add/Update the kernel command-line parameters. Edit your PXE configuration (for example, :file:`pxelinux.cfg/default`) on the PXE server and add the following parameters to the append line similar to that shown below: .. code:: bash append initrd=boot/initrd rd.kiwi.install.pxe rd.kiwi.install.image=tftp://192.168.100.16/image/{exc_image_base_name_disk}.x86_64-{exc_image_version}.xz The location of the image is specified as a source URI that can point to any location supported by the `curl` command. {kiwi} uses `curl` to fetch the data from this URI. This means that the image, checksum file, system kernel, and initrd can be fetched from any server and do not need to be stored on the `PXE_SERVER`. By default, {kiwi} does not use specific `curl` options or flags. But it is possible to specify desired options by adding the `rd.kiwi.install.pxe.curl_options` flag to the kernel command line (`curl` options are passed as comma-separated values), for example: .. code:: bash rd.kiwi.install.pxe.curl_options=--retry,3,--retry-delay,3,--speed-limit,2048 The above instructs {kiwi} to run `curl` as follows: .. code:: bash curl --retry 3 --retry-delay 3 --speed-limit 2048 -f This can be particularly useful when the deployment infrastructure requires a specific download configuration. For example, setting more robust retries over an unstable network connection. .. note:: {kiwi} replaces commas with spaces and appends the result to the `curl` command. Keep that in mind, because command-line options that include commas break the command. .. note:: The initrd and Linux Kernel for PXE boot are always loaded via TFTP from the `PXE_SERVER`. 5. Create a target disk. Follow the steps above to create a virtual target disk. 6. Connect the client to the network and boot QEMU with the target disk attached to the virtual machine: .. code:: bash $ sudo qemu -boot n -hda target_disk -m 4096 .. note:: QEMU bridged networking To connect QEMU to the network, we recommend setting up a network bridge on the host system and connecting QEMU to it via a custom `/etc/qemu-ifup` configuration. For details, see https://en.wikibooks.org/wiki/QEMU/Networking. .. _oem_customize: OEM Customization ----------------- The deployment process of an OEM image can be customized using the `oemconfig` element. This element is a child section of the `type` element, for example: .. code:: xml 512 Below is a list of the most commonly used optional `oemconfig` element settings. oemconfig.oem-resize Determines if the disk has the capability to expand itself to a new disk geometry or not. By default, this feature is activated. The implementation of the resize capability is done in a dracut module packaged as `dracut-kiwi-oem-repart`. If `oem-resize` is set to `false`, the installation of the corresponding dracut package can be skipped as well. oemconfig.oem-boot-title By default, the string `OEM` is used as the boot manager menu entry when KIWI creates the GRUB configuration during deployment. The `oem-boot-title` element allows you to set a custom name for the grub menu entry. This value is represented by the `kiwi_oemtitle` variable in the initrd. oemconfig.oem-bootwait Determines if the system waits for user interaction before continuing the boot process after the disk image has been dumped to the designated storage device (the default value is `false`). This value is represented by the `kiwi_oembootwait` variable in the initrd. oemconfig.oem-reboot When enabled, the system is rebooted after the disk image has been deployed to the designated storage device (the default value is `false`). This value is represented by the `kiwi_oemreboot` variable in the initrd. oemconfig.oem-reboot-interactive When enabled, the system is rebooted after the disk image has been deployed to the designated storage device (the default value is `false`). Before the reboot, a message is displayed, and it must be acknowledged by the user for the system to reboot. This value is represented by the `kiwi_oemrebootinteractive` variable in the initrd. oemconfig.oem-silent-boot Determines if the system boots in silent mode after the disk image has been deployed to the designated storage device (the default value is `false`). This value is represented by the `kiwi_oemsilentboot` variable in the initrd. oemconfig.oem-shutdown Determines if the system is powered down after the disk image has been deployed to the designated storage device (the default value is `false`). This value is represented by the `kiwi_oemshutdown` variable in the initrd. oemconfig.oem-shutdown-interactive Determines if the system is powered down after the disk image has been deployed to the designated storage device (the default value is `false`). Before the shutdown, a message is displayed, and it must be acknowledged by the user for the system to power off. This value is represented by the `kiwi_oemshutdowninteractive` variable in the initrd. oemconfig.oem-swap Determines if a swap partition is created. By default, no swap partition is created. This setting is evaluated by {kiwi} at build time. oemconfig.oem-swapname Specifies the name of the swap space. By default, the name is set to `LVSwap`. The default indicates that this setting is only useful in combination with the LVM volume manager. In this case, the swapspace is configured as a volume in the volume group, and every volume requires a name. The name specified in `oemconfig.oem-swapname` here is used as the name of the swap volume. oemconfig.oem-swapsize Specifies the size of the swap partition in MB. The value is only used if `oem-swap` is set to `true`. If a swap partition is created while the size of the swap partition is not specified, KIWI uses a default size of 128 MB. This value is represented by the `kiwi_oemswapMB` variable in the initrd. oemconfig.oem-systemsize Specifies the MB size the operating system is allowed to occupy on the target disk. The size limit does not include any swap space or recovery partition considerations. In a setup *without* the `systemdisk` element, this value specifies the size of the root partition. In a setup that *includes* the `systemdisk` element when using LVM, this value specifies the size of the LVM partition that contains all specified volumes. This means that the sum of all specified volume sizes plus the sum of the specified freespace for each volume must be smaller than or equal to the size specified with the `oem-systemsize` element. This value is represented by the variable `kiwi_oemrootMB` in the initrd. The specified value can be dynamically overwritten via the `rd.kiwi.install.systemsize` kernel boot option. If `rd.kiwi.install.systemsize` is set to `all`, this will unset the size limit, and the maximum possible size will apply. oemconfig.oem-unattended The installation of the image to the target system occurs automatically without requiring user interaction. If multiple possible target devices are discovered, the image is deployed to the first device. This value is represented by the `kiwi_oemunattended` variable in the initrd. oemconfig.oem-unattended-id Selects a target disk device for the installation according to the specified device ID. The device ID corresponds to the name of the device for the configured `devicepersistency`. By default, it is the `by-uuid` device name. If no representation exists, for example, for ramdisk devices, the UNIX device node can be used to select one. The given name must be present in the device list detected by KIWI. oemconfig.oem-skip-verify Disables the checksum verification process after installing the image on the target disk. The verification process computes the checksum of the image installed on the target. This value is then compared to the initrd-embedded checksum generated at the build time of the image. Depending on the size of the image and machine power, computing the checksum may take time. .. _installmedia_customize: Installation Media Customization -------------------------------- The installation media created for OEM network or CD/DVD deployments can be customized with the `installmedia` section. It is a child section of the `type` element, for example: .. code:: xml The `installmedia` is only available for OEM image types that include the request to create an installation media. The `initrd` child element of `installmedia` lists dracut modules. The element's `action` attribute determines whether the dracut module is omitted (`action="omit"`) or added (`action="add"`). Use `action="set"` to use only the listed modules and nothing else (that is, none of the dracut modules included by default). ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_in_buildservice.rst0000644000000000000000000003061515273675667022341 0ustar00Building in the Open Build Service ================================== .. note:: **Abstract** This document gives a brief overview of how to build images with {kiwi} in version |version| inside of the Open Build Service. A tutorial on the Open Build Service itself can be found here: https://en.opensuse.org/openSUSE:Build_Service_Tutorial The next-generation {kiwi} is fully integrated with the Open Build Service. To start, it's best to check out one of the integration test image build projects from the base Testing project `Virtualization:Appliances:Images:Testing_$ARCH:$DISTRO` at: https://build.opensuse.org For example, the test images for SUSE on x86 can be found `here `__. Advantages of using the Open Build Service (OBS) ------------------------------------------------ The Open Build Service offers multiple advantages over running {kiwi} locally: * OBS will host the latest successful build for you without you having to set up a server yourself. * As {kiwi} is fully integrated into OBS, OBS will automatically rebuild your images if one of the included packages, one of its dependencies, or {kiwi} itself is updated. * The builds will no longer have to be executed on your own machine but will run on OBS, thereby saving you resources. Nevertheless, if a build fails, you will get a notification via email (if enabled in your user's preferences). Differences Between Building Locally and on OBS ----------------------------------------------- Note that there are a number of differences when building images with {kiwi} using the Open Build Service. Your image that built locally just fine might not build without modifications. The notable differences to running {kiwi} locally include: * OBS will pick the {kiwi} package from the repositories configured in your project, which will most likely not be the same version that you are running locally. This is especially relevant when building images for older versions, like SUSE Linux Enterprise. Therefore, include the custom appliances repository as described in the following section: :ref:`obs-recommended-settings`. * When {kiwi} runs on OBS, OBS will extract the list of packages from :file:`config.xml` and use it to create a build root. In contrast to a local build (where your distribution's package manager will resolve the dependencies and install the packages), OBS will **not** build your image if there are multiple packages that could be chosen to satisfy the dependencies of your packages [#f1]_. This shows errors like this: .. code:: bash unresolvable: have choice for SOMEPACKAGE: SOMEPACKAGE_1 SOMEPACKAGE_2 This can be solved by explicitly specifying one of the two packages in the project configuration via the following setting: .. code:: bash Prefer: SOMEPACKAGE_1 Place the above line into the project configuration, which can be accessed either via the web interface (click on the tab ``Project Config`` on your project's main page) or via ``osc meta -e prjconf``. .. warning:: We strongly encourage you to remove your repositories from :file:`config.xml` and move them to the repository configuration in your project's settings. This usually prevents the issue of having the choice for multiple package versions and results in a much smoother experience when using OBS. * By default, OBS builds only a single build type and the default profile. If your appliance uses multiple build types, put each build type into a profile, as OBS cannot handle multiple build types. There are two options to build multiple profiles on OBS: 1. Add the `OBS-Profiles` XML comment between the XML declaration (``) and the opening `` element: .. code:: xml 2. Use the `multibuild `_ feature. The first option is simpler to use but has the disadvantage that your appliances are built sequentially. The `multibuild` feature allows you to build each profile as a single package, thereby enabling parallel execution, but it requires an additional :file:`_multibuild` file. For the above example, :file:`config.xml` would have to be adapted as follows: .. code:: xml The file :file:`_multibuild` would have the following contents: .. code:: xml foo_profile bar_profile * Subfolders in OBS projects are ignored by default by :command:`osc` and must be explicitly added via `osc add $FOLDER` [#f2]_. Bear that in mind when adding the overlay files inside the :file:`root/` directory to your project. * OBS ignores file permissions. Therefore, :file:`config.sh` and :file:`images.sh` will **always** be executed through BASH (see also: :ref:`working-with-kiwi-user-defined-scripts`). .. _obs-recommended-settings: Recommendations --------------- Working with OBS ^^^^^^^^^^^^^^^^ Although OBS is an online service, it is not necessary to test every change by uploading it. OBS will use the same process as `osc build` does, so if your image builds locally via `osc build`, it should also build online on OBS. Repository Configuration ^^^^^^^^^^^^^^^^^^^^^^^^ When setting up the project, enable the `images` repository: the `images` repository's checkbox can be found at the bottom of the selection screen that appears when clicking `Add from a Distribution` in the `Repositories` tab. Or, specify it manually in the project configuration (it can be accessed via `osc meta -e prj`): .. code:: xml x86_64 Furthermore, OBS requires additional repositories from which it obtains your dependent packages. These repositories can be provided in two ways: #. Add the repositories to the project configuration on OBS and omit them from :file:`config.xml`. Provide only the following repository inside the image description: .. code:: xml This instructs OBS to inject the repositories from your project into your appliance. Additional repositories can be added by invoking `osc meta -e prj` and adding a line of the following form as a child of ``: .. code:: xml The order in which you add repositories matters: if a package is present in multiple repositories, then it is taken from the **first** repository. The **last** repository is subject to path expansion: its repository paths are included as well. Don't forget to add the repository from the `Virtualization:Appliances:Builder` project, providing the latest stable version of {kiwi} (which you are very likely using for your local builds). The following example repository configuration [#f3]_ adds the repositories from the `Virtualization:Appliances:Builder` project and those from the latest snapshot of openSUSE Tumbleweed: .. code:: xml Tumbleweed JeOS images Host JeOS images for Tumbleweed x86_64 The above can be simplified further using the path expansion of the last repository to: .. code:: xml Tumbleweed JeOS images Host JeOS images for Tumbleweed x86_64 Now, `Virtualization:Appliances:Builder` is the last repository whose repositories are included into the search path. As `openSUSE:Factory/snapshot` is among these, it can be omitted from the repository list. #. Keep the repositories in your :file:`config.xml` configuration file. If you have installed the latest stable {kiwi} as described in :ref:`kiwi-installation`, then you should add the following repository to your project's configuration (accessible via :command:`osc meta -e prj`), so that OBS will pick the latest stable {kiwi} version too: .. code:: xml x86_64 Replace `$DISTRO` with the appropriate name for the distribution that you are currently building and optionally adjust the architecture. We recommend using the first method, as it integrates better into OBS. Note that your image description will then no longer build outside of OBS, though. If building locally is required, use the second method. .. warning:: Adding the repositories to the project's configuration makes it impossible to build images for different distributions from the same project. Since the repositories are added for every package in your project, all your image builds will share the same repositories, thereby resulting in conflicts for different distributions. We recommend creating a separate project for each distribution. If that is impossible, you can keep all your repositories (including `Virtualization:Appliances:Builder`) in :file:`config.xml`. That, however, usually requires a large number of workarounds via `Prefer:` settings in the project configuration and is thus **not** recommended. Project Configuration ^^^^^^^^^^^^^^^^^^^^^ The Open Build Service will, by default, create the same output file as {kiwi} when run locally but with a custom filename ending (that is, unfortunately, unpredictable). This has the consequence that the download URL of your image will change with every rebuild (and thus break automated scripts). OBS can create symbolic links with static names to the latest build by adding the following line to the project configuration: .. code:: bash Repotype: staticlinks If you build Vagrant images (see :ref:`setup_vagrant`), add the repository-type `vagrant`. OBS creates a `boxes/` subdirectory in your download repositories, which contains JSON files for Vagrant [#f4]_. If you have added your repositories to :file:`config.xml`, you will probably see errors of the following type: .. code:: bash unresolvable: have choice for SOMEPACKAGE: SOMEPACKAGE_1 SOMEPACKAGE_2 Instead of starting from scratch and manually adding `Prefer:` statements to the project configuration, we recommend copying the current project configuration of the testing project `Virtualization:Appliances:Images:Testing_$ARCH:$DISTRO` into your own project. It provides a good starting point and can be adapted to your OBS project. .. [#f1] This is a design decision made by OBS: as its purpose is to build packages in a reproducible fashion, it cannot make a decision which package to choose from multiple available ones. A package manager built for end users, on the other hand, **must** make a choice, as it would be otherwise hardly usable. .. [#f2] `osc` compresses added folders into a `cpio `_ archive and decompresses it before running your builds. The only downside of this is that the contents of your overlay are not conveniently visible via the web interface. .. [#f3] Taken from the project `Virtualization:Appliances:Images:openSUSE-Tumbleweed `_. .. [#f4] Vagrant uses these JSON files for automatic updates of your Vagrant boxes. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_kis.rst0000644000000000000000000001006115273675667017752 0ustar00.. _kis: Build KIS Image (Kernel, Initrd, System) ======================================== .. sidebar:: Abstract This page explains how to build an image that consists of three components: the kernel, an initrd, and an optional root filesystem image. In {kiwi} terminology, this type of image is called KIS. A KIS image is a collection of image components that are not associated with a dedicated use case. This means that as far as {kiwi} is concerned, it is not known in which environment these components are expected to be used. The predecessor of this image type was called `pxe` under the assumption that the components will be used in a PXE boot environment. However, this assumption is not always true, and the image components may be used in different ways. Because there are so many possible deployment strategies for a `kernel` plus `initrd` and an optional `system root filesystem`, {kiwi} provides this as the universal `KIS` type. The former `pxe` image type still exists, but it is expected to be used only in combination with the legacy `netboot` infrastructure, as described in :ref:`build_legacy_pxe`. To add a KIS build to an appliance, create a `type` element with `image` set to `kis` in :file:`config.xml`, as shown below: .. code:: xml With this image type setup, {kiwi} builds a kernel and initrd not associated with any system root filesystem. Normally, such an image is only useful with certain custom dracut extensions as part of the image description. The following attributes of the `type` element are often used when building KIS images: - `archive`: By default the output files of a KIS image will be stored in a tarball archive. If you want to store the output files individually instead, set this attribute to `false`. - `compressed`: Specifies whether the root filesystem image should be xz-compressed. If set to `true`, the tar archive that contains the output files is not compressed again; otherwise, the tar archive is xz-compressed. If the `archive` attribute is set to `false`, only the compression of the root filesystem image applies. - `filesystem`: Specifies the root filesystem and triggers the build of an additional filesystem image of that filesystem. The generated kernel command-line options file (append file) will then also include a `root=` parameter that references this filesystem image's UUID. Whether the information from the append file should be used or not is optional. - `kernelcmdline`: Specifies kernel command-line options that are part of the generated kernel command-line options file (append file). By default, the append file contains no information other than the reference to the root UUID if the `filesystem` attribute is used. All other attributes of the `type` element that apply to an optional root filesystem image remain in effect in the system image of a KIS image as well. With the appropriate settings present in :file:`config.xml`, you can use {kiwi} to build the image: .. code:: bash $ sudo kiwi-ng --type kis system build \ --description kiwi/build-tests/{exc_description_kis} \ --set-repo {exc_repo_tumbleweed} \ --target-dir /tmp/myimage The resulting image components are saved in :file:`/tmp/myimage`. Outside of a deployment infrastructure, the example KIS image can be tested with QEMU as follows: .. code:: bash $ sudo qemu \ -kernel /tmp/myimage/*.kernel \ -initrd /tmp/myimage/*.initrd \ -append "$(cat /tmp/myimage/*.append) rw" \ -drive file=/tmp/myimage/{exc_image_base_name_kis}.x86_64-{exc_image_version_kis},if=virtio,driver=raw \ -serial stdio .. note:: Testing the components of a KIS image normally requires a deployment infrastructure and a deployment process encoded in the initrd. An example of a deployment infrastructure using PXE is provided by {kiwi} with the `netboot` infrastructure. However, that netboot infrastructure is no longer developed and is only kept for compatibility reasons. For details, see :ref:`build_legacy_pxe`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_live_iso.rst0000644000000000000000000001460315273675667021003 0ustar00.. _hybrid_iso: Build an ISO Hybrid Live Image ============================== .. sidebar:: Abstract This page explains how to build a live image. It covers the following topics: * how to build an ISO image * how to run the image with QEMU A Live ISO image is a system on removable media, for example, a CD/DVD or a USB stick. Booting a Live ISO image does not interfere with other system storage components, making it a useful portable system for demonstration, testing, and debugging. To add a Live ISO build to your appliance, create a `type` element with `image` set to `iso` in the :file:`config.xml` file, as shown below: .. code:: xml The following attributes of the `type` element are relevant when building Live ISO images: - `flags`: Specifies the dracut module to use. If set to `overlay`, the kiwi-live dracut module supplied by {kiwi} is used for booting. If set to `dmsquash`, the dracut-supplied `dmsquash-live` module is used for booting. Both modules support a different set of live features. For details, see :ref:`live_features`. - `filesystem`: Specifies the root filesystem for the live system. If set to `squashfs`, the root filesystem is written into a squashfs image. This option is not compatible with device-mapper-specific features of the `dmsquash-live` dracut module. In that case, use `overlayfs`. If set to a value different from `squashfs` or `erofs`, the root filesystem is written into a filesystem image of the specified type, and the filesystem image is written into a squashfs image for compression. The default value of this option is `ext4`. - `hybridpersistent`: Accepts `true` or `false`; if set to `true`, the resulting image is created with a COW file to keep data persistent over a reboot. - `hybridpersistent_filesystem`: The filesystem used for the COW file. Valid values are `ext4` or `xfs`, with `ext4` being the default. With the appropriate settings specified in :file:`config.xml`, you can build an image using {kiwi}: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_live} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage The resulting image is saved in :file:`/tmp/myimage`, and the image can be tested with QEMU: .. code:: bash $ sudo qemu -cdrom \ {exc_image_base_name_live}.x86_64-{exc_image_version}.iso \ -m 4096 -serial stdio The image is now complete and ready to use. See :ref:`iso_to_usb_stick` and :ref:`iso_as_file_to_usb_stick` for further information concerning deployment. .. _live_features: `overlay` or `dmsquash` ---------------------------------- Whether you choose the `overlay` or `dmsquash` dracut module depends on the features you intend to use. The `overlay` module supports only `overlayfs`-based overlays but with automatic creation of a writable layer for persistence. The `dmsquash` module supports `overlayfs` as well as device-mapper-based overlays. The following list describes important Live ISO features and their support status in the `overlay` and `dmsquash` modules. ISO scan Usable in the same way with both dracut modules. This feature allows you to boot the Live ISO as a file from a grub loopback-configured bootloader. The `live-grub-stick` tool is one example that uses this feature. For details on how to set up ISO scan with the `overlay` module, see :ref:`iso_as_file_to_usb_stick`. ISO in RAM completely Usable with the `dmsquash` module through `rd.live.ram`. The `overlay` module does not support this mode, while {kiwi} supports RAM-only systems as OEM deployment into RAM from an install ISO media. For details on how to set up RAM-only deployments in {kiwi}, see :ref:`ramdisk_deployment`. Overlay based on overlayfs Usable with both dracut modules. The read-only root filesystem is overlaid with a read-write filesystem using the kernel's `overlayfs` filesystem. Overlay based on device-mapper snapshots Usable with the `dmsquash` module. A squashfs-compressed read-only root is overlaid with a read-write filesystem using a device-mapper snapshot. Media Checksum Verification Boot the Live ISO only for ISO checksum verification. This is possible with both modules, but the `overlay` module uses the `checkmedia` tool, whereas the upstream `dmsquash` module uses `checkisomd5`. The verification process is triggered by passing the kernel option `mediacheck` for the `overlay` module and `rd.live.check` for the `dmsquash` module. Live ISO through PXE boot Boot the Live image via the network. This is possible with both modules, but it uses different technologies. The `overlay` module supports network boot by fetching the ISO image via `ftp`, `http`, `https`, or `dolly` from `root=live:`, as well as via the AoE (ATA over Ethernet) protocol. For details, see :ref:`network_live_boot`. The `dmsquash` module supports network boot by fetching the ISO image into memory from `root=live:` using the `livenet` module. Persistent Data Keep new data persistent on a writable storage device. This can be done with both modules but in different ways. The `overlay` module activates persistency with the kernel boot parameter `rd.live.overlay.persistent`. If the persistent setup cannot be created, the fallback to the non-persistent mode applies automatically. The `overlay` module autodetects if it is used on a disk or ISO scan loop booted from a file. If booted as a disk, persistency is set up on a new partition of that disk. If loop-booted from a file, persistency is set up on a new COW file. The COW file/partition setup can be influenced by the kernel boot parameters: `rd.live.overlay.cowfs` and `rd.live.cowfile.mbsize`. The `dmsquash` module configures persistency through the `rd.live.overlay` option exclusively and does not support the automatic creation of a write partition in disk mode. .. admonition:: dmsquash documentation Documentation for the upstream `dmsquash` module can be found `here `_. Options to set up `dmsquash` are marked with `rd.live`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_simple_disk.rst0000644000000000000000000002745715273675667021510 0ustar00.. _simple_disk: Build a Virtual Disk Image ========================== .. sidebar:: Abstract This page explains how to build a simple disk image. It covers the following topics: - defines a simple disk image in the image description - builds a simple disk image - runs it with QEMU A simple virtual disk image is a compressed system disk with additional metadata useful for cloud frameworks like Amazon EC2, Google Compute Engine, or Microsoft Azure. It is used as the native disk of a system, and it does not require an additional installation workflow or a complex first-boot setup procedure. To enable {kiwi} to build a simple disk image, add a `type` element with `image="oem"` in :file:`config.xml`, where the `oem-resize` option is disabled. An example configuration for a 42-GB large VMDK image with 512 MB of RAM, an IDE controller, and a bridged network interface is shown below: .. code:: xml 42 false The following attributes of the `type` element deserve attention when building simple disk images: - `format`: Specifies the format of the virtual disk. Possible values are `gce`, `ova`, `qcow2`, `vagrant`, `vmdk`, `vdi`, `vhd`, `vhdx`, and `vhd-fixed`. In addition, a disk can be wrapped into a container by prefixing the format with `oci:` or `docker:`, for example `format="oci:qcow2"`. The permitted sub-formats are `gce`, `ova`, `qcow2`, `vmdk`, `vdi`, `vhd`, `vhdx`, `vhd-fixed` and `raw`. Note that `vagrant` cannot be used as a sub-format. - `formatoptions`: Specifies additional format options passed to :command:`qemu-img`. `formatoptions` is a comma-separated list of format-specific options in a `name=value` format, as expected by :command:`qemu-img`. {kiwi} forwards the settings from the attribute as a parameter to the `-o` option in the :command:`qemu-img` call. The `bootloader`, `size`, and `machine` child elements of `type` can be used to customize the virtual machine image. These elements are described in the following sections: :ref:`disk-bootloader`, :ref:`disk-the-size-element`, and :ref:`disk-the-machine-element`. Once your image description is finished, you can build the image using the following {kiwi} command: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_disk_simple} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage The resulting :file:`.raw` image is stored in :file:`/tmp/myimage`. You can test the image using QEMU: .. code:: bash $ sudo qemu \ -drive file={exc_image_base_name_disk_simple}.x86_64-{exc_image_version}.raw,format=raw,if=virtio \ -m 4096 For further information on how to configure the image to work within a cloud framework, see: * :ref:`setup_for_ec2` * :ref:`setup_for_azure` * :ref:`setup_for_gce` For information on how to set up a Vagrant system, see :ref:`setup_vagrant`. .. _disk-bootloader: Setting up the Bootloader in the Image -------------------------------------- .. code:: xml The `bootloader` element defines which bootloader to use in the image, and the element offers several options for customizing its configuration. For details, see :ref:`preferences-type-bootloader`. .. _disk-the-size-element: Modifying the Size of the Image ------------------------------- The `size` child element of `type` specifies the size of the resulting disk image. The following example shows an image description where 20 GB are added to the virtual machine image, of which 5 GB are left unpartitioned: .. code:: xml 20 false The following optional attributes can be used to further customize the image size: - `unit`: Defines the unit used for the provided numerical value. Possible values are `M` for megabytes and `G` for gigabytes. The default unit is megabytes. - `additive`: A boolean value that determines whether the provided value is added to the current image size (`additive="true"`) or whether it is the total size (`additive="false"`). The default value is `false`. - `unpartitioned`: Specifies the image space in the image that is not partitioned. The attribute uses either the same unit as defined in the attribute `unit` or the default value. .. _disk-the-machine-element: Customizing the Virtual Machine ------------------------------- The `machine` child element of `type` can be used to customize the virtual machine configuration, including the number of CPUs and the connected network interfaces. The following attributes are supported by the `machine` element: - `ovftype`: The OVF configuration type. The Open Virtualization Format is a standard for describing virtual appliances and distributing them in an archive called an Open Virtual Appliance (OVA). The standard describes the major components associated with a disk image. The exact specification depends on the product using the format. Supported value is `vmware`. - `HWversion`: The virtual machine's hardware version (`vmdk` and `ova` formats only); refer to VMware documentation for further information on the supported values. - `arch`: the VM architecture (`vmdk` format only). Valid values are `ix86` (= `i586` and `i686`) and `x86_64`. - `xen_loader`: the Xen target loader that is expected to load the guest. Valid values are `hvmloader`, `pygrub`, and `pvgrub`. - `guestOS`: The virtual guest OS' identification string for the VM (only applicable for `vmdk` and `ova` formats. Note that the name designation is different for the two formats). Note: For vmware ovftools, guestOS is a VMX GuestOS, but not a VIM GuestOS. For instance, the correct value for Ubuntu 64-bit is "ubuntu-64", but not "ubuntu64Guest". See GUEST_OS_KEY_MAP in guest_os_tables.h at https://github.com/vmware/open-vm-tools for another guestOS values. - `min_memory`: The virtual machine's minimum memory in MB (`ova` format only). - `max_memory`: The virtual machine's maximum memory in MB (`ova` format only). - `min_cpu`: The virtual machine's minimum CPU count (`ova` format only). - `max_cpu`: The virtual machine's maximum CPU count (`ova` format only). - `memory`: The virtual machine's memory in MB (all formats). - `ncpus`: The number of virtual CPUs available to the virtual machine (all formats). `machine` also supports additional child elements that are covered in the following subsections. Modifying the VM Configuration Directly ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `vmconfig-entry` element is used to add entries directly into the virtual machine's configuration file. This is currently only supported for the `vmdk` format, where the provided strings are directly pasted into the :file:`.vmx` file. The `vmconfig-entry` element has no attributes and can appear multiple times. The entries are added to the configuration file in the provided order. Note that {kiwi} does not check the entries for correctness. The following example adds the two entries `numvcpus = "4"` and `cpuid.coresPerSocket = "2"` into the VM configuration file: .. code:: xml numvcpus = "4" cpuid.coresPerSocket = "2" Adding Network Interfaces to the VM ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Network interfaces can be explicitly specified for the VM when required via the `vmnic` element. This makes it possible to add another bridged interface or to specify the driver to be used. Note that this element is used for the `vmdk` image format only. The following example adds a bridged network interface that uses the `e1000` driver: .. code:: xml The `vmnic` element supports the following attributes: - `interface`: **Mandatory** interface ID for the VM's network interface. - `driver`: An optional driver. - `mac`: The MAC address of the specified interface. - `mode`: The mode of the interface. Note that {kiwi} does **not** verify the values of the attributes; it only inserts them into the appropriate configuration files. Specifying Disks and Disk Controllers ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ The `vmdisk` element can be used to customize the disk and disk controller for the virtual machine. At most one `vmdisk` element may be specified per `machine` element. Note that this element is used for `vmdk` and `ova` image formats only. The following example adds a disk with the ID 0 that uses an IDE controller: .. code:: xml Each `vmdisk` element can be further customized using optional attributes: - `controller`: The disk controller used for the VM guest (`vmdk` and `ova` formats). Supported values are `ide`, `sata`, `nvme`, `scsi`, `buslogic`, `lsilogic`, `lsisas1068`, `legacyESX`, `pvscsi`, and `paravirtual`. The values `scsi`, `paravirtual`, and `pvscsi` all select the VMware paravirtual SCSI controller (`VirtualSCSI` in the OVF descriptor). Recent vCenter versions only accept this SCSI controller type for many guest operating systems. `sata` and `nvme` are supported for the `ova` format only. `lsisas1068` and `legacyESX` are supported for the `vmdk` format only. If no controller is set, the `ova` format uses an NVMe controller. - `device`: The disk device to appear in the guest (`xen` format only). - `diskmode`: The disk mode (`vmdk` format only). Valid values are `monolithicSparse`, `monolithicFlat`, `twoGbMaxExtentSparse`, `twoGbMaxExtentFlat`, and `streamOptimized` (see also https://vdc-download.vmware.com/vmwb-repository/dcr-public/6335f27c-c6e9-4804-95b0-ea9449958403/c7798a8b-4c73-41d9-84e8-db5453de7b17/doc/vddkDataStruct.5.3.html). - `disktype`: The type of the disk handled internally by the VM (`ova` format only). This attribute is currently unused. - `id`: The disk ID of the VM disk (`vmdk` format only). Adding CD/DVD Drives ^^^^^^^^^^^^^^^^^^^^ {kiwi} supports adding one IDE or SCSI CD/DVD drive to the virtual machine using the `vmdvd` element for the `vmdk` and `ova` image formats. At most one `vmdvd` element may be specified per `machine` element. The following example adds a single drive with a SCSI controller: .. code:: xml The `vmdvd` element features two **mandatory** attributes: - `id`: The CD/DVD ID of the drive. - `controller`: The CD/DVD controller used for the VM guest. Valid values are `ide` and `scsi`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_with_profiles.rst0000644000000000000000000000404715273675667022051 0ustar00.. _building-build-with-profiles: Building Images with Profiles ============================= {kiwi} supports so-called *profiles* inside the XML image description. Profiles act as namespaces for additional settings to be applied on top of the defaults. For further details, see :ref:`image-profiles`. Local Builds ------------ To execute local {kiwi} builds with a specific, selected profile, add the command line flag `--profile=$PROFILE_NAME`: .. code:: shell-session $ sudo kiwi-ng --type oem --profile libvirt system build \ --description kiwi/build-tests/{exc_description_vagrant} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage Consult the manual page of :file:`kiwi` for further details: :ref:`db_commands_kiwi_synopsis`. Building with the Open Build Service ------------------------------------ The Open Build Service (OBS) supports profiles via the `multibuild `_ feature. To enable and use the profiles, follow these steps: #. Add the following XML comment to your :file:`config.xml`: .. code:: xml It must be added before the opening `` element and after the `` element, e.g.: .. code:: xml #. Add a file :file:`_multibuild` into your package's repository with the following contents: .. code:: xml profile_1 profile_2 Add a line ``$PROFILE`` for each profile that you want OBS to build. Note that, by default, OBS excludes the build **without** any profile enabled. Running a build of a multibuild-enabled repository via :file:`osc` can be achieved via the `-M $PROFILE` flag: .. code:: shell-session $ osc build -M $PROFILE ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_without_debianbootstrap.rst0000644000000000000000000001100515273675667024126 0ustar00.. _debianbootstrap_alternative: Circumvent Debian Bootstrap =========================== .. sidebar:: Abstract This page provides information on how to build Debian-based images without an extra bootstrap process. When building Debian-based images, {kiwi} uses `apt` in the bootstrap and the system phase to create the image root tree. However, `apt` does not support a native way to bootstrap an empty root tree. Therefore, the bootstrap phase uses apt only to resolve the given bootstrap packages and to download these packages from the given repositories. The list of packages is then manually extracted into the new root tree, which is not exactly the same as if `apt` would have installed them natively. For the purpose of creating an initial tree to begin with, this procedure is acceptable, though. If, for some reason, this bootstrap procedure is not applicable, {kiwi} allows for an alternative process that is based on a prebuilt bootstrap-root archive provided as a package. To make use of a `bootstrap_package`, the name of that package needs to be referenced in the {kiwi} description as follows: .. code:: xml The bootstrap process now changes in a way that the provided `bootstrap_package`, `bootstrap-root`, will be installed on the build host machine. Next, {kiwi} searches for a tar archive file, :file:`/var/lib/bootstrap/bootstrap-root.ARCH.tar.xz`, where `ARCH` is the name of the host architecture, e.g., `x86_64`. If found, the archive is unpacked and serves as the bootstrap root tree to begin with. The optionally provided additional bootstrap packages, `a` and `b` in this example, will be installed like system packages via `chroot` and `apt`. Usually, no additional bootstrap packages are needed, as they could all be handled as system packages. How to Create a bootstrap_package --------------------------------- Changing the setup in {kiwi} to use a `bootstrap_package` rather than using {kiwi}'s Debian bootstrap method to do the job comes with the task of creating that package, providing the bootstrap root tree. There is more than one way to do this. The following procedure is just one example and requires some background knowledge about the Open Build Service (`OBS `__) and its {kiwi} integration. 1. Create an OBS project and repository setup that matches your image target. 2. Create an image build package. .. code:: bash osc mkpac bootstrap-root 3. Create the following :file:`appliance.kiwi` file: .. code:: xml The Author author@example.com prebuilt bootstrap rootfs for ... 1.0.1 apt .. code:: bash osc add appliance.kiwi osc ci 4. Package the image build results into a Debian package. In step 3, the bootstrap root tarball was created but not yet packaged. A Debian package is needed so that it can be referenced with the `bootstrap_package` attribute and the repository providing it. The simplest way to package the `bootstrap-root` tarball is to create another package in OBS and use the tarball file as its source. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/build_wsl_container.rst0000644000000000000000000001321015273675667022032 0ustar00.. _building_wsl_build: Build a WSL Container Image =========================== .. sidebar:: Abstract This page explains how to build a WSL/Appx container image. WSL stands for Windows Subsystem for Linux, and it is a zip-based container format consumable by Windows 10 with WSL enabled. {kiwi} can build WSL images using the :command:`appx` utility. Make sure you have installed the package that provides the command on your build host. Once the build host has the `appx` command installed, the following image type setup is required in the XML description, :file:`config.xml`: .. code:: xml The :file:`/meta/data` path specifies a path that provides additional information required for the `WSL-DistroLauncher`. This component consists of a Windows (`exe`) executable file and an :file:`AppxManifest.xml` file that references other files, like icons and resource configurations for the startup of the container under Windows. .. note:: **/meta/data** Except for the root filesystem tarball, {kiwi} is not responsible for providing the metadata required for the `WSL-DistroLauncher`. It is expected that the given metadata path contains all the needed information. Typically, this information is delivered in a package provided by the distribution, and it is installed on the build host. Setup of the WSL-DistroLauncher ------------------------------- The contents of the :file:`AppxManifest.xml` are changed by {kiwi} if the `containerconfig` section is provided in the XML description. In the context of a WSL image, the following container configuration parameters are taken into account: .. code:: xml Container Description Text All information provided here, including the entire section, is optional. If the information is not specified, the existing :file:`AppxManifest.xml` is left untouched. created_by Specifies the name of a publisher organization. An `appx` container must be signed off with a digital signature. If the image is built in the Open Build Service (OBS), this is done automatically. Outside of OBS, you must make sure that the given publisher organization name matches the certificate used for signing. author Provides the name of the author and maintainer of this container. application_id Specifies an ID name for the container. The name must start with a letter, and only alphanumeric characters are allowed. {kiwi} does not validate the specified name string because there are no common criteria for the various container architectures. package_version Specifies the version identification for the container. {kiwi} validates it against the `Microsoft Package Version Numbering `_ rules. launcher Specifies the binary file name of the launcher :file:`.exe` file. .. warning:: {kiwi} does not check the configuration in :file:`AppxManifest.xml` for validity or completeness. The following example shows how to build a WSL image based on openSUSE Tumbleweed: 1. Include the `Virtualization/WSL` repository in the list (replace `` with the desired distribution): .. code:: bash $ zypper addrepo http://download.opensuse.org/repositories/Virtualization:/WSL/ WSL #. Install the `fb-util-for-appx` utility and the package that provides the `WSL-DistroLauncher` metadata. See the previous note on :file:`/meta/data`. .. code:: bash $ zypper in fb-util-for-appx DISTRO_APPX_METADATA_PACKAGE .. note:: When building images with the Open Build Service, make sure to add the packages from the `zypper` command above to the project configuration via `osc meta -e prjconf` along with the line `support: PACKAGE_NAME` for each package that needs to be installed on the Open Build Service worker that runs the {kiwi} build process. #. Configure the image type: Add the following type and container configuration to :file:`kiwi/build-tests/{exc_description_wsl}/appliance.kiwi`: .. code:: xml Tumbleweed Appliance text based .. warning:: If the configured metadata path does not exist, the build will fail. Furthermore, {kiwi} does not check whether the metadata is complete or is valid according to the requirements of the `WSL-DistroLauncher`. #. Build the image with {kiwi}: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_wsl} \ --set-repo {exc_repo_tumbleweed} \ --target-dir /tmp/myimage Testing the WSL image --------------------- For testing the image, you need a Windows 10 system. Before you proceed, enable the optional feature named :file:`Microsoft-Windows-Subsystem-Linux`. For further details on how to set up the Windows machine, see `Windows Subsystem for Linux `__. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/clone_partitions.rst0000644000000000000000000001367015273675667021372 0ustar00.. _clone_partitions: Partition Clones ================ .. sidebar:: Abstract This page provides details about the partition clone feature and its use cases. {kiwi} allows you to create block-level clones of certain partitions used in the image. Clones can be created from the `root`, `boot`, and any other partition listed in the `` element. A partition clone is a simple byte dump from one block storage device to another. However, this would cause conflicts during boot of the system because all unique identifiers, like the UUID of a filesystem, will no longer be unique. The clone feature of {kiwi} takes care of this part and re-creates the relevant unique identifiers per cloned partition. {kiwi} allows this also for complex partitions like LVM, LUKS, or RAID. The partition clone(s) will always appear first in the partition table, followed by the origin partition. The origin partition is the one whose identifier will be referenced and used by the system. By default, no cloned partition will be mounted or used by the system at boot time. Let's take a look at the following example: .. code:: xml 2048 false 100 With the above setup, {kiwi} will create a disk image that contains the following partition table: .. code:: Number Start (sector) End (sector) Size Code Name 1 2048 6143 2.0 MiB EF02 p.legacy 2 6144 47103 20.0 MiB EF00 p.UEFI 3 47104 661503 300.0 MiB 8300 p.lxbootclone1 4 661504 1275903 300.0 MiB 8300 p.lxboot 5 1275904 1296383 10.0 MiB 8300 p.lxhomeclone1 6 1296384 1316863 10.0 MiB 8300 p.lxhomeclone2 7 1316864 1337343 10.0 MiB 8300 p.lxhome 8 1337344 3864575 2 GiB 8300 p.lxrootclone1 9 3864576 6287326 2 GiB 8300 p.lxroot When booting the system, only the origin partitions `p.lxboot`, `p.lxroot`, and `p.lxhome` will be mounted and visible in, e.g., :file:`/etc/fstab`, the bootloader, or the initrd. Thus, partition clones are present as a data source but are not relevant for the operating system from a functional perspective. As shown in the above example, there is one clone request for root and boot and two clone requests for the home partition. {kiwi} does not sanity- check the provided number of clones (e.g., whether your partition table can hold that many partitions). .. warning:: There is a limit to how many partitions a partition table can hold. This also limits how many clones can be created. It's important when using the `root_clone` attribute to specify a size for the part of the system that represents the root partition. As of today, {kiwi} does not automatically divide the root partition into two identical pieces. To create a clone of the partition, a size specification is required. In the above example, the size for root is provided via the `oem-systemsize` element. Using a root clone and fixed size values has the following consequences: 1. The resize capability must be disabled. This is done via the `oem-resize` element. The reason is that only the last partition in the partition table can be resized without destroying data. If there is a clone of the root partition, it should never be resized because then the two partitions will be different in size and no longer be clones of each other. 2. There can be unpartitioned space left. In the above example, the overall disk size is set to 100G. The sum of all partition sizes will be smaller than this value, and there is no resize available anymore. Depending on the overall size setup for the disk, this will leave unpartitioned space free on the disk. Use Case -------- Potential use cases for which a clone of one or more partitions is useful include, among others: Factory Resets: Creating an image with the option to roll back to the state of the system at deployment time can be very helpful for disaster recovery. System Updates with Rollbacks e.g A/B: Creating an image that holds extra space allowing to roll back modified data can make a system more robust. For example, in a simple A/B update concept, partition A would be updated but would flip to B if A is considered broken after applying the update. .. note:: Most probably, any use case based on partition clones requires additional software to manage them. {kiwi} provides the option to create the clone layout, but it does not provide the software to implement the actual use case for which the partition clones are needed. Developers writing applications based on a clone layout created with {kiwi} can leverage the metadata file :file:`/config.partids`. This file is created at build time and contains the mapping between the partition `name` and the actual partition number in the partition table. For partition clones, the following naming convention applies: .. code:: kiwi_(name)PartClone(id)="(partition_number)" The `(name)` is either taken from the `name` attribute of the `` element, or it is a fixed name assigned by {kiwi}. There are the following reserved partition names for which cloning is supported: * root * readonly * boot For the mentioned example, this will result in the following :file:`/config.partids`: .. code:: kiwi_BiosGrub="1" kiwi_EfiPart="2" kiwi_bootPartClone1="3" kiwi_BootPart="4" kiwi_homePartClone1="5" kiwi_homePartClone2="6" kiwi_HomePart="7" kiwi_rootPartClone1="8" kiwi_RootPart="9" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/custom_fstab_extension.rst0000644000000000000000000000543515273675667022603 0ustar00.. _custom_fstab_extension: Add or Update the Fstab File ============================ .. sidebar:: Abstract This page provides further information for customizing the `/etc/fstab` file which controls the mounting of filesystems at boot time. In {kiwi}, all major filesystems that were created at image build time are handled by {kiwi} itself and setup in `/etc/fstab`. Thus there is usually no need to add entries or change the ones added by {kiwi}. However depending on where the image is deployed later it might be required to pre-populate fstab entries that are unknown at the time the image is built. Possible use cases are for example: * Adding NFS locations that should be mounted at boot time. Using autofs would be an alternative to avoid additional entries to fstab. The information about the NFS location will make this image specific to the target network. This will be independent of the mount method, either fstab or the automount map has to provide it. * Adding or changing entries in a read-only root system which becomes effective on first boot but can't be added at that time because of the read-only characteristics. .. note:: Modifications to the fstab file are a critical change. If done wrongly the risk exists that the system will not boot. In addition this type of modification makes the image specific to its target and creates a dependency to the target hardware, network, etc... Thus this feature should be used with care. The optimal way to provide custom fstab information is through a package. If this can't be done the files can also be provided via the overlay file tree of the image description. {kiwi} supports three ways to modify the contents of the `/etc/fstab` file: Providing an `/etc/fstab.append` file If that file exists in the image root tree, {kiwi} will take its contents and append it to the existing `/etc/fstab` file. The provided `/etc/fstab.append` file will be deleted after successful modification. Providing an `/etc/fstab.patch` file The `/etc/fstab.patch` represents a patch file that will be applied to `/etc/fstab` using the `patch` program. This method also allows to change the existing contents of `/etc/fstab`. On success `/etc/fstab.patch` will be deleted. Providing an `/etc/fstab.script` file The `/etc/fstab.script` represents an executable which is called as chrooted process. This method is the most flexible one and allows to apply any change. On success `/etc/fstab.script` will be deleted. .. note:: All three variants to handle the fstab file can be used together. Appending happens first, patching afterwards and the script call is last. When using the script call, there is no validation that checks if the script actually handles fstab or any other file in the image rootfs. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/custom_partitions.rst0000644000000000000000000001457315273675667021607 0ustar00.. _custom_partitions: Custom Disk Partitions ====================== .. sidebar:: Abstract This page provides details about the opportunities and limitations of customizing the partition table in addition to the volume management settings from :ref:`custom_volumes`. {kiwi} has its own partitioning schema, which is defined according to several different user configurations: boot firmware, boot partition, expandable layouts, etc. Those supported features have an impact on the partitioning schema. MBR or GUID partition tables are not flexible, have limitations, and are tied to some specific disk geometry. Because of that, the preferred alternative to disk layouts based on traditional partition tables is using flexible approaches like logical volumes. However, under certain conditions, additional entries to the low-level partition table are needed. For this purpose, the `` section exists and allows you to add custom entries, as shown in the following example: .. code:: xml Each `` entry puts a partition of the configured size in the low-level partition table, creates a filesystem on it, and includes it in the system's fstab file. If parts of the root filesystem are moved into their own partition, as in the above example, this partition will also contain the data that gets installed during the image creation process to that area. The following attributes must/can be set to configure a partition entry: name="identifier" Mandatory name of the partition as handled by {kiwi}. .. note:: The following reserved names cannot be used because they are already represented by existing attributes: `root`, `readonly`, `boot`, `prep`, `spare`, `swap`, `efi_csm`, and `efi`. partition_name="name" Optional name of the partition as it appears when listing the table contents with tools like `gdisk`. If no name is set, {kiwi} constructs a name of the form `p.lx(identifier_from_name_attr)`. partition_type="type_identifier" Optional partition type identifier as handled by {kiwi}. Allowed values are `t.linux` and `t.raid`. If not specified, `t.linux` is the default. size="size_string" Mandatory size of the partition. A size string can end with `M` or `G` to indicate a megabyte or gigabyte value. Without a unit specification, megabyte is used. mountpoint="path" Optional mountpoint to mount the partition in the system. filesystem="btrfs|ext2|ext3|ext4|squashfs|xfs|fat32|fat16" Optional filesystem configuration to create one of the supported filesystems on the partition. label="string" Optional filesystem label if a filesystem is provided. If no label is specified, the `name` identifier in uppercase letters is set as the filesystem label. clone="number" Optional setting to indicate that this partition should be cloned `number` of times. A clone partition is content-wise an exact byte-for-byte copy of the origin. However, to avoid conflicts at boot time, the UUID of any cloned partition will be made unique. In the sequence of partitions, the clone(s) will always be created first, followed by the partition considered the origin. The origin partition is the one that will be referenced and used by the system. part_id="number" Specify the partition number for this partition. This is the number used for the partition in the partition table. The value impacts the name of the Unix device node associated with the partition. This means if the disk name is, e.g., `/dev/sda` and `part_id="42"` is set, the Unix node for this partition will be `/dev/sda42`. Please note that the number has no impact on the geometry or order of partitions as they are created. The partitions in the `` section are still created in the order in which they are listed. Please also note, if custom partition IDs are used, it is strongly recommended that they be used across all partitions created by kiwi. The reason for this is that kiwi uses an incremental counter that, by default, matches the number of the partition in the table. If custom partition IDs are not used for all partitions, this results in a mix of numbers, which works but is not obviously visible when reading the image description. Because of that, also see the information about the `` attributes `eficsmpart_id`, `efipart_id`, `rootpart_id`, and `bootpart_id`. Despite the customization options of the partition table shown above, there are the following limitations: 1. By default, the root partition is always the last one. Disk images built with {kiwi} are designed to be expandable. For this feature to work, the partition containing the system's rootfs must always be the last one. If this is unwanted for some reason, {kiwi} offers an opportunity for one extra/spare partition with the option to also be placed at the end of the table. For details, look up `spare_part` in :ref:`image-description-elements`. 2. By default, there are no gaps in the partition table. The way partitions are configured is done such that there are no gaps in the table of the image. However, leaving some space free at the **end** of the partition geometry is possible in the following ways: * **Deploy with unpartitioned free space.** To leave space unpartitioned on the first boot of a disk image, it is possible to configure an `` that is smaller than the disk the image gets deployed to. Details about this setting can be found in :ref:`image-description-elements`. * **Build with unpartitioned free space.** To leave space unpartitioned at build time of the image, it is possible to disable `` and configure an `` which is smaller than the kiwi-calculated disk size or the fixed setting for the disk size via the `` element. * **Build with unpartitioned free space.** Setting some unpartitioned free space on the disk can be done using the `unpartitioned` attribute of the `size` element in the type's section. For details, see :ref:`disk-the-size-element`. * **Resize built image adding unpartitioned free space.** A built image can be resized by using the `kiwi-ng image resize` command and setting a new extended size for the disk. See {kiwi} commands docs :ref:`here `. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/custom_volumes.rst0000644000000000000000000001003215273675667021067 0ustar00.. _custom_volumes: Custom Disk Volumes =================== .. sidebar:: Abstract This chapter provides high-level explanations on how to handle volume or subvolume definitions for disk images using {kiwi}. {kiwi} supports defining custom volumes by using the logical volume manager (LVM) for the Linux kernel or by setting volumes at the filesystem level when the filesystem supports it (e.g., btrfs). Volumes are defined in the {kiwi} description file, :file:`config.xml`, using `systemdisk`. This element is a child of the `type` element, which in turn is a child of `preferences`. Volumes themselves are added via (multiple) `volume` child elements of the `systemdisk` element: .. code:: xml Additional non-root volumes are created for each `volume` element. Volume details can be defined by setting the following `volume` attributes: - `name`: Required attribute representing the volume's name. Additionally, this attribute is interpreted as the mountpoint if the `mountpoint` attribute is not used. - `mountpoint`: Optional attribute that specifies the mountpoint of this volume. - `size`: Optional attribute to set the size of the volume. If no suffix (`M` or `G`) is used, then the value is considered to be in megabytes. .. note:: Special name for the root volume You can use the `@root` name to refer to the volume mounted at `/` in case some specific size attributes for the root volume have to be defined. For instance: .. code:: xml In addition to the custom size of the root volume, it's also possible to set up the name of the root volume as follows: .. code:: xml If no name for the root volume is specified, the default name, **LVRoot**, applies. - `freespace`: Optional attribute defining the additional free space added to the volume. If no suffix (`M` or `G`) is used, the value is considered to be in megabytes. - `label`: Optional attribute to set the filesystem label of the volume. - `copy_on_write`: Optional attribute to set the filesystem copy-on-write attribute for this volume. - `quota`: Optional attribute for the `btrfs` filesystem only. Allows you to specify a quota size for the generated volume. - `parent`: Optional attribute for the `btrfs` volume manager only. The name or path of the parent subvolume this volume should be nested in. If not set, the volume is created below the default volume. - `filesystem_check`: Optional attribute to indicate that this filesystem should perform the validation to become filesystem-checked. The actual constraints of whether the check is performed or not depends on systemd and filesystem-specific components. If not set or set to `false`, no system component will be triggered to run an eventual filesystem check, which results in this filesystem never being checked. The latter is the default. - `arch`: Optional attribute to create the volume only if it matches the specified host architecture. Multiple architecture names can be specified as a comma-separated list. .. warning:: The size attributes for filesystem volumes, as for btrfs, are ignored and have no effect. The `systemdisk` element additionally supports the following optional attributes: - `name`: The volume group name, by default, `systemVG` is used. This setting is only relevant for LVM volumes. - `preferlvm`: A boolean value instructing {kiwi} to prefer LVM even if the used filesystem has its own volume management system. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_ramdisk_deployment.rst0000644000000000000000000000777415273675667022732 0ustar00.. _ramdisk_deployment: Deploy and Run a System in a Ramdisk ==================================== .. sidebar:: Abstract This page provides further information for handling oem images built with {kiwi} and references the following articles: * :ref:`expandable_disk` If a machine should run the OS completely in memory without the need for any persistent storage, the approach to deploy the image into a ramdisk serves this purpose. .. note:: brd support For this functionality to work, brd (Block Ramdisk Driver) must be supported. If brd is available as a kernel module (as opposed to being compiled in the kernel), you must include the driver into the initrd. To do this, create (or edit) the `config.sh` file and add the following command to it: .. code:: bash echo 'add_drivers+=" brd "' > /etc/dracut.conf.d/10-brd.conf {kiwi} allows you to create a bootable ISO image that deploys the image into a ramdisk and activates that image with the following oem type definition: .. code:: xml true true /dev/ram1 false false 2048000 The type specification above builds an installation ISO image that deploys the System Image into the specified ramdisk device (`/dev/ram1`). The setup of the ISO image boots with a short boot timeout of 1 second and just runs through the process without asking any questions. In a ramdisk deployment, the optional target verification, swap space, and multipath targets are out of scope and therefore disabled. The configured size of the ramdisk via `oem-ramdisk-size` specifies the kB size of the OS disk, and it must be at least of the size of the System Image. The disk size can be configured dynamically with the following value in the kernelcmdline attribute: * ramdisk_size=kbyte-value An image built with the above setup can be tested in QEMU as follows: .. code:: bash $ sudo qemu -cdrom \ {exc_image_base_name}.x86_64-{exc_image_version}.install.iso \ -serial stdio .. note:: Enough Main Memory The machine, no matter if it's a virtual machine like QEMU or a real machine, must provide enough RAM to hold the image in the ramdisk as well as have enough RAM available to operate the OS and its applications. The {kiwi} build image with the extension .raw provides the System Image that gets deployed into the RAM space. Subtract the size of the System Image from the RAM space the machine offers and make sure the result is still big enough for the use case of the appliance. In the case of a virtual machine, attach enough main memory to fit this calculation. In the case of QEMU, this can be done with the `-m` option. Like all other oem {kiwi} images, the ramdisk setup also supports all the deployment methods as explained in :ref:`deployment_methods`. This means it's also possible to dump the ISO image on a USB stick, let the system boot from it, and unplug the stick from the machine because the system was deployed into RAM. .. note:: Limitations Of Ramdisk Deployments Only standard images that can be booted by a simple root mount and root switch can be used. Usually, {kiwi} calls kexec after deployment such that the correct dracut initrd created for the image will boot the image. In the case of a RAM-only system, kexec does not work because it would lose the ramdisk contents. Thus, the dracut initrd driving the deployment is also the environment to boot the system. There are cases where this environment is not suitable to boot the system. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_setup_for_azure.rst0000644000000000000000000000342415273675667022240 0ustar00.. _setup_for_azure: Image Description for Microsoft Azure ===================================== .. sidebar:: Abstract This page provides further information for handling Azure disk images built with {kiwi} and references the following articles: * :ref:`simple_disk` A virtual disk image that is able to boot in the Microsoft Azure cloud framework has to comply with the following constraints: * Hyper-V tools must be installed. * The Microsoft Azure Agent must be installed. * The disk size must be set to 30G. * Kernel parameters must allow for a serial console. To meet these requirements, update the {kiwi} image description as follows: 1. Software packages Make sure to add the following packages to the package list: .. note:: Package names used in the following list match the package names of the SUSE distribution and might be different on other distributions. .. code:: xml 2. Image Type definition Update the oem image type setup as follows: .. code:: xml 30720 false An image built with the above setup can be uploaded into the Microsoft Azure cloud and registered as an image. For further information on how to upload to Azure, see: `azurectl `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_setup_for_ec2.rst0000644000000000000000000000675515273675667021575 0ustar00.. _setup_for_ec2: Image Description for Amazon EC2 ================================ .. sidebar:: Abstract This page provides further information for handling Amazon EC2 images built with {kiwi} and references the following articles: * :ref:`simple_disk` A virtual disk image that is able to boot in the Amazon EC2 cloud framework has to comply with the following constraints: * Xen tools and libraries must be installed. * The cloud-init package must be installed. * A cloud-init configuration for Amazon must be provided. * Grub bootloader modules for Xen must be installed. * AWS tools must be installed. * The disk size must be set to 10G. * Kernel parameters must allow for a Xen console. To meet these requirements, add or update the {kiwi} image description as follows: 1. Software packages Make sure to add the following packages to the package list: .. note:: Package names used in the following list match the package names of the SUSE distribution and might be different on other distributions. .. code:: xml 2. Image Type definition Update the oem image type setup as follows: .. code:: xml 10240 false 3. Cloud-Init setup Cloud-init is a service that runs at boot time and allows you to customize the system by activating one or more cloud-init modules. For Amazon EC2, the following configuration file, :file:`/etc/cloud/cloud.cfg`, needs to be provided as part of the overlay files in your {kiwi} image description: .. code:: yaml users: - default disable_root: true preserve_hostname: false syslog_fix_perms: root:root datasource_list: [ NoCloud, Ec2, None ] cloud_init_modules: - migrator - bootcmd - write-files - growpart - resizefs - set_hostname - update_hostname - update_etc_hosts - ca-certs - rsyslog - users-groups - ssh cloud_config_modules: - mounts - ssh-import-id - locale - set-passwords - package-update-upgrade-install - timezone cloud_final_modules: - scripts-per-once - scripts-per-boot - scripts-per-instance - scripts-user - ssh-authkey-fingerprints - keys-to-console - phone-home - final-message - power-state-change system_info: default_user: name: ec2-user gecos: "cloud-init created default user" lock_passwd: True sudo: ["ALL=(ALL) NOPASSWD:ALL"] shell: /bin/bash paths: cloud_dir: /var/lib/cloud/ templates_dir: /etc/cloud/templates/ ssh_svcname: sshd An image built with the above setup can be uploaded into the Amazon EC2 cloud and registered as an image. For further information on how to upload to EC2, see: `ec2uploadimg `_. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_setup_for_google.rst0000644000000000000000000000435715273675667022374 0ustar00.. _setup_for_gce: Image Description for Google Compute Engine =========================================== .. sidebar:: Abstract This page provides further information for handling GCE images built with {kiwi} and references the following articles: * :ref:`simple_disk` A virtual disk image that is able to boot in the Google Compute Engine cloud framework has to comply with the following constraints: * {kiwi} type must be an expandable disk. * Google Compute Engine init must be installed. * Disk size must be set to 10G. * Kernel parameters must allow for a serial console. To meet these requirements, update the {kiwi} image description as follows: 1. Software packages Make sure to add the following packages to the package list: .. note:: Package names used in the following list match the package names of the SUSE distribution and might be different on other distributions. .. code:: xml 2. Image Type definition To allow the image to be expanded to the configured disk geometry of the instance started by Google Compute Engine, it is suggested to let {kiwi}'s OEM boot code take over that task. It would also be possible to try cloud-init's resize module, but we found conflicts when two cloud-init systems, `google-compute-engine-init` and `cloud-init`, were used together. Thus, for now, we stick with {kiwi}'s boot code, which can resize the disk from within the initrd before the system is activated through systemd. Update the oem image type setup to be changed into an expandable type as follows: .. code:: xml 10240 true false An image built with the above setup can be uploaded into the Google Compute Engine cloud and registered as an image. For further information on how to upload to Google, see: `google-cloud-sdk` on software.opensuse.org. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_setup_for_luks.rst0000644000000000000000000000433115273675667022066 0ustar00.. _setup_for_luks: Image Description for an Encrypted Disk ======================================= .. sidebar:: Abstract This page provides further information for handling disk images with an encrypted root filesystem. The information here is based on the following article: * :ref:`simple_disk` A virtual disk image can be partially or fully encrypted using the LUKS extension supported by {kiwi}. A fully encrypted image also includes the data in `/boot` to be encrypted. Such an image requests the passphrase for the master key to be entered at the bootloader stage. A partially encrypted image keeps `/boot` unencrypted and on an extra boot partition. Such an image requests the passphrase for the master key later in the boot process when the root partition is accessed by the systemd mount service. In any case, the master passphrase is requested only once. Update the {kiwi} image description as follows: 1. Software packages Make sure to add the following package to the package list: .. note:: Package names used in the following list match the package names of the SUSE distribution and might be different on other distributions. .. code:: xml 2. Image Type definition Update the oem image type setup as follows: Full disk encryption, including `/boot`: .. code:: xml false Encrypted root partition with an unencrypted extra `/boot` partition: .. code:: xml false .. note:: The value for the `luks` attribute sets the master passphrase for the LUKS keyring. Therefore, the XML description becomes security-critical and should only be readable by trustworthy people. Alternatively, the credentials information can be stored in a key file and referenced as: .. code:: xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/disk_setup_for_vagrant.rst0000644000000000000000000002105215273675667022551 0ustar00.. _setup_vagrant: Image Description for Vagrant ============================= .. sidebar:: Abstract This page provides further information for handling Vagrant-controlled disk images built with {kiwi} and references the following article: * :ref:`simple_disk` `Vagrant `_ is a framework to implement consistent processing/testing work environments based on Virtualization technologies. To run a system, Vagrant needs so-called **boxes**. A box is a TAR archive containing a virtual disk image and some metadata. To build Vagrant boxes, you can use `Packer `_, which is provided by Hashicorp itself. Packer is based on the official installation media (DVDs) as shipped by the distribution vendor. The {kiwi} way of building images might be helpful if such a media does not exist or does not suit your needs. For example, if the distribution is still under development or you want to use a collection of your own repositories. Note that in contrast to Packer, {kiwi} only supports the libvirt and VirtualBox providers. Other providers require a different box layout that is currently not supported by {kiwi}. In addition, you can use the {kiwi} image description as a source for the `Open Build Service `_, which allows building and maintaining boxes. Vagrant expects boxes to be set up in a specific way (for details refer to the `Vagrant box documentation `_). Applied to the referenced {kiwi} image description from :ref:`simple_disk`, the following steps are required: 1. Update the image type setup. .. code:: xml 42 false This modifies the type to build a Vagrant box for the libvirt provider, including a pre-defined disk size. The disk size is optional but recommended to provide some free space on disk. For the VirtualBox provider, the additional attribute `virtualbox_guest_additions_present` can be set to `true` when the VirtualBox guest additions are installed in the {kiwi} image: .. code:: xml 42 false The resulting Vagrant box then uses the `vboxsf` module for the synchronized folder instead of `rsync`, which is used by default. 2. Add mandatory packages. .. code:: xml 3. Add additional packages. If you have set the attribute `virtualbox_guest_additions_present` to `true`, add the VirtualBox guest additions. For openSUSE, the following packages are required: .. code:: xml Otherwise, you must add `rsync`: .. code:: xml Note that {kiwi} cannot verify whether these packages are installed. If they are missing, the resulting Vagrant box will be broken. 4. Add Vagrant user. .. code:: xml This adds the **vagrant** user to the system and applies the name of the user as the password for login. 5. Configure SSH, the default shared folder, and sudo permissions. Vagrant expects that it can log in as the user `vagrant` using an insecure public key [#f1]_. Furthermore, Vagrant also usually uses :file:`/vagrant` as the default shared folder and assumes that the `vagrant` user can invoke commands via `sudo` without having to enter a password. This can be achieved using the function `baseVagrantSetup` in :file:`config.sh`: .. code:: bash baseVagrantSetup 6. Additional customizations: In addition to `baseVagrantSetup`, you might want to also ensure the following: - If you have installed the VirtualBox guest additions into your box, then also load the `vboxsf` kernel module. - When building boxes for libvirt, ensure that the default wired networking interface is called `eth0` and uses DHCP. This is necessary since libvirt uses `dnsmasq` to issue IPs to the VMs. This step can be omitted for VirtualBox boxes. An image built with the above setup creates a Vagrant box file with the extension :file:`.vagrant.libvirt.box` or :file:`.vagrant.virtualbox.box`. Add the box file to Vagrant with the command: .. code:: bash vagrant box add my-box image-file.vagrant.libvirt.box .. note:: Using the box with the libvirt provider requires, alongside a correct Vagrant installation: - the plugin `vagrant-libvirt` to be installed - a running libvirtd daemon Once added to Vagrant, boot the box and log in with the following sequence of `vagrant` commands: .. code:: bash vagrant init my-box vagrant up --provider libvirt vagrant ssh Customizing the embedded Vagrantfile ------------------------------------ .. warning:: This is an advanced topic and not required for most users. Vagrant ships with an embedded :file:`Vagrantfile` that carries settings specific to this box, for instance, the synchronization mechanism for the shared folder. {kiwi} generates such a file automatically for you, and it should be sufficient for most use cases. If a box requires different settings in the embedded :file:`Vagrantfile`, then the user can provide {kiwi} with a path to an alternative via the attribute `embedded_vagrantfile` of the `vagrantconfig` element: it specifies a relative path to the :file:`Vagrantfile` that will be included in the finished box. In the following example snippet from :file:`config.xml`, we add a custom :file:`MyVagrantfile` into the box (the file should be in the image description directory next to :file:`config.sh`): .. code:: xml 42 false The option to provide a custom :file:`Vagrantfile` can be combined with the usage of *profiles* (see :ref:`image-profiles`), so that certain builds can use the automatically generated :file:`Vagrantfile` (in the following example, that is the VirtualBox build) and others get a customized one (the libvirt profile in the following example): .. code:: xml 42 false 42 false .. [#f1] The insecure key is removed from the box when it is first booted via Vagrant. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/integration_testing.rst0000644000000000000000000000507515273675667022076 0ustar00Building Build Tests ==================== {kiwi} provides a collection of integration test images for different architectures and distributions. The test descriptions cover a number of appliance features that can be built with {kiwi}. The test descriptions are stored in a directory structure of the following layout: `build-tests/ARCH/DISTRIBUTION/test-image-NAME`. To build integration test image(s), the script `build-tests.sh` exists. The implementation of `build-tests.sh` calls kiwi's `boxbuild` command, by default in container mode, which allows calling the script on any host system that allows you to run container instances via `podman`. The `--vm` option of `build-tests.sh` runs the build in a virtual machine instead. .. warning:: **Architectures** Cross-architecture image building is possible and also supported via boxbuild, but the performance impact is big, even in containers using qemu-binfmt, and even bigger in full qemu arch emulation. This would slow down building the integration tests a lot, and therefore, `build-tests.sh` requires the host architecture to match the image target architecture. Prior to calling `build-tests.sh`, the following requirements must be met: ``Tools`` Install the packages providing the following tools: * tree * git * xmllint * podman * pip ``Source Checkout`` Check out the kiwi git repo, which provides the test descriptions as well as the `build-tests.sh` script. .. code:: bash $ git clone https://github.com/OSInside/kiwi.git ``kiwi`` Install {kiwi}. It provides the boxbuild command used by `build-tests.sh`. If there are no {kiwi} packages for your host, install {kiwi} from pip: .. code:: bash $ pip install --upgrade kiwi .. warning:: Make sure to be able to execute `kiwi-ng`. When installed from pip as a user, `kiwi-ng` is placed in `~/.local/bin`. Update your path via `export PATH=~/.local/bin:$PATH` if needed. Building a specific integration test can be done as follows: .. code:: bash $ cd kiwi $ ./build-tests.sh \ --test-dir build-tests/x86/tumbleweed/ \ --test-name test-image-disk Building all integration tests for a specific arch and distribution can be done as follows: .. code:: bash $ cd kiwi $ ./build-tests.sh \ --test-dir build-tests/x86/tumbleweed/ .. note:: Building all integration tests can take some time and depends on the number of tests provided as well as on the build power of the host system. In general, the tests can also run in parallel or be distributed to multiple hosts. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/iso_to_usb_stick_deployment.rst0000644000000000000000000000271315273675667023614 0ustar00.. _iso_to_usb_stick: Deploy ISO Image on a USB Stick ================================ .. sidebar:: Abstract This page provides further information for handling ISO images built with {kiwi} and references the following articles: * :ref:`hybrid_iso` In {kiwi}, all generated ISO images are created to be hybrid. This means, the image can be used as a CD/DVD or as a disk. This works because the ISO image also has a partition table embedded. With more and more computers delivered without a CD/DVD drive, this becomes important. The very same ISO image can be copied onto a USB stick and used as a bootable disk. The following procedure shows how to do this: 1. Plug in a USB stick. Once plugged in, check which Unix device name the stick was assigned to. The following command provides an overview of all Linux storage devices: .. code:: bash $ lsblk 2. Dump the ISO image on the USB stick: .. warning:: Make sure the selected device really points to your stick because the following operation cannot be revoked and will destroy all data on the selected device. .. code:: bash $ dd if={exc_image_base_name_live}.x86_64-{exc_image_version}.iso of=/dev/ 3. Boot from your USB Stick. Activate booting from USB in your BIOS/UEFI. As many firmwares have different procedures on how to do it, look into your user manual. Many firmwares offer a boot menu that can be activated at boot time. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/iso_to_usb_stick_file_based_deployment.rst0000644000000000000000000000520515273675667025750 0ustar00.. _iso_as_file_to_usb_stick: Deploy ISO Image as a File on a FAT32 Formatted USB Stick ========================================================= .. sidebar:: Abstract This page provides further information for handling ISO images built with {kiwi} and references the following articles: * :ref:`hybrid_iso` In {kiwi}, all generated ISO images are created to be hybrid. This means, the image can be used as a CD/DVD or as a disk. The deployment of such an image onto a disk like a USB stick normally destroys all existing data on this device. Most USB sticks are pre-formatted with a FAT32 Windows filesystem, and to keep the existing data on the stick untouched, a different deployment needs to be used. The following deployment process copies the ISO image as an additional file to the USB stick and makes the USB stick bootable. The ability to boot from the stick is configured through a Grub2 feature that allows you to loopback mount an ISO file and boot the kernel and initrd directly from the ISO file. The initrd loaded in this process must also be able to loopback mount the ISO file to access the root filesystem and boot the live system. The dracut initrd system used by {kiwi} provides this feature upstream, called "iso-scan". Therefore, all {kiwi}-generated live ISO images support this deployment mode. For copying the ISO file to the USB stick and the setup of the Grub2 bootloader to make use of the "iso-scan" feature, an extra tool named `live-grub-stick` exists. The following procedure shows how to set up the USB stick with `live-grub-stick`: 1. Install the `live-grub-stick` package from software.opensuse.org. 2. Plug in a USB stick. Once plugged in, check which Unix device name the FAT32 partition was assigned to. The following command provides an overview of all storage devices and their filesystems: .. code:: bash $ sudo lsblk --fs 3. Call the `live-grub-stick` command as follows: Assuming "/dev/sdz1" was the FAT32 partition selected from the output of the `lsblk` command: .. code:: bash $ sudo live-grub-stick {exc_image_base_name_live}.x86_64-{exc_image_version}.iso /dev/sdz1 4. Boot from your USB Stick. Activate booting from USB in your BIOS/UEFI. As many firmwares have different procedures on how to do it, look into your user manual. EFI booting from an ISO image is not supported at the moment. For EFI booting, use the --isohybrid option with live-grub-stick; however, note that all the data on the stick will be lost. Many firmwares offer a boot menu that can be activated at boot time. Usually, this can be reached by pressing the :kbd:`Esc` or :kbd:`F12` keys. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/iso_to_usb_stick_grub2_boot_from_iso.rst0000644000000000000000000001337215273675667025400 0ustar00.. _grub_boot_from_iso: Booting Live ISO Images from Grub2 ==================================== .. sidebar:: Abstract This page provides further information for handling ISO images built with {kiwi} and references the following articles: * :ref:`hybrid_iso` In {kiwi}, all generated ISO images are created to be hybrid. This means, the image can be used as a CD/DVD or as a disk. This works because the ISO image also has a partition table embedded. With more and more computers delivered without a CD/DVD drive, this becomes important. Writing this image to a USB stick will permanently erase all existing data on the device. Additionally, the stick will no longer be usable for general data storage. Most USB sticks are pre-formatted with a FAT32 or exFAT Windows filesystem, and to keep the existing data untouched, a different deployment needs to be used. Fortunately, Grub2 supports booting directly from ISO files. It does not matter whether it is installed on your computer's hard drive or on a USB stick. The following deployment process copies the ISO image as an additional file to the USB stick or hard drive. The ability to boot from the disk is configured through a Grub2 feature that allows you to loopback mount an ISO file and boot the kernel and initrd directly from the ISO file. The initrd loaded in this process must also be able to loopback mount the ISO file to access the root filesystem and boot the live system. FAT32 is widely supported on USB sticks. For hard drives, any filesystem supported by grub is also appropriate. The dracut initrd system used by {kiwi} provides this feature upstream, called "iso-scan/filename". Therefore, all {kiwi}-generated live ISO images support this deployment mode. The following procedure expects an existing Grub2 installation on your hard drive or USB stick. 1. Copy the ISO image to a folder of your choice on your hard drive. For example, create a folder called "iso" in the root of your hard drive and copy the ISO image there: .. code:: bash sudo mkdir /iso sudo cp some-kiwi-live.iso /iso/ 2. Look up the root filesystem UUID of your current operating system. Get this information by running the command: .. code:: bash findmnt -n -o UUID / 3. Add the following submenu setup to the :file:`grub.cfg` file: .. note:: Verify that the path to the ISO image is correct. Set your own menu name. The drive identification for Grub2 can be checked at boot time by pressing the 'c' key and typing 'ls'. The following example assumes a live ISO image for the x86_64 architecture. Adjust the paths to kernel and initrd if you have a different architecture. .. code:: bash menuentry "Boot from Live ISO" { load_video set gfxpayload=keep insmod gzio insmod part_gpt insmod iso9660 insmod btrfs insmod ext2 insmod xfs insmod lvm insmod loopback search --no-floppy --fs-uuid --set=root UUID_FROM_STEP_2 set isofile="/iso/some-kiwi-live.iso" set kernel="(loop)/boot/x86_64/loader/linux" set initrd="(loop)/boot/x86_64/loader/initrd" set options="rd.live.image root=live:CDLABEL=CDROM" loopback loop $isofile linux $kernel iso-scan/filename=$isofile $options initrd $initrd } 4. Restart your computer and select the added menu entry. .. note:: For USB sticks, the procedure is identical. You would install Grub2 on the USB drive and follow the steps above. However, it is uncommon to have Grub2 already installed on a USB stick. To simplify the setup, there are good helper tools such as `MultiOS-USB`. Booting Install ISO Images from Grub2 ------------------------------------- The install ISO image created for an OEM disk image via the `installiso="true"` attribute supports the "iso-scan/filename" feature as well. The procedure is the same as for live ISO images, only the boot options differ. The following example assumes the default install ISO volume ID `INSTALL`: .. code:: bash menuentry "Install from ISO" { load_video set gfxpayload=keep insmod gzio insmod part_gpt insmod iso9660 insmod btrfs insmod ext2 insmod xfs insmod lvm insmod loopback search --no-floppy --fs-uuid --set=root UUID_FROM_STEP_2 set isofile="/iso/some-kiwi.install.iso" set kernel="(loop)/boot/x86_64/loader/linux" set initrd="(loop)/boot/x86_64/loader/initrd" set options="cdinst=1 root=install:CDLABEL=INSTALL" loopback loop $isofile linux $kernel iso-scan/filename=$isofile $options initrd $initrd } Alternatively, the grub configuration of the ISO image itself can be used through the `loopback.cfg` file provided on all {kiwi} ISO images. The ISO path must be passed via the `iso_path` variable. Depending on the distribution, the grub directory on the ISO image is named `grub2` or `grub`: .. code:: bash menuentry "Install from ISO" { insmod loopback search --no-floppy --fs-uuid --set=root UUID_FROM_STEP_2 set iso_path="/iso/some-kiwi.install.iso" export iso_path loopback loop $iso_path configfile (loop)/boot/grub2/loopback.cfg } .. note:: ISO File on the Install Target Disk If the disk selected as installation target holds the ISO file, the installation would overwrite the ISO file while reading from it. In this case {kiwi} copies the ISO file into main memory and releases the disk before the image is dumped. Make sure the system has enough main memory to store the ISO file plus an additional 512 MB. If there is not enough memory available, the installation stops before the target disk is modified. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/legacy_netboot_root_filesystem.rst0000644000000000000000000003236415273675667024324 0ustar00.. _build_legacy_pxe: Build PXE Root Filesystem Image for the legacy netboot infrastructure ====================================================================== .. _PXE: https://en.wikipedia.org/wiki/Preboot_Execution_Environment .. _TFTP: https://en.wikipedia.org/wiki/Trivial_File_Transfer_Protocol .. _NBD: https://en.wikipedia.org/wiki/Network_block_device .. _AoE: https://en.wikipedia.org/wiki/ATA_over_Ethernet .. sidebar:: Abstract This page explains how to build a filesystem image for use with {kiwi}'s PXE boot infrastructure. It contains: * how to build a PXE filesystem image * how to set up the PXE filesystem image on the PXE server * how to run it with QEMU `PXE`_ is a network boot protocol that is shipped with most BIOS implementations. The protocol sends a DHCP request to get an IP address. When an IP address is assigned, it uses the `TFTP`_ protocol to download a Kernel and boot instructions. Contrary to other images built with {kiwi}, a PXE image consists of separate boot, kernel, and root filesystem images, since these images need to be made available in different locations on the PXE boot server. A root filesystem image that can be deployed via {kiwi}'s PXE netboot infrastructure represents the system's rootfs in a Linux filesystem. A user could loop mount the image and access the contents of the root filesystem. The image does not contain any information about the system disk, its partitions, or the bootloader setup. All of this information is provided by a client configuration file on the PXE server, which controls how the root filesystem image should be deployed. Many different deployment strategies are possible, e.g., root over `NBD`_ (network block device), `AoE`_ (ATA over Ethernet), or NFS for diskless and diskful clients. This particular example shows how to build an overlayfs-based union system based on openSUSE Leap for a diskless client that receives the squashfs- compressed root filesystem image in a ramdisk overlayed via overlayfs and writes new data into another ramdisk on the same system. As a diskless client, a QEMU virtual machine is used. .. compound:: **Things to know before** * To use the image, all image parts need to be copied to the PXE boot server. If you have not set up such a server, refer to :ref:`network-boot-server` for instructions. * The following example assumes you will create the PXE image on the PXE boot server itself (if not, use `scp` to copy the files to the remote host). * To let QEMU connect to the network, we recommend setting up a network bridge on the host system and letting QEMU connect to it via a custom :file:`/etc/qemu-ifup`. For details, see https://en.wikibooks.org/wiki/QEMU/Networking * The PXE root filesystem image approach is considered a legacy setup. The required netboot initrd code will be maintained outside of the {kiwi} appliance builder code base. If possible, we recommend switching to the OEM disk image deployment via PXE. 1. Build the image with {kiwi}: .. code:: bash $ sudo kiwi-ng --profile Flat system build \ --description kiwi/build-tests/{exc_description_pxe} \ --set-repo {exc_repo_tumbleweed} \ --target-dir /tmp/mypxe-result 2. Change into the build directory: .. code:: bash $ cd /tmp/mypxe-result 3. Copy the initrd and the kernel to :file:`/srv/tftpboot/boot`: .. code:: bash $ cp *.initrd /srv/tftpboot/boot/initrd $ cp *.kernel /srv/tftpboot/boot/linux 4. Copy the system image and its SHA256 sum to :file:`/srv/tftpboot/image`: .. code:: bash $ cp {exc_image_base_name_pxe}.x86_64-{exc_image_version_pxe} /srv/tftpboot/image $ cp {exc_image_base_name_pxe}.x86_64-{exc_image_version_pxe}.sha256 /srv/tftpboot/image 5. Adjust the PXE configuration file. The configuration file controls which kernel and initrd is loaded and which kernel parameters are set. A template has been installed at :file:`/srv/tftpboot/pxelinux.cfg/default` from the ``kiwi-pxeboot`` package. The minimal configuration required to boot the example image looks like the following: .. code:: bash DEFAULT KIWI-Boot LABEL KIWI-Boot kernel boot/linux append initrd=boot/initrd IPAPPEND 2 6. Create the image client configuration file: .. code:: bash $ vi /srv/tftpboot/KIWI/config.default IMAGE=/dev/ram1;{exc_image_base_name_pxe}.x86_64;{exc_image_version_pxe};192.168.100.2;4096 UNIONFS_CONFIG=/dev/ram2,/dev/ram1,overlay All PXE boot-based deployment methods are controlled by a client configuration file. The above configuration tells the client where to find the image and how to activate it. In this case, the image will be deployed into a ramdisk (`ram1`) and overlay mounted such that all write operations will land in another ramdisk (`ram2`). {kiwi} supports a variety of different deployment strategies based on the rootfs image created beforehand. For details, refer to :ref:`pxe_legacy_client_config`. 7. Connect the client to the network and boot. This can also be done in a virtualized environment using QEMU as follows: .. code:: bash $ sudo qemu -boot n -m 4096 .. _pxe_legacy_client_config: PXE Client Setup Configuration ------------------------------ All PXE boot-based deployment methods are controlled by configuration files located in :file:`/srv/tftpboot/KIWI` on the PXE server. Such a configuration file can either be client-specific (config.MAC_ADDRESS, for example config.00.AB.F3.11.73.C8), or generic (config.default). In an environment with heterogeneous clients, this allows for a default configuration suitable for the majority of clients, configurations suitable for a group of clients (for example, machines with similar or identical hardware), and individual configurations for selected machines. The configuration file contains data about the image and about configuration, synchronization, and partition parameters. The configuration file has the following general format: .. code:: bash IMAGE="device;name;version;srvip;bsize;compressed,...," DISK="device" PART="size;id;Mount,...,size;id;Mount" RAID="raid-level;device1;device2;..." AOEROOT=ro-device[,rw-device] NBDROOT="ip-address;export-name;device;swap-export-name;swap-device;write-export-name;write-device" NFSROOT="ip-address;path" UNIONFS_CONFIG="rw-partition,compressed-partition,overlay" CONF="src;dest;srvip;bsize;[hash],...,src;dest;srvip;bsize;[hash]" KIWI_BOOT_TIMEOUT="seconds" KIWI_KERNEL_OPTIONS="opt1 opt2 ..." REBOOT_IMAGE=1 RELOAD_CONFIG=1 RELOAD_IMAGE=1 .. note:: Quoting the Values The configuration file is sourced by Bash, so the same quoting rules as for Bash apply. Not all configuration options need to be specified. It depends on the setup of the client which configuration values are required. The following is a collection of client setup examples that cover all supported PXE client configurations. Setup Client with Remote Root ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To serve the image from a remote location and redirect all write operations to a tmpfs, the following setup is required: .. code:: bash # When using AoE, see vblade toolchain for image export AOEROOT=/dev/etherd/e0.1 UNIONFS_CONFIG=tmpfs,aoe,overlay # When using NFS, see exports manual page for image export NFSROOT="192.168.100.2;/srv/tftpboot/image/root" UNIONFS_CONFIG=tmpfs,nfs,overlay # When using NBD, see nbd-server manual page for image export NBDROOT=192.168.100.2;root_export;/dev/nbd0 UNIONFS_CONFIG=tmpfs,nbd,overlay The above setup shows the most common use case where the image built with {kiwi} is populated over the network using either AoE, NBD, or NFS in combination with overlayfs, which redirects all write operations to be local to the client. In any case, a setup of either AoE, NBD, or NFS on the image server is required beforehand. Setup Client with System on Local Disk ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To deploy the image on a local disk, the following setup is required: .. note:: In the referenced `{exc_description_pxe}` XML description, the `pxe` type must be changed as follows, and the image needs to be rebuilt: .. code:: xml .. code:: bash IMAGE="/dev/sda2;{exc_image_base_name_pxe}.x86_64;{exc_image_version_pxe};192.168.100.2;4096" DISK="/dev/sda" PART="5;S;X,X;L;/" The setup above will create a partition table on `sda` with a 5MB swap partition (no mountpoint), and the rest of the disk will be a Linux(L) partition with `/` as its mountpoint. The (`X`) in the `PART` setup specifies a placeholder to indicate the default behavior. Setup Client with System on Local MD RAID Disk ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To deploy the image on a local disk with a prior software RAID configuration, the following setup is required: .. note:: In the referenced `{exc_description_pxe}` XML description, the `pxe` type must be changed as follows, and the image needs to be rebuilt: .. code:: xml .. code:: bash RAID="1;/dev/sda;/dev/sdb" IMAGE="/dev/md1;{exc_image_base_name_pxe}.x86_64;{exc_image_version_pxe};192.168.100.2;4096" PART="5;S;x,x;L;/" The first parameter of the RAID line is the RAID level. So far, only raid1 (mirroring) is supported. The second and third parameters specify the raid disk devices that make up the array. If a `RAID` line is present, all partitions in `PART` will be created as RAID partitions. The first RAID is named `md0`, the second one `md1`, and so on. It is required to specify the correct RAID partition in the `IMAGE` line according to the `PART` setup. In this case, `md0` is reserved for the SWAP space, and `md1` is reserved for the system. Setup Loading of Custom Configuration File(s) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To load, for example, a custom :file:`/etc/hosts` file on the client, the following setup is required: .. code:: bash CONF="hosts;/etc/hosts;192.168.1.2;4096;ffffffff" On boot of the client, {kiwi}'s boot code will fetch the :file:`hosts` file from the root of the server (192.168.1.2) with 4k blocksize and deploy it as :file:`/etc/hosts` on the client. The protocol is tftp by default but can be changed via the `kiwiservertype` kernel command-line option. For details, see :ref:`custom-download-server`. Setup Client to Force Reload Image ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To force the reload of the system image even if the image on the disk is up-to-date, the following setup is required: .. code:: bash RELOAD_IMAGE=1 The option only applies to configurations with a `DISK`/`PART` setup. Setup Client to Force Reload Configuration Files ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To force the reload of all configuration files specified in CONF, the following setup is required: .. code:: bash RELOAD_CONFIG=1 By default, only configuration files that have changed according to their checksum value will be reloaded. With the above setup, all files will be reloaded from the PXE server. The option only applies to configurations with a `DISK`/`PART` setup. Setup Client for Reboot After Deployment ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To reboot the system after the initial deployment process is done, the following setup is required: .. code:: bash REBOOT_IMAGE=1 Setup custom kernel boot options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To deactivate the kernel mode setting on the local boot of the client, the following setup is required: .. code:: bash KIWI_KERNEL_OPTIONS="nomodeset" .. note:: This does not influence the kernel options passed to the client if it boots from the network. To set up those, the PXE configuration on the PXE server needs to be changed. Setup a Custom Boot Timeout ~~~~~~~~~~~~~~~~~~~~~~~~~~~ To set up a 10-second custom timeout for the local boot of the client, the following setup is required. .. code:: bash KIWI_BOOT_TIMEOUT="10" .. note:: This does not influence the boot timeout if the client boots from the network. .. _custom-download-server: Setup a Different Download Protocol and Server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ By default, all downloads controlled by the {kiwi} linuxrc code are performed by an atftp call using the TFTP protocol. With PXE, the download protocol is fixed, and thus you cannot change the way the kernel and the boot image (:file:`initrd`) are downloaded. As soon as Linux takes over, the download protocols http, https, and ftp are also supported. {kiwi} uses the `curl` program to support the additional protocols. To select one of the additional download protocols, the following kernel parameters need to be specified: .. code:: bash kiwiserver=192.168.1.1 kiwiservertype=ftp To set up these parameters, edit the file :file:`/srv/tftpboot/pxelinux.cfg/default` on your PXE boot server and change the append line accordingly. .. note:: Once configured, all downloads except for kernel and initrd are now controlled by the given server and protocol. You need to make sure that this server provides the same directory and file structure as initially provided by the `kiwi-pxeboot` package. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/network_live_iso_boot.rst0000644000000000000000000001261415273675667022420 0ustar00.. _network_live_boot: Booting a Live ISO Image from Network ===================================== .. sidebar:: Abstract This page provides further information for handling ISO images built with {kiwi} and references the following articles: * :ref:`hybrid_iso` In {kiwi}, live ISO images can be configured to boot via the PXE boot protocol. This functionality requires a network boot server setup on the system. Details on how to set up such a server can be found in :ref:`network-boot-server`. After the live ISO is built as shown in :ref:`hybrid_iso`, the following configuration steps are required to boot from the network: 1. Extract initrd/kernel from Live ISO The PXE boot process loads the configured kernel and initrd from the PXE server. For this reason, these two files must be extracted from the live ISO image and copied to the PXE server as follows: .. code:: bash $ mount {exc_image_base_name_live}.x86_64-{exc_image_version}.iso /mnt $ cp /mnt/boot/x86_64/loader/initrd /srv/tftpboot/boot/initrd $ cp /mnt/boot/x86_64/loader/linux /srv/tftpboot/boot/linux $ umount /mnt .. note:: This step must be repeated with any new build of the live ISO image. 2. Export Live ISO to the Network Access to the live ISO file must be provided by either `ftp`, `http`, `https`, or `dolly`. The simplest method is to set up an FTP server, e.g., `vsftpd`, and copy the live ISO file to the data directory: .. code:: bash $ mkdir -p /srv/ftp/image $ cp {exc_image_base_name_live}.x86_64-{exc_image_version}.iso /srv/ftp/image .. note:: Check if the image can be downloaded via: `wget ftp://IP/image/{exc_image_base_name_live}.x86_64-{exc_image_version}.iso` before the next step. 3. Set up live ISO boot entry in PXE configuration .. note:: The following step assumes that the `pxelinux.0` loader has been configured on the boot server to boot up network clients. Edit the file :file:`/srv/tftpboot/pxelinux.cfg/default` and create a boot entry of the form: .. code:: bash LABEL Live-Boot kernel boot/linux append initrd=boot/initrd ip=dhcp root=live:ftp://IP/image/{exc_image_base_name_live}.x86_64-{exc_image_version}.iso * The `ip=` parameter controls how the dracut network module sets up the network. Several options exist to control how the network interface should be set up. Please consult the dracut manual for further information. * The boot parameter `root=live:PROTOCOL://IP/PATH...` specifies the remote endpoint and protocol to find the live ISO file. So far, `ftp`, `http`, `https`, and `dolly` are supported. 4. Boot from the Network Within the network that has access to the PXE server and the IP in the `root=` option, any network client can now boot the live system. A test based on QEMU can be done as follows: .. code:: bash $ qemu -boot n Available Remote Boot Options ----------------------------- The following kernel boot options are available to control the behavior of the remote boot process. rd.kiwi.live.curl_options= Options passed along to the curl call rd.kiwi.live.dolly_options= Options passed along to the dolly call rd.kiwi.live.system= Block device to use for the system. By default, this is set to `/dev/ram0`. ramdisk_size=kbytes Size of the ramdisk in kilobytes; the value is passed to the kernel `brd` module as `rd_size`. By default, this is set to 2097152, which is 2 GiB. rd.kiwi.live.reset Force reset of the live system. This option only makes sense if the live system device (rd.kiwi.live.system) points to a persistent storage device. In this case, {kiwi} loads the system only once and does not overwrite it unless a reset is requested. Persistent Live System ---------------------- The remote boot process of a live ISO image places the ISO file into a ramdisk by default. This means all data lives in memory and is not persistent. To boot the live system from a remote location and keep it on persistent storage, it's required to pass the `rd.kiwi.live.system` boot option with the device name pointing to that persistent storage. .. warning:: All data on the device given via `rd.kiwi.live.system` will be wiped. A test based on QEMU can be done as follows: Edit the file :file:`/srv/tftpboot/pxelinux.cfg/default` and create a boot entry of the form: .. code:: bash LABEL Live-Boot kernel boot/linux append initrd=boot/initrd ip=dhcp root=live:ftp://IP/image/{exc_image_base_name_live}.x86_64-{exc_image_version}.iso ramdisk_size=3545728 rd.kiwi.live.system=/dev/sda rd.live.overlay.persistent rd.live.overlay.cowfs=xfs * The `rd.live.overlay.persistent` and `rd.live.overlay.cowfs=xfs` options are standard {kiwi} live ISO options to control if and how a persistent write partition should be created. The options only take effect when booting into a persistent storage device. Next, create a persistent storage disk of 3G and attach it to the QEMU instance. .. code:: bash $ qemu-img create mydisk.raw 3G $ qemu -boot n -hda mydisk.raw The live system will be deployed once to the locally attached disk and will boot from it. Any subsequent boot process will not modify the local disk unless `rd.kiwi.live.reset` is passed on the kernel command line. If deployed, there is no need for the network anymore, and the system could also boot standalone. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/network_overlay_boot.rst0000644000000000000000000001206115273675667022264 0ustar00.. _network_overlay_boot: Booting a Root Filesystem from Network ====================================== .. sidebar:: Abstract This page provides further information for handling KIS images built with {kiwi} and references the following article: * :ref:`kis` In {kiwi}, the `kiwi-overlay` dracut module can be used to boot from a remote exported root filesystem. The exported device is visible as a block device on the network client. {kiwi} supports the two export backends, `NBD` (Network Block Device) and `AoE` (ATA over Ethernet), for this purpose. A system that is booted in this mode will read the contents of the root filesystem from a remote location and target any write action into RAM by default. The kernel cmdline option `rd.root.overlay.write` can be used to specify an alternative device to use for writing. The two layers (read and write) are combined using the `overlayfs` filesystem. For remote boot of a network client, the PXE boot protocol is used. This functionality requires a network boot server setup on the system. Details on how to set up such a server can be found in :ref:`network-boot-server`. Before the KIS image can be built, the following configuration step is required: * Create dracut configuration to include the `kiwi-overlay` module .. code:: bash $ cd kiwi/build-tests/{exc_description_kis} $ mkdir -p root/etc/dracut.conf.d $ cd root/etc/dracut.conf.d $ echo 'add_dracutmodules+=" kiwi-overlay "' > overlay.conf Now, the KIS image can be built as shown in :ref:`kis`. After the build, the following configuration steps are required to boot from the network: 1. Copy the initrd/kernel from the KIS build to the PXE server The PXE boot process loads the configured kernel and initrd from the PXE server. For this reason, the following files must be copied to the PXE server as follows: .. code:: bash $ cp *.initrd /srv/tftpboot/boot/initrd $ cp *.kernel /srv/tftpboot/boot/linux 2. Export Root FileSystem to the Network Access to the root filesystem is implemented using either the AoE or the NBD protocol. This requires the export of the root filesystem image as a remote block device: Export via AoE: Install the `vblade` package on the system that is expected to export the root filesystem. .. note:: Not all versions of AoE are compatible with any kernel. This means the kernel on the system exporting the root filesystem image must provide a compatible aoe kernel module compared to the kernel used inside of the root filesystem image. Once done, export the filesystem from the KIS build above as follows: .. code:: bash $ vbladed 0 1 IFACE {exc_image_base_name_kis}.x86_64-{exc_image_version_kis} The above command exports the given filesystem image file as a block storage device to the network of the given `IFACE`. On any machine except the one exporting the file, it will appear as :file:`/dev/etherd/e0.1` once the `aoe` kernel module has been loaded. The two numbers, 0 and 1 in the above example, classify a major and minor number that is used in the device node name on the reading side, in this case, :file:`e0.1`. .. note:: Only machines in the same network of the given `IFACE` can see the exported block device. Export via NBD: Install the `nbd` package on the system that is expected to export the root filesystem. Once done, export the filesystem from the KIS build above as follows: .. code:: bash $ losetup /dev/loop0 {exc_image_base_name_kis}.x86_64-{exc_image_version_kis} $ vi /etc/nbd-server/config [generic] user = root group = root [export] exportname = /dev/loop0 $ nbd-server 3. Setup boot entry in the PXE configuration .. note:: The following step assumes that the `pxelinux.0` loader has been configured on the boot server to boot up network clients. Edit the file :file:`/srv/tftpboot/pxelinux.cfg/default` and create a boot entry of the form: Using NBD: .. code:: bash LABEL Overlay-Boot kernel boot/linux append initrd=boot/initrd root=overlay:nbd=server-ip:export The boot parameter `root=overlay:nbd=server-ip:export` specifies the NBD server's IP address and the name of the export as used in :file:`/etc/nbd-server/config`. Using AoE: .. code:: bash LABEL Overlay-Boot kernel boot/linux append initrd=boot/initrd root=overlay:aoe=AOEDEVICE The boot parameter `root=overlay:aoe=AOEDEVICE` specifies the AoE device name below :file:`/dev/etherd` as it was exported by the `vbladed` command, for example, `e0.1`. 4. Boot from the Network Within the network that has access to the PXE server and the exported root filesystem image, any network client can now boot the system. A test based on QEMU can be done as follows: .. code:: bash $ sudo qemu -boot n ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/setup_network_bootserver.rst0000644000000000000000000000771315273675667023202 0ustar00.. _network-boot-server: Setting Up a Network Boot Server ================================ .. sidebar:: Abstract This page provides general information on how to set up a network boot server that provides all services needed for the PXE boot protocol. To be able to deploy a system through the PXE boot protocol, you need to set up a network boot server providing the services DHCP and TFTP. With `dnsmasq`, a utility exists that allows you to set up all needed services at once: Installing and Configuring DHCP and TFTP with dnsmasq ----------------------------------------------------- The following instructions can only serve as an example. Depending on your network structure, the IP addresses, ranges, and domain settings need to be adapted to allow the DHCP server to work within your network. If you already have a DHCP server running in your network, make sure that the `filename` and `next-server` directives are correctly set on this server. The following steps describe how to set up dnsmasq to work as DHCP and TFTP server. 1. Install the `dnsmasq` package. 2. Create the file :file:`/etc/dnsmasq.conf` and insert the following content: .. code:: bash # Do not function as a DNS server. port=0 # Log information about DHCP transactions. log-dhcp # Set the root directory for files available via TFTP, # usually "/srv/tftpboot": tftp-root=TFTP_ROOT_DIR enable-tftp dhcp-range=BOOT_SERVER_IP,proxy In the next step, it's required to decide on the boot method. There is the PXE loader provided via `pxelinux.0` from the `syslinux` package, and there is the GRUB loader provided via the `grub` package. .. note:: Placeholders Replace all placeholders (written in uppercase) with data fitting your network setup. 2.1. Insert the following content to use `pxelinux.0`: .. code:: bash # The boot filename, server name, and server IP address dhcp-boot=pxelinux.0,,BOOT_SERVER_IP # Disable re-use of the DHCP servername and filename fields as extra # option space. This is to avoid confusing some old or broken # DHCP clients. dhcp-no-override # PXE menu. The first part is the text displayed to the user. # The second is the timeout, in seconds. pxe-prompt="Booting FOG Client", 1 # The known types are x86PC, PC98, IA64_EFI, Alpha, Arc_x86, # Intel_Lean_Client, IA32_EFI, BC_EFI, Xscale_EFI and X86-64_EFI # This option is first and will be the default if there is no input # from the user. pxe-service=X86PC, "Boot to FOG", pxelinux.0 pxe-service=X86-64_EFI, "Boot to FOG UEFI", ipxe pxe-service=BC_EFI, "Boot to FOG UEFI PXE-BC", ipxe .. note:: On boot of a network client with that configuration, the default `pxelinux.0` config file is expected at :file:`TFTP_ROOT_DIR/pxelinux.cfg/default` 2.2. Insert the following content to use GRUB: .. code:: bash # The boot filename, server name, and server IP address dhcp-boot=boot/grub2/i386-pc/core.0,,BOOT_SERVER_IP When using GRUB, the referenced `dhcp-boot` GRUB module must be generated. To do this, change the directory to :file:`TFTP_ROOT_DIR` and create the :file:`setvars.conf` file with the following content: .. code:: bash set root=(tftp) set net_default_server=BOOT_SERVER_IP set prefix=boot/grub2 Now, call the following commands to create the GRUB module: .. code:: bash $ grub2-mknetdir --net-directory=TFTP_ROOT_DIR --subdir=boot/grub2 $ grub2-mkimage -O i386-pc-pxe \ --output boot/grub2/i386-pc/core.0 \ --prefix=/boot/grub2 \ -c setvars.conf \ pxe tftp .. note:: On boot of a network client with that configuration, the GRUB config file is expected at :file:`TFTP_ROOT_DIR/boot/grub2/grub.cfg` 3. Run the dnsmasq server by calling: .. code:: bash systemctl start dnsmasq ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/example_use_cases/use_suse_media.rst0000644000000000000000000000404515273675667021004 0ustar00.. _suse_media: Using SUSE Product ISO To Build =============================== .. sidebar:: Abstract This page provides information on how to use the SUSE media ISO with {kiwi}. When building an image with {kiwi}, the image description usually points to a number of public/private package source repositories from which the new image root tree will be created. Alternatively, the vendor-provided product ISO image(s) can be used. The contents of the ISO (DVD) media also provide package source repositories but are organized in a vendor-specific structure. As a user, it's important to know about this structure so that the {kiwi} image description can consume it. To use a SUSE product media, the following steps are required: 1. Mount the ISO media from the file or DVD drive: .. code:: bash $ sudo mount Product_ISO_file.iso|DVD_drive /media/suse 2. Look up all `Product` and `Module` directories: Below `/media/suse`, there is a directory structure that provides package repositories in directories starting with `Product-XXX` and `Module-XXX`. It depends on the package list in the {kiwi} image description from which location a package or a dependency of the package is taken. Therefore, it is best practice to browse through all the directories and create a `` definition for each of them in the {kiwi} image description, as the following example shows: .. code:: xml Once all the individual product and module repos have been created in the {kiwi} image description, the build process can be started as usual. .. note:: Because of the manual mount process, the `/media/suse` location stays busy after {kiwi} has created the image. The cleanup of this resource is the responsibility of the user and not done by {kiwi}. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.735329 kiwi-11.1.8/doc/source/example_use_cases.rst0000644000000000000000000000270415273675667016012 0ustar00Examples ======== .. note:: This chapter collects practical KIWI examples for image creation, deployment, and customization. All images provided for testing use the root password `linux`. .. toctree:: :maxdepth: 1 example_use_cases/build_live_iso example_use_cases/build_simple_disk example_use_cases/build_expandable_disk example_use_cases/build_container_image example_use_cases/build_wsl_container example_use_cases/build_kis example_use_cases/build_enclave example_use_cases/iso_to_usb_stick_deployment example_use_cases/iso_to_usb_stick_file_based_deployment example_use_cases/iso_to_usb_stick_grub2_boot_from_iso example_use_cases/disk_setup_for_ec2 example_use_cases/disk_setup_for_azure example_use_cases/disk_setup_for_google example_use_cases/disk_setup_for_vagrant example_use_cases/disk_setup_for_luks example_use_cases/disk_ramdisk_deployment example_use_cases/custom_partitions example_use_cases/custom_volumes example_use_cases/clone_partitions example_use_cases/setup_network_bootserver example_use_cases/legacy_netboot_root_filesystem example_use_cases/network_overlay_boot example_use_cases/network_live_iso_boot example_use_cases/custom_fstab_extension example_use_cases/build_with_profiles example_use_cases/build_in_buildservice example_use_cases/use_suse_media example_use_cases/build_without_debianbootstrap example_use_cases/integration_testing.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/image_description/elements.rst0000644000000000000000000031414315273675667017631 0ustar00.. _image-description-elements: Schema Reference ================ .. note:: This document is the exhaustive reference for the {kiwi} image description schema in :file:`kiwi/schema/kiwi.rnc` for version |version|. .. contents:: Schema sections :local: :depth: 2 The sections are documented in the same order as they appear below the :ref:`sec.image` root element. Use :doc:`/image_description` for the guided introduction and this page when you need the complete list of valid elements and attributes. .. _sec.image: ------- The top level of any {kiwi} image description. .. code:: xml The image definition starts with an image tag and requires the schema format at version {schema_version}. The attribute name specifies the name of the image, which is also used for the filenames created by KIWI. Because we don’t want spaces in filenames, the name attribute must not have any spaces in its name. The following optional attributes can be inserted in the image tag: displayname Allows setting up the boot menu title for the selected boot loader. So you can have *suse-SLED-foo* as the image name but a different name as the boot display name. Spaces are not allowed in the display name because it causes problems for some boot loaders, and kiwi did not take the effort to separate the ones that can display them correctly from the ones that can't. id sets an identification number that appears as file ``/etc/ImageID`` within the image. .. _sec.include: --------- Optional include of XML file content from a file. .. code:: xml with file :file:`description.xml` as follows: .. code:: xml name contact text This will replace the `include` statement with the contents of :file:`description.xml`. The validation of the result happens after the inclusion of all `include` references. The value for the `from` attribute is interpreted as a URI. As of now, only local URI types are supported, as well as the `this://` resource locator, which translates into the path to the KIWI image description. .. note:: The include information must be embedded into an `` root node. Only the inner elements of the root node will be included. The processing of XML data via XSLT always requires a root node, which is the reason why this is required to be specified for include files as well. .. note:: Nesting of include statements in other include files is not supported. This will lead to unresolved include statements in the final document and will cause the runtime checker to complain about it. .. note:: The include is implemented via an XSLT stylesheet and therefore expects an XML document. Other markup formats are not supported as an include reference. .. _sec.description: ------------- Provide an image identity. .. code:: xml name contact text The mandatory description section contains information about the creator of this image description. The attribute `type` could be either of the value `system`, which indicates this is a system image description, or a value `boot` for custom kiwi boot image descriptions. The following optional subsections can be inserted below the description tag: license Specifies the license name that applies to this image description. .. _sec.certificates: -------------- Add a cert-file to the directory storing additional local CA certificates. The import will occur immediately after the bootstrap process, where the required CA update tooling is expected to be installed. This setting is useful for situations where certificates are not packaged, or the certificates are required during the build process, e.g., due to proxy servers in the build environment that need certificates in chroot. The required `target_distribution` attribute must be set to allow kiwi a correct matching for the CA store path and the update tool with regard to the image's target distribution. The following settings apply: +--------------+-------------------------------------------+------------------------+ | Distributor | CA Store | Update Tool | +==============+===========================================+========================+ | SUSE | /etc/pki/trust/anchors | update-ca-certificates | +--------------+-------------------------------------------+------------------------+ | Red Hat | /etc/pki/ca-trust/source/anchors | update-ca-trust | +--------------+-------------------------------------------+------------------------+ | Debian Based | /usr/local/share/ca-certificates | update-ca-certificates | +--------------+-------------------------------------------+------------------------+ | Arch Linux | /etc/ca-certificates/trust-source/anchors | update-ca-trust | +--------------+-------------------------------------------+------------------------+ .. code:: xml The `certificates` element may be specified at most once and accepts the following optional attribute: profiles="profile_a,profile_b" Import the listed certificates only when one of the listed build profiles is active (see :ref:`sec.profiles`). .. _sec.preferences: ------------- Set up image type and layout. .. code:: xml 1.2.3 zypper The mandatory preferences section contains information about the supported image type(s), the used package manager, the version of this image, and further optional elements. The preferences element may be provided multiple times and accepts the following optional attributes: arch="arch_name[,arch_name]" Use this preferences section only when building on one of the listed host architectures, as reported by :command:`uname -m`. profiles="profile_a,profile_b" Use this preferences section only when one of the listed build profiles is active (see :ref:`sec.profiles`). Preferences sections without a `profiles` attribute apply in any case and are combined with the profiled ones. ~~~~~~~~~~~~~~~~~~~~~~ The mandatory image version must be a three-part version number of the format: **Major**.\ **Minor**.\ **Release**. In case of changes to the image description, the following rules should apply: * For smaller image modifications that do not add or remove any new packages, only the release number is incremented. The XML description file(``config.xml``) remains unchanged. * For image changes that involve the addition or removal of packages, the minor number is incremented, and the release number is reset. * For image changes that change the behavior or geometry of the image file, the major number is incremented. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The mandatory `packagemanager` element specifies which package manager should be used to handle software packages. The `packagemanager` setup is connected to the distribution used to build the image. The following table shows which package manager is connected to which distributor: +--------------+-----------------+ | Distributor | Package Manager | +==============+=================+ | SUSE | zypper | +--------------+-----------------+ | Red Hat | dnf4 / dnf5 / | | | microdnf | +--------------+-----------------+ | Debian Based | apt | +--------------+-----------------+ | Arch Linux | pacman | +--------------+-----------------+ | Alpine Linux | apk | +--------------+-----------------+ In general, the specification of one preferences section is sufficient. However, it’s possible to specify multiple preferences sections and distinguish between the sections via the `profiles` attribute (see :ref:`sec.profiles`). In combination with the above, the preferences element supports the following optional elements: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ locale-filtering can be set to "true" or "false". If set to "true", it sets the `install_lang` macro for RPM-based installations to the RPM- configured locale list. This results in language-specific files being filtered out by `rpm` if they don't match the configured list. .. code:: xml true .. note:: It depends on the individual package design if the `install_lang` macro contents apply to the package or not. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies whether package signatures should be checked or not. .. code:: xml true ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies whether files marked as documentation should be skipped during installation. .. code:: xml true ~~~~~~~~~~~~~~~~~~~~~~~ Specifies the name of the console keymap to use. The value corresponds to a map file in `/usr/share/kbd/keymaps/xkb`. .. code:: xml us ~~~~~~~~~~~~~~~~~~~~~~~ Specifies the time zone. Available time zones are located in the `/usr/share/zoneinfo` directory. Specify the attribute value relative to `/usr/share/zoneinfo`. For example, specify `Europe/Berlin` for `/usr/share/zoneinfo/Europe/Berlin`. .. code:: xml Europe/Berlin ~~~~~~~~~~~~~~~~~~~~~ Specifies the name of the UTF-8 locale to use, which defines the contents of the `RC_LANG` system environment variable used in the image and to run the custom scripts specified as part of the {kiwi} image description. Please note, only UTF-8 locales are supported here, which also means that the encoding must *not* be part of the locale information. This means you need to specify the locale using the 4-digit name, like the following example: `en_US` or `en_US,de_DE`. .. code:: xml en_US ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies the name of the plymouth bootsplash theme to use. .. code:: xml bgrt ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies the name of the bootloader theme to use if that used bootloader has theme support. .. code:: xml openSUSE Along with the version and the packagemanager, at least one image type element must be specified to indicate which image type should be built. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies the distribution's global release version as consumed by package managers. Currently, the release version is not set or set to `0` for package managers that require a value to operate. With the optional `release-version` section, users have an opportunity to specify a custom value which is passed along the package manager to define the distribution release. .. note:: The release version information is currently used in dnf/dnf5 and microdnf package managers only. It might happen that it gets applied to the other package manager backends as well. This will happen on demand, though. ~~~~~~~~~~~~~~~~~~~~~~~~~~ Specifies the name of a license to be displayed in a dialog window at boot time. The element may be provided multiple times to show more than one license. .. code:: xml license_name ~~~~~~~~~~~~~~~~~~~ At least one type element must be configured. It is possible to specify multiple type elements in a preferences block. To set a given type description as the default image, use the boolean attribute `primary` and set its value to `true`: .. code:: xml The image type to be created is determined by the value of the image attribute. The following list describes the supported types and possible values of the image attribute: image="tbz" A simple tar archive image. The `tbz` type packs the contents of the image root tree into an xz-compressed tarball. image="btrfs|ext2|ext3|ext4|squashfs|erofs|xfs" A filesystem image. The image root tree data is packed into a filesystem image of the given type. An image of that type can be loop-mounted and accessed according to the capabilities of the selected filesystem. image="iso" An ISO image that can be dumped on a CD/DVD or USB stick and boots from this media without interfering with other system storage components. A useful pocket system for testing, demo, and debugging purposes. image="oem" An image representing an expandable system disk. This means after deployment, the system can resize itself to the new disk geometry. The resize operation is configurable as part of the image description, and an installation image for CD/DVD, USB stick, and network deployment can be created in addition. For use in cloud frameworks like Amazon EC2, Google Compute Engine, or Microsoft Azure, this disk type also supports the common virtual disk formats. image="docker" An archive image suitable for the Docker container engine. The image can be loaded via the `docker load` command and works within the scope of the container engine. image="oci" An archive image that builds a container matching the OCI (Open Container Initiative) standard. The container should be able to run with any OCI-compliant container engine. image="appx" An archive image suitable for the Windows Subsystem for Linux container engine. The image can be loaded from a Windows System that has support for WSL activated. image="wsl" An archive image suitable for the Windows Subsystem for Linux container engine >= v2.4.4. The image represents a gzip-compressed tar file as described in https://learn.microsoft.com/en-us/windows/wsl/build-custom-distro and can be loaded from a Windows System that has support for WSL in that version activated. image="kis" An optional root filesystem image associated with a kernel and initrd. The use case for this component image type is highly customizable. Many different deployment strategies are possible. image="pxe" The legacy network boot image type, superseded by `kis`. It builds the same kernel, initrd and root filesystem components but defaults to the {kiwi} initrd system rather than dracut. image="cpio" A cpio archive of the image root tree, as used for an initrd. image="enclave" An AWS Nitro Enclave image. Requires `enclave_format="aws-nitro"`. See :ref:`eif`. The complete set of accepted values is `btrfs`, `cpio`, `docker`, `enclave`, `erofs`, `ext2`, `ext3`, `ext4`, `iso`, `kis`, `oci`, `oem`, `pxe`, `squashfs`, `tbz`, `wsl`, `xfs` and `appx`. For completion of a type description, there could be several other optional attributes and child elements. The `type` element supports a plethora of optional attributes; some of these are only relevant for certain build types and will be covered in extra chapters that describe the individual image types in more detail. Certain attributes are, however, useful for nearly all build types and will be covered next: bootpartition="true|false": A boolean parameter notifying {kiwi} whether an extra boot partition should be used or not (the default depends on the current layout). This will override {kiwi}'s default layout. Only available for the `oem` type. bootpartsize="nonNegativeInteger": For images with a separate boot partition, this attribute specifies the size in MB. If not set, the boot partition size is set to 300 MB. Only available for the `oem` type. eficsm="true|false": For images with an EFI layout, specify if the legacy CSM (BIOS) mode should be supported or not. By default, CSM mode is enabled. Only available for the `oem` and `iso` types. efipartsize="nonNegativeInteger": For images with an EFI fat partition, this attribute specifies the size in MB. If not set, the EFI partition size is set to 20 MB. Only available for the `oem` type. efimountoptions="option_string": For images with an EFI fat partition, this attribute specifies the mount options of the EFI partition as written to :file:`/etc/fstab`, for example `umask=0077,shortname=winnt`. If not set, the `defaults` mount option is used. Only available for the `oem` type. efifatimagesize="nonNegativeInteger": For ISO images (live and install), the EFI boot requires an embedded FAT image. This attribute specifies the size in MB. If not set, the FAT image size is set to 20 MB. Only available for the `oem` and `iso` types. efiparttable="msdos|gpt": For images with an EFI firmware, specifies the partition table type to use. If not set, it defaults to the GPT partition table type. Only available for the `oem` and `iso` types. dosparttable_extended_layout="true|false": For OEM disk images, specifies to make use of logical partitions inside of an extended one. If set to true and if the msdos table type is active, this will cause the fourth partition to be an extended partition, and all following partitions will be placed as logical partitions inside of that extended partition. This setting is useful if more than 4 primary partitions need to be created in an msdos table. Only available for the `oem` type. btrfs_quota_groups="true|false": A boolean parameter to activate filesystem quotas if the filesystem is `btrfs`. By default, quotas are inactive. Only available for the `oem` type. btrfs_set_default_volume="true|false": For OEM disk images using the btrfs filesystem, this requests to set a default volume for the rootfs, which is used when the filesystem gets mounted. In case a `true` value is provided or the attribute is not specified at all, kiwi will make a volume the default volume. This can be either `/` or the configured root subvolume or the configured root snapshot. Consequently, the entry created for the rootfs in the `/etc/fstab` file will not contain any specific volume definition. In case a `false` value is provided, kiwi will not set any default volume, which also means that the entry for the rootfs in the `/etc/fstab` file requires a volume definition which is placed by kiwi as a `subvol=` parameter in the respective fstab field entry. In addition, the parameter `rootflags=subvol=` is added to the kernel commandline such that early initrd code has a chance to know about the rootfs volume. btrfs_root_is_subvolume="true|false": Tell kiwi to create a root volume to host (/) inside. The name of this subvolume is, by default, set to: `@`. The name of the subvolume can be changed via a volume entry of the form: .. code:: xml By default, the creation of a top-level volume is set to `true`. Only available for the `oem` type. btrfs_root_is_snapper_snapshot="true|false": A boolean parameter that tells {kiwi} to install the system into a btrfs snapshot. The snapshot layout is compatible with the snapper management toolkit and follows a concept by SUSE. By default, snapshots are turned off. Only available for the `oem` type. btrfs_root_is_readonly_snapshot="true|false": A boolean parameter notifying {kiwi} that the btrfs root filesystem snapshot has to be made read-only. If this option is set to true, the root filesystem snapshot will be turned into read-only mode once all data has been placed in it. The option is only effective if `btrfs_root_is_snapper_snapshot` is also set to true. By default, the root filesystem snapshot is writable. Only available for the `oem` type. compressed="true|false": Specifies whether the root filesystem image should be xz-compressed or not. Only available for the `pxe` and `kis` types. If set to `true`, the tar archive that contains the build results is not compressed again; otherwise, the tar archive is xz-compressed. archive="true|false": Specifies whether the image output file should be an archive or not. This option is only available for the `kis` type. When set to `true`, all image build results will be placed into an archive file. When set to `false`, the build results will stay as individual files in the build directory. The default is `true`. editbootconfig="file_path": Specifies the path to a script that is called right before the bootloader is installed. The script runs relative to the directory that contains the image structure. Only available for the `oem` and `iso` types. editbootinstall="file_path": Specifies the path to a script that is called right after the bootloader is installed. The script runs relative to the directory that contains the image structure. Only available for the `oem` type. filesystem="btrfs|ext2|ext3|ext4|squashfs|erofs|xfs": The root filesystem. Available for the `oem`, `pxe`, `kis` and `iso` types, and **required** for the `oem` type. firmware="efi|uefi|bios|ec2|ofw|opal|custom": Specifies the boot firmware of the appliance. This attribute is used to differentiate the image according to the firmware which boots up the system. It mostly impacts the disk layout and the partition table type. By default, `bios` is used on x86, `ofw` on PowerPC, and `efi` on ARM, RISC-V and LoongArch. Only available for the `oem`, `pxe` and `iso` types. * `efi` Standard EFI layout. * `uefi` Standard EFI layout for secure boot. * `bios` Standard BIOS layout for x86. * `ec2` Standard BIOS layout for x86 using Xen grub modules for old- style Xen boot in Amazon EC2. * `ofw` Standard PPC layout. * `opal` Standard openPOWER PPC64 layout. kexec-based boot process. * `custom` The firmware setup is entirely provided by custom scripts and {kiwi} takes no further action. force_mbr="true|false": A boolean parameter to force the usage of an MBR partition table even if the system would default to GPT. This is occasionally required on ARM systems that use an EFI partition layout but must not be stored in a GPT. Note that forcing an MBR partition table incurs limitations with respect to the number of available partitions and their sizes. Only available for the `oem` type. apply_dps_codes="true|false": A boolean parameter to apply the partition type codes of the `Discoverable Partitions Specification `_ (DPS) to the partitions of a GPT which match a DPS partition type, e.g the root partition gets the root partition type of the image architecture and the boot partition the extended boot loader (XBOOTLDR) type. The type codes are taken from `systemd-id128 show` on the build host, or from a built-in table if it is not available. By default the type codes are not applied and the partitions keep the type of their purpose, e.g Linux filesystem data for the root partition. Only available for the `oem` type. fsmountoptions="option_string": Specifies the filesystem mount options that are passed via the `-o` flag to :command:`mount` and are included in :file:`/etc/fstab`. fscreateoptions="option_string": Specifies the filesystem options used to create the filesystem. In {kiwi}, the filesystem utility to create a filesystem is called without any custom options. The default options are filesystem- specific and are provided along with the package that provides the filesystem utility. For the Linux `ext[234]` filesystem, the default options can be found in the :file:`/etc/mke2fs.conf` file. Other filesystems provide this differently and document information about options and their defaults in the respective manual page, e.g., :command:`man mke2fs`. With the `fscreateoptions` attribute, it's possible to directly influence how the filesystem will be created. The options provided as a string are passed to the command that creates the filesystem without any further validation by {kiwi}. For example, to turn off the journal on the creation of an ext4 filesystem, the following option would be required: .. code:: xml kernelcmdline="string": Additional kernel parameters passed to the kernel by the bootloader. Only available for the `oem`, `iso`, `pxe`, `kis` and `enclave` types. root_clone="number" For OEM disk images, this attribute allows you to create `number` clone(s) of the root partition, with `number` >= 1. A clone partition is content-wise an exact byte-for-byte copy of the origin root partition. However, to avoid conflicts at boot time, the UUID of any cloned partition will be made unique. In the sequence of partitions, the clone(s) will always be created first, followed by the partition considered the origin. The origin partition is the one that will be referenced and used by the system. Also, see :ref:`clone_partitions`. Only available for the `oem` type. boot_clone="number" Same as `root_clone` but applied to the boot partition if present. Only available for the `oem` type. luks="passphrase|file:///path/to/keyfile|random": Supplying a value will trigger the encryption of the partition serving the root filesystem using the LUKS extension. The supplied value represents either the passphrase string or the location of a key file if specified as a `file://...` resource or the reserved name `random`. When using a passphrase, the system will interactively ask for that passphrase on first boot unless it is set empty. In case of an empty passphrase, the system cannot be considered secure. When using a key file, the information from the file is read and used as a passphrase. The given key file is **not automatically** placed into the system or added to `/etc/crypttab`, which means the passphrase in the key file is, by default, requested from an interactive dialog at boot time. When using the reserved word `random`, kiwi will create a key file with a random passphrase and place this information into `/etc/crypttab`. This allows the system to boot without user interaction but also requires the initrd to be protected in some way because it will contain the key file. The use of `random` is therefore only secure if the image adds additional security that encrypts the initrd, as is, e.g., done in the IBM secure execution process. If the encryption of the system is combined with the attribute `bootpartition="false"`, it's important to understand that this will place `/boot` into the encrypted area of the system and leave reading boot data from it as a responsibility to the bootloader. Not every bootloader can cope with that, and those that can, e.g., grub, will then open an interactive dialog at the bootloader level asking for the credentials to decrypt the root filesystem. Only available for the `oem`, `iso`, `pxe` and `kis` types. luks_version="luks|luks1|luks2": Specify which `LUKS` version should be used. If not set, and by default, `luks` is used. The interpretation of the default depends on the distribution and could result in either 'luks1' or 'luks2'. The specification of the `LUKS` version allows using a different set of `luksformat` options. To investigate the differences between the two, please consult the `cryptsetup` manual page. Only available for the `oem`, `iso`, `pxe` and `kis` types. target_blocksize="number": Specifies the image blocksize in bytes, which has to match the logical blocksize of the target storage device. By default, 512 bytes is used, which works on many disks. You can obtain the blocksize from the `SSZ` column in the output of the following command: .. code:: shell-session blockdev --report $DEVICE Only available for the `oem` and `kis` types. target_removable="true|false": Indicate if the target disk for OEM images is deployed to a removable device, e.g., a USB stick or not. This only affects the EFI setup if requested, and in the end, avoids the creation of a custom boot menu entry in the firmware of the target machine. By default, the target disk is expected to be non-removable. Only available for the `oem` type. selinux_policy="targeted|mls|minimum": The `selinux_policy` attribute sets the SELinux policy to use. The `targeted` policy is the default policy. Only change this option if you want to use the `mls` or `minimum` policy. spare_part="number": Request a spare partition right before the root partition of the requested size. The attribute takes a size value and allows a unit in MB or GB, e.g., 200M. If no unit is given, the value is considered to be mbytes. A spare partition can only be configured for the disk image type oem. spare_part_mountpoint="dir_path": Specify a mount point for the spare partition in the system. Can only be configured for the disk image type oem. spare_part_fs="btrfs|ext2|ext3|ext4|xfs": Specify a filesystem for the spare partition in the system. Can only be configured for the disk image type oem. spare_part_fs_attributes="attribute_list": Specify filesystem attributes for the spare partition. Attributes can be specified as a comma-separated list. Currently, the attributes `no-copy-on-write` and `synchronous-updates` are available. Can only be configured for the disk image type oem. spare_part_is_last="true|false": Specify if the spare partition should be the last one in the partition table. It can only be configured for the `oem` type with oem-resize switched off. By default, the root partition is the last one, and the spare partition lives before it. With this attribute, that setup can be toggled. However, if the root partition is no longer the last one, the OEM repart/resize code can no longer work because the spare part would block it. Because of that, moving the spare part at the end of the disk is only applied if oem-resize is switched off. There is a runtime check in the {kiwi} code to check this condition. devicepersistency="by-uuid|by-label|by-partuuid": Specifies which method to use for persistent device names. This will affect all files written by kiwi that include device references, for example, `/etc/fstab` or the `root=` parameter in the kernel command line. By default, by-uuid is used. .. note:: devicepersistency and its impact to install images Please note that specifying `devicepersistency` also affects an eventually generated install image. This means that the installation media created from the image description will also use the specified device persistency method. It is possible that the selected method does not provide device representations for the disk devices found by the installation process. In this case the fallback to the raw unix device node e.g., `/dev/sda` will be used. If the installation media is created to allow user interaction and there are more than one disk device found during installation, the user will be prompted with a list of disk device names to select from. If the list shows the raw unix device nodes, it might be difficult for the user to select the correct disk. In this case, it is recommended to explicitly set the persistency method to use for the installation media via the `rd.kiwi.install.devicepersistency` kernel command line parameter. The parameter can also be set as a fixed value in the `kernelcmdline` attribute of the image description. Available persistency methods useful for disk device mappings are: `by-id` and `by-path`. squashfscompression="uncompressed|gzip|lzo|lz4|xz|zstd": Specifies the compression type for mksquashfs. Only available for the `oem`, `pxe`, `kis`, `iso` and `squashfs` types. erofscompression="text" Specifies the compression type and level for erofs. The attribute is a free-form text because erofs allows parameters for the different compression types. Please consult the erofs man page for details on how to specify a value for the `-z` option on `mkfs.erofs` and pass a proper value as `erofscompression`. Only available for the `oem`, `pxe`, `kis`, `iso` and `erofs` types. standalone_integrity="true|false": For the `oem` type only, specifies to create a standalone `dm_integrity` layer on top of the root filesystem. integrity_legacy_hmac="true|false": For the `oem` type only and in combination with the `standalone_integrity` attribute, this allows the use of old, flawed HMAC calculations (does not protect the superblock). .. warning:: Do not use this attribute unless compatibility with a specific old kernel is required! integrity_keyfile="filepath": For the `oem` type only, and in combination with the `standalone_integrity` attribute, this protects access to the integrity map using the given keyfile. integrity_metadata_key_description="string": For the `oem` type only, and in combination with the `embed_integrity_metadata` attribute, specifies a custom description of an integrity key as it is expected to be present in the kernel keyring. The information is placed in the integrity metadata block. If not specified, kiwi creates a key argument string instead, which is based on the given `integrity_keyfile` filename. The format of this key argument is: .. code:: bash :BASENAME_OF_integrity_keyfile_WITHOUT_FILE_EXTENSION embed_integrity_metadata="true|false": For the `oem` type only, and in combination with the `standalone_integrity` attribute, specifies to write a binary block at the end of the partition serving the root filesystem, containing information to create the `dm_integrity` device map in the following format: .. code:: bash |header|0xFF|dm_integrity_meta|0xFF|0x0| header: Is a string of the following information separated by spaces: * **version**: currently set to `1` * **fstype**: name of the `filesystem` attribute * **access**: either `ro` or `rw`, depending on the filesystem capabilities * `integrity`: fixed identifier value dm_integrity_meta: Is a string of the following information separated by spaces: * **provided_data_sectors**: number of data sectors * **sector_size**: sector size in bytes; defaults to 512 * **parameter_count**: number of parameters needed to construct the integrity device map. After the `parameter_count`, a list of space-separated parameters follows, and the `parameter_count` specifies the quantity of these parameters. * **parameters**: The first element of the parameter list contains information about the used hash algorithm, which is not part of the superblock and is provided according to the parameters passed along when {kiwi} calls `integritysetup`. As of now, this defaults to: - internal_hash:sha256 All subsequent parameters are taken from the `flags` field of the dm-integrity superblock. see the dm-integrity documentation on the web for possible flag values. verity_blocks="number|all": For the `oem` and `iso` types only, specifies to create a dm-verity hash from the number of given blocks (or all) placed at the end of the root filesystem. For later verification of the device, the credentials information produced by `veritysetup` from the cryptsetup tools are needed. This data as of now is only printed as debugging information to the build log file. A concept to persistently store the verification metadata as part of the partition(s) will be a next step. embed_verity_metadata="true|false": For the `oem` type only, and in combination with the `verity_blocks` attribute, specifies to write a binary block at the end of the partition serving the root filesystem, containing information for `dm_verity` verification in the following format: .. code:: bash |header|0xFF|dm_verity_credentials|0xFF|0x0| header: Is a string of the following information separated by spaces: * **version**: currently set to `1` * **fstype**: name of `filesystem` attribute * **access**: either `ro` or `rw` depending on the filesystem capabilities * `verity`: fixed identifier value dm_verity_credentials: Is a string of the following information separated by spaces: * **hash_type**: hash type name as returned by `veritysetup` * **data_blksize**: data blocksize as returned by `veritysetup` * **hash_blksize**: hash blocksize as returned by `veritysetup` * **data_blocks**: number of data blocks as returned by `veritysetup` * **hash_start_block**: hash start block as required by the kernel to construct the device map * **algorithm**: hash algorithm as returned by `veritysetup` * **root_hash**: root hash as returned by `veritysetup` * **salt**: salt hash as returned by `veritysetup` .. note:: dm-verity Runtime Options Additional veritysetup options can be provided at boot time using the kernel parameter `rd.kiwi.verity_options=option1,option2`. This allows runtime customization. KIWI automatically provides `hash-offset` and `hash-block-size` parameters based on the image configuration. These should not be specified manually via `rd.kiwi.verity_options=`. **Common Options:** * `panic-on-corruption`: System panics on corruption detection. * `restart-on-corruption`: System restarts on corruption (often default). * `ignore-corruption`: Ignore corruption (debugging only). * `ignore-zero-blocks`: Skip verification of zero blocks. * `check-at-most-once`: Verify each block only once. For complete options, see `veritysetup(8) `_. overlayroot="true|false": For the `oem` type only, specifies to use an `overlayfs`-based root filesystem consisting of a squashfs-compressed read-only root filesystem combined with an optional write partition or tmpfs. The optional kernel boot parameter `rd.root.overlay.temporary` can be used to point the write area into a `tmpfs` instead of a persistent write partition. In this mode, all written data is temporary until the system is rebooted. The kernel boot parameter `rd.root.overlay.size` can be used to configure the size for the `tmpfs` that is used for the `overlayfs` mount process if `rd.root.overlay.temporary` is requested. That size configures the amount of space available for writing new data during the runtime of the system. The default value is set to `50%`, which means one half of the available RAM space can be used for writing new data. By default, the persistent write partition is used. The size of that partition can be influenced via the optional `` element in the `` section or via the optional `` element in the `` section of the XML description. Setting a fixed `` value will set the size of the image disk to that value and results in an image file of that size. The available space for the write partition is that size, reduced by the size the squashfs read-only system needs. If the `` element is set to `true`, an eventually given `` element will not have any effect because the write partition will be resized on first boot to the available disk space. To disable the use of any overlay, the kernel boot parameter `rd.root.overlay.readonly` can be used. It takes precedence over all other overlay kernel parameters because it leads to the deactivation of any overlayfs-based action and just boots up with the squashfs root filesystem. In fact, this mode is the same as not installing the `kiwi-overlay` dracut module. overlayroot_readonly_filesystem="squashfs|erofs": For the `oem` type only, specifies the filesystem type to use as a read-only filesystem in an `overlayroot` setup. By default, `squashfs` is used. overlayroot_readonly_createoptions="string": For the `oem` type only, specifies additional creation options passed to the tool that builds the read-only filesystem of an `overlayroot` setup (`mkfs.erofs` or `mksquashfs`). The value is a whitespace separated list of options, analogous to `fscreateoptions` for the read-write root filesystem. A typical use is to select a larger EROFS physical cluster for a better compression ratio, for example `overlayroot_readonly_createoptions="-C 1048576"`. overlayroot_write_partition="true|false": For the `oem` type only, this allows you to specify if the extra read-write partition in an `overlayroot` setup should be created or not. By default, the partition is created, and the kiwi-overlay dracut module also expects it to be present. However, the overlayroot feature can also be used without dracut (`initrd_system="none"`), and under certain circumstances, it is handy to configure whether the partition table should contain the read-write partition or not. overlayroot_readonly_partsize="mbsize": Specifies the size in MB of the partition that stores the squashfs-compressed read-only root filesystem in an overlayroot setup. If not specified, kiwi calculates the needed size by a preliminary creation of the squashfs compressed file. However, this is only accurate if no changes to the root filesystem data happen after this calculation, which cannot be guaranteed as there is at least one optional script hook that is allowed and applied after the calculation. In addition, the pre-calculation requires some time in the build process. If the value can be provided beforehand, this also speeds up the build process significantly. Only available for the `oem` type. bootfilesystem="btrfs|ext2|ext3|ext4|xfs|fat32|fat16": If an extra boot partition is required, this attribute specifies which filesystem should be used for it. The type of the selected bootloader might overwrite this setting if there is no alternative possible, though. Only available for the `oem` type. flags="overlay|dmsquash": For the ISO image type, this specifies the live ISO technology and dracut module to use. If set to `overlay`, the kiwi-live dracut module will be used to support a live ISO system based on squashfs+overlayfs. If set to `dmsquash`, the dracut standard dmsquash-live module will be used to support a live ISO system based on the capabilities of the upstream dracut module. format="format_name": For disk image type `oem`, this specifies the format of the virtual disk so that it can run on the desired target virtualization platform. The supported format names are: * `gce`: Google Compute Engine image format. * `ova`: Open Virtualization Format Archive. * `qcow2`: QEMU Copy-On-Write version 2. * `vagrant`: Vagrant box format (tar.gz). * `vmdk`: VMware Virtual Machine Disk. * `vdi`: VirtualBox Disk Image. * `vhd`: Microsoft Virtual Hard Disk. * `vhdx`: Microsoft Virtual Hard Disk version 2. * `vhd-fixed`: Microsoft Virtual Hard Disk with fixed size. * `oci:image_format`: OCI image archive (oci format). * `docker:image_format`: OCI image archive (docker format). The `(oci|docker):image_format` format is a special case that stores the disk image inside of an OCI-compliant container. The disk image is stored in the specified `image_format` in the `disk/` directory of the container. The disk format can be one of the above formats, except `vagrant` and the container formats themselves, or just `raw` if the disk should be stored as a raw disk inside of the container. Custom naming conventions for the disk image can be applied by using the `bundle_format` attribute. The `derived_from` attribute can be used to specify the source container. The resulting container image will be built by adding the disk image as a new layer on top of the specified reference container. If there is no `derived_from` attribute, the container image will be built from scratch using an empty root directory. This format is particularly useful for building an image which bundles the actual disk image and its runtime requirements into one artifact. formatoptions="string": Specifies additional format options passed on to `qemu-img`. `formatoptions` is a comma-separated list of format-specific options in a `name=value` format, as `qemu-img` expects it. kiwi will take the information and pass it as a parameter to the `-o` option in the `qemu-img` call. Only available for the `oem` type. gpt_hybrid_mbr="true|false": For disk types, create a hybrid GPT/MBR partition table with an 'accurate' MBR table that will have no 'bootable' flagged partition. For ISO types, create a hybrid GPT/MBR partition table where the MBR partition table contains a whole-disk 'protective' partition and a second bootable-flagged partition (intended to make the image bootable in both UEFI and BIOS modes on as much hardware as possible). In both cases, it only has an effect if the EFI partition table type is GPT. Only available for the `oem` and `iso` types. hybridpersistent="true|false": For the live ISO type, this triggers the creation of a partition for a COW file to keep data persistent over a reboot. Only available for the `iso` and `pxe` types. hybridpersistent_filesystem="ext4|xfs": For the live ISO type, set the filesystem to use for persistent writing if a hybrid image is used as a disk on, e.g., a USB Stick. By default, the ext4 filesystem is used. Only available for the `iso` and `pxe` types. initrd_system="kiwi|dracut|none": Specify which initrd builder to use; the default is set to `dracut`, except for the `pxe` type, which defaults to `kiwi`. If set to `none`, the image is built without an initrd. Depending on the image type, this can lead to a non-bootable system, as it's now a kernel responsibility if the given root device can be mounted or not. Only available for the `oem`, `pxe` and `kis` types. metadata_path="dir_path": Specifies a path to additional metadata required for the selected image type or its tools used to create that image type. .. note:: Currently, this is only effective for the `appx` container image type, where it is **mandatory**. installboot="failsafe-install|harddisk|install": Specifies the bootloader's default boot entry for the initial boot of a {kiwi} install image. .. note:: This value is only evaluated for grub. install_continue_on_timeout="true|false": Specifies the boot timeout handling for the {kiwi} install image. If set to "true", the configured timeout or its default value applies. If set to "false", no timeout applies in the boot menu of the install image. installprovidefailsafe="true|false": Specifies if the bootloader menu should provide a failsafe entry with special kernel parameters or not. Only available for the `oem` type. installiso="true|false" Specifies if an install ISO image should be created. This attribute is only available for the `oem` type. The generated ISO image is a hybrid ISO that can be used as a disk on, e.g., a USB stick or as an ISO. installstick="true|false": Specifies if an install stick image should be created. Only available for the `oem` type. installpxe="true|false": Specifies if a tarball that contains all data for a PXE network installation should be created. This attribute is only available for the `oem` type. mediacheck="true|false": For ISO images, specifies whether the bootloader menu should provide a mediacheck entry to verify ISO integrity or not. It is disabled by default and only available for the x86 arch family. mdraid="mirroring|striping": Set up software RAID in degraded mode with one disk. Thus, only mirroring and striping are possible. Only available for the `oem` type. primary="true|false": Specifies this type to be the primary type. If no type option is given on the commandline, {kiwi} will build this type. ramonly="true|false": For all images that are configured to use the overlay filesystem, this setting forces any COW(Copy-On-Write) action to happen in RAM. Only available for the `oem` type. rootfs_label="string": A label name to set for the root filesystem. By default, `ROOT` is used. Only available for the `oem`, `pxe`, `kis` and `docker` types. volid="string": For the `iso` and `oem` install ISO only, this specifies the volume ID (volume name or label) to be written into the master block. Can be at most 32 characters. application_id="string": For the ISO/(oem install ISO) type only, specifies the Application ID to be written into the master block. There is space for 128 characters. vhdfixedtag="GUID_string": For the VHD disk format, this specifies the GUID. Only available for the `oem` type. derived_from="string": For container images, specifies the image URI of the container image. The image created by {kiwi} will use the specified container as the base root to work on. Only available for the `docker`, `oci` and `oem` types. On the `oem` type it may only be combined with a container output format, that is `format="oci:..."` or `format="docker:..."`. delta_root="true|false": For container images and in combination with the `derived_from` attribute. If `delta_root` is set to `true`, {kiwi} creates a container image that only contains the differences compared to the given `derived_from` container. Such a container is, on its own, no longer functional and requires a tool that is able to provision a container instance from the `derived_from` container combined with the `delta_root` application container. Such a tool exists with the `oci-pilot `_ project and allows you to manage applications as containers that feel like native applications on the host system. Only available for the `docker` and `oci` types. ensure_empty_tmpdirs="true|false": For OCI container images, this specifies whether to ensure the `/run` and `/tmp` directories are empty in the container image created by {kiwi}. The default is true. Only available for the `docker` and `oci` types. publisher="string": For ISO images, this specifies the publisher name of the ISO. Only available for the `iso` type. eficsmpart_id="number": For OEM images, this specifies the partition number for the EFI CSM (legacy BIOS) partition. For details about the evaluation and impact of custom partition ids, please see: :ref:`custom_partitions`. efipart_id="number": For OEM images, specifies the partition number for the EFI partition. For details about the evaluation and impact of custom partition ids, please see: :ref:`custom_partitions`. rootpart_id="number": For OEM images, this specifies the partition number for the ROOT partition. For details about the evaluation and impact of custom partition ids, please see: :ref:`custom_partitions`. bootpart_id="number": For OEM images, specifies the partition number for the BOOT partition. For details about the evaluation and impact of custom partition ids, please see: :ref:`custom_partitions`. boot="path": Path to a {kiwi} boot image description used to build the initrd. Requires `initrd_system="kiwi"`. A relative path is looked up below :file:`/usr/share/kiwi/custom_boot`. Only available for the `oem` and `pxe` types. bootkernel="name": Name of the kernel profile of the boot image description to use. Passed on as `add-profile` when the boot image is built. bootprofile="name": Name of the profile of the boot image description to use. Passed on as `add-profile` when the boot image is built. Only available for the `oem`, `iso`, `pxe` and `cpio` types. bundle_format="string": Base name pattern used for all target image files by :command:`kiwi-ng result bundle`. See :ref:`image_types` for the list of supported placeholders. disk_start_sector="number": First usable sector of the partition table. The minimum, and the default, is 2048. Only available for the `oem` type. enclave_format="aws-nitro": Target format of the enclave image. `aws-nitro` is currently the only supported value. Only available for the `enclave` type. gcelicense="string": License tag to embed into an image built with `format="gce"`. Only available for the `oem` type. luksOS="sle12": Passes a predefined set of cipher, keysize and hash format options to the :command:`cryptsetup` call, producing a format compatible with the named distribution. Only available for the `oem`, `iso`, `pxe` and `kis` types. luks_pbkdf="pbkdf2|argon2i|argon2id": Key derivation function used by LUKS to derive the encryption key from the password. Only available for the `oem`, `iso`, `pxe` and `kis` types. luks_randomize="true|false": By default all blocks of a LUKS volume are filled with pseudo-random data. Set this to `false` to reduce the image size when shipping an image with a well-known key that is re-encrypted at deployment time. Only available for the `oem`, `iso`, `pxe` and `kis` types. provide_system_files="true|false": Emit a metadata file listing the system files of the image, for later consumption by a derived build. Only available for the `docker` and `oci` types. Use a `` section to filter the generated list. require_system_files="true|false": Consume the system file metadata provided by a base image. Only available for the `docker` and `oci` types. wwid_wait_timeout="number": Seconds to wait for a device WWID to appear. Defaults to 3. Only available for the `oem` type. xen_server="true|false": Marks the image as a Xen dom0 (server) rather than a Xen guest. Only available for the `oem` type. See also `check_xen_uniquely_setup_as_server_or_guest`. The following sections show the supported child elements of the `type` element, including references to their usage in a detailed type setup: .. _preferences-type-luksformat: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `luksformat` element is used to specify additional luks options passed on to the `cryptsetup luksFormat` call. The element requires the attribute `luks` to be set in the `` section referring to `luksformat`. Several custom settings related to the LUKS and LUKS2 format features can be set up. For example, the setup of the `dm_integrity` feature: .. code:: xml .. _preferences-type-bootloader: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `bootloader` element is used to select the bootloader. At the moment, `grub2`, `systemd_boot`, and the combination of zipl plus userspace grub2, `grub2_s390x_emu`, are supported. The special `custom` entry allows you to skip the bootloader configuration and installation and leaves this up to the user, which can be done by using the `editbootinstall` and `editbootconfig` custom scripts. .. note:: Bootloaders provide a very different set of features and only work within their individual implementation priorities. {kiwi} provides an API for bootloaders, but not all API methods can be implemented for all bootloaders due to the fact that some features only exist in one but not in another bootloader. If a bootloader setting is used that is not understood by the selected bootloader, the image build process will fail with an exception message. name="grub2|grub2_s390x_emu|systemd_boot|custom|zipl": Specifies the bootloader to use for this image. .. note:: systemd_boot ESP size The implementation to support systemd-boot reads all data from the ESP (EFI System Partition). This also includes the kernel and initrd, which requires the size of the ESP to be configured appropriately. By default, {kiwi} configures the ESP with 20MB. For systemd_boot, this is usually too small and can be changed with the `efipartsize` attribute. Reading boot-relevant files from another filesystem requires providing alternative EFI filesystem drivers, e.g., efifs, and also needs adaptations on the setup of `bootctl`. .. note:: systemd_boot and shim At the moment, the EFI image provided along with systemd-boot is not compatible with the shim-signed loader provided in an extra effort by the distributions. In addition to the mandatory name attribute, the following optional attributes are supported: bls="true|false": Specifies whether to use Bootloader Spec-style configuration if `grub` in the image supports it. It is a no-op if the `blscfg` module is not available. This option is only available for `grub` and defaults to true if not set. output_console="none|console|gfxterm|serial|vga_text|mda_text|morse|spkmodem": Specifies the bootloader output console. The attribute is available for the `grub` bootloader type. Multiple console types can be provided, separated by a space. input_console="none|console|serial|at_keyboard|usb_keyboard": Specifies the bootloader input console. The attribute is available for the `grub` bootloader type. Multiple console types can be provided, separated by a space. If no `input_console` is provided, the value for the `output_console` is used if specified. grub_template="filename": Specifies a custom grub bootloader template file that will be used instead of the one provided with {kiwi}. A static bootloader template to create the grub config file is only used in {kiwi} if the native method via the grub `mkconfig` toolchain does not work properly. As of today, this is only the case for live and install ISO images. Thus, this setting only affects the oem and iso image types. The template file should contain a `Template string `_ and can use the following variables: +-----------------------+----------------------------------------------+ | Variable | Description | +=======================+==============================================+ | search_params | parameters needed for grub's `search` | | | command to locate the root volume | +-----------------------+----------------------------------------------+ | default_boot | number of the default menu item to boot | +-----------------------+----------------------------------------------+ | kernel_file | the name of the kernel file | +-----------------------+----------------------------------------------+ | initrd_file | the name of the initial ramdisk file | +-----------------------+----------------------------------------------+ | boot_options | kernel command-line options for booting | | | normally | +-----------------------+----------------------------------------------+ | failsafe_boot_options | kernel command-line options for booting in | | | failsafe mode | +-----------------------+----------------------------------------------+ | gfxmode | the resolution to use for the bootloader; | | | passed to grub's `gfxmode` command | +-----------------------+----------------------------------------------+ | theme | the name of a graphical theme to use | +-----------------------+----------------------------------------------+ | boot_timeout | the boot menu timeout, set by the `timeout` | | | attribute | +-----------------------+----------------------------------------------+ | boot_timeout_style | the boot timeout style, set by the | | | `timeout_style` attribute | +-----------------------+----------------------------------------------+ | serial_line_setup | directives used to initialize the serial | | | port, set by the `serial_line` attribute | +-----------------------+----------------------------------------------+ | title | a title for the image: this will be the | | | `` tag's `displayname` attribute or | | | its `name` attribute if `displayname` is not | | | set; see: :ref:`sec.image` | +-----------------------+----------------------------------------------+ | bootpath | the bootloader lookup path | +-----------------------+----------------------------------------------+ | boot_directory_name | the name of the grub directory | +-----------------------+----------------------------------------------+ | efi_image_name | architecture-specific EFI boot binary name | +-----------------------+----------------------------------------------+ | terminal_setup | the bootloader console mode, set by the | | | `console` attribute | +-----------------------+----------------------------------------------+ | title_N, | title, kernel and initrd file name of the | | kernel_file_N, | further kernel flavors, numbered from 1, set | | initrd_file_N | by the `kernel_flavor` attribute. title, | | | kernel_file and initrd_file belong to the | | | first flavor in this case | +-----------------------+----------------------------------------------+ serial_line="string": Specifies the bootloader serial line setup. The setup is effective if the bootloader console is set to use the serial line. The attribute is available for the grub bootloader only. video_mode="string": Specifies the video mode to use for the bootloader. The attribute is available for the grub bootloader only and is passed to grub's `gfxmode` command. For details on the supported video modes, please see the grub manual at https://www.gnu.org/software/grub/manual/grub/html_node/gfxmode.html. timeout="number": Specifies the boot timeout in seconds prior to launching the default boot option. By default, the timeout is set to 10 seconds. It makes sense to set this value to `0` for images intended to be started non-interactively (e.g., virtual machines). timeout_style="countdown|hidden|menu": Specifies the boot timeout style to control the way in which the timeout interacts with displaying the menu. If set, the display of the bootloader menu is delayed after the timeout has expired. In countdown mode, an indication of the remaining time is displayed. The attribute is available for the grub loader only. targettype="CDL|FBA|SCSI|GPT": Specifies the device type of the disk zipl should boot. On zFCP and/or KVM devices, use `SCSI` or `GPT`; on DASD devices, use `CDL`; on emulated DASD devices, use `FBA`. The attribute is available for the `grub2_s390x_emu` and `zipl` loaders only. use_disk_password="true|false": When `/boot` is encrypted, makes the bootloader store the password in its configuration file (in cleartext). This is useful for full disk encryption images. kernel_flavor="flavor,...": Specifies one or more kernel flavors as a comma-separated list, for example, `kernel_flavor="rt,default"`. {kiwi} creates an initrd and a boot menu entry for the latest installed kernel of each flavor. The menu entries follow the order of the list, and the entry of the first flavor is the default entry. In the example above, the entry of the `rt` kernel is selected by default. If not specified, the boot menu entry is created for the latest installed kernel only. The flavor is the end of the kernel version following the release, separated by `-` or `+`. For example, `6.4.0-150600.23.25-rt` is of flavor `rt`, `6.1.0-21-rt-amd64` is of flavor `rt-amd64`, and `5.14.0-427.13.1.el9_4.x86_64+rt` is of flavor `rt`. A kernel of each flavor must be installed in the image. The menu entry titles are extended by the flavor name, for example `Title (rt)`. The special flavor `none` stands for kernels whose version does not end with a flavor name, for example `7.2.15-100.fc43.x86_64` of the Fedora `kernel` package. The version of such a kernel ends with its release, a part with a dot or a part without letters, after the last `-` or `+`. To boot the Fedora `kernel` by default and provide a menu entry for `kernel-rt` as well, set `kernel_flavor="none,rt"`. The menu entry title of the `none` flavor is not extended. The attribute is available for the `grub2`, `grub2_s390x_emu`, `systemd_boot` and `zipl` loaders and is handled as follows: grub2 (disk images): The menu entries are created by `grub2-mkconfig` for all installed kernels. {kiwi} selects the entry of the first flavor as the default entry by setting the `saved_entry` variable in the grub environment. This requires `GRUB_DEFAULT=saved`, which is the {kiwi} default. grub2 (live and install ISO images): The ISO image contains the kernel and initrd of each flavor. The files of the first flavor are named `linux` and `initrd`, those of further flavors have the flavor appended, for example `linux-default`. The boot menu provides one boot entry (live) or install entry (install) per flavor. Failsafe and mediacheck entries use the kernel of the first flavor, as does the system booted after the installation. systemd_boot: A loader entry is written for each flavor, and the entry of the first flavor is set as the default in `loader.conf`. zipl: A loader entry is written for each flavor, and the entry of the first flavor is set as the default section. The attribute requires the `dracut` initrd system. It is not supported in combination with a UKI (``), or with zipl secure execution (``). ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to specify data required to set up secure Linux execution. Secure Linux execution reads the kernel, initrd, and boot parameters from an encrypted data blob and couples the image to the machine it gets executed on. Typically, the private key is protected in hardware on the machine itself. {kiwi} supports secure execution for the IBM secure Linux target on the s390 platform, along with the bootloaders `zipl` and `grub2_s390x_emu`. .. code:: xml The `securelinux` element itself can be specified multiple times, for example to couple one image to several machines that use different signing certificates. hkd_ca_cert: Mandatory attribute providing the `Common Authority` certificate (signed by the root CA) that is used to establish a chain of trust for the verification of the `Host Key Document`. hkd_sign_cert: Mandatory attribute providing the `Signing` certificate that is used to establish a chain of trust for the verification of the `Host Key Document`. hkd_cert: Child element, required at least once. The file specified in the `name` attribute defines the `Host Key Document` and tightly couples the image to the host matching the document. Can be specified multiple times. hkd_revocation_list: Optional child element, providing in its `name` attribute the revocation list to check on the use of expired certificates in the chain of trust. Can be specified multiple times. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to specify custom arguments for the tools called to set up the bootloader: * secure boot via ``, e.g., `shim-install` * installation of the bootloader via ``, e.g., `grub-install` * configuration of the bootloader via ``, e.g., `grub-mkconfig` * setup of bootloader environment variables via ``, e.g., `grub2-editenv` .. code:: xml .. note:: {kiwi} does not judge the given parameters, and if the provided data is effectively used depends on the individual bootloader implementation. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to describe the container configuration metadata in docker or wsl image types. For details, see: :ref:`building_container_build` and: :ref:`building_wsl_build`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to describe vagrant configuration metadata in a disk image that is being used as a vagrant box. For details, see: :ref:`setup_vagrant`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to describe the volumes of the disk area, which contains the root filesystem. Volumes are either a feature of the used filesystem, or LVM is used for this purpose. For details, see: :ref:`custom_volumes`. .. note:: When both `` and `` are used, `` are evaluated first, and mount points defined in `` cannot be redefined as `` volumes. The two types define a complete disk setup, so there cannot be any overlapping volumes or mount points. As a result, whatever is written in `` cannot be expressed in the same way in ``. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to describe the geometry of the disk on the level of the partition table. For details, see: :ref:`custom_partitions`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to customize the deployment process in an OEM disk image. For details, see: :ref:`oem_customize`. ~~~~~~~~~~~~~~~~~~~~~~~~~ Used to customize the size of the resulting disk image in an OEM image. For details, see: :ref:`disk-the-size-element`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to customize the virtual machine configuration, which describes the components of an emulated hardware. For details, see: :ref:`disk-the-machine-element`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to customize the installation media images created for OEM images deployment. For details, see: :ref:`installmedia_customize`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~ Used to specify custom arguments for the initrd tooling, e.g., dracut. .. code:: xml dracut.uefi.true: As part of the `setup` action, this enables creating a UKI EFI binary. dracut.module.NAME: As part of the `add` or `omit` action, this adds or omits the given module name. The element can be specified multiple times. dracut.driver.NAME: As part of the `add` or `omit` action, this adds or omits the given kernel driver. The element can be specified multiple times and accepts kernel driver names without the `.ko` extension. .. _sec.registry: ------------ Set up containers to fetch from a registry assigned to one of the supported container backends. .. code:: xml The optional containers element specifies the location of one or more containers on a registry `source` server. {kiwi} will take this information and fetch the containers as OCI archives to the image. On first boot, these container archives will be loaded into the local container backend store for the selected backend, and the archive files will be deleted. Supported `backend` values are `docker`, `podman`, and `container-snap`. The `backend` attribute is mandatory and specifies for which container backend the image should be available in the system. The `containers` element has the following optional attributes: arch="arch" The containers section can be configured to apply only for a certain architecture. In this case, specify the `arch` attribute with a value as it is reported by :command:`uname -m`. profiles="profile_a,profile_b" Fetch the containers of this section only when one of the listed build profiles is active (see :ref:`sec.profiles`). ----------------------- Details about the container. .. code:: xml The `name` attributes are mandatory and specify the name of the container as it exists in the registry. The `container` element has the following optional attributes: path="some/path" The path to the container in the registry. If not specified, the value defaults to `/`. fetch_only="true|false" If set to `true`, kiwi will only fetch the container but will not set up the systemd unit for loading the container into the local registry. In this mode, the container archive file stays in the system and can be handled in a custom way. By default, `fetch_only` is set to `false`. tag="tagname" Specifies the container tag to fetch. If not set, the tag name defaults to `latest`. arch="arch" The container section can be configured to apply only for a certain architecture. In this case, specify the `arch` attribute with a value as it is reported by :command:`uname -m`. .. _sec.repository: ------------- Set up software sources for the image. .. code:: xml The repository element specifies the location and type of a repository to be used by the package manager as a package installation source. It is optional and may be provided multiple times, but at least one repository is required for {kiwi} to be able to install software. {kiwi} supports apk, apt, dnf4, dnf5, microdnf, pacman, and zypper as package managers, specified via the packagemanager element. The repository element has the following optional attributes: alias="name" Specifies an alternative name for the configured repository. If the attribute is not specified, {kiwi} will generate a random alias name for the repository. The specified name must match the pattern: `[a-zA-Z0-9_\-\.]+`. type="apk|apt-deb|apt-rpm|deb-dir|mirrors|rpm-md" Specifies the repository metadata format. If not set, {kiwi} derives a suitable default from the configured package manager. arch="arch_name[,arch_name]" Use this repository only when building on one of the listed host architectures. profiles="profile_a,profile_b" Use this repository only when one of the listed build profiles is active (see :ref:`sec.profiles`). architectures="arch_name[,arch_name]" Used for `apt-deb` repositories only. Declares which architectures this repository provides packages for. components="name" Used for Debian (apt) based repositories only. Specifies the component name that should be used from the repository. By default, the `main` component is used. distribution="name" Used for Debian (apt) based repositories only. Specifies the distribution name used in the repository data structure. imageonly="true|false" Specifies whether or not this repository should be configured in the resulting image without using it at build time. By default, the value is set to false. This attribute is mutually exclusive with `imageinclude`. repository_gpgcheck="true|false" Specifies whether or not this specific repository values the result of the repository signature validation. If not set, the default is `false`. If set, the relevant key information needs to be provided on the {kiwi} commandline using the `--signing-key` option or via the `` element as part of the `` setting in the image description. Some distributions provides the signing keys as part of the `distribution-gpg-keys` package. In that case, the keys can be included in the image by specifying a repository like the following: .. code:: xml Debian based distributions, on the other hand, usually provide the keys at https://ftp-master.debian.org/keys.html. Fetch the relevant key for the image target distribution from there and use them in the same way as described above. package_gpgcheck="true|false" Specify whether or not this specific repository values the result of the package signature validation for each package taken from this repository. If not set, the default value is `false`. If set, the same key information requirements as for the `repository_gpgcheck` attribute apply. customize="/path/to/custom_script" A custom script hook that is invoked with the repo file as a parameter for each file created by {kiwi}. .. note:: If the script is provided as a relative path, it will be searched in the image description directory. imageinclude="true|false" Specifies whether the given repository should be configured as a repository in the image or not. The default behavior is that repositories used to build an image are not configured as a repository inside the image. This feature allows you to change the behavior by setting the value to true. .. note:: Scope of repository uri's The repository is configured in the image according to the source path as specified with the `path` attribute of the source element. Therefore, if the path is not a fully qualified URL, you may need to adjust the repository file in the image to accommodate the expected location. It is recommended that you use the alias attribute in combination with the `imageinclude` attribute to avoid having unpredictable random names assigned to the repository you wish to include in the image. password="string" Specifies a password for the given repository. The password attribute must be used in combination with the username attribute. Dependent on the repository location, this information may not be used. username="name" Specifies a user name for the given repository. The username attribute must be used in combination with the password attribute. Dependent on the repository location, this information may not be used. priority="number" Specifies the repository priority for this given repository. Priority values are treated differently by different package managers. Repository priorities allow the package management system to disambiguate packages that may be contained in more than one of the configured repositories. The zypper package manager, for example, prefers packages from a repository with a *lower* priority over packages from a repository with higher priority values. The apt package manager behaves the other way round: {kiwi} writes the value verbatim as ``Pin-Priority`` into an apt preferences file for the repository, and apt prefers packages from the repository with the *higher* priority. The default priority of a repository in apt is 500. A value of 1, for example, causes apt to use a package from this repository only if no other repository provides it, even if the version in this repository is newer. A value above 1000 forces apt to use packages from this repository, even if this means a downgrade. The priority is pinned to the host name of the repository URI, or to all local repositories in case of a local path. Thus it also applies to all other repositories from the same host, or to all other local repositories. For details see ``apt_preferences(5)``. If the attribute is not set, {kiwi} writes no priority into the generated repository configuration, so the package manager's own default applies. For other package managers, please refer to the individual documentation about repository priorities. sourcetype="baseurl|metalink|mirrorlist" Specifies the source type of the repository path. Depending on whether the source path is a simple URL or a pointer to a metadata file or mirror list, the configured package manager needs to be set up appropriately. By default, the source is expected to be a simple repository baseurl. ~~~~~~~~~~~~~~~~~~~~ The location of a repository is specified by the path attribute of the mandatory source child element: .. code:: xml The location specification may include the `%arch` macro, which will expand to the architecture of the image building host. The value for the path attribute may begin with any of the following location indicators: * ``file:///local/path/to/file`` An absolute path to a file accessible through the local filesystem. * ``ftp://`` A ftp-protocol-based network location. * ``http://`` A http-protocol-based network location. * ``https://`` A https-protocol-based network location. .. note:: https repositories When specifying a https location for a repository, it is generally necessary to include the openssl certificates and a cracklib word dictionary as package entries in the bootstrap section of the image configuration. The names of the packages to include are individual to the used distribution. On SUSE systems, as one example, this would be `openssl-certs` and `cracklib-dict-full`. * ``obs://Open:Build:Service:Project:Name`` A reference to a project in the Open Build Service (OBS). {kiwi} translates the given project path into a remote URL at which the given project hosts the packages. * ``obsrepositories:/`` A placeholder for the Open Build Service (OBS) to indicate that all repositories are taken from the project configuration in OBS. A repository `` element can optionally contain one or more signing keys for the packages from this repository, as shown in the following example: .. code:: xml The `key` attribute is a URI which can point to a remote location via `http://`, `https://` or `ftp://`, or to a local file. According to RFC 8089, a local file must be specified with an absolute path, either as `file:/path/to/key` or as `file:///path/to/key`. Relative paths like `file:path/to/key` or `file://path/to/key` are not supported. To use a key file stored in the image description directory, use the `this://` URI type: .. code:: xml All signing keys from all repositories will be collected and incorporated into the keyring as used by the selected package manager. .. note:: Signing keys of image repositories with apt For the `apt` package manager, the signing keys are only available at build time, unless the repository is marked with `imageinclude` or `imageonly`. For such a repository, its signing keys are stored permanently in the image as :file:`/usr/share/keyrings/ALIAS-archive-keyring.[asc|gpg]` and referenced via the `Signed-By` option of the repository configuration in the image. This follows the `Debian recommendation for third party repositories `_. Use the `alias` attribute to get a predictable keyring file name. .. _sec.packages: ----------- Set up software components to be installed in the image. .. code:: xml The packages element specifies the setup of a packages group for the given type. It is optional and may be provided multiple times, but in practice at least a `bootstrap` group is required to produce a usable appliance. The value of the type attribute specifies at which state in the build process the packages group is handled. The attribute accepts the following set of values: type="bootstrap" Bootstrap packages, a list of packages to be installed first into a new (empty) root tree. The packages list the required components to support a chroot environment from which further software components can be installed. type="image" Image packages, a list of packages to be installed as part of a chroot operation inside the new root tree. type="uninstall|delete" Packages to be uninstalled or deleted. For further details, see :ref:`uninstall-system-packages`. type="docker|iso|kis|oci|oem|pxe" Packages to be installed only when the named image type is being built. For example, with `type="iso"` the packages in this group are installed only if the ISO image type is built. Note that these six are the only image type names accepted here. Other `` values such as `appx`, `wsl`, `enclave`, `tbz`, `cpio` and the plain filesystem types have no corresponding packages group, use `type="image"` for those. type="systemfiles" Used together with the `provide_system_files` and `require_system_files` type attributes. The group typically holds `` entries naming packages to filter out of the generated system file list. In addition to `type`, the `packages` element accepts the following optional attributes: profiles="profile_a,profile_b" Use this packages group only when one of the listed build profiles is active (see :ref:`sec.profiles`). patternType="onlyRequired|plusRecommended" Controls how `namedCollection` entries are resolved. bootstrap_package="package_name" For use with the `apt` package manager only, and only on a `type="bootstrap"` group. Specifies the name of a bootstrap package that provides a bootstrap tarball in :file:`/var/lib/bootstrap/PACKAGE_NAME.ARCH.tar.xz`. The tarball is unpacked and used as the bootstrap rootfs to begin with, providing an alternative bootstrap method. For further details, see :ref:`debianbootstrap_alternative`. The packages element may contain any number of the following child elements, which provide specific configuration information for the specified packages group: ~~~~~~~~~~~~~~~~~~~ .. code:: xml The package element installs the given package name. The optional `arch` attribute can be used to limit the installation of the package to the host architecture from which {kiwi} is called. The `arch` attribute is also available in all of the following elements. Two further optional attributes control the {kiwi} initrd: bootinclude="true|false" Additionally install this package into the {kiwi} initrd. bootdelete="true|false" Remove this package from the {kiwi} initrd again. Only evaluated in combination with `bootinclude`. ~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: xml The `namedCollection` element is used to install a number of packages grouped together under a name. This is a feature of the individual distribution and is used in the implementation of the {kiwi} package manager backend. At the moment, collections are only supported for SUSE- and Red Hat-based distributions. The optional `patternType` attribute is used to control the behavior of the dependency resolution of the package collection. `onlyRequired` installs only the collection and its required packages. `plusRecommended` installs the collection, any of its required packages, and any recommended packages. .. note:: Collections on SUSE On SUSE-based distributions, collections are called `patterns` and are just simple packages. To get the names of the patterns such that they can be used in a namedCollection type, use the following command: `$ zypper patterns`. If for some reason the collection name cannot be used, it is also possible to add the name of the package that provides the collection as part of a `package` element. To get the names of the pattern packages, type the following command: `$ zypper search patterns`. By convention, all packages that start with the name "patterns-" represent a pattern package. .. note:: Collections on Red Hat On Red Hat-based distributions, collections are called `groups` and are extra metadata. To get the names of these groups, type the following command: `$ dnf group list -v`. Please note that since {kiwi} v9.23.39, group IDs are allowed only, e.g.: ``. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code:: xml In CentOS Stream >= 8 and Red Hat Enterprise Linux >= 8, there are Application Streams that are offered in the form of modules (using Fedora Modularity technology). To build images that use this content, {kiwi} offers to enable/disable modules when using the `dnf` or `microdnf` package manager backend. Modules are set up prior to the bootstrap phase, and their setup persists as part of the image. There are the following constraints when adding `collectionModule` elements: * `collectionModule` elements can only be specified as part of the `` section. This is because the setup of modules must be done once and as early as possible in the process of installing the image root tree. * Disabling a module can only be done as a whole, and therefore, the `stream` attribute is not allowed for disabling modules. For enabling modules, the `stream` attribute is optional. * The `enable` attribute is mandatory because it should be an explicit setting if a module is effectively used or not. ~~~~~~~~~~~~~~~~~~~ .. code:: xml The `file` element takes the `name` attribute and looks up the given name as a file on the system. If specified as a relative path, {kiwi} looks up the name in the image description directory. The file is installed using the `rsync` program. The file element has the following optional attributes: owner="user:group" The `owner` attribute can be specified to make the file belong to the specified owner and group. The ownership of the original file is meaningless in this case. The provided value is passed along to the `chown` program. permissions="perms" The `permissions` attribute can be specified to store the file with the provided permission. The permission bits of the original file are meaningless in this case. The provided value is passed along to the `chmod` program. target="some/path" The `target` attribute can be used to specify a target path to install the file to the specified directory and name. Eventually, missing parent directories will be created. ~~~~~~~~~~~~~~~~~~~ .. code:: xml The `archive` element takes the `name` attribute and looks up the given name as a file on the system. If specified as a relative path, {kiwi} looks up the name in the image description directory. The archive is installed using the `tar` program. Thus, the file name is expected to be a tar archive. The compression of the archive is detected automatically by the tar program. The optional `target_dir` attribute can be used to specify a target directory to unpack the archive inside the image root tree. The optional `bootinclude` attribute additionally unpacks the archive into the {kiwi} initrd. ~~~~~~~~~~~~~~~~~~ .. code:: xml The `ignore` element instructs the used package manager to ignore the given package name at installation time. Please note whether or not the package can be ignored is up to the package manager. Packages that are hard-required by other packages in the install procedure cannot be ignored, and the package manager will simply ignore the request. ~~~~~~~~~~~~~~~~~~~ .. code:: xml The `product` element instructs the used package manager to install the given product. What the installation of a product means is up to the package manager and is also distribution-specific. This feature currently only works on SUSE-based distributions. .. _sec.users: -------- Set up image users. .. code:: xml The optional `users` element contains the user setup {kiwi} should create in the system. The optional `arch` attribute can be used to limit the user's setup to the host architecture from which {kiwi} is called. At least one user child element must be specified as part of the `users` element. Multiple user elements may be specified. Each `user` element represents a specific user that is added or modified. The following attributes are mandatory: name="name": the UNIX username. password="string" The password for this user account. It can be provided either in cleartext form or encrypted. An encrypted password can be created using `openssl` as follows: .. code:: $ openssl passwd -1 -salt xyz PASSWORD It is also possible to specify the password as a non-encrypted string by using the `pwdformat` attribute and setting its value to `plain`. {kiwi} will then encrypt the password prior to the user being added to the system. .. warning:: plain text passwords We do not recommend plain passwords, as they will be readable in the image configuration in plain text. All specified users and groups will be created if they do not already exist. The defined users will be part of the group(s) specified with the `groups` attribute or belong to the default group as configured in the system. If specified, the first entry in the groups list is used as the login group. Additionally, the following optional attributes can be specified: home="path": The path to the user's home directory. groups="group_a,group_b,group_c:id": A comma-separated list of UNIX groups. The first element of the list is used as the user's primary group. The remaining elements are appended to the user's supplementary groups. When no groups are assigned, then the system's default primary group will be used. If a group should be of a specific group ID, it can be appended to the name, separated by a colon. .. note:: Group IDs can only be set for groups that do not yet exist at the time when {kiwi} creates them. A check is made if the desired group is already present, and if it exists, the user will become a member of that group, but any given group ID from the {kiwi} configuration will **not** be taken into account. Usually, all standard system groups are affected by this behavior because they are provided by the OS itself. Thus, it's by intention not possible to overwrite the group ID of an existing group. id="number": The numeric user ID of this account. pwdformat="plain|encrypted": The format in which `password` is provided. The default, if not specified, is `encrypted`. realname="string": The full name of the user, stored in the GECOS field. shell="path": The login shell for this account. The `users` element itself may be provided multiple times and accepts the following optional attributes to restrict a whole group of accounts: arch="arch_name[,arch_name]" Create the users of this section only when building on one of the listed host architectures. profiles="profile_a,profile_b" Create the users of this section only when one of the listed build profiles is active (see :ref:`sec.profiles`). .. _sec.drivers: --------- .. code:: xml Lists kernel driver modules to include in the {kiwi} initrd. Each `file` child element takes a `name` attribute holding a path relative to the kernel module directory (shell globs are supported). The `drivers` element may be provided multiple times and accepts the following optional attribute: profiles="profile_a,profile_b" Include the drivers of this section only when one of the listed build profiles is active (see :ref:`sec.profiles`). .. _sec.strip: ------- .. code:: xml Lists files to remove from, or to keep in, the image root tree. The mandatory `type` attribute selects the meaning of the `file` children: type="delete" A list of custom files and directories to delete. type="tools" A list of file names in the Linux `bin` and `sbin` directories to keep; everything else is removed. type="libs" A list of file names in the Linux library directories to keep; everything else is removed. The `strip` element may be supplied multiple times and accepts the following optional attribute: profiles="profile_a,profile_b" Apply the strip section only when one of the listed build profiles is active (see :ref:`sec.profiles`). .. _sec.extension: ----------- An optional container for namespaced XML belonging to a {kiwi} plugin. Each XML namespace may appear at most once, and its content is validated against an external RELAX NG schema resolved through the XML catalog. See :doc:`../contributing/schema_extensions` for how to register and consume one. .. _sec.profiles: ----------- Manage image namespace(s). .. code:: xml The optional profiles section lets you maintain one image description while allowing for variation of other sections that are included. A separate profile element must be specified for each variation. The profile child element specifies an alias name used to mark sections as belonging to a profile, and a short description explaining what this profile does. Both `name` and `description` are **mandatory**. Two further attributes are optional: import="true|false" Select this profile automatically when no profile was given on the command line. If any profile is passed via `--profile`, `import` is ignored entirely and only the profiles named on the command line plus their transitive `requires` are used. arch="arch_name" Limit this profile to the listed host architectures. A profile can require other profiles, and the required profiles will be evaluated in the order of their declaration. The optional arch attribute can be used to limit the profile and required profiles to a certain architecture. For example: .. code:: xml For example, to mark a set of packages as belonging to a profile, simply annotate them with the profiles attribute, as shown below: .. code:: xml It is also possible to mark sections as belonging to multiple profiles by separating the names in the profiles attribute with a comma: .. code:: xml The `profiles` attribute is supported on the following elements: * :ref:`sec.certificates` * :ref:`sec.preferences` * :ref:`sec.users` * :ref:`sec.drivers` * :ref:`sec.strip` * :ref:`sec.repository` * :ref:`sec.registry` * :ref:`sec.packages` Child elements, for example ``, `` or ``, do not support the `profiles` attribute. To make them profile specific, place them into a parent section that carries the `profiles` attribute. If a section tag does not have a profiles attribute, it is globally present in the configuration. If global sections and profiled sections contain the same sub-sections, the profiled sections will overwrite the global sections in the order of the provided profiles. For a better overview of the result configuration when profiles are used we recommend to put data that applies in any case to non profiled (global) sections and only extend those global sections with profiled data. For example: .. code:: xml 1.2.3 zypper The above example configures two versions of the same oem type. One builds a disk in qcow2 format the other builds a disk in vmdk format. The global preferences section without a profile assigned will be used in any case and defines those preferences settings that are common to any build process. A user can select both profiles at a time but that will result in building the disk format that is specified last because one is overwriting the other. Use of one or more profile(s) during image generation is triggered by the use of the ``--profile`` command line argument. Multiple profiles can be selected by passing this option multiple times. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7355366 kiwi-11.1.8/doc/source/image_description.rst0000644000000000000000000001627615273675667016023 0ustar00.. _image-description: Image Description ================= KIWI builds images from a description directory. The directory describes what packages to install, how to configure the target system, and which image types should be created from the prepared root tree. .. contents:: On this page :local: :depth: 2 Description Directory Layout ---------------------------- A description directory usually contains these files: :file:`config.xml` or :file:`*.kiwi` The main image description. It defines metadata, repositories, packages, users, image types, and optional profile-specific variants. :file:`root/` or :file:`root.tar.gz` Overlay content copied into the prepared root tree after package installation. :file:`config.sh` An optional shell hook executed near the end of the prepare step. :file:`images.sh` An optional shell hook executed at the beginning of the create step. :file:`config-cdroot.tar[.*]` Optional additional files for ISO-style images, such as licenses or offline documentation. Referenced archives, drivers, certificate files, and include files Additional resources referenced from the image description itself. Minimal Example --------------- The following description is enough to prepare a root tree and create a simple image from it: .. code:: xml Example Author example@example.invalid Small example image 1.0.0 zypper Top-Level Schema Layout ----------------------- The :file:`kiwi/schema/kiwi.rnc` schema organizes the document below the :ref:`sec.image` root element in this order: .. list-table:: Top-level sections :widths: 18 34 32 16 :header-rows: 1 * - Section - Purpose - Main attributes and children - Reference * - ```` - Root node of the description. - Required: ``name``, ``schemaversion``. Optional: ``displayname``, ``id``. - :ref:`sec.image` * - ```` - Insert the inner content of another XML description file. - ``from`` URI attribute. - :ref:`sec.include` * - ```` - Import additional CA certificates during the build. - ``target_distribution`` and optional ``profiles`` with one or more ```` entries. - :ref:`sec.certificates` * - ```` - Define human-readable identity and ownership. - ``type`` plus ````, ````, ````, and optional ````. - :ref:`sec.description` * - ```` - Define versions, package manager behavior, and one or more image types. - Optional ``arch`` and ``profiles`` filters; includes ````, ````, ````, locale, timezone, themes, and release settings. - :ref:`sec.preferences` * - ```` - Group conditional variants of the same description. - ```` entries with ``name``, ``description``, ``arch``, ``import``, and optional ````. - :ref:`sec.profiles` * - ```` - Create or adjust users and groups in the image. - Optional ``arch`` and ``profiles`` filters; user records, group membership, home, shell, and password data. - :ref:`sec.users` * - ```` - Add driver files to the image description. - Optional ``profiles`` filter; one or more ```` references. - :ref:`sec.drivers` * - ```` - Remove files, libraries, or toolchains from the prepared root tree. - Required ``type`` (``delete``, ``tools``, or ``libs``) and optional ``profiles`` with one or more ```` entries. - :ref:`sec.strip` * - ```` - Point KIWI to package sources. - Repository type, optional ``arch`` and ``profiles`` filters, source type, priority, GPG behavior, credentials, and source URLs. - :ref:`sec.repository` * - ```` - Define containers to fetch from a registry into the image. - Required ``source`` and ``backend``, optional ``arch`` and ``profiles`` with one or more ```` entries. - :ref:`sec.registry` * - ```` - Define what KIWI installs, deletes, or imports. - ``type``, optional ``profiles``, ``patternType``, ``bootstrap_package`` with package, collection, product, file, ignore, and archive entries. - :ref:`sec.packages` * - ```` - Attach custom XML namespaces validated outside the built-in schema. - Arbitrary namespaced content. - :ref:`sec.extension` What to Configure First ----------------------- When you create or review an image description, work in this order: #. Start with :ref:`sec.description` so the image has clear ownership and a purpose. #. Add at least one :ref:`sec.preferences` block with a version, package manager, and one or more :ref:`type definitions `. #. Add the :ref:`sec.repository` entries that provide the packages needed for the build. #. Add one or more :ref:`sec.packages` sections to populate the root tree. #. Add optional sections such as :ref:`sec.users`, :ref:`sec.profiles`, or :ref:`sec.certificates` only when the use case needs them. Common Patterns --------------- Use profiles when one description should build multiple variants. .. code:: xml 1.0.0 zypper 1.0.0 zypper Use multiple package sections to separate bootstrap packages from the final image content. .. code:: xml Complete Schema Reference ------------------------- The exhaustive element and attribute reference is documented in :doc:`image_description/elements`. That reference follows the schema in :file:`kiwi/schema/kiwi.rnc` and covers: * every top-level section allowed below ````; * all common and type-specific attributes of ````; * repository, package, user, profile, bootloader, storage, and container substructures; and * examples for the most frequently used sections. Use this page as the guide to the structure of a description directory and use :doc:`image_description/elements` as the schema-backed lookup for every valid section and attribute. .. toctree:: :maxdepth: 1 image_description/elements.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/image_types_and_results.rst0000644000000000000000000002440415273675667017237 0ustar00.. _image_types: Image Types =========== .. note:: Before building an image with {kiwi}, it's important to understand the different image types and their meaning. This document provides an overview of the supported {kiwi} image types, their results, and some words about the environment to run the build. ISO Hybrid Live Image An ISO image that can be dumped on a CD/DVD or USB stick and boots from this media without interfering with other system storage components. A useful pocket system for testing, demo, and debugging purposes. For further details, refer to :ref:`hybrid_iso`. Virtual Disk Image An image representing the system disk, useful for cloud frameworks like Amazon EC2, Google Compute Engine, or Microsoft Azure. For further details, refer to :ref:`simple_disk`. OEM Expandable Disk Image An image representing an expandable system disk. This means after deployment, the system can resize itself to the new disk geometry. The resize operation is configurable as part of the image description, and an installation image for CD/DVD, USB stick, and network deployment can be created in addition. For further details, refer to: :ref:`expandable_disk`. Docker Container Image An archive image suitable for the Docker container engine. The image can be loaded via the `docker load` command and works within the scope of the container engine. For further details, refer to: :ref:`building_container_build`. WSL Container Image An archive image suitable for the Windows Subsystem for Linux container engine. The image can be loaded from a Windows System that has support for WSL activated. For further details, refer to: :ref:`building_wsl_build`. KIS Root Filesystem Image An optional root filesystem image associated with a kernel and initrd. The use case for this component image type is highly customizable. Many different deployment strategies are possible. For further details, refer to: :ref:`kis`. AWS Nitro Enclave An initrd-based image using the `eif` binary format. The image is expected to be used in the AWS Nitro Enclave system or for testing in QEMU. For further details, refer to: :ref:`eif`. Image Results ------------- {kiwi} execution results in an appliance image after a successful run of the :ref:`kiwi_system_build` or :ref:`kiwi_system_create` command. The result is the image binary plus some additional metadata files that are needed for image deployment and/or exist for informative reasons. By default, the output files follow this naming convention: `.\-\.\` where `` is the name stated in the :ref:`image-description` as an attribute of the :ref:`sec.image` element. The `` is the CPU architecture used for the build, `` is the image version defined in the :ref:`` element of the image description, and the `` is dependent on the image type and its definition. Any {kiwi} appliance build results in, at least, the following output files: 1. The image binary, `.\-\.\`: This is the file containing the actual image binary. Depending on the image type and its definition, it can be a virtual disk image file, an ISO image, a tarball, etc. 2. The `.-.packages` file: This file includes a sorted list of the packages that are included in the image. In fact, this is a normalized dump of the package manager database. It follows the following CSV format, where each line is represented by: `|\|\|\|\|\|\` The values represented here are mainly based on RPM packages' metadata. Other package managers may not provide all of these values. In such cases, the format is the same, and the fields that cannot be provided are set as `None`. This list can be used to track changes across multiple builds of the same image description over time by diffing the packages installed. 3. The `.-.verified` file: This file is the output of a verification done by the package manager against the package database. More specifically, it is the output of the `rpm` verification process or `dpkg` verification, depending on the packaging technology selected for the image. In both cases, the output follows the RPM verification syntax. This provides an overview of all package statuses right before any boot of the image. Depending on the image type, the following output files exist: image="tbz" For this image type, the result is mainly a root tree packed in a tarball: - **root archive**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.tar.xz` image="btrfs|ext2|ext3|ext4|squashfs|erofs|xfs" The image root tree data is packed into a filesystem image of the given type; hence, the result for an `ext4` image would be: - **filesystem image**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.ext4` image="iso" The image result is an ISO file: - **live image**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.iso` image="oem" An image representing an expandable disk image. {kiwi} can also produce an installation ISO for this disk image by setting `installiso="true"` in the :ref:`\\` section or a tarball, including the artifacts for a network deployment, by setting `installpxe="true"`. For further details, see :ref:`expandable_disk`. The results for `oem` can be: - **disk image**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.raw` - **installation image (optional)**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.install.iso` - **installation pxe archive (optional)**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.install.tar` The disk image can also be provided in one of the various virtual disk formats, which can be specified in the `format` attribute of the :ref:`\\` section. For further details, see :ref:`simple_disk`. The result for e.g., `format="qcow2"` would be: - **disk image**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.qcow2` instead of the `.raw` default disk format. image="docker" An archive image suitable for the Docker container engine. The result is a loadable (`docker load -i `) tarball: - **container**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.docker.tar.xz` image="oci" An archive image that builds a container matching the OCI (Open Container Initiative) standard. The result is a tarball matching OCI standards: - **container**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.oci.tar.xz` image="appx" An archive image suitable for the Windows Subsystem for Linux container engine. The result is an `appx` binary file: - **container**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.appx` image="wsl" An archive image suitable for the Windows Subsystem for Linux container engine >= v2.4.4. The result is a gzip-compressed tar archive with the `.wsl` extension: - **container**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.wsl` image="kis" An optional root filesystem image associated with a kernel and initrd. All three binaries are packed in a tarball. See :ref:`kis` for further details about the kis archive: - **kis archive**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.tar.xz` image="enclave" An initrd-based image in the `eif` binary format for the AWS Nitro Enclave system. See :ref:`eif` for further details: - **enclave image**: :file:`{exc_image_base_name}.x86_64-{exc_image_version}.eif` Image Bundle Format ------------------- The result files as mentioned above are used in the {kiwi} result bundler. The `kiwi-ng result bundle` command can be used to copy or package the mandatory image files to create a customer release. In this process, it's possible to apply a specific name pattern suitable for the requirements of the release. A typical result bundle call can look like the following: .. code:: bash $ kiwi-ng result bundle --target-dir /path/to/image/build_result \ --bundle-dir=/path/to/image/release_result \ --id=release_identifier In this call, and depending on the image type, the required files as they exist in :file:`/path/to/image/build_result` are copied to :file:`/path/to/image/release_result/`. The only modification to the file names is the `--id` information, which is appended with a `-` at the end of the version substring. If we take :file:`{exc_image_base_name}.x86_64-{exc_image_version}.iso` as an example. This file would be bundled as :file:`{exc_image_base_name}.x86_64-{exc_image_version}-release_identifier.iso`. Depending on the use case and the customer requirements, this naming schema and the default way the kiwi bundler processes the result files is not appropriate. To allow for a more flexible naming schema when bundling results, {kiwi} allows you to specify a bundle_format per type, like in the following example: .. code:: xml The specified `name_pattern` is used as the base name for the image files the bundler uses. As part of the `name_pattern`, the following placeholders, which are replaced by their real value, can be used: %N Turns into the contents of the `name` attribute of the `` section. %P Turns into the profile name used at build time of the image. If multiple profiles were used to build the image, the result name consists of the individual profile names concatenated by a `_` in the order of their specification in the image description and/or the command line. %A Turns into the architecture name at build time of the image. Arch names are taken from Python's `platform.machine` information. %I Turns into the identifier name given via the `--id` option at the call time of the bundler. %T Turns into the contents of the `image` attribute of the `` section. %M Turns into the major number of the `` section. %m Turns into the minor number of the `` section. %p Turns into the patch number of the `` section. %v Turns into the version text of the `` section. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/index.rst0000644000000000000000000001524215273675667013435 0ustar00.. kiwi documentation master file .. only:: suse .. include:: suse_metadata.rst Welcome to KIWI NG ================== **Build Linux images from one description in a predictable two-step workflow.** KIWI NG is a command-line image builder for Linux distributions. It creates bootable, reproducible images for virtual machines, cloud platforms, removable media, containers, and specialized deployment targets. .. note:: This documentation covers {kiwi-product} |version|. .. toctree:: :maxdepth: 1 :hidden: overview installation quickstart concept_and_workflow commands image_description build_methods example_use_cases image_types_and_results troubleshooting contributing Build Architecture ------------------ .. figure:: .images/architecture.svg :align: center :alt: KIWI NG needs an image description and repositories with packages. The prepare step creates a new root tree, and the create step turns it into one or more images. KIWI NG builds from an image description and the repositories that provide the packages for it. In the *prepare* step, KIWI creates a new root tree. In the *create* step, KIWI converts that prepared root tree into one or more image artifacts. Diagram description: The diagram shows two required inputs: an image description and the repositories that contain the packages used for the build. Both feed the *prepare* step, which creates a new root tree. The *create* step then turns that prepared root tree into one or more images such as disk, ISO, or container artifacts. The image description directory typically contains: * :file:`config.xml` or a :file:`*.kiwi` file with the image definition. * Repository definitions that point to the package sources used during the build. * Optional overlays, archives, and shell hooks that customize the prepared root tree or the final image artifacts. Why KIWI? --------- * **One source, multiple outputs**: prepare a root filesystem once and create one or more image formats from it. * **Declarative image definitions**: keep image metadata, repositories, packages, users, and storage settings in a versioned description. * **Broad target support**: build for bare metal, virtual machines, cloud environments, live media, and containers. * **Distribution-aware builds**: work with the package managers and repository layouts used by the supported Linux distributions. * **Controlled customization**: extend builds with overlays, archives, and scripts when the schema alone is not enough. .. sidebar:: Links * `GitHub Sources `__ * `GitHub Releases `__ * `RPM Packages `__ * `Build Tests TumbleWeed(x86) `__ * `Build Tests TumbleWeed(arm) `__ * `Build Tests TumbleWeed(s390) `__ * `Build Tests TumbleWeed(ppc64le) `__ * `Build Tests RawHide(x86) `__ * `Build Tests RawHide(s390) `__ * `Build Tests Leap(x86) `__ * `Build Tests Fedora(x86) `__ * `Build Tests Fedora(arm) `__ * `Build Tests CentOS(x86) `__ * `Build Tests Ubuntu(x86) `__ * `Build Tests Ubuntu(arm) `__ * `Build Tests Debian(x86) `__ * `Build Tests ArchLinux(x86) `__ Get Started ----------- Start with :doc:`quickstart` for a first build, :doc:`concept_and_workflow` for workflow details, and :doc:`image_description` for the schema-backed reference for description files. System Requirements ------------------- To build images with {kiwi}, you need: * A recent Linux distribution. See :ref:`supported-distributions` for the tested targets. * Enough free disk space for the build root, caches, and resulting images. A minimum of 15 GB is recommended. * Python 3.9 or newer. * Git to clone image descriptions and example repositories. * Optionally, a virtualization environment such as QEMU to boot and inspect the resulting images. The project officially tests and supports the following distributions: ============================================================ ================= Distribution Version ============================================================ ================= `Fedora Linux `_ 43+ `CentOS Stream `_ 10+ `CentOS Stream Hyperscale `_ 10+ `openSUSE Tumbleweed `_ after April 2025 `openSUSE Leap 16.0+ `_ 16.0+ `Canonical Ubuntu Linux `_ 26.04+ `Debian GNU/Linux `_ 13+ `Arch Linux `_ after April 2025 ============================================================ ================= Contact ------- * `Mailing list `__ The `kiwi-images` group is an open group, and anyone can `subscribe `__, even if you do not have a Google account. * `Matrix `__ An open network for secure, decentralized communication. Please find the ``#kiwi:matrix.org`` room via `Matrix `__ on the web or by using the supported `clients `__. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/installation.rst0000644000000000000000000000567115273675667015034 0ustar00.. _kiwi-installation: Installation ============ .. note:: This document describes how to install {kiwi}. Apart from the preferred method of installing {kiwi} via a distribution package manager, it is also available on `pypi `__ and can be installed using Python's package manager, pip, as follows: .. code:: shell-session $ sudo pip install kiwi .. _installation-from-obs: Installation from OBS --------------------- The most up-to-date packages of {kiwi} can be found on the Open Build Service in the `Virtualization:Appliances:Builder `__ project. To install {kiwi}, follow these steps: 1. Open the URL https://download.opensuse.org/repositories/Virtualization:/Appliances:/Builder in your browser. 2. Right-click on the link of your preferred operating system and copy the URL. In Firefox, it is the menu :menuselection:`Copy link address`. 3. Insert the copied URL from the last step into your shell. The `DIST` placeholder contains the respective distribution. Use :command:`zypper addrepo` to add it to the list of your repositories: .. code:: shell-session $ sudo zypper addrepo \ http://download.opensuse.org/repositories/Virtualization:/Appliances:/Builder/ \ kiwi-appliance-builder 4. Install {kiwi}: .. code:: shell-session $ sudo zypper --gpg-auto-import-keys install python3-kiwi .. note:: Other Distributions If your distribution is not using `zypper`, please use your package manager's appropriate command instead. For `dnf`, as an example, that is: .. code:: shell-session $ sudo dnf config-manager \ --add-repo https://download.opensuse.org/repositories/Virtualization:/Appliances:/Builder//Virtualization:Appliances:Builder.repo $ sudo dnf install python3-kiwi Installation from Distribution Repositories ------------------------------------------- Some Linux distributions ship {kiwi} in their official repositories. These include **openSUSE** and **Fedora** since version 28. Note that these packages tend not to be as up-to-date as the packages from OBS, so some features described here might not exist yet. .. note:: There are many packages that contain the name *kiwi* in their name; some of these are not even Python packages. Please double-check the package's description to see whether it is actually the {kiwi} Appliance builder before installing it. Please also note that, depending on how the responsible packager has integrated {kiwi} into the distribution, the install name can be different from the instructions provided in: :ref:`installation-from-obs`. To install {kiwi} for the desired distribution, run the following command: Leap/Tumbleweed: .. code:: shell-session $ sudo zypper install python3-kiwi Fedora/Rawhide: .. code:: shell-session $ sudo dnf install kiwi-cli ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/overview/workflow.rst0000644000000000000000000001010315273675667016035 0ustar00Basic Workflow ============== .. note:: **Abstract** The installation of a Linux system generally occurs by booting the target system from an installation source such as an installation CD/DVD, a live CD/DVD, or a network boot environment (PXE). The installation process is often driven by an installer that interacts with the user to collect information about the installation. This information generally includes the *software to be installed*, the *timezone*, system *user* data, and other information. Once all the information is collected, the installer installs the software onto the target system using packages from the available software sources (repositories). After the installation is complete, the system usually reboots and enters a configuration procedure upon startup. The configuration may be fully automatic, or it may include user interaction. This description applies for version |version|. A system image (usually called an "image") is a *complete installation* of a Linux system within a file. The image represents an operating system and, optionally, contains the "final" configuration. The behavior of the image upon deployment varies depending on the image type and the image configuration since {kiwi} allows you to completely customize the initial startup behavior of the image. Among others, this includes images that: * can be deployed inside an existing virtual environment without requiring configuration at startup. * automatically configure themselves in a known target environment. * prompt the user for an interactive system configuration. The image creation process with {kiwi} is automated and does not require any user interaction. The information required for the image creation process is provided by the primary configuration file named :file:`config.xml`. This file is validated against the schema documented in: :ref:`image-description`. In addition, the image can optionally be customized using the :file:`config.sh` and :file:`images.sh` scripts and by using an *overlay tree (directory)* called :file:`root`. See the :ref:`description_components` section for further details. .. note:: Previous Knowledge This documentation assumes that you are familiar with the general concepts of Linux, including the boot process and distribution concepts such as package management. .. _description_components: Components of an Image Description ---------------------------------- A {kiwi} image description can be composed of several parts. The main part is the {kiwi} description file itself (named :file:`config.xml` or an arbitrary name plus the :file:`*.kiwi` extension). The configuration XML is the only required component; others are optional. These are the optional components of an image description: #. Shell scripts There are optional hook scripts to influence the build process at several stages. For details about the different script break points see :ref:`User-Defined Scripts ` #. Overlay tree directory The *overlay tree* is a folder (called :file:`root`) or a tarball file (called :file:`root.tar.gz`) that contains files and directories that will be copied to the target image build tree during the :ref:`prepare step `. It is applied after all the packages included in the :file:`config.xml` file have been installed. Any already present file is overwritten. #. CD root user data For live ISO images and install ISO images, an optional cdroot archive is supported. This is a tar archive matching the name :file:`config-cdroot.tar[.compression_postfix]`. If present, it will be unpacked as user data on the ISO image. This is mostly useful to add, e.g., license files or user documentation on the CD/DVD, which can be read directly without booting from the media. #. Archives included in the :file:`config.xml` file. The archives that are included in the `` using the `` subsection: .. code:: xml ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/overview.rst0000644000000000000000000000544315273675667014176 0ustar00.. _db_overview: Overview ======== .. note:: **Abstract** This document provides a conceptual overview of the steps of creating an image with {kiwi}. It also explains the terminology regarding the concept and process when building system images with {kiwi} |version|. .. toctree:: :maxdepth: 1 overview/workflow Conceptual Overview ------------------- A system image (usually called an "image") is a *complete installation* of a Linux system within a file. The image represents an operating system and, optionally, contains the "final" configuration. {kiwi} creates images in a two-step process: 1. The first step, the *prepare operation*, generates a so-called *unpacked image tree* (directory) using the information provided in the image description. 2. The second step, the *create operation*, creates the *packed image* or *image* in the specified format based on the unpacked image and the information provided in the configuration file. The image creation process with {kiwi} is automated and does not require any user interaction. The information required for the image creation process is provided by the image description. Terminology ----------- Appliance An appliance is a ready-to-use image of an operating system, including a pre-configured application for a specific use case. The appliance is provided as an image file and needs to be deployed to or activated in the target system or service. Image The result of a {kiwi} build process. Image Description A specification to define an appliance. The image description is a collection of human-readable files in a directory. At least one XML file, :file:`config.xml` or :file:`*.kiwi`, is required. In addition, there may be other files, like scripts or configuration data. These can be used to customize certain parts of either the {kiwi} build process or the initial startup behavior of the image. Overlay Files A directory structure with files and subdirectories stored as part of the Image Description. This directory structure is packaged as a file, :file:`root.tar.gz`, or stored inside a directory named :file:`root`. Additional overlay directories for selected profiles are also supported and are taken into account if the directory name matches the name of the profile. The content of each of the directory structures is copied on top of the existing filesystem (overlayed) of the appliance root. This also includes permissions and attributes as a supplement. {kiwi} An OS appliance builder. Virtualization Technology Software-simulated computer hardware. A virtual machine acts like a real computer but is separated from the physical hardware. Within this documentation, the QEMU virtualization system is used. Another popular alternative is VirtualBox. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/quickstart.rst0000644000000000000000000000445715273675667014526 0ustar00.. _quick-start: Quick Start =========== .. note:: **Abstract** This document describes how to start working with {kiwi}, an OS appliance builder. This description applies to version |version|. Before you start ---------------- 1. Install {kiwi} first, either via your distribution's package manager (see :ref:`kiwi-installation`) or via: .. code:: bash $ sudo pip install kiwi 2. Clone the {kiwi} repository containing example appliances: .. code:: bash $ git clone https://github.com/OSInside/kiwi .. note:: Please see the :ref:`troubleshooting` chapter and/or our `Contact` info on the main page if the following procedure causes any trouble. Choose a First Image -------------------- Find example appliance descriptions in the {kiwi} repository checkout as follows: .. code:: bash $ tree -L 3 kiwi/build-tests Take a look at which images are available in the example appliances repository and select one that matches your desired image as closely as possible. Or just use the one given in the examples below. Build your First Image ---------------------- Your first image will be a simple system disk image that can run in any full virtualization system like QEMU. Invoke the following {kiwi} command in order to build it: .. code:: bash $ sudo kiwi-ng system build \ --description kiwi/build-tests/{exc_description_disk} \ --set-repo {exc_repo_leap} \ --target-dir /tmp/myimage The resulting image will be placed into the folder :file:`/tmp/myimage` with the suffix :file:`.raw`. If you don't wish to create an openSUSE Leap {exc_os_version} image, substitute the folder following the ``--description`` option with another folder that contains the image description which you selected. Run your Image -------------- Running an image actually means booting the operating system. To do that, attach the disk image to a virtual system. In this example, we use QEMU and boot it as follows: .. code:: bash $ sudo qemu -boot c \ -drive file=/tmp/myimage/{exc_image_base_name_disk}.x86_64-{exc_image_version}.raw,format=raw,if=virtio \ -m 4096 -serial stdio Tweak and Customize your Image ------------------------------ Now that you have successfully built and started your first image, you can start tweaking it to match your needs. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/spelling_wordlist.txt0000644000000000000000000000026415273675667016077 0ustar00KIWI NG QEMU OBS zypper dnf microdnf apt apk dracut initrd snapper qcow2 vmdk vhdx PXE OCI Vagrant VMware Rawhide Tumbleweed openSUSE SUSE CentOS Ubuntu Debian Archlinux LoongArch ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/suse_metadata.rst0000644000000000000000000000040715273675667015142 0ustar00:meta: :description: Your flexible operating system image and appliance builder :keyword: KIWI :social-descr: The SEO description :seo-title: KIWI - Appliance Builder Next Generation :task: Deployment, Installation :series: Products & Solutions ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/troubleshooting/architectures.rst0000644000000000000000000000353715273675667020426 0ustar00Architectures ------------- With regards to the information in :ref:`supported-distributions`, one requirement between the build host and the image, when it comes to architecture support, is that the image architecture should match the build host architecture. Cross-arch building would require any core tool that is used to build an image to be cross-arch capable. To patch, e.g., an x86_64 system such that it can build an aarch64 image would require some work on binutils and hacks as well as performance tweaks, which is all not worth the effort and can still lead to broken results. Thus, we recommend providing native systems for the target architecture and building there. One possible alternative is to use the {kiwi} `boxbuild` command (see :ref:`self_contained`), together with a box created for the desired architecture. However, keep in mind the performance problems when running a VM of a different architecture. The majority of the image builds are based on the x86 architecture. As mentioned, {kiwi} also supports other architectures, as shown in the table below: .. table:: :align: left +--------------+---------------------+ | Architecture | Supported | +==============+=====================+ | x86_64 | yes | +--------------+---------------------+ | ix86 | yes **note:distro** | +--------------+---------------------+ | s390/s390x | yes **note:distro** | +--------------+---------------------+ | arm/aarch64 | yes **note:distro** | +--------------+---------------------+ | ppc64 | yes **note:distro** | +--------------+---------------------+ .. admonition:: distro The support status for an architecture depends on the distribution. If the distribution does not build its packages for the desired architecture, {kiwi} will not be able to build an image for it. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.737635 kiwi-11.1.8/doc/source/troubleshooting/boxbuild_tweaks.rst0000644000000000000000000000467615273675667020754 0ustar00Boxbuild Tweaks =============== .. note:: Abstract This document describes a few ways to modify box-build VMs for testing and debugging. Increase Box Build Image Size ----------------------------- In particularly large builds, you may find that the upstream build boxes are not quite large enough and fail to build during the final few steps. While it is a bit of a kludge, it is possible to increase the size of the build box. To do so, follow these steps: .. note:: For this example, we will assume the box increase in question is an Ubuntu box, located in `~/.kiwi_boxes/ubuntu`. 1. While the VM is offline, locate the VM you want to modify and resize with `qemu-img`. Here, we will increase the size by 20G. The VM will have to be told to utilize this space in the following steps. .. code:: bash $ qemu-img resize Ubuntu-Box.x86_64-System.qcow2 +20G 2. When relaunching your `kiwi-ng` box build, make sure you use the `--no-snapshot` and `--box-debug` options within your build command/script. Example: .. code:: bash $ kiwi-ng --debug --profile="Disk" --type oem system boxbuild --no-snapshot \ --box-memory=32G --box-smp-cpus=16 --box-debug --box ubuntu kiwi \ --description ./ubuntu-jammy --target-dir /build/kiwi/outputs/ 3. When the build fails and drops you into the VM console, you will need to extend the partition of the VM's rootfs, then resize it with `resize2fs`. In this example, `parted` was used, and the partition in question was `/dev/vda3`. .. code:: bash $ parted # You can run parted print to check for relevant partitions if needed. (parted) $ print (parted) $ resizepart 3 100% # Exit from parted (parted) $ quit # Run resize2fs to grow the filesystem to fill the space $ resize2fs /dev/vda3 4. From this point, depending on where your build failed, it may be possible to continue your build from inside the box using the existing 9p mount points defined by your build command. Using the command above as an example, the build results within the box are stored below `/result`, and it's possible to run: .. code:: bash $ kiwi-ng --profile="Disk" --type oem system create \ --root=/result/build/image-root/ --target-dir=/result 5. If the rebuild from within was successful, you can copy the files from `/result` to `/bundle` from within the VM, where `/bundle` maps to your `target-dir` on the host. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.737635 kiwi-11.1.8/doc/source/troubleshooting/buildhost_constraints.rst0000644000000000000000000001130715273675667022177 0ustar00.. _supported-distributions: Build Host Constraints ====================== For building images, a host system is required that runs the build process. Tools to create the image are used from that host, and this creates an indirect dependency on the target image. For example, building an Ubuntu image requires the apt and dpkg tools and metadata to be available and functional on the host to build an Ubuntu image. There are many more of those host vs. image dependencies, and not all of them can be resolved in a clear and clean way. The most compatible environment is provided if the build host is of the same distribution as the target image. In other cases, our recommendation is that the build host is of the same distribution as the target and near to the major version (+-1) compared to the target. Such an environment can be found in: * The Open Build Service `OBS `__. * The {kiwi} `boxbuild` command: :ref:`self_contained` In general, our goal is to support any major distribution with {kiwi}. However, for building images, we rely on core tools that are not under our control. Also, several design aspects of distributions like **secure boot** and working with **upstream projects** are different and not influenced by us. There are many side effects that can be annoying, especially if the build host is not of the same distribution vendor as the image target. Package Manager Behavior ------------------------ One of the key requirements for {kiwi} is that the target distribution consists of a collection of software packages provided through software repositories. As of today, this applies to every Linux distribution in some shape or form. For the installation of these software packages, there are tools available called `package manager`, and {kiwi} implements an API for a variety of package managers to support the different Linux distributions. In {kiwi}, the package manager tools are called in a way that allows for a non-interactive installation of the packages specified in the image description. However, {kiwi} does not intentionally configure other features of the package manager to keep their distribution default behavior as much as possible. There are many options that can be set for a package manager to behave differently. This part of the troubleshooting exists to inform about the most common surprises or unexpected behavior of certain package managers and solutions on how to address them. .. note:: All of the following information applies to package manager calls performed inside of the image root tree. Meaning, `after` the image bootstrap phase. During the image bootstrap phase, all package manager calls happen on the build host, and their behavior can only be influenced by changing the setup of the build host. An isolated build environment is required to address issues in this area. See :ref:`self_contained` for details. zypper: modalias matching ~~~~~~~~~~~~~~~~~~~~~~~~~ A module alias is an identifier that exists if the respective hardware was found by the kernel on this system. The package manager can use this information to match package(s) that holds a reference to the alias name and installs it automatically. For example: Automatically install a driver package if the respective hardware is present. In zypper, this behavior is by default enabled. When building an image, this feature can be unwanted and lead to interesting side effects. As it's required to build the image on some host, there is also some hardware available during the build. This must not necessarily be the same or compatible hardware the image is expected to run on later. For example, the build host has a NVIDIA graphics card. An image build process would install the NVIDIA driver package due to the modalias match. The image is expected to run on a system without an NVIDIA card. The build process would install unneeded software. To switch off modalias matching in zypper, follow these steps: 1. Add a `post_bootstrap.sh` hook script to your image description with the following code: .. code:: bash echo 'ZYPP_MODALIAS_SYSFS=""' > .kiwi.package_manager.env 2. Optionally, add a `config.sh` hook script to your image description with the following code: .. code:: bash rm .kiwi.package_manager.env This will delete the custom environment file so that it does not appear in the final image. 3. Rebuild your image. The `.kiwi.package_manager.env` environment file is used by kiwi and added to the execution environment of the package manager. The `ZYPP_MODALIAS_SYSFS` variable can be used to switch off the modalias matching. For more details on zypper settings, please refer to: https://doc.opensuse.org/projects/libzypp/HEAD/zypp-envars.html ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.737635 kiwi-11.1.8/doc/source/troubleshooting/filesystems.rst0000644000000000000000000000557715273675667020136 0ustar00Incompatible Filesystem Settings on Host vs. Image ================================================== .. note:: **Abstract** This page provides further information on how to solve image boot problems if the filesystem toolchain on the image build host is incompatible with the image's target distribution. When {kiwi} builds an image that requests the creation of a filesystem, the required filesystem creation tool, for example, `mkfs.xfs`, is called from the host on which {kiwi} is running. It is expected that the generated filesystem is compatible with the image's target distribution. This expectation is not always correct and depends on the compatibility of the filesystem default settings between build host and image target. We know about the following settings that cause an incompatible filesystem, which will not be able to be used on boot: Ext[2,3,4] Check `/etc/mke2fs.conf` on the build host and make sure the configured `inode_size` is the same as the setting used for the target image. To solve an issue of this type, use the following filesystem creation option in your {kiwi} image configuration: .. code:: xml XFS Check the XFS metadata setup on the build host and make sure the settings are compatible with the target image. XFS has the default settings compiled-in, thus it might be needed to build the image first and use the `xfs_info` tool in a `disk.sh` script to fetch the settings at the build time of the image. We know from community reports that the setting `sparse=1` will cause issues on older versions of grub's xfs module, which does not know how to handle this setting properly. To solve an issue of this type, use the following filesystem creation option in your {kiwi} image configuration: .. code:: xml btrfs btrfs and default page sizes (4k vs 64k). By default, btrfs autodetects the sector size according to the used kernel page size. If the sector size differs from the page size, the created filesystem cannot be mounted by the image's target kernel. If there is a different kernel page size between the kernel on the system the image is built on and the later kernel used for the image, it's required to specify the filesystem sector size to match with the kernel page size of the kernel used for the image. This can be done as in the following example: .. code:: xml .. note:: There can be more inconsistencies in the area of filesystems that we have not listed here. In general, it's advisable to build the image in a compatible environment. At best, the build host distribution is of the same major Linux version as the image target. For this purpose, {kiwi} provides the `boxbuild` command. Further details can be found in :ref:`self_contained`. ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.737635 kiwi-11.1.8/doc/source/troubleshooting/security.rst0000644000000000000000000000447315273675667017430 0ustar00Host Security Settings Conflicts with KIWI ========================================== .. note:: **Abstract** This page provides further information on how to solve image build problems caused by `selinux` security policies. Linux systems are protected against write/read or other operations depending on the application that wants to access or modify data. The rules for this protection are provided in security policies. There are several applications enforcing these security settings, e.g. `apparmor` or `selinux`. In this troubleshooting chapter, the focus is on `selinux`. Protecting files, process groups, kernel filesystems, device nodes and more from unauthorized access and restricting it to a certain set of applications is a nice concept. However, if taken seriously, no other application except the ones configured in the security policy will function properly. When building an appliance, the appliance builder has to have access to a wide range of services. It must be able to create a new package database elsewhere in the system. It must be able to create, read and write device nodes, create filesystems, partitions, bootloader configurations, etc. The list is very long and no security policy could cover this in a way that it would not be open to everything, which in the end leads to a pointless exercise and no security at all. This means for users who would like to keep the security settings of the system enforced and unchanged, the only way to allow {kiwi} to do its job is to run it through `boxbuild` as explained in :ref:`self_contained`. For users who can afford to open the system security policy, the following procedure will allow {kiwi} to work: .. code:: bash sudo setenforce 0 This action disables selinux temporarily. To disable selinux permanently, perform the following steps: 1. Open the SELinux configuration file: :file:`/etc/selinux/config` 2. Locate the following line: `SELINUX=enforcing` 3. Change the value to disabled: .. code:: bash SELINUX=disabled 4. On the next reboot, SELinux is permanently disabled. .. note:: Similar instructions apply to other application security subsystems like `apparmor`. Due to the complexity of these systems, this article only mentions the most common issue people run into when building images on systems protected by `selinux`. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7363155 kiwi-11.1.8/doc/source/troubleshooting.rst0000644000000000000000000000076515273675667015561 0ustar00.. _troubleshooting: Troubleshooting =============== .. note:: **Abstract** This document describes situations that lead to issues during build or boot time of the image built with {kiwi}. The suggested solutions are considered best practice but are just one of many possible solutions. .. toctree:: :maxdepth: 1 troubleshooting/buildhost_constraints troubleshooting/architectures troubleshooting/security troubleshooting/filesystems troubleshooting/boxbuild_tweaks ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378054 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/.gitignore0000644000000000000000000000002315273675667017312 0ustar00/kiwi-parse-verity ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/Makefile0000644000000000000000000000067415273675667016776 0ustar00buildroot ?= / CROSS_COMPILE ?= CC ?= $(CROSS_COMPILE)gcc CFLAGS += -Werror -Wall -Wextra BINARY := kiwi-parse-verity .PHONY: all all: build .PHONY: install install: build install -Dm0755 \ kiwi-verity-setup.sh kiwi-veritytab-setup.sh module-setup.sh \ -t ${buildroot}usr/lib/dracut/modules.d/50kiwi-verity install -Dm0755 $(BINARY) ${buildroot}usr/bin/$(BINARY) .PHONY: build build: $(BINARY) .PHONY: clean clean: rm -f $(BINARY) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/README.rst0000644000000000000000000000477015273675667017026 0ustar00kiwi-parse-verity ================= Overview -------- This tool is used to parse the information stored by `Kiwi `__ inside the unused space of a disk partition when using a SquashFS root filesystem backed by ``dm-verity``. This data is stored at a known offset (generally 4096 bytes, or 1 block, from the end of the partition) and follows the layout as described in the `Kiwi documentation `__ for the attribute ``embed_verity_metadata``. Usage ----- The following parameters are required in the Kiwi appliance description: - ``verity_blocks="all"`` - ``embed_verity_metadata="true"`` The tool is used to pull a specific entry from the metadata and will return that entry as plain text. It can be called using the following syntax: :: kiwi-parse-verity -p -o The data type is one of the entries stored by Kiwi inside the metadata, such as ``root-hash`` or ``algorithm``. More information about the parameters can be found by calling ``kiwi-parse-verity --help``. You will probably need all of the metadata to successfully map the device, as Kiwi sets up the ``dm-verity`` device with the ``--no-superblock`` option, which does not store any data inside the partition, meaning all parameters need to be passed to it on the command line. Example ~~~~~~~ Detailed example of pulling all of the metadata from the partition and mapping the device using ``veritysetup``: :: VERITY_ROOT=/dev/sda7 ROOT_HASH=$(kiwi-parse-verity -p ${VERITY_ROOT} -o root-hash) ALGORITHM=$(kiwi-parse-verity -p ${VERITY_ROOT} -o algorithm) HASH_TYPE=$(kiwi-parse-verity -p ${VERITY_ROOT} -o hash-type) HASH_BLOCK_SIZE=$(kiwi-parse-verity -p ${VERITY_ROOT} -o hash-blocksize) DATA_BLOCK_SIZE=$(kiwi-parse-verity -p ${VERITY_ROOT} -o data-blocksize) SALT=$(kiwi-parse-verity -p ${VERITY_ROOT} -o salt) DATA_BLOCKS=$(kiwi-parse-verity -p ${VERITY_ROOT} -o data-blocks) HASH_START_BLOCK=$(kiwi-parse-verity -p ${VERITY_ROOT} -o hash-start-block) HASH_OFFSET=$(( ${HASH_START_BLOCK} * ${HASH_BLOCK_SIZE} )) veritysetup open ${VERITY_ROOT} verityRoot ${VERITY_ROOT} ${ROOT_HASH} \ --no-superblock --hash ${ALGORITHM} --format ${HASH_TYPE} \ --hash-block-size ${HASH_BLOCK_SIZE} --data-block-size ${DATA_BLOCK_SIZE} \ --salt ${SALT} --data-blocks ${DATA_BLOCKS} --hash-offset ${HASH_OFFSET} The device will then be mapped to ``/dev/mapper/verityRoot``. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/kiwi-parse-verity.c0000644000000000000000000003147015273675667021073 0ustar00// vi: set sw=4 ts=4 : // SPDX-License-Identifier: GPL-2.0-or-later // Copyright (C) 2023 Canonical Ltd // Author: Isaac True // // This file is part of kiwi. // // kiwi 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 3 of the License, or // (at your option) any later version. // // kiwi 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 kiwi. If not, see // #define _POSIX_C_SOURCE (200809L) #define _LARGEFILE64_SOURCE #include #include #include #include #include #include #include #include #include #include #include #include enum output_type { OUTPUT_VERSION, OUTPUT_FSTYPE, OUTPUT_ACCESS, OUTPUT_IDENTIFIER, OUTPUT_HASH_TYPE, OUTPUT_DATA_BLOCK_SIZE, OUTPUT_HASH_BLOCK_SIZE, OUTPUT_DATA_BLOCKS, OUTPUT_HASH_START_BLOCK, OUTPUT_ALGORITHM, OUTPUT_ROOT_HASH, OUTPUT_SALT, OUTPUT_UNKNOWN, }; struct args { char *path; uint64_t block_size; uint64_t end_offset; bool verbose; enum output_type output; }; struct kiwi_verity_header { /* currently set to 1 */ char version; /* name of filesystem attribute */ char *fstype; /* either ro or rw depending on the filesystem capabilities */ char *access; /* fixed identifier value */ char *identifier; }; struct kiwi_verity_metadata { /* hash type name as returned by veritysetup */ char hash_type; /* data blocksize as returned by veritysetup */ char *data_blksize; /* hash blocksize as returned by veritysetup */ char *hash_blksize; /* number of data blocks as returned by veritysetup */ char *data_blocks; /* hash start block as required by the kernel to construct the device map */ char *hash_start_block; /* hash algorithm as returned by veritysetup */ char *algorithm; /* root hash as returned by veritysetup */ char *root_hash; /* salt hash as returned by veritysetup */ char *salt; }; static const char *const output_table[] = { [OUTPUT_VERSION] = "version", [OUTPUT_FSTYPE] = "fs-type", [OUTPUT_ACCESS] = "access", [OUTPUT_IDENTIFIER] = "identifier", [OUTPUT_HASH_TYPE] = "hash-type", [OUTPUT_DATA_BLOCK_SIZE] = "data-blocksize", [OUTPUT_HASH_BLOCK_SIZE] = "hash-blocksize", [OUTPUT_DATA_BLOCKS] = "data-blocks", [OUTPUT_HASH_START_BLOCK] = "hash-start-block", [OUTPUT_ALGORITHM] = "algorithm", [OUTPUT_ROOT_HASH] = "root-hash", [OUTPUT_SALT] = "salt", }; static void print_usage(char *const argv[]); static enum output_type parse_output_type(const char *const str); static int parse_args(struct args *const args, const int argc, char *const argv[]); static int kiwi_read_verity_info(const struct args *const args, struct kiwi_verity_header *const header, struct kiwi_verity_metadata *const metadata); static bool is_data_separator(char c); static int read_string(int fd, char **const dest); static int read_char(int fd, char *const dest); static void cleanup(struct kiwi_verity_header *const header, struct kiwi_verity_metadata *const metadata); static enum output_type parse_output_type(const char *const str) { char buffer[32] = {'\0'}; assert(str); /* * Copy the string to a temporary buffer so we can ensure it's * NULL-terminated. */ memcpy(buffer, str, strnlen(str, sizeof(buffer) - 1)); buffer[sizeof(buffer) - 1] = '\0'; for (size_t i = 0; i < (sizeof(output_table) / sizeof(output_table[0])); i++) { if (!memcmp(buffer, output_table[i], strnlen(output_table[i], sizeof(buffer) - 1) + 1)) { return (enum output_type)i; } } return OUTPUT_UNKNOWN; } static int parse_args(struct args *const args, const int argc, char *const argv[]) { int c; assert(args); assert(argc > 0); assert(argv); while ((c = getopt(argc, argv, "p:b:e:vo:")) != -1) switch (c) { case 'p': args->path = optarg; break; case 'b': args->block_size = strtoul(optarg, NULL, 10); break; case 'e': args->end_offset = strtoul(optarg, NULL, 10); break; case 'v': args->verbose = true; break; case 'o': args->output = parse_output_type(optarg); break; case '?': return 1; default: print_usage(argv); return 1; } return 0; } static void print_usage(char *const argv[]) { assert(argv); puts("kiwi-parse-verity"); puts("Tool for extracting verity information from a Kiwi image"); puts("Written by Isaac True "); puts(""); printf("Usage: %s -p [options]\n", argv[0]); puts(" -p : path to the device where the verity data is stored"); puts(" -o : data to extract (default root-hash). Can be one of the following:"); for (size_t i = 0; i < (sizeof(output_table) / sizeof(output_table[0])); i++) { printf(" %s\n", output_table[i]); } puts(" -e : offset in blocks from the end of the target device where the data is stored (default 1)"); puts(" -b : hash block size (default 4096)"); puts(" -v: enable verbose mode"); } static int kiwi_read_verity_info(const struct args *const args, struct kiwi_verity_header *const header, struct kiwi_verity_metadata *const metadata) { int fd; int ret; off64_t offset; assert(args); assert(header); assert(metadata); fd = open(args->path, O_RDONLY); if (fd < 0) { printf("Failed to open %s: %s (%d)\n", args->path, strerror(errno), errno); return -errno; } offset = (off64_t)args->block_size * (off64_t)args->end_offset; if (lseek64(fd, -offset, SEEK_END) < 0) { printf("Failed to go to offset %"PRIu64" in %s: %s (%d)\n", offset, args->path, strerror(errno), errno); return -errno; } /* * Make sure that we are parsing a known version. This also serves as * a basic preliminary sanity check. */ ret = read_char(fd, &header->version); if (ret < 0) { return ret; } if (header->version != '1') { printf("Invalid Kiwi verity metadata version: %c\n", header->version); return -EINVAL; } ret = read_string(fd, &header->fstype); if (ret < 0) { return ret; } ret = read_string(fd, &header->access); if (ret < 0) { return ret; } ret = read_string(fd, &header->identifier); if (ret < 0) { return ret; } ret = read_char(fd, &metadata->hash_type); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->data_blksize); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->hash_blksize); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->data_blocks); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->hash_start_block); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->algorithm); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->root_hash); if (ret < 0) { return ret; } ret = read_string(fd, &metadata->salt); if (ret < 0) { return ret; } close(fd); return 0; } static bool is_data_separator(char c) { return (c == ' ' || c == '\xff'); } static int read_string(int fd, char **const dest) { char buffer[256] = {'\0'}; size_t i = 0; assert(fd >= 0); assert(dest); do { ssize_t ret = read(fd, &buffer[i], 1); if (ret < 0) { return -errno; } else if (ret < 1) { return -ENOSPC; } if (is_data_separator(buffer[i])) { /* End the string here */ buffer[i] = '\0'; break; } } while (i++ < (sizeof(buffer) - 1)); *dest = strdup(buffer); if (!*dest) { return -errno; } return 0; } static int read_char(int fd, char *const dest) { char *buffer = NULL; int ret; assert(fd >= 0); assert(dest); ret = read_string(fd, &buffer); if (ret < 0) { return 0; } assert(buffer); *dest = buffer[0]; free(buffer); return 0; } static void cleanup(struct kiwi_verity_header *const header, struct kiwi_verity_metadata *const metadata) { assert(header); assert(metadata); if (header->access != NULL) { free(header->access); } if (header->fstype != NULL) { free(header->fstype); } if (header->identifier != NULL) { free(header->identifier); } if (metadata->algorithm != NULL) { free(metadata->algorithm); } if (metadata->data_blksize != NULL) { free(metadata->data_blksize); } if (metadata->data_blocks != NULL) { free(metadata->data_blocks); } if (metadata->hash_blksize != NULL) { free(metadata->hash_blksize); } if (metadata->hash_start_block != NULL) { free(metadata->hash_start_block); } if (metadata->root_hash != NULL) { free(metadata->root_hash); } if (metadata->salt != NULL) { free(metadata->salt); } } int main(int argc, char *argv[]) { int ret = 0; struct args args = { .path = NULL, .block_size = 4096, .end_offset = 1, .verbose = false, .output = OUTPUT_ROOT_HASH, }; struct kiwi_verity_header header = {0}; struct kiwi_verity_metadata metadata = {0}; ret = parse_args(&args, argc, argv); if (ret) { return ret; } if (args.path == NULL || args.output == OUTPUT_UNKNOWN || args.block_size == 0 || args.end_offset == 0) { print_usage(argv); return -1; } if (args.verbose) { printf("Reading from %s, block size %"PRIu64", block offset from end %"PRIu64"\n", args.path, args.block_size, args.end_offset); } ret = kiwi_read_verity_info(&args, &header, &metadata); if (ret) { printf("Failed to read metadata: %s (%d)\n", strerror(-ret), ret); goto exit; } if (args.verbose) { printf("\nHeader:\n"); printf(" Version: %c\n", header.version); printf(" FS Type: %s\n", header.fstype); printf(" Access: %s\n", header.access); printf(" Identifier: %s\n", header.identifier); printf("\nMetadata:\n"); printf(" Hash type: %c\n", metadata.hash_type); printf(" Data blksize: %s\n", metadata.data_blksize); printf(" Hash blksize: %s\n", metadata.hash_blksize); printf(" Data blocks: %s\n", metadata.data_blocks); printf(" Hash start block: %s\n", metadata.hash_start_block); printf(" Algorithm: %s\n", metadata.algorithm); printf(" Root hash: %s\n", metadata.root_hash); printf(" Salt: %s\n", metadata.salt); } switch (args.output) { case OUTPUT_VERSION: printf("%c\n", header.version); break; case OUTPUT_FSTYPE: printf("%s\n", header.fstype); break; case OUTPUT_ACCESS: printf("%s\n", header.access); break; case OUTPUT_IDENTIFIER: printf("%s\n", header.identifier); break; case OUTPUT_HASH_TYPE: printf("%c\n", metadata.hash_type); break; case OUTPUT_DATA_BLOCK_SIZE: printf("%s\n", metadata.data_blksize); break; case OUTPUT_HASH_BLOCK_SIZE: printf("%s\n", metadata.hash_blksize); break; case OUTPUT_DATA_BLOCKS: printf("%s\n", metadata.data_blocks); break; case OUTPUT_HASH_START_BLOCK: printf("%s\n", metadata.hash_start_block); break; case OUTPUT_ALGORITHM: printf("%s\n", metadata.algorithm); break; case OUTPUT_ROOT_HASH: printf("%s\n", metadata.root_hash); break; case OUTPUT_SALT: printf("%s\n", metadata.salt); break; default: return -1; } exit: cleanup(&header, &metadata); return ret; } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/kiwi-verity-setup.sh0000755000000000000000000000403315273675667021307 0ustar00#!/bin/sh # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2023 Canonical Ltd # Author: Isaac True # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # shellcheck disable=SC1091 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh if ! getarg "verityroot="; then return fi VERITY_ROOT=$(getarg "verityroot=") if [ ! -e "${VERITY_ROOT}" ]; then # Source device does not exist (yet?) exit 1 fi if [ -e "/dev/mapper/verityRoot" ]; then # Mapping already exists - we don't need to do it again exit 0 fi ROOT_HASH=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o root-hash) ALGORITHM=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o algorithm) HASH_TYPE=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o hash-type) HASH_BLOCK_SIZE=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o hash-blocksize) DATA_BLOCK_SIZE=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o data-blocksize) SALT=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o salt) DATA_BLOCKS=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o data-blocks) HASH_START_BLOCK=$(kiwi-parse-verity -p "${VERITY_ROOT}" -o hash-start-block) HASH_OFFSET=$(( "${HASH_START_BLOCK}" * "${HASH_BLOCK_SIZE}" )) veritysetup open "${VERITY_ROOT}" verityRoot "${VERITY_ROOT}" "${ROOT_HASH}" \ --no-superblock --hash "${ALGORITHM}" --format "${HASH_TYPE}" \ --hash-block-size "${HASH_BLOCK_SIZE}" --data-block-size "${DATA_BLOCK_SIZE}" \ --salt "${SALT}" --data-blocks "${DATA_BLOCKS}" --hash-offset "${HASH_OFFSET}" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/kiwi-veritytab-setup.sh0000755000000000000000000000321015273675667021772 0ustar00#!/bin/sh # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # shellcheck disable=SC1091 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh if [ ! -e "/etc/veritytab" ];then return fi read -r name data_device hash_device root_hash options < /etc/veritytab if [ "$(echo "${data_device}" | cut -f1 -d=)" = "UUID" ];then data_device=/dev/disk/by-uuid/$(echo "${data_device}" | cut -f2 -d=) fi if [ "$(echo "${hash_device}" | cut -f1 -d=)" = "UUID" ];then hash_device=/dev/disk/by-uuid/$(echo "${hash_device}" | cut -f2 -d=) fi # Read kernel command line verity options and merge the options kiwi_verity_options=$(getarg rd.kiwi.verity_options=) if [ -n "${kiwi_verity_options}" ]; then if [ -n "${options}" ]; then options="${options},${kiwi_verity_options}" else options="${kiwi_verity_options}" fi fi veritysetup="veritysetup open " veritysetup="${veritysetup} ${data_device} ${name} ${hash_device} ${root_hash}" for option in $(echo "${options}" | tr , " ");do veritysetup="${veritysetup} --${option}" done eval "${veritysetup}" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/50kiwi-verity/module-setup.sh0000755000000000000000000000231415273675667020311 0ustar00#!/bin/bash # SPDX-License-Identifier: GPL-2.0-or-later # Copyright (C) 2023 Canonical Ltd # Author: Isaac True # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # declare moddir=${moddir} check() { require_binaries /usr/bin/kiwi-parse-verity || return 1 require_binaries /usr/sbin/veritysetup || return 1 } depends() { return 0 } install() { inst_multiple \ /usr/bin/kiwi-parse-verity \ /usr/sbin/veritysetup \ cut \ tr inst_hook initqueue/settled 70 "$moddir/kiwi-verity-setup.sh" inst_hook initqueue/settled 71 "$moddir/kiwi-veritytab-setup.sh" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7378643 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/Makefile0000644000000000000000000000022615273675667016417 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/55kiwi-dump cp -a * ${buildroot}usr/lib/dracut/modules.d/55kiwi-dump ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/kiwi-dump-image.sh0000755000000000000000000012712515273675667020314 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type setup_debug >/dev/null 2>&1 || . /lib/kiwi-lib.sh type run_dialog >/dev/null 2>&1 || . /lib/kiwi-dialog-lib.sh type get_block_device_kbsize >/dev/null 2>&1 || . /lib/kiwi-partitions-lib.sh type fetch_file >/dev/null 2>&1 || . /lib/kiwi-net-lib.sh #====================================== # Functions #-------------------------------------- function scan_multipath_devices { # """ # starts multipath daemon from multipath module # """ systemctl start multipathd } function find_disk_entry { # """ # lookup disk entry by name and echo the # associated entry parameter (size) when found # """ local list_items=$1 local search=$2 local count=0 local found= for entry in ${list_items};do if [ -n "${found}" ];then echo "${entry}" return fi if [ $((count % 2)) -eq 0 ];then if [ "${entry}" = "${search}" ];then found=1 fi fi count=$((count + 1)) done } function sort_disk_entries { # """ # sort the disk entry names # """ local list_items=$1 local count=0 local device_index=0 local device_array local list_items_sorted for entry in ${list_items};do if [ $((count % 2)) -eq 0 ];then device_array[${device_index}]=${entry} device_index=$((device_index + 1)) fi count=$((count + 1)) done printf '%s\0' "${device_array[@]}" | sort -z > list_items_sorted.txt readarray -td '' list_items_sorted < list_items_sorted.txt echo "${list_items_sorted[*]}" } function load_ramdisk_driver { declare kiwi_oemramdisksize=${kiwi_oemramdisksize} local rd_size local custom_rd_size local modfile=/etc/modprobe.d/99-brd.conf custom_rd_size=$(getarg ramdisk_size=) if [ -n "${custom_rd_size}" ];then rd_size="${custom_rd_size}" elif [ -n "${kiwi_oemramdisksize}" ];then rd_size="${kiwi_oemramdisksize}" fi mkdir -p /etc/modprobe.d if [ -n "${rd_size}" ];then echo "options brd rd_size=${rd_size}" > ${modfile} fi modprobe brd udev_pending } function get_disk_list { declare kiwi_oemdevicefilter=${kiwi_oemdevicefilter} declare kiwi_oemmultipath_scan=${kiwi_oemmultipath_scan} declare kiwi_devicepersistency=${kiwi_devicepersistency} declare kiwi_install_volid=${kiwi_install_volid} declare kiwi_oemunattended=${kiwi_oemunattended} declare kiwi_oemunattended_id=${kiwi_oemunattended_id} local disk_id="by-id" local disk_size local disk_device local disk_device_by_id local disk_trans local disk_meta local list_items local list_items_sorted local max_disk local kiwi_oem_maxdisk local blk_opts="-p -n -r --sort NAME -o NAME,SIZE,TYPE,TRAN" local message local blk_opts_plus_label="${blk_opts},LABEL" local kiwi_install_disk_part if [ -n "${kiwi_devicepersistency}" ];then disk_id=${kiwi_devicepersistency} fi max_disk=0 kiwi_install_devicepersistency=$(getarg rd.kiwi.install.devicepersistency=) kiwi_oemmultipath_scan=$(bool "${kiwi_oemmultipath_scan}") kiwi_oem_maxdisk=$(getarg rd.kiwi.oem.maxdisk=) kiwi_oem_installdevice=$(getarg rd.kiwi.oem.installdevice=) # Check for smallest disk parameter # - Configure lsblk options based on the selection mode if getargbool 0 rd.kiwi.oem.smallest_disk; then # Sort by SIZE (ascending), include TRAN to identify USBs blk_opts="-p -n -r --sort SIZE -o NAME,SIZE,TYPE,TRAN" blk_opts_plus_label="${blk_opts},LABEL" fi if [ -n "${kiwi_install_devicepersistency}" ];then disk_id=${kiwi_install_devicepersistency} fi if [ -n "${kiwi_oem_maxdisk}" ]; then max_disk=$(binsize_to_bytesize "${kiwi_oem_maxdisk}") || max_disk=0 fi if getargbool 0 rd.kiwi.ramdisk; then # target should be a ramdisk on request. Thus actively # load the ramdisk block driver and support custom sizes load_ramdisk_driver # target should be a ramdisk on request. Thus instruct # lsblk to list only ramdisk devices (Major=1) blk_opts="-I 1 ${blk_opts}" elif [ "${kiwi_oemmultipath_scan}" = "true" ];then scan_multipath_devices fi kiwi_install_disk_part=$( eval lsblk "${blk_opts_plus_label}" | \ tr -s ' ' ":" | \ grep ":${kiwi_install_volid}$" | \ cut -f1 -d: ) for disk_meta in $( eval lsblk "${blk_opts}" | grep -E "disk|raid" | tr ' ' ":" );do disk_device="$(echo "${disk_meta}" | cut -f1 -d:)" if [[ "${kiwi_install_disk_part}" == "${disk_device}"* ]]; then # ignore install source device continue fi disk_mpath_child=$( eval lsblk "${blk_opts}" "${disk_device}" | grep mpath | \ tr ' ' ":" ) if [[ -n "${disk_mpath_child}" ]];then # use mpath device instead of parent device for multipath disk_device="$(echo "${disk_mpath_child}" | cut -f1 -d:)" fi # Filter USBs if smallest disk is requested disk_trans=$(echo "${disk_meta}" | cut -f4 -d:) if getargbool 0 rd.kiwi.oem.smallest_disk; then if [[ "${disk_trans}" == "usb" ]]; then continue fi fi disk_size=$(echo "${disk_meta}" | cut -f2 -d:) if [ "${max_disk}" -gt 0 ]; then local disk_size_bytes disk_size_bytes=$(binsize_to_bytesize "${disk_size}") || \ disk_size_bytes=0 if [ "${disk_size_bytes}" -gt "${max_disk}" ]; then message="${disk_device} filtered out by" message="${message} rd.kiwi.oem.maxdisk=${kiwi_oem_maxdisk}" message="${message} (disk size is: ${disk_size})" info "${message}" >&2 continue fi fi disk_device_by_id=$( get_persistent_device_from_unix_node "${disk_device}" "${disk_id}" ) if [ -n "${disk_device_by_id}" ];then disk_device=${disk_device_by_id} fi # check for static filter rules if [[ ${disk_device} =~ ^/dev/fd ]];then # ignore floppy disk devices continue fi # check for custom filter rule if [ -n "${kiwi_oemdevicefilter}" ];then if [[ ${disk_device} =~ ${kiwi_oemdevicefilter} ]];then message="${disk_device} filtered out by rule:" message="${message} ${kiwi_oemdevicefilter}" info "${message}" >&2 continue fi fi if [[ "${list_items}" == *"${disk_device}"* ]];then # device already in the list, can happen in multipath systems continue fi list_items="${list_items} ${disk_device} ${disk_size}" done if [ -n "${kiwi_oem_installdevice}" ];then # install device overwritten by cmdline. local device=${kiwi_oem_installdevice} local device_meta local device_size if [ ! -e "${device}" ];then local no_dev="Given device ${device} does not exist" report_and_quit "${no_dev}" fi if [ ! -b "${device}" ];then local no_block_dev="Given device ${device} is not a block special" report_and_quit "${no_block_dev}" fi device_meta=$( eval lsblk "${blk_opts}" "${device}" |\ grep -E "disk|raid|mpath" | tr ' ' ":" ) device_size=$(echo "${device_meta}" | cut -f2 -d:) list_items="${device} ${device_size}" fi if [ -z "${list_items}" ];then local no_device_text="No device(s) for installation found" report_and_quit "${no_device_text}" fi # Conditional Sorting if getargbool 0 rd.kiwi.oem.smallest_disk; then # If looking for smallest disk, preserve the lsblk size-sort order echo "${list_items}" else # Default behavior: # apply final alphabetical sorting for the used disk_device names list_items_sorted=$(sort_disk_entries "${list_items}") for entry in ${list_items_sorted[*]}; do echo -n "${entry} $(find_disk_entry "${list_items}" "${entry}") " done echo fi } function validate_disk_selection { local selected=$1 local disk_list local entry local count=0 disk_list=$(get_disk_list) for entry in ${disk_list};do if [ $((count % 2)) -eq 0 ];then if [ ! "${entry}" = "${selected}" ];then # check system_identifier for the not selected disk # if one of them matches the system_identifier because # that would produce a global system inconsistency if disk_matches_system_identifier "${entry}";then report_and_quit "Disk ${entry} has the same partition UUID" fi fi fi count=$((count + 1)) done } function get_selected_disk { declare kiwi_oemunattended=${kiwi_oemunattended} declare kiwi_oemunattended_id=${kiwi_oemunattended_id} local disk_list local device_array kiwi_oemunattended=$(bool "${kiwi_oemunattended}") disk_list=$(get_disk_list) if [ -n "${disk_list}" ];then local count=0 local device_index=0 for entry in ${disk_list};do if [ $((count % 2)) -eq 0 ];then device_array[${device_index}]=${entry} device_index=$((device_index + 1)) fi count=$((count + 1)) done if [ "${device_index}" -eq 1 ];then # one single disk device found, use it echo "${device_array[0]}" elif [ "${kiwi_oemunattended}" = "true" ];then if [ -z "${kiwi_oemunattended_id}" ];then # unattended mode requested but no target specifier, # thus use first device from list echo "${device_array[0]}" else # unattended mode requested with target specifier # use this device if present local device for device in "${device_array[@]}"; do if [[ ${device} =~ ${kiwi_oemunattended_id} ]];then echo "${device}" return fi done fi else # manually select from storage list if ! run_dialog \ --menu "\"Select Installation Disk\"" 20 75 15 "${disk_list}" then report_and_quit "System installation canceled" fi get_dialog_result fi fi } function export_image_metadata { local image_source_files=$1 export checksum export blocks export blocksize export zblocks export zblocksize local meta_file meta_file="$(echo "${image_source_files}" | cut -f2 -d\|)" if ! read -r checksum blocks blocksize zblocks zblocksize < "${meta_file}" then report_and_quit "Reading ${meta_file} failed" fi echo "Image checksum: ${checksum}" echo "Image blocks: ${blocks} / blocksize: ${blocksize}" if [ -n "${zblocks}" ];then echo "Image compressed blocks: ${zblocks} / blocksize: ${zblocksize}" fi } function check_image_fits_target { local image_target=$1 local need_mbytes local have_mbytes need_mbytes=$((blocks * blocksize / 1048576)) have_mbytes=$(($(get_block_device_kbsize "${image_target}") / 1024)) echo "Have size: ${image_target} -> ${have_mbytes} MB" echo "Need size: ${image_source} -> ${need_mbytes} MB" if [ ${need_mbytes} -gt ${have_mbytes} ];then report_and_quit "Not enough space available for this image" fi } function check_image_matches_target_blocksize { declare kiwi_target_blocksize=${kiwi_target_blocksize} local image_blocksize=${kiwi_target_blocksize} local target_node local target_blocksize local message target_node="${1}" if [ -L "${target_node}" ];then target_node="$(readlink "${target_node}")" fi target_node=$(basename "${target_node}") target_blocksize="/sys/block/${target_node}/queue/logical_block_size" mountpoint -q /sys || mount -t sysfs sysfs /sys if [ -n "${image_blocksize}" ] && [ -e "${target_blocksize}" ];then read -r target_blocksize < "${target_blocksize}" if [ ! "${target_blocksize}" = "${image_blocksize}" ];then message="Target blocksize(${target_blocksize}b) doesn't match" message="${message} image blocksize(${image_blocksize}b)" report_and_quit "${message}" fi fi } function dump_image { declare kiwi_oemsilentinstall=${kiwi_oemsilentinstall} declare kiwi_oemunattended=${kiwi_oemunattended} local image_source_files=$1 local image_target=$2 local image_from_remote=$3 local image_source local image_basename kiwi_oemsilentinstall=$(bool "${kiwi_oemsilentinstall}") kiwi_oemunattended=$(bool "${kiwi_oemunattended}") image_source="$(echo "${image_source_files}" | cut -f1 -d\|)" image_basename=$(basename "${image_source}") local progress=/dev/install_progress local image_source_file=/image.raw local load_text="Loading ${image_basename}..." local title_text="Installation..." local dump local copy local parttable local count_32k local image_size local count=0 local block_size=32k # can we dump this check_image_fits_target "${image_target}" check_image_matches_target_blocksize "${image_target}" # select dump method if [ -n "${image_from_remote}" ];then if getargbool 0 rd.kiwi.install.partition_based; then copy=dump_remote_image dump=dump_local_image_partition_based else dump=dump_remote_image fi else if getargbool 0 rd.kiwi.install.partition_based; then if getargbool 0 rd.kiwi.install.clone_local; then copy=dump_local_image fi dump=dump_local_image_partition_based else dump=dump_local_image fi fi # preserve pre-dump partition table if getargbool 0 rd.kiwi.install.retain_past; then fetch_local_partition_table "${image_target}" /tmp/parttable_pre fi # setup blocks and blocksize to retain last if getargbool 0 rd.kiwi.install.retain_last || \ getargbool 0 rd.kiwi.install.retain_past then if [ -n "${image_from_remote}" ];then image_size=$((blocks * blocksize)) parttable=$( fetch_remote_partition_table "${image_source}" "${image_size}" ) else parttable=$( fetch_local_partition_table "${image_source}" ) fi if compatible_to_retain "${parttable}" "${image_target}"; then if getargbool 0 rd.kiwi.install.retain_last; then count=$(get_disk_offset_retain_last_partition "${parttable}") else # retain_past always dumps the complete image # the compatible_to_retain check for this image blob # has been done and the deployment can happen for # all blocks count=0 fi fi if [ "${count}" -gt 0 ];then block_size=$(get_sector_size_from_table_dump "${parttable}") count_32k=$( optimize_count_for_32k_blocksize "${block_size}" "${count}" ) if [ ! "${count}" = "${count_32k}" ];then count="${count_32k}" block_size=32k fi fi fi # last chance to stop us if [ "${kiwi_oemunattended}" = "false" ];then local ack_dump_text="Destroying ALL data on ${image_target}, continue ?" if ! run_dialog --yesno "\"${ack_dump_text}\"" 7 80; then local install_cancel_text="System installation canceled" report_and_quit "${install_cancel_text}" fi fi # deploy echo "${load_text} [${image_target}]..." if with_progress && [ "${kiwi_oemsilentinstall}" = "false" ]; then # dump with dialog based progress information setup_progress_fifo ${progress} if [ -n "${copy}" ];then eval \ "${copy}" \ "${image_source}" \ "${image_source_file}" \ "${count}" \ "${block_size}" \ "${progress}" \ & run_progress_dialog "${load_text}" "${title_text}" image_source="${image_source_file}" fi eval \ "${dump}" \ "${image_source}" \ "${image_target}" \ "${count}" \ "${block_size}" \ "${progress}" \ & run_progress_dialog "${load_text}" "${title_text}" else # dump with silently blocked console if [ -n "${copy}" ];then if ! eval \ "${copy}" \ "${image_source}" \ "${image_source_file}" \ "${count}" \ "${block_size}" then report_and_quit "Failed to copy image" fi image_source="${image_source_file}" fi if ! eval \ "${dump}" \ "${image_source}" \ "${image_target}" \ "${count}" \ "${block_size}" then report_and_quit "Failed to install image" fi fi # recreate last partition from pre-dump table if getargbool 0 rd.kiwi.install.retain_past && \ [ ! -e /tmp/retain_not_applicable ] then recreate_last_partition "${image_target}" fi # update EFI boot entry for the installed system remove_efi_boot_entry add_efi_boot_entry "${image_target}" } function efi_boot_manager_available { # """ # check if EFI boot entries can be managed on this system # """ if [ ! -d /sys/firmware/efi ];then warn "System not booted in EFI mode, skipping EFI boot entry setup" return 1 fi if ! command -v efibootmgr &>/dev/null;then warn "efibootmgr not found, skipping EFI boot entry setup" return 1 fi return 0 } function get_esp_partition { # """ # lookup the EFI system partition on the given disk # by its filesystem label EFI # """ local disk_device=$1 udev_pending lsblk -p -n -r -o NAME,LABEL "${disk_device}" |\ awk '$2 == "EFI" { print $1; exit }' } function get_partition_number { # """ # get the partition number of the given partition device # """ local partition_device=$1 local partition_node local partition_number partition_node=$(readlink -f "${partition_device}") partition_node=${partition_node##*/} if [ -e "/sys/class/block/${partition_node}/partition" ];then read -r partition_number \ < "/sys/class/block/${partition_node}/partition" elif [[ "${partition_device}" =~ ([0-9]+)$ ]];then # e.g. device mapper partitions created by kpartx partition_number=${BASH_REMATCH[1]} fi echo "${partition_number}" } function find_efi_binary { # """ # print the first EFI binary found in the given directories. # The name patterns are tried in the given order, each one # across all directories before the next pattern is tried # """ local patterns local pattern local efi_dir local candidate local candidate_name read -r -a patterns <<< "$1" shift for pattern in "${patterns[@]}";do for efi_dir in "$@";do # shellcheck disable=SC2231 for candidate in "${efi_dir}"/${pattern};do [ -f "${candidate}" ] || continue candidate_name=${candidate##*/} # skip MokManager and fallback helpers shipped next to shim case "${candidate_name,,}" in mm*|fb*|mokmanager*|fallback*) continue ;; esac echo "${candidate}" return 0 done done done return 1 } function get_efi_loader_path { # """ # mount the given ESP read-only and search for an EFI binary. # Any directory below EFI except BOOT is a vendor directory # and searched first. The EFI/BOOT removable media path is # searched last. In both cases shim is preferred over any # other loader. The result is printed in EFI path notation, # e.g \EFI\opensuse\shim.efi # """ local esp_device=$1 local esp_mount=/run/kiwi-esp local efi_dir local efi_dir_name local vendor_dirs=() local boot_dirs=() local loader mkdir -p "${esp_mount}" if ! mount -o ro "${esp_device}" "${esp_mount}";then warn "Failed to mount ESP ${esp_device}" rmdir "${esp_mount}" return 1 fi # FAT is case insensitive, match the same way shopt -s nullglob nocaseglob for efi_dir in "${esp_mount}"/[Ee][Ff][Ii]/*/;do efi_dir=${efi_dir%/} efi_dir_name=${efi_dir##*/} if [ "${efi_dir_name,,}" = "boot" ];then boot_dirs+=("${efi_dir}") else vendor_dirs+=("${efi_dir}") fi done if [ ${#vendor_dirs[@]} -gt 0 ];then loader=$( find_efi_binary "shim*.efi grub*.efi *.efi" "${vendor_dirs[@]}" ) fi if [ -z "${loader}" ] && [ ${#boot_dirs[@]} -gt 0 ];then loader=$( find_efi_binary "shim*.efi boot*.efi *.efi" "${boot_dirs[@]}" ) fi shopt -u nullglob nocaseglob umount "${esp_mount}" rmdir "${esp_mount}" if [ -z "${loader}" ];then warn "No EFI binary found on ESP ${esp_device}" return 1 fi loader=${loader#"${esp_mount}"} echo "${loader//\//\\}" } function remove_efi_boot_entry { # """ # delete all EFI boot entries labeled with the name given # in rd.kiwi.install.efi_display_label # """ local label local line local bootnum local bootnums local entry_regex='^Boot([0-9A-Fa-f]{4})[* ] (.*)$' label=$(getarg rd.kiwi.install.efi_display_label=) if [ -z "${label}" ] || ! efi_boot_manager_available;then return fi while read -r line;do if [[ "${line}" =~ ${entry_regex} ]];then # newer efibootmgr versions append the device path # to the label separated by a tab if [ "${BASH_REMATCH[2]%%$'\t'*}" = "${label}" ];then bootnums="${bootnums} ${BASH_REMATCH[1]}" fi fi done < <(efibootmgr) for bootnum in ${bootnums};do info "Removing EFI boot entry Boot${bootnum} (${label})" if ! efibootmgr --quiet --bootnum "${bootnum}" --delete-bootnum;then warn "Failed to remove EFI boot entry Boot${bootnum}" fi done } function add_efi_boot_entry { # """ # create an EFI boot entry labeled with the name given in # rd.kiwi.install.efi_display_label for the ESP on the # given disk # """ local disk_device=$1 local label local esp_device local esp_partnum local loader label=$(getarg rd.kiwi.install.efi_display_label=) if [ -z "${label}" ] || ! efi_boot_manager_available;then return fi esp_device=$(get_esp_partition "${disk_device}") if [ -z "${esp_device}" ];then warn "No partition labeled EFI found on ${disk_device}" return fi esp_partnum=$(get_partition_number "${esp_device}") if [ -z "${esp_partnum}" ];then warn "Failed to get partition number of ESP ${esp_device}" return fi if ! loader=$(get_efi_loader_path "${esp_device}");then return fi info "Adding EFI boot entry ${label}: ${loader} on ${esp_device}" if ! efibootmgr --quiet --create \ --disk "$(readlink -f "${disk_device}")" --part "${esp_partnum}" \ --label "${label}" --loader "${loader}" then warn "Failed to add EFI boot entry ${label}" fi } function with_progress { if getargbool 0 rd.debug || ! command -v pv &>/dev/null; then return 1 fi return 0 } function fetch_local_partition_table { local image_source=$1 local parttable=$2 if [ -z "${parttable}" ];then parttable=/tmp/parttable fi sfdisk -d "${image_source}" > "${parttable}" 2>/dev/null echo "${parttable}" } function recreate_last_partition { # recreate last partition as it existed prior dump local image_target=$1 local table_type table_type=$(get_partition_table_type "${image_target}") if [ "${table_type}" = "gpt" ];then relocate_gpt_at_end_of_disk "${image_target}" fi # parttable after image dump, the OS sfdisk -d "${image_target}" > /tmp/parttable_1 2>/dev/null # last partition of table as it existed prior dump tail -n 1 /tmp/parttable_pre > /tmp/parttable_2 # combine table snippets. Please note there can now be a gap # between the end of the OS table and the start of the last # partition of the table prior dump. Please also note, if the # table prior dump had more partitions than only the last # one to retain, this table concat will not retain those. # and the OS dump might have overwritten them. A more # sophisticated concat procedure would be needed to support # this case here in this code and also in the # compatible_to_retain() method cat /tmp/parttable_1 /tmp/parttable_2 > /tmp/parttable set_device_lock "${image_target}" \ sfdisk -f "${image_target}" < /tmp/parttable } function fetch_remote_partition_table { local image_source=$1 local image_size=$2 local parttable=/tmp/parttable dd if=/dev/zero of=/tmp/table bs=1 count=0 seek="${image_size}" &>/dev/null fetch_file "${image_source}" 2>/dev/null | dd of=/tmp/table bs=512 count=1 conv=notrunc &>/dev/null sfdisk -d /tmp/table > "${parttable}" 2>/dev/null echo "${parttable}" } function get_sector_size_from_table_dump { local parttable=$1 sector_size=$(grep sector-size: "${parttable}" | cut -f2 -d:) echo "${sector_size}" } function compatible_to_retain { local parttable=$1 local image_target=$2 local source_start local target_start source_start=$( tail -n 1 "${parttable}" | cut -f2 -d= | cut -f1 -d, ) target_start=$( sfdisk -d "${image_target}" 2>/dev/null |\ tail -n 1 | cut -f2 -d= | cut -f1 -d, ) if [ -z "${source_start}" ] || [ -z "${target_start}" ];then # no partition information for either source or target # broken or net new deployment on empty disk touch /tmp/retain_not_applicable return 1 fi if getargbool 0 rd.kiwi.install.retain_last; then if [ ! "${source_start}" = "${target_start}" ];then report_and_quit "Cannot retain partition, start address mismatch" fi elif getargbool 0 rd.kiwi.install.retain_past; then if [ "${source_start}" -gt "${target_start}" ];then report_and_quit "Cannot retain partition, image overlaps" fi fi return 0 } function get_disk_offset_retain_last_partition { local parttable=$1 local next_to_last local start local size local offset next_to_last=$(tail -n 2 "${parttable}" | head -n 1) if [ -n "${next_to_last}" ];then start=$(echo "${next_to_last}" | cut -f2 -d= | cut -f1 -d,) size=$(echo "${next_to_last}" | cut -f3 -d= | cut -f1 -d,) offset=$((start + size)) echo "${offset}" else echo 0 fi } function optimize_count_for_32k_blocksize { local block_size=$1 local count=$2 local dump_bytes dump_bytes=$((block_size * count)) if [ $((dump_bytes % 32768)) -eq 0 ];then # dump_bytes is a multiple of 32k, use it for better I/O performance count=$((dump_bytes / 32768)) fi echo "${count}" } function dump_local_image { local image_source=$1 local image_target=$2 local count=$3 local block_size=$4 local progress=$5 if [ "${count}" -gt 0 ];then count="count=${count}" else unset count fi # shellcheck disable=SC2086 if [ -e "${progress}" ];then ( pv -n "${image_source}" |\ dd bs="${block_size}" ${count} of="${image_target}" &>/dev/null ) 2>"${progress}" else dd \ if="${image_source}" bs="${block_size}" ${count} \ of="${image_target}" &>/dev/null fi } function clone_partition_table { local image_source=$1 local image_target=$2 fetch_local_partition_table "${image_source}" sfdisk "${image_target}" < /tmp/parttable # clone MBR bootloader code dd if="${image_source}" of="${image_target}" bs=1 count=446 partx -u "${image_target}" udev_pending } function btrfs_seed_device_on_target { local source_device=$1 local target_device=$2 local seed=/seed if [ -n "${source_device}" ]; then mkdir -p "${seed}" if getargbool 0 rd.kiwi.install.clone_local; then btrfstune -S 1 "${source_device}" fi mount "${source_device}" "${seed}" btrfs device add "${target_device}" "${seed}" umount "${seed}" mount "${target_device}" "${seed}" btrfs device delete "${source_device}" "${seed}" umount "${seed}" fi } function btrfs_replay_uuid { local target_device=$1 local uuid=$2 if [ -n "${uuid}" ];then btrfstune -f -U "${uuid}" "${target_device}" fi } function check_partition_based_compatibility { # partition based mode always clones the complete partition # table of the image to the target, which would wipe out the # partition to retain if getargbool 0 rd.kiwi.install.partition_based; then if getargbool 0 rd.kiwi.install.retain_last || \ getargbool 0 rd.kiwi.install.retain_past then report_and_quit \ "rd.kiwi.install.partition_based can't be combined with retain_last|retain_past" fi fi } function clean_partition_map { local source_disk=$1 kpartx -d "${source_disk}" && losetup -d "${source_disk}" } function dump_local_image_partition_based { local image_source=$1 local image_target=$2 local count=$3 local block_size=$4 local progress=$5 local source_disk local device_index=0 local source_device_array local target_device_array local btrfs_source_device_array local btrfs_target_device_array local btrfs_source_uuid_array local filesystem local part # no partial dump in partition based mode unset count # clone partition table to target clone_partition_table "${image_source}" "${image_target}" # map source image partitions source_disk=$(losetup --show --find "${image_source}") kpartx -a "${source_disk}" # walk through source image partitions and store them for part in $( lsblk -p -n -r --sort SIZE -o NAME,TYPE "${source_disk}" |\ grep part | cut -f1 -d" " );do filesystem=$(blkid "${part}" -s TYPE -o value) uuid=$(blkid "${part}" -s UUID -o value) if [ "${filesystem}" = "btrfs" ];then btrfs_source_device_array[device_index]=${part} btrfs_source_uuid_array[device_index]=${uuid} else source_device_array[device_index]=${part} fi device_index=$((device_index + 1)) done # walk through target image partitions and store them device_index=0 for part in $( lsblk -p -n -r --sort SIZE -o NAME,TYPE "${image_target}" |\ grep part | cut -f1 -d" " );do if [ -n "${btrfs_source_device_array[${device_index}]}" ];then btrfs_target_device_array[device_index]=${part} else target_device_array[device_index]=${part} fi device_index=$((device_index + 1)) done # seed/sprout for btrfs based partitions for ((index=0; index>/seed.log done # dump standard partitions... if [ -e "${progress}" ];then ( for ((index=0; index/dev/null fi done clean_partition_map "${source_disk}" &>/dev/null for ((index=0; index/dev/null done ) 2>"${progress}" else for ((index=0; index/dev/null fi done clean_partition_map "${source_disk}" &>/dev/null for ((index=0; index/dev/null done fi } function dump_remote_image { local image_source=$1 local image_target=$2 local count=$3 local block_size=$4 local progress=$5 local image_size image_size=$((blocks * blocksize)) if [ "${count}" -gt 0 ];then count="count=${count}" else unset count fi # shellcheck disable=SC2086 if [ -e "${progress}" ];then ( fetch_file "${image_source}" "${image_size}" |\ dd bs="${block_size}" ${count} of="${image_target}" &>/dev/null ) 2>"${progress}" else fetch_file "${image_source}" |\ dd bs="${block_size}" ${count} of="${image_target}" &>/dev/null fi } function check_image_integrity { declare kiwi_oemskipverify=${kiwi_oemskipverify} declare kiwi_oemsilentverify=${kiwi_oemsilentverify} local image_target=$1 local progress=/dev/install_verify_progress local verify_text="Verifying ${image_target}" local title_text="Installation..." local verify_result local shasum local sha_extension kiwi_oemskipverify=$(bool "${kiwi_oemskipverify}") kiwi_oemsilentverify=$(bool "${kiwi_oemsilentverify}") shasum=$(shasum_tool) sha_extension=$(shasum_extension) verify_result=/dumped_image."${sha_extension}" if [ "${kiwi_oemskipverify}" = "true" ];then # no verification wanted return fi if getargbool 0 rd.kiwi.install.retain_last || \ getargbool 0 rd.kiwi.install.retain_past then if [ ! -e /tmp/retain_not_applicable ];then # no verification possible as only a portion of # the image got deployed intentionally (retain_last) # or parts of the former partition table got # restored (retain_past) return fi fi if with_progress && [ "${kiwi_oemsilentverify}" = "false" ]; then # verify with dialog based progress information setup_progress_fifo ${progress} ( pv --size $((blocks * blocksize)) --stop-at-size \ -n "${image_target}" | "${shasum}" - > "${verify_result}" ) 2>${progress} & run_progress_dialog "${verify_text}" "${title_text}" else # verify with silently blocked console head --bytes=$((blocks * blocksize)) "${image_target}" |\ "${shasum}" - > "${verify_result}" fi local checksum_dumped_image local checksum_fileref read -r checksum_dumped_image checksum_fileref < "${verify_result}" echo "Dumped Image checksum: ${checksum_dumped_image}/${checksum_fileref}" if [ "${checksum}" != "${checksum_dumped_image}" ];then report_and_quit "Image checksum test failed" fi } function get_isoscan_devices { # """ # print the kernel names of the block device stack, e.g. # partition, LVM, RAID and disk, which holds the filesystem # with the ISO file loop mounted via iso-scan/filename # """ local isoscan_device=/run/initramfs/isoscandev local device if [[ -e "${isoscan_device}" ]];then device=$(readlink -f "${isoscan_device}") lsblk -p -n -r -s -o KNAME "${device}" fi } function target_holds_isoscan_file { # """ # check if the given install target disk holds the # ISO file loop mounted via iso-scan/filename # """ local target=$1 local target_device local device target_device=$(readlink -f "${target}") for device in $(get_isoscan_devices);do if [[ "${device}" = "${target_device}" ]];then return 0 fi done return 1 } function move_isoscan_file_to_ram { # """ # The install ISO file loop mounted via iso-scan/filename is # stored on the install target disk. Dumping the image would # overwrite the ISO file while reading from it. Thus copy the # ISO file into main memory, release the target disk and # continue the installation from the copy # """ declare root=${root} local target=$1 local iso_device="${root#install:}" local isoscan_mount=/run/initramfs/isoscan local iso_ram_mount=/run/initramfs/isoram local iso_ram_file="${iso_ram_mount}/install.iso" local iso_file local iso_loop local iso_size_bytes local iso_size_kb local mem_available_kb local mem_required_kb # keep some main memory for the installation process local mem_reserve_kb=524288 local message iso_file="${isoscan_mount}/$(getarg iso-scan/filename)" iso_loop=$(readlink -f "${iso_device}") if [[ ! -f "${iso_file}" ]] || [[ ! "${iso_loop}" =~ ^/dev/loop ]];then report_and_quit "Install ISO file not found on ${isoscan_mount}" fi iso_size_bytes=$(lsblk -n -b -d -o SIZE "${iso_loop}") iso_size_kb=$((iso_size_bytes / 1024 + 1024)) mem_required_kb=$((iso_size_kb + mem_reserve_kb)) mem_available_kb=$(awk '/^MemAvailable:/ { print $2 }' /proc/meminfo) if [[ "${mem_required_kb}" -gt "${mem_available_kb:-0}" ]];then message="Install ISO file is stored on target disk ${target}." message="${message} Not enough memory to copy it away, required:" message="${message} ${mem_required_kb}kB available:" message="${message} ${mem_available_kb}kB" report_and_quit "${message}" fi info "Install ISO file is stored on target disk ${target}" info "Copying ${iso_file} into main memory..." mkdir -p "${iso_ram_mount}" if ! mount -t tmpfs -o "size=${iso_size_kb}k,mode=0755" \ tmpfs "${iso_ram_mount}" then report_and_quit "Failed to create tmpfs for install ISO copy" fi if ! cp "${iso_file}" "${iso_ram_file}";then report_and_quit "Failed to copy install ISO file into main memory" fi # release the target disk udev_pending if ! losetup -d "${iso_loop}";then report_and_quit "Failed to detach install ISO loop ${iso_loop}" fi udev_pending if ! umount "${isoscan_mount}";then report_and_quit "Failed to release ${isoscan_mount} on ${target}" fi rm -f /run/initramfs/isoscandev # attach the ISO copy, udev recreates the install media label link if ! losetup -f -r "${iso_ram_file}";then report_and_quit "Failed to attach install ISO copy ${iso_ram_file}" fi udev_pending if [[ ! -e "${iso_device}" ]];then report_and_quit "Install ISO device ${iso_device} not found" fi } function get_local_image_source_files { declare root=${root} local iso_device="${root#install:}" local iso_mount_point=/run/install local image_mount_point=/run/image local sha_extension local image_source local image_sha sha_extension=$(shasum_extension) mkdir -m 0755 -p "${iso_mount_point}" if ! mount -n "${iso_device}" "${iso_mount_point}"; then report_and_quit "Failed to mount install ISO device" fi mkdir -m 0755 -p "${image_mount_point}" if ! mount -n "${iso_mount_point}"/*.squashfs ${image_mount_point};then report_and_quit "Failed to mount install image squashfs filesystem" fi image_source="$(echo "${image_mount_point}"/*.raw)" image_sha="$(echo "${image_mount_point}"/*."${sha_extension}")" echo "${image_source}|${image_sha}" } function get_remote_image_source_files { local image_uri local sha_extension local image_sha local install_dir=/run/install local metadata_dir="${install_dir}/boot/remote/loader" sha_extension=$(shasum_extension) local image_sha="${install_dir}/image.${sha_extension}" mkdir -p "${metadata_dir}" image_uri=$(getarg rd.kiwi.install.image=) # make sure the protocol type is tftp for metadata files. There is no need for # complex protocol types on small files and for standard PXE boot operations # only tftp can be guaranteed image_sha_uri=$( echo "${image_uri}" | awk '{ gsub("\\.xz",".'"${sha_extension}"'", $1); gsub("dolly:","tftp:", $1); print $1 }' ) image_initrd_uri=$( echo "${image_uri}" | awk '{ gsub("\\.xz",".initrd", $1); gsub("dolly:","tftp:", $1); print $1 }' ) image_kernel_uri=$( echo "${image_uri}" | awk '{ gsub("\\.xz",".kernel", $1); gsub("dolly:","tftp:", $1); print $1 }' ) image_config_uri=$( echo "${image_uri}" | \ awk '{ gsub("\\.xz",".config.bootoptions", $1); gsub("dolly:","tftp:", $1); print $1 }' ) # if we can not access image_sha_uri, maybe network setup # by dracut did fail, so collect some additional info if ! fetch_file "${image_sha_uri}" > "${image_sha}";then { echo "--- ip a ---"; ip a echo "--- ip r ---"; ip r } >> /tmp/fetch.info 2>&1 show_log_and_quit \ "Failed to fetch ${image_sha_uri}" /tmp/fetch.info fi if ! getargbool 0 rd.kiwi.ramdisk; then if ! fetch_file "${image_kernel_uri}" > "${metadata_dir}/linux";then show_log_and_quit \ "Failed to fetch ${image_kernel_uri}" /tmp/fetch.info fi if ! fetch_file "${image_initrd_uri}" > "${install_dir}/initrd.system_image" then show_log_and_quit \ "Failed to fetch ${image_initrd_uri}" /tmp/fetch.info fi fi if ! fetch_file "${image_config_uri}" > "/config.bootoptions" then show_log_and_quit \ "Failed to fetch ${image_config_uri}" /tmp/fetch.info fi echo "${image_uri}|${image_sha}" } #====================================== # Perform image dump/install operations #-------------------------------------- setup_debug lib_initialize udev_pending check_partition_based_compatibility image_target=$(get_selected_disk) if getargbool 0 rd.kiwi.oem.disk.consistency; then validate_disk_selection "${image_target}" fi if ! getargbool 0 rd.kiwi.install.pxe && \ target_holds_isoscan_file "${image_target}" then move_isoscan_file_to_ram "${image_target}" fi if getargbool 0 rd.kiwi.install.pxe; then image_source_files=$(get_remote_image_source_files) else image_source_files=$(get_local_image_source_files) fi export_image_metadata "${image_source_files}" if getargbool 0 rd.kiwi.install.partition_based; then export kiwi_oemskipverify=true fi if getargbool 0 rd.kiwi.install.pxe; then dump_image "${image_source_files}" "${image_target}" "remote_image" else dump_image "${image_source_files}" "${image_target}" fi check_image_integrity "${image_target}" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/kiwi-installer-device.sh0000755000000000000000000000024115273675667021506 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type setup_debug >/dev/null 2>&1 || . /lib/kiwi-lib.sh setup_debug info "Install Device is $1" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/kiwi-installer-genrules.sh0000755000000000000000000000102015273675667022067 0ustar00#!/bin/bash declare root=${root} case "${root}" in install:/dev/*) { printf 'KERNEL=="%s", RUN+="/sbin/initqueue --settled --onetime --unique /sbin/kiwi-installer-device %s"\n' \ "${root#install:/dev/}" "${root#install:}" printf 'SYMLINK=="%s", RUN+="/sbin/initqueue --settled --onetime --unique /sbin/kiwi-installer-device %s"\n' \ "${root#install:/dev/}" "${root#install:}" } >> /etc/udev/rules.d/99-installer-kiwi.rules wait_for_dev -n "${root#install:}" ;; esac ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/kiwi-mount-ramdisk.sh0000755000000000000000000000202415273675667021047 0ustar00#!/bin/bash type setup_debug >/dev/null 2>&1 || . /lib/kiwi-lib.sh type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh #====================================== # Functions #-------------------------------------- function mount_ramdisk { local root_dev local boot_options boot_options="/config.bootoptions" if [ ! -e "${boot_options}" ]; then die "Missing ${boot_options} file" fi # Add a space to /config.bootoptions to make sure the # following token based read captures all entries echo -n ' ' >> "${boot_options}" root_dev=$( while read -r -d ' ' opt; do echo "${opt}";done < "${boot_options}" |\ grep root= | cut -f2- -d= ) if [ -z "${root_dev}" ]; then die "'root=' argument not found in ${boot_options}" fi mount --options defaults "${root_dev}" /sysroot } #====================================== # Mount ramdisk root in /sysroot #-------------------------------------- setup_debug if getargbool 0 rd.kiwi.ramdisk; then mount_ramdisk fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/kiwi-ramdisk-deployment-generator.sh0000755000000000000000000000321115273675667024050 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh if ! getargbool 0 rd.kiwi.ramdisk; then # we create a sysroot generator only for the special # ramdisk deployment case exit 0 fi root_cmdline=$(getarg root=) if [ -z "${root_cmdline}" ];then # if the root= parameter is missing there is no guarantee # that the /config.bootoptions file is present on local # storage at the time this generator code is called. This # case applies on PXE deployments which serves config.bootoptions # from the network. The mount process including config.bootoptions # paramters is then handled by the dracut # mount hook: kiwi-mount-ramdisk.sh exit 0 fi GENERATOR_DIR="$1" [ -z "${GENERATOR_DIR}" ] && exit 1 [ -d "${GENERATOR_DIR}" ] || mkdir -p "${GENERATOR_DIR}" [ -e /config.bootoptions ] || exit 1 root_uuid=$( awk '{ for(i=1; i <= NF; i++) {print $i } }' /config.bootoptions |\ grep root= | cut -f2- -d= ) [ -z "${root_uuid}" ] && exit 2 { echo "[Unit]" echo "Before=initrd-root-fs.target" echo "DefaultDependencies=no" echo "[Mount]" echo "Where=/sysroot" echo "What=${root_uuid}" _dev=$(systemd-escape -p /dev/disk/by-uuid/"${root_uuid#UUID=}") } > "$GENERATOR_DIR"/sysroot.mount if [ ! -e "$GENERATOR_DIR/initrd-root-fs.target.requires/sysroot.mount" ]; then mkdir -p "$GENERATOR_DIR"/initrd-root-fs.target.requires ln -s "$GENERATOR_DIR"/sysroot.mount \ "$GENERATOR_DIR"/initrd-root-fs.target.requires/sysroot.mount fi mkdir -p "$GENERATOR_DIR/$_dev.device.d" { echo "[Unit]" echo "JobTimeoutSec=infinity" } > "$GENERATOR_DIR/$_dev.device.d/timeout.conf" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/module-setup.sh0000755000000000000000000000320015273675667017734 0ustar00#!/bin/bash # called by dracut check() { return 255 } # called by dracut depends() { echo network rootfs-block dm kiwi-lib return 0 } # called by dracut installkernel() { instmods squashfs loop iso9660 vfat nls_cp437 nls_iso8859-1 } # called by dracut install() { declare moddir=${moddir} declare systemdutildir=${systemdutildir} declare dracutbasedir=${dracutbasedir} local dmsquashdir= inst_multiple \ tr lsblk dd sha256sum sha512sum head pv kexec \ basename awk kpartx sort mount mountpoint readlink \ btrfs btrfstune mount umount losetup cp inst_multiple -o efibootmgr # support booting the install ISO as a file on disk via # iso-scan/filename=, e.g. through a grub loopback setup dmsquashdir=$(find "${dracutbasedir}/modules.d" -name "*dmsquash-live") if [[ -n "${dmsquashdir}" ]] && \ [[ -f "${dmsquashdir}/parse-iso-scan.sh" ]] && \ [[ -f "${dmsquashdir}/iso-scan.sh" ]]; then inst_hook cmdline 31 "${dmsquashdir}/parse-iso-scan.sh" inst_script "${dmsquashdir}/iso-scan.sh" "/sbin/iso-scan" fi inst_hook pre-udev 30 "${moddir}/kiwi-installer-genrules.sh" inst_script "${moddir}/kiwi-installer-device.sh" \ "/sbin/kiwi-installer-device" inst_hook cmdline 30 "${moddir}/parse-kiwi-install.sh" inst_hook pre-mount 30 "${moddir}/kiwi-dump-image.sh" inst_hook mount 30 "${moddir}/kiwi-mount-ramdisk.sh" inst_script "${moddir}/kiwi-ramdisk-deployment-generator.sh" \ "${systemdutildir}/system-generators/dracut-kiwi-ramdisk-generator" inst_rules 60-cdrom_id.rules dracut_need_initqueue } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-dump/parse-kiwi-install.sh0000755000000000000000000000167315273675667021044 0ustar00#!/bin/bash # install images are specified with # root=install:CDLABEL=label type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh [ -z "${root}" ] && root=$(getarg root=) if getargbool 0 rd.kiwi.install.pxe; then root="install:REMOTE" echo "rd.neednet=1" > /etc/cmdline.d/kiwi-generated.conf if ! getarg "ip="; then echo "ip=dhcp" >> /etc/cmdline.d/kiwi-generated.conf fi fi if [ "${root%%:*}" = "install" ] ; then installroot=${root} fi [ "${installroot%%:*}" = "install" ] || return 1 modprobe -q loop case "${installroot}" in install:CDLABEL=*|CDLABEL=*) \ root="${root#install:}" root="${root//\//\\x2f}" root="install:/dev/disk/by-label/${root#CDLABEL=}" rootok=1 ;; install:REMOTE) \ rootok=1 ;; esac [ "$rootok" = "1" ] || return 1 info "root was ${installroot}, is now ${root}" # make sure that init doesn't complain [ -z "${root}" ] && root="install" return 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7381246 kiwi-11.1.8/dracut/modules.d/55kiwi-live/Makefile0000644000000000000000000000022615273675667016411 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/55kiwi-live cp -a * ${buildroot}usr/lib/dracut/modules.d/55kiwi-live ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-generator.sh0000755000000000000000000000342515273675667020243 0ustar00#!/bin/bash -x type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type getOverlayBaseDirectory >/dev/null 2>&1 || . /lib/kiwi-live-lib.sh [ -z "${root}" ] && root=$(getarg root=) if [ "${root%%:*}" = "live" ] ; then liveroot="${root}" fi [ "${liveroot%%:*}" = "live" ] || exit 0 case "${liveroot}" in live:CDLABEL=*|CDLABEL=*) \ root="${root#live:}" root="${root//\//\\x2f}" root="live:/dev/disk/by-label/${root#CDLABEL=}" rootok=1 ;; live:AOEINTERFACE=*|AOEINTERFACE=*) \ root="${root#live:}" root="${root//\//\\x2f}" root="live:aoe:/dev/etherd/${root#AOEINTERFACE=}" rootok=1 ;; live:ftp:*|live:http:*|live:https:*|live:dolly:*) \ root="${root#live:}" root="live:net:${root}" rootok=1 ;; esac [ "${rootok}" != "1" ] && exit 0 GENERATOR_DIR="$2" [ -z "$GENERATOR_DIR" ] && exit 1 [ -d "$GENERATOR_DIR" ] || mkdir "$GENERATOR_DIR" OVERLAY_BASE="$(getOverlayBaseDirectory)" ROOTFLAGS="$(getarg rootflags)" { echo "[Unit]" echo "Before=initrd-root-fs.target" echo "DefaultDependencies=no" echo "[Mount]" echo "Where=/sysroot" echo "What=LiveOS_rootfs" echo "Options=${ROOTFLAGS},lowerdir=${OVERLAY_BASE}/rootfsbase,upperdir=${OVERLAY_BASE}/overlayfs/rw,workdir=${OVERLAY_BASE}/overlayfs/work" echo "Type=overlay" _dev=LiveOS_rootfs } > "$GENERATOR_DIR"/sysroot.mount if [ ! -e "$GENERATOR_DIR/initrd-root-fs.target.requires/sysroot.mount" ]; then mkdir -p "$GENERATOR_DIR"/initrd-root-fs.target.requires ln -s "$GENERATOR_DIR"/sysroot.mount \ "$GENERATOR_DIR"/initrd-root-fs.target.requires/sysroot.mount fi mkdir -p "$GENERATOR_DIR/$_dev.device.d" { echo "[Unit]" echo "JobTimeoutSec=3000" } > "$GENERATOR_DIR/$_dev.device.d/timeout.conf" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-live-checkmedia.sh0000755000000000000000000000042215273675667021261 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type runMediaCheck >/dev/null 2>&1 || . /lib/kiwi-live-lib.sh if getargbool 0 mediacheck || getargbool 0 rd.live.check; then declare root=${root} initGlobalDevices "${root#live:}" runMediaCheck fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-live-genrules.sh0000755000000000000000000000255715273675667021043 0ustar00#!/bin/bash # shellcheck disable=SC1091 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh declare root=${root} case "${root}" in live:/dev/*) { printf 'KERNEL=="%s", RUN+="/sbin/initqueue %s %s %s"\n' \ "${root#live:/dev/}" "--settled --onetime --unique" \ "/sbin/kiwi-live-root" "${root#live:}" printf 'SYMLINK=="%s", RUN+="/sbin/initqueue %s %s %s"\n' \ "${root#live:/dev/}" "--settled --onetime --unique" \ "/sbin/kiwi-live-root" "${root#live:}" } >> /etc/udev/rules.d/99-live-kiwi.rules wait_for_dev -n "${root#live:}" ;; live:aoe:/dev/*) { printf 'KERNEL=="%s", RUN+="/sbin/initqueue %s %s %s"\n' \ "${root#live:aoe:/dev/}" "--settled --onetime --unique" \ "/sbin/kiwi-live-root" "${root#live:aoe:}" } >> /etc/udev/rules.d/99-live-aoe-kiwi.rules wait_for_dev -n "${root#live:aoe:}" ;; live:net:*) system=$(getarg rd.kiwi.live.system=) if [ -z "${system}" ];then system=/dev/ram0 fi { printf 'KERNEL=="%s", RUN+="/sbin/initqueue %s %s %s"\n' \ "${system#/dev/}" "--settled --onetime --unique" \ "/sbin/kiwi-live-root" "${root}" } >> /etc/udev/rules.d/99-live-net-kiwi.rules wait_for_dev -n "${system}" ;; esac ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-live-lib.sh0000755000000000000000000004773315273675667017772 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh function getOverlayBaseDirectory { # initialize and print overlay base directory below which # the iso and overlayfs based mountpoints are managed local overlay_base=/run/overlay local overlay_size mkdir -p "${overlay_base}" if ! mountpoint -q "${overlay_base}";then overlay_size=$(getarg rd.live.overlay.size) [ -z "${overlay_size}" ] && overlay_size="50%" mount -t tmpfs -o "size=${overlay_size}" tmpfs "${overlay_base}" fi echo "${overlay_base}" } function ProvideIso { local root=$1 local iso_url=${root#live:net:} local system local rd_size local custom_rd_size local modfile=/etc/modprobe.d/99-brd.conf system=$(getarg rd.kiwi.live.system=) if [ ! -b "${system}" ];then system=/dev/ram0 fi ptable=$(blkid -s PTTYPE -o value "${system}" 2>/dev/null) case "${iso_url}" in ftp:*|http:*|https:*|dolly:*) \ if [ -z "${ptable}" ] || getargbool 0 rd.kiwi.live.reset; then custom_rd_size=$(getarg ramdisk_size=) if [ -n "${custom_rd_size}" ];then rd_size="${custom_rd_size}" else # set minimum default ramdisk size: 2G rd_size="2097152" fi mkdir -p /etc/modprobe.d if [ -n "${rd_size}" ];then echo "options brd rd_size=${rd_size}" > ${modfile} fi modprobe --remove brd modprobe brd udev_pending fetch_file "${iso_url}" | dd bs=32k of="${system}" &>/dev/null fi if [[ ${system} =~ ^/dev/ram ]];then echo "${system}" else udev_pending # This is not 100% safe as in theory the volume ID # of the live ISO could be configured in the kiwi image # description and doesn't have to be set to: CDROM. # The value could be obtained as follows: # label=$( # isoinfo -j utf-8 -d -i ${system} |\ # grep "Volume id:" | cut -f2 -d: # ) # but this would require an additional tool as part of the # initrd. Thus this is subject to a followup pull request echo "/dev/disk/by-label/CDROM" fi ;; *) \ echo "${root}" ;; esac } function lookupIsoDiskDevice { local root=$1 local iso_label=${root#/dev/disk/by-label/} local disk local mount_point local iso_volid for disk in $(lsblk -p -n -r -o NAME,TYPE | grep disk | cut -f1 -d' ');do if [[ ${disk} =~ ^/dev/fd ]];then # ignore floppy disk devices continue fi # 1. Mapping: # If the ISO is bootet as disk in hybrid mode the exposed LABEL # of the disk device matches the ISO label iso_volid=$(blkid -s LABEL -o value "${disk}") if [ "${iso_volid}" = "${iso_label}" ];then echo "${disk}%disk_boot" return fi # 2. Mapping: # If the ISO is booted via grub loopback the partition of the # disk device which contains the ISO file and was loopbacked # by grub is mounted to /run/initramfs/isoscan for mount_point in $(lsblk -p -n -r -o MOUNTPOINT "${disk}");do if [ "${mount_point}" = "/run/initramfs/isoscan" ];then echo "${disk}%loop_boot" return fi done done } function setupDebugMode { if getargbool 0 rd.kiwi.debug; then local log=/run/initramfs/log mkdir -p ${log} exec > ${log}/boot.kiwi exec 2>> ${log}/boot.kiwi set -x fi } function initialize { local profile=/.profile test -f ${profile} && cat ${profile} } function loadKernelModules { modprobe squashfs } function udev_pending { declare DEVICE_TIMEOUT=${DEVICE_TIMEOUT} local limit=120 if [[ "${DEVICE_TIMEOUT}" =~ ^[0-9]+$ ]]; then limit="${DEVICE_TIMEOUT}" fi udevadm settle --timeout="${limit}" } function set_device_lock { # """ # Set device lock, preferrable via udevadm or via # flock as fallback if systemd/udev does not provide # the command # """ if udevadm lock --help &>/dev/null;then udevadm lock --device "$@" else flock -x "$@" fi } function initGlobalDevices { if [ -z "$1" ]; then die "No root device for operation given" fi isodev="$1" local isodisk isodisk=$(lookupIsoDiskDevice "${isodev}") isodiskdev=$(echo "${isodisk}" | cut -f1 -d%) isodiskmode=$(echo "${isodisk}" | cut -f2 -d%) isofs_type=$(blkid -s TYPE -o value "${isodev}") media_check_device=${isodev} if [ -n "${isodiskdev}" ]; then media_check_device=${isodiskdev} fi export media_check_device export isodev export isodiskdev export isodiskmode export isofs_type } function initGlobalOptions { live_dir=$(getarg rd.live.dir -d live_dir) [ -z "${live_dir}" ] && live_dir="LiveOS" squash_image=$(getarg rd.live.squashimg) [ -z "${squash_image}" ] && squash_image="squashfs.img" cow_filesystem=$(getarg rd.live.overlay.cowfs) [ -z "${cow_filesystem}" ] && cow_filesystem="ext4" cow_file_mbsize=$(getarg rd.live.cowfile.mbsize) [ -z "${cow_file_mbsize}" ] && cow_file_mbsize="500" cow_file_path=$(getarg rd.live.cowfile.path) [ -z "${cow_file_path}" ] && cow_file_path="live_system.cow" } function mountIso { local overlay_base overlay_base=$(getOverlayBaseDirectory) ln -s "${isodev}" "${overlay_base}/isodev" local iso_mount_point="${overlay_base}/live" mkdir -m 0755 -p "${iso_mount_point}" if ! mount -o ro -n -t "${isofs_type}" "${isodev}" "${iso_mount_point}"; then die "Failed to mount live ISO device" fi echo "${iso_mount_point}" } function mountCompressedContainerFromIso { local iso_mount_point=$1 local overlay_base local squashfs_container overlay_base=$(getOverlayBaseDirectory) local container_mount_point="${overlay_base}/squashfs_container" squashfs_container="${iso_mount_point}/${live_dir}/${squash_image}" mkdir -m 0755 -p "${container_mount_point}" if activate_verity "${squashfs_container}";then squashfs_container=/dev/mapper/verityroot fi if activate_luks "${squashfs_container}" luks; then squashfs_container=/dev/mapper/luks fi if ! mount -n "${squashfs_container}" "${container_mount_point}";then die "Failed to mount live ISO compressed read-only container" fi echo "${container_mount_point}" } function activate_verity { # The method loopback mounts the given compressed root image # and calls veritysetup to activate the verity device. The # activatrion of the verity device can also be done with help # from systemd-veritysetup which would automatically be called # after losetup if the dracut module systemd-veritysetup # would be included to the initrd. Unfortunately no proper # veritytab file can be provided if the root image is based on # squashfs because there is no support for UUIDs in squashfs # which means the veritytab would have to be modified with the # plain loopback device after the losetup call. Such a modification # then requires to call "systemctl daemon-reload" followed by # "systemctl restart systemd-veritysetup@verityroot" to make # systemd-veritysetup aware of the changes in the veritytab. # To avoid this complexity the verity device is activated via # veritysetup directly. local compressed_root=$1 local verity_loop local verity_name local data_device local hash_device local root_hash local verity_options local kiwi_verity_options local veritysetup local option if [ -f /etc/veritytab ];then read -r \ verity_name data_device hash_device root_hash verity_options \ < /etc/veritytab verity_loop=$(losetup --show --find "${compressed_root}") udevadm wait "${verity_loop}" if [ "${data_device}" = "PLACEHOLDER" ];then # squashfs does not support UUIDs, therefore the initial # veritytab only contains a placeholder for the device name sed -ie "s|PLACEHOLDER|${verity_loop}|g" /etc/veritytab fi read -r \ verity_name data_device hash_device root_hash verity_options \ < /etc/veritytab if [ "$(echo "${data_device}" | cut -f1 -d=)" = "UUID" ];then data_device=/dev/disk/by-uuid/$(echo "${data_device}" | cut -f2 -d=) udevadm wait "${data_device}" fi if [ "$(echo "${hash_device}" | cut -f1 -d=)" = "UUID" ];then hash_device=/dev/disk/by-uuid/$(echo "${hash_device}" | cut -f2 -d=) udevadm wait "${hash_device}" fi # Read kernel command line verity options and merge the options kiwi_verity_options=$(getarg rd.kiwi.verity_options=) if [ -n "${kiwi_verity_options}" ]; then if [ -n "${verity_options}" ]; then verity_options="${verity_options},${kiwi_verity_options}" else verity_options="${kiwi_verity_options}" fi fi veritysetup="veritysetup open " veritysetup="${veritysetup} ${data_device} ${verity_name} " veritysetup="${veritysetup} ${hash_device} ${root_hash}" for option in $(echo "${verity_options}" | tr , " ");do veritysetup="${veritysetup} --${option}" done eval "${veritysetup}" set_device_lock "${verity_loop}" \ udevadm wait "/dev/mapper/verityroot" return 0 else return 1 fi } function activate_luks { local rootfs_image=$1 local mapname=$2 declare kiwi_luks_empty_passphrase=${kiwi_luks_empty_passphrase} if ! cryptsetup isLuks "${rootfs_image}" &>/dev/null;then return 1 fi if [ "${kiwi_luks_empty_passphrase}" = "true" ];then cryptsetup \ --key-file /dev/zero \ --keyfile-size 32 \ luksOpen "${rootfs_image}" "${mapname}" else systemd-cryptsetup attach "${mapname}" "${rootfs_image}" fi } function mountReadOnlyRootImageFromContainer { local container_mount_point=$1 local overlay_base overlay_base=$(getOverlayBaseDirectory) local rootfs_image="${container_mount_point}/${live_dir}/rootfs.img" local root_mount_point="${overlay_base}/rootfsbase" mkdir -m 0755 -p "${root_mount_point}" if activate_luks "${rootfs_image}" luks; then rootfs_image=/dev/mapper/luks fi if ! [ -e "${rootfs_image}" ] && [ -d "${container_mount_point}/proc" ]; then # It's the root filesystem directly, just do a bind mount mount -n --bind "${container_mount_point}" "${root_mount_point}" echo "${root_mount_point}" return fi if ! mount -n "${rootfs_image}" "${root_mount_point}"; then die "Failed to mount live ISO root filesystem" fi echo "${root_mount_point}" } function prepareTemporaryOverlay { local overlay_base overlay_base=$(getOverlayBaseDirectory) mkdir -m 0755 -p "${overlay_base}/overlayfs/rw" mkdir -m 0755 -p "${overlay_base}/overlayfs/work" } function preparePersistentOverlay { if [ -z "${isodiskmode}" ] || [ -z "${isodiskdev}" ]; then return 1 fi local overlay_base overlay_base=$(getOverlayBaseDirectory) local overlay_mount_point="${overlay_base}/overlayfs" mkdir -m 0755 -p "${overlay_mount_point}" if [ "${isodiskmode}" = "disk_boot" ];then if ! preparePersistentOverlayDiskBoot "${overlay_mount_point}"; then return 1 fi else if ! preparePersistentOverlayLoopBoot "${overlay_mount_point}"; then return 1 fi fi mkdir -m 0755 -p "${overlay_mount_point}/rw" mkdir -m 0755 -p "${overlay_mount_point}/work" } function preparePersistentOverlayLoopBoot { # Create or re-use a cow file on the filesystem the iso # was loopback booted from. If the file already exists # it will be used as presented. If not it will be created # with the custom size configured in rd.live.cowfile.mbsize # or the default size of 500MB local overlay_mount_point=$1 local isoscan_loop_mount=/run/initramfs/isoscan local cow_file_name="${isoscan_loop_mount}/${cow_file_path}" mkdir -m 0755 -p "${overlay_mount_point}" if ! mount -o "remount,rw" "${isoscan_loop_mount}"; then return 1 fi if activate_luks "${cow_file_name}" lukscow; then cow_file_name=/dev/mapper/lukscow fi if ! mount "${cow_file_name}" "${overlay_mount_point}"; then if ! dd if=/dev/zero of="${cow_file_name}" \ count=0 bs=1M seek="${cow_file_mbsize}"; then return 1 fi if getargbool 0 rd.live.encrypt; then if prepareEncryptedOverlay "${cow_file_name}" lukscow; then cow_file_name=/dev/mapper/lukscow fi fi if ! mkfs."${cow_filesystem}" "${cow_file_name}"; then return 1 fi if ! mount "${cow_file_name}" "${overlay_mount_point}"; then return 1 fi fi } function preparePersistentOverlayDiskBoot { # Create a write partition and filesystem on the iso # disk device with the cow label and mount it. If the # disk already populates a cow labeled partition it is # used as presented local overlay_mount_point=$1 local partitions_before_cow_part mkdir -m 0755 -p "${overlay_mount_point}" activate_luks /dev/disk/by-label/lukscow lukscow if ! mount -L cow "${overlay_mount_point}"; then partitions_before_cow_part=$(_partition_count) echo -e "n\np\n\n\n\nw\nq" | fdisk "${isodiskdev}" if ! set_device_lock "${isodiskdev}" partx -u "${isodiskdev}"; then return 1 fi udev_pending if [ "$(_partition_count)" -le "${partitions_before_cow_part}" ];then return 1 fi local write_partition write_partition=$(lsblk "${isodiskdev}" -p -l -n -o NAME | tail -n1) if getargbool 0 rd.live.encrypt; then if prepareEncryptedOverlay "${write_partition}" lukscow; then write_partition=/dev/mapper/lukscow fi fi if ! mkfs."${cow_filesystem}" -L cow "${write_partition}"; then return 1 fi if ! mount -L cow "${overlay_mount_point}"; then return 1 fi fi } function prepareEncryptedOverlay { # Create LUKS on provided read-write overlay local device=$1 local label=$2 cat >/run/dracut-interactive <<-EOF cryptsetup -q luksFormat \\ --force-password --type luks2 --label "${label}" "${device}" EOF _run_interactive systemd-cryptsetup attach "${label}" "${device}" } function runMediaCheck { # messages are redirected to stderr because this code is called # as part of the pre-mount hook via the dracut-pre-mount.service # which only shows messages on stderr to the console and we want # to see the check results during boot if ! command -v checkmedia &>/dev/null; then echo "No mediacheck program installed, mediacheck skipped" 1>&2 return fi local timeout=20 local check_result check_result=/run/initramfs/checkmedia.result set_device_lock "${media_check_device}" \ checkmedia --verbose "${media_check_device}" &>${check_result} local check_status=$? if [ ${check_status} != 0 ];then echo "ISO check failed" >> ${check_result} else echo "ISO check passed" >> ${check_result} fi echo "Press key to continue (waiting ${timeout}sec...)" >> ${check_result} _run_dialog --timeout ${timeout} --tailbox ${check_result} 20 70 if [ ${check_status} != 0 ];then die "Failed to verify system integrity" fi } function fetch_file { # Fetch file from remote location local source_url=$1 local source_uncompressed_bytes=$2 local fetch_info=/tmp/fetch.info local fetch local curl_options local dolly_options curl_options=$(getarg rd.kiwi.live.curl_options=) dolly_options=$(getarg rd.kiwi.live.dolly_options=) if [ -n "${curl_options}" ]; then curl_options=$(str_replace "${curl_options}" "," " ") fetch="curl ${curl_options} -f ${source_url}" else fetch="curl -f ${source_url}" fi if [ -n "${dolly_options}" ]; then dolly_options=$(str_replace "${dolly_options}" "," " ") fetch="dolly ${dolly_options} -f ${source_url}" fi if _is_dolly "${source_url}";then fetch="dolly" fi if _is_xz_compressed "${source_url}";then fetch="${fetch} | xz -d" fi if [ -z "${source_uncompressed_bytes}" ];then eval "${fetch}" 2>${fetch_info} else eval "${fetch}" 2>${fetch_info} |\ pv --size "${source_uncompressed_bytes}" --stop-at-size -n fi return "${PIPESTATUS[0]}" } function show_log_and_quit { local text_message="$1" local log_file="$2" local timeout timeout=$(_dialog_timeout) if [ "${timeout}" = "off" ];then _run_dialog --backtitle "\"${text_message}\"" \ --textbox "${log_file}" 15 80 else _run_dialog --timeout "${timeout}" --backtitle "\"${text_message}\"" \ --textbox "${log_file}" 15 80 fi if getargbool 0 rd.debug; then die "${text_message}" else reboot -f fi } #========================================= # Methods considered private #----------------------------------------- function _is_xz_compressed { local source_url=$1 [[ ${source_url} =~ .xz$ ]] } function _is_dolly { local source_url=$1 [[ ${source_url} =~ ^dolly ]] } function _setup_interactive_service { local service=/run/systemd/system/dracut-run-interactive.service mkdir -p /run/systemd/system [ -e ${service} ] && return { echo "[Unit]" echo "Description=Dracut Run Interactive" echo "DefaultDependencies=no" echo "After=systemd-vconsole-setup.service" echo "Wants=systemd-vconsole-setup.service" echo "Conflicts=emergency.service emergency.target" echo "[Service]" echo "Environment=HOME=/" echo "Environment=DRACUT_SYSTEMD=1" echo "Environment=NEWROOT=/sysroot" echo "WorkingDirectory=/" echo "ExecStart=/bin/bash /run/dracut-interactive" echo "Type=oneshot" echo "StandardInput=tty-force" echo "StandardOutput=inherit" echo "StandardError=inherit" echo "KillMode=process" echo "IgnoreSIGPIPE=no" echo "KillSignal=SIGHUP" } > ${service} } function _run_interactive { _setup_interactive_service systemctl start dracut-run-interactive.service } function _dialog_timeout { local timeout=60 custom_timeout=$(getarg "rd.kiwi.dialog.timeout=") [ -n "${custom_timeout}" ] && timeout="${custom_timeout}" echo "${timeout}" } function _run_dialog { # """ # Run the dialog program via the systemd service either in a # framebuffer terminal or on the console. The return code of # the function is the return code of the dialog call. The # output of the dialog call is stored in a file and can be # one time read via the get_dialog_result function # """ stop_plymouth local dialog_result=/tmp/dialog_result local dialog_exit_code=/tmp/dialog_code { echo "dialog $* 2>$dialog_result" echo "echo -n \$? >$dialog_exit_code" } >/run/dracut-interactive _run_interactive return "$(cat $dialog_exit_code)" } function stop_plymouth { if ! getargbool 0 rd.kiwi.allow_plymouth; then if command -v plymouth &>/dev/null;then plymouth --quit --wait fi fi } function _partition_count { if [ -z "${isodiskdev}" ]; then echo 0 else lsblk "${isodiskdev}" -p -r -n -o TYPE | grep -c part fi } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-live-net-genrules.sh0000755000000000000000000000060515273675667021617 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh if getargbool 0 rd.kiwi.live.pxe; then { echo -n "SUBSYSTEM==\"net\", ACTION==\"add\", " echo -n "DRIVERS==\"?*\", ATTR{address}==\"?*\", " echo -n "ATTR{dev_id}==\"0x0\", ATTR{type}==\"1\", " echo -n "KERNEL==\"?*\", NAME=\"lan0\"" echo } > /etc/udev/rules.d/90-net.rules fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/kiwi-live-root.sh0000755000000000000000000000227215273675667020174 0ustar00#!/bin/bash # called from /etc/udev/rules.d/99-live-kiwi.rules type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type mountIso >/dev/null 2>&1 || . /lib/kiwi-live-lib.sh PATH=/usr/sbin:/usr/bin:/sbin:/bin # init debug log file if wanted setupDebugMode # initialize profile environment initialize # load required kernel modules loadKernelModules # provide ISO when remote root=$(ProvideIso "$1") # device nodes and types initGlobalDevices "$root" # live options and their default values initGlobalOptions # prepare overlay for generated systemd LiveOS_rootfs service declare isodiskdev=${isodiskdev} if getargbool 0 rd.live.overlay.persistent && [ -n "${isodiskdev}" ]; then if ! preparePersistentOverlay; then warn "Failed to setup persistent write space !" warn "Falling back to temporary overlay" prepareTemporaryOverlay fi else prepareTemporaryOverlay fi # mount ISO device iso_mount_point=$(mountIso) # mount squashfs compressed container container_mount_point=$( mountCompressedContainerFromIso "${iso_mount_point}" ) # mount readonly root filesystem from container mountReadOnlyRootImageFromContainer "${container_mount_point}" need_shutdown exit 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/module-setup.sh0000755000000000000000000000354615273675667017743 0ustar00#!/bin/bash # called by dracut check() { # a live host-only image doesn't really make a lot of sense declare hostonly=${hostonly} [[ ${hostonly} ]] && return 1 return 255 } # called by dracut depends() { # The kiwi-live hooks are bash scripts sourced by the dracut # /bin/sh scripts. Depending on bash makes bash the initrd # /bin/sh, also on hosts where /bin/sh is e.g. dash echo network rootfs-block dm bash return 0 } # called by dracut installkernel() { instmods squashfs loop aoe brd iso9660 overlay } # called by dracut install() { declare moddir=${moddir} declare systemdutildir=${systemdutildir} declare dracutbasedir=${dracutbasedir} local dmsquashdir= inst_multiple -o checkmedia inst_multiple \ umount dmsetup partx blkid lsblk dd losetup \ grep cut find wc fdisk tail mkfs.ext4 mkfs.xfs \ dialog cat mountpoint curl dolly dd cryptsetup veritysetup \ flock udevadm sed dmsquashdir=$(find "${dracutbasedir}/modules.d" -name "*dmsquash-live") if [ -n "${dmsquashdir}" ] && \ [ -f "${dmsquashdir}/parse-iso-scan.sh" ] && \ [ -f "${dmsquashdir}/iso-scan.sh" ]; then inst_hook cmdline 31 "${dmsquashdir}/parse-iso-scan.sh" inst_script "${dmsquashdir}/iso-scan.sh" "/sbin/iso-scan" fi inst_hook cmdline 30 "${moddir}/parse-kiwi-live.sh" inst_hook pre-udev 30 "${moddir}/kiwi-live-genrules.sh" inst_hook pre-udev 60 "${moddir}/kiwi-live-net-genrules.sh" inst_hook pre-mount 30 "${moddir}/kiwi-live-checkmedia.sh" inst_script "${moddir}/kiwi-live-root.sh" "/sbin/kiwi-live-root" inst_script "${moddir}/kiwi-generator.sh" \ "${systemdutildir}/system-generators/dracut-kiwi-generator" inst_simple "${moddir}/kiwi-live-lib.sh" "/lib/kiwi-live-lib.sh" inst_rules 60-cdrom_id.rules dracut_need_initqueue } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-live/parse-kiwi-live.sh0000755000000000000000000000234115273675667020320 0ustar00#!/bin/bash # live images are specified with # root=live:CDLABEL=label # root=live:AOEINTERFACE=name type getOverlayBaseDirectory >/dev/null 2>&1 || . /lib/kiwi-live-lib.sh [ -z "${root}" ] && root=$(getarg root=) if [ "${root%%:*}" = "live" ] ; then liveroot=${root} fi [ "${liveroot%%:*}" = "live" ] || return 1 modprobe -q loop case "${liveroot}" in live:CDLABEL=*|CDLABEL=*) \ root="${root#live:}" root="${root//\//\\x2f}" root="live:/dev/disk/by-label/${root#CDLABEL=}" rootok=1 ;; live:AOEINTERFACE=*|AOEINTERFACE=*) \ modprobe -q aoe root="${root#live:}" root="${root//\//\\x2f}" root="live:aoe:/dev/etherd/${root#AOEINTERFACE=}" rootok=1 ;; live:ftp:*|live:http:*|live:https:*|live:dolly:*) \ modprobe -q brd root="${root#live:}" root="live:net:${root}" rootok=1 ;; esac [ "$rootok" = "1" ] || return 1 info "root was ${liveroot}, is now ${root}" # make sure that init doesn't complain [ -z "${root}" ] && root="live" OVERLAY_BASE="$(getOverlayBaseDirectory)" wait_for_dev -n "${OVERLAY_BASE}/rootfsbase" wait_for_dev -n "${OVERLAY_BASE}/overlayfs/rw" wait_for_dev -n "${OVERLAY_BASE}/overlayfs/work" return 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7384837 kiwi-11.1.8/dracut/modules.d/55kiwi-overlay/Makefile0000644000000000000000000000023415273675667017132 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/55kiwi-overlay cp -a * ${buildroot}usr/lib/dracut/modules.d/55kiwi-overlay ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7387881 kiwi-11.1.8/dracut/modules.d/55kiwi-overlay/kiwi-generator.sh0000755000000000000000000000363115273675667020764 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type getOverlayBaseDirectory >/dev/null 2>&1 || . /lib/kiwi-filesystem-lib.sh [ -z "${root}" ] && root=$(getarg root=) if [ "${root%%:*}" = "overlay" ] ; then overlayroot=${root} fi # Generate sysroot unit only if overlay is requested [ "${overlayroot%%:*}" = "overlay" ] || exit 0 GENERATOR_DIR="$2" [ -z "$GENERATOR_DIR" ] && exit 1 [ -d "$GENERATOR_DIR" ] || mkdir "$GENERATOR_DIR" OVERLAY_BASE="$(getOverlayBaseDirectory)" ROOTFLAGS="$(getarg rootflags)" if getargbool 0 rd.root.overlay.readonly; then { echo "[Unit]" echo "Before=initrd-root-fs.target" echo "DefaultDependencies=no" echo "After=run-overlay-rootfsbase.mount" echo "After=run-overlay.mount" echo "[Mount]" echo "Where=/sysroot" echo "What=${OVERLAY_BASE}/rootfsbase" echo "Options=bind" echo "Type=none" _dev=ReadOnlyOS_rootfs } > "$GENERATOR_DIR"/sysroot.mount else { echo "[Unit]" echo "Before=initrd-root-fs.target" echo "DefaultDependencies=no" echo "After=run-overlay-rootfsbase.mount" echo "After=run-overlay.mount" echo "[Mount]" echo "Where=/sysroot" echo "What=OverlayOS_rootfs" echo "Options=${ROOTFLAGS},lowerdir=${OVERLAY_BASE}/rootfsbase,upperdir=${OVERLAY_BASE}/overlayfs/rw,workdir=${OVERLAY_BASE}/overlayfs/work" echo "Type=overlay" _dev=OverlayOS_rootfs } > "$GENERATOR_DIR"/sysroot.mount fi if [ ! -e "$GENERATOR_DIR/initrd-root-fs.target.requires/sysroot.mount" ]; then mkdir -p "$GENERATOR_DIR"/initrd-root-fs.target.requires ln -s "$GENERATOR_DIR"/sysroot.mount \ "$GENERATOR_DIR"/initrd-root-fs.target.requires/sysroot.mount fi mkdir -p "$GENERATOR_DIR/$_dev.device.d" { echo "[Unit]" echo "JobTimeoutSec=3000" } > "$GENERATOR_DIR/$_dev.device.d/timeout.conf" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7387881 kiwi-11.1.8/dracut/modules.d/55kiwi-overlay/kiwi-overlay-root.sh0000755000000000000000000000726615273675667021450 0ustar00#!/bin/bash # root=overlay:* was converted into a root=block:/dev/... node type getOverlayBaseDirectory >/dev/null 2>&1 || . /lib/kiwi-filesystem-lib.sh #====================================== # functions #-------------------------------------- function setupDebugMode { if getargbool 0 rd.kiwi.debug; then local log=/run/initramfs/log mkdir -p ${log} exec > ${log}/boot.kiwi exec 2>> ${log}/boot.kiwi set -x fi } function loadKernelModules { modprobe squashfs } function initGlobalDevices { local nbd_host local nbd_export if [ -z "$1" ]; then die "No root device for operation given" fi write_partition=$(getarg rd.root.overlay.write=) if [[ "${root_cmdline}" =~ "overlay:nbd=" ]];then nbd_host=$(echo "${root_cmdline}" | cut -f2 -d= | cut -f1 -d:) nbd_export=$(echo "${root_cmdline}" | cut -f3 -d:) read_only_partition="/dev/nbd0" if ! nbd-client \ "${nbd_host}" "${read_only_partition}" -name "${nbd_export}" then die "Failed to setup nbd client for ${root_cmdline}" fi elif [[ "${root_cmdline}" =~ "overlay:aoe=" ]];then read_only_partition="/dev/etherd/$(echo "${root_cmdline}"|cut -f2 -d=)" else read_only_partition="$1" fi } function mountReadOnlyRootImage { local overlay_base local unit_name overlay_base=$(getOverlayBaseDirectory) local root_mount_point="${overlay_base}/rootfsbase" mkdir -m 0755 -p "${root_mount_point}" unit_name=$(echo "${root_mount_point}" | cut -c 2- | tr / -) cat >/run/systemd/system/"${unit_name}".mount <<-EOF [Unit] Before=initrd-root-fs.target After=run-overlay.mount DefaultDependencies=no [Mount] Where=$root_mount_point What=$read_only_partition Type=auto DirectoryMode=0755 [Install] WantedBy=multi-user.target EOF if ! systemctl start "${unit_name}".mount;then die "Failed to mount overlay(ro) root filesystem" fi echo "${root_mount_point}" } function prepareTemporaryOverlay { local overlay_base overlay_base=$(getOverlayBaseDirectory) mkdir -m 0755 -p "${overlay_base}/overlayfs/rw" mkdir -m 0755 -p "${overlay_base}/overlayfs/work" } function preparePersistentOverlay { local overlay_base local unit_name overlay_base=$(getOverlayBaseDirectory) local overlay_mount_point="${overlay_base}/overlayfs" mkdir -m 0755 -p "${overlay_mount_point}" unit_name=$(echo "${overlay_mount_point}" | cut -c 2- | tr / -) cat >/run/systemd/system/"${unit_name}".mount <<-EOF [Unit] Before=initrd-root-fs.target After=run-overlay.mount DefaultDependencies=no [Mount] Where=$overlay_mount_point What=$write_partition Type=auto DirectoryMode=0755 [Install] WantedBy=multi-user.target EOF if ! systemctl start "${unit_name}".mount; then die "Failed to mount overlay(rw) filesystem" fi mkdir -m 0755 -p "${overlay_mount_point}/rw" mkdir -m 0755 -p "${overlay_mount_point}/work" } #====================================== # perform root access preparation #-------------------------------------- PATH=/usr/sbin:/usr/bin:/sbin:/bin declare root=${root} declare root_cmdline root_cmdline=$(getarg root=) # only run if overlay is requested if [[ ! "${root_cmdline}" =~ "overlay:" ]];then return 0 fi # init debug log file if wanted setupDebugMode # device nodes and types initGlobalDevices "${root#block:}" # load required kernel modules loadKernelModules # mount readonly root filesystem mountReadOnlyRootImage if [ -z "${write_partition}" ] || getargbool 0 rd.root.overlay.temporary; then prepareTemporaryOverlay else preparePersistentOverlay fi need_shutdown return 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7387881 kiwi-11.1.8/dracut/modules.d/55kiwi-overlay/module-setup.sh0000755000000000000000000000141315273675667020454 0ustar00#!/bin/bash # called by dracut check() { return 255 } # called by dracut depends() { echo network rootfs-block dm kiwi-lib return 0 } # called by dracut installkernel() { instmods squashfs loop overlay nbd aoe } # called by dracut install() { declare moddir=${moddir} declare systemdutildir=${systemdutildir} inst_multiple \ lsblk losetup grep cut mount nbd-client inst_hook cmdline 30 "${moddir}/parse-kiwi-overlay.sh" # kiwi-repart priority pre-mount hook is 20 # overlay pre-mount needs to happend after any repartition inst_hook pre-mount 30 "${moddir}/kiwi-overlay-root.sh" inst_script "${moddir}/kiwi-generator.sh" \ "${systemdutildir}/system-generators/dracut-kiwi-generator" dracut_need_initqueue } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7387881 kiwi-11.1.8/dracut/modules.d/55kiwi-overlay/parse-kiwi-overlay.sh0000755000000000000000000000415015273675667021564 0ustar00#!/bin/bash # overlay images are specified with # root=overlay:UUID=uuid # root=overlay:PARTUUID=partuuid # root=overlay:LABEL=label # root=overlay:nbd=ip:exportname # root=overlay:aoe=interface [ -z "${root}" ] && root=$(getarg root=) if [ "${root%%:*}" = "overlay" ] ; then overlayroot=$root fi [ "${overlayroot%%:*}" = "overlay" ] || return 1 modprobe -q loop need_network=0 case "${overlayroot}" in overlay:UUID=*|UUID=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/disk/by-uuid/${root#UUID=}" rootok=1 ;; overlay:PARTUUID=*|PARTUUID=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/disk/by-partuuid/${root#PARTUUID=}" rootok=1 ;; overlay:PARTLABEL=*|PARTLABEL=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/disk/by-partlabel/${root#PARTLABEL=}" rootok=1 ;; overlay:LABEL=*|LABEL=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/disk/by-label/${root#LABEL=}" rootok=1 ;; overlay:UNIXNODE=*|UNIXNODE=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/${root#UNIXNODE=}" rootok=1 ;; overlay:MAPPER=*|MAPPER=*) \ root="${root#overlay:}" root="${root//\//\\x2f}" root="block:/dev/mapper/${root#MAPPER=}" rootok=1 ;; overlay:nbd=*) \ root="${root#overlay:nbd=}" root="block:/dev/nbd0" need_network=1 rootok=1 ;; overlay:aoe=*) \ root="${root#overlay:aoe=}" root="block:/dev/etherd/${root}" need_network=1 rootok=1 ;; esac if [ "${need_network}" = "1" ];then echo "rd.neednet=1" > /etc/cmdline.d/kiwi-generated.conf if ! getarg "ip="; then echo "ip=dhcp" >> /etc/cmdline.d/kiwi-generated.conf fi fi [ "${rootok}" = "1" ] || return 1 info "root was ${overlayroot}, is now ${root}" # make sure that init doesn't complain [ -z "${root}" ] && root="overlay" wait_for_dev -n "${root#block:}" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7387881 kiwi-11.1.8/dracut/modules.d/55kiwi-repart/Makefile0000644000000000000000000000023215273675667016744 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/55kiwi-repart cp -a * ${buildroot}usr/lib/dracut/modules.d/55kiwi-repart ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.738933 kiwi-11.1.8/dracut/modules.d/55kiwi-repart/kiwi-repart-disk.sh0000755000000000000000000002403215273675667021035 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type lookup_disk_device_from_root >/dev/null 2>&1 || . /lib/kiwi-lib.sh type create_partitions >/dev/null 2>&1 || . /lib/kiwi-partitions-lib.sh type resize_filesystem >/dev/null 2>&1 || . /lib/kiwi-filesystem-lib.sh type activate_volume_group >/dev/null 2>&1 || . /lib/kiwi-lvm-lib.sh type activate_mdraid >/dev/null 2>&1 || . /lib/kiwi-mdraid-lib.sh type luks_system >/dev/null 2>&1 || . /lib/kiwi-luks-lib.sh #====================================== # Functions #-------------------------------------- function initialize { local profile=/.profile if test ! -f ${profile}; then warn "No profile setup found" warn "Settings from the kiwi description will be ignored" fi test -f ${profile} && import_file ${profile} # Optional env TERM setup term=$(getarg "rd.kiwi.term=") [ -n "${term}" ] && export TERM="${term}" # Create dialog profile from current env # Used in the systemd dialog unit env >/dialog_profile # Lookup the partition setup while the device mappings on # top of the partitions are still active. They are looked # up through the root device and get deactivated for the # repartition disk=$(lookup_disk_device_from_root) export disk root_partition=$(lookup_partition_device_from_root) export root_partition raid_device=$(lookup_raid_device_from_root) export raid_device # the bootable flag gets lost if its partition is recreated boot_partition_id=$(get_active_partition_id "${disk}") export boot_partition_id last_device=$(get_last_partition_device "${disk}") export last_device disk_free_mbytes=$(( $(get_free_disk_bytes "${disk}") / 1048576 )) export disk_free_mbytes disk_root_mbytes=$(( $(get_block_device_kbsize "${last_device}") / 1024 )) export disk_root_mbytes } function deactivate_device_mappings { if lvm_system;then deactivate_volume_group fi if mdraid_system "${raid_device}";then deactivate_mdraid "${raid_device}" fi if luks_system "${root_partition}";then deactivate_luks fi } function finalize_disk_repart { local kiwi_ResizePart kiwi_ResizePart=$(get_last_partition_id "${disk}") finalize_partition_table "${disk}" "${boot_partition_id}" set_root_map \ "$(get_partition_node_name "${disk}" "${kiwi_ResizePart}")" } function get_target_rootpart_size { declare kiwi_oemrootMB=${kiwi_oemrootMB} local oemrootMB oemrootMB=$(getarg rd.kiwi.install.install.systemsize=) if [ -n "${oemrootMB}" ]; then if [ "${oemrootMB}" = "all" ];then kiwi_oemrootMB="" else kiwi_oemrootMB="${oemrootMB}" fi fi echo "${kiwi_oemrootMB}" } function repart_standard_disk { # """ # repartition disk with read/write root filesystem # Image partition table layout is: # ===================================== # pX: [ boot ] # pX+1: ( root ) [+luks +raid] # ------------------------------------- # """ local kiwi_oemrootMB local kiwi_ResizePart local command_query local root_part_size local part_name kiwi_oemrootMB=$(get_target_rootpart_size) kiwi_ResizePart=$(get_last_partition_id "${disk}") part_name=$(get_partition_name "${disk}" "${kiwi_ResizePart}") if [ -z "${kiwi_oemrootMB}" ];then local disk_have_root_system_mbytes=$(( disk_root_mbytes + disk_free_mbytes )) local min_additional_mbytes=10 else local disk_have_root_system_mbytes=${kiwi_oemrootMB} local min_additional_mbytes=$(( kiwi_oemrootMB - disk_root_mbytes )) fi if [ "${min_additional_mbytes}" -lt 10 ];then min_additional_mbytes=10 fi # check if we can repart this disk if ! check_repart_possible \ ${disk_root_mbytes} ${disk_free_mbytes} ${min_additional_mbytes} then return 1 fi # deactivate all active device mappings deactivate_device_mappings # repart root partition root_part_size=+${disk_have_root_system_mbytes}M if [ -z "${kiwi_oemrootMB}" ];then # no new parts and no rootsize limit, use rest disk space root_part_size=. fi command_query=" d ${kiwi_ResizePart} n ${part_name} ${kiwi_ResizePart} . ${root_part_size} " if mdraid_system "${raid_device}"; then command_query=" d ${kiwi_ResizePart} n ${part_name} ${kiwi_ResizePart} . ${root_part_size} t ${kiwi_ResizePart} fd " fi if ! create_partitions "${disk}" "${command_query}";then die "Failed to create partition table" fi if mdraid_system "${raid_device}"; then update_devicesize_mdraid "${raid_device}" fi # finalize table changes finalize_disk_repart } function repart_lvm_disk { # """ # repartition disk if LVM partition plus boot partition # is used. Initial partition table layout is: # ===================================== # pX: ( boot ) # pX+1: ( LVM ) [+luks +raid] # ------------------------------------- # """ local kiwi_oemrootMB local kiwi_ResizePart local command_query local lvm_part_size local part_name kiwi_oemrootMB=$(get_target_rootpart_size) kiwi_ResizePart=$(get_last_partition_id "${disk}") part_name=$(get_partition_name "${disk}" "${kiwi_ResizePart}") if [ -z "${kiwi_oemrootMB}" ];then local disk_have_root_system_mbytes=$(( disk_root_mbytes + disk_free_mbytes )) local min_additional_mbytes=10 else local disk_have_root_system_mbytes=$(( kiwi_oemrootMB )) local min_additional_mbytes=$(( kiwi_oemrootMB - disk_root_mbytes )) fi if [ "${min_additional_mbytes}" -lt 10 ];then min_additional_mbytes=10 fi # check if we can repart this disk if ! check_repart_possible \ ${disk_root_mbytes} ${disk_free_mbytes} ${min_additional_mbytes} then return 1 fi # deactivate all active device mappings deactivate_device_mappings # create lvm.conf appropriate for resize setup_lvm_config # repart lvm partition lvm_part_size=+${disk_have_root_system_mbytes}M if [ -z "${kiwi_oemrootMB}" ];then # no rootsize limit, use rest disk space lvm_part_size=. fi command_query=" d ${kiwi_ResizePart} n ${part_name} ${kiwi_ResizePart} . ${lvm_part_size} t ${kiwi_ResizePart} 8e " if ! create_partitions "${disk}" "${command_query}";then die "Failed to create partition table" fi # finalize table changes finalize_disk_repart } function check_repart_possible { local disk_root_mbytes=$1 local disk_free_mbytes=$2 local min_additional_mbytes=$3 local kiwi_oemrootMB kiwi_oemrootMB=$(get_target_rootpart_size) if [ -n "${kiwi_oemrootMB}" ];then if [ "${kiwi_oemrootMB}" -lt "${disk_root_mbytes}" ];then # specified oem-systemsize is smaller than root partition warn "Requested OEM systemsize is smaller than root partition:" warn "Disk won't be re-partitioned !" echo warn "Current Root partition: ${disk_root_mbytes} MB" warn "==> Requested size: ${kiwi_oemrootMB} MB" return 1 fi fi if [ "${min_additional_mbytes}" -gt "${disk_free_mbytes}" ];then if [ "${disk_free_mbytes}" -gt 2 ];then # Requested size for root exceeds free space on disk local requested_size if [ -n "${kiwi_oemrootMB}" ];then requested_size="root:($((kiwi_oemrootMB - disk_root_mbytes)) MB)" else requested_size="root:(keep)" fi warn "Requested OEM systemsize exceeds free space on the disk:" warn "Disk won't be re-partitioned !" echo warn "Requested size(s): ${requested_size}" warn "==> Free Space on disk: ${disk_free_mbytes} MB" else # The free space on disk calculated to a very small number. # This usually indicates that the disk geometry was not # intentionally changed and the rest free space is a # rounding number on the partition alignment. In this case # no warning message is shown because it's the typical # situation on reboot of a machine without disk geometry # changes. : fi return 1 fi return 0 } #====================================== # Perform repart/resize operations #-------------------------------------- PATH=/usr/sbin:/usr/bin:/sbin:/bin setup_debug # abort for iso/live mode declare root=${root} case "${root}" in install:* | live:*) return ;; esac # initialize for disk repartition initialize # reencrypt luks device if luks_system "${root_partition}";then if getargbool 0 rd.kiwi.oem.luks.reencrypt; then reencrypt_luks "${root_partition}" fi fi # wait for the root device to appear wait_for_storage_device "${last_device}" # check if repart/resize is wanted if ! resize_wanted "${last_device}" "${disk}"; then return fi # prepare disk for repartition if [ "$(get_partition_table_type "${disk}")" = 'gpt' ];then relocate_gpt_at_end_of_disk "${disk}" fi # wait for the root device to appear wait_for_storage_device "${last_device}" # resize disk partition table if lvm_system;then repart_lvm_disk || return else repart_standard_disk || return fi # resize luks if present if luks_system "${root_partition}";then activate_luks "$(get_root_map)" resize_luks fi # resize raid if present if mdraid_system "${raid_device}";then activate_mdraid "${raid_device}" resize_mdraid "${raid_device}" fi # resize volumes and filesystems if lvm_system; then resize_pyhiscal_volumes activate_volume_group resize_lvm_volumes_and_filesystems else resize_filesystem "$(get_root_map)" fi # wait for the root device to appear wait_for_storage_device "${last_device}" ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.738933 kiwi-11.1.8/dracut/modules.d/55kiwi-repart/module-setup.sh0000755000000000000000000000062215273675667020271 0ustar00#!/bin/bash # called by dracut check() { return 255 } # called by dracut depends() { echo rootfs-block dm kiwi-lib return 0 } # called by dracut installkernel() { instmods squashfs loop } # called by dracut install() { declare moddir=${moddir} test -f /.profile && inst_simple /.profile inst_hook pre-mount 20 "${moddir}/kiwi-repart-disk.sh" dracut_need_initqueue } ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.738933 kiwi-11.1.8/dracut/modules.d/59kiwi-dump-reboot/Makefile0000644000000000000000000000024415273675667017713 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/59kiwi-dump-reboot cp -a * ${buildroot}usr/lib/dracut/modules.d/59kiwi-dump-reboot ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7390528 kiwi-11.1.8/dracut/modules.d/59kiwi-dump-reboot/kiwi-dump-reboot-system.sh0000755000000000000000000000664315273675667023343 0ustar00#!/bin/bash type lib_initialize >/dev/null 2>&1 || . /lib/kiwi-lib.sh type report_and_quit >/dev/null 2>&1 || . /lib/kiwi-dump-image.sh type get_selected_disk >/dev/null 2>&1 || . /lib/kiwi-dump-image.sh type run_dialog >/dev/null 2>&1 || . /lib/kiwi-dialog-lib.sh #====================================== # Functions #-------------------------------------- function boot_installed_system { declare kiwi_oemreboot=${kiwi_oemreboot} declare kiwi_oemrebootinteractive=${kiwi_oemrebootinteractive} declare kiwi_oemshutdown=${kiwi_oemshutdown} declare kiwi_oemshutdowninteractive=${kiwi_oemshutdowninteractive} local ask_reboot_text="Reboot System ?" local ask_shutdown_text="Shutdown System ?" local boot_options kiwi_oemreboot=$(bool "${kiwi_oemreboot}") kiwi_oemrebootinteractive=$(bool "${kiwi_oemrebootinteractive}") kiwi_oemshutdown=$(bool "${kiwi_oemshutdown}") kiwi_oemshutdowninteractive=$(bool "${kiwi_oemshutdowninteractive}") if [ "${kiwi_oemrebootinteractive}" = "true" ];then ask_and_reboot "${ask_reboot_text}" fi if [ "${kiwi_oemreboot}" = "true" ];then reboot -f fi if [ "${kiwi_oemshutdowninteractive}" = "true" ];then ask_and_shutdown "${ask_shutdown_text}" fi if [ "${kiwi_oemshutdown}" = "true" ];then systemctl poweroff fi # if rd.kiwi.install.pass.bootparam is given, pass on most # boot options to the kexec kernel if getargbool 0 rd.kiwi.install.pass.bootparam; then local cmdline local option read -r cmdline < /proc/cmdline for option in ${cmdline}; do case ${option} in rd.kiwi.*) ;; # skip all rd.kiwi options, they might do harm iso-scan/filename=*) ;; # install ISO file, not for the system *) boot_options="${boot_options}${option} ";; esac done fi boot_options="${boot_options}$(cat /config.bootoptions)" if getargbool 0 rd.kiwi.debug; then boot_options="${boot_options} rd.kiwi.debug" fi kexec_options="" if kexec --help | grep -q kexec-syscall-auto;then # try file based syscall first and fall back to kexec_load # syscall if not present. On systems using kexec that does # provide --kexec-syscall-auto the assumption is made that # neither kexec nor the kernel supports file based syscall # and thus no syscall selection is made kexec_options="${kexec_options} --kexec-syscall-auto" fi # shellcheck disable=SC2116,SC2086 kexec_options=$(echo ${kexec_options}) # strip spaces kexec "${kexec_options}" \ --load /run/install/boot/*/loader/linux \ --initrd /run/install/initrd.system_image \ --command-line "${boot_options}" if ! kexec -e; then report_and_quit "Failed to kexec boot system" fi } #====================================== # Reboot into system #-------------------------------------- lib_initialize if getargbool 0 rd.kiwi.ramdisk; then # For ramdisk deployment a kexec boot is not possible as it # will wipe the contents of the ramdisk. Therefore we prepare # the switch_root from this deployment initrd. See # kiwi-mount-ramdisk.sh mount hook for further details. image_target=$(get_selected_disk) kpartx -s -a "${image_target}" else # Standard deployment will use kexec to activate and boot the # deployed system boot_installed_system fi ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7390528 kiwi-11.1.8/dracut/modules.d/59kiwi-dump-reboot/module-setup.sh0000755000000000000000000000052515273675667021237 0ustar00#!/bin/bash # called by dracut check() { return 255 } # called by dracut depends() { echo network rootfs-block dm kiwi-lib kiwi-dump return 0 } # called by dracut installkernel() { return 0 } # called by dracut install() { declare moddir=${moddir} inst_hook pre-mount 40 "${moddir}/kiwi-dump-reboot-system.sh" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7390528 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/Makefile0000644000000000000000000000022415273675667016222 0ustar00buildroot = / install: install -d -m 755 ${buildroot}usr/lib/dracut/modules.d/59kiwi-lib cp -a * ${buildroot}usr/lib/dracut/modules.d/59kiwi-lib ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-dialog-lib.sh0000644000000000000000000001204415273675667020065 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh function run_dialog { # """ # Run the dialog program via the systemd service either in a # framebuffer terminal or on the console. The return code of # the function is the return code of the dialog call. The # output of the dialog call is stored in a file and can be # one time read via the get_dialog_result function # """ stop_plymouth local dialog_result=/tmp/dialog_result local dialog_exit_code=/tmp/dialog_code { echo "dialog $* 2>$dialog_result" echo "echo -n \$? >$dialog_exit_code" } >/run/dracut-interactive _run_interactive return "$(cat $dialog_exit_code)" } function setup_progress_fifo { local fifo=$1 export current_progress_fifo="${fifo}" [ -e "${fifo}" ] || mkfifo "${fifo}" } function stop_dialog { _stop_interactive } function run_progress_dialog { # """ # Run the gauge dialog via systemd reading progress # status information from stdin # """ stop_plymouth declare fifo=${current_progress_fifo} local title=$1 local backtitle=$2 local progress_at="${fifo}" if [ ! -e "${progress_at}" ];then return fi { echo -n "cat ${fifo} | dialog --backtitle \"${backtitle}\" " echo -n "--gauge \"${title}\"" 7 65 0 echo } >/run/dracut-interactive _run_interactive } function get_dialog_result { local dialog_result=/tmp/dialog_result [ -e "${dialog_result}" ] && cat ${dialog_result}; rm -f ${dialog_result} } function stop_plymouth { if ! getargbool 0 rd.kiwi.allow_plymouth; then if command -v plymouth &>/dev/null;then plymouth --quit --wait fi fi } #========================================= # Methods considered private #----------------------------------------- function _setup_interactive_service { local service=/run/systemd/system/dracut-run-interactive.service local script=/run/dracut-interactive mkdir -p /run/systemd/system [ -e ${service} ] && return { echo "[Unit]" echo "Description=Dracut Run Interactive" echo "DefaultDependencies=no" echo "After=systemd-vconsole-setup.service" echo "Wants=systemd-vconsole-setup.service" echo "Conflicts=emergency.service emergency.target" echo "[Service]" echo "EnvironmentFile=/dialog_profile" echo "Environment=HOME=/" echo "Environment=DRACUT_SYSTEMD=1" echo "Environment=NEWROOT=/sysroot" echo "WorkingDirectory=/" if _fbiterm_ok; then echo "ExecStart=fbiterm -- /bin/bash ${script} " else echo "ExecStart=/bin/bash ${script}" fi echo "Type=oneshot" echo "StandardInput=tty-force" echo "StandardOutput=inherit" echo "StandardError=inherit" echo "KillMode=process" echo "IgnoreSIGPIPE=no" echo "KillSignal=SIGHUP" } > ${service} } function _run_interactive { _setup_interactive_service systemctl start dracut-run-interactive.service } function _stop_interactive { systemctl stop dracut-run-interactive.service } function _fbiterm_ok { if [ ! -e /dev/fb0 ];then # no framebuffer device found return 1 fi if ! command -v fbiterm &>/dev/null;then # no framebuffer terminal program found return 1 fi if ! fbiterm echo &>/dev/null;then # fbiterm can't be called with echo test cmd return 1 fi return 0 } function dialog_timeout { local timeout=60 custom_timeout=$(getarg "rd.kiwi.dialog.timeout=") [ -n "${custom_timeout}" ] && timeout="${custom_timeout}" echo "${timeout}" } function show_log_and_quit { local text_message="$1" local log_file="$2" local timeout timeout=$(dialog_timeout) if [ "${timeout}" = "off" ];then run_dialog --backtitle "\"${text_message}\"" \ --textbox "${log_file}" 15 80 else run_dialog --timeout "${timeout}" --backtitle "\"${text_message}\"" \ --textbox "${log_file}" 15 80 fi if getargbool 0 rd.debug; then die "${text_message}" else reboot -f fi } function report_and_quit { local text_message="$1" local timeout timeout=$(dialog_timeout) if [ "${timeout}" = "off" ];then run_dialog --msgbox "\"${text_message}\"" 5 80 else run_dialog --timeout "${timeout}" --msgbox "\"${text_message}\"" 5 80 fi if getargbool 0 rd.debug; then die "${text_message}" else reboot -f fi } function ask_and_reboot { local text_message="$1" if ! run_dialog --yesno "\"${text_message}\"" 7 80; then die "${text_message}" else reboot -f fi } function ask_and_shutdown { local text_message="$1" if ! run_dialog --yesno "\"${text_message}\"" 7 80; then die "${text_message}" else systemctl poweroff fi } function ask_for_credentials { local text_message="$1" run_dialog --insecure --passwordbox "\"${text_message}\"" 7 60 } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-filesystem-lib.sh0000644000000000000000000001071015273675667021010 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh function getOverlayBaseDirectory { # initialize and print overlay base directory below which # the overlayfs based mountpoints are managed local overlay_base=/run/overlay local unit_name local overlay_size mkdir -p "${overlay_base}" if ! mountpoint -q "${overlay_base}";then overlay_size=$(getarg rd.root.overlay.size) [ -z "${overlay_size}" ] && overlay_size="50%" unit_name=$(echo "${overlay_base}" | cut -c 2- | tr / -) cat >/run/systemd/system/"${unit_name}".mount <<-EOF [Unit] Before=initrd-root-fs.target DefaultDependencies=no [Mount] Where=$overlay_base What=tmpfs Type=tmpfs DirectoryMode=0755 [Install] WantedBy=multi-user.target EOF systemctl start "${unit_name}".mount fi echo "${overlay_base}" } function resize_filesystem { local device=$1 test -n "${device}" || return local resize_fs local mpoint=/fs-resize local fstype fstype=$(probe_filesystem "${device}") lock="set_device_lock ${device}" case ${fstype} in ext2|ext3|ext4) resize_fs="${lock} resize2fs -f -p ${device}" ;; btrfs) resize_fs="mkdir -p ${mpoint} && mount ${device} ${mpoint} &&" resize_fs="${resize_fs} ${lock} btrfs filesystem resize max ${mpoint}" resize_fs="${resize_fs};umount ${mpoint} && rmdir ${mpoint}" ;; xfs) resize_fs="mkdir -p ${mpoint} && mount ${device} ${mpoint} &&" resize_fs="${resize_fs} ${lock} xfs_growfs ${mpoint}" resize_fs="${resize_fs};umount ${mpoint} && rmdir ${mpoint}" ;; swap) resize_fs="${lock} mkswap ${device} --label SWAP" ;; *) # don't know how to resize this filesystem warn "Don't know how to resize ${fstype}... skipped" return ;; esac if ! _is_ramdisk_device "${device}"; then if [ ! "${fstype}" = "btrfs" ];then # btrfs reports the free space cache to be outdated/corrupt # after kiwi has resized the partition table. This is an # expected condition prior resize of the filesystem and # should not prevent the following resize. As such the # filesystem check is not applied for btrfs prior resize check_filesystem "${device}" fi fi info "Resizing ${fstype} filesystem on ${device}..." if ! eval "${resize_fs}"; then die "Failed to resize filesystem" fi } function check_filesystem { local device=$1 test -n "${device}" || return local check_fs local check_fs_return_ok local fstype fstype=$(probe_filesystem "${device}") lock="set_device_lock ${device}" case ${fstype} in ext2|ext3|ext4) # The exit code by e2fsck is the sum of the following conditions: # 0 - No errors # 1 - File system errors corrected # 2 - File system errors corrected, system should be rebooted # 4 - File system errors left uncorrected # 8 - Operational error # 16 - Usage or syntax error # 32 - E2fsck canceled by user request # 128 - Shared library error check_fs="${lock} e2fsck -p -f ${device}" check_fs_return_ok="test \$? -le 2" ;; btrfs) # btrfs check returns a zero exit status if it succeeds. # Non zero is returned in case of failure. check_fs="${lock} btrfsck ${device}" check_fs_return_ok="test \$? -eq 0" ;; xfs) # xfs_repair can be used to check the filesystem. However # for subsequent xfs_growfs no check is needed. Thus for # xfs we skip the checking return ;; *) # don't know how to check this filesystem warn "Don't know how to check ${fstype}... skipped" return ;; esac info "Checking ${fstype} filesystem on ${device}..." eval "${check_fs}" if ! eval "${check_fs_return_ok}"; then die "Failed to check filesystem" fi } function probe_filesystem { local device=$1 test -n "${device}" || return local fstype fstype=$(blkid "${device}" -s TYPE -o value) if [ -z "${fstype}" ];then fstype=unknown fi if [ "${fstype}" = "crypto_LUKS" ];then fstype=luks fi echo "${fstype}" } #====================================== # Methods considered private #-------------------------------------- function _is_ramdisk_device { echo "$1" | grep -qi "/dev/ram" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-lib.sh0000644000000000000000000002333315273675667016633 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh function setup_debug { if getargbool 0 rd.kiwi.debug; then local log=/run/initramfs/log mkdir -p ${log} exec >> ${log}/boot.kiwi exec 2>> ${log}/boot.kiwi set -x fi } function get_system_id { local disk_device=$1 blkid -s PTUUID -o value "${disk_device}" } function set_device_lock { # """ # Set device lock, preferrable via udevadm or via # flock as fallback if systemd/udev does not provide # the command # """ if udevadm lock --help &>/dev/null;then udevadm lock --device "$@" else flock -x "$@" fi } function disk_matches_system_identifier { local disk_device=$1 local system_identifier=/system_identifier local id_disk local id_image if [ -e "${system_identifier}" ];then id_disk=$(get_system_id "${disk_device}") read -r id_image < "${system_identifier}" if [ "${id_disk}" = "${id_image}" ]; then return 0 fi fi return 1 } function set_root_map { root_map=$1 export root_map } function get_root_map { echo "${root_map}" } function get_mapped_multipath_disk { declare DEVICE_TIMEOUT=${DEVICE_TIMEOUT} local disk_device=$1 local check=0 local limit=30 if [[ "${DEVICE_TIMEOUT}" =~ ^[0-9]+$ ]]; then limit=$(((DEVICE_TIMEOUT + 1)/ 2)) fi udev_pending &>/dev/null while true;do for wwn in $(multipath -l -v1 "${disk_device}");do if [ -e "/dev/mapper/${wwn}" ];then echo "/dev/mapper/${wwn}" return fi done if [ "${check}" -eq "${limit}" ]; then die "Multipath map for ${disk_device} did not show up" fi check=$((check + 1)) sleep 2 done } function get_root_device { # """ # Get unix node of the root device from the root= setup # """ declare root=${root} local root_device=${root#block:} if [ -z "${root_device}" ];then die "No root device found" fi if [ -L "${root_device}" ];then root_device=/dev/$(basename "$(readlink "${root_device}")") fi echo "${root_device}" } function lookup_partition_device_from_root { # """ # Get unix node of the partition which stores the root # filesystem. The root filesystem is either on the partition # itself or on a device mapping on top of it, e.g LUKS, RAID # or LVM. The device mappings are looked up through the # active root device, call this before they get deactivated # """ local root_device local name local type root_device=$(get_root_device) while read -r name type;do if [ "${type}" = "part" ];then echo "${name}" return fi done < <(lsblk -p -n -r -s -o NAME,TYPE "${root_device}") } function lookup_raid_device_from_root { # """ # Get unix node of the md raid device the root filesystem # is stored on, if this raid is created on top of a partition. # This is the raid kiwi creates at build time. A raid created # from whole disks, e.g by a firmware raid controller, holds # the partitions itself and is not provided. The device # mappings are looked up through the active root device, call # this before they get deactivated # """ local root_device local raid_device local name local type root_device=$(get_root_device) while read -r name type;do case "${type}" in raid*) raid_device=${name} ;; part) # first partition below the root device, a raid # found before it is created on top of it if [ -n "${raid_device}" ];then echo "${raid_device}" fi return ;; esac done < <(lsblk -p -n -r -s -o NAME,TYPE "${root_device}") } function lookup_disk_device_from_root { declare kiwi_oemmultipath_scan=${kiwi_oemmultipath_scan} local root_device local disk_device local raid_device local wwn kiwi_oemmultipath_scan=$(bool "${kiwi_oemmultipath_scan}") root_device=$(get_root_device) raid_device=$(lookup_raid_device_from_root) for disk_device in $( lsblk -p -n -r -s -o NAME,TYPE "${root_device}" |\ grep -E "disk|raid" | cut -f1 -d ' ' ); do # If multipath is requested, set the disk_device to the # multipath mapped device if [ "${kiwi_oemmultipath_scan}" = "true" ];then disk_device=$(get_mapped_multipath_disk "${disk_device}") fi # Check if root_device is managed by mdadm and that the md raid # is not created as part of the kiwi image building process. If # this is the case the md raid device comes from a fake raid # controller and we need to prefer the md device over the storage # disks if type mdadm &> /dev/null && [ -z "${raid_device}" ]; then if mdadm --detail -Y "${disk_device}" &>/dev/null; then echo "${disk_device}" return fi fi done echo "${disk_device}" } function shasum_tool { declare kiwi_shasum_suffix=${kiwi_shasum_suffix} if getargbool 0 rd.kiwi.oem.sha512; then echo sha512sum elif [ "${kiwi_shasum_suffix}" = ".sha512" ]; then echo sha512sum else echo sha256sum fi } function shasum_extension { declare kiwi_shasum_suffix=${kiwi_shasum_suffix} if getargbool 0 rd.kiwi.oem.sha512; then echo sha512 elif [ "${kiwi_shasum_suffix}" = ".sha512" ]; then echo sha512 else echo sha256 fi } function udev_pending { declare DEVICE_TIMEOUT=${DEVICE_TIMEOUT} local limit=30 if [[ "${DEVICE_TIMEOUT}" =~ ^[0-9]+$ ]]; then limit=$(((DEVICE_TIMEOUT + 1)/ 2)) fi udevadm settle --timeout=${limit} } function get_persistent_device_from_unix_node { local unix_device=$1 local schema=$2 local node local persistent_name node=$(basename "${unix_device}") udev_pending for persistent_name in /dev/disk/"${schema}"/*; do if [ "$(basename "$(readlink "${persistent_name}")")" = "${node}" ];then echo "${persistent_name}" return fi done warn "Could not find ${schema} representation of ${node}" warn "Using original device ${unix_device}" echo "${unix_device}" } function deactivate_all_device_maps { dmsetup remove_all } function import_file { # """ # import file with key=value format. the function # will export each entry of the file as variable into # the current shell environment # """ local source_format=/tmp/source_file_formatted # create clean input, no empty lines and comments grep -v '^$' "$1" | grep -v '^[ \t]*#' > ${source_format} # remove start/stop quoting from values sed -i -e s"#\(^[a-zA-Z0-9_]\+\)=[\"']\(.*\)[\"']#\1=\2#" ${source_format} # remove backslash quotes if any sed -i -e s"#\\\\\(.\)#\1#g" ${source_format} # quote simple quotation marks sed -i -e s"#'\+#'\\\\''#g" ${source_format} # add '...' quoting to values sed -i -e s"#\(^[a-zA-Z0-9_]\+\)=\(.*\)#\1='\2'#" ${source_format} source ${source_format} &>/dev/null while read -r line;do local key key=$(echo "${line}" | cut -d '=' -f1) eval "export ${key}" &>/dev/null done < ${source_format} } function binsize_to_bytesize { # """ # converts binary sizes (1024k, 2.4G) to bytes # uses awk to handle floating point numbers # """ local sz="$1" local bs=${sz:0:-1} case ${sz} in *K|*k) mult=1024 ;; *M) mult=$((1024*1024)) ;; *G) mult=$((1024*1024*1024)) ;; *T) mult=$((1024*1024*1024*1024)) ;; *) bs=${sz}; mult=1 ;; esac awk "BEGIN {print int(${bs}*${mult})}" } function bool { # """ # provides boolean string true|false for given value. # Only if value matches true return true, in any other # case return false # """ local value=$1 if [ -n "${value}" ] && [ "${value}" = "true" ] ;then echo "true" else echo "false" fi } function lib_initialize { # """ # Source profile variables into runtime environment # The method will exit from the initrd if the profile # file does not exist # """ local term local kiwi_oem_installdevice local profile=/.profile local profile_extra=/.profile.extra # Read and import mandatory image profile to env test -f ${profile} || \ report_and_quit "No profile setup found" import_file ${profile} # Optional env TERM setup term=$(getarg "rd.kiwi.term=") [ -n "${term}" ] && export TERM="${term}" # Source optional profile.extra to env test -f ${profile_extra} && source ${profile_extra} # Create dialog profile from current env # Used in the systemd dialog unit env >/dialog_profile # Handle kiwi specific env overwrites kiwi_oem_installdevice=$(getarg rd.kiwi.oem.installdevice=) if [ -n "${kiwi_oem_installdevice}" ];then # activate unattended mode. In case a user explicitly # provides the device name to deploy the image to via # the kernel commandline, no further questions if this # is wanted should appear message="rd.kiwi.oem.installdevice explicitly set via cmdline." message="${message} The following OEM device settings will be ignored:" message="${message} oem-unattended," message="${message} oem-unattended-id," message="${message} oem-device-filter" export kiwi_oemunattended="true" export kiwi_oemunattended_id="" export kiwi_oemdevicefilter="" info "${message}" >&2 fi } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-luks-lib.sh0000644000000000000000000001107415273675667017606 0ustar00#!/bin/bash # shellcheck disable=SC1091 type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type set_root_map >/dev/null 2>&1 || . /lib/kiwi-lib.sh type wait_for_storage_device >/dev/null 2>&1 || . /lib/kiwi-partitions-lib.sh type ask_for_password >/dev/null 2>&1 || . /lib/dracut-crypt-lib.sh type run_progress_dialog >/dev/null 2>&1 || . /lib/kiwi-dialog-lib.sh function luks_system { # """ # Check if the given root partition, see # lookup_partition_device_from_root(), is a LUKS device # """ local device=$1 if cryptsetup isLuks "${device}" &>/dev/null;then return 0 fi return 1 } function deactivate_luks { /usr/lib/systemd/systemd-cryptsetup detach luks } function reencrypt_luks { # """ # Reencrypt the given root partition, see # lookup_partition_device_from_root(), if its LUKS # header is still the one of the image # """ local device=$1 local keyslot=/root/.luks.slot local header_checksum_origin=/root/.luks.header local header_checksum_cur=/root/.luks.header.cur local keyfile=/root/.root.keyfile local new_keyfile=/run/.kiwi_reencrypt.keyfile local passphrase_file=/root/.slotpass local progress=/dev/install_progress local load_text="Reencrypting..." local title_text="LUKS" local shasum read -r header_checksum_origin < "${header_checksum_origin}" read -r keyslot < "${keyslot}" # Checksum test if luks header is still the image origin header shasum=$(shasum_tool) cryptsetup luksHeaderBackup \ "${device}" --header-backup-file "${header_checksum_cur}" header_checksum_cur=$( "${shasum}" "${header_checksum_cur}" |\ cut -f1 -d" "; rm -f "${header_checksum_cur}" ) if [ "${header_checksum_origin}" == "${header_checksum_cur}" ];then if getargbool 0 rd.kiwi.oem.luks.reencrypt_randompass; then # reset insecure built time passphrase with a random # onetime passphrase that will be stored in memory at $new_keyfile # This action require that the boot process uses $new_keyfile # and sets a retrievable keyfile information for subsequent # boot processes of this system (umask 077; touch "${new_keyfile}") tr -dc '[:graph:]' 2>/dev/null < /dev/urandom |\ head -c 32 > "${new_keyfile}" chmod 0400 "${new_keyfile}" cryptsetup \ --key-file "${passphrase_file}" \ --key-slot "${keyslot}" \ luksChangeKey "${device}" "${new_keyfile}" cp "${new_keyfile}" "${passphrase_file}" if [ -e "${keyfile}" ]; then # if there is a keyfile it's referenced in the crypttab # of this initrd instance in memory. Make sure all subsequent # tasks e.g. luks resize have permissions to complete while # inside of this initrd instance cp "${new_keyfile}" "${keyfile}" fi fi # reencrypt setup_progress_fifo ${progress} ( # reencrypt, this will overwrite all key slots set_device_lock "${device}" cryptsetup reencrypt \ --progress-frequency 1 \ --key-file "${passphrase_file}" \ --key-slot "${keyslot}" \ "${device}" 2>&1 | sed -u 's/.* \([0-9]*\)[0-9.]*%.*/\1/' ) >"${progress}" & run_progress_dialog "${load_text}" "${title_text}" if [ -e "${keyfile}" ] && [ ! -e "${new_keyfile}" ];then # re-add keyfile if present and no other keyfile was created cryptsetup --key-file "${passphrase_file}" luksAddKey \ "${device}" "${keyfile}" fi fi } function resize_luks { cryptsetup resize luks } function activate_luks { declare kiwi_luks_empty_passphrase=${kiwi_luks_empty_passphrase} local device=$1 if [ "${kiwi_luks_empty_passphrase}" = "true" ];then # There is no keyfile and kiwi has created the luks pool with # an empty key. Therefore it can be opened without interaction # but this requires to manually call luksOpen since # with systemd-cryptsetup we saw it still asking for # a passphrase set_device_lock "${device}" cryptsetup \ --key-file /dev/zero \ --keyfile-size 32 \ luksOpen "${device}" luks else # There is a keyfile and we need to get prompted to enter the passphrase systemctl start systemd-cryptsetup@luks fi wait_for_storage_device "/dev/mapper/luks" set_root_map "/dev/mapper/luks" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-lvm-lib.sh0000644000000000000000000001231215273675667017422 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type udev_pending >/dev/null 2>&1 || . /lib/kiwi-lib.sh function lvm_system { declare kiwi_lvm=${kiwi_lvm} if [ "${kiwi_lvm}" = "true" ];then return 0 fi return 1 } function setup_lvm_config { mkdir -p /etc/lvm cat > /etc/lvm/lvm.conf <<- EOF global { locking_type = 0 use_lvmetad = 0 } EOF } function activate_volume_group { declare kiwi_lvmgroup=${kiwi_lvmgroup} local vg_count=0 local vg_name if lvm_system;then for vg_name in $(vgs --noheadings -o vg_name 2>/dev/null);do if [ "${vg_name}" = "${kiwi_lvmgroup}" ];then vg_count=$((vg_count + 1)) fi done if [ ${vg_count} -gt 1 ];then die "Duplicate VolumeGroup name ${kiwi_lvmgroup} found !" fi vgchange -a y "${kiwi_lvmgroup}" udev_pending fi } function deactivate_volume_group { declare kiwi_lvmgroup=${kiwi_lvmgroup} vgchange -a n "${kiwi_lvmgroup}" udev_pending } function create_volume { declare kiwi_lvmgroup=${kiwi_lvmgroup} local volume_name=$1 local volume_mb_size=$2 if [ ! -f "$(get_volume_path_for_volume "${volume_name}")" ];then lvcreate --yes --wipesignatures y \ --size "${volume_mb_size}M" -n "${volume_name}" "${kiwi_lvmgroup}" fi } function get_volume_path_for_volume { declare kiwi_lvmgroup=${kiwi_lvmgroup} echo "/dev/${kiwi_lvmgroup}/$1" } function resize_pyhiscal_volumes { local device device=$(get_root_map) set_device_lock "${device}" pvresize "${device}" } function resize_lvm_volumes_and_filesystems { declare kiwi_lvmgroup=${kiwi_lvmgroup} local volume_name local resize_mode local resize_prefix local volume_size local volspec local all_free_volume for volspec in $(read_volume_setup "/.profile" "skip_all_free_volume");do resize_prefix='' volume_name=$(get_volume_name "${volspec}") resize_mode=$(get_volume_size_mode "${volspec}") volume_size=$(get_volume_size "${volspec}") info "Resizing volume ${volume_name}..." if [ ! "${resize_mode}" = "size" ];then resize_prefix=+ fi if lvextend -L "${resize_prefix}${volume_size}M" \ "/dev/${kiwi_lvmgroup}/${volume_name}" then resize_filesystem "/dev/${kiwi_lvmgroup}/${volume_name}" else warn "Warning: requested size cannot be reached !" fi done all_free_volume=$(get_all_free_volume) if [ -n "${all_free_volume}" ];then if lvextend -l +100%FREE "/dev/${kiwi_lvmgroup}/${all_free_volume}";then resize_filesystem "/dev/${kiwi_lvmgroup}/${all_free_volume}" fi fi } function read_volume_setup { # """ # read the volume setup from the profile file and return # a list with the following values: # # volume_name,resize_mode,requested_size,mount_point ... # """ local profile=$1 local skip_all_free_volume=$2 local volume local name local mode local size local mpoint local result local volspec result=$( while read -r volspec;do if ! [[ "${volspec}" =~ "kiwi_Volume_" ]];then continue fi volume=$(echo "${volspec}" | cut -f2 -d= | tr -d \' | tr -d \") size=$(echo "${volume}" | cut -f2 -d\| | cut -f2 -d:) if [ "${size}" = "all" ] && [ -n "${skip_all_free_volume}" ];then continue fi mode=$(echo "${volume}" | cut -f2 -d\| | cut -f1 -d:) name=$(echo "${volume}" | cut -f1 -d\|) mpoint=$(echo "${volume}" | cut -f3 -d\|) if [ -z "${mpoint}" ];then mpoint=noop fi echo "${name},${mode},${size},${mpoint}" done < "${profile}" ) echo "${result}" } function read_volume_setup_all_free { # """ # read the volume setup from the profile file and # lookup the volume that should take the rest space # available in the volume group. # # return a list with the following values: # # volume_name,mount_point # # If no @root volume is configured in the kiwi XML # description the default LVRoot volume will be used # to take the rest space. # """ local profile=$1 local size local volume local name local mpoint while read -r volspec;do if ! [[ "${volspec}" =~ "kiwi_Volume_" ]];then continue fi volume=$(echo "${volspec}" | cut -f2 -d= | tr -d \' | tr -d \") size=$(echo "${volume}" | cut -f2 -d\| | cut -f2 -d:) name=$(echo "${volume}" | cut -f1 -d\|) if [ "${size}" = "all" ];then mpoint=$(echo "${volume}" | cut -f3 -d\|) echo "${name},${mpoint}" return fi done < "${profile}" } function get_all_free_volume { read_volume_setup_all_free "/.profile" | cut -f1 -d, } function get_volume_name { echo "$1" | cut -f1 -d, } function get_volume_mount_point { echo "$1" | cut -f4 -d, } function get_volume_size_mode { echo "$1" | cut -f2 -d, } function get_volume_size { echo "$1" | cut -f3 -d, } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-mdraid-lib.sh0000644000000000000000000000205215273675667020064 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type set_root_map >/dev/null 2>&1 || . /lib/kiwi-lib.sh type wait_for_storage_device >/dev/null 2>&1 || . /lib/kiwi-partitions-lib.sh # The functions are called with the md raid device kiwi # created at build time, see lookup_raid_device_from_root() function mdraid_system { local raid_device=$1 if [ -n "${raid_device}" ];then return 0 fi return 1 } function deactivate_mdraid { local raid_device=$1 mdadm --stop "${raid_device}" } function activate_mdraid { local raid_device=$1 mdadm --assemble --scan "${raid_device}" wait_for_storage_device "${raid_device}" set_root_map "${raid_device}" } function update_devicesize_mdraid { local raid_device=$1 mdadm --assemble --scan "${raid_device}" --update=devicesize wait_for_storage_device "${raid_device}" deactivate_mdraid "${raid_device}" } function resize_mdraid { local raid_device=$1 set_device_lock "${raid_device}" \ mdadm --grow --size=max "${raid_device}" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-net-lib.sh0000644000000000000000000000464715273675667017426 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh # URI pattern Based on https://tools.ietf.org/html/rfc3986#appendix-B # with additional sub-expressions to split authority into # userinfo, host and port readonly URI_REGEX='^(([^:/?#]+):)?(//((([^:/?#]+)@)?([^:/?#]+)(:([0-9]+))?))?(/([^?#]*))(\?([^#]*))?(#(.*))?' function fetch_file { local source_url=$1 local source_uncompressed_bytes=$2 local fetch_info=/tmp/fetch.info local fetch local curl_options local dolly_options curl_options=$(getarg rd.kiwi.install.pxe.curl_options=) dolly_options=$(getarg rd.kiwi.install.pxe.dolly_options=) if [ -n "${curl_options}" ]; then curl_options=$(str_replace "${curl_options}" "," " ") fetch="curl ${curl_options} -f ${source_url}" else fetch="curl -f ${source_url}" fi if [ -n "${dolly_options}" ]; then dolly_options=$(str_replace "${dolly_options}" "," " ") fetch="dolly ${dolly_options} -f ${source_url}" fi if _is_dolly "${source_url}";then fetch="dolly" fi if _is_xz_compressed "${source_url}";then fetch="${fetch} | xz -d" fi if [ -z "${source_uncompressed_bytes}" ];then eval "${fetch}" 2>${fetch_info} else eval "${fetch}" 2>${fetch_info} |\ pv --size "${source_uncompressed_bytes}" --stop-at-size -n fi return "${PIPESTATUS[0]}" } function uri_scheme { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[2]}" } function uri_authority { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[4]}" } function uri_user { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[6]}" } function uri_host { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[7]}" } function uri_port { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[9]}" } function uri_path { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[10]}" } function uri_relative_path { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[11]}" } function uri_query { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[13]}" } function uri_fragment { [[ "$*" =~ ${URI_REGEX} ]] && echo "${BASH_REMATCH[15]}" } #====================================== # Methods considered private #-------------------------------------- function _is_xz_compressed { local source_url=$1 [[ ${source_url} =~ .xz$ ]] } function _is_dolly { local source_url=$1 [[ ${source_url} =~ ^dolly ]] } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/kiwi-partitions-lib.sh0000644000000000000000000004514615273675667021033 0ustar00#!/bin/bash type getarg >/dev/null 2>&1 || . /lib/dracut-lib.sh type udev_pending >/dev/null 2>&1 || . /lib/kiwi-lib.sh #====================================== # Library Methods #-------------------------------------- function create_partitions { local disk_device=$1 local partition_setup=$2 local pt_table_type pt_table_type=$(get_partition_table_type "${disk_device}") case ${pt_table_type} in "dos") create_msdos_partitions "${disk_device}" "${partition_setup}" ;; "gpt") create_gpt_partitions "${disk_device}" "${partition_setup}" ;; "dasd") create_dasd_partitions "${disk_device}" "${partition_setup}" ;; esac # notify the kernel about partition table changes if [ "${pt_table_type}" = "dasd" ];then # partx doesn't work for DASD disk, so on s390 partprobe is used partprobe "${disk_device}" else set_device_lock "${disk_device}" \ partx -u "${disk_device}" fi # the changes cause udev to remove and recreate links in /dev, # wait until that has settled udev_pending } function create_msdos_partitions { # """ # create partitions using sfdisk (msdos table) # """ local disk_device=$1 local partition_setup=$2 local partid local part_size_end local cmd_list local start_sector_from local cmd # put partition setup in a command list(cmd_list) for cmd in ${partition_setup};do # default values in sfdisk are used if - is provided cmd=$(echo "${cmd}" | tr . -) cmd_list[$index]=${cmd} index=$((index + 1)) done # operate on index based cmd_list index=0 for cmd in ${cmd_list[*]};do case ${cmd} in "d") # delete a partition... partid=${cmd_list[$index + 1]} start_sector_from[$partid]=$( _get_msdos_partition_start_sector "${disk_device}" "${partid}" ) set_device_lock "${disk_device}" \ sfdisk --force --delete "${disk_device}" "${partid}" ;; "n") # create a partition... partid=${cmd_list[$index + 2]} part_size_end=${cmd_list[$index + 4]} echo "${start_sector_from[$partid]},${part_size_end}" > /tmp/sfdisk.in set_device_lock "${disk_device}" \ sfdisk --force -N "${partid}" "${disk_device}" < /tmp/sfdisk.in ;; "t") # change a partition type... part_type=${cmd_list[$index + 2]} partid=${cmd_list[$index + 1]} set_device_lock "${disk_device}" \ sfdisk --force --change-id "${disk_device}" "${partid}" "${part_type}" ;; esac index=$((index + 1)) done } function create_gpt_partitions { # """ # create partitions using sgdisk (gpt table) # """ local disk_device=$1 local partition_setup=$2 local partid local part_name local part_size_start local part_size_end local cmd_list local cmd # put partition setup in a command list(cmd_list) for cmd in ${partition_setup};do # default values in sgdisk are used if 0 is provided cmd=$(echo "${cmd}" | tr . 0) cmd_list[$index]=${cmd} index=$((index + 1)) done # operate on index based cmd_list index=0 for cmd in ${cmd_list[*]};do case ${cmd} in "d") # delete a partition... partid=${cmd_list[$index + 1]} set_device_lock "${disk_device}" \ sgdisk --delete "${partid}" "${disk_device}" ;; "n") # create a partition... part_name=$(echo "${cmd_list[$index + 1]}" | tr : .) partid=${cmd_list[$index + 2]} part_size_start=${cmd_list[$index + 3]} part_size_end=${cmd_list[$index + 4]} set_device_lock "${disk_device}" \ sgdisk --new "${partid}:${part_size_start}:${part_size_end}" "${disk_device}" set_device_lock "${disk_device}" \ sgdisk --change-name "${partid}:${part_name}" "${disk_device}" ;; "t") # change a partition type... part_type=${cmd_list[$index + 2]} partid=${cmd_list[$index + 1]} set_device_lock "${disk_device}" \ sgdisk --typecode "${partid}:$(_to_guid "${part_type}")" "${disk_device}" ;; esac index=$((index + 1)) done } function create_dasd_partitions { # """ # create partitions using fdasd (s390) # """ local disk_device=$1 local partition_setup=$2 local partition_setup_file=/run/fdasd.cmds local ignore_cmd=0 local ignore_cmd_once=0 local cmd # We resize first partition to enfore rewrite of the current # partition table updated to the actual disk geometry. This is # to circumvent the fdasd limitation of not being capable to # expand the partition table up to the disk size bsc#1209247 parted --script --machine "${disk_device}" resizepart 1 # Trigger recreation of the VTOC to work around # https://github.com/ibm-s390-linux/s390-tools/issues/166 echo "u" >> ${partition_setup_file} echo "y" >> ${partition_setup_file} for cmd in ${partition_setup};do if [ "${ignore_cmd}" = 1 ] && echo "${cmd}" | grep -qE '[dntwq]';then ignore_cmd=0 elif [ "${ignore_cmd}" = 1 ];then continue fi if [ "${ignore_cmd_once}" = "1" ];then ignore_cmd_once=0 continue fi if [ "${cmd}" = "a" ];then ignore_cmd=1 continue fi if [[ "${cmd}" =~ ^p: ]];then ignore_cmd_once=1 continue fi if [ "${cmd}" = "83" ] || [ "${cmd}" = "8e" ];then cmd=1 fi if [ "${cmd}" = "82" ];then cmd=2 fi if [ "${cmd}" = "." ];then echo >> ${partition_setup_file} continue fi echo "$cmd" >> ${partition_setup_file} done echo "w" >> ${partition_setup_file} echo "q" >> ${partition_setup_file} set_device_lock "${disk_device}" \ fdasd "${disk_device}" < ${partition_setup_file} 1>&2 } function get_partition_node_name { # """ # Get unix node of the partition with the given number. # Partition numbers can have gaps, the number is therefore # not the position of the partition in the table but read # from the partition, see get_partition_number() # """ local disk=$1 local partid=$2 local name local kname local type udev_pending while read -r name kname type;do if [[ "${type}" =~ ^(part|md)$ ]] && \ [ "$(get_partition_number "${kname}")" = "${partid}" ] then echo "${name}" return 0 fi done < <(lsblk -p -l -n -o NAME,KNAME,TYPE "${disk}") return 1 } function get_partition_number { # """ # Get the number of the given partition, by its kernel # device name. The kernel provides the number for its # partitions in sysfs. A device mapper partition, e.g of a # multipath map, has the number in its device mapper uuid, # which is of the form part- # """ local kname local sysfs_device local partition_number local dm_uuid kname=$(basename "$1") sysfs_device=/sys/class/block/${kname} if [ -e "${sysfs_device}/partition" ];then read -r partition_number < "${sysfs_device}/partition" echo "${partition_number}" elif [ -e "${sysfs_device}/dm/uuid" ];then read -r dm_uuid < "${sysfs_device}/dm/uuid" if [[ "${dm_uuid}" =~ ^part([0-9]+)- ]];then echo "${BASH_REMATCH[1]}" fi fi } function get_partition_name { local disk=$1 local part_id=$2 local part_name local table_type table_type=$(get_partition_table_type "${disk}") if [ "${table_type}" = "gpt" ];then part_name=$(sfdisk --part-label "${disk}" "${part_id}") else part_name=p:unsupported fi echo "${part_name}" } function get_last_partition_device { # """ # Get unix node of last partition in table # """ local disk=$1 lsblk -p -n -r -o NAME,TYPE "${disk}" |\ grep -E "part|md$" | tail -n 1 | cut -f1 -d ' ' } function get_last_partition_id { # """ # Get number of the last partition from the current table. # Partition numbers can have gaps, e.g for custom partitions # with a given number, thus the highest number is read from # the table. If partx cannot read the table, e.g dasd on s390, # the partitions are counted instead # """ local disk=$1 local index=0 local partition_id for partition_id in $(partx -g -o NR "${disk}" 2>/dev/null);do if [ "${partition_id}" -gt "${index}" ];then index=${partition_id} fi done if [ "${index}" -gt 0 ];then echo "${index}" return fi lsblk -p -l -o NAME,TYPE "${disk}" | grep -cE "part|md$" } function wait_for_storage_device { # """ # function to check access on a storage device which could be # a whole disk or a partition. The function will wait until # the size of the storage device could be obtained and is # greater than zero or the timeout is reached. Default timeout # is set to 60 seconds, however it can be set to different # value by setting the DEVICE_TIMEOUT variable on the kernel # command line. # """ declare DEVICE_TIMEOUT=${DEVICE_TIMEOUT} local device=$1 local check=0 local limit=30 local storage_size=0 if [[ "${DEVICE_TIMEOUT}" =~ ^[0-9]+$ ]]; then limit=$(((DEVICE_TIMEOUT + 1)/ 2)) fi udev_pending while true;do storage_size=$(get_block_device_kbsize "${device}") if [ "${storage_size}" -gt 0 ]; then sleep 1; return 0 fi if [ "${check}" -eq "${limit}" ]; then die "Storage device ${device} did not appear" fi info "Waiting for storage device ${device} to settle..." check=$((check + 1)) sleep 2 done } function get_block_device_kbsize { local device=$1 if [ ! -e "${device}" ];then echo 0; return 1 fi echo $(($(blockdev --getsize64 "${device}") / 1024)) } function get_free_disk_bytes { local disk=$1 local pt_table_type local disk_bytes local max_dos_disk pt_table_type=$(get_partition_table_type "${disk}") disk_bytes=$(blockdev --getsize64 "${disk}") # max msdos table geometry is at 2TB - 512 max_dos_disk=$((4294967295 * 512)) if \ [ "${pt_table_type}" = "dos" ] && \ [ "${disk_bytes}" -gt "${max_dos_disk}" ] then warn "msdos table allows a max disk size of 2TB" warn "disk expansion will be truncated to 2TB" disk_bytes=${max_dos_disk} fi local rest_bytes rest_bytes=${disk_bytes} local part_bytes=0 local part_count=0 local part_uuids udev_pending for part in $( lsblk -p -l -o NAME,TYPE "${disk}" | grep -E "part|md$" | cut -f1 -d ' ' );do current_part_uuid=$(get_partition_uuid "${part}") for part_uuid in ${part_uuids[*]};do if [ "${current_part_uuid}" = "${part_uuid}" ];then # this partition uuid was already handled. The device # node is pointing to the same physical device and # should only be taken into account once unset part break fi done if [ -n "${part}" ]; then part_bytes=$((part_bytes + $(blockdev --getsize64 "${part}"))) part_uuids[${part_count}]=${current_part_uuid} part_count=$((part_count + 1)) fi done rest_bytes=$((rest_bytes - part_bytes)) echo ${rest_bytes} } function get_partition_table_type { local disk_device="$1" local table_type # blkid doesn't work for dasd partitions, so on s390 fdasd is used # checking if it can read the partition table of the device and if it # can set dasd as table_type. In any other case use blkid # to detect the partition type (bsc#1209247) if [[ "$(uname -m)" =~ s390 ]] && fdasd -p "${disk_device}" &>/dev/null;then table_type="dasd" echo "${table_type}" else blkid -s PTTYPE -o value "$disk_device" fi } function get_partition_uuid { blkid -s PARTUUID -o value "$1" } function relocate_gpt_at_end_of_disk { if ! sfdisk --relocate gpt-bak-std "$1";then die "Failed to write backup GPT at end of disk" fi } function disk_has_unallocated_space { local disk_device=$1 local free_mbytes local pt_table_type local flag="GPT PMBR size mismatch" pt_table_type=$(get_partition_table_type "${disk_device}") if [ "${pt_table_type}" = "gpt" ];then # GPT disks store a backup table at the end of the disk # if the disk geometry changes the backup table is no # longer at the end and this condition can be easily # checked and used to detect that there is space # unallocated due to a geometry change of the underlying # block device layer if sfdisk --list-free "${disk_device}" 2>&1 | grep -q "${flag}"; then return fi fi # lookup if there is free space on disk compared to # the current geometry values in the partition table free_mbytes=$( sfdisk --list-free "${disk_device}" |\ grep -i Unpartitioned | cut -f2 -d, | grep -o -E '[0-9]+' ) if [ "${free_mbytes}" != "0" ];then true else false fi } function activate_boot_partition { local disk_device=$1 local boot_partition_id=$2 local pt_table_type pt_table_type=$(get_partition_table_type "${disk_device}") if [[ "$(uname -m)" =~ i.86|x86_64 ]];then if [ "${pt_table_type}" = "dos" ];then set_device_lock "${disk_device}" \ sfdisk --activate "${disk_device}" "${boot_partition_id}" fi fi } function create_hybrid_gpt { local disk_device=$1 local partition_count udev_pending partition_count=$(lsblk -r -o NAME,TYPE "${disk_device}" | grep -c part) if [ "${partition_count}" -gt 3 ]; then # The max number of partitions to embed is 3 # see man sgdisk for details partition_count=3 fi if ! set_device_lock "${disk_device}" \ sgdisk -h "$(seq -s : 1 "${partition_count}")" "${disk_device}" then die "Failed to create hybrid GPT/MBR !" fi } function get_active_partition_id { # """ # Get number of the partition with the bootable flag # set in an msdos table. Nothing is provided if there is # no such partition or no msdos table. sfdisk names the # partitions it lists by the disk and the partition number # """ local disk_device=$1 local partition_device if [ "$(get_partition_table_type "${disk_device}")" = "dos" ];then partition_device=$( sfdisk --activate "${disk_device}" 2>/dev/null | head -n 1 ) if [[ "${partition_device}" =~ ([0-9]+)$ ]];then echo "${BASH_REMATCH[1]}" fi fi } function finalize_partition_table { # """ # finalize partition table with flags which # could have got lost during repartition. The # given boot partition is the one which was active # prior repartition, see get_active_partition_id() # """ declare kiwi_gpt_hybrid_mbr=${kiwi_gpt_hybrid_mbr} local disk_device=$1 local boot_partition_id=$2 if [ -n "${boot_partition_id}" ];then activate_boot_partition "${disk_device}" "${boot_partition_id}" fi if [ "${kiwi_gpt_hybrid_mbr}" = "true" ];then create_hybrid_gpt "${disk_device}" fi } function resize_wanted { # """ # check if oem-resize-once was requested in the image # description. If not we always try to repart/resize # the image according to the configured constraints. # # If oem-resize-once is set to true we check if the # system has been already resized compared to the # original image PARTUUID and repart/resize the system # only if the PARTUUID is still the original value. # After resize a new PARTUUID will be written by the # partitioner and that will result in the repart/resize # operation to happen only once in the livetime of # the image # # If the resize is wanted the method also checks for # a real change in geometry on the block device layer # and returns accordingly. Please note geometry change # can currently only be detected on GPT disks. In any # other case it is assumed the geometry has changed # such that a resize can at least be tried # """ declare kiwi_oemresizeonce=${kiwi_oemresizeonce} declare kiwi_rootpartuuid=${kiwi_rootpartuuid} local current_rootpart_uuid local root_device=$1 local disk_device=$2 kiwi_oemresizeonce=$(bool "${kiwi_oemresizeonce}") if getargbool 0 rd.kiwi.oem.force_resize; then info "System resize forced via rd.kiwi.oem.force_resize" info "Activating resize operation" return 0 fi if [ "${kiwi_oemresizeonce}" = "true" ];then current_rootpart_uuid=$(get_partition_uuid "${root_device}") if [ "${current_rootpart_uuid}" == "${kiwi_rootpartuuid}" ];then info "System was not yet resized" else info "System was already resized and oem-resize-once is requested" info "Skipping resize operation" return 1 fi else info "System resize is active on every reboot" fi if disk_has_unallocated_space "${disk_device}";then info "Activating resize operation" return 0 else info "Disk geometry did not change" info "Skipping resize operation" return 1 fi } #====================================== # Methods considered private #-------------------------------------- function _get_msdos_partition_start_sector { # """ # read start sector from given partition # """ local disk_device=$1 local partid=$2 sfdisk --dump "${disk_device}" |\ grep "${partid} :" | cut -f1 -d, | cut -f2 -d= | tr -d " " } function _to_guid { # """ # convert two digit partition id to guid id # """ local partid=$1 if [ "${partid}" = "83" ];then # Linux filesystem echo 8300 elif [ "${partid}" = "8e" ];then # Linux LVM echo 8e00 elif [ "${partid}" = "fd" ];then # Linux RAID echo fd00 elif [ "${partid}" = "82" ];then # Linux swap echo 8200 else # Assume Linux filesystem echo 8300 fi } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/dracut/modules.d/59kiwi-lib/module-setup.sh0000755000000000000000000000256415273675667017555 0ustar00#!/bin/bash # called by dracut check() { return 255 } # called by dracut depends() { echo udev-rules crypt bash return 0 } # called by dracut install() { declare moddir=${moddir} inst_multiple \ blkid blockdev dd mkdir rmdir env \ grep cut tail head tr bc true false mountpoint \ basename sfdisk sgdisk mkswap readlink lsblk \ btrfs xfs_growfs resize2fs \ e2fsck btrfsck xfs_repair \ vgs vgchange lvextend lvcreate lvresize pvresize \ mdadm cryptsetup dialog \ pv curl xz sha256sum sha512sum sed \ dmsetup touch chmod flock partx inst_multiple -o dolly if [[ "$(uname -m)" =~ s390 ]];then inst_multiple fdasd parted partprobe fi inst_simple \ "${moddir}/kiwi-lib.sh" "/lib/kiwi-lib.sh" inst_simple \ "${moddir}/kiwi-partitions-lib.sh" "/lib/kiwi-partitions-lib.sh" inst_simple \ "${moddir}/kiwi-filesystem-lib.sh" "/lib/kiwi-filesystem-lib.sh" inst_simple \ "${moddir}/kiwi-dialog-lib.sh" "/lib/kiwi-dialog-lib.sh" inst_simple \ "${moddir}/kiwi-mdraid-lib.sh" "/lib/kiwi-mdraid-lib.sh" inst_simple \ "${moddir}/kiwi-lvm-lib.sh" "/lib/kiwi-lvm-lib.sh" inst_simple \ "${moddir}/kiwi-luks-lib.sh" "/lib/kiwi-luks-lib.sh" inst_simple \ "${moddir}/kiwi-net-lib.sh" "/lib/kiwi-net-lib.sh" } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7391617 kiwi-11.1.8/helper/build_status.sh0000755000000000000000000000525715273675667014054 0ustar00#!/bin/bash pushd /run for project in \ Virtualization:Appliances:SelfContained:fedora \ Virtualization:Appliances:SelfContained:tumbleweed \ Virtualization:Appliances:SelfContained:leap \ Virtualization:Appliances:SelfContained:ubuntu \ Virtualization:Appliances:SelfContained:universal \ Virtualization:Appliances:SelfContained:staging \ Virtualization:Appliances:Images:Testing_x86:tumbleweed \ Virtualization:Appliances:Images:Testing_x86:rawhide \ Virtualization:Appliances:Images:Testing_ppc:rawhide \ Virtualization:Appliances:Images:Testing_x86:leap \ Virtualization:Appliances:Images:Testing_x86:slfo \ Virtualization:Appliances:Images:Testing_x86:centos \ Virtualization:Appliances:Images:Testing_x86:fedora \ Virtualization:Appliances:Images:Testing_x86:ubuntu \ Virtualization:Appliances:Images:Testing_arm:ubuntu \ Virtualization:Appliances:Images:Testing_x86:debian \ Virtualization:Appliances:Images:Testing_s390:tumbleweed \ Virtualization:Appliances:Images:Testing_s390:rawhide \ Virtualization:Appliances:Images:Testing_s390:leap \ Virtualization:Appliances:Images:Testing_arm:tumbleweed \ Virtualization:Appliances:Images:Testing_arm:fedora \ Virtualization:Appliances:Images:Testing_ppc:tumbleweed \ Virtualization:Appliances:Images:Testing_x86:archlinux do echo "${project}" if [ ! "$1" = "refresh" ];then while read -r line;do echo -e "$(echo "${line}" |\ sed -e s@F\ @'\\033[31mF \\e[0m'@g |\ sed -e s@U\ @'\\033[33mU \\e[0m'@g |\ sed -e s@D\ @'\\033[36mD\\e[0m'@g |\ sed -e s@\\.@'\\033[32m.\\e[0m'@g |\ sed -e 's@^x\sF@x \\033[31mF\\e[0m'@ |\ sed -e 's@^x\sU@x \\033[33mU\\e[0m'@ |\ sed -e 's@^x\sD@x \\033[36mD\\e[0m'@ |\ sed -e 's@^x\s\.@x \\033[32m.\\e[0m'@)" done < <(osc -A https://api.opensuse.org \ results -V "${project}" | sed -e 's@^\(\s\{1,\}\)@D\1@g' |\ grep -B100 Legend | grep -v Legend ) else for package in $(osc -A https://api.opensuse.org list "${project}");do if [[ "${package}" =~ ^test- ]];then echo -n "[refresh requested for ${package}: ]" osc -A https://api.opensuse.org \ service remoterun "${project}" "${package}" fi done echo fi done if [ ! "$1" = "refresh" ];then cat << EOF Legend: . succeeded D Disabled U unresolvable F failed B broken b blocked % building f finished s scheduled L locked x excluded d dispatching S signing ? buildstatus not available EOF fi popd ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7395945 kiwi-11.1.8/helper/rebuild_changelog_reference.sh0000755000000000000000000000024015273675667017010 0ustar00#!/bin/bash git log --no-merges --format=fuller > package/python-kiwi.changes git commit -S -m 'Update changelog reference' \ package/python-kiwi.changes ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7395945 kiwi-11.1.8/helper/update_changelog.py0000755000000000000000000001366415273675667014662 0ustar00#!/usr/bin/env python3 import typer import os import glob import subprocess import sys from dateutil import parser from dateutil import tz from typing import ( Annotated, Optional ) from pathlib import Path def main( since: Annotated[ Optional[Path], typer.Option( help='changes since the latest entry in the reference file' ) ] = None, file: Annotated[ Optional[Path], typer.Option( help='changes from the given file' ) ] = None, utc: Annotated[ Optional[bool], typer.Option( '--utc', help='print date/time in UTC' ) ] = False, fix: Annotated[ Optional[bool], typer.Option( '--fix', help='lookup .fix files and apply them' ) ] = False ): if not since and not file: print('Either --since or --file must be specified') sys.exit(1) if since and file: print('Only one of --since or --file must be specified') sys.exit(1) # Commandline arguments arguments = { '--since': since, '--file': file, '--utc': utc, '--fix': fix } # Latest date of the given reference file date_reference = None # List of skipped commits older than date_reference skip_list = [] # hash of git history log entries log_data = {} # raw list of log lines from git history or reference file log_lines = [] # Author and Date log_author = None log_date = None # changelog header line log_start = '-' * 67 + os.linesep # date format for rpm changelog date_format = '%a %b %d %T %Z %Y' # commit message commit_message = [] # Open reference log file reference_file = arguments['--since'] or arguments['--file'] # Custom fix files fix_dict = {} if arguments['--fix']: for fix in glob.iglob(f'{os.path.dirname(reference_file)}/*.fix'): sys.stderr.write(f'Reading fix: {fix}{os.linesep}') with open(fix, 'r') as fixlog: commit = fixlog.readline() fix_dict[commit] = fixlog.read() if arguments['--since']: # Read latest date from reference file with open(reference_file, 'r') as gitlog: # read commit and author gitlog.readline() gitlog.readline() # read date latest_date = gitlog.readline().replace('AuthorDate:', '').strip() date_reference = parser.parse(latest_date) # Read git history since latest entry from reference file process = subprocess.Popen( [ 'git', 'log', '--no-merges', '--format=fuller', '--since="{0}"'.format(latest_date) ], stdout=subprocess.PIPE ) from_git_log = True for line in iter(process.stdout.readline, b''): if line.startswith(b'commit'): commit = line.decode() if from_git_log and fix_dict.get(commit): sys.stderr.write(f' Apply fix for: {commit}') from_git_log = False log_lines.append(line) for fix_line in fix_dict.get(commit).split(os.linesep): log_lines.append(fix_line.encode()) elif not from_git_log: from_git_log = True if from_git_log: log_lines.append(line) else: with open(reference_file, 'rb') as gitlog: log_lines = gitlog.readlines() # Iterate over log data and convert to changelog format for line_data in log_lines: line = line_data.decode(encoding='utf-8') if line.startswith('commit'): if commit_message: commit_message.pop(0) message_header = commit_message.pop(0).lstrip() message_body = [] for line in commit_message: message_line = line.lstrip() if not message_line: message_body.append(os.linesep) else: message_body.append( ' {0}{1}'.format(message_line, os.linesep) ) log_data[log_date] = ''.join( [ log_start, '{0} - {1}{2}{2}'.format( log_date.astimezone( tz.UTC if arguments['--utc'] else tz.tzlocal() ).strftime(date_format), log_author, os.linesep ), '- {0}{1}'.format( message_header, os.linesep ) ] + message_body ) commit_message = [] elif line.startswith('Author:'): log_author = line.replace('Author:', '').strip() elif line.startswith('AuthorDate:'): log_date = parser.parse(line.replace('AuthorDate:', '').strip()) elif line.startswith('Commit:'): pass elif line.startswith('CommitDate:'): pass else: commit_message.append(line.strip()) # print in changelog format on stdout for author_date in reversed(sorted(log_data.keys())): if date_reference: if date_reference < author_date: sys.stdout.write(log_data[author_date]) else: skip_list.append(author_date) else: sys.stdout.write(log_data[author_date]) # print inconsistencies if any on stderr if skip_list: sys.stderr.write( 'Reference Date: {0}{1}'.format(date_reference, os.linesep) ) for date in skip_list: sys.stderr.write( ' + Skipped: {0}: past reference{1}'.format( date, os.linesep ) ) if __name__ == "__main__": typer.run(main) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7395945 kiwi-11.1.8/helper/update_obs_services.sh0000755000000000000000000000122215273675667015366 0ustar00#!/bin/bash set -euo pipefail api_target="https://api.opensuse.org/trigger/runservice" api_token=$1 while read -r build_test; do build_test_path=$(dirname "${build_test}") dist=$(echo "${build_test_path}" | cut -f 3 -d/) arch=$(echo "${build_test_path}" | cut -f 2 -d/) project="Virtualization:Appliances:Images:Testing_${arch}:${dist}" package=$(basename "${build_test_path}") curl_target="${api_target}?project=${project}&package=${package}" echo "Updating Test: ${package} for: ${project}" curl -H "Authorization: Token ${api_token}" \ -X POST "${curl_target}" done < <(find build-tests -name "appliance.kiwi") ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7395945 kiwi-11.1.8/helper/xsl_to_v74/convert14to20.xsl0000644000000000000000000000635015273675667016100 0ustar00 Changed attribute schemeversion from 1.4 to 2.0. Copy all attributes, when contents is NOT 'split'. If contents contains 'split' and attribute filesystem contains a comma, then split this attribute and create two new attributes fsreadwrite and fsreadonly. Change attribute value boot to bootstrap. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert20to24.xsl0000644000000000000000000000536415273675667016105 0ustar00 Changed attribute schemeversion from 2.0 to 2.4. Remove attributes memory,disk,HWversion,guestOS_32Bit and guestOS_64Bit from the 2.0 packages type [vmware|xen] version. This information needs to be provided by an additional vmwareconfig or xenconfig element NOTE: You need to setup a vmwareconfig and/or xenconfig section in order to setup an appropriate guest configuration. For details see the 'KIWI image description' chapter of the kiwi cookbook ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert24to35.xsl0000644000000000000000000000354015273675667016105 0ustar00 Changed attribute schemeversion to schemaversion from 2.4 to 3.5. Remove compressed element as it was moved into the type ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert35to37.xsl0000644000000000000000000000711715273675667016115 0ustar00 ]> Changed attribute schemaversion to schemaversion from 3.5 to 3.7. Change possible mixed cases into lowercase and normalize any whitespaces false true true false Wrong value! ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert37to38.xsl0000644000000000000000000000355215273675667016117 0ustar00 Changed attribute schemaversion to schemaversion from 3.7 to 3.8. Change attribute value deploy to pxedeploy. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert38to39.xsl0000644000000000000000000001143315273675667016116 0ustar00 Changed attribute schemaversion to schemaversion from 3.8 to 3.9. Create new oemconfig element to collect all oem-* elements in one place and remove them from the preferences element. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert39to41.xsl0000644000000000000000000002141215273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 3.9 to 4.1. Adding image attribute to type element and collecting the appropriate *config elements as children of the type element. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert41to42.xsl0000644000000000000000000001056515273675667016107 0ustar00 Changed attribute schemaversion to schemaversion from 4.1 to 4.2. Created new ec2config element to collect ec2* attributes. The ec2config element is a child of the type element. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert42to43.xsl0000644000000000000000000000625615273675667016113 0ustar00 Changed attribute schemaversion to schemaversion from 4.2 to 4.3. Moved the lvmgroup attribute from the type to the lvmvolumes element. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert43to44.xsl0000644000000000000000000000356515273675667016115 0ustar00 Changed attribute schemaversion to schemaversion from 4.3 to 4.4. Remove compressed element as it was moved into the type represented as attribute kernelcmdline ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert44to45.xsl0000644000000000000000000000354215273675667016112 0ustar00 Changed attribute schemaversion to schemaversion from 4.4 to 4.5. Remove patternPackageType attribute, it's no longer used ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert45to46.xsl0000644000000000000000000000363615273675667016120 0ustar00 Changed attribute schemaversion to schemaversion from 4.5 to 4.6. Change attribute value vmware to vmx. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert46to47.xsl0000644000000000000000000001033415273675667016113 0ustar00 Changed attribute schemaversion to schemaversion from 4.6 to 4.7. ide 0 true true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert47to48.xsl0000644000000000000000000001153615273675667016122 0ustar00 ]> Changed attribute schemaversion to schemaversion from 4.7 to 4.8. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert48to49.xsl0000644000000000000000000000351115273675667016116 0ustar00 ]> Changed attribute schemaversion to schemaversion from 4.8 to 4.9. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert49to50.xsl0000644000000000000000000000711015273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 4.9 to 5.0. vmx xenk xen domU ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert50to51.xsl0000644000000000000000000000401015273675667016073 0ustar00 Changed attribute schemaversion to schemaversion from 5.0 to 5.1. Remove obsolete baseroot attribute from types Remove defaultbaseroot element from preferences ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert51to52.xsl0000644000000000000000000000354015273675667016104 0ustar00 Changed attribute schemaversion to schemaversion from 5.1 to 5.2. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert52to53.xsl0000644000000000000000000000371115273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 5.2 to 5.3. AP-Northeast AP-Southeast ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert53to54.xsl0000644000000000000000000000345315273675667016113 0ustar00 Changed attribute schemaversion to schemaversion from 5.3 to 5.4. Remove obsolete type attribute from drivers ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert54to55.xsl0000644000000000000000000000336415273675667016116 0ustar00 Changed attribute schemaversion to schemaversion from 5.4 to 5.5. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert55to56.xsl0000644000000000000000000000573415273675667016123 0ustar00 Changed attribute schemaversion to schemaversion from 5.5 to 5.6. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert56to57.xsl0000644000000000000000000000361415273675667016120 0ustar00 Changed attribute schemaversion to schemaversion from 5.6 to 5.7. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert57to58.xsl0000644000000000000000000000344515273675667016124 0ustar00 Changed attribute schemaversion to schemaversion from 5.7 to 5.8. Remove obsolete syncMBR attribute from type ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert58to59.xsl0000644000000000000000000000472215273675667016125 0ustar00 Changed attribute schemaversion to schemaversion from 5.8 to 5.9. rename controller="scsi" to controller="lsilogic" in vmdisk ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert59to60.xsl0000644000000000000000000000310515273675667016110 0ustar00 Changed attribute schemaversion to schemaversion from 5.9 to 6.0. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert60to61.xsl0000644000000000000000000000340715273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 6.0 to 6.1. Change section opensuseProduct to product ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert61to62.xsl0000644000000000000000000000371415273675667016111 0ustar00 Changed attribute schemaversion to schemaversion from 6.1 to 6.2. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert62to63.xsl0000644000000000000000000000406015273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 6.2 to 6.3. Remove obsolete fsreadonly, fsreadwrite and zfsoptions attributes from types Remove obsolete split section ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert63to64.xsl0000644000000000000000000000445315273675667016116 0ustar00 Changed attribute schemaversion to schemaversion from 6.3 to 6.4. Remove obsolete group and id attribute from users Move global group attribute as groups to user section(s) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert64to65.xsl0000644000000000000000000000411215273675667016110 0ustar00 Changed attribute schemaversion to schemaversion from 6.4 to 6.5. Move container attribute from type to containerconfig ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert65to66.xsl0000644000000000000000000000435515273675667016123 0ustar00 Changed attribute schemaversion to schemaversion from 6.5 to 6.6. Add xen_server attribute to type for dom0 configurations Delete domain attribute from machine sections ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert66to67.xsl0000644000000000000000000000772415273675667016130 0ustar00 Changed attribute schemaversion to schemaversion from 6.6 to 6.7. Delete checkprebuilt and fsnocheck attributes from type Delete status and prefer-license attributes from repository Delete replaces attribute from package Delete defaultprebuilt section Delete defaultdestination section Delete defaultroot section Delete partitioner section Delete rpm-force section ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert67to68.xsl0000644000000000000000000000326415273675667016125 0ustar00 Changed attribute schemaversion to schemaversion from 6.7 to 6.8. Delete hwclock section ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert68to69.xsl0000644000000000000000000000365015273675667016126 0ustar00 Changed attribute schemaversion to schemaversion from 6.8 to 6.9. Delete hybrid attribute from type ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert69to70.xsl0000644000000000000000000000310115273675667016106 0ustar00 Changed attribute schemaversion to schemaversion from 6.9 to 7.0. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert70to71.xsl0000644000000000000000000000402215273675667016102 0ustar00 Changed attribute schemaversion to schemaversion from 7.0 to 7.1. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert71to72.xsl0000644000000000000000000000703715273675667016115 0ustar00 Changed attribute schemaversion to schemaversion from 7.1 to 7.2. Move bootloader, bootloader_console, boottimeout and zipl_targettype attributes from types into a new bootloader subsection ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert72to73.xsl0000644000000000000000000000367415273675667016122 0ustar00 Changed attribute schemaversion to schemaversion from 7.2 to 7.3. false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/convert73to74.xsl0000644000000000000000000000372315273675667016117 0ustar00 Changed attribute schemaversion to schemaversion from 7.3 to 7.4. apt dnf ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/include.xsl0000644000000000000000000000223215273675667015164 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/pretty.xsl0000644000000000000000000000361415273675667015075 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/helper/xsl_to_v74/update.xsl0000644000000000000000000001627015273675667015032 0ustar00 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7397454 kiwi-11.1.8/kiwi/__init__.py0000644000000000000000000000077315273675667012606 0ustar00""" API Documentation """ import logging from kiwi.logger import Logger # Initialize custom logger class for kiwi logging.setLoggerClass(Logger) # Set the highest log level possible as the default log level # in the main Logger class. This is needed to allow any logfile # handler to log all messages by default and to allow custom log # levels per handler. Our own implementation in Logger::setLogLevel # will then set the log level on a handler basis logging.getLogger('kiwi').setLevel(logging.DEBUG) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74099 kiwi-11.1.8/kiwi/api_helper.py0000644000000000000000000000506115273675667013152 0ustar00# Copyright (c) 2021 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import warnings from functools import wraps from typing import Any, Callable def obsolete(decommission_at: str, version: str) -> Callable: """ Decorator method to mark an API method as obsolete Methods marked with this decorator are still called. If the provided decommission date is reached it is allowed to decorate the obsoleted method as decommissioned. Example: .. code:: python @obsolete(decommission_at='2025-01-28', version='1.2.3') def method(): print('Method implementation...') :param str decommission_at: decommision date string. :param str version: the first version when this function was deprecated. :return: decorated function which is marked as deprecated """ def decorate_obsolete(func: Callable) -> Callable: @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> Any: warnings.warn( f'Function {func.__name__!r} is marked obsolete ' f'since version {version!r} ' f'and will be decommissioned at: {decommission_at}' ) return func(*args, **kwargs) return wrapper return decorate_obsolete def decommissioned(func: Callable) -> Callable: """ Decorator method to mark an API method as decommissioned Methods marked with this decorator are no longer called and raises DeprecationWarning when used. The method implementation is allowed to be replaced by a simple pass statement to get rid of old code Example: .. code:: python @decommissioned def method(): pass """ @wraps(func) def wrapper(*args: Any, **kwargs: Any) -> None: raise DeprecationWarning( f'Function {func.__name__!r} has been decommissioned' ) return wrapper ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74099 kiwi-11.1.8/kiwi/app.py0000644000000000000000000000234515273675667011624 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project from .tasks.base import CliTask class App: """ **Implements creation of task instances** Each task class implements a process method which is called when constructing an instance of App """ def __init__(self) -> None: app = CliTask(should_perform_task_setup=False) action = app.cli.get_command() service = app.cli.get_servicename() task_class_name = service.title() + action.title() + 'Task' task_class = app.task.__dict__[task_class_name] task_class().process() ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74099 kiwi-11.1.8/kiwi/archive/__init__.py0000644000000000000000000000000015273675667014206 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7412367 kiwi-11.1.8/kiwi/archive/cpio.py0000644000000000000000000000474715273675667013427 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Optional # project from kiwi.command import Command class ArchiveCpio: """ **Extraction/Creation of cpio archives** :param string filename: filename to use for archive extraction or creation """ def __init__(self, filename: str) -> None: self.filename = filename def create( self, source_dir: str, exclude: Optional[List[str]] = None ) -> None: """ Create cpio archive :param string source_dir: data source directory :param list exclude: list of excluded items """ find_excludes: List[str] = [] find_command = ['cd', source_dir, '&&', 'find', '.'] cpio_command = [ 'cpio', '--quiet', '-o', '-H', 'newc', '>', self.filename, ] if exclude: for path in exclude: if find_excludes: find_excludes.append('-or') find_excludes.append('-path') find_excludes.append('.' + path) find_excludes.append('-prune') find_excludes.append('-o') find_excludes.append('-print') find_command += find_excludes bash_command = find_command + ['|'] + cpio_command Command.run(['bash', '-c', ' '.join(bash_command)]) def extract(self, dest_dir: str) -> None: """ Extract cpio archive contents :param string dest_dir: target data directory """ bash_command = [ 'cd', dest_dir, '&&', 'cat', self.filename, '|', 'cpio', '-i', '--make-directories', ] Command.run(['bash', '-c', ' '.join(bash_command)]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7412367 kiwi-11.1.8/kiwi/archive/tar.py0000644000000000000000000001466315273675667013261 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from typing import List, Optional # project from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.utils.command_capabilities import CommandCapabilities class ArchiveTar: """ **Extraction/Creation of tar archives** The tarfile python module is not used by that class, since it does not provide support for some relevant features in comparison to the GNU tar command (e.g. numeric-owner). Moreover tarfile lacks support for xz compression under Python v2.7. :param string filename: filename to use for archive extraction or creation :param bool create_from_file_list: use file list not entire directory to create the archive :param list file_list: list of files and directorie names to archive """ def __init__( self, filename: str, create_from_file_list: bool = True, file_list: Optional[List[str]] = None, ) -> None: self.filename = filename self.create_from_file_list = create_from_file_list self.file_list = file_list if CommandCapabilities.check_version('tar', (1, 27)): self.xattrs_options = ['--xattrs', '--xattrs-include=*'] else: self.xattrs_options = [] def create( self, source_dir: str, exclude: Optional[List[str]] = None, options: Optional[List[str]] = None, ) -> str: """ Create uncompressed tar archive :param string source_dir: data source directory :param list exclude: list of excluded items :param list options: custom creation options """ if not options: options = [] Command.run( ['tar', '-C', source_dir] + options + self.xattrs_options + ['-c', '-f', self.filename] + self._get_archive_items(source_dir, exclude) ) return self.filename def append_files( self, source_dir: str, files_to_append: List[str], options: Optional[List[str]] = None, ) -> str: """ Append files to an already existing uncompressed tar archive :param string source_dir: data source directory :param list files_to_append: list of items to append :param list options: custom options """ if not options: options = [] Command.run( ['tar', '-C', source_dir, '-r', '--file=' + self.filename] + options + self.xattrs_options + files_to_append ) return self.filename def create_xz_compressed( self, source_dir: str, exclude: Optional[List[str]] = None, options: Optional[List[str]] = None, xz_options: Optional[List[str]] = None, ) -> str: """ Create XZ compressed tar archive :param string source_dir: data source directory :param list exclude: list of excluded items :param list options: custom tar creation options :param list xz_options: custom xz compression options """ if not options: options = [] if not xz_options: xz_options = Defaults.get_xz_compression_options() bash_command = ( ['tar', '-C', source_dir] + options + self.xattrs_options + ['-c', '--to-stdout'] + self._get_archive_items(source_dir, exclude) + ['|', 'xz', '-f'] + xz_options + ['>', self.filename + '.xz'] ) Command.run(['bash', '-c', ' '.join(bash_command)]) return self.filename + '.xz' def create_gnu_gzip_compressed( self, source_dir: str, exclude: Optional[List[str]] = None ) -> str: """ Create gzip compressed tar archive :param string source_dir: data source directory :param list exclude: list of excluded items """ Command.run( [ 'tar', '-C', source_dir, '--format=gnu', '-cSz', '-f', self.filename + '.gz', ] + self._get_archive_items(source_dir, exclude) ) return self.filename + '.gz' def extract(self, dest_dir: str) -> None: """ Extract tar archive contents :param string dest_dir: target data directory """ Command.run(['tar', '-C', dest_dir, '-x', '-v', '-f', self.filename]) def _get_archive_items( self, source_dir: str, exclude_list: Optional[List[str]] ) -> List[str]: if self.create_from_file_list: return self._get_archive_content_list( source_dir, exclude_list, self.file_list ) else: return self._get_archive_exclude_list(exclude_list) + ['.'] def _get_archive_content_list( self, source_dir: str, exclude_list: Optional[List[str]] = None, file_list: Optional[List[str]] = None, ) -> List[str]: final_archive_contents = [] archive_contents = file_list if not archive_contents: archive_contents = sorted(os.listdir(source_dir)) for item in archive_contents: if not exclude_list: final_archive_contents.append(item) elif item not in exclude_list: final_archive_contents.append(item) return final_archive_contents def _get_archive_exclude_list( self, exclude_list: Optional[List[str]] ) -> List[str]: archive_items = [] for exclude in exclude_list or []: archive_items.append('--exclude') archive_items.append('./' + exclude) return archive_items ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7412367 kiwi-11.1.8/kiwi/boot/__init__.py0000644000000000000000000000000015273675667013530 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7413633 kiwi-11.1.8/kiwi/boot/image/__init__.py0000644000000000000000000000456215273675667014633 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project import importlib from typing import List, Optional from abc import ABCMeta, abstractmethod from kiwi.xml_state import XMLState from kiwi.boot.image.base import BootImageBase from kiwi.exceptions import KiwiBootImageSetupError class BootImage(metaclass=ABCMeta): """ **BootImge Factory** :param object xml_state: Instance of :class:`XMLState` :param string target_dir: target dir to store the initrd :param string root_dir: system image root directory :param list signing_keys: list of package signing keys """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( xml_state: XMLState, target_dir: str, root_dir: Optional[str] = None, signing_keys: Optional[List[str]] = None, ) -> BootImageBase: initrd_system = xml_state.get_initrd_system() name_map = { 'kiwi': {'builtin_kiwi': 'BootImageKiwi'}, 'dracut': {'dracut': 'BootImageDracut'}, 'none': {'base': 'BootImageBase'}, } try: boot_image_namespace, boot_image_name = list( name_map[initrd_system].items() )[0] boot_image = importlib.import_module( 'kiwi.boot.image.{0}'.format(boot_image_namespace) ) return boot_image.__dict__[boot_image_name]( xml_state, target_dir, root_dir, signing_keys ) except Exception as issue: raise KiwiBootImageSetupError( 'Support for {0} initrd system not implemented: {1}'.format( initrd_system, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7414281 kiwi-11.1.8/kiwi/boot/image/base.py0000644000000000000000000005120115273675667013776 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import os import logging import glob from typing import Optional, List, Dict, NamedTuple # project from kiwi.mount_manager import MountManager from kiwi.defaults import Defaults from kiwi.system.setup import SystemSetup from kiwi.system.identifier import SystemIdentifier from kiwi.xml_description import XMLDescription from kiwi.xml_state import XMLState from kiwi.path import Path from kiwi.utils.sysconfig import SysConfig from kiwi.system.kernel import Kernel from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.exceptions import ( KiwiTargetDirectoryNotFound, KiwiConfigFileNotFound, KiwiDiskBootImageError, ) boot_names_type = NamedTuple( 'boot_names_type', [ ('kernel_name', str), ('initrd_name', str), ('kernel_version', str), ('kernel_filename', str), ], ) log = logging.getLogger('kiwi') class BootImageBase: """ **Base class for boot image(initrd) task** """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: Optional[str] = None, signing_keys: Optional[List[str]] = None, target_arch: Optional[str] = None, ) -> None: """ Create a base class instance of a BootImage The base class instance only support the common operations for boot images. The actual preparation and creation of an initrd is specific to the used initrd creation toolchain :param XMLState xml_state: Instance of :class:`XMLState` :param str target_dir: target dir to store the initrd :param str root_dir: system image root directory :param List[str] signing_keys: list of package signing keys :param str target_arch: target architecture """ self.xml_state = xml_state self.target_dir = target_dir self.initrd_filename = '' self.boot_xml_state: Optional[XMLState] = None self.setup: Optional[SystemSetup] = None self.signing_keys = signing_keys self.target_arch = target_arch self.boot_root_directory = root_dir or '' if not os.path.exists(target_dir): raise KiwiTargetDirectoryNotFound( f'target directory {target_dir} not found' ) self.initrd_base_name = ''.join( [ self.xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + self.xml_state.get_image_version(), '.initrd', ] ) self.post_init() def post_init(self) -> None: """ Post initialization method Implementation in specialized boot image class """ pass @staticmethod def has_initrd_support() -> bool: """ Indicates if this instance supports actual creation of an initrd The method needs to be overwritten by the subclass implementing preparation and creation of an initrd """ return False def add_argument(self, option: str, value: str = '') -> None: """ Add caller argument to boot image creation tool :param str option: argument name :param str value: optional argument value """ pass def include_file( self, filename: str, install_media: bool = False, delete_after_include: bool = False, ) -> None: """ Include file to boot image For kiwi boot images this is done by adding package or archive definitions with the bootinclude attribute. Thus for kiwi boot images the method is a noop :param str filename: file path name :param bool install_media: include also for installation media initrd :param bool delete_after_include: delete the file from the boot root once it is included """ pass def include_module(self, module: str, install_media: bool = False) -> None: """ Include module to boot image For kiwi boot no modules configuration is required. Thus in such a case this method is a noop. :param str module: module to include :param bool install_media: include the module for install initrds """ pass def omit_module(self, module: str, install_media: bool = False) -> None: """ Omit module to boot image For kiwi boot no modules configuration is required. Thus in such a case this method is a noop. :param str module: module to omit :param bool install_media: omit the module for install initrds """ pass def set_static_modules( self, modules: List[str], install_media: bool = False ) -> None: """ Set static modules list for boot image For kiwi boot no modules configuration is required. Thus in such a case this method is a noop. :param list modules: list of modules to include :param bool install_media: lists the modules for install initrds """ pass def include_driver(self, driver: str, install_media: bool = False) -> None: """ Include driver to the boot image For kiwi boot no drivers configuration is required. Thus in such a case this method is a noop. :param str driver: driver to include :param bool install_media: include the driver for install initrds """ pass def omit_driver(self, driver: str, install_media: bool = False) -> None: """ Omit driver to the boot image For kiwi boot no drivers configuration is required. Thus in such a case this method is a noop. :param str driver: driver to omit :param bool install_media: omit the driver for install initrds """ pass def write_system_config_file( self, config: Dict, config_file: Optional[str] = None ) -> None: """ Writes relevant boot image configuration into configuration file that will be part of the system image. This is used to configure any further boot image rebuilds after deployment. For instance, initrds recreated on kernel update. For kiwi boot no specific configuration is required for initrds recreation, thus this method is a noop in that case. :param dict config: dictonary including configuration parameters :param str config_file: configuration file to write """ pass def get_boot_names(self, flavor: str = '') -> boot_names_type: """ Provides kernel and initrd names for the boot image :param str flavor: names of the latest kernel of this flavor, of the latest kernel if not set, see Kernel.get_kernel() :return: Contains boot_names_type tuple .. code:: python boot_names_type( kernel_name='INSTALLED_KERNEL', initrd_name='DRACUT_OUTPUT_NAME', kernel_version='KERNEL_VERSION', kernel_filename='KERNEL_FILE_NAME' ) :rtype: boot_names_type """ kernel = Kernel(self.boot_root_directory) kernel_info = kernel.get_kernel(flavor=flavor) if not kernel_info: if self.xml_state.get_initrd_system() == 'none': return boot_names_type( kernel_name='none', initrd_name='none', kernel_version='none', kernel_filename='none', ) flavor_info = f' of flavor {flavor}' if flavor else '' raise KiwiDiskBootImageError( f'No kernel{flavor_info} in boot image tree ' f'{self.boot_root_directory} found' ) dracut_output_format = self._get_boot_image_output_file_format( kernel_info.version ) return boot_names_type( kernel_version=kernel_info.version, kernel_name=kernel_info.name, initrd_name=dracut_output_format.format( kernel_version=kernel_info.version ), kernel_filename=kernel_info.filename, ) def prepare(self) -> None: """ Prepare new root system to create initrd from. Implementation is only needed if there is no other root system available Implementation in specialized boot image class """ raise NotImplementedError def create_uki(self, cmdline: str) -> str: """ Create UKI EFI binary Implementation in specialized boot image class """ raise NotImplementedError def create_initrd( self, mbrid: Optional[SystemIdentifier] = None, basename: Optional[str] = None, install_initrd: bool = False, flavor: str = '', delete_included_files: bool = True, ) -> None: """ Implements creation of the initrd :param SystemIdentifier mbrid: instance of SystemIdentifier :param str basename: base initrd file name :param bool install_initrd: installation media initrd :param str flavor: create the initrd for the latest kernel of this flavor, for the latest kernel if not set :param bool delete_included_files: delete the files included with delete_after_include once the initrd is created. Keep them if another initrd which includes them is created afterwards, e.g for the kernel of another flavor Implementation in specialized boot image class """ raise NotImplementedError def is_prepared(self) -> bool: """ Check if initrd system is prepared. :return: True or False :rtype: bool """ return bool(os.listdir(self.boot_root_directory)) def load_boot_xml_description(self) -> None: """ Load the boot image description referenced by the system image description boot attribute """ log.info('Loading Boot XML description') boot_description_directory = self.get_boot_description_directory() if not boot_description_directory: raise KiwiConfigFileNotFound( 'no boot reference specified in XML description' ) boot_config_file = boot_description_directory + '/config.xml' if not os.path.exists(boot_config_file): raise KiwiConfigFileNotFound( f'no Boot XML description found in {boot_description_directory}' ) boot_description = XMLDescription( description=boot_config_file, derived_from=self.xml_state.xml_data.description_dir, ) boot_image_profile = self.xml_state.build_type.get_bootprofile() if not boot_image_profile: boot_image_profile = 'default' boot_kernel_profile = self.xml_state.build_type.get_bootkernel() if not boot_kernel_profile: boot_kernel_profile = 'std' self.boot_xml_state = XMLState( boot_description.load(), [boot_image_profile, boot_kernel_profile] ) log.info('--> loaded %s', boot_config_file) if self.boot_xml_state.build_type: log.info( '--> Selected build type: %s', self.boot_xml_state.get_build_type_name(), ) if self.boot_xml_state.profiles: log.info( '--> Selected boot profiles: image: %s, kernel: %s', boot_image_profile, boot_kernel_profile, ) def import_system_description_elements(self) -> None: """ Copy information from the system image relevant to create the boot image to the boot image state XML description """ self.xml_state.copy_displayname(self.boot_xml_state) self.xml_state.copy_name(self.boot_xml_state) self.xml_state.copy_repository_sections( target_state=self.boot_xml_state, wipe=True ) self.xml_state.copy_drivers_sections(self.boot_xml_state) strip_description = XMLDescription(Defaults.get_boot_image_strip_file()) strip_xml_state = XMLState(strip_description.load()) strip_xml_state.copy_strip_sections(self.boot_xml_state) self.xml_state.copy_strip_sections(self.boot_xml_state) preferences_subsection_names = [ 'bootloader_theme', 'bootsplash_theme', 'locale', 'packagemanager', 'rpm_check_signatures', 'rpm_excludedocs', 'showlicense', ] self.xml_state.copy_preferences_subsections( preferences_subsection_names, self.boot_xml_state ) self.xml_state.copy_bootincluded_packages(self.boot_xml_state) self.xml_state.copy_bootincluded_archives(self.boot_xml_state) self.xml_state.copy_bootdelete_packages(self.boot_xml_state) type_attributes = [ 'bootkernel', 'bootprofile', 'btrfs_root_is_snapper_snapshot', 'gpt_hybrid_mbr', 'devicepersistency', 'filesystem', 'firmware', 'fsmountoptions', 'hybridpersistent', 'hybridpersistent_filesystem', 'initrd_system', 'installboot', 'installprovidefailsafe', 'kernelcmdline', 'ramonly', 'target_removable', 'wwid_wait_timeout', ] self.xml_state.copy_build_type_attributes( type_attributes, self.boot_xml_state ) self.xml_state.copy_bootloader_section(self.boot_xml_state) self.xml_state.copy_systemdisk_section(self.boot_xml_state) self.xml_state.copy_machine_section(self.boot_xml_state) self.xml_state.copy_oemconfig_section(self.boot_xml_state) def get_boot_description_directory(self) -> Optional[str]: """ Provide path to the boot image XML description :return: path name :rtype: str """ boot_description = self.xml_state.build_type.get_boot() if boot_description: if not boot_description[0] == '/': boot_description = ( Defaults.get_boot_image_description_path() + '/' + boot_description ) return boot_description def cleanup(self) -> None: """ Cleanup temporary boot image data if any """ pass def _get_boot_image_output_file_format(self, kernel_version: str) -> str: """ The initrd output file format varies between the different Linux distributions. Tools like lsinitrd, and also grub2 rely on the initrd output file to be in that format. Thus kiwi should use the same file format to stay compatible with the distributions. The format is determined in three stages: a) check for an existing initrd file and use this naming schema b) if no initrd file is found check the dracut binary for its default output file name schema c) if no output file schema could be detected from the dracut binary return a default output file format which is initramfs-{kernel_version}.img Let's hope all this mess can be deleted once all distros just can agree on one initrd file name schema """ if self.xml_state.get_initrd_system() == 'kiwi': # The custom kiwi initrd system is used only on SUSE systems. # The initrd environment does not provide dracut and thus the # outfile format cannot be determined. on SUSE systems the # initrd format is different than on upstream and therefore # it can be explicitly specified. Note that the custom initrd # system will become obsolete in the future. default_outfile_format = 'initrd-{kernel_version}' else: default_outfile_format = 'initramfs-{kernel_version}.img' outfile_format = ( self._get_boot_image_output_file_format_from_existing_file( kernel_version ) ) if not outfile_format: outfile_format = ( self._get_boot_image_output_file_format_from_dracut_code() ) if not outfile_format: outfile_format = ( self._get_boot_image_output_file_format_from_config( kernel_version ) ) if outfile_format: return outfile_format else: log.warning('Could not detect dracut output file format') log.warning( 'Using default initrd file name format {0}'.format( default_outfile_format ) ) return default_outfile_format def _get_boot_image_output_file_format_from_dracut_code( self, ) -> Optional[str]: dracut_tool = Path.which( 'dracut', root_dir=self.boot_root_directory, access_mode=os.X_OK ) if dracut_tool: outfile_expression = r'outfile=".*/boot/(init.*kernel.*)"' with open(dracut_tool) as dracut: matches = re.findall(outfile_expression, dracut.read()) if matches: return ( matches[0] .replace('$kernel', '{kernel_version}') .replace('${kernel}', '{kernel_version}') ) return None def _get_boot_image_output_file_format_from_existing_file( self, kernel_version: str ) -> Optional[str]: """ Derive the initrd file name format from an initrd file which exists in the boot root directory Only a file whose name contains the version of an installed kernel tells the format: the version is replaced by the {kernel_version} placeholder, see Kernel.get_version_in_name(). A file of another name, e.g the initrd of a kernel which is not installed anymore, is skipped, its name would otherwise become the name of the new initrd and not fit to its kernel """ kernel = Kernel(self.boot_root_directory) for initrd_file in sorted( glob.iglob(self.boot_root_directory + '/boot/init*') ): if not os.path.islink(initrd_file): initrd_name = os.path.basename(initrd_file) version = kernel.get_version_in_name(initrd_name) if version: return initrd_name.replace(version, '{kernel_version}') return None def _get_boot_image_output_file_format_from_config( self, kernel_version: str ) -> Optional[str]: if CommandCapabilities.has_option_in_help( 'dracut', '--printconfig', ['--help'], root=self.boot_root_directory, raise_on_error=False, ): with MountManager( device='tmpfs', mountpoint=os.path.join(self.boot_root_directory, 'var/tmp'), ) as var_tmp_mount: var_tmp_mount.tmpfs_mount() dracut_call = Command.run( [ 'chroot', self.boot_root_directory, 'dracut', '--printconfig', ] ) temp_file = Temporary().new_file() with open(temp_file.name, 'w') as config: config.write(dracut_call.output.replace(' ', '')) dracut_config = SysConfig(temp_file.name) initrdname = dracut_config.get('dracutconfig:initrdname') if initrdname: return initrdname.replace( kernel_version, '{kernel_version}' ) return None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7414281 kiwi-11.1.8/kiwi/boot/image/builtin_kiwi.py0000644000000000000000000001703315273675667015562 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from kiwi.utils.temporary import Temporary from typing import Optional, List # project from kiwi.defaults import Defaults from kiwi.system.identifier import SystemIdentifier from kiwi.utils.sync import DataSync from kiwi.system.prepare import SystemPrepare from kiwi.system.profile import Profile from kiwi.system.setup import SystemSetup from kiwi.archive.cpio import ArchiveCpio from kiwi.utils.compress import Compress from kiwi.path import Path from kiwi.boot.image.base import BootImageBase log = logging.getLogger('kiwi') class BootImageKiwi(BootImageBase): """ **Implements preparation and creation of kiwi boot(initrd) images** The kiwi initrd is a customized first boot initrd which allows to control the first boot an appliance. The kiwi initrd replaces itself after first boot by the result of dracut. """ @staticmethod def has_initrd_support() -> bool: """ This instance supports initrd preparation and creation """ return True def post_init(self) -> None: """ Post initialization method Creates custom directory to prepare the boot image root filesystem which is a separate image to create the initrd from """ # builtin kiwi initrd builds its own root tree to create # an initrd from. Thus there is no pre defined boot root # directory self.boot_root_directory = '' self.temp_directories: List[str] = [] self.load_boot_xml_description() def prepare(self) -> None: """ Prepare new root system suitable to create a kiwi initrd from it """ if self.boot_xml_state: self.boot_root_directory_temporary = Temporary( prefix='kiwi_boot_root.', path=self.target_dir ).new_dir() self.boot_root_directory = self.boot_root_directory_temporary.name self.temp_directories.append(self.boot_root_directory) boot_image_name = self.boot_xml_state.xml_data.get_name() self.import_system_description_elements() log.info('Preparing boot image') with SystemPrepare( xml_state=self.boot_xml_state, root_dir=self.boot_root_directory, allow_existing=True, ) as system: with system.setup_repositories( signing_keys=self.signing_keys, target_arch=self.target_arch ) as manager: system.install_bootstrap(manager) system.install_system(manager) profile = Profile(self.boot_xml_state) profile.add('kiwi_initrdname', boot_image_name) defaults = Defaults() defaults.to_profile(profile) self.setup = SystemSetup( self.boot_xml_state, self.boot_root_directory ) profile.create( Defaults.get_profile_file(self.boot_root_directory) ) self.setup.import_description() self.setup.import_overlay_files(follow_links=True) self.setup.call_config_script() system.pinch_system(manager=manager, force=True) self.setup.call_image_script() self.setup.create_init_link_from_linuxrc() def create_initrd( self, mbrid: Optional[SystemIdentifier] = None, basename: Optional[str] = None, install_initrd: bool = False, flavor: str = '', delete_included_files: bool = True, ) -> None: """ Create initrd from prepared boot system tree and compress the result :param SystemIdentifier mbrid: instance of ImageIdentifier :param str basename: base initrd file name :param bool install_initrd: installation media initrd :param str flavor: unused, kernel flavors are only supported with the dracut initrd system :param bool delete_included_files: unused """ if self.is_prepared(): log.info('Creating initrd cpio archive') # we can't simply exclude boot when building the archive # because the file boot/mbrid must be preserved. Because of # that we create a copy of the boot directory and remove # everything in boot/ except for boot/mbrid. The original # boot directory should not be changed because we rely # on other data in boot/ e.g the kernel to be available # for the entire image building process if basename: kiwi_initrd_basename = basename else: kiwi_initrd_basename = self.initrd_base_name temp_boot_root = Temporary(prefix='kiwi_boot_root_copy.').new_dir() temp_boot_root_directory = temp_boot_root.name os.chmod(temp_boot_root_directory, 0o755) data = DataSync( self.boot_root_directory + '/', temp_boot_root_directory ) data.sync_data(options=Defaults.get_sync_options()) boot_directory = temp_boot_root_directory + '/boot' Path.wipe(boot_directory) if mbrid: log.info('--> Importing mbrid: %s', mbrid.get_id()) Path.create(boot_directory) image_identifier = boot_directory + '/mbrid' mbrid.write(image_identifier) cpio = ArchiveCpio( os.sep.join([self.target_dir, kiwi_initrd_basename]) ) # the following is a list of directories which were needed # during the process of creating an image but not when the # image is actually booting with this initrd exclude_from_archive = [ '/' + Defaults.get_shared_cache_location(), '/image', '/usr/lib/grub*', ] # the following is a list of directories to exclude which # are not needed inside of the initrd exclude_from_archive += [ '/usr/share/doc', '/usr/share/man', '/home', '/media', '/srv', ] cpio.create( source_dir=temp_boot_root_directory, exclude=exclude_from_archive, ) log.info('--> xz compressing archive') compress = Compress( os.sep.join([self.target_dir, kiwi_initrd_basename]) ) self.initrd_filename = compress.xz( ['--check=crc32', '--lzma2=dict=1MiB', '--threads=0'] ) def cleanup(self) -> None: for directory in self.temp_directories: if directory and os.path.exists(directory): Path.wipe(directory) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7414281 kiwi-11.1.8/kiwi/boot/image/dracut.py0000644000000000000000000004243015273675667014352 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from contextlib import ExitStack from typing import List, Optional, Dict # project from kiwi.command import Command from kiwi.system.kernel import Kernel from kiwi.boot.image.base import BootImageBase from kiwi.defaults import Defaults from kiwi.system.profile import Profile from kiwi.system.setup import SystemSetup from kiwi.system.identifier import SystemIdentifier from kiwi.mount_manager import MountManager log = logging.getLogger('kiwi') class BootImageDracut(BootImageBase): """ **Implements creation of dracut boot(initrd) images.** """ @staticmethod def has_initrd_support() -> bool: """ This instance supports initrd preparation and creation """ return True def post_init(self) -> None: """ Post initialization method Initialize empty list of dracut caller options """ # signing keys are only taken into account on install of # packages. As dracut runs from a pre defined root directory, # no signing keys will be used in the process of creating # an initrd with dracut self.signing_keys = None # Initialize empty list of dracut caller options self.dracut_options: List[str] = [] self.included_files: List[str] = [] self.delete_after_include_files: List[str] = [] self.modules: List[str] = [] self.add_modules: List[str] = [] self.omit_modules: List[str] = [] self.available_modules = self._get_modules() self.add_drivers: List[str] = [] self.omit_drivers: List[str] = [] self.available_drivers = self._get_drivers() def include_file( self, filename: str, install_media: bool = False, delete_after_include: bool = False, ) -> None: """ Include file to dracut boot image :param str filename: file path name :param bool install_media: unused :param bool delete_after_include: delete the file from the boot root once the initrd is created """ self.included_files.append('--install') self.included_files.append(filename) if delete_after_include: self.delete_after_include_files.append(filename) def include_module(self, module: str, install_media: bool = False) -> None: """ Include module to dracut boot image :param str module: module to include :param bool install_media: unused """ warn_msg = 'module "{0}" not included in initrd'.format(module) if self._module_available(module): if module not in self.add_modules: self.add_modules.append(module) else: log.warning(warn_msg) def omit_module(self, module: str, install_media: bool = False) -> None: """ Omit module to dracut boot image :param str module: module to omit :param bool install_media: unused """ if module not in self.omit_modules: self.omit_modules.append(module) def set_static_modules( self, modules: List[str], install_media: bool = False ) -> None: """ Set static dracut modules list for boot image :param list modules: list of the modules to include :param bool install_media: unused """ self.modules = modules def include_driver(self, driver: str, install_media: bool = False) -> None: """ Include driver to dracut boot image :param str driver: driver to include :param bool install_media: unused """ warn_msg = 'driver "{0}" not included in initrd'.format(driver) if self._driver_available(driver): if driver not in self.add_drivers: self.add_drivers.append(driver) else: log.warning(warn_msg) def omit_driver(self, driver: str, install_media: bool = False) -> None: """ Omit driver to dracut boot image :param str driver: driver to omit :param bool install_media: unused """ if driver not in self.omit_drivers: self.omit_drivers.append(driver) def write_system_config_file( self, config: Dict, config_file: Optional[str] = None ) -> None: """ Writes modules configuration into a dracut configuration file. :param dict config: a dictionary containing the modules to add and omit :param str conf_file: configuration file to write """ dracut_config = [] if not config_file: config_file = os.path.normpath( self.boot_root_directory + Defaults.get_dracut_conf_name() ) if config.get('modules'): modules = [ module for module in config['modules'] if self._module_available(module) ] dracut_config.append( 'add_dracutmodules+=" {0} "\n'.format(' '.join(modules)) ) if config.get('omit_modules'): dracut_config.append( 'omit_dracutmodules+=" {0} "\n'.format( ' '.join(config['omit_modules']) ) ) if config.get('install_items'): dracut_config.append( 'install_items+=" {0} "\n'.format( ' '.join(config['install_items']) ) ) if config.get('drivers'): drivers = [ driver for driver in config['drivers'] if self._driver_available(driver) ] dracut_config.append( 'add_drivers+=" {0} "\n'.format(' '.join(drivers)) ) if config.get('omit_drivers'): dracut_config.append( 'omit_drivers+=" {0} "\n'.format( ' '.join(config['omit_drivers']) ) ) if dracut_config and config_file: with open(config_file, 'w') as config_handle: config_handle.writelines(dracut_config) def prepare(self) -> None: """ Prepare dracut caller environment * Setup machine_id(s) to be generic and rebuild by dracut on boot """ setup = SystemSetup(self.xml_state, self.boot_root_directory) setup.setup_machine_id() self.dracut_options.append('--install') self.dracut_options.append('/.profile') def add_argument(self, option: str, value: str = '') -> None: """ Add caller argument to boot image creation tool :param str option: argument name :param str value: optional argument value """ self.dracut_options.append(option) if value: self.dracut_options.append(value) def create_uki(self, cmdline: str) -> str: """ Create UKI EFI binary """ if self.is_prepared(): log.info('Creating generic UKI EFI binary') self._create_profile_environment() kernel_info = Kernel(self.boot_root_directory) kernel_details = kernel_info.get_kernel(raise_on_not_found=True) boot_names = self.get_boot_names() uki_base_name = f'{boot_names.initrd_name}.efi' uki_base_name = uki_base_name.replace('initrd-', 'vmlinuz-') included_files = self.included_files modules_args = ( ['--modules', ' {0} '.format(' '.join(self.modules))] if self.modules else [] ) modules_args += ( ['--add', ' {0} '.format(' '.join(self.add_modules))] if self.add_modules else [] ) modules_args += ( ['--omit', ' {0} '.format(' '.join(self.omit_modules))] if self.omit_modules else [] ) drivers_arg = ( ['--drivers', ' {0} '.format(' '.join(self.add_drivers))] if self.add_drivers else [] ) drivers_arg += ( ['--omit-drivers', ' {0} '.format(' '.join(self.omit_drivers))] if self.omit_drivers else [] ) options = ( self.dracut_options + modules_args + drivers_arg + included_files ) if kernel_details: with ExitStack() as stack: device_mount = MountManager( device='/dev', mountpoint=self.boot_root_directory + '/dev', ) stack.push(device_mount) device_mount.bind_mount() proc_mount = MountManager( device='/proc', mountpoint=self.boot_root_directory + '/proc', ) stack.push(proc_mount) proc_mount.bind_mount() dracut_call = Command.run( [ 'chroot', self.boot_root_directory, 'dracut', '--no-hostonly', '--no-hostonly-cmdline', '--force', '--verbose', '--reproducible', '--kver', kernel_details.version, '--uefi', '--kernel-cmdline', cmdline, ] + options + [uki_base_name], stderr_to_stdout=True, ) log.debug(dracut_call.output) Command.run( [ 'mv', os.sep.join([self.boot_root_directory, uki_base_name]), f'{self.target_dir}/kiwi.efi', ] ) for filename in self.delete_after_include_files: os.unlink(f'{self.boot_root_directory}/{filename}') return os.path.normpath(os.sep.join([self.target_dir, 'kiwi.efi'])) return '' def create_initrd( self, mbrid: Optional[SystemIdentifier] = None, basename: Optional[str] = None, install_initrd: bool = False, flavor: str = '', delete_included_files: bool = True, ) -> None: """ Create kiwi .profile environment to be included in dracut initrd. Call dracut as chroot operation to create the initrd and move the result into the image build target directory :param SystemIdentifier mbrid: unused :param str basename: base initrd file name :param bool install_initrd: unused :param str flavor: create the initrd for the latest kernel of this flavor, for the latest kernel if not set :param bool delete_included_files: delete the files included with delete_after_include once the initrd is created. Keep them if another initrd which includes them is created afterwards, e.g for the kernel of another flavor """ if self.is_prepared(): log.info( 'Creating generic dracut initrd archive{0}'.format( f' for kernel flavor {flavor}' if flavor else '' ) ) self._create_profile_environment() kernel_info = Kernel(self.boot_root_directory) kernel_details = kernel_info.get_kernel( raise_on_not_found=True, flavor=flavor ) if basename: dracut_initrd_basename = basename else: dracut_initrd_basename = self.initrd_base_name included_files = self.included_files modules_args = ( ['--modules', ' {0} '.format(' '.join(self.modules))] if self.modules else [] ) modules_args += ( ['--add', ' {0} '.format(' '.join(self.add_modules))] if self.add_modules else [] ) modules_args += ( ['--omit', ' {0} '.format(' '.join(self.omit_modules))] if self.omit_modules else [] ) drivers_arg = ( ['--drivers', ' {0} '.format(' '.join(self.add_drivers))] if self.add_drivers else [] ) drivers_arg += ( ['--omit-drivers', ' {0} '.format(' '.join(self.omit_drivers))] if self.omit_drivers else [] ) options = ( self.dracut_options + modules_args + drivers_arg + included_files ) if kernel_details: with ExitStack() as stack: device_mount = MountManager( device='/dev', mountpoint=self.boot_root_directory + '/dev', ) stack.push(device_mount) device_mount.bind_mount() proc_mount = MountManager( device='/proc', mountpoint=self.boot_root_directory + '/proc', ) stack.push(proc_mount) proc_mount.bind_mount() dracut_call = Command.run( [ 'chroot', self.boot_root_directory, 'dracut', '--verbose', '--reproducible', '--no-hostonly', '--no-hostonly-cmdline', ] + options + [dracut_initrd_basename, kernel_details.version], stderr_to_stdout=True, ) log.debug(dracut_call.output) Command.run( [ 'mv', os.sep.join( [self.boot_root_directory, dracut_initrd_basename] ), self.target_dir, ] ) self.initrd_filename = os.sep.join( [self.target_dir, dracut_initrd_basename] ) Command.run(['chmod', '644', self.initrd_filename]) if delete_included_files: for filename in self.delete_after_include_files: os.unlink(f'{self.boot_root_directory}/{filename}') def _get_modules(self) -> List[str]: cmd = Command.run( [ 'chroot', self.boot_root_directory, 'dracut', '--list-modules', '--no-kernel', ] ) return cmd.output.splitlines() def _get_drivers(self) -> List[str]: try: kernel_info = Kernel(self.boot_root_directory).get_kernel() if not kernel_info: log.warning("No kernel found in boot directory") return [] cmd = Command.run( [ 'chroot', self.boot_root_directory, 'cat', f'/lib/modules/{kernel_info.version}/modules.dep', ] ) drivers = [ os.path.basename(line.split(':')[0].strip()).split('.ko')[0] for line in cmd.output.splitlines() if line.strip() ] return drivers except Exception as e: log.warning(f"Error reading drivers: {str(e)}") return [] def _module_available(self, module: str) -> bool: warn_msg = 'dracut module "{0}" not found in the root tree' if module in self.available_modules: return True log.warning(warn_msg.format(module)) return False def _driver_available(self, driver: str) -> bool: warn_msg = 'dracut driver "{0}" not found in the root tree' if driver in self.available_drivers: return True log.warning(warn_msg.format(driver)) return False def _create_profile_environment(self) -> None: profile = Profile(self.xml_state) defaults = Defaults() defaults.to_profile(profile) profile.create(Defaults.get_profile_file(self.boot_root_directory)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7414281 kiwi-11.1.8/kiwi/bootloader/__init__.py0000644000000000000000000000000015273675667014717 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7416813 kiwi-11.1.8/kiwi/bootloader/config/__init__.py0000644000000000000000000000725515273675667016207 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import TYPE_CHECKING, Dict, Literal, Optional, Union, overload from kiwi.exceptions import KiwiBootLoaderConfigSetupError if TYPE_CHECKING: # pragma: nocover from kiwi.xml_state import XMLState from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot from kiwi.bootloader.config.custom import BootLoaderConfigCustom from kiwi.bootloader.config.zipl import BootLoaderZipl @overload def create_boot_loader_config( *, name: Literal["grub2"], xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> "BootLoaderConfigGrub2": ... # pragma: nocover @overload def create_boot_loader_config( *, name: Literal["grub2_s390x_emu"], xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> "BootLoaderConfigGrub2": ... # pragma: nocover @overload def create_boot_loader_config( *, name: Literal["systemd_boot"], xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> "BootLoaderSystemdBoot": ... # pragma: nocover @overload def create_boot_loader_config( *, name: Literal["zipl"], xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> "BootLoaderZipl": ... # pragma: nocover @overload def create_boot_loader_config( *, name: str, xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> ( "Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderZipl]" ): ... # pragma: nocover def create_boot_loader_config( *, name: str, xml_state: "XMLState", root_dir: str, boot_dir: Optional[str] = None, custom_args: Optional[Dict] = None, ) -> "Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot, BootLoaderZipl, BootLoaderConfigCustom]": # the loaders read their custom arguments from a dict if custom_args is None: custom_args = {} if name in ("grub2", "grub2_s390x_emu"): from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 return BootLoaderConfigGrub2(xml_state, root_dir, boot_dir, custom_args) if name == "systemd_boot": from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot return BootLoaderSystemdBoot(xml_state, root_dir, boot_dir, custom_args) if name == "zipl": from kiwi.bootloader.config.zipl import BootLoaderZipl return BootLoaderZipl(xml_state, root_dir, boot_dir, custom_args) if name == "custom": from kiwi.bootloader.config.custom import BootLoaderConfigCustom return BootLoaderConfigCustom( xml_state, root_dir, boot_dir, custom_args ) raise KiwiBootLoaderConfigSetupError( f'Support for {name} bootloader config not implemented' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/base.py0000644000000000000000000006522615273675667015364 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from abc import ABC, abstractmethod import os import re import logging from collections import namedtuple from types import TracebackType from typing import Dict, List, Optional, Type, TypeVar # project from kiwi.mount_manager import MountManager from kiwi.storage.setup import DiskSetup from kiwi.path import Path from kiwi.defaults import Defaults, KERNEL_FLAVOR_NONE from kiwi.utils.block import BlockID from kiwi.system.setup import SystemSetup from kiwi.system.identifier import SystemIdentifier from kiwi.storage.disk import ptable_entry_type from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiBootLoaderTargetError log = logging.getLogger('kiwi') # the type of the bootloader config instance used as context manager BootLoaderConfigT = TypeVar('BootLoaderConfigT', bound='BootLoaderConfigBase') class BootLoaderConfigBase(ABC): """ **Base class for bootloader configuration** :param object xml_state: instance of :class:`XMLState` :param string root_dir: root directory path name :param dict custom_args: custom bootloader arguments dictionary """ def __init__( self, xml_state: XMLState, root_dir: str, boot_dir: Optional[str] = None, custom_args: Dict = {}, ) -> None: self.root_dir = root_dir self.boot_dir = boot_dir or root_dir self.xml_state = xml_state self.bootloader = xml_state.get_build_type_bootloader_name() self.arch = Defaults.get_platform_name() self.system_is_mounted = False self.volumes_mount: List[MountManager] = [] self.partitions_mount: List[MountManager] = [] self.root_mount: Optional[MountManager] = None self.boot_mount: Optional[MountManager] = None self.efi_mount: Optional[MountManager] = None self.device_mount: Optional[MountManager] = None self.proc_mount: Optional[MountManager] = None self.sys_mount: Optional[MountManager] = None self.tmp_mount: Optional[MountManager] = None self.etc_kernel_mount: Optional[MountManager] = None self.root_filesystem_is_overlay = xml_state.build_type.get_overlayroot() self.post_init(custom_args) def __enter__(self: BootLoaderConfigT) -> BootLoaderConfigT: return self def post_init(self, custom_args: Dict) -> None: """ Post initialization method Store custom arguments by default :param dict custom_args: custom bootloader arguments """ self.custom_args = custom_args @abstractmethod def write(self) -> None: """ Write config data to config file. Implementation in specialized bootloader class required """ def write_meta_data( self, root_device: Optional[str] = None, write_device: Optional[str] = None, boot_options: str = '', ) -> None: """ Write bootloader setup meta data files :param string root_device: root device node :param string write_device: overlay root write device node :param string boot_options: kernel options as string Implementation in specialized bootloader class optional """ pass @abstractmethod def setup_disk_image_config( self, boot_uuid: Optional[str] = None, root_uuid: Optional[str] = None, hypervisor: Optional[str] = None, kernel: Optional[str] = None, initrd: Optional[str] = None, boot_options: Dict = {}, ) -> None: """ Create boot config file to boot from disk. :param string boot_uuid: boot device UUID :param string root_uuid: root device UUID :param string hypervisor: hypervisor name :param string kernel: kernel name :param string initrd: initrd name :param dict boot_options: custom options dictionary required to setup the bootloader. The scope of the options covers all information needed to setup and configure the bootloader and gets effective in the individual implementation. boot_options should not be mixed up with commandline options used at boot time. This information is provided from the get_*_cmdline methods. The contents of the dictionary can vary between bootloaders or even not be needed Implementation in specialized bootloader class required """ @abstractmethod def setup_install_image_config( self, mbrid: SystemIdentifier, hypervisor: str, kernel: str, initrd: str ) -> None: """ Create boot config file to boot from install media in EFI mode. :param string mbrid: mbrid file name on boot device :param string hypervisor: hypervisor name :param string kernel: kernel name :param string initrd: initrd name Implementation in specialized bootloader class required """ @abstractmethod def setup_live_image_config( self, mbrid: SystemIdentifier, hypervisor: str, kernel: str, initrd: str ) -> None: """ Create boot config file to boot live ISO image in EFI mode. :param string mbrid: mbrid file name on boot device :param string hypervisor: hypervisor name :param string kernel: kernel name :param string initrd: initrd name Implementation in specialized bootloader class required """ @abstractmethod def setup_disk_boot_images( self, boot_uuid: str, efi_uuid: Optional[str] = None, lookup_path: Optional[str] = None, ) -> None: """ Create bootloader images for disk boot Some bootloaders requires to build a boot image the bootloader can load from a specific offset address or from a standardized path on a filesystem. :param string boot_uuid: boot device UUID :param string efi_uuid: EFI device UUID :param string lookup_path: custom module lookup path Implementation in specialized bootloader class required """ @abstractmethod def setup_install_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: """ Create bootloader images for ISO boot an install media :param string mbrid: mbrid file name on boot device :param string lookup_path: custom module lookup path Implementation in specialized bootloader class required """ @abstractmethod def setup_live_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: """ Create bootloader images for ISO boot a live ISO image :param string mbrid: mbrid file name on boot device :param string lookup_path: custom module lookup path Implementation in specialized bootloader class required """ @abstractmethod def setup_sysconfig_bootloader(self) -> None: """ Create or update etc/sysconfig/bootloader by parameters required according to the bootloader setup Implementation in specialized bootloader class required """ def create_efi_path(self, in_sub_dir: str = 'boot/efi') -> str: """ Create standard EFI boot directory structure :param string in_sub_dir: toplevel directory :return: Full qualified EFI boot path :rtype: str """ efi_boot_path = os.path.normpath( os.sep.join([self.boot_dir, in_sub_dir, 'EFI/BOOT']) ) Path.create(efi_boot_path) return efi_boot_path def get_boot_theme(self) -> Optional[str]: """ Bootloader Theme name :return: theme name :rtype: str """ theme = None for preferences in self.xml_state.get_preferences_sections(): section_content = preferences.get_bootloader_theme() if section_content: theme = section_content[0] return theme def get_boot_timeout_seconds(self) -> int: """ Bootloader timeout in seconds If no timeout is specified the default timeout applies :return: timeout seconds :rtype: int """ timeout_seconds = self.xml_state.get_build_type_bootloader_timeout() if timeout_seconds is None: timeout_seconds = Defaults.get_default_boot_timeout_seconds() return timeout_seconds def get_continue_on_timeout(self) -> bool: """ Check if the boot should continue after boot timeout or not :return: True or False :rtype: bool """ continue_on_timeout = ( self.xml_state.build_type.get_install_continue_on_timeout() ) if continue_on_timeout is None: continue_on_timeout = True return continue_on_timeout def failsafe_boot_entry_requested(self) -> bool: """ Check if a failsafe boot entry is requested :return: True or False :rtype: bool """ if self.xml_state.build_type.get_installprovidefailsafe() is False: return False return True def get_boot_cmdline( self, boot_device: Optional[str], write_device: Optional[str] = None ) -> str: """ Boot commandline arguments passed to the kernel :param string boot_device: boot device node. If no extra boot device exists then boot device equals root device. In case of an overlay setup the boot device equals the readonly root device :param string write_device: optional overlay write device node :return: kernel boot arguments :rtype: str """ cmdline = '' custom_cmdline = self.xml_state.build_type.get_kernelcmdline() if custom_cmdline: cmdline += ' ' + custom_cmdline overlay_cmdline = self._get_root_overlay_cmdline_parameter( boot_device, write_device ) if overlay_cmdline: cmdline += ' ' + overlay_cmdline custom_root = self._get_root_cmdline_parameter(boot_device) if custom_root and custom_root not in cmdline: cmdline += ' ' + custom_root return cmdline.strip() def get_install_image_boot_default( self, loader: Optional[str] = None ) -> str: """ Provide the default boot menu entry identifier for install images The install image can be configured to provide more than one boot menu entry. Menu entries configured are: * [0] Boot From Hard Disk * [1] Install * [2] Failsafe Install With kernel flavors there is one install entry per flavor, the failsafe install entry follows the last one. The install entry of the first flavor is the one selected by install The installboot attribute controlls which of these are used by default. If not specified the boot from hard disk entry will be the default. Depending on the specified loader type either an entry number or name will be returned. :param string loader: bootloader name :return: menu name or id :rtype: str """ menu_entry_title = self.get_menu_entry_title(plain=True) failsafe_install_id = format(1 + len(self.get_kernel_flavors())) menu_type = namedtuple('menu_type', ['name', 'menu_id']) menu_list = [ menu_type(name='Boot_from_Hard_Disk', menu_id='0'), menu_type(name='Install_' + menu_entry_title, menu_id='1'), menu_type( name='Failsafe_--_Install_' + menu_entry_title, menu_id=failsafe_install_id, ), ] boot_id = 0 install_boot_name = self.xml_state.build_type.get_installboot() if install_boot_name == 'failsafe-install': boot_id = 2 elif install_boot_name == 'install': boot_id = 1 if not self.failsafe_boot_entry_requested() and boot_id == 2: log.warning( 'Failsafe install requested but failsafe menu entry is disabled' ) log.warning('Switching to standard install') boot_id = 1 return menu_list[boot_id].menu_id def get_boot_path(self, target: str = 'disk') -> str: """ Bootloader lookup path on boot device If the bootloader reads the data it needs to boot, it does that from the configured boot device. Depending if that device is an extra boot partition or the root partition or or based on a non standard filesystem like a btrfs snapshot, the path name varies :param string target: target name: disk|iso :return: path name :rtype: str """ if target != 'disk' and target != 'iso': raise KiwiBootLoaderTargetError( f'Invalid boot loader target {target}' ) bootpath = '/boot' need_boot_partition = False if target == 'disk': disk_setup = DiskSetup(self.xml_state, self.boot_dir) need_boot_partition = disk_setup.need_boot_partition() if need_boot_partition: if self.bootloader != 'zipl': # if an extra boot partition is used we will find the # data directly in the root of this partition and not # below the boot/ directory. An exception to this case # is the zipl bootloader which finds its target # according to the mount path. bootpath = '/' if target == 'iso': bootpath = '/boot/' + self.arch + '/loader' return bootpath def quote_title(self, name: str) -> str: """ Quote special characters in the title name Not all characters can be displayed correctly in the bootloader environment. Therefore a quoting is required :param string name: title name :return: quoted text :rtype: str """ name = name.replace(' ', '_') name = name.replace('[', '(') name = name.replace(']', ')') return name def get_menu_entry_title(self, plain: bool = False) -> str: """ Prefixed menu entry title If no displayname is specified in the image description, the menu title is constructed from the image name and build type :param bool plain: indicate to add built type into title text :return: title text :rtype: str """ title = self.xml_state.xml_data.get_displayname() if not title: title = self.xml_state.xml_data.get_name() else: # if the title is set via the displayname attribute no custom # kiwi prefix or other style changes to that text should # be made plain = True type_name = self.xml_state.build_type.get_image() if plain: return title return title + ' [ ' + type_name.upper() + ' ]' def get_kernel_flavors(self) -> List[str]: """ Kernel flavors to create a boot menu entry for, in the order of the menu entries. The first flavor provides the default menu entry. If no kernel flavor is configured, the empty flavor stands for the latest kernel, see Kernel.get_kernel() :return: list of kernel flavor names :rtype: list """ return self.xml_state.get_build_type_bootloader_kernel_flavors() or [''] def get_kernel_flavor_title(self, title: str, flavor: str) -> str: """ Menu entry title for the kernel of the given flavor :param str title: menu entry title :param str flavor: kernel flavor name, the title is used as it is for the empty flavor and for the kernels without flavor, see Kernel.is_flavor() :return: title text :rtype: str """ if flavor and flavor != KERNEL_FLAVOR_NONE: return f'{title} ({flavor})' return title def get_menu_entry_install_title(self) -> str: """ Prefixed menu entry title for install images If no displayname is specified in the image description, the menu title is constructed from the image name :return: title text :rtype: str """ title = self.xml_state.xml_data.get_displayname() if not title: title = self.xml_state.xml_data.get_name() return title def _mount_system( self, device_map: Optional[Dict], root_device: str, boot_device: str, efi_device: Optional[str] = None, volumes: Optional[Dict] = None, root_volume_name: Optional[str] = None, partitions: Optional[Dict[str, ptable_entry_type]] = None, ) -> None: self.root_mount = MountManager(device=root_device) if 's390' in self.arch and self.bootloader == 'grub2_s390x_emu': self.boot_mount = MountManager( device=boot_device, mountpoint=self.root_mount.mountpoint + '/boot/zipl', ) else: self.boot_mount = MountManager( device=boot_device, mountpoint=self.root_mount.mountpoint + '/boot', ) if efi_device: self.efi_mount = MountManager( device=efi_device, mountpoint=self.root_mount.mountpoint + '/boot/efi', ) custom_root_mount_args = [] if root_volume_name and root_volume_name != '/': custom_root_mount_args += [f'subvol={root_volume_name}'] readonly_device = (device_map or {}).get('readonly') if readonly_device and root_device == readonly_device.get_device(): # The overlayroot readonly partition holds a read-only # filesystem (squashfs/erofs), mount it as such custom_root_mount_args += ['ro'] self.root_mount.mount(options=custom_root_mount_args) if not self.root_mount.device == self.boot_mount.device: self.boot_mount.mount() if volumes: for volume_path in Path.sort_by_hierarchy(sorted(volumes.keys())): volume_mount = MountManager( device=volumes[volume_path]['volume_device'], mountpoint=self.root_mount.mountpoint + '/' + volume_path, ) self.volumes_mount.append(volume_mount) volume_mount.mount( options=[volumes[volume_path]['volume_options']] ) if partitions: # the partitions are mounted from the given device map assert device_map is not None for map_name in sorted(partitions.keys()): if map_name in device_map: partition_mount = MountManager( device=device_map[map_name].get_device(), mountpoint=os.path.join( self.root_mount.mountpoint, partitions[map_name].mountpoint.lstrip(os.sep), ), ) self.partitions_mount.append(partition_mount) partition_mount.mount() if efi_device: assert self.efi_mount is not None self.efi_mount.mount() if self.root_filesystem_is_overlay: # In case of an overlay root system all parts of the rootfs # are read-only. However tools like grub's mkconfig creates # temporary files at call time and therefore /tmp needs to # be writable during the call time of the tools self.tmp_mount = MountManager( device='/tmp', mountpoint=self.root_mount.mountpoint + '/tmp' ) self.tmp_mount.bind_mount() # There are also tools that writes to /etc/kernel, e.g # systemd-boot. If it exists we map it to the ESP etc_kernel = f'{self.root_mount.mountpoint}/etc/kernel' if efi_device and os.path.exists(etc_kernel): self.etc_kernel_mount = MountManager( device=efi_device, mountpoint=etc_kernel ) self.etc_kernel_mount.mount() self.device_mount = MountManager( device='/dev', mountpoint=self.root_mount.mountpoint + '/dev' ) self.proc_mount = MountManager( device='/proc', mountpoint=self.root_mount.mountpoint + '/proc' ) self.sys_mount = MountManager( device='/sys', mountpoint=self.root_mount.mountpoint + '/sys' ) self.device_mount.bind_mount() self.proc_mount.bind_mount() self.sys_mount.bind_mount() self.system_is_mounted = True def _umount_system(self) -> None: if self.system_is_mounted: assert self.root_mount is not None # Rebuild security context setup = SystemSetup(self.xml_state, self.root_mount.mountpoint) setup.setup_selinux_file_contexts() # Umount system if self.efi_mount: self.efi_mount.umount() if self.boot_mount: self.boot_mount.umount() for volume_mount in reversed(self.volumes_mount): volume_mount.umount() for partition_mount in reversed(self.partitions_mount): partition_mount.umount() if self.device_mount: self.device_mount.umount() if self.proc_mount: self.proc_mount.umount() if self.sys_mount: self.sys_mount.umount() if self.tmp_mount: self.tmp_mount.umount() if self.etc_kernel_mount: self.etc_kernel_mount.umount() if self.root_mount: self.root_mount.umount() self.system_is_mounted = False def _get_root_cmdline_parameter( self, boot_device: Optional[str] ) -> Optional[str]: """ root= argument passed to the kernel :param string boot_device: boot device node. If no extra boot device exists then boot device equals root device. In case of an overlay setup the boot device equals the readonly root device """ cmdline = self.xml_state.build_type.get_kernelcmdline() if cmdline and 'root=' in cmdline: log.warning('Kernel root device explicitly set via kernelcmdline') root_search = re.search(r'(root=(.*)[ ]+|root=(.*)$)', cmdline) if root_search: return root_search.group(1) if boot_device: if self.xml_state.build_type.get_overlayroot(): # In case of an overlay setup the root partition is read-only # In this case the root location will be specified by the # partition uuid because not all read-only filesystems have one. # Exception to this is if the overlay is also encrypted # Exception to this is if the overlay is on verity verity = self.xml_state.build_type.get_verity_blocks() luks = self.xml_state.get_luks_credentials() if luks is not None: return 'root=overlay:MAPPER=luks' elif verity: return 'root=overlay:MAPPER=verityroot' else: root_location = self._get_location( boot_device, 'by-partuuid' ) return 'root=overlay:{0}={1}'.format( root_location['type'], root_location['name'] ) else: root_location = self._get_location(boot_device) return 'root={0}={1}'.format( root_location['type'], root_location['name'] ) else: log.warning('No explicit root= cmdline provided') return None def _get_root_overlay_cmdline_parameter( self, boot_device: Optional[str], write_device: Optional[str] ) -> str: """ rd.root.overlay.write= argument passed to the kernel :param string boot_device: boot device node. If no extra boot device exists then boot device equals root device. In case of an overlay setup the boot device equals the readonly root device :param string write_device: overlay write device """ root_overlay_parameter = '' cmdline = self.xml_state.build_type.get_kernelcmdline() if self.xml_state.build_type.get_overlayroot(): if cmdline and 'rd.root.overlay.write=' in cmdline: log.warning( 'Overlay write device explicitly set via kernelcmdline' ) elif write_device and write_device != boot_device: write_location = self._get_location(write_device) root_overlay_parameter = 'rd.root.overlay.write={0}'.format( write_location['node'] ) return root_overlay_parameter def _get_location( self, device: str, persistency_type: str = '' ) -> Dict[str, str]: if not persistency_type: persistency_type = self.xml_state.build_type.get_devicepersistency() block_operation = BlockID(device) if persistency_type == 'by-label': blkid_type = 'LABEL' elif persistency_type == 'by-partuuid': blkid_type = 'PARTUUID' else: persistency_type = 'by-uuid' blkid_type = 'UUID' location = block_operation.get_blkid(blkid_type) return { 'type': blkid_type, 'name': location, 'node': f'/dev/disk/{persistency_type}/{location}', } def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self._umount_system() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/bootloader_spec_base.py0000644000000000000000000002336415273675667020605 0ustar00# Copyright (c) 2022 Marcus Schäfer # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import glob from typing import Dict, NamedTuple, Optional # project from kiwi.firmware import FirmWare from kiwi.bootloader.config.base import BootLoaderConfigBase from kiwi.system.identifier import SystemIdentifier from kiwi.defaults import Defaults from kiwi.utils.os_release import OsRelease from kiwi.system.kernel import Kernel target_type = NamedTuple( 'target_type', [('disk', str), ('install', str), ('live', str)] ) class BootLoaderSpecBase(BootLoaderConfigBase): """ **Base class for Boot Loader Specification** This base class follows the bootloader specifications as described in: * https://uapi-group.org/specifications/specs/boot_loader_specification All supported bootloaders which follows this specification are derived from this base class :param object xml_state: instance of :class:`XMLState` :param str root_dir: root directory path name :param str boot_dir: boot directory path name :param dict custom_args: custom bootloader configuration parameters """ def post_init(self, custom_args: Dict = {}) -> None: """ Post init for all bootloader spec loaders Store custom arguments in an instance dict and initialize target identifiers """ self.entries_dir = Defaults.get_bls_loader_entries_dir() self.target = target_type( disk='disk', install='install(iso)', live='live(iso)' ) self.custom_args = custom_args self.timeout = self.get_boot_timeout_seconds() self.disk_type = self.xml_state.get_build_type_bootloader_targettype() self.disk_blocksize = self.xml_state.build_type.get_target_blocksize() self.firmware = FirmWare(self.xml_state) self.cmdline = '' def write(self) -> None: """ For bootloaders following the bootloader spec no collective write will happen """ pass def setup_sysconfig_bootloader(self) -> None: """ For bootloaders following the bootloader spec no setting to sysconfig/bootloader will be made """ pass def write_meta_data( self, root_device: Optional[str] = None, write_device: Optional[str] = None, boot_options: str = '', ) -> None: """ For bootloaders following the bootloader spec take over the cmdline options and store them for later use :param string root_device: unused :param string write_device: unused :param string boot_options: kernel options as string """ self.cmdline = f'{self.cmdline} {boot_options}'.strip() def setup_disk_image_config( self, boot_uuid: Optional[str] = '', root_uuid: Optional[str] = '', hypervisor: Optional[str] = '', kernel: Optional[str] = '', initrd: Optional[str] = '', boot_options: Dict = {}, ) -> None: """ Create boot config file to boot from disk :param str boot_uuid: boot device UUID :param str root_uuid: root device UUID :param str hypervisor: unused :param str kernel: kernel name :param str initrd: initrd name :param dict boot_options: custom options dictionary required to setup the bootloader. Targeted to bootloader spec interface """ self.custom_args['boot_uuid'] = boot_uuid self.custom_args['root_uuid'] = root_uuid self.custom_args['kernel'] = kernel self.custom_args['initrd'] = initrd self.custom_args['boot_options'] = boot_options plus_cmdline = ' '.join( [ self.get_boot_cmdline( boot_options.get('root_device'), boot_options.get('write_device'), ) ] ) self.cmdline = f'{self.cmdline} {plus_cmdline}'.strip() self.setup_loader(self.target.disk) def setup_install_image_config( self, mbrid: SystemIdentifier, hypervisor: str = '', kernel: str = '', initrd: str = '', ) -> None: """ Create boot config file to boot install ISO image :param str mbrid: mbrid file name on boot device :param str hypervisor: unused :param str kernel: kernel name :param str initrd: initrd name Targeted to bootloader spec interface """ pass def setup_live_image_config( self, mbrid: SystemIdentifier, hypervisor: str = '', kernel: str = '', initrd: str = '', ) -> None: """ Create boot config file to boot live ISO image :param str mbrid: mbrid file name on boot device :param str hypervisor: unused :param str kernel: kernel name :param str initrd: initrd name Targeted to bootloader spec interface """ pass def setup_disk_boot_images( self, boot_uuid: str, efi_uuid: Optional[str] = '', lookup_path: Optional[str] = '', ) -> None: """ Create bootloader image(s) for disk boot :param string boot_uuid: unused :param string efi_uuid: unused :param str lookup_path: unused Targeted to bootloader spec interface """ self.create_loader_image(self.target.disk) def setup_install_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = '' ) -> None: """ Create bootloader image(s) for install ISO boot :param string mbrid: unused :param str lookup_path: unused Targeted to bootloader spec interface """ self.create_loader_image(self.target.install) def setup_live_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = '' ) -> None: """ Create bootloader image(s) for live ISO boot :param string mbrid: unused :param str lookup_path: unused Targeted to bootloader spec interface """ self.create_loader_image(self.target.live) def create_loader_image(self, target: str) -> None: """ Create on demand bootloader image(s) For bootloaders following the bootloader spec we expect that the creation of a custom boot image is not needed. However, if needed this entry point exists :param str target: target identifier, one of disk, live(iso) or install(iso) Implementation in specialized loader class """ raise NotImplementedError def setup_loader(self, target: str) -> None: """ Setup main bootloader configuration boot/loader.conf :param str target: target identifier, one of disk, live(iso) or install(iso) Implementation in specialized loader class """ raise NotImplementedError def set_loader_entry(self, root_dir: str, target: str) -> None: """ Setup bootloader menu entry boot/loader/entries/X.conf :param str root_dir: path to root directory tree :param str target: target identifier, one of disk, live(iso) or install(iso) Implementation in specialized loader class """ raise NotImplementedError def get_entry_name(self, flavor: str = '') -> str: """ Construct entry filename of the form [OS_release_ID]-[kernel-version].conf or use the naming policy of an existing entries file The entry belongs to the kernel of the latest version, the one kiwi creates the initrd for, optionally of the given kernel flavor only, see Kernel.get_kernel(). An entry file produced by the distribution logic/policy is preferred over the kiwi naming policy: the entry of that kernel if there is one, otherwise the name of the entry of another installed kernel with the version replaced. An entry name without a kernel version is used as it is if no entry refers to a kernel :param str flavor: kernel flavor name :return: file basename :rtype: str """ os_release = OsRelease(self.root_dir) kernel = Kernel(self.root_dir) kernel_info = kernel.get_kernel(raise_on_not_found=True, flavor=flavor) kernel_name = kernel_info.version if kernel_info else '' entry_filename = f'{os_release.get("ID")}-{kernel_name}.conf' other_kernel_entry = '' versionless_entry = '' for dist_entry in sorted( glob.iglob(f'{self.root_dir}/{self.entries_dir}/*') ): dist_entry_name = os.path.basename(dist_entry) version = kernel.get_version_in_name(dist_entry_name) if version == kernel_name: return dist_entry_name if version and not other_kernel_entry: other_kernel_entry = dist_entry_name.replace( version, kernel_name ) if not version and not versionless_entry: versionless_entry = dist_entry_name return other_kernel_entry or versionless_entry or entry_filename ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/custom.py0000644000000000000000000000454215273675667015756 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Dict, Optional # project from kiwi.system.identifier import SystemIdentifier from kiwi.bootloader.config.base import BootLoaderConfigBase class BootLoaderConfigCustom(BootLoaderConfigBase): """ **custom bootloader configuration.** """ def setup_disk_boot_images( self, boot_uuid: str, efi_uuid: Optional[str] = None, lookup_path: Optional[str] = None, ) -> None: raise NotImplementedError def setup_disk_image_config( self, boot_uuid: Optional[str] = '', root_uuid: Optional[str] = '', hypervisor: Optional[str] = '', kernel: Optional[str] = '', initrd: Optional[str] = '', boot_options: Dict = {}, ) -> None: raise NotImplementedError def setup_install_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = '' ) -> None: raise NotImplementedError def setup_install_image_config( self, mbrid: SystemIdentifier, hypervisor: str = '', kernel: str = '', initrd: str = '', ) -> None: raise NotImplementedError def setup_live_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = '' ) -> None: raise NotImplementedError def setup_live_image_config( self, mbrid: SystemIdentifier, hypervisor: str = '', kernel: str = '', initrd: str = '', ) -> None: raise NotImplementedError def setup_sysconfig_bootloader(self) -> None: raise NotImplementedError def write(self) -> None: raise NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/grub2.py0000644000000000000000000023562715273675667015477 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from string import Template from textwrap import dedent import re import sys import os import logging import glob import shutil from collections import OrderedDict from typing import Dict, List, Optional, Union # project from kiwi.xml_state import HostKeyCertificateT from kiwi.bootloader.config.base import BootLoaderConfigBase from kiwi.bootloader.template.grub2 import BootLoaderTemplateGrub2 from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.exceptions import KiwiFileNotFound from kiwi.firmware import FirmWare from kiwi.path import Path from kiwi.system.identifier import SystemIdentifier from kiwi.system.kernel import Kernel from kiwi.utils.sparse import SparseFile from kiwi.utils.sync import DataSync from kiwi.utils.sysconfig import SysConfig from kiwi.utils.temporary import Temporary from kiwi.exceptions import ( KiwiTemplateError, KiwiBootLoaderGrubPlatformError, KiwiBootLoaderGrubModulesError, KiwiBootLoaderGrubSecureBootError, KiwiBootLoaderGrubFontError, KiwiBootLoaderGrubMenuEntryError, KiwiDiskGeometryError, KiwiCommandNotFound, ) import kiwi.defaults as defaults log = logging.getLogger('kiwi') def func_name() -> str: """ helper method to return name of caller """ return sys._getframe(1).f_code.co_name class BootLoaderConfigGrub2(BootLoaderConfigBase): """ **grub2 bootloader configuration.** """ def post_init(self, custom_args: Dict) -> None: """ grub2 post initialization method :param dict custom_args: Contains grub config arguments .. code:: python {'grub_directory_name': 'grub|grub2'} """ self.host_key_certificates: List[HostKeyCertificateT] = ( self.xml_state.get_host_key_certificates() ) self.efi_csm = ( True if self.xml_state.build_type.get_eficsm() is None else self.xml_state.build_type.get_eficsm() ) self.custom_args = custom_args self.config_options: List[str] = [] arch = Defaults.get_platform_name() if arch == 'x86_64': # grub2 support for bios and efi systems self.arch = arch elif arch == 'loongarch64': # grub2 support for efi systems self.arch = arch elif arch.startswith('ppc64'): # grub2 support for ofw and opal systems self.arch = arch elif arch == 'ix86': # grub2 support for bios systems self.arch = arch elif arch == 'aarch64' or arch.startswith('arm'): # grub2 support for efi systems self.arch = arch elif arch.startswith('s390'): # grub2 support for s390x systems self.arch = arch elif arch == 'riscv64': # grub2 support for efi systems self.arch = arch else: raise KiwiBootLoaderGrubPlatformError( f'host architecture {arch} not supported for grub2 setup' ) if self.custom_args and 'grub_directory_name' in self.custom_args: self.boot_directory_name = self.custom_args['grub_directory_name'] else: self.boot_directory_name = 'grub' if self.custom_args and 'grub_load_command' in self.custom_args: self.grub_load = self.custom_args['grub_load_command'] else: self.grub_load = 'source' if self.custom_args and 'config_options' in self.custom_args: self.config_options = self.custom_args['config_options'] self.terminal_output = ( self.xml_state.get_build_type_bootloader_console()[0] or 'console' ) self.terminal_input = ( self.xml_state.get_build_type_bootloader_console()[1] or 'console' ) self.gfxmode = self.xml_state.get_build_type_bootloader_video_mode() self.theme = self.get_boot_theme() self.timeout = self.get_boot_timeout_seconds() self.bootpartition = self.xml_state.build_type.get_bootpartition() self.timeout_style = ( self.xml_state.get_build_type_bootloader_timeout_style() ) self.displayname = self.xml_state.xml_data.get_displayname() self.bls = self.xml_state.get_build_type_bootloader_bls() self.serial_line_setup = ( self.xml_state.get_build_type_bootloader_serial_line_setup() ) self.continue_on_timeout = self.get_continue_on_timeout() self.failsafe_boot = self.failsafe_boot_entry_requested() self.mediacheck_boot = self.xml_state.build_type.get_mediacheck() self.xen_guest = self.xml_state.is_xen_guest() self.persistency_type = ( self.xml_state.build_type.get_devicepersistency() ) self.firmware = FirmWare(self.xml_state) self.target_table_type = self.firmware.get_partition_table_type() self.live_type = self.xml_state.build_type.get_flags() if not self.live_type: self.live_type = Defaults.get_default_live_iso_type() self.volume_id = ( self.xml_state.build_type.get_volid() or Defaults.get_volume_id() ) self.install_volid = ( self.xml_state.build_type.get_volid() or Defaults.get_install_volume_id() ) self.use_disk_password = ( self.xml_state.get_build_type_bootloader_use_disk_password() ) self.live_boot_options = [ 'root=live:CDLABEL={0}'.format(self.volume_id), 'rd.live.image', ] self.install_boot_options = ['loglevel=0'] if self.xml_state.get_initrd_system() == 'dracut': self.install_boot_options.append( 'root=install:CDLABEL={0}'.format(self.install_volid) ) if self.xml_state.build_type.get_hybridpersistent(): self.live_boot_options += ( Defaults.get_live_iso_persistent_boot_options( self.xml_state.build_type.get_hybridpersistent_filesystem() ) ) if self.xml_state.is_xen_server(): self.multiboot = True elif self.xen_guest: self.multiboot = False else: self.multiboot = False self.grub2 = BootLoaderTemplateGrub2() self.config: Optional[str] = None self.efi_boot_path: Optional[str] = None self.cmdline_failsafe: Optional[str] = None self.root_reference: Optional[str] = None self.cmdline: Optional[str] = None self.iso_boot = False self.early_boot_script_efi: Optional[str] = None def write(self) -> None: """ Write bootloader configuration * writes grub.cfg template by KIWI if template system is used * creates an embedded fat efi image for EFI ISO boot """ if self.config: log.info('Writing KIWI template grub.cfg file') config_dir = self._get_grub2_boot_path() config_file = config_dir + '/grub.cfg' Path.create(config_dir) with open(config_file, 'w') as config: config.write(self.config) def setup_sysconfig_bootloader(self) -> None: raise NotImplementedError def write_meta_data( self, root_device: Optional[str] = None, write_device: Optional[str] = None, boot_options: str = '', ) -> None: """ Write bootloader setup meta data files * cmdline arguments initialization * etc/default/grub setup file * etc/default/zipl2grub.conf.in (s390 only) * etc/sysconfig/bootloader :param string root_device: root device node :param string write_device: overlay root write device node :param string boot_options: kernel options as string :param bool iso_boot: indicate target is an ISO """ self.cmdline = ' '.join( [self.get_boot_cmdline(root_device, write_device), boot_options] ) self.cmdline_failsafe = ' '.join( [self.cmdline, Defaults.get_failsafe_kernel_options(), boot_options] ) self.root_reference = self._get_root_cmdline_parameter(root_device) self._setup_default_grub() if self.arch.startswith('s390'): self._setup_zipl2grub_conf() def setup_disk_image_config( self, boot_uuid: Optional[str] = None, root_uuid: Optional[str] = None, hypervisor: Optional[str] = None, kernel: Optional[str] = None, initrd: Optional[str] = None, boot_options: Dict = {}, ) -> None: """ Create grub2 config file to boot from disk using grub2-mkconfig :param string boot_uuid: unused :param string root_uuid: unused :param string hypervisor: unused :param string kernel: unused :param string initrd: unused :param dict boot_options: options dictionary that has to contain the root and boot device and optional volume configuration. KIWI has to mount the system prior to run grub2-mkconfig. .. code:: python { 'root_device': string, 'boot_device': string, 'efi_device': string, 'system_volumes': volume_manager_instance.get_volumes(), 'system_root_volume': volume_manager_instance.get_root_volume_name(), 'system_partitions': xml_state.get_partitions() } """ self._mount_system( boot_options.get('device_map'), boot_options['root_device'], boot_options['boot_device'], boot_options.get('efi_device'), boot_options.get('system_volumes'), boot_options.get('system_root_volume'), boot_options.get('system_partitions'), ) assert self.root_mount is not None config_file = os.sep.join( [ self.root_mount.mountpoint, 'boot', self.boot_directory_name, 'grub.cfg', ] ) # Setup/Update loader environment self._setup_loader_variables( self.xml_state.get_build_type_bootloader_environment_variables() ) # Disable os-prober, it takes information from the host it # runs on which is not necessarily matching with the image os.environ.update({'GRUB_DISABLE_OS_PROBER': 'true'}) Command.run( [ 'chroot', self.root_mount.mountpoint, os.path.basename(self._get_grub2_mkconfig_tool()), '-o', config_file.replace(self.root_mount.mountpoint, ''), ], os.environ, ) if boot_options.get('root_device') != boot_options.get('boot_device'): # Create a boot -> . link on the boot partition. # The link is useful if the grub mkconfig command # references all boot files from /boot/... even if # an extra boot partition should cause it to read the # data from the toplevel bash_command = [ 'cd', os.sep.join([self.root_mount.mountpoint, 'boot']), '&&', 'rm', '-f', 'boot', '&&', 'ln', '-s', '.', 'boot', ] # not every filesystem supports symlinks, and the link is optional # therefore don't fail in case the link cannot be created Command.run( ['bash', '-c', ' '.join(bash_command)], raise_on_error=False ) # Create UKI image if requested dracut_setup = self.xml_state.get_dracut_config('setup') if dracut_setup.uefi: boot_image = boot_options.get('boot_image') if boot_image: efi_uki_path = ( f'{self.root_mount.mountpoint}/boot/efi/EFI/Linux/' ) Path.create(efi_uki_path) uki_image = boot_image.create_uki(self.cmdline) Command.run(['mv', uki_image, efi_uki_path]) # Patch the written grub config file to actually work: # Unfortunately the grub tooling has several bugs and issues # which prevents it to work in image build environments. # More details can be found in the individual methods. # One fine day the following fix methods can hopefully # be deleted... self._fix_grub_root_device_reference(config_file, boot_options) self._fix_grub_loader_entries_boot_cmdline() self._fix_grub_loader_entries_linux_and_initrd_paths() # Select the menu entry of the first kernel flavor as default self._setup_kernel_flavor_default_entry(config_file) if self.firmware.efi_mode() and self.early_boot_script_efi: # in EFI mode the ESP is mounted by _mount_system() assert self.efi_mount is not None self._copy_grub_config_to_efi_path( self.efi_mount.mountpoint, self.early_boot_script_efi ) self._umount_system() def setup_install_image_config( self, mbrid: SystemIdentifier, hypervisor: str = 'xen.gz', kernel: str = 'linux', initrd: str = 'initrd', ) -> None: """ Create grub2 config file to boot from an ISO install image :param string mbrid: mbrid file name on boot device :param string hypervisor: hypervisor name :param string kernel: kernel name :param string initrd: initrd name """ log.info('Creating grub2 install config file from template') self.iso_boot = True has_graphics = False has_serial = False if 'gfxterm' in self.terminal_output: has_graphics = True if 'serial' in self.terminal_output or 'serial' in self.terminal_input: has_serial = True assert self.cmdline is not None assert self.cmdline_failsafe is not None # the identifier is calculated by the image builder mbrid_id = mbrid.get_id() assert mbrid_id is not None parameters = { 'search_params': '--file --set=root /boot/' + mbrid_id, 'default_boot': self.get_install_image_boot_default(), 'kernel_file': kernel, 'initrd_file': initrd, 'boot_options': ' '.join( [self.cmdline] + self.install_boot_options ), 'failsafe_boot_options': ' '.join( [self.cmdline_failsafe] + self.install_boot_options ), 'gfxmode': self.gfxmode, 'theme': self.theme, 'boot_timeout': self.timeout, 'boot_timeout_style': self.timeout_style or 'menu', 'serial_line_setup': self.serial_line_setup or '', 'bootpath': self.get_boot_path('iso'), 'boot_directory_name': self.boot_directory_name, 'efi_image_name': Defaults.get_efi_image_name(self.arch), 'terminal_input': self.terminal_input, 'terminal_output': self.terminal_output, } parameters.update( self._get_kernel_flavor_parameters( self.get_menu_entry_install_title(), kernel, initrd ) ) kernel_count = len(self.get_kernel_flavors()) custom_template_path = self._get_custom_template() if custom_template_path: log.info('--> Using custom boot template') with open(custom_template_path) as custom_template_file: template = Template(custom_template_file.read()) elif self.multiboot: log.info('--> Using multiboot install template') parameters['hypervisor'] = hypervisor template = self.grub2.get_multiboot_install_template( self.failsafe_boot, has_graphics, has_serial, self.continue_on_timeout, self._has_btrfs_relative_path(), kernel_count=kernel_count, ) else: log.info('--> Using standard boot install template') template = self.grub2.get_install_template( self.failsafe_boot, has_graphics, has_serial, self.continue_on_timeout, self._has_btrfs_relative_path(), kernel_count=kernel_count, ) try: self.config = template.substitute(parameters) except Exception as e: raise KiwiTemplateError(f'{type(e).__name__}: {format(e)}') if self.firmware.efi_mode() and self.early_boot_script_efi: self._copy_grub_config_to_efi_path( self.boot_dir, self.early_boot_script_efi, 'iso' ) def setup_live_image_config( self, mbrid: SystemIdentifier, hypervisor: str = 'xen.gz', kernel: str = 'linux', initrd: str = 'initrd', ) -> None: """ Create grub2 config file to boot a live media ISO image :param string mbrid: mbrid file name on boot device :param string hypervisor: hypervisor name :param string kernel: kernel name :param string initrd: initrd name """ log.info('Creating grub2 live ISO config file from template') self.iso_boot = True has_graphics = False has_serial = False if 'gfxterm' in self.terminal_output: has_graphics = True if 'serial' in self.terminal_output or 'serial' in self.terminal_input: has_serial = True assert self.cmdline is not None assert self.cmdline_failsafe is not None # the identifier is calculated by the image builder mbrid_id = mbrid.get_id() assert mbrid_id is not None parameters = { 'search_params': '--file --set=root /boot/' + mbrid_id, 'default_boot': '0', 'kernel_file': kernel, 'initrd_file': initrd, 'boot_options': ' '.join([self.cmdline] + self.live_boot_options), 'failsafe_boot_options': ' '.join( [self.cmdline_failsafe] + self.live_boot_options ), 'gfxmode': self.gfxmode, 'theme': self.theme, 'boot_timeout': self.timeout, 'boot_timeout_style': self.timeout_style or 'menu', 'serial_line_setup': self.serial_line_setup or '', 'bootpath': self.get_boot_path('iso'), 'boot_directory_name': self.boot_directory_name, 'efi_image_name': Defaults.get_efi_image_name(self.arch), 'terminal_input': self.terminal_input, 'terminal_output': self.terminal_output, } parameters.update( self._get_kernel_flavor_parameters( self.get_menu_entry_title(plain=True), kernel, initrd ) ) kernel_count = len(self.get_kernel_flavors()) custom_template_path = self._get_custom_template() if custom_template_path: log.info('--> Using custom boot template') with open(custom_template_path) as custom_template_file: template = Template(custom_template_file.read()) elif self.multiboot: log.info('--> Using multiboot template') parameters['hypervisor'] = hypervisor template = self.grub2.get_multiboot_iso_template( self.failsafe_boot, has_graphics, has_serial, self.mediacheck_boot, self._has_btrfs_relative_path(), kernel_count=kernel_count, ) else: log.info('--> Using standard boot template') template = self.grub2.get_iso_template( self.failsafe_boot, has_graphics, has_serial, self.mediacheck_boot, self._has_btrfs_relative_path(), kernel_count=kernel_count, ) try: self.config = template.substitute(parameters) except Exception as e: raise KiwiTemplateError(f'{type(e).__name__}: {format(e)}') if self.firmware.efi_mode() and self.early_boot_script_efi: self._copy_grub_config_to_efi_path( self.boot_dir, self.early_boot_script_efi, 'iso' ) def _get_kernel_flavor_parameters( self, title: str, kernel: str, initrd: str ) -> Dict[str, str]: """ Template parameters of the boot menu entry of the kernel of each kernel flavor, see get_kernel_flavors() The parameters of the first flavor are title, kernel_file and initrd_file, those of each further flavor have the index of the flavor appended, see BootLoaderTemplateGrub2. The file names of a flavor are provided by Defaults.get_iso_kernel_flavor_file_name() :param str title: menu entry title :param str kernel: kernel file name :param str initrd: initrd file name :return: template parameters :rtype: dict """ parameters = {} for index, flavor in enumerate(self.get_kernel_flavors()): suffix = f'_{index}' if index else '' parameters[f'title{suffix}'] = self.get_kernel_flavor_title( title, flavor ) parameters[f'kernel_file{suffix}'] = ( Defaults.get_iso_kernel_flavor_file_name(kernel, index, flavor) ) parameters[f'initrd_file{suffix}'] = ( Defaults.get_iso_kernel_flavor_file_name(initrd, index, flavor) ) return parameters def setup_install_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: """ Create/Provide grub2 boot images and metadata In order to boot from the ISO grub2 modules, images and theme data needs to be created and provided at the correct place on the iso filesystem :param string mbrid: mbrid file name on boot device :param string lookup_path: custom module lookup path """ log.info('Creating grub2 bootloader images') if self.firmware.efi_mode(): self.efi_boot_path = self.create_efi_path(in_sub_dir='') # the identifier is calculated by the image builder mbrid_id = mbrid.get_id() assert mbrid_id is not None log.info('--> Creating identifier file %s', mbrid_id) grub_boot_path = self._get_grub2_boot_path() Path.create(grub_boot_path) mbrid.write(self.boot_dir + '/boot/' + mbrid_id) mbrid.write(self.boot_dir + '/boot/mbrid') if self.theme: self._copy_theme_data_to_boot_directory(lookup_path, 'iso') if self._supports_platform_modules(): self._copy_bios_modules_to_boot_directory(lookup_path) self._setup_platform_image(mbrid=mbrid, lookup_path=lookup_path) if self.firmware.efi_mode(): self._setup_EFI_path(lookup_path) if self.firmware.efi_mode() == 'efi': self._setup_efi_image( mbrid=mbrid, lookup_path=lookup_path, target_type='iso' ) self._copy_efi_modules_to_boot_directory(lookup_path) elif self.firmware.efi_mode() == 'uefi': self._setup_secure_boot_efi_image( lookup_path=lookup_path, mbrid=mbrid, target_type='iso' ) log.info('--> Creating loopback config') loopback_file = os.path.normpath( os.sep.join([grub_boot_path, 'loopback.cfg']) ) self._create_loopback_config(loopback_file) def setup_live_boot_images( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: """ Create/Provide grub2 boot images and metadata Calls setup_install_boot_images because no different action required """ self.setup_install_boot_images(mbrid, lookup_path) def setup_disk_boot_images( self, boot_uuid: str, efi_uuid: Optional[str] = None, lookup_path: Optional[str] = None, ) -> None: """ Create/Provide grub2 boot images and metadata In order to boot from the disk grub2 modules, images and theme data needs to be created and provided at the correct place in the filesystem :param string boot_uuid: boot device UUID :param string lookup_path: custom module lookup path """ log.info('Creating grub2 bootloader images') if self.firmware.efi_mode(): self.efi_boot_path = self.create_efi_path() if self.theme: self._copy_theme_data_to_boot_directory(lookup_path, 'disk') if not self.xen_guest and self._supports_platform_modules(): self._copy_bios_modules_to_boot_directory(lookup_path) if self.firmware.efi_mode() == 'efi': self._setup_efi_image( uuid=boot_uuid, efi_uuid=efi_uuid, lookup_path=lookup_path ) self._copy_efi_modules_to_boot_directory(lookup_path) elif self.firmware.efi_mode() == 'uefi': if not self._get_shim_install(): self._setup_secure_boot_efi_image( lookup_path=lookup_path, uuid=boot_uuid, efi_uuid=efi_uuid ) if self.xen_guest: self._copy_xen_modules_to_boot_directory(lookup_path) def _copy_grub_config_to_efi_path( self, root_path: str, config_file: str, target_type: str = 'disk' ) -> None: efi_boot_paths = Defaults.get_shim_vendor_directories(root_path) if not efi_boot_paths: # not all distributors installs a vendor directory but # have them in their encoded early boot script. Thus # the following code tries to look up the vendor string # from the signed grub binary grub_images = Defaults.get_signed_grub_loader( self.root_dir, target_type ) for grub_image in grub_images: if grub_image and grub_image.filename: strings_abspath = Path.which('strings', access_mode=os.X_OK) if not strings_abspath: raise KiwiCommandNotFound( 'strings command not found on buildhost' ) bash_command = [ strings_abspath, grub_image.filename, '|', 'grep', r'EFI\/', ] vendor_paths = Command.run( ['bash', '-c', ' '.join(bash_command)], raise_on_error=False, raise_on_command_not_found=True, ) if vendor_paths and vendor_paths.output: for path in vendor_paths.output.strip().split( os.linesep ): efi_boot_paths.append( os.path.normpath( os.sep.join([root_path, path.strip()]) ) ) if not efi_boot_paths: efi_boot_paths = [ os.path.normpath(os.sep.join([root_path, 'EFI/BOOT'])) ] for efi_boot_path in efi_boot_paths: Path.create(efi_boot_path) grub_config_file_for_efi_boot = os.sep.join( [efi_boot_path, 'grub.cfg'] ) if config_file != grub_config_file_for_efi_boot: log.info( 'Copying {0} -> {1} to be found by EFI'.format( config_file, grub_config_file_for_efi_boot ) ) shutil.copy2(config_file, grub_config_file_for_efi_boot) def _supports_platform_modules(self) -> bool: if self.efi_csm and ( self.arch == 'ix86' or self.arch == 'x86_64' or Defaults.is_ppc64_arch(self.arch) ): return True return False def _setup_zipl2grub_conf(self) -> None: zipl2grub_config_file = ''.join( [self.root_dir, '/etc/default/zipl2grub.conf.in'] ) zipl2grub_config_file_orig = ''.join( [self.root_dir, '/etc/default/zipl2grub.conf.in.orig'] ) target_type = self.xml_state.get_build_type_bootloader_targettype() target_blocksize = self.xml_state.build_type.get_target_blocksize() if target_type and os.path.exists(zipl2grub_config_file): if os.path.exists(zipl2grub_config_file_orig): # reset the original template file first shutil.copy2(zipl2grub_config_file_orig, zipl2grub_config_file) else: # no copy of the original template, create it shutil.copy2(zipl2grub_config_file, zipl2grub_config_file_orig) with open(zipl2grub_config_file) as zipl_config_file: zipl_config = zipl_config_file.read() zipl_config = re.sub( r'(:menu)', ':menu\n' ' targettype = {0}\n' ' targetbase = {1}\n' ' targetblocksize = {2}\n' ' targetoffset = {3}\n' ' {4}'.format( target_type if target_type != 'GPT' else 'SCSI', self.custom_args['targetbase'], target_blocksize or 512, self._get_partition_start( self.custom_args['targetbase'] ), self._get_disk_geometry(self.custom_args['targetbase']), ), zipl_config, ) log.debug('Updated zipl template as follows') with open(zipl2grub_config_file, 'w') as zipl_config_file: log.debug(zipl_config) zipl_config_file.write(zipl_config) def _setup_default_grub(self) -> None: """ Create or update etc/default/grub by parameters required according to the root filesystem setup * GRUB_DEFAULT * GRUB_TIMEOUT * GRUB_TIMEOUT_STYLE * SUSE_BTRFS_SNAPSHOT_BOOTING * SUSE_REMOVE_LINUX_ROOT_PARAM * GRUB_BACKGROUND * GRUB_THEME * GRUB_SERIAL_COMMAND * GRUB_CMDLINE_LINUX * GRUB_CMDLINE_LINUX_DEFAULT * GRUB_GFXMODE * GRUB_TERMINAL_INPUT * GRUB_TERMINAL_OUTPUT * GRUB_DISTRIBUTOR * GRUB_DISABLE_LINUX_UUID * GRUB_DISABLE_LINUX_PARTUUID * GRUB_ENABLE_LINUX_LABEL """ assert self.cmdline is not None # elements to set only if not already in etc/default/grub grub_default_if_not_set_entries = {'GRUB_DEFAULT': 'saved'} # elements to set in any case grub_default_entries: Dict[str, Union[str, int]] = { 'GRUB_TIMEOUT': self.timeout, 'GRUB_GFXMODE': self.gfxmode, } if self.terminal_input: grub_default_entries['GRUB_TERMINAL_INPUT'] = '"{0}"'.format( self.terminal_input ) if self.terminal_output: grub_default_entries['GRUB_TERMINAL_OUTPUT'] = '"{0}"'.format( self.terminal_output ) grub_final_cmdline = re.sub( r'(^root=[^\s]+)|( root=[^\s]+)', '', self.cmdline ).strip() if self.persistency_type == 'by-label': label = re.search(r'(^root=[^\s]+)|( root=[^\s]+)', self.cmdline) if label: for match in label.groups(): if match and 'LABEL' in match: grub_default_entries['GRUB_CMDLINE_LINUX'] = ( '"{0}"'.format(match.strip()) ) grub_default_entries['SUSE_REMOVE_LINUX_ROOT_PARAM'] = ( 'true' ) grub_default_entries['GRUB_ENABLE_LINUX_LABEL'] = 'true' grub_default_entries['GRUB_DISABLE_LINUX_UUID'] = 'true' elif self.persistency_type == 'by-partuuid': grub_default_entries['GRUB_DISABLE_LINUX_UUID'] = 'true' grub_default_entries['GRUB_DISABLE_LINUX_PARTUUID'] = 'false' if self.displayname: grub_default_entries['GRUB_DISTRIBUTOR'] = '"{0}"'.format( self.displayname ) if self.timeout_style: grub_default_entries['GRUB_TIMEOUT_STYLE'] = self.timeout_style if grub_final_cmdline: grub_default_entries['GRUB_CMDLINE_LINUX_DEFAULT'] = '"{0}"'.format( grub_final_cmdline ) if self.serial_line_setup and ( 'serial' in self.terminal_input or 'serial' in self.terminal_output ): grub_default_entries['GRUB_SERIAL_COMMAND'] = '"{0}"'.format( self.serial_line_setup ) if self.theme: theme_setup = '{0}/{1}/theme.txt' grub_default_entries['GRUB_THEME'] = theme_setup.format( ''.join(['/boot/', self.boot_directory_name, '/themes']), self.theme, ) theme_background = '{0}/{1}/background.png'.format( ''.join(['/boot/', self.boot_directory_name, '/themes']), self.theme, ) if os.path.exists(os.sep.join([self.root_dir, theme_background])): grub_default_entries['GRUB_BACKGROUND'] = theme_background if self.xml_state.build_type.get_btrfs_root_is_snapper_snapshot(): grub_default_entries['SUSE_BTRFS_SNAPSHOT_BOOTING'] = 'true' if self.custom_args.get('crypto_disk'): grub_default_entries['GRUB_ENABLE_CRYPTODISK'] = 'y' enable_blscfg_implemented = Command.run( [ 'grep', '-q', 'GRUB_ENABLE_BLSCFG', self._get_grub2_mkconfig_tool(), ], raise_on_error=False, ) if self.bls and enable_blscfg_implemented.returncode == 0: grub_default_entries['GRUB_ENABLE_BLSCFG'] = 'true' if self.arch.startswith('s390') and self.host_key_certificates: # At the moment no generic enabler for secure execution # on s390 exists in grub upstream. Thus the following # variable setup is distribution specific: # Enable Secure Execution: SUSE specific variables host_key_list: List[str] = [] signing_key_list: List[str] = [] revocation_key_list: List[str] = [] grub_default_entries['SUSE_S390_SE_ENABLE'] = 'true' grub_default_entries['SUSE_S390_SE_CA_CERT'] = ( self.host_key_certificates[0]['hkd_ca_cert'] ) for host_key_certificate in self.host_key_certificates: signing_key_list.append(host_key_certificate['hkd_sign_cert']) host_key_list += host_key_certificate['hkd_cert'] if host_key_certificate.get('hkd_revocation_list'): revocation_key_list += host_key_certificate[ 'hkd_revocation_list' ] grub_default_entries['SUSE_S390_SE_HOST_KEY'] = ','.join( host_key_list ) grub_default_entries['SUSE_S390_SE_HOST_KEY_SIGNING_KEY'] = ( ','.join(signing_key_list) ) if revocation_key_list: grub_default_entries['SUSE_S390_SE_REVOCATION_LIST'] = ','.join( revocation_key_list ) if grub_default_entries: log.info('Writing grub2 defaults file') grub_default_location = ''.join([self.root_dir, '/etc/default/']) if os.path.exists(grub_default_location): grub_default_file = ''.join([grub_default_location, 'grub']) grub_default = SysConfig(grub_default_file) grub_default_entries_sorted = OrderedDict( sorted(grub_default_entries.items()) ) for key, value in list(grub_default_entries_sorted.items()): log.info('--> {0}:{1}'.format(key, value)) grub_default[key] = value grub_default_if_not_set_entries_sorted = OrderedDict( sorted(grub_default_if_not_set_entries.items()) ) for key, value in list( grub_default_if_not_set_entries_sorted.items() ): if not grub_default.get(key): log.info('--> {0}:{1}'.format(key, value)) grub_default[key] = value grub_default.write() def _setup_secure_boot_efi_image( self, lookup_path: Optional[str], uuid: Optional[str] = None, efi_uuid: Optional[str] = None, mbrid: Optional[SystemIdentifier] = None, target_type: str = 'disk', ) -> None: """ Provide the shim loader and the shim signed grub2 loader in the required boot path. Normally this task is done by the shim-install tool. However, shim-install does not exist on all distributions and the script does not operate well in e.g CD environments from which we generate live and/or install media. Thus shim-install is used if possible at install time of the bootloader because it requires access to the target block device. In any other case this setup code should act as the fallback solution """ log.warning('Running fallback setup for shim secure boot efi image') if not lookup_path: lookup_path = self.boot_dir grub_images = Defaults.get_signed_grub_loader(lookup_path, target_type) shim_images = Defaults.get_shim_loader(lookup_path) if not grub_images: raise KiwiBootLoaderGrubSecureBootError( 'Signed grub2 efi loader not found' ) assert self.efi_boot_path is not None # The shim concept is based on a two step system including a # grub image(shim) that got signed by Microsoft followed by # a grub image that got signed by the shim. The shim image # is the one that gets loaded by the firmware which itself # loads the second stage grub image for grub_image in grub_images: if grub_image.binaryname: if shim_images: target_grub_image_name = os.sep.join( [self.efi_boot_path, grub_image.binaryname] ) else: # Without shim a self signed grub image is used that # gets loaded by the firmware target_grub_image_name = os.sep.join( [self.efi_boot_path, grub_image.targetname] ) if not os.path.isfile(target_grub_image_name): log.info(f'--> Using grub image: {grub_image.filename}') Command.run( ['cp', grub_image.filename, target_grub_image_name] ) for shim_image in shim_images: if shim_image.filename: target_efi_image_name = os.sep.join( [self.efi_boot_path, shim_image.binaryname] ) if not os.path.isfile(target_efi_image_name): log.info(f'--> Using shim image: {shim_image.filename}') Command.run( ['cp', shim_image.filename, target_efi_image_name] ) # mok manager mok_managers = Defaults.get_mok_manager(lookup_path) if mok_managers and shim_images: for mok_manager in mok_managers: target_mok_manager = os.sep.join( [self.efi_boot_path, os.path.basename(mok_manager)] ) if not os.path.isfile(target_mok_manager): log.info(f'--> Using mok image: {mok_manager}') Command.run(['cp', mok_manager, self.efi_boot_path]) self._create_efi_config_search(uuid, efi_uuid, mbrid) def _setup_efi_image( self, uuid: Optional[str] = None, efi_uuid: Optional[str] = None, mbrid: Optional[SystemIdentifier] = None, lookup_path: Optional[str] = None, target_type: str = 'disk', ) -> None: """ Provide the unsigned grub2 efi image in the required boot path If a prebuilt efi image as provided by the distribution could be found, this image will be used. If no such image could be found we create a custom image with a pre defined set of grub modules """ if not lookup_path: lookup_path = self.boot_dir grub_images = Defaults.get_unsigned_grub_loader( lookup_path, target_type ) can_write = self.xml_state.build_type.get_overlayroot_write_partition() if grub_images and can_write is not False: assert self.efi_boot_path is not None for grub_image in grub_images: log.info(f'--> Using prebuilt unsigned efi image: {grub_image}') target_grub_image_name = os.sep.join( [self.efi_boot_path, grub_image.targetname] ) Command.run(['cp', grub_image.filename, target_grub_image_name]) self._create_efi_config_search(uuid, efi_uuid, mbrid) else: log.info('--> Creating unsigned efi image') self._create_efi_image(uuid, efi_uuid, mbrid, lookup_path) def _setup_chrp_config(self, mbrid: SystemIdentifier) -> None: early_boot_script = os.path.normpath( os.sep.join( [self._get_grub2_boot_path(), 'powerpc-ieee1275', 'grub.cfg'] ) ) self._create_early_boot_script_for_mbrid_search( early_boot_script, mbrid ) chrp_dir = os.path.normpath(os.sep.join([self.boot_dir, 'ppc'])) Path.create(chrp_dir) chrp_bootinfo_file = os.sep.join([chrp_dir, 'bootinfo.txt']) chrp_config = ( dedent( ''' {os_name} {os_name} boot &device;:1,\\boot\\grub2\\powerpc-ieee1275\\{chrp_loader} ''' ).strip() + os.linesep ) with open(chrp_bootinfo_file, 'w') as chrp_bootinfo: chrp_bootinfo.write( chrp_config.format( os_name=self.get_menu_entry_install_title(), chrp_loader=Defaults.get_grub_chrp_loader(self.boot_dir), ) ) def _setup_platform_image( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: """ Provide bios grub image """ if not lookup_path: lookup_path = self.boot_dir grub_image = Defaults.get_grub_platform_core_loader(lookup_path) if grub_image: log.info(f'--> Using prebuilt bios image: {grub_image}') else: log.info('--> Creating bios image') self._create_platform_image(mbrid, lookup_path) if Defaults.is_ppc64_arch(Defaults.get_platform_name()): self._setup_chrp_config(mbrid) return bash_command = ' '.join( [ 'cat', self._get_grub_platform_modules_path(lookup_path) + '/cdboot.img', grub_image or self._get_grub_platform_image_name(lookup_path), '>', os.sep.join( [ self._get_grub_platform_modules_path(lookup_path), Defaults.get_iso_grub_loader(), ] ), ] ) Command.run(['bash', '-c', bash_command]) def _create_embedded_fat_efi_image(self, path: str) -> None: """ Creates a EFI system partition image at the given path. Must be called after setup_install_boot_images and write. """ fat_image_mbsize = int( self.xml_state.build_type.get_efifatimagesize() or defaults.EFI_FAT_IMAGE_SIZE ) efi_folder = Temporary(prefix='efi_folder').new_dir() efi_data = DataSync(self.boot_dir + '/EFI/', efi_folder.name) efi_data.sync_data( options=Defaults.get_sync_options(), exclude=Defaults.get_exclude_list_from_custom_exclude_files_for_efifatimage( self.root_dir ), ) SparseFile.create(path, f'{fat_image_mbsize}M') Command.run(['mkdosfs', '-n', 'BOOT', path]) Command.run( ['mcopy', '-Do', '-s', '-i', path, efi_folder.name, '::EFI'] ) def _create_efi_image( self, uuid: Optional[str] = None, efi_uuid: Optional[str] = None, mbrid: Optional[SystemIdentifier] = None, lookup_path: Optional[str] = None, ) -> None: assert self.efi_boot_path is not None dracut_setup = self.xml_state.get_dracut_config('setup') early_boot_script = os.path.normpath( os.sep.join([self.efi_boot_path, 'earlyboot.cfg']) ) self.early_boot_script_efi = early_boot_script if dracut_setup.uefi and efi_uuid: self._create_early_boot_script_for_uki_chainload( early_boot_script, efi_uuid ) elif uuid: self._create_early_boot_script_for_uuid_search( early_boot_script, uuid ) else: self._create_early_boot_script_for_mbrid_search( early_boot_script, mbrid ) # EFI images are only created on architectures with EFI support efi_image_name = Defaults.get_efi_image_name(self.arch) efi_module_directory_name = Defaults.get_efi_module_directory_name( self.arch ) assert efi_image_name and efi_module_directory_name root_efi_path = '/boot/efi/EFI/BOOT/' root_efi_image = root_efi_path + efi_image_name Path.create(self.root_dir + root_efi_path) module_list = Defaults.get_grub_efi_modules(multiboot=self.xen_guest) module_path = self._get_efi_modules_path(self.root_dir) if os.path.exists(module_path + '/linuxefi.mod'): module_list.append('linuxefi') early_boot_script_on_media = ( self.root_dir + root_efi_path + 'earlyboot.cfg' ) if early_boot_script != early_boot_script_on_media: log.debug( f'Copy earlyboot to media path: {early_boot_script_on_media}' ) shutil.copy2(early_boot_script, early_boot_script_on_media) mkimage_call = Command.run( [ 'chroot', self.root_dir, os.path.basename(self._get_grub2_mkimage_tool()) or 'grub2-mkimage', '-O', efi_module_directory_name, '-o', root_efi_image, '-c', root_efi_path + 'earlyboot.cfg', '-p', os.path.join(self.get_boot_path(), self.boot_directory_name), '-d', module_path.replace(self.root_dir, ''), ] + module_list ) log.debug(mkimage_call.output) # Copy generated EFI image to the media directory if this # is different from the system root directory, e.g the case # for live image builds or when using a custom kiwi initrd efi_image_root_file = self.root_dir + root_efi_image efi_image_media_file = os.sep.join( [self.efi_boot_path, os.path.basename(efi_image_root_file)] ) if efi_image_root_file != efi_image_media_file: log.debug(f'Copy grub image to media path: {efi_image_media_file}') Path.create(os.path.dirname(efi_image_media_file)) shutil.copy2(efi_image_root_file, efi_image_media_file) def _create_efi_config_search( self, uuid: Optional[str] = None, efi_uuid: Optional[str] = None, mbrid: Optional[SystemIdentifier] = None, ) -> None: assert self.efi_boot_path is not None dracut_setup = self.xml_state.get_dracut_config('setup') efi_boot_config = self.efi_boot_path + '/grub.cfg' self.early_boot_script_efi = efi_boot_config if dracut_setup.uefi and efi_uuid: self._create_early_boot_script_for_uki_chainload( efi_boot_config, efi_uuid ) elif uuid: self._create_early_boot_script_for_uuid_search( efi_boot_config, uuid ) else: self._create_early_boot_script_for_mbrid_search( efi_boot_config, mbrid ) def _create_platform_image( self, mbrid: SystemIdentifier, lookup_path: Optional[str] = None ) -> None: early_boot_script = os.path.normpath( os.sep.join([self._get_grub2_boot_path(), 'earlyboot.cfg']) ) self._create_early_boot_script_for_mbrid_search( early_boot_script, mbrid ) early_boot_script_on_media = os.sep.join( [self.root_dir, 'boot', self.boot_directory_name, 'earlyboot.cfg'] ) if early_boot_script != early_boot_script_on_media: log.debug( f'Copy earlyboot to media path: {early_boot_script_on_media}' ) Path.create(os.path.dirname(early_boot_script_on_media)) shutil.copy2(early_boot_script, early_boot_script_on_media) mkimage_call = Command.run( [ 'chroot', self.root_dir, os.path.basename(self._get_grub2_mkimage_tool()) or 'grub2-mkimage', '-O', Defaults.get_grub_platform_module_directory_name(), '-o', self._get_grub_platform_image_name(self.root_dir).replace( self.root_dir, '' ), '-c', early_boot_script.replace(self.boot_dir, ''), '-p', os.sep.join([self.get_boot_path(), self.boot_directory_name]), '-d', self._get_grub_platform_modules_path(self.root_dir).replace( self.root_dir, '' ), ] + Defaults.get_grub_platform_modules(multiboot=self.xen_guest) ) log.debug(mkimage_call.output) # Copy generated EFI image to the media directory if this # is different from the system root directory, e.g the case # for live image builds or when using a custom kiwi initrd bios_image_root_file = self._get_grub_platform_image_name(self.root_dir) # without a lookup path the boot directory is used, the # same as for the lookup of the grub modules bios_image_media_file = bios_image_root_file.replace( self.root_dir, lookup_path or self.boot_dir ) if bios_image_root_file != bios_image_media_file: log.debug(f'Copy grub image to media path: {bios_image_media_file}') Path.create(os.path.dirname(bios_image_media_file)) shutil.copy2(bios_image_root_file, bios_image_media_file) def _create_early_boot_script_for_uki_chainload( self, filename: str, uuid: str ) -> None: with open(filename, 'w') as early_boot: early_boot.write( f'search --no-floppy --set=root --fs-uuid {uuid}{os.linesep}' ) early_boot.write( f'chainloader ($root)/EFI/Linux/kiwi.efi{os.linesep}' ) early_boot.write(f'boot{os.linesep}') def _create_early_boot_script_for_uuid_search( self, filename: str, uuid: str ) -> None: with open(filename, 'w') as early_boot: if self._has_btrfs_relative_path(): early_boot.write( 'set btrfs_relative_path="yes"{0}'.format(os.linesep) ) if self.custom_args.get('boot_is_crypto'): early_boot.write('insmod cryptodisk{0}'.format(os.linesep)) early_boot.write('insmod luks{0}'.format(os.linesep)) early_boot.write( 'cryptomount -u {0}{1}'.format( uuid.replace('-', ''), os.linesep ) ) early_boot.write( 'set root="cryptouuid/{0}"{1}'.format( uuid.replace('-', ''), os.linesep ) ) early_boot.write( 'search --fs-uuid --set=root {0}{1}'.format(uuid, os.linesep) ) early_boot.write( 'set prefix=($root){0}{1}'.format( os.path.join( self.get_boot_path(), self.boot_directory_name ), os.linesep, ) ) early_boot.write( '{0} ($root){1}/grub.cfg{2}'.format( self.grub_load, os.path.join( self.get_boot_path(), self.boot_directory_name ), os.linesep, ) ) def _create_early_boot_script_for_mbrid_search( self, filename: str, mbrid: Optional[SystemIdentifier] ) -> None: with open(filename, 'w') as early_boot: if self._has_btrfs_relative_path(): early_boot.write( 'set btrfs_relative_path="yes"{0}'.format(os.linesep) ) if mbrid: early_boot.write( f'search --file --set=root /boot/{mbrid.get_id()}{os.linesep}' ) else: # Fallback search for /boot/mbrid early_boot.write( f'search --file --set=root /boot/mbrid{os.linesep}' ) early_boot.write( 'set prefix=($root)/boot/{0}{1}'.format( self.boot_directory_name, os.linesep ) ) early_boot.write( '{0} ($root)/boot/{1}/grub.cfg{2}'.format( self.grub_load, self.boot_directory_name, os.linesep ) ) def _get_grub2_mkimage_tool(self) -> str: grub_mkimage_file_path = '' for grub_mkimage_tool in ['grub2-mkimage', 'grub-mkimage']: grub_mkimage_file_path = ( Path.which(grub_mkimage_tool, root_dir=self.root_dir) or '' ) if grub_mkimage_file_path: break return grub_mkimage_file_path def _get_grub2_mkconfig_tool(self) -> str: grub_mkconfig_file_path = '' for grub_mkconfig_tool in ['grub2-mkconfig', 'grub-mkconfig']: grub_mkconfig_file_path = ( Path.which(grub_mkconfig_tool, root_dir=self.root_dir) or '' ) if grub_mkconfig_file_path: break return grub_mkconfig_file_path def _get_grub2_boot_path(self) -> str: return self.boot_dir + '/boot/' + self.boot_directory_name def _get_grub_platform_image_name(self, lookup_path: Optional[str]) -> str: return os.sep.join( [ self._get_grub_platform_modules_path(lookup_path), Defaults.get_grub_platform_image_name(), ] ) def _get_efi_modules_path( self, lookup_path: Optional[str] = '', arch: str = '', raise_on_error: bool = True, ) -> str: # EFI modules are only used on architectures with EFI support efi_module_directory_name = Defaults.get_efi_module_directory_name( arch or self.arch ) assert efi_module_directory_name return self._get_module_path( efi_module_directory_name, lookup_path, raise_on_error ) def _get_grub_platform_modules_path( self, lookup_path: Optional[str] = None ) -> str: return self._get_module_path( Defaults.get_grub_platform_module_directory_name(), lookup_path ) def _get_xen_modules_path(self, lookup_path: Optional[str] = None) -> str: xen_module_directory_name = Defaults.get_efi_module_directory_name( 'x86_64_xen' ) assert xen_module_directory_name return self._get_module_path( xen_module_directory_name, lookup_path, ) def _get_module_path( self, format_name: str, lookup_path: Optional[str] = '', raise_on_error: bool = True, ) -> str: if not lookup_path: lookup_path = self.boot_dir return Defaults.get_grub_path(lookup_path, format_name, raise_on_error) def _find_theme_background_file(self, lookup_path: str) -> Optional[str]: background_pattern = os.sep.join( [ lookup_path, 'boot', self.boot_directory_name, 'themes', '*', 'background.png', ] ) for background_file in glob.iglob(background_pattern): return background_file return None def _copy_theme_data_to_boot_directory( self, lookup_path: Optional[str], target: str ) -> None: if not lookup_path: lookup_path = self.boot_dir font_name = 'unicode.pf2' efi_font_dir = Defaults.get_grub_efi_font_directory(lookup_path) boot_fonts_dir = os.path.normpath( os.sep.join( [ self.boot_dir, self.get_boot_path(target), self.boot_directory_name, 'fonts', ] ) ) try: unicode_font = Defaults.get_grub_path(lookup_path, font_name) if not os.path.exists(os.sep.join([boot_fonts_dir, font_name])): Path.create(boot_fonts_dir) Command.run(['cp', unicode_font, boot_fonts_dir]) if efi_font_dir: Command.run(['cp', unicode_font, efi_font_dir]) except Exception as issue: raise KiwiBootLoaderGrubFontError( 'Setting up unicode font failed with {0}'.format(issue) ) boot_theme_dir = os.sep.join( [self.boot_dir, 'boot', self.boot_directory_name, 'themes'] ) Path.create(boot_theme_dir) assert self.theme is not None theme_dir = Defaults.get_grub_path( lookup_path, 'themes/' + self.theme, raise_on_error=False ) boot_theme_background_file = self._find_theme_background_file( lookup_path ) if theme_dir and os.path.exists(theme_dir): if boot_theme_background_file: # A background file was found. Preserve a copy of the # file which was created at install time of the theme # package by the activate-theme script boot_theme_background_backup_file = os.sep.join( [self.boot_dir, 'background.png'] ) Command.run( [ 'cp', boot_theme_background_file, boot_theme_background_backup_file, ] ) # sync theme data from install path to boot path data = DataSync(theme_dir, boot_theme_dir) data.sync_data(options=['-a']) if boot_theme_background_file: # Install preserved background file to the theme Command.run( [ 'mv', boot_theme_background_backup_file, os.sep.join([boot_theme_dir, self.theme]), ] ) elif boot_theme_background_file: # assume all theme data is in the directory of the # background file and just sync that directory to the # boot path data = DataSync( os.path.dirname(boot_theme_background_file), boot_theme_dir ) data.sync_data(options=['-a']) self._check_boot_theme_exists() def _check_boot_theme_exists(self) -> None: if self.theme: theme_dir = os.sep.join( [ self.boot_dir, 'boot', self.boot_directory_name, 'themes', self.theme, ] ) if not os.path.exists(theme_dir): log.warning('Theme %s not found', theme_dir) log.warning('Set bootloader terminal to console mode') self.terminal_input = 'console' self.terminal_output = 'console' def _setup_EFI_path(self, lookup_path: Optional[str]) -> None: """ Copy efi boot data from lookup_path to the root directory """ if not lookup_path: lookup_path = self.boot_dir efi_path = lookup_path + '/boot/efi/' if os.path.exists(efi_path): efi_data = DataSync(efi_path, self.boot_dir) efi_data.sync_data(options=['-a']) def _copy_efi_modules_to_boot_directory( self, lookup_path: Optional[str] ) -> None: self._copy_modules_to_boot_directory_from( self._get_efi_modules_path(lookup_path) ) if self.arch == 'x86_64': # Also copy the 32bit EFI modules if present efi32_module_path = self._get_efi_modules_path( lookup_path, 'i386', raise_on_error=False ) if os.path.exists(efi32_module_path): self._copy_modules_to_boot_directory_from(efi32_module_path) def _copy_bios_modules_to_boot_directory( self, lookup_path: Optional[str] ) -> None: self._copy_modules_to_boot_directory_from( self._get_grub_platform_modules_path(lookup_path) ) def _copy_xen_modules_to_boot_directory( self, lookup_path: Optional[str] ) -> None: self._copy_modules_to_boot_directory_from( self._get_xen_modules_path(lookup_path) ) def _copy_modules_to_boot_directory_from(self, module_path: str) -> None: boot_module_path = ( self._get_grub2_boot_path() + '/' + os.path.basename(module_path) ) try: Path.create(boot_module_path) data = DataSync(module_path + '/', boot_module_path) data.sync_data(options=['-a'], exclude=['*.module']) except Exception as e: raise KiwiBootLoaderGrubModulesError( f'Module synchronisation failed with: {format(e)}' ) def _get_shim_install(self) -> Optional[str]: return Path.which(filename='shim-install', root_dir=self.boot_dir) def _fix_grub_root_device_reference( self, config_file: str, boot_options: Dict ) -> None: if self.root_reference: # grub2-mkconfig has no idea how the correct root= setup is # for disk images created with overlayroot enabled or in a # buildservice worker environment. Because of that the mkconfig # tool just finds the raw partition loop device and includes it # which is wrong. In this particular case we have to patch the # written config file and replace the wrong root= reference with # the correct value. with open(config_file) as grub_config_file: grub_config = grub_config_file.read() # The following expression matches any of the following # grub mkconfig root= settings and replaces it with a # correct value # 1. root=LOCAL-KIWI-MAPPED-DEVICE # 2. root=[a-zA-Z]=ANY-LINUX-BY-ID-VALUE grub_config = re.sub( r'(root=[a-zA-Z]+=[a-zA-Z0-9:\.-]+)|(root={0})'.format( boot_options.get('root_device') ), '{0}'.format(self.root_reference), grub_config, ) try: with open(config_file, 'w') as grub_config_file: grub_config_file.write(grub_config) except OSError as issue: log.warning(f'{func_name()} skipped: {issue}') return if self.firmware.efi_mode(): # in EFI mode the ESP is mounted by _mount_system() assert self.efi_mount is not None vendor_grubenv_file = Defaults.get_vendor_grubenv( self.efi_mount.mountpoint ) if vendor_grubenv_file: self._setup_loader_variables([self.root_reference]) def _fix_grub_loader_entries_boot_cmdline(self) -> None: if self.cmdline: # For distributions that follows the bootloader spec here: # https://www.freedesktop.org/wiki/Specifications/BootLoaderSpec # the menu entries are managed in extra files written to # boot/loader/entries. The grub2-mkconfig tool imports the # information from there and has no own menu entries data # in grub.cfg anymore. Unfortunately the system that writes # those new boot/loader/entries has several issues. It produces # a complete mess when used in chroot environments. In such # an environment it takes the proc/cmdline data from the host # that built the image and completely ignores the setup of # the image description. Overall for image building the # tooling is completely broken and we are forced to patch the # entire cmdline for the menuentries on such systems. assert self.root_mount is not None loader_entries_pattern = os.sep.join( [ self.root_mount.mountpoint, 'boot', 'loader', 'entries', '*.conf', ] ) for menu_entry_file in glob.iglob(loader_entries_pattern): with open(menu_entry_file) as grub_menu_entry_file: menu_entry = grub_menu_entry_file.read() menu_entry = re.sub( r'options (.*)', r'options {0}'.format(self.cmdline), menu_entry, ) try: with open(menu_entry_file, 'w') as grub_menu_entry_file: grub_menu_entry_file.write(menu_entry) except OSError as issue: log.warning(f'{func_name()} skipped: {issue}') return def _fix_grub_loader_entries_linux_and_initrd_paths(self) -> None: # For the same reasons encoded in _fix_grub_loader_entries_boot_cmdline # this method exists. In this method the wrong paths to the linux # kernel and initrd gets fixed assert self.root_mount is not None loader_entries_pattern = os.sep.join( [self.root_mount.mountpoint, 'boot', 'loader', 'entries', '*.conf'] ) for menu_entry_file in glob.iglob(loader_entries_pattern): with open(menu_entry_file) as grub_menu_entry_file: menu_entry_lines = grub_menu_entry_file.read().split(os.linesep) for line_number, menu_entry_line in enumerate(menu_entry_lines): if bool(re.match(r'^(linux|initrd) .*', menu_entry_line)): log.debug(f'Existing loader entry: {menu_entry_line}') config_path = menu_entry_line.split(' ', 1)[0] basename = os.path.basename(menu_entry_line) if self.bootpartition: config_path = f'{config_path} /{basename}' else: config_path = f'{config_path} /boot/{basename}' menu_entry_lines[line_number] = config_path log.debug(f'Updated loader entry: {config_path}') menu_entry = os.linesep.join(menu_entry_lines) try: with open(menu_entry_file, 'w') as grub_menu_entry_file: grub_menu_entry_file.write(menu_entry) except OSError as issue: log.warning(f'{func_name()} skipped: {issue}') def _get_partition_start(self, disk_device: str) -> str: if self.target_table_type == 'dasd': blocks = self._get_dasd_disk_geometry_element( disk_device, 'blocks per track' ) bash_command: Union[List[str], str] = [ 'fdasd', '-f', '-s', '-p', disk_device, '|', 'grep', '"^ "', '|', 'head', '-n', '1', '|', 'tr', '-s', '" "', ] fdasd_call = Command.run(['bash', '-c', ' '.join(bash_command)]) fdasd_output = fdasd_call.output try: start_track = int(fdasd_output.split(' ')[2].lstrip()) except Exception: raise KiwiDiskGeometryError( f'unknown partition format: {fdasd_output}' ) return '{0}'.format(start_track * blocks) else: bash_command = ' '.join( [ 'sfdisk', '--dump', disk_device, '|', 'grep', '"1 :"', '|', 'cut', '-f1', '-d,', '|', 'cut', '-f2', '-d=', ] ) return Command.run(['bash', '-c', bash_command]).output.strip() def _get_disk_geometry(self, disk_device: str) -> str: disk_geometry = '' if self.target_table_type == 'dasd': disk_geometry = 'targetgeometry = {0},{1},{2}'.format( self._get_dasd_disk_geometry_element(disk_device, 'cylinders'), self._get_dasd_disk_geometry_element( disk_device, 'tracks per cylinder' ), self._get_dasd_disk_geometry_element( disk_device, 'blocks per track' ), ) return disk_geometry def _get_dasd_disk_geometry_element( self, disk_device: str, search: str ) -> int: fdasd = ['fdasd', '-f', '-p', disk_device] bash_command = fdasd + ['|', 'grep', '"' + search + '"'] fdasd_call = Command.run(['bash', '-c', ' '.join(bash_command)]) fdasd_output = fdasd_call.output try: return int(fdasd_output.split(':')[1].lstrip()) except Exception: raise KiwiDiskGeometryError( f'unknown format for disk geometry: {fdasd_output}' ) def _create_loopback_config(self, filename: str) -> None: with open(filename, 'w') as loopback_cfg: loopback_cfg.write( 'source /boot/{0}/grub.cfg{1}'.format( self.boot_directory_name, os.linesep ) ) def _get_custom_template(self) -> str: if not self.xml_state.build_type.bootloader: return '' template_file = self.xml_state.build_type.bootloader[ 0 ].get_grub_template() if not template_file: return '' template_path = os.path.join( os.path.abspath(self.xml_state.xml_data.description_dir), self.xml_state.build_type.bootloader[0].get_grub_template(), ) if not os.path.exists(template_path): raise KiwiFileNotFound( f'failed to locate custom GRUB template {template_file}' ) return template_path def _has_btrfs_relative_path(self) -> bool: if ( self.xml_state.build_type.get_btrfs_set_default_volume() or self.xml_state.build_type.get_btrfs_root_is_snapper_snapshot() ): return True return False def _setup_kernel_flavor_default_entry(self, config_file: str) -> None: """ Select the menu entry of the latest kernel of the first kernel flavor as the default menu entry, see get_kernel_flavors() grub2-mkconfig creates the menu entries for all installed kernels in its own order. The default entry is selected by the saved_entry variable of the grub environment, which is taken into account with GRUB_DEFAULT=saved, the default kiwi sets up, see _setup_default_grub() :param str config_file: path of the grub config file :raises KiwiBootLoaderGrubMenuEntryError: if no menu entry of the kernel was found """ kernel_flavors = ( self.xml_state.get_build_type_bootloader_kernel_flavors() ) if not kernel_flavors: return assert self.root_mount is not None kernel_info = Kernel(self.root_mount.mountpoint).get_kernel( raise_on_not_found=True, flavor=kernel_flavors[0] ) kernel_version = kernel_info.version if kernel_info else '' menu_entry_id = self._get_menu_entry_id( config_file, kernel_version ) or self._get_loader_entry_id(kernel_version) if not menu_entry_id: raise KiwiBootLoaderGrubMenuEntryError( f'No menu entry for kernel {kernel_version} of flavor ' f'{kernel_flavors[0]} found in {config_file}' ) grub_default_file = f'{self.root_mount.mountpoint}/etc/default/grub' if os.path.exists(grub_default_file): grub_default = SysConfig(grub_default_file).get('GRUB_DEFAULT') if grub_default and grub_default.strip('"\'') != 'saved': log.warning( f'GRUB_DEFAULT={grub_default} is set, the default ' f'entry of kernel flavor {kernel_flavors[0]} is only ' 'selected with GRUB_DEFAULT=saved' ) self._setup_loader_variables([f'saved_entry={menu_entry_id}']) @staticmethod def _get_menu_entry_id(config_file: str, kernel_version: str) -> str: """ Provide the id of the menu entry of the given kernel version in the given grub config file The id of an entry in a submenu is prefixed by the id of the submenu, separated by >. An entry in a submenu, which belongs to the kernel version only, is preferred over a top level entry, which grub2-mkconfig creates for the latest kernel no matter its version. Recovery entries are not taken into account :param str config_file: path of the grub config file :param str kernel_version: kernel version :return: menu entry id or empty string if not found :rtype: str """ menu_ids: List[str] = [] top_level_entry_id = '' with open(config_file) as config: for line in config: line = line.strip() if line == '}': if menu_ids: menu_ids.pop() elif line.endswith('{'): menu_id = re.search( r'^(?:menuentry|submenu)\s.*' r'\$menuentry_id_option\s+\'([^\']+)\'', line, ) menu_ids.append(menu_id.group(1) if menu_id else '') elif re.match(r'(linux|linuxefi|linux16)\s', line): entry_ids = [menu_id for menu_id in menu_ids if menu_id] kernel_name = os.path.basename(line.split()[1]) if ( entry_ids and 'recovery' not in entry_ids[-1] and re.fullmatch( rf'[^-]+-{re.escape(kernel_version)}', kernel_name ) ): if len(entry_ids) > 1: return '>'.join(entry_ids) top_level_entry_id = top_level_entry_id or entry_ids[0] return top_level_entry_id def _get_loader_entry_id(self, kernel_version: str) -> str: """ Provide the id of the boot loader specification entry of the given kernel version, which is the entry file name without its extension. Such entries are used by grub if the menu entries are not part of the grub config file :param str kernel_version: kernel version :return: loader entry id or empty string if not found :rtype: str """ assert self.root_mount is not None kernel = Kernel(self.root_mount.mountpoint) for loader_entry in sorted( glob.iglob( f'{self.root_mount.mountpoint}/boot/loader/entries/*.conf' ) ): loader_entry_name = os.path.basename(loader_entry) if kernel.get_version_in_name(loader_entry_name) == kernel_version: return loader_entry_name[: -len('.conf')] return '' def _setup_loader_variables(self, variable_list: List[str]) -> None: """ Run grub2-editenv for writing an updated environment blob """ if self.root_mount and variable_list: log.info(f'Set grub environment variables: {variable_list}') Command.run( [ 'chroot', self.root_mount.mountpoint, 'grub2-editenv', '-', 'set', ] + variable_list ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/systemd_boot.py0000644000000000000000000002614115273675667017156 0ustar00# Copyright (c) 2022 Marcus Schäfer # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from string import Template from contextlib import ExitStack from typing import Dict, Tuple, Union # project from kiwi.path import Path from kiwi.bootloader.template.systemd_boot import BootLoaderTemplateSystemdBoot from kiwi.bootloader.config.bootloader_spec_base import BootLoaderSpecBase from kiwi.boot.image.base import BootImageBase from kiwi.mount_manager import MountManager from kiwi.storage.loop_device import LoopDevice from kiwi.storage.disk import Disk from kiwi.command import Command from kiwi.utils.os_release import OsRelease from kiwi.utils.sparse import SparseFile from kiwi.system.kernel import Kernel from kiwi.exceptions import KiwiTemplateError, KiwiBootLoaderTargetError import kiwi.defaults as defaults log = logging.getLogger('kiwi') class BootLoaderSystemdBoot(BootLoaderSpecBase): def post_init(self, custom_args: Dict = {}) -> None: """ Post init for systemd-boot In addition to the bootloader spec setup, initialize the record of the kernel and initrd files on the ESP :param dict custom_args: custom bootloader arguments """ super().post_init(custom_args) # kernel and initrd file on the ESP per kernel flavor self.boot_files: Dict[str, Tuple[str, str]] = {} def create_loader_image(self, target: str) -> None: """ Handle EFI images For systemd boot EFI images are provided along with systemd. Thus the only action taken care of is the creation of the ESP path :param str target: target identifier, one of disk, live(iso) or install(iso) """ self.efi_boot_path = self.create_efi_path() def setup_loader(self, target: str) -> None: """ Setup ESP for systemd-boot using bootctl For disk images only, setup the ESP layout for systemd boot using bootctl. All data will be loaded from the ESP. :param str target: target identifier, one of disk, live(iso) or install(iso) Currently only the disk identifier is supported """ if target != self.target.disk: raise KiwiBootLoaderTargetError( 'systemd-boot is only supported with the EFI disk image target' ) boot_options = self.custom_args['boot_options'] self._mount_system( boot_options.get('device_map'), boot_options.get('root_device'), boot_options.get('boot_device'), boot_options.get('efi_device'), boot_options.get('system_volumes'), boot_options.get('system_root_volume'), boot_options.get('system_partitions'), ) assert self.root_mount is not None dracut_setup = self.xml_state.get_dracut_config('setup') self._run_bootctl(self.root_mount.mountpoint) if dracut_setup.uefi: boot_image = boot_options.get('boot_image') if boot_image: log.info('Creating UKI EFI binary') Command.run( [ 'mv', boot_image.create_uki(self.cmdline), f'{self.root_mount.mountpoint}/boot/efi/EFI/Linux/', ] ) else: log.info('Creating loader entry') self.set_loader_entry(self.root_mount.mountpoint, self.target.disk) def set_loader_entry(self, root_dir: str, target: str) -> None: """ Setup/update loader entries, one entry for the kernel of each kernel flavor, see get_kernel_flavors() :param str target: target identifier, one of disk, live(iso) or install(iso) """ for flavor in self.get_kernel_flavors(): entry_name = self._get_entry_name(root_dir, flavor) BootLoaderSystemdBoot._write_config_file( BootLoaderTemplateSystemdBoot().get_entry_template(), root_dir + f'/boot/efi/loader/entries/{entry_name}', self._get_template_parameters(entry_name, flavor), ) def _create_embedded_fat_efi_image(self, path: str) -> None: """ Creates a EFI system partition image at the given path. Installs systemd-boot required EFI layout into the image """ fat_image_mbsize = int( self.xml_state.build_type.get_efifatimagesize() or defaults.EFI_FAT_IMAGE_SIZE ) SparseFile.create(path, f'{fat_image_mbsize}M') Command.run(['sgdisk', '-n', ':1.0', '-t', '1:EF00', path]) with LoopDevice(path) as loop_provider: loop_provider.create(overwrite=False) with Disk('gpt', loop_provider) as disk: disk.map_partitions() disk.partitioner.partition_id = 1 disk._add_to_map('efi') Command.run( ['mkdosfs', '-n', 'BOOT', disk.partition_map['efi']] ) Path.create(f'{self.root_dir}/boot/efi') with ExitStack() as stack: efi_mount = MountManager( device=disk.partition_map['efi'], mountpoint=f'{self.root_dir}/boot/efi', ) stack.push(efi_mount) device_mount = MountManager( device='/dev', mountpoint=f'{self.root_dir}/dev' ) stack.push(device_mount) proc_mount = MountManager( device='/proc', mountpoint=f'{self.root_dir}/proc' ) stack.push(proc_mount) sys_mount = MountManager( device='/sys', mountpoint=f'{self.root_dir}/sys' ) stack.push(sys_mount) efi_mount.mount() device_mount.bind_mount() proc_mount.bind_mount() sys_mount.bind_mount() self._run_bootctl(self.root_dir) self.set_loader_entry(self.root_dir, self.target.live) Command.run( ['dd', f'if={disk.partition_map["efi"]}', f'of={path}.img'] ) Command.run(['mv', f'{path}.img', path]) def _run_bootctl(self, root_dir: str) -> None: """ Setup ESP for systemd-boot using bootctl """ Command.run( [ 'chroot', root_dir, 'bootctl', 'install', '--esp-path=/boot/efi', '--no-variables', '--entry-token', 'os-id', ] ) self._write_kernel_cmdline_file(root_dir) # copy kernel and initrd of each kernel flavor dracut_setup = self.xml_state.get_dracut_config('setup') if not dracut_setup.uefi: entry_dir = f'{root_dir}/boot/efi/loader/entries' os_dir = f'{root_dir}/boot/efi/os' Path.create(entry_dir) Path.create(os_dir) for flavor in self.get_kernel_flavors(): kernel_info = BootImageBase( self.xml_state, root_dir, root_dir ).get_boot_names(flavor) self.boot_files[flavor] = ( f'os/{os.path.basename(kernel_info.kernel_filename)}', f'os/{kernel_info.initrd_name}', ) Command.run(['cp', kernel_info.kernel_filename, os_dir]) Command.run( ['cp', f'{root_dir}/boot/{kernel_info.initrd_name}', os_dir] ) # create loader.conf, the entry of the first kernel flavor # is the default entry kernel_flavors = ( self.xml_state.get_build_type_bootloader_kernel_flavors() ) default_entry = 'main.conf' if kernel_flavors: default_entry = self._get_entry_name(root_dir, kernel_flavors[0]) BootLoaderSystemdBoot._write_config_file( BootLoaderTemplateSystemdBoot().get_loader_template( with_default_entry=bool(kernel_flavors) ), root_dir + '/boot/efi/loader/loader.conf', self._get_template_parameters(default_entry), ) def _get_entry_name(self, root_dir: str, flavor: str = '') -> str: """ Name of the loader entry of the kernel of the given flavor The entry belongs to the kernel of the latest version of the flavor, the one kiwi creates the initrd for :param str root_dir: path to root directory tree :param str flavor: kernel flavor, see Kernel.get_kernel() :return: entry file basename :rtype: str """ os_release = OsRelease(root_dir) kernel_info = Kernel(root_dir).get_kernel( raise_on_not_found=True, flavor=flavor ) kernel_name = kernel_info.version if kernel_info else '' return f'{os_release.get("ID")}-{kernel_name}.conf' def _get_template_parameters( self, default_entry: str = 'main.conf', flavor: str = '' ) -> Dict[str, Union[str, int]]: kernel_file, initrd_file = self.boot_files.get( flavor, (self.custom_args['kernel'], self.custom_args['initrd']) ) return { 'kernel_file': kernel_file or 'vmlinuz', 'initrd_file': initrd_file or 'initrd', 'boot_options': self.cmdline, 'boot_timeout': self.timeout, 'bootpath': self.get_boot_path(), 'title': self.get_kernel_flavor_title( self.get_menu_entry_title(), flavor ), 'default_entry': default_entry, } @staticmethod def _write_config_file( template: Template, filename: str, parameters: Dict[str, Union[str, int]], ) -> None: try: config_data = template.substitute(parameters) Path.create(os.path.dirname(filename)) with open(filename, 'w') as config: config.write(config_data) except Exception as issue: raise KiwiTemplateError( '{0}: {1}'.format(type(issue).__name__, issue) ) def _write_kernel_cmdline_file(self, root_dir: str) -> None: kernel_cmdline = f'{root_dir}/etc/kernel/cmdline' Path.create(os.path.dirname(kernel_cmdline)) with open(kernel_cmdline, 'w') as cmdline: cmdline.write(self.cmdline) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/config/zipl.py0000644000000000000000000004207215273675667015422 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import copy import logging import shutil from string import Template from typing import Dict, List, Union # project from kiwi.xml_state import HostKeyCertificateT from kiwi.path import Path from kiwi.boot.image.base import BootImageBase from kiwi.bootloader.template.zipl import BootLoaderTemplateZipl from kiwi.bootloader.config.bootloader_spec_base import BootLoaderSpecBase from kiwi.command import Command from kiwi.utils.temporary import Temporary from kiwi.exceptions import ( KiwiTemplateError, KiwiBootLoaderTargetError, KiwiDiskGeometryError, ) log = logging.getLogger('kiwi') class BootLoaderZipl(BootLoaderSpecBase): def create_loader_image(self, target: str) -> None: """ Nothing to be done here for zipl :param str target: unused """ pass # pragma: nocover def setup_loader(self, target: str) -> None: """ Setup temporary zipl config and install zipl for supported targets. Please note we are not touching the main zipl.conf file provided by the distributors :param str target: target identifier, one of disk, live(iso) or install(iso) Currently only the disk identifier is supported """ if target != self.target.disk: raise KiwiBootLoaderTargetError( 'zipl is only supported with the disk image target' ) boot_options = self.custom_args['boot_options'] self._mount_system( boot_options.get('device_map'), boot_options.get('root_device'), boot_options.get('boot_device'), boot_options.get('efi_device'), boot_options.get('system_volumes'), boot_options.get('system_root_volume'), boot_options.get('system_partitions'), ) assert self.root_mount is not None root_dir = self.root_mount.mountpoint self.custom_args['secure_linux'] = False host_key_certificates: List[HostKeyCertificateT] = ( self.xml_state.get_host_key_certificates() ) if host_key_certificates: # The secure execution image is created from the kernel and # initrd of the latest kernel. Kernel flavors are not # supported in this setup, see RuntimeChecker self._set_boot_files(root_dir) cc_boot_image = f'{self.custom_args["kernel"]}.cc' with Temporary( path=self.root_mount.mountpoint, prefix='kiwi_zipl_parmfile_' ).new_file() as hkd_parm_file: with open(hkd_parm_file.name, 'w') as parm_file: parm_file.write(f'{self.cmdline}{os.linesep}') genprotimg_init = [ 'chroot', root_dir, 'genprotimg', '--offline', '--verbose', '-o', cc_boot_image, '-i', self.custom_args['kernel'], '-r', self.custom_args['initrd'], '-p', hkd_parm_file.name.replace(root_dir, ''), ] # verify all host key documents individually and call # genprotimg with --no-verify afterwards. This is done # because genprotimg does not support verification with # multiple signing keys for host_key_certificate in host_key_certificates: genprotimg_host_key_check = copy.deepcopy(genprotimg_init) genprotimg_host_key_check.append('--cert') genprotimg_host_key_check.append( host_key_certificates[0]['hkd_ca_cert'] ) genprotimg_host_key_check.append('--cert') genprotimg_host_key_check.append( host_key_certificate['hkd_sign_cert'] ) for host_key in host_key_certificate['hkd_cert']: genprotimg_host_key_check.append('-k') genprotimg_host_key_check.append(host_key) for host_crl in host_key_certificate['hkd_revocation_list']: genprotimg_host_key_check.append('--crl') genprotimg_host_key_check.append(host_crl) log.info( 'Checking HKDs for signing cert: {}'.format( host_key_certificate['hkd_sign_cert'] ) ) Command.run(genprotimg_host_key_check) os.unlink(f'{root_dir}/{cc_boot_image}') # generate secure execution image genprotimg = genprotimg_init genprotimg.append('--no-verify') for host_key_certificate in host_key_certificates: for host_key in host_key_certificate['hkd_cert']: genprotimg.append('-k') genprotimg.append(host_key) for host_crl in host_key_certificate['hkd_revocation_list']: genprotimg.append('--crl') genprotimg.append(host_crl) Command.run(genprotimg) # extract secure execution header secure_execution_header = 'secure_execution_header.bin' Command.run( [ 'chroot', root_dir, 'pvextract-hdr', '-o', secure_execution_header, cc_boot_image, ] ) shutil.move( os.sep.join([root_dir, secure_execution_header]), os.sep.join( [ self.custom_args['target_dir'], '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), secure_execution_header, ), ] ), ) # generate attestation request binary and response key attestation_request = 'attestation_request.bin' attestation_response_key = 'attestation_response.key' pvattest = [ 'chroot', root_dir, 'pvattest', 'create', '--no-verify', '--verbose', '-o', attestation_request, '--arpk', attestation_response_key, ] for host_key_certificate in host_key_certificates: for host_key in host_key_certificate['hkd_cert']: pvattest.append('-k') pvattest.append(host_key) Command.run(pvattest) shutil.move( os.sep.join([root_dir, attestation_request]), os.sep.join( [ self.custom_args['target_dir'], '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), attestation_request, ), ] ), ) shutil.move( os.sep.join([root_dir, attestation_response_key]), os.sep.join( [ self.custom_args['target_dir'], '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), attestation_response_key, ), ] ), ) self.custom_args['secure_linux'] = True self.custom_args['secure_image_file'] = cc_boot_image os.unlink(f'{root_dir}/{self.custom_args["kernel"]}') os.unlink(f'{root_dir}/{self.custom_args["initrd"]}') self.custom_args['kernel'] = '' self.custom_args['initrd'] = '' # The entry of the first kernel flavor is the default entry. # zipl names the section of an entry by its title kernel_flavors = ( self.xml_state.get_build_type_bootloader_kernel_flavors() ) default_entry = '' if kernel_flavors: default_entry = self.get_kernel_flavor_title( self.get_menu_entry_title(), kernel_flavors[0] ) with Temporary( path=self.root_mount.mountpoint, prefix='kiwi_zipl.conf_' ).new_file() as runtime_zipl_config_file: BootLoaderZipl._write_config_file( BootLoaderTemplateZipl().get_loader_template( with_default_entry=bool(kernel_flavors) ), runtime_zipl_config_file.name, self._get_template_parameters(default_entry), ) self.set_loader_entry(self.root_mount.mountpoint, self.target.disk) self._install_zipl(root_dir, runtime_zipl_config_file.name) def set_loader_entry(self, root_dir: str, target: str) -> None: """ Setup/update loader entries of the form {boot_path}/loader/entries/{get_entry_name}, one entry for the kernel of each kernel flavor, see get_kernel_flavors() :param str target: target identifier, one of disk, live(iso) or install(iso) """ for flavor in self.get_kernel_flavors(): if not self.custom_args['secure_linux']: self._set_boot_files(root_dir, flavor) entry_name = self.get_entry_name(flavor) BootLoaderZipl._write_config_file( BootLoaderTemplateZipl().get_entry_template( self.custom_args['secure_linux'] ), root_dir + f'{self.entries_dir}/{entry_name}', self._get_template_parameters(entry_name, flavor), ) def _set_boot_files(self, root_dir: str, flavor: str = '') -> None: """ Set the kernel and initrd file of the kernel of the given flavor as the files of the loader entry :param str root_dir: path to root directory tree :param str flavor: kernel flavor, see Kernel.get_kernel() """ boot_path = self.get_boot_path() kernel_info = BootImageBase( self.xml_state, root_dir, root_dir ).get_boot_names(flavor) self.custom_args['kernel'] = ( f'{boot_path}/{os.path.basename(kernel_info.kernel_filename)}' ) self.custom_args['initrd'] = f'{boot_path}/{kernel_info.initrd_name}' def _install_zipl(self, root_dir: str, zipl_config: str) -> None: """ Install zipl on target """ zipl = [ 'chroot', root_dir, 'zipl', '--noninteractive', '--config', zipl_config.replace(root_dir, ''), '--blsdir', self.entries_dir, '--verbose', ] assert self.sys_mount is not None self.sys_mount.umount() Command.run(zipl) def _get_template_parameters( self, default_entry: str = '', flavor: str = '' ) -> Dict[str, Union[str, int]]: disk_type = self.disk_type or 'SCSI' disk_type = disk_type if disk_type != 'GPT' else 'SCSI' blocksize = self.disk_blocksize or 512 unsupported_for_target_geometry = ['FBA', 'SCSI'] targetbase = f'targetbase={self.custom_args.get("targetbase")}' geometry = '' if disk_type not in unsupported_for_target_geometry: geometry = f'targetgeometry={self._get_disk_geometry()}' return { 'secure_image_file': self.custom_args.get('secure_image_file') or '', 'kernel_file': self.custom_args['kernel'] or 'vmlinuz', 'initrd_file': self.custom_args['initrd'] or 'initrd', 'boot_options': self.cmdline, 'boot_timeout': self.timeout, 'bootpath': self.get_boot_path(), 'targetbase': targetbase, 'targettype': disk_type, 'targetblocksize': format(blocksize), 'targetoffset': self._get_partition_start(), 'targetgeometry': geometry, 'title': self.get_kernel_flavor_title( self.get_menu_entry_title(), flavor ), 'default_entry': default_entry, } def _get_disk_geometry(self) -> str: target_table_type = self.firmware.get_partition_table_type() disk_device = self.custom_args['targetbase'] disk_geometry = '' if target_table_type == 'dasd': disk_geometry = '{0},{1},{2}'.format( self._get_dasd_disk_geometry_element(disk_device, 'cylinders'), self._get_dasd_disk_geometry_element( disk_device, 'tracks per cylinder' ), self._get_dasd_disk_geometry_element( disk_device, 'blocks per track' ), ) return disk_geometry def _get_partition_start(self) -> str: target_table_type = self.firmware.get_partition_table_type() disk_device = self.custom_args['targetbase'] if target_table_type == 'dasd': blocks = self._get_dasd_disk_geometry_element( disk_device, 'blocks per track' ) fdasd_command = [ 'fdasd', '-f', '-s', '-p', disk_device, '|', 'grep', '"^ "', '|', 'head', '-n', '1', '|', 'tr', '-s', '" "', ] fdasd_call = Command.run(['bash', '-c', ' '.join(fdasd_command)]) fdasd_output = fdasd_call.output try: start_track = int(fdasd_output.split(' ')[2].lstrip()) except Exception: raise KiwiDiskGeometryError( f'unknown partition format: {fdasd_output}' ) return '{0}'.format(start_track * blocks) else: sfdisk_command = ' '.join( [ 'sfdisk', '--dump', disk_device, '|', 'grep', '"1 :"', '|', 'cut', '-f1', '-d,', '|', 'cut', '-f2', '-d=', ] ) return Command.run(['bash', '-c', sfdisk_command]).output.strip() def _get_dasd_disk_geometry_element( self, disk_device: str, search: str ) -> int: fdasd = ['fdasd', '-f', '-p', disk_device] bash_command = fdasd + ['|', 'grep', '"' + search + '"'] fdasd_call = Command.run(['bash', '-c', ' '.join(bash_command)]) fdasd_output = fdasd_call.output try: return int(fdasd_output.split(':')[1].lstrip()) except Exception: raise KiwiDiskGeometryError( f'unknown format for disk geometry: {fdasd_output}' ) @staticmethod def _write_config_file( template: Template, filename: str, parameters: Dict[str, Union[str, int]], ) -> None: try: config_data = template.substitute(parameters) Path.create(os.path.dirname(filename)) with open(filename, 'w') as config: config.write(config_data) except Exception as issue: raise KiwiTemplateError( '{0}: {1}'.format(type(issue).__name__, issue) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7417269 kiwi-11.1.8/kiwi/bootloader/install/__init__.py0000644000000000000000000000462515273675667016406 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import Dict, Optional from kiwi.xml_state import XMLState from kiwi.bootloader.install.base import BootLoaderInstallBase from abc import ABCMeta, abstractmethod from ...exceptions import KiwiBootLoaderInstallSetupError class BootLoaderInstall(metaclass=ABCMeta): """ **BootLoaderInstall Factory** :param string name: bootloader name :param string root_dir: root directory path name :param object device_provider: instance of :class:`DeviceProvider` :param dict custom_args: custom arguments dictionary """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( name: str, xml_state: XMLState, root_dir: str, device_provider: object, custom_args: Optional[Dict] = None, ) -> BootLoaderInstallBase: name_map = { 'grub2': {'grub2': 'BootLoaderInstallGrub2'}, 'grub2_s390x_emu': {'grub2': 'BootLoaderInstallGrub2'}, 'systemd_boot': {'systemd_boot': 'BootLoaderInstallSystemdBoot'}, 'zipl': {'zipl': 'BootLoaderInstallZipl'}, } try: bootloader_namespace, bootloader_name = list( name_map[name].items() )[0] bootloader_install = importlib.import_module( 'kiwi.bootloader.install.{}'.format(bootloader_namespace) ) return bootloader_install.__dict__[bootloader_name]( xml_state, root_dir, device_provider, custom_args ) except Exception: raise KiwiBootLoaderInstallSetupError( f'Support for {name} bootloader installation not implemented' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7423341 kiwi-11.1.8/kiwi/bootloader/install/base.py0000644000000000000000000000513615273675667015557 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Dict, Optional # project from kiwi.xml_state import XMLState from kiwi.storage.device_provider import DeviceProvider class BootLoaderInstallBase: """ **Base class for bootloader installation on device** :param string root_dir: root directory path name :param object device_provider: instance of :class:`DeviceProvider` :param dict custom_args: custom arguments dictionary """ def __init__( self, xml_state: XMLState, root_dir: str, device_provider: DeviceProvider, custom_args: Optional[Dict] = None, ) -> None: self.xml_state = xml_state self.root_dir = root_dir self.device_provider = device_provider self.device = self.device_provider.get_device() self.post_init(custom_args) def post_init(self, custom_args: Optional[Dict]) -> None: """ Post initialization method Store custom arguments by default :param dict custom_args: custom bootloader arguments """ self.custom_args = custom_args def install_required(self) -> bool: """ Check if bootloader needs to be installed Implementation in specialized bootloader install class required """ raise NotImplementedError def install(self) -> None: """ Install bootloader on self.device Implementation in specialized bootloader install class required """ raise NotImplementedError def secure_boot_install(self) -> None: """ Run shim-install in self.device for secure boots Implementation in specialized bootloader install class required """ raise NotImplementedError def set_disk_password(self, password: str) -> None: """ Include crypto mount credentials Implementation in specialized bootloader install class optional """ pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7423341 kiwi-11.1.8/kiwi/bootloader/install/grub2.py0000644000000000000000000005020115273675667015657 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import re import shutil import logging from contextlib import ExitStack from typing import Dict, List, Literal, Optional, overload # project from kiwi.bootloader.install.base import BootLoaderInstallBase from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.firmware import FirmWare from kiwi.mount_manager import MountManager from kiwi.system.setup import SystemSetup from kiwi.path import Path from kiwi.storage.disk import ptable_entry_type from kiwi.exceptions import ( KiwiBootLoaderGrubInstallError, KiwiBootLoaderGrubPlatformError, KiwiBootLoaderGrubDataError, KiwiBootLoaderDiskPasswordError, ) log = logging.getLogger('kiwi') class BootLoaderInstallGrub2(BootLoaderInstallBase): """ **grub2 bootloader installation** """ def post_init(self, custom_args: Optional[Dict]) -> None: """ grub2 post initialization method :param dict custom_args: Contains custom grub2 bootloader arguments .. code:: python { 'target_removable': bool, 'system_volumes': list_of_volumes, 'system_root_volume': root volume name if required 'firmware': FirmWare_instance, 'efi_device': string, 'boot_device': string, 'root_device': string } """ self.arch = Defaults.get_platform_name() self.custom_args = custom_args self.install_arguments: List[str] = [] self.shim_install_arguments: List[str] = [] self.firmware: Optional[FirmWare] = None self.root_mount: Optional[MountManager] = None self.sysfs_mount: Optional[MountManager] = None self.volumes: Optional[Dict] = None self.partitions: Optional[Dict[str, ptable_entry_type]] = None self.root_volume_name: Optional[str] = None self.target_removable: Optional[bool] = None self.device_map: Optional[Dict] = None if custom_args and 'target_removable' in custom_args: self.target_removable = custom_args['target_removable'] if custom_args and 'system_volumes' in custom_args: self.volumes = custom_args['system_volumes'] if custom_args and 'system_partitions' in custom_args: self.partitions = custom_args['system_partitions'] if custom_args and 'device_map' in custom_args: self.device_map = custom_args['device_map'] if custom_args and 'system_root_volume' in custom_args: self.root_volume_name = custom_args['system_root_volume'] if custom_args and 'firmware' in custom_args: self.firmware = custom_args['firmware'] if custom_args and 'install_options' in custom_args: self.install_arguments = custom_args['install_options'] if custom_args and 'shim_options' in custom_args: self.shim_install_arguments = custom_args['shim_options'] if self.firmware and self.firmware.efi_mode(): if not custom_args or 'efi_device' not in custom_args: raise KiwiBootLoaderGrubInstallError( 'EFI device name required for shim installation' ) if not custom_args or 'boot_device' not in custom_args: raise KiwiBootLoaderGrubInstallError( 'boot device name required for grub2 installation' ) if not custom_args or 'root_device' not in custom_args: raise KiwiBootLoaderGrubInstallError( 'root device name required for grub2 installation' ) def install_required(self) -> bool: """ Check if grub2 has to be installed Take architecture and firmware setup into account to check if bootloader code in a boot record is required :return: True or False :rtype: bool """ # the firmware is passed along by the disk builder assert self.firmware is not None if 'ppc64' in self.arch and self.firmware.opal_mode(): # OPAL doesn't need a grub2 stage1, just a config file. # The machine will be setup to kexec grub2 in user space log.info( 'No grub boot code installation in opal mode on %s', self.arch ) return False elif 'arm' in self.arch or self.arch == 'aarch64': # On arm grub2 is used for EFI setup only, no install # of grub2 boot code makes sense log.info('No grub boot code installation on %s', self.arch) return False elif self.arch == 'riscv64': # On riscv grub2 is used for EFI setup only, no install # of grub2 boot code makes sense log.info('No grub boot code installation on %s', self.arch) return False elif self.firmware.efi_mode() and not self.firmware.legacy_bios_mode(): # In EFI mode without CSM, no install # of grub2 boot code makes sense log.info('No grub boot code installation in EFI only mode') return False return True def install(self) -> None: """ Install bootloader on disk device """ log.info('Installing grub2 on disk %s', self.device) if self.target_removable: self.install_arguments.append('--removable') if Defaults.is_x86_arch(self.arch): self.target = 'i386-pc' self.install_device = self.device self.modules = ' '.join( Defaults.get_grub_platform_modules(multiboot=True) ) self.install_arguments.append('--skip-fs-probe') elif self.arch.startswith('ppc64'): if not self.custom_args or 'prep_device' not in self.custom_args: raise KiwiBootLoaderGrubInstallError( 'prep device name required for grub2 installation on ppc' ) self.target = 'powerpc-ieee1275' self.install_device = self.custom_args['prep_device'] self.modules = ' '.join(Defaults.get_grub_ofw_modules()) self.install_arguments.append('--skip-fs-probe') self.install_arguments.append('--no-nvram') elif self.arch.startswith('s390'): self.target = 's390x-emu' self.install_device = self.device self.modules = ' '.join(Defaults.get_grub_s390_modules()) self.install_arguments.append('--skip-fs-probe') self.install_arguments.append('--no-nvram') else: raise KiwiBootLoaderGrubPlatformError( f'host architecture {self.arch} not supported for grub2 installation' ) with ExitStack() as stack: self._mount_device_and_volumes(stack) assert self.root_mount is not None assert self.sysfs_mount is not None # check if a grub installation could be found in the image system module_directory = Defaults.get_grub_path( self.root_mount.mountpoint, self.target, raise_on_error=False ) if not module_directory: raise KiwiBootLoaderGrubDataError( 'No grub2 installation found in {0} for target {1}'.format( self.root_mount.mountpoint, self.target ) ) module_directory = module_directory.replace( self.root_mount.mountpoint, '' ) boot_directory = '/boot' # install grub2 boot code, the firmware is passed # along by the disk builder assert self.firmware is not None if self.firmware.get_partition_table_type() == 'dasd': # On s390 and in CDL mode (4k DASD) the call of grub2-install # does not work because grub2-install is not able to identify # a 4k fdasd partitioned device as a grub supported device # and fails. As grub2-install is only used to invoke # grub2-zipl-setup and has no other job to do we can # circumvent this problem by directly calling grub2-zipl-setup # instead. self.sysfs_mount.umount() Command.run( [ 'chroot', self.root_mount.mountpoint, 'grub2-zipl-setup', '--keep', ] ) else: Command.run( [ 'chroot', self.root_mount.mountpoint, self._get_grub2_install_tool_name( self.root_mount.mountpoint ), ] + self.install_arguments + [ '--directory', module_directory, '--boot-directory', boot_directory, '--target', self.target, '--modules', self.modules, self.install_device, ] ) # Cleanup temporary modified zipl template and config files # back to its original state or lave a .kiwi copy for # reference in the system zipl_config_file = ''.join( [self.root_mount.mountpoint, '/boot/zipl/config'] ) zipl2grub_config_file_orig = ''.join( [ self.root_mount.mountpoint, '/etc/default/zipl2grub.conf.in.orig', ] ) if os.path.exists(zipl2grub_config_file_orig): Command.run( [ 'mv', zipl2grub_config_file_orig, zipl2grub_config_file_orig.replace('.orig', ''), ] ) if os.path.exists(zipl_config_file): Command.run( ['mv', zipl_config_file, zipl_config_file + '.kiwi'] ) # Rebuild security context setup = SystemSetup(self.xml_state, self.root_mount.mountpoint) setup.setup_selinux_file_contexts() def secure_boot_install(self) -> None: if ( self.firmware and self.firmware.efi_mode() == 'uefi' and ( Defaults.is_x86_arch(self.arch) or 'arm' in self.arch or self.arch == 'aarch64' # noqa: W503 ) ): with ExitStack() as stack: self._mount_device_and_volumes(stack) assert self.root_mount is not None shim_install = self._get_shim_install_tool_name( self.root_mount.mountpoint ) # if shim-install does _not_ exist the fallback mechanism # has applied at the bootloader/config level and we expect # no further tool calls to be required if shim_install: # Before we call shim-install, the grub installer binary is # replaced by a noop. Actually there is no reason for # shim-install to call the grub installer because it should # only setup the system for EFI secure boot which does not # require any bootloader code in the master boot record. # In addition kiwi has called the grub installer right # before try: self._disable_grub2_install(self.root_mount.mountpoint) Command.run( [ 'chroot', self.root_mount.mountpoint, 'shim-install', '--removable', ] + self.shim_install_arguments + [self.device] ) finally: # restore the grub installer noop self._enable_grub2_install(self.root_mount.mountpoint) def set_disk_password(self, password: str) -> None: with ExitStack() as stack: self._mount_device_and_volumes(stack) if self.root_mount and password is not None: log.debug('Include cryptomount credentials...') config_file = ( f'{self.root_mount.mountpoint}/boot/efi/EFI/BOOT/grub.cfg' ) try: with open(config_file) as config: grub_config = config.read() grub_config = re.sub( r'cryptomount', f'cryptomount -p "{password}"', grub_config, ) with open(config_file, 'w') as grub_config_file: grub_config_file.write(grub_config) log.debug(f'<<< {grub_config} >>>') except IOError as issue: raise KiwiBootLoaderDiskPasswordError( f'Failed to open {config_file}: {issue}' ) def _mount_device_and_volumes(self, stack: ExitStack) -> None: assert self.custom_args is not None self.root_mount = MountManager(device=self.custom_args['root_device']) stack.push(self.root_mount) custom_root_mount_args = [] if self.root_volume_name and self.root_volume_name != '/': custom_root_mount_args += [f'subvol={self.root_volume_name}'] readonly_device = (self.device_map or {}).get('readonly') if ( readonly_device and self.custom_args['root_device'] == readonly_device.get_device() ): # The overlayroot readonly partition holds a read-only # filesystem (squashfs/erofs), mount it as such custom_root_mount_args += ['ro'] self.root_mount.mount(options=custom_root_mount_args) if 's390' in self.arch: boot_mount = MountManager( device=self.custom_args['boot_device'], mountpoint=self.root_mount.mountpoint + '/boot/zipl', ) else: boot_mount = MountManager( device=self.custom_args['boot_device'], mountpoint=self.root_mount.mountpoint + '/boot', ) stack.push(boot_mount) if not self.root_mount.device == boot_mount.device: boot_mount.mount() if self.custom_args.get('efi_device'): efi_mount = MountManager( device=self.custom_args['efi_device'], mountpoint=self.root_mount.mountpoint + '/boot/efi', ) stack.push(efi_mount) efi_mount.mount() if self.volumes: for volume_path in Path.sort_by_hierarchy( sorted(self.volumes.keys()) ): volume_mount = MountManager( device=self.volumes[volume_path]['volume_device'], mountpoint=self.root_mount.mountpoint + '/' + volume_path, ) stack.push(volume_mount) volume_mount.mount( options=[self.volumes[volume_path]['volume_options']] ) if self.partitions: # the partitions are mounted from the given device map assert self.device_map is not None for map_name in sorted(self.partitions.keys()): if map_name in self.device_map: partition_mount = MountManager( device=self.device_map[map_name].get_device(), mountpoint=os.path.join( self.root_mount.mountpoint, self.partitions[map_name].mountpoint.lstrip(os.sep), ), ) stack.push(partition_mount) partition_mount.mount() device_mount = MountManager( device='/dev', mountpoint=self.root_mount.mountpoint + '/dev' ) stack.push(device_mount) device_mount.bind_mount() proc_mount = MountManager( device='/proc', mountpoint=self.root_mount.mountpoint + '/proc' ) stack.push(proc_mount) proc_mount.bind_mount() self.sysfs_mount = MountManager( device='/sys', mountpoint=self.root_mount.mountpoint + '/sys' ) stack.push(self.sysfs_mount) self.sysfs_mount.bind_mount() def _disable_grub2_install(self, root_path: str) -> None: if os.access(root_path, os.W_OK): grub2_install = ''.join( ['/usr/sbin/', self._get_grub2_install_tool_name(root_path)] ) grub2_install_backup = ''.join([grub2_install, '.orig']) grub2_install_noop = '/bin/true' shutil.copy2( os.path.normpath(f'{root_path}/{grub2_install}'), os.path.normpath(f'{root_path}/{grub2_install_backup}'), ) shutil.copy2( os.path.normpath(f'{root_path}/{grub2_install_noop}'), os.path.normpath(f'{root_path}/{grub2_install}'), ) def _enable_grub2_install(self, root_path: str) -> None: if os.access(root_path, os.W_OK): grub2_install = ''.join( ['/usr/sbin/', self._get_grub2_install_tool_name(root_path)] ) grub2_install_backup = ''.join([grub2_install, '.orig']) if os.path.exists(''.join([root_path, grub2_install_backup])): Command.run( [ 'chroot', root_path, 'mv', grub2_install_backup, grub2_install, ] ) def _get_grub2_install_tool_name(self, root_path: str) -> str: return self._get_tool_name( root_path, lookup_list=['grub2-install', 'grub-install'] ) def _get_shim_install_tool_name(self, root_path: str) -> Optional[str]: return self._get_tool_name( root_path, lookup_list=['shim-install'], fallback_on_not_found=False ) @overload def _get_tool_name( self, root_path: str, lookup_list: List[str], fallback_on_not_found: Literal[True] = True, ) -> str: ... # pragma: nocover @overload def _get_tool_name( self, root_path: str, lookup_list: List[str], fallback_on_not_found: Literal[False], ) -> Optional[str]: ... # pragma: nocover def _get_tool_name( self, root_path: str, lookup_list: List[str], fallback_on_not_found: bool = True, ) -> Optional[str]: for tool in lookup_list: if Path.which(filename=tool, root_dir=root_path): return tool if fallback_on_not_found: # no tool from the list was found, we intentionally don't # raise here but return the default tool name and raise # an exception at invocation time in order to log the # expected call and its arguments return lookup_list[0] return None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7423341 kiwi-11.1.8/kiwi/bootloader/install/systemd_boot.py0000644000000000000000000000350415273675667017355 0ustar00# Copyright (c) 2022 Marcus Schäfer. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import Dict, Optional # project from kiwi.bootloader.install.base import BootLoaderInstallBase log = logging.getLogger('kiwi') class BootLoaderInstallSystemdBoot(BootLoaderInstallBase): """ **systemd-boot bootloader installation** """ def post_init(self, custom_args: Optional[Dict]) -> None: """ systemd-boot post initialization method :param dict custom_args: Contains custom systemd-boot bootloader arguments """ self.custom_args = custom_args def install_required(self) -> bool: """ Check if systemd-boot needs to install boot code systemd-boot supports EFI boot only and does not need to install boot code since it's expected that the firmware can read from the EFI partition :return: True or False :rtype: bool """ return False def secure_boot_install(self) -> None: """ Run shim installation for secure boot setup For systemd-boot this is currently skipped since details for secure boot are not yet clear """ pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7423341 kiwi-11.1.8/kiwi/bootloader/install/zipl.py0000644000000000000000000000336415273675667015624 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import Dict, Optional # project from kiwi.bootloader.install.base import BootLoaderInstallBase log = logging.getLogger('kiwi') class BootLoaderInstallZipl(BootLoaderInstallBase): """ **zipl bootloader installation** """ def post_init(self, custom_args: Optional[Dict]) -> None: """ zipl post initialization method :param dict custom_args: unused """ self.custom_args = custom_args def install_required(self) -> bool: """ Check if zipl needs to install boot code zipl requires boot code installation, but it is done as part of the BLS implementation in bootloader/config/zipl.py Thus this method always returns: False :return: False :rtype: bool """ return False def secure_boot_install(self) -> None: """ Run shim installation for secure boot setup For zipl this is skipped since details for secure boot are not yet clear. """ pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7423341 kiwi-11.1.8/kiwi/bootloader/template/__init__.py0000644000000000000000000000000015273675667016532 0ustar00././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.742636 kiwi-11.1.8/kiwi/bootloader/template/grub2.py0000644000000000000000000004671115273675667016037 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import re from string import Template from textwrap import dedent class BootLoaderTemplateGrub2: """ **grub2 configuraton file templates** """ def __init__(self) -> None: self.header = ( dedent( ''' # kiwi generated one time grub2 config file search ${search_params} set default=${default_boot} if [ -n "$$extra_cmdline" ]; then submenu "Bootable snapshot $$snapshot_num" { menuentry "If OK, run snapper rollback and reboot." { true; } } fi if [ -n "$${iso_path}" ]; then isoboot="iso-scan/filename=$${iso_path}" fi ''' ).strip() + os.linesep ) self.header_btrfs_relative_path = ( dedent( ''' set btrfs_relative_path="y" export btrfs_relative_path ''' ).strip() + os.linesep ) self.timeout = ( dedent( ''' set timeout=${boot_timeout} ''' ).strip() + os.linesep ) self.timeout_style = ( dedent( ''' set timeout_style=${boot_timeout_style} ''' ).strip() + os.linesep ) self.header_gfxmode = ( dedent( ''' if [ "$${grub_platform}" = "efi" ]; then echo "Please press 't' to show the boot menu on this console" fi set gfxmode=${gfxmode} ''' ).strip() + os.linesep ) self.header_serial = ( dedent( ''' ${serial_line_setup} ''' ).strip() + os.linesep ) self.header_terminal_setup = ( dedent( ''' terminal_input ${terminal_input} terminal_output ${terminal_output} ''' ).strip() + os.linesep ) self.fonts = ( dedent( ''' set font=($$root)${bootpath}/${boot_directory_name}/fonts/unicode.pf2 set ascii_font=${boot_directory_name}/themes/${theme}/ascii.pf2 set sans_bold_14_font=${boot_directory_name}/themes/${theme}/DejaVuSans-Bold14.pf2 set sans_10_font=${boot_directory_name}/themes/${theme}/DejaVuSans10.pf2 set sans_12_font=${boot_directory_name}/themes/${theme}/DejaVuSans12.pf2 ''' ).strip() + os.linesep ) self.header_theme = ( self.fonts + dedent( ''' if [ -f $${font} ];then loadfont $${font} fi if [ -f ($$root)${bootpath}/$${ascii_font} ];then loadfont ($$root)${bootpath}/$${ascii_font} fi if [ -f ($$root)${bootpath}/$${sans_bold_14_font} ];then loadfont ($$root)${bootpath}/$${sans_bold_14_font} fi if [ -f ($$root)${bootpath}/$${sans_10_font} ];then loadfont ($$root)${bootpath}/$${sans_10_font} fi if [ -f ($$root)${bootpath}/$${sans_12_font} ];then loadfont ($$root)${bootpath}/$${sans_12_font} fi if [ -f ($$root)${bootpath}/${boot_directory_name}/themes/${theme}/theme.txt ];then set theme=($$root)${bootpath}/${boot_directory_name}/themes/${theme}/theme.txt export theme fi ''' ).strip() + os.linesep ) self.header_theme_iso = ( self.fonts + dedent( ''' if [ -f $${font} ];then loadfont $${font} fi if [ -f ($$root)/boot/$${ascii_font} ];then loadfont ($$root)/boot/$${ascii_font} fi if [ -f ($$root)/boot/$${sans_bold_14_font} ];then loadfont ($$root)/boot/$${sans_bold_14_font} fi if [ -f ($$root)/boot/$${sans_10_font} ];then loadfont ($$root)/boot/$${sans_10_font} fi if [ -f ($$root)/boot/$${sans_12_font} ];then loadfont ($$root)/boot/$${sans_12_font} fi if [ -f ($$root)/boot/${boot_directory_name}/themes/${theme}/theme.txt ];then set theme=($$root)/boot/${boot_directory_name}/themes/${theme}/theme.txt fi ''' ).strip() + os.linesep ) self.menu_entry_boot_snapshots = ( dedent( ''' if [ -f "/.snapshots/grub-snapshot.cfg" ]; then source "/.snapshots/grub-snapshot.cfg" fi ''' ).strip() + os.linesep ) self.menu_entry_console_switch = ( dedent( ''' if [ "$${grub_platform}" = "efi" ]; then hiddenentry "Text mode" --hotkey "t" { set textmode=true terminal_output console } fi ''' ).strip() + os.linesep ) self.menu_entry_multiboot = ( dedent( ''' menuentry "${title}" --class os --unrestricted { set gfxpayload=keep echo Loading hypervisor... multiboot ${bootpath}/${hypervisor} dummy echo Loading kernel... module ${bootpath}/${kernel_file} dummy ${boot_options} echo Loading initrd... module ${bootpath}/${initrd_file} dummy } ''' ).strip() + os.linesep ) self.menu_entry = ( dedent( ''' menuentry "${title}" --class os --unrestricted { set gfxpayload=keep echo Loading kernel... linux ($$root)${bootpath}/${kernel_file} $${extra_cmdline} $${isoboot} ${boot_options} echo Loading initrd... initrd ($$root)${bootpath}/${initrd_file} } ''' ).strip() + os.linesep ) self.menu_entry_failsafe_multiboot = ( dedent( ''' menuentry "Failsafe -- ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading hypervisor... multiboot ${bootpath}/${hypervisor} dummy echo Loading kernel... module ${bootpath}/${kernel_file} dummy ${failsafe_boot_options} echo Loading initrd... module ${bootpath}/${initrd_file} dummy } ''' ).strip() + os.linesep ) self.menu_entry_failsafe = ( dedent( ''' menuentry "Failsafe -- ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading kernel... linux ($$root)${bootpath}/${kernel_file} $${extra_cmdline} $${isoboot} ${failsafe_boot_options} echo Loading initrd... initrd ($$root)${bootpath}/${initrd_file} } ''' ).strip() + os.linesep ) self.menu_install_entry_multiboot = ( dedent( ''' menuentry "Install -- ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading hypervisor... multiboot ${bootpath}/${hypervisor} dummy echo Loading kernel... module ${bootpath}/${kernel_file} dummy cdinst=1 ${failsafe_boot_options} echo Loading initrd... module ${bootpath}/${initrd_file} dummy } ''' ).strip() + os.linesep ) self.menu_install_entry = ( dedent( ''' menuentry "Install ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading kernel... linux ($$root)${bootpath}/${kernel_file} cdinst=1 $${isoboot} ${boot_options} echo Loading initrd... initrd ($$root)${bootpath}/${initrd_file} } ''' ).strip() + os.linesep ) self.menu_mediacheck_entry_multiboot = ( dedent( ''' menuentry "Mediacheck" --class os --unrestricted { set gfxpayload=keep echo Loading hypervisor... multiboot ${bootpath}/${hypervisor} dummy echo Loading kernel... module ${bootpath}/${kernel_file} dummy mediacheck=1 plymouth.enable=0 ${failsafe_boot_options} echo Loading initrd... module ${bootpath}/${initrd_file} dummy } ''' ).strip() + os.linesep ) self.menu_mediacheck_entry = ( dedent( ''' menuentry "Mediacheck" --class os --unrestricted { set gfxpayload=keep echo Loading kernel... linux ($$root)${bootpath}/${kernel_file} mediacheck=1 plymouth.enable=0 $${isoboot} ${boot_options} echo Loading initrd... initrd ($$root)${bootpath}/${initrd_file} } ''' ).strip() + os.linesep ) self.menu_install_entry_failsafe_multiboot = ( dedent( ''' menuentry "Failsafe -- Install ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading hypervisor... multiboot ${bootpath}/${hypervisor} dummy echo Loading kernel... module ${bootpath}/${kernel_file} dummy cdinst=1 ${failsafe_boot_options} echo Loading initrd... module ${bootpath}/${initrd_file} dummy } ''' ).strip() + os.linesep ) self.menu_install_entry_failsafe = ( dedent( ''' menuentry "Failsafe -- Install ${title}" --class os --unrestricted { set gfxpayload=keep echo Loading kernel... linux ($$root)${bootpath}/${kernel_file} cdinst=1 $${isoboot} ${failsafe_boot_options} echo Loading initrd... initrd ($$root)${bootpath}/${initrd_file} } ''' ).strip() + os.linesep ) self.menu_iso_harddisk_entry = ( dedent( ''' menuentry "Boot from Hard Disk" --class os --unrestricted { exit } ''' ).strip() + os.linesep ) def get_iso_template( self, failsafe: bool = True, has_graphics: bool = True, has_serial: bool = False, checkiso: bool = False, has_btrfs_relative_path: bool = False, kernel_count: int = 1, ) -> Template: """ Bootloader configuration template for live ISO media :param bool failsafe: with failsafe true|false :param bool has_graphics: supports graphics terminal :param bool has_serial: supports serial terminal :param int kernel_count: number of kernels to create a boot menu entry for, see _get_kernel_menu_entries() :return: instance of :class:`Template` :rtype: Template """ if has_btrfs_relative_path: template_data = self.header_btrfs_relative_path template_data += self.header else: template_data = self.header template_data += self.timeout template_data += self.timeout_style if has_graphics: template_data += self.header_gfxmode template_data += self.header_theme_iso if has_serial: template_data += self.header_serial template_data += self.header_terminal_setup template_data += self._get_kernel_menu_entries( self.menu_entry, kernel_count ) if failsafe: template_data += self.menu_entry_failsafe if checkiso: template_data += self.menu_mediacheck_entry template_data += self.menu_iso_harddisk_entry template_data += self.menu_entry_boot_snapshots if has_graphics: template_data += self.menu_entry_console_switch return Template(template_data) def get_multiboot_iso_template( self, failsafe: bool = True, has_graphics: bool = True, has_serial: bool = False, checkiso: bool = False, has_btrfs_relative_path: bool = False, kernel_count: int = 1, ) -> Template: """ Bootloader configuration template for live ISO media with hypervisor, e.g Xen dom0 :param bool failsafe: with failsafe true|false :param bool has_graphics: supports graphics terminal :param bool has_serial: supports serial terminal :param int kernel_count: number of kernels to create a boot menu entry for, see _get_kernel_menu_entries() :return: instance of :class:`Template` :rtype: Template """ if has_btrfs_relative_path: template_data = self.header_btrfs_relative_path template_data += self.header else: template_data = self.header template_data += self.timeout template_data += self.timeout_style if has_graphics: template_data += self.header_gfxmode template_data += self.header_theme_iso if has_serial: template_data += self.header_serial template_data += self.header_terminal_setup template_data += self._get_kernel_menu_entries( self.menu_entry_multiboot, kernel_count ) if failsafe: template_data += self.menu_entry_failsafe_multiboot if checkiso: template_data += self.menu_mediacheck_entry_multiboot template_data += self.menu_iso_harddisk_entry template_data += self.menu_entry_boot_snapshots if has_graphics: template_data += self.menu_entry_console_switch return Template(template_data) def get_install_template( self, failsafe: bool = True, has_graphics: bool = True, has_serial: bool = False, with_timeout: bool = True, has_btrfs_relative_path: bool = False, kernel_count: int = 1, ) -> Template: """ Bootloader configuration template for install media :param bool failsafe: with failsafe true|false :param bool has_graphics: supports graphics terminal :param bool has_serial: supports serial terminal :param int kernel_count: number of kernels to create a boot menu entry for, see _get_kernel_menu_entries() :return: instance of :class:`Template` :rtype: Template """ if has_btrfs_relative_path: template_data = self.header_btrfs_relative_path template_data += self.header else: template_data = self.header if with_timeout: template_data += self.timeout template_data += self.timeout_style if has_graphics: template_data += self.header_gfxmode template_data += self.header_theme_iso if has_serial: template_data += self.header_serial template_data += self.header_terminal_setup template_data += self.menu_iso_harddisk_entry template_data += self._get_kernel_menu_entries( self.menu_install_entry, kernel_count ) if failsafe: template_data += self.menu_install_entry_failsafe template_data += self.menu_entry_boot_snapshots if has_graphics: template_data += self.menu_entry_console_switch return Template(template_data) def get_multiboot_install_template( self, failsafe: bool = True, has_graphics: bool = True, has_serial: bool = False, with_timeout: bool = True, has_btrfs_relative_path: bool = False, kernel_count: int = 1, ) -> Template: """ Bootloader configuration template for install media with hypervisor, e.g Xen dom0 :param bool failsafe: with failsafe true|false :param bool has_graphics: supports graphics terminal :param bool has_serial: supports serial terminal :param int kernel_count: number of kernels to create a boot menu entry for, see _get_kernel_menu_entries() :return: instance of :class:`Template` :rtype: Template """ if has_btrfs_relative_path: template_data = self.header_btrfs_relative_path template_data += self.header else: template_data = self.header if with_timeout: template_data += self.timeout template_data += self.timeout_style if has_graphics: template_data += self.header_gfxmode template_data += self.header_theme_iso if has_serial: template_data += self.header_serial template_data += self.header_terminal_setup template_data += self.menu_iso_harddisk_entry template_data += self._get_kernel_menu_entries( self.menu_install_entry_multiboot, kernel_count ) if failsafe: template_data += self.menu_install_entry_failsafe_multiboot template_data += self.menu_entry_boot_snapshots if has_graphics: template_data += self.menu_entry_console_switch return Template(template_data) @staticmethod def _get_kernel_menu_entries(menu_entry: str, kernel_count: int) -> str: """ Provide the given menu entry once for each kernel The entry of the first kernel uses the title, kernel_file and initrd_file parameters. The entry of each further kernel uses the parameters of the same name with the index of the kernel appended, e.g title_1, kernel_file_1 and initrd_file_1 :param str menu_entry: menu entry template :param int kernel_count: number of kernels :return: menu entries template data :rtype: str """ menu_entries = menu_entry for index in range(1, kernel_count): menu_entries += re.sub( r'\$\{(title|kernel_file|initrd_file)\}', rf'${{\g<1>_{index}}}', menu_entry, ) return menu_entries ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.742636 kiwi-11.1.8/kiwi/bootloader/template/systemd_boot.py0000644000000000000000000000475115273675667017527 0ustar00# Copyright (c) 2022 Marcus Schäfer. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from string import Template from textwrap import dedent class BootLoaderTemplateSystemdBoot: """ **systemd-boot configuraton file templates** """ def __init__(self) -> None: self.cr = '\n' self.loader = ( dedent( ''' # kiwi generated loader config file console-mode max editor no ''' ).strip() + self.cr ) self.timeout = ( dedent( ''' timeout ${boot_timeout} ''' ).strip() + self.cr ) self.default = ( dedent( ''' default ${default_entry} ''' ).strip() + self.cr ) self.entry = ( dedent( ''' title ${title} options ${boot_options} linux ${kernel_file} initrd ${initrd_file} ''' ).strip() + self.cr ) def get_loader_template(self, with_default_entry: bool = False) -> Template: """ Bootloader main configuration template :param bool with_default_entry: select the default entry by the default_entry parameter :return: instance of :class:`Template` :rtype: Template """ template_data = self.loader template_data += self.timeout if with_default_entry: template_data += self.default return Template(template_data) def get_entry_template(self) -> Template: """ Bootloader entry configuration template :return: instance of :class:`Template` :rtype: Template """ template_data = self.entry return Template(template_data) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.742636 kiwi-11.1.8/kiwi/bootloader/template/zipl.py0000644000000000000000000000573515273675667015775 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from string import Template from textwrap import dedent class BootLoaderTemplateZipl: """ **zipl configuraton file templates** """ def __init__(self) -> None: self.cr = '\n' self.main_conf = ( dedent( ''' [defaultboot] defaultauto prompt=1 target=${bootpath} ${targetbase} targettype=${targettype} targetblocksize=${targetblocksize} targetoffset=${targetoffset} ${targetgeometry} timeout=${boot_timeout} secure=auto ''' ).strip() + self.cr ) self.default = ( dedent( ''' default=${default_entry} ''' ).strip() + self.cr ) self.entry_secure = ( dedent( ''' title ${title} options ${boot_options} linux ${secure_image_file} ''' ).strip() + self.cr ) self.entry_standard = ( dedent( ''' title ${title} options ${boot_options} linux ${kernel_file} initrd ${initrd_file} ''' ).strip() + self.cr ) def get_loader_template(self, with_default_entry: bool = False) -> Template: """ Bootloader main configuration template :param bool with_default_entry: select the default entry by the default_entry parameter, which is the name of a section, e.g the title of a loader entry :return: instance of :class:`Template` :rtype: Template """ template_data = self.main_conf if with_default_entry: template_data += self.default return Template(template_data) def get_entry_template(self, secure: bool = False) -> Template: """ Bootloader entry configuration template :return: instance of :class:`Template` :rtype: Template """ if secure: template_data = self.entry_secure else: template_data = self.entry_standard return Template(template_data) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.742636 kiwi-11.1.8/kiwi/boxbuild/__init__.py0000644000000000000000000000132515273675667014410 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/box_build.py0000644000000000000000000002774115273675667014632 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import pwd import time import logging import platform from threading import Thread from typing import List, Optional # project from kiwi.path import Path from kiwi.command import Command from kiwi.boxbuild.box_download import BoxDownload from kiwi.boxbuild.defaults import BoxBuildDefaults import kiwi.boxbuild.defaults as runtime from kiwi.exceptions import ( KiwiBoxDownloadError, KiwiBoxQEMUBinaryNotFound, KiwiBoxSSHPortInvalid, KiwiBoxSSHKeyNotFound, KiwiBoxBuildError, ) log = logging.getLogger('kiwi') class BoxBuild: """ **Implements boxbuild command** Implements an interface to run a kiwi build box using the KVM virtualization platform :param str boxname: name of the box from kiwi_boxed_plugin.yml :param str ram: amount of main memory to use for the box :param str console: console name for the box kernel :param str smp: number of CPUs to use in the SMP setup for the box :param str arch: arch name for box :param str machine: machine emulaton type for the box :param str cpu: CPU emulation type :param str sharing_backend: guest/host sharing backend type :param str ssh_key: name of the ssh key to authorize for sshfs sharing :param str ssh_port: host port number to use to forward the guest's SSH port :param bool accel: use KVM hardware acceleration True|False """ def __init__( self, boxname: str, ram: Optional[str] = None, console: Optional[str] = None, smp: Optional[str] = None, arch: str = '', machine: Optional[str] = None, cpu: str = 'host', sharing_backend: str = '9p', ssh_key: str = 'id_rsa', ssh_port: str = '', accel: bool = True, ) -> None: self.ram = ram self.console = console self.smp = smp self.cpu = cpu self.machine = machine self.arch = arch or platform.machine() self.box = BoxDownload(boxname, arch) self.sharing_backend = sharing_backend self.ssh_key = ssh_key self.ssh_port = ssh_port self.kiwi_exit: Optional[int] = None self.accel = accel def run( self, kiwi_build_command: List[str], update_check: bool = True, snapshot: bool = True, keep_open: bool = False, kiwi_version: Optional[str] = None, custom_shared_path: Optional[str] = None, ) -> None: """ Start the build process in a box VM using KVM :param list kiwi_build_command: kiwi build command line Example: .. code:: python [ '--type', 'oem', 'system', 'build', '--description', 'some/description', '--target-dir', 'some/target-dir' ] :param bool update_check: check for box updates True|False :param bool snapshot: run box in snapshot mode True|False :param bool keep_open: keep VM running True|False :param str kiwi_version: pip install this kiwi version :param str custom_shared_path: make this path available in the box """ self.kiwi_build_command = kiwi_build_command desc = self._pop_arg_param('--description') target_dir = self._pop_arg_param('--target-dir') BoxBuildDefaults.create_build_target_dir(target_dir) try: vm_setup = self.box.fetch(update_check, snapshot) except Exception as issue: raise KiwiBoxDownloadError(f'Failed to fetch box file(s): {issue}') vm_append = [ vm_setup.append, 'kiwi=\\"{0}\\"'.format(' '.join(self.kiwi_build_command)), ] if self.console: vm_append[0] = vm_append[0].replace( f'console={vm_setup.console}', f'console={self.console}' ) if keep_open: vm_append.append('kiwi-no-halt') if kiwi_version: vm_append.append('kiwi_version=_{0}_'.format(kiwi_version)) if custom_shared_path: vm_append.append('custom_mount=_{0}_'.format(custom_shared_path)) if self.sharing_backend == 'sshfs': ssh_key_file = os.sep.join( [os.environ.get('HOME') or '~', '.ssh', f'{self.ssh_key}.pub'] ) if not os.path.isfile(ssh_key_file): raise KiwiBoxSSHKeyNotFound( f'SSH pkey file {ssh_key_file} not found, use --ssh-key' ) with open(ssh_key_file) as key_fd: ssh_key = key_fd.read().split() key_type = ssh_key[0] key_value = ssh_key[1] vm_append.append(f'ssh_key=_{key_value}_') vm_append.append(f'ssh_key_type=_{key_type}_') user = pwd.getpwuid(os.geteuid()).pw_name vm_append.append( 'host_kiwidescription=_{0}_'.format(f'{user}@localhost:{desc}') ) vm_append.append( 'host_kiwibundle=_{0}_'.format(f'{user}@localhost:{target_dir}') ) if custom_shared_path: vm_append.append( 'host_custompath=_{0}_'.format( f'{user}@localhost:{custom_shared_path}' ) ) vm_append.append('sharing_backend=_{0}_'.format(self.sharing_backend)) vm_machine = [] if self.machine: vm_machine.append('-machine') vm_machine.append(self.machine) if self.accel: vm_machine.append('-accel') vm_machine.append('accel=kvm') if len(self.ssh_port) > 0: if not self.ssh_port.isdigit(): raise KiwiBoxSSHPortInvalid( f'Invalid SSH port: {self.ssh_port} ' '(must be a positive integer)' ) BoxBuildDefaults.box_ssh_port_forwarded_to_host = int(self.ssh_port) vm_machine.append('-cpu') vm_machine.append(self.cpu) vm_memory = format(self.ram or vm_setup.ram) qemu_binary = self._find_qemu_call_binary() if not qemu_binary: raise KiwiBoxQEMUBinaryNotFound( f'No QEMU binary for {self.arch} found' ) vm_run = ( [qemu_binary, '-m', vm_memory] + vm_machine + [] + BoxBuildDefaults.get_qemu_generic_setup() + [ '-kernel', vm_setup.kernel, '-append', '"{0}"'.format(' '.join(vm_append)), ] + BoxBuildDefaults.get_qemu_storage_setup(vm_setup.system, snapshot) + BoxBuildDefaults.get_qemu_network_setup() + BoxBuildDefaults.get_qemu_console_setup() + BoxBuildDefaults.get_qemu_shared_path_setup( 0, desc, 'kiwidescription', self.sharing_backend ) + BoxBuildDefaults.get_qemu_shared_path_setup( 1, target_dir, 'kiwibundle', self.sharing_backend ) ) if custom_shared_path: vm_run += BoxBuildDefaults.get_qemu_shared_path_setup( 2, custom_shared_path, 'custompath', self.sharing_backend ) if self.sharing_backend == 'virtiofs': vm_run += [ '-object', '{0},id=mem,size={1},mem-path={2},share=on'.format( 'memory-backend-file', vm_memory, '/dev/shm' ), '-numa', 'node,memdev=mem', ] if vm_setup.initrd: vm_run += ['-initrd', vm_setup.initrd] if vm_setup.smp: vm_run += ['-smp', format(self.smp or vm_setup.smp)] os.environ['TMPDIR'] = BoxBuildDefaults.get_local_box_cache_dir() log.debug('Set TMPDIR: {0}'.format(os.environ['TMPDIR'])) if self.sharing_backend == 'sshfs': log.debug('Initiating port forwarding') # delete eventual existing host key for localhost on # the kvm forwared box ssh port box_ssh_port_forwarded_to_host # this is required because the host ssh key changes # with every kvm box run. ssh identifies this as # a potential man-in-the-middle attack and will disable # port forwarding which is required though. Command.run( [ 'ssh-keygen', '-R', '[localhost]:{0}'.format( runtime.BoxBuildDefaults.box_ssh_port_forwarded_to_host ), ], raise_on_error=False, ) # remote forward the host ssh port(22) into the box # at port HOST_SSH_PORT_FORWARDED_TO_BOX using the kvm forwarded # box ssh port box_ssh_port_forwarded_to_host. This action only # completes successfully when the box has started up and is # ready to operate through ssh ssh_forward_thread = Thread( target=self._forward_host_ssh_to_guest, args=() ) ssh_forward_thread.start() log.debug('Calling Qemu: {0}'.format(vm_run)) os.system(' '.join(vm_run)) for virtiofsd_process in runtime.VIRTIOFSD_PROCESS_LIST: virtiofsd_process.terminate() exit_code_file = os.sep.join( [target_dir, BoxBuildDefaults.result_exitcode_name] ) build_log_file = os.sep.join( [target_dir, BoxBuildDefaults.result_log_name] ) self.kiwi_exit = 0 if os.path.exists(exit_code_file): with open(exit_code_file) as exit_code: self.kiwi_exit = int(exit_code.readline()) if self.kiwi_exit != 0: raise KiwiBoxBuildError( f'Box build failed. Find build log at: {build_log_file!r}' ) log.info(f'Box build done. Find build log at: {build_log_file!r}') def _pop_arg_param(self, arg: str) -> str: arg_index = self.kiwi_build_command.index(arg) arg_value = '' if arg_index: arg_value = self.kiwi_build_command[arg_index + 1] del self.kiwi_build_command[arg_index + 1] del self.kiwi_build_command[arg_index] return arg_value def _find_qemu_call_binary(self) -> str: qemu_by_system = Path.which(f'qemu-system-{self.arch}') if qemu_by_system: return qemu_by_system if self.arch == 'x86_64': return Path.which('qemu-kvm') or '' return '' def _forward_host_ssh_to_guest(self) -> None: while self.kiwi_exit is None: try: Command.run( [ 'ssh', '-NT', '-o', 'StrictHostKeyChecking=no', 'root@localhost', '-p', format( runtime.BoxBuildDefaults.box_ssh_port_forwarded_to_host ), '-R', '{0}:localhost:22'.format( runtime.HOST_SSH_PORT_FORWARDED_TO_BOX ), ] ) except Exception as issue: log.debug(issue) time.sleep(2) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/box_config.py0000644000000000000000000000652215273675667014772 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import platform from typing import List, Dict # project from kiwi.boxbuild.boxes_config import BoxesConfig from kiwi.exceptions import KiwiBoxNameError, KiwiBoxArchNotFoundError class BoxConfig: """ **Implements reading of box configuration:** :param str boxname: name of the box from kiwi_boxed_plugin.yml :param str arch: arch name for box """ def __init__(self, boxname: str, arch: str = '') -> None: self.arch = arch or platform.machine() boxes_config = BoxesConfig() self.box_config = self._get_box_config( boxes_config.get_config(), boxname ) if not self.box_config: raise KiwiBoxNameError('Box: {0} not found'.format(boxname)) self.box_arch_config = self._get_box_arch_config( self.box_config, self.arch ) def get_box_arch(self) -> str: return self.arch def get_box_memory_mbytes(self) -> str: return self.box_config.get('mem_mb') or '' def get_box_processors(self) -> str: return self.box_config.get('processors') or '' def get_box_console(self) -> str: return self.box_config.get('console') or '' def get_box_kernel_cmdline(self) -> str: return ' '.join(self.box_arch_config.get('cmdline') or '') def get_box_source(self) -> str: return self.box_arch_config.get('source') or '' def get_box_container(self) -> str: return self.box_arch_config.get('container') or '' def get_box_packages_file(self) -> str: return self.box_arch_config.get('packages_file') or '' def get_box_packages_shasum_file(self) -> str: packages_file = self.box_arch_config.get('packages_file') or '' if packages_file: packages_file += '.sha256' return packages_file def get_box_files(self) -> List[str]: source_files = [] for vm_file in self.box_arch_config.get('boxfiles') or []: source_files.append(vm_file) source_files.append(f'{vm_file}.sha256') return source_files def use_initrd(self) -> bool: return bool(self.box_arch_config.get('use_initrd')) def _get_box_config(self, boxes_config: List[Dict], name: str) -> Dict: for box in boxes_config: if box.get('name') == name: return box return {} def _get_box_arch_config(self, box_config: Dict, arch: str) -> Dict: for box_arch in box_config.get('arch') or []: if box_arch.get('name') == arch: return box_arch raise KiwiBoxArchNotFoundError( f'No box configuration found for architecture: {arch}' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/box_container_build.py0000644000000000000000000001346015273675667016665 0ustar00# Copyright (c) 2025 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging import platform from tempfile import NamedTemporaryFile from typing import List, Optional # project from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.boxbuild.defaults import BoxBuildDefaults from kiwi.boxbuild.box_download import BoxDownload from kiwi.exceptions import KiwiBoxBuildError log = logging.getLogger('kiwi') class BoxContainerBuild: """ **Implements boxbuild command for container based build** Implements an interface to run a kiwi build box using the podman container engine :param str boxname: name of the box from kiwi_boxed_plugin.yml :param str arch: arch name for box """ def __init__(self, boxname: str, arch: str = '') -> None: self.kiwi_cache = f'/{Defaults.get_shared_cache_location()}' self.arch = arch or platform.machine() self.box = BoxDownload(boxname, arch) self.kiwi_exit: Optional[int] = None def run( self, kiwi_build_command: List[str], keep_open: bool = False, kiwi_version: Optional[str] = None, custom_shared_path: Optional[str] = None, ) -> None: """ Start the build process in a container :param list kiwi_build_command: kiwi build command line Example: .. code:: python [ '--type', 'oem', 'system', 'build', '--description', 'some/description', '--target-dir', 'some/target-dir' ] :param bool keep_open: keep container running True|False :param str kiwi_version: pip install this kiwi version :param str custom_shared_path: make this path available in the container """ self.kiwi_build_command = kiwi_build_command desc = os.path.abspath(self._pop_arg_param('--description')) target_dir = os.path.abspath(self._pop_arg_param('--target-dir')) BoxBuildDefaults.create_build_target_dir(target_dir) container_name = self.box.fetch_container() container_cmdline = [ 'kiwi="{0}"'.format(' '.join(self.kiwi_build_command)) ] if keep_open: container_cmdline.append('kiwi-no-halt') if kiwi_version: container_cmdline.append('kiwi_version=_{0}_'.format(kiwi_version)) if custom_shared_path: custom_shared_path = os.path.abspath(custom_shared_path) if not os.path.exists(custom_shared_path): raise KiwiBoxBuildError( f'Custom share path {custom_shared_path} does not exist' ) container_cmdline.append( 'custom_mount=_{0}_'.format(custom_shared_path) ) if not os.path.exists(desc): raise KiwiBoxBuildError(f'Image description {desc} does not exist') self.cmdline = NamedTemporaryFile(prefix='cmdline') with open(self.cmdline.name, 'w') as cmdline: cmdline.write('{0}'.format(' '.join(container_cmdline))) cmdline.write(os.linesep) if not os.path.isdir(self.kiwi_cache): Command.run(['sudo', 'mkdir', '-p', self.kiwi_cache]) container_run = [ 'sudo', 'podman', 'run', '--rm', '-ti', '--privileged', '--net', 'host', '--cap-add', 'AUDIT_WRITE', '--cap-add', 'AUDIT_CONTROL', '--cap-add', 'CAP_MKNOD', '--cap-add', 'CAP_SYS_ADMIN', '--volume', '/var/cache/kiwi:/var/cache/kiwi', '--volume', f'{self.cmdline.name}:/container.cmdline', '--volume', f'{target_dir}:/bundle', '--volume', f'{desc}:/description', '--volume', '/dev:/dev', ] if custom_shared_path: container_run.append('--volume') container_run.append(f'{custom_shared_path}:{custom_shared_path}') container_run.append(container_name) log.debug('Calling podman: {0}'.format(container_run)) os.system(' '.join(container_run)) exit_code_file = os.sep.join( [target_dir, BoxBuildDefaults.result_exitcode_name] ) build_log_file = os.sep.join( [target_dir, BoxBuildDefaults.result_log_name] ) self.kiwi_exit = 0 if os.path.exists(exit_code_file): with open(exit_code_file) as exit_code: self.kiwi_exit = int(exit_code.readline()) if self.kiwi_exit != 0: raise KiwiBoxBuildError( f'Box build failed. Find build log at: {build_log_file!r}' ) log.info(f'Box build done. Find build log at: {build_log_file!r}') def _pop_arg_param(self, arg: str) -> str: arg_index = self.kiwi_build_command.index(arg) arg_value = '' if arg_index: arg_value = self.kiwi_build_command[arg_index + 1] del self.kiwi_build_command[arg_index + 1] del self.kiwi_build_command[arg_index] return arg_value ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/box_download.py0000644000000000000000000001764015273675667015337 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import platform import logging from typing import NamedTuple # project from kiwi.command import Command from kiwi.utils.checksum import Checksum from kiwi.utils.dir_files import DirFiles from kiwi.utils.fetch_files import FetchFiles from kiwi.solver.repository import SolverRepository from kiwi.system.uri import Uri from kiwi.path import Path from kiwi.boxbuild.box_config import BoxConfig from kiwi.boxbuild.defaults import BoxBuildDefaults from kiwi.exceptions import KiwiBoxChecksumError vm_setup_type = NamedTuple( 'vm_setup_type', [ ('system', str), ('kernel', str), ('initrd', str), ('append', str), ('console', str), ('ram', str), ('smp', str), ], ) log = logging.getLogger('kiwi') class BoxDownload: """ **Implements download of box file(s)** Reads the box configuration file and provides an interface to download the box files according to the configuration :param string boxname: name of the box from kiwi_boxed_plugin.yml :param string arch: arch name for box """ def __init__(self, boxname: str, arch: str = '') -> None: self.arch = arch or platform.machine() self.box_config = BoxConfig(boxname, arch) self.box_dir = os.sep.join( [BoxBuildDefaults.get_local_box_cache_dir(), boxname] ) self.box_stage = DirFiles(self.box_dir) self.system = '' self.kernel = '' self.initrd = '' Path.create(self.box_dir) def fetch_container(self) -> str: """ Download container box from the open build service """ container_source = self.box_config.get_box_container() if container_source: container_pull = ['sudo', 'podman', 'pull', container_source] os.system(' '.join(container_pull)) return container_source def fetch( self, update_check: bool = True, snapshot: bool = True ) -> vm_setup_type: """ Download box from the open build service :param bool update_check: check for box updates True|False :param bool snapshot: box is used in snapshot mode True|False """ fetcher = FetchFiles() download = update_check repo_source = self.box_config.get_box_source() if repo_source: repo = SolverRepository.new(Uri(repo_source, 'rpm-md')) packages_file = self.box_config.get_box_packages_file() packages_shasum_file = ( self.box_config.get_box_packages_shasum_file() ) if update_check and packages_file and packages_shasum_file: local_packages_file = os.sep.join([self.box_dir, packages_file]) local_packages_shasum_file = os.sep.join( [self.box_dir, packages_shasum_file] ) local_packages_file_tmp = self.box_stage.register( local_packages_file ) local_packages_shasum_file_tmp = self.box_stage.register( local_packages_shasum_file ) repo.download_from_repository( packages_file, local_packages_file_tmp ) checksum = Checksum(local_packages_file_tmp) shasum = checksum.sha256() if checksum.matches(shasum, local_packages_shasum_file): download = False self.box_stage.deregister(local_packages_shasum_file) else: self._create_packages_checksum( local_packages_shasum_file_tmp, shasum ) for box_file in self.box_config.get_box_files(): local_box_file = os.sep.join([self.box_dir, box_file]) if not os.path.exists(local_box_file): download = True if download: log.info('Downloading {0}'.format(box_file)) local_box_file_tmp = self.box_stage.register(local_box_file) box_file_link = os.sep.join( [repo._get_mime_typed_uri(), box_file] ) fetcher.wget(url=box_file_link, filename=local_box_file_tmp) if download: self.box_stage.commit() for box_file in self.box_config.get_box_files(): local_box_file = os.sep.join([self.box_dir, box_file]) if box_file.endswith('.qcow2'): if ( update_check and snapshot and not self._checksum_ok(local_box_file) ): raise KiwiBoxChecksumError( f'Checksum failed for {local_box_file}' ) self.system = local_box_file if box_file.endswith('.tar.xz'): if update_check and not self._checksum_ok(local_box_file): raise KiwiBoxChecksumError( f'Checksum failed for {local_box_file}' ) self.kernel = self._extract_kernel_from_tarball( local_box_file ) if self.box_config.use_initrd(): self.initrd = self._extract_initrd_from_tarball( local_box_file ) return vm_setup_type( system=self.system, kernel=self.kernel, initrd=self.initrd, append='console={0} {1}'.format( self.box_config.get_box_console(), self.box_config.get_box_kernel_cmdline(), ), console=self.box_config.get_box_console(), ram=self.box_config.get_box_memory_mbytes(), smp=self.box_config.get_box_processors(), ) def _checksum_ok(self, filename: str) -> bool: checksum = Checksum(filename) shasum = checksum.sha256() sumfile = f'{filename}.sha256' log.info('Checksum test {0} -> {1}'.format(filename, sumfile)) return checksum.matches(shasum, sumfile) def _create_packages_checksum(self, filename: str, shasum: str) -> None: with open(filename, 'w') as sha_file: sha_file.write(shasum) def _extract_kernel_from_tarball(self, tarfile: str) -> str: Command.run( [ 'tar', '-C', self.box_dir, '--transform', f's/.*/kernel.{self.arch}/', '--wildcards', '-xf', tarfile, '*.kernel', ] ) return os.sep.join([self.box_dir, f'kernel.{self.arch}']) def _extract_initrd_from_tarball(self, tarfile: str) -> str: Command.run( [ 'tar', '-C', self.box_dir, '--transform', f's/.*/initrd.{self.arch}/', '--wildcards', '-xf', tarfile, '*.initrd', ] ) return os.sep.join([self.box_dir, f'initrd.{self.arch}']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/boxes_config.py0000644000000000000000000000457215273675667015325 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging import yaml from cerberus import Validator from typing import List, Dict # project from kiwi.boxbuild.boxes_config_schema import schema from kiwi.boxbuild.defaults import BoxBuildDefaults from kiwi.exceptions import KiwiBoxConfigError log = logging.getLogger('kiwi') class BoxesConfig: """ **Implements reading of the boxes config file:** kiwi_boxed_plugin.yml The KIWI box configuration file is a yaml formatted file containing information about available virtual disk images and containers usable as build boxes """ def __init__(self) -> None: self.config_data = {} config_file = BoxBuildDefaults.get_box_config_file() log.info('Reading boxes config file: {0}'.format(config_file)) try: with open(config_file, 'r') as config: self.config_data = yaml.safe_load(config) except Exception as issue: raise KiwiBoxConfigError(issue) validator = Validator(schema) validator.validate(self.config_data, schema) if validator.errors: raise KiwiBoxConfigError( 'Failed to validate {0}: {1}'.format( config_file, validator.errors ) ) def get_config(self) -> List[Dict]: """ Return config data dictionary """ return self.config_data.get('box') or [] def dump_config(self) -> str: """ Return config dump as pretty string for the console """ config_dump = '' box_dict = self.config_data.get('box') or {} if box_dict: config_dump = yaml.dump(box_dict) return config_dump ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/boxes_config_schema.py0000644000000000000000000000635215273675667016643 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # schema = { 'box': { 'required': True, 'type': 'list', 'schema': { 'type': 'dict', 'schema': { 'name': {'type': 'string', 'required': True, 'empty': False}, 'mem_mb': {'type': 'string', 'required': True, 'empty': False}, 'processors': { 'type': 'number', 'required': False, 'empty': False, }, 'console': {'type': 'string', 'required': True, 'empty': False}, 'arch': { 'required': True, 'type': 'list', 'schema': { 'type': 'dict', 'schema': { 'name': { 'type': 'string', 'allowed': [ 'x86_64', 's390x', 'aarch64', 'ppc64', 'ppc64le', ], 'required': True, 'empty': False, }, 'cmdline': { 'type': 'list', 'required': True, 'nullable': False, }, 'source': { 'type': 'string', 'required': True, 'empty': False, }, 'packages_file': { 'type': 'string', 'required': True, 'empty': False, }, 'boxfiles': { 'type': 'list', 'required': True, 'nullable': False, }, 'container': { 'type': 'string', 'required': True, 'nullable': False, }, 'use_initrd': {'type': 'boolean', 'required': True}, }, }, }, }, }, } } ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/boxbuild/defaults.py0000644000000000000000000001552115273675667014463 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import pathlib import subprocess from subprocess import Popen from typing import List, Optional # project from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.exceptions import KiwiBoxVirtioFsError, KiwiBoxTargetPathError VIRTIOFSD_PROCESS_LIST: List[Popen[bytes]] = [] HOST_SSH_PORT_FORWARDED_TO_BOX = 10000 class BoxBuildDefaults: """ **Implements default values for the boxbuild command** Provides static methods for default values and state information """ box_ssh_port_forwarded_to_host = 10022 result_log_name = 'result.log' result_exitcode_name = 'result.code' @staticmethod def get_box_config_file() -> str: """ Config file name: `kiwi_boxed_plugin.yml` Locations are searched in this order: 1. ENV variable: $KIWI_BOXED_PLUGIN_CFG full path to file, name is freely selectable 2. $PWD/kiwi_boxed_plugin.yml 3. $HOME/.config/kiwi/kiwi_boxed_plugin.yml 4. /etc/kiwi_boxed_plugin.yml 5. Resource (default config, coming with kiwi) """ config_name = 'kiwi_boxed_plugin.yml' # 1. config_path_env: str = os.environ.get('KIWI_BOXED_PLUGIN_CFG') or '' if config_path_env and os.path.exists(config_path_env): return config_path_env # 2. config_path_pwd = os.path.abspath(config_name) if os.path.exists(config_path_pwd): return config_path_pwd # 3. config_path_home: pathlib.Path = pathlib.Path.home().joinpath( f'.config/kiwi/{config_name}' ) if config_path_home.exists(): return config_path_home.as_posix() # 4. config_path_system = f'/etc/{config_name}' if os.path.exists(config_path_system): return config_path_system # 5. return Defaults.project_file(f'config/{config_name}') @staticmethod def get_local_box_cache_dir() -> str: local_box_cache_dir_env: Optional[str] = os.environ.get( 'KIWI_BOXED_CACHE_DIR' ) if local_box_cache_dir_env and os.path.isdir(local_box_cache_dir_env): return local_box_cache_dir_env return f'{os.environ.get("HOME")}/.kiwi_boxes' @staticmethod def get_qemu_generic_setup() -> List[str]: return ['-nographic', '-nodefaults', '-snapshot'] @staticmethod def get_qemu_network_setup() -> List[str]: return [ '-nic', 'user,model=virtio,hostfwd=tcp::{0}-:22'.format( BoxBuildDefaults.box_ssh_port_forwarded_to_host ), ] @staticmethod def get_qemu_shared_path_setup( index: int, path: str, mount_tag: str, sharing_backend: str = '9p' ) -> List[str]: shared_setup: List[str] = [] if sharing_backend == '9p': shared_setup = BoxBuildDefaults.get_qemu_shared_path_setup_9p( index, path, mount_tag ) if sharing_backend == 'virtiofs': shared_setup = BoxBuildDefaults.get_qemu_shared_path_setup_virtiofs( index, path, mount_tag ) return shared_setup @staticmethod def get_qemu_shared_path_setup_9p( index: int, path: str, mount_tag: str ) -> List[str]: return [ '-fsdev', 'local,security_model=mapped,id=fsdev{0},path={1}'.format( index, path ), '-device', 'virtio-9p-pci,id=fs{0},fsdev=fsdev{0},mount_tag={1}'.format( index, mount_tag ), ] @staticmethod def get_qemu_shared_path_setup_virtiofs( index: int, path: str, mount_tag: str ) -> List[str]: virtiofsd_lookup_paths = { 'PATH': '/usr/libexec:/usr/lib/virtiofsd:/usr/lib/qemu/virtiofsd' } virtiofsd = Path.which('virtiofsd', virtiofsd_lookup_paths) if not virtiofsd: raise KiwiBoxVirtioFsError( 'virtiofsd not found in: {0}'.format(virtiofsd_lookup_paths) ) try: virtiofsd_process = subprocess.Popen( [ virtiofsd, '--socket-path=/tmp/vhostqemu_{0}'.format(index), '--shared-dir', os.path.abspath(path), '--sandbox', 'namespace', '--cache', 'always', '--allow-direct-io', '--posix-acl', '--xattr', ], close_fds=True, ) except Exception as issue: raise KiwiBoxVirtioFsError( 'Failed to start virtiofsd: {0}'.format(issue) ) VIRTIOFSD_PROCESS_LIST.append(virtiofsd_process) return [ '-chardev', 'socket,id=char{0},path=/tmp/vhostqemu_{0}'.format(index), '-device', 'vhost-user-fs-pci,queue-size=1024,chardev=char{0},tag={1}'.format( index, mount_tag ), ] @staticmethod def get_qemu_console_setup() -> List[str]: return [ '-device', 'virtio-serial', '-chardev', 'stdio,id=virtiocon0', '-device', 'virtconsole,chardev=virtiocon0', ] @staticmethod def get_qemu_storage_setup( image_file: str, snapshot: bool = True ) -> List[str]: return [ '-drive', 'file={0},if=virtio,driver=qcow2,cache=off,snapshot={1}'.format( image_file, 'on' if snapshot else 'off' ), ] @staticmethod def create_build_target_dir(target_dir: str) -> None: try: pathlib.Path(target_dir).mkdir(parents=True, exist_ok=True) build_log_file = os.path.join( target_dir, BoxBuildDefaults.result_log_name ) pathlib.Path(build_log_file).touch() except Exception as issue: raise KiwiBoxTargetPathError( f'Failed to create/setup target path {target_dir}: {issue}' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7427986 kiwi-11.1.8/kiwi/builder/__init__.py0000644000000000000000000000633215273675667014231 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project import importlib from typing import TYPE_CHECKING, Dict, Optional, Union from abc import ABCMeta, abstractmethod from kiwi.defaults import Defaults from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiRequestedTypeError if TYPE_CHECKING: # pragma: nocover from kiwi.builder.archive import ArchiveBuilder from kiwi.builder.container import ContainerBuilder from kiwi.builder.disk import DiskBuilder from kiwi.builder.enclave import EnclaveBuilder from kiwi.builder.filesystem import FileSystemBuilder from kiwi.builder.kis import KisBuilder from kiwi.builder.live import LiveImageBuilder class ImageBuilder(metaclass=ABCMeta): """ Image builder factory """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ) -> Union[ 'FileSystemBuilder', 'DiskBuilder', 'LiveImageBuilder', 'KisBuilder', 'EnclaveBuilder', 'ArchiveBuilder', 'ContainerBuilder', ]: image_type = xml_state.get_build_type_name() if image_type in Defaults.get_filesystem_image_types(): name_token = ('filesystem', 'FileSystemBuilder') elif image_type in Defaults.get_disk_image_types(): name_token = ('disk', 'DiskBuilder') elif image_type in Defaults.get_live_image_types(): name_token = ('live', 'LiveImageBuilder') elif image_type in Defaults.get_kis_image_types(): name_token = ('kis', 'KisBuilder') elif image_type in Defaults.get_enclaves_image_types(): name_token = ('enclave', 'EnclaveBuilder') elif image_type in Defaults.get_archive_image_types(): name_token = ('archive', 'ArchiveBuilder') elif image_type in Defaults.get_container_image_types(): name_token = ('container', 'ContainerBuilder') else: name_token = ('None', 'None') try: builder_namespace, builder_name = name_token builder = importlib.import_module( 'kiwi.builder.{0}'.format(builder_namespace) ) return builder.__dict__[builder_name]( xml_state, target_dir, root_dir, custom_args ) except Exception as issue: raise KiwiRequestedTypeError( f'Requested image type {image_type} not supported: {issue}' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/archive.py0000644000000000000000000001273515273675667014117 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import Dict, Optional # project from kiwi.defaults import Defaults from kiwi.archive.tar import ArchiveTar from kiwi.archive.cpio import ArchiveCpio from kiwi.system.setup import SystemSetup from kiwi.system.result import Result from kiwi.runtime_config import RuntimeConfig from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiArchiveSetupError log = logging.getLogger('kiwi') class ArchiveBuilder: """ **Root archive image builder** :param object xml_state: Instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state self.requested_archive_type = xml_state.get_build_type_name() self.bundle_format = xml_state.get_build_type_bundle_format() self.result = Result(xml_state) self.system_setup = SystemSetup( xml_state=xml_state, root_dir=self.root_dir ) self.filename = '' self.xz_options = ( custom_args['xz_options'] if custom_args and 'xz_options' in custom_args else None ) self.runtime_config = RuntimeConfig() def create(self) -> Result: """ Create a root archive tarball Build a simple XZ compressed root tarball from the image root tree Image types which triggers this builder are: * image="tbz" * image="cpio" :return: result :rtype: instance of :class:`Result` """ supported_archives = Defaults.get_archive_image_types() if self.requested_archive_type not in supported_archives: raise KiwiArchiveSetupError( f'Unknown archive type: {self.requested_archive_type}' ) if self.requested_archive_type == 'tbz': log.info('Creating XZ compressed tar archive') self.filename = self._target_file_for('tar.xz') ArchiveTar(self._target_file_for('tar')).create_xz_compressed( self.root_dir, xz_options=self.xz_options, exclude=Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ), ) if self.requested_archive_type == 'cpio': log.info('Creating CPIO archive') self.filename = self._target_file_for('cpio') ArchiveCpio(self.filename).create( self.root_dir, exclude=Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ), ) if self.filename: Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.filename ) if self.bundle_format: self.result.add_bundle_format(self.bundle_format) self.result.add( key='root_archive', filename=self.filename, use_for_bundle=True, compress=False, shasum=True, ) self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes( self.target_dir ), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result def _target_file_for(self, suffix: str) -> str: return ''.join( [ self.target_dir, '/', self.xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + self.xml_state.get_image_version(), '.', suffix, ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/container.py0000644000000000000000000001672315273675667014461 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging import os from typing import Dict, Optional # project from kiwi.container import ContainerImage from kiwi.container.setup import ContainerSetup from kiwi.system.setup import SystemSetup from kiwi.system.result import Result from kiwi.utils.checksum import Checksum from kiwi.defaults import Defaults from kiwi.exceptions import KiwiContainerBuilderError from kiwi.runtime_config import RuntimeConfig from kiwi.xml_state import XMLState log = logging.getLogger('kiwi') class ContainerBuilder: """ **Container image builder** :param object xml_state: Instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.runtime_config = RuntimeConfig() self.custom_args = custom_args or {} self.root_dir = root_dir self.target_dir = target_dir self.bundle_format = xml_state.get_build_type_bundle_format() self.container_config = xml_state.get_container_config() self.requested_container_type = xml_state.get_build_type_name() self.delta_root = xml_state.build_type.get_delta_root() self.base_image = None self.base_image_sha = None self.ensure_empty_tmpdirs = True self.container_config['xz_options'] = self.custom_args.get('xz_options') self.container_config['metadata_path'] = ( xml_state.build_type.get_metadata_path() ) if xml_state.get_derived_from_image_uri() and not self.delta_root: # The base image(all derived imports) is expected to be unpacked # by the kiwi prepare step and stored inside of the root_dir/image # directory. In addition a sha file of the image is expected too self.base_image = Defaults.get_imported_root_image(self.root_dir) self.base_image_sha = '{}{}'.format( self.base_image, self.runtime_config.get_checksum_handler( self.base_image ).suffix, ) if not os.path.exists(self.base_image): raise KiwiContainerBuilderError( 'Unpacked Base image {0} not found'.format(self.base_image) ) if not os.path.exists(self.base_image_sha): raise KiwiContainerBuilderError( 'Base image shasum file {0} not found'.format( self.base_image_sha ) ) if xml_state.build_type.get_ensure_empty_tmpdirs() is False: self.ensure_empty_tmpdirs = False self.system_setup = SystemSetup( xml_state=xml_state, root_dir=self.root_dir ) self.special_needs = ['appx', 'wsl'] self.filename = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + xml_state.get_image_version(), '.', self.requested_container_type, ( '.tar' if self.requested_container_type not in self.special_needs else '' ), ] ) self.result = Result(xml_state) def create(self) -> Result: """ Builds a container image which is usually a data archive including container specific metadata. Image types which triggers this builder are: * image="docker" * image="oci" * image="appx" * image="wsl" :return: result :rtype: instance of :class:`Result` """ if not self.base_image: log.info('Setting up %s container', self.requested_container_type) container_setup = ContainerSetup.new( self.requested_container_type, self.root_dir, self.container_config, ) container_setup.setup() else: shasum = self.runtime_config.get_checksum_handler( source_filename=self.base_image ) checksum = Checksum(self.base_image) # the sha file is set along with the base image assert self.base_image_sha is not None if not checksum.matches(shasum.digest(), self.base_image_sha): raise KiwiContainerBuilderError( 'base image file {0} checksum validation failed'.format( self.base_image ) ) log.info('--> Creating container image') container_image = ContainerImage.new( self.requested_container_type, self.root_dir, self.container_config ) self.filename = container_image.create( self.filename, self.base_image or '', self.ensure_empty_tmpdirs, ( self.runtime_config.get_container_compression() # appx containers already contains a compressed root # wsl containers recommends gzip and we default to it if self.requested_container_type not in self.special_needs else False ), ) Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.filename ) if self.bundle_format: self.result.add_bundle_format(self.bundle_format) self.result.add( key='container', filename=self.filename, use_for_bundle=True, compress=False, shasum=True, ) if not self.delta_root: self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes( self.target_dir ), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/disk.py0000644000000000000000000025754715273675667013444 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from contextlib import ExitStack import os import logging from typing import Dict, List, Optional, Tuple, TypedDict, Union # project import kiwi.defaults as defaults from kiwi.path import Path from kiwi.mount_manager import MountManager from kiwi.utils.veritysetup import VeritySetup from kiwi.utils.temporary import Temporary from kiwi.system.mount import ImageSystem from kiwi.storage.disk import ptable_entry_type from kiwi.defaults import Defaults from kiwi.filesystem.base import FileSystemBase from kiwi.bootloader.config import create_boot_loader_config from kiwi.bootloader.config.base import BootLoaderConfigBase from kiwi.bootloader.install import BootLoaderInstall from kiwi.system.identifier import SystemIdentifier from kiwi.boot.image import BootImage from kiwi.boot.image.base import BootImageBase from kiwi.storage.setup import DiskSetup from kiwi.storage.loop_device import LoopDevice from kiwi.storage.clone_device import CloneDevice from kiwi.firmware import FirmWare from kiwi.storage.disk import Disk from kiwi.storage.raid_device import RaidDevice from kiwi.storage.luks_device import LuksDevice from kiwi.storage.integrity_device import ( IntegrityDevice, integrity_credentials_type, ) from kiwi.storage.device_provider import DeviceProvider from kiwi.filesystem import FileSystem from kiwi.volume_manager import VolumeManager from kiwi.volume_manager.base import VolumeManagerBase from kiwi.command import Command from kiwi.system.setup import SystemSetup from kiwi.builder.install import InstallImageBuilder from kiwi.system.kernel import Kernel from kiwi.storage.subformat import DiskFormat from kiwi.system.result import Result from kiwi.utils.block import BlockID from kiwi.utils.fstab import Fstab from kiwi.runtime_config import RuntimeConfig from kiwi.partitioner import Partitioner from kiwi.xml_state import XMLState from kiwi.exceptions import ( KiwiDiskBootImageError, KiwiInstallMediaError, KiwiVolumeManagerSetupError, ) log = logging.getLogger('kiwi') class StorageMap(TypedDict): system: Optional[Union[FileSystemBase, VolumeManagerBase]] system_boot: Optional[FileSystemBase] system_efi: Optional[FileSystemBase] system_spare: Optional[FileSystemBase] system_custom_parts: Dict[str, FileSystemBase] luks_root: Optional[LuksDevice] raid_root: Optional[RaidDevice] integrity_root: Optional[IntegrityDevice] class DiskBuilder: """ **Disk image builder** :param object xml_state: Instance of :class:`XMLState` :param str target_dir: Target directory path name :param str root_dir: Root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: * signing_keys: list of package signing keys * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.arch = Defaults.get_platform_name() self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state self.cmdline = xml_state.build_type.get_kernelcmdline() or '' self.spare_part_mbsize = xml_state.get_build_type_spare_part_size() self.spare_part_fs = xml_state.build_type.get_spare_part_fs() self.spare_part_is_last = xml_state.build_type.get_spare_part_is_last() self.spare_part_mountpoint = ( xml_state.build_type.get_spare_part_mountpoint() ) self.persistency_type = xml_state.build_type.get_devicepersistency() self.root_filesystem_is_overlay = xml_state.build_type.get_overlayroot() self.root_filesystem_read_only_type = ( xml_state.build_type.get_overlayroot_readonly_filesystem() ) self.root_filesystem_has_write_partition = ( xml_state.build_type.get_overlayroot_write_partition() ) self.root_filesystem_read_only_partsize = ( xml_state.build_type.get_overlayroot_readonly_partsize() ) self.veritysetup: Optional[VeritySetup] = None self.root_filesystem_verity_blocks = ( xml_state.build_type.get_verity_blocks() ) self.root_filesystem_embed_verity_metadata = ( xml_state.build_type.get_embed_verity_metadata() ) self.dosparttable_extended_layout = ( xml_state.build_type.get_dosparttable_extended_layout() ) self.apply_dps_codes = xml_state.build_type.get_apply_dps_codes() self.boot_clone_count = ( int(xml_state.build_type.get_boot_clone()) if xml_state.build_type.get_boot_clone() else 0 ) self.root_clone_count = ( int(xml_state.build_type.get_root_clone()) if xml_state.build_type.get_root_clone() else 0 ) self.custom_root_mount_args = xml_state.get_fs_mount_option_list() self.custom_efi_mount_args = xml_state.get_efi_mount_option_list() self.custom_root_creation_args = xml_state.get_fs_create_option_list() self.custom_root_readonly_creation_args = ( xml_state.get_fs_readonly_create_option_list() ) self.build_type_name = xml_state.get_build_type_name() self.image_format = xml_state.build_type.get_format() self.install_iso = xml_state.build_type.get_installiso() self.install_stick = xml_state.build_type.get_installstick() self.install_pxe = xml_state.build_type.get_installpxe() self.blocksize = xml_state.build_type.get_target_blocksize() self.volume_manager_name = xml_state.get_volume_management() self.volumes = xml_state.get_volumes() self.custom_partitions = xml_state.get_partitions() self.volume_group_name = xml_state.get_volume_group_name() self.dracut_setup = xml_state.get_dracut_config('setup') self.dracut_add_modules = xml_state.get_dracut_config('add').modules self.dracut_omit_modules = xml_state.get_dracut_config('omit').modules self.dracut_add_drivers = xml_state.get_dracut_config('add').drivers self.dracut_omit_drivers = xml_state.get_dracut_config('omit').drivers # an initrd is created for the kernel of each flavor, the # empty flavor stands for the latest kernel self.kernel_flavors = ( xml_state.get_build_type_bootloader_kernel_flavors() or [''] ) self.mdraid = xml_state.build_type.get_mdraid() self.hybrid_mbr = xml_state.build_type.get_gpt_hybrid_mbr() self.force_mbr = xml_state.build_type.get_force_mbr() self.luks = xml_state.get_luks_credentials() self.use_disk_password = ( xml_state.get_build_type_bootloader_use_disk_password() ) self.host_key_certificates = xml_state.get_host_key_certificates() self.integrity = xml_state.build_type.get_standalone_integrity() self.integrity_legacy_hmac = ( xml_state.build_type.get_integrity_legacy_hmac() ) self.integrity_keyfile = xml_state.build_type.get_integrity_keyfile() self.integrity_key_description = ( xml_state.build_type.get_integrity_metadata_key_description() ) self.root_filesystem_embed_integrity_metadata = ( xml_state.build_type.get_embed_integrity_metadata() ) self.luks_format_options = xml_state.get_luks_format_options() self.luks_randomize = ( xml_state.build_type.get_luks_randomize() if xml_state.build_type.get_luks_randomize() is not None else True ) self.luks_os = xml_state.build_type.get_luksOS() self.xen_server = xml_state.is_xen_server() self.requested_filesystem = xml_state.build_type.get_filesystem() self.requested_boot_filesystem = ( xml_state.build_type.get_bootfilesystem() ) self.bootloader = xml_state.get_build_type_bootloader_name() self.initrd_system = xml_state.get_initrd_system() self.target_removable = xml_state.build_type.get_target_removable() self.root_filesystem_is_multipath = ( xml_state.get_oemconfig_oem_multipath_scan() ) self.btrfs_default_volume_requested = ( xml_state.btrfs_default_volume_requested() ) self.oem_systemsize = xml_state.get_oemconfig_oem_systemsize() self.oem_resize = xml_state.get_oemconfig_oem_resize() self.disk_resize_requested = xml_state.get_oemconfig_oem_resize() self.swap_mbytes = xml_state.get_oemconfig_swap_mbytes() self.disk_start_sector = xml_state.get_disk_start_sector() self.disk_setup = DiskSetup(xml_state, root_dir) self.unpartitioned_bytes = ( xml_state.get_build_type_unpartitioned_bytes() ) self.custom_args = custom_args self.signing_keys = None if custom_args and 'signing_keys' in custom_args: self.signing_keys = custom_args['signing_keys'] self.boot_image = BootImage.new( xml_state, target_dir, root_dir, signing_keys=self.signing_keys ) self.firmware = FirmWare(xml_state) self.system_setup = SystemSetup( xml_state=xml_state, root_dir=self.root_dir ) self.bundle_format = xml_state.get_build_type_bundle_format() self.diskname = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + self.arch, '-' + xml_state.get_image_version(), '.raw', ] ) self.boot_is_crypto = ( True if self.luks and not self.disk_setup.need_boot_partition() else False ) self.install_media = self._install_image_requested() self.fstab = Fstab() self.volume_manager_custom_parameters = { 'fs_mount_options': self.custom_root_mount_args, 'fs_create_options': self.custom_root_creation_args, 'root_label': self.disk_setup.get_root_label(), 'root_is_snapper_snapshot': self.xml_state.build_type.get_btrfs_root_is_snapper_snapshot(), 'root_is_readonly_snapshot': self.xml_state.build_type.get_btrfs_root_is_readonly_snapshot(), 'root_is_subvolume': self.xml_state.build_type.get_btrfs_root_is_subvolume(), 'btrfs_default_volume_requested': self.btrfs_default_volume_requested, 'quota_groups': self.xml_state.build_type.get_btrfs_quota_groups(), 'resize_on_boot': self.disk_resize_requested, } self.filesystem_custom_parameters = { 'mount_options': self.custom_root_mount_args, 'create_options': self.custom_root_creation_args, } self.need_root_filesystem = False if ( not self.root_filesystem_is_overlay or self.root_filesystem_has_write_partition is not False ): self.need_root_filesystem = True # result store self.result = Result(xml_state) self.runtime_config = RuntimeConfig() if not self.boot_image.has_initrd_support(): log.warning('Building without initrd support !') def create(self) -> Result: """ Build a bootable disk image and optional installation image The installation image is a bootable hybrid ISO image which embeds the disk image and an image installer Image types which triggers this builder are: * image="oem" :return: result :rtype: instance of :class:`Result` """ result = self.create_disk() result = self.create_install_media(result) self.append_unpartitioned_space() return self.create_disk_format(result) def _get_root_readonly_compression(self) -> Optional[str]: """ Compression setting for the overlay read-only root filesystem The read-only root filesystem type of an overlayroot image is selectable via the overlayroot_readonly_filesystem attribute (squashfs or erofs). Return the compression value that matches that type, so that both squashfscompression and erofscompression are honored. Before this, the overlay code path always read squashfscompression, which silently ignored erofscompression for an erofs overlay root. """ if self.root_filesystem_read_only_type == 'erofs': return self.xml_state.build_type.get_erofscompression() return self.xml_state.build_type.get_squashfscompression() def create_disk(self) -> Result: """ Build a bootable raw disk image :raises KiwiInstallMediaError: if install media is required and image type is not oem :raises KiwiVolumeManagerSetupError: root overlay at the same time volumes are defined is not supported :return: result :rtype: instance of :class:`Result` """ # initialize device dict self.storage_map: StorageMap = { 'system': None, 'system_boot': None, 'system_efi': None, 'system_spare': None, 'system_custom_parts': {}, 'luks_root': None, 'raid_root': None, 'integrity_root': None, } if self.install_media and self.build_type_name != 'oem': raise KiwiInstallMediaError( 'Install media requires oem type setup, got {0}'.format( self.build_type_name ) ) if self.root_filesystem_is_overlay and self.volume_manager_name: raise KiwiVolumeManagerSetupError( 'Volume management together with root overlay is not supported' ) # setup recovery archive, cleanup and create archive if requested self.system_setup.create_recovery_archive() # prepare initrd if self.boot_image.has_initrd_support(): log.info('Preparing boot system') self.boot_image.prepare() # precalculate needed disk size disksize_mbytes = self.disk_setup.get_disksize_mbytes( root_clone=self.root_clone_count, boot_clone=self.boot_clone_count ) # create the disk log.info('Creating raw disk image %s', self.diskname) with LoopDevice( self.diskname, disksize_mbytes, self.blocksize ) as loop_provider: loop_provider.create() # create the disk partitioner, still unmapped with self._disk_instance(loop_provider) as disk: # create disk partitions and instance device map device_map = self._build_and_map_disk_partitions( disk, disksize_mbytes ) # update device and disk id map if no root write partition if ( self.root_filesystem_is_overlay and self.root_filesystem_has_write_partition is False ): device_map['root'] = device_map['readonly'] disk.public_partition_id_map['kiwi_RootPart'] = ( disk.public_partition_id_map['kiwi_ROPart'] ) with ExitStack() as stack: if self.mdraid: # create raid on current root device raid_root = self._raid_instance(device_map) stack.push(raid_root) device_map = self._map_raid(device_map, disk, raid_root) if self.integrity: # create integrity on current root device integrity_root = self._integrity_instance(device_map) stack.push(integrity_root) device_map = self._map_integrity( device_map, integrity_root ) if self.luks is not None: # create luks on current root device luks_root = self._luks_instance(device_map) stack.push(luks_root) device_map = self._map_luks(device_map, luks_root) # create system layout for root system device_map = self._create_system_instance(device_map, stack) # build bootable disk self._build_main_system( stack, device_map, disk, self.storage_map['system'], self.storage_map['system_boot'], self.storage_map['system_efi'], self.storage_map['system_spare'], self.storage_map['system_custom_parts'] or {}, self.storage_map['luks_root'], self.storage_map['raid_root'], self.storage_map['integrity_root'], ) # store image bundle_format in result if self.bundle_format: self.result.add_bundle_format(self.bundle_format) # store image file name in result compression = self.runtime_config.get_bundle_compression(default=True) if self.luks is not None and self.luks_randomize: compression = False self.result.add( key='disk_image', filename=self.diskname, use_for_bundle=True if not self.image_format else False, compress=compression, shasum=True, ) if self.host_key_certificates and 's390' in self.arch: self.result.add( key='secure_execution_header', filename=os.path.normpath( os.sep.join( [ self.target_dir, '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), 'secure_execution_header.bin', ), ] ) ), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='attestation_response_key', filename=os.path.normpath( os.sep.join( [ self.target_dir, '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), 'attestation_response.key', ), ] ) ), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='attestation_request', filename=os.path.normpath( os.sep.join( [ self.target_dir, '{0}.{1}-{2}.{3}'.format( self.xml_state.xml_data.get_name(), self.arch, self.xml_state.get_image_version(), 'attestation_request.bin', ), ] ) ), use_for_bundle=True, compress=False, shasum=False, ) # create image root metadata self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes(self.target_dir), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result def create_disk_format(self, result_instance: Result) -> Result: """ Create a bootable disk format from a previously created raw disk image :param object result_instance: instance of :class:`Result` :return: updated result_instance :rtype: instance of :class:`Result` """ if self.image_format: log.info('Creating %s Disk Format', self.image_format) with DiskFormat.new( self.image_format, self.xml_state, self.root_dir, self.target_dir, ) as disk_format: disk_format.create_image_format() disk_format.store_to_result(result_instance) return result_instance def append_unpartitioned_space(self) -> None: """ Extends the raw disk if an unpartitioned area is specified """ if self.unpartitioned_bytes: log.info( 'Expanding disk with %d bytes of unpartitioned space', self.unpartitioned_bytes, ) with DiskFormat.new( 'raw', self.xml_state, self.root_dir, self.target_dir ) as disk_format: disk_format.resize_raw_disk( self.unpartitioned_bytes, append=True ) firmware = FirmWare(self.xml_state) with LoopDevice(disk_format.diskname) as loop_provider: loop_provider.create(overwrite=False) partitioner = Partitioner.new( firmware.get_partition_table_type(), loop_provider ) partitioner.resize_table() def create_install_media(self, result_instance: Result) -> Result: """ Build an installation image. The installation image is a bootable hybrid ISO image which embeds the raw disk image and an image installer :param object result_instance: instance of :class:`Result` :return: updated result_instance with installation media :rtype: instance of :class:`Result` """ if self.install_media: boot_image = None if self.initrd_system == 'kiwi': boot_image = self.boot_image install_image = InstallImageBuilder( self.xml_state, self.root_dir, self.target_dir, boot_image, self.custom_args, ) if self.install_iso or self.install_stick: log.info('Creating hybrid ISO installation image') install_image.create_install_iso() result_instance.add( key='installation_image', filename=install_image.isoname, use_for_bundle=True, compress=False, shasum=True, ) if self.install_pxe: log.info('Creating PXE installation archive') install_image.create_install_pxe_archive() result_instance.add( key='installation_pxe_archive', filename=install_image.pxetarball, use_for_bundle=True, compress=False, shasum=True, ) return result_instance def _disk_instance(self, loop_provider: LoopDevice) -> Disk: return Disk( self.firmware.get_partition_table_type(), loop_provider, self.disk_start_sector, extended_layout=bool(self.dosparttable_extended_layout), apply_dps_codes=bool(self.apply_dps_codes), ) def _bootloader_instance(self, disk: Disk) -> BootLoaderConfigBase: custom_args = { 'targetbase': disk.storage_provider.get_device(), 'crypto_disk': True if self.luks is not None else False, 'boot_is_crypto': self.boot_is_crypto, 'config_options': self.xml_state.get_bootloader_config_options(), 'target_dir': self.target_dir, } if self.bootloader.startswith('grub'): custom_args.update( Defaults.get_grub_custom_arguments(self.root_dir) ) return create_boot_loader_config( name=self.bootloader, xml_state=self.xml_state, root_dir=self.root_dir, boot_dir=self.root_dir, custom_args=custom_args, ) def _raid_instance(self, device_map: Dict) -> RaidDevice: return RaidDevice(device_map['root']) def _luks_instance(self, device_map: Dict) -> LuksDevice: return LuksDevice(device_map['root']) def _integrity_instance(self, device_map: Dict) -> IntegrityDevice: return IntegrityDevice( device_map['root'], defaults.INTEGRITY_ALGORITHM, integrity_credentials_type( keydescription=self.integrity_key_description, keyfile=self.integrity_keyfile, keyfile_algorithm=defaults.INTEGRITY_KEY_ALGORITHM, options=['legacy_hmac'] if self.integrity_legacy_hmac else [], ), ) def _create_system_instance( self, device_map: Dict, stack: ExitStack ) -> Dict: # create spare filesystem on spare partition if present self.storage_map['system_spare'] = self._build_spare_filesystem( device_map ) # create custom partitions and filesystems self.storage_map['system_custom_parts'] = ( self._build_custom_parts_filesystem( device_map, self.custom_partitions ) ) # create filesystems on boot partition(s) if any self.storage_map['system_boot'], self.storage_map['system_efi'] = ( self._build_boot_filesystems(device_map) ) if self.volume_manager_name: volume_manager = VolumeManager.new( self.volume_manager_name, device_map, self.root_dir + '/', self.volumes, self.volume_manager_custom_parameters, ) stack.push(volume_manager) device_map = self._map_root_volume_manager( device_map, volume_manager ) elif self.need_root_filesystem: filesystem = FileSystem.new( self.requested_filesystem, device_map['root'], self.root_dir + '/', self.filesystem_custom_parameters, ) stack.push(filesystem) self._map_root_filesystem(device_map, filesystem) return device_map def _map_raid( self, device_map: Dict, disk: Disk, raid_root: RaidDevice ) -> Dict: # build the raid device raid_root.create_degraded_raid(raid_level=self.mdraid) device_map['root'] = raid_root.get_device() disk.public_partition_id_map['kiwi_RaidPart'] = ( disk.public_partition_id_map['kiwi_RootPart'] ) disk.public_partition_id_map['kiwi_RaidDev'] = device_map[ 'root' ].get_device() self.storage_map['raid_root'] = raid_root return device_map def _map_integrity( self, device_map: Dict, integrity_root: IntegrityDevice ) -> Dict: # build the integrity device integrity_root.create_dm_integrity() device_map['integrity_root'] = device_map['root'] device_map['root'] = integrity_root.get_device() if ( self.root_filesystem_is_overlay and self.root_filesystem_has_write_partition is False ): device_map['readonly'] = device_map['root'] self.storage_map['integrity_root'] = integrity_root return device_map def _map_luks(self, device_map: Dict, luks_root: LuksDevice) -> Dict: # build the luks self.luks_boot_keyname = '/root/.root.keyfile' self.luks_boot_keyfile = ''.join( [self.root_dir, self.luks_boot_keyname] ) # use LUKS key file for the following conditions: # 1. /boot is encrypted # In this case grub needs to read from LUKS via the # cryptodisk module which at the moment always asks # for the passphrase even when empty. The keyfile # setup makes sure only one interaction on the grub # stage is needed # 2. LUKS passphrase is configured as empty string # In this case the keyfile allows to open the # LUKS pool without asking # luks_need_keyfile = ( True if self.boot_is_crypto or self.luks == '' or self.luks == 'random' else False ) luks_root.create_crypto_luks( passphrase=self.luks or '', osname=self.luks_os, options=self.luks_format_options, keyfile=self.luks_boot_keyname if luks_need_keyfile else '', randomize=self.luks_randomize, root_dir=self.root_dir, ) if luks_need_keyfile: self.luks_boot_keyfile_setup = ''.join( [self.root_dir, '/etc/dracut.conf.d/99-luks-boot.conf'] ) self.boot_image.write_system_config_file( config={'install_items': [self.luks_boot_keyname]}, config_file=self.luks_boot_keyfile_setup, ) self.boot_image.include_file( '/root/' + os.path.basename(self.luks_boot_keyfile) ) device_map['luks_root'] = device_map['root'] device_map['root'] = luks_root.get_device() if ( self.root_filesystem_is_overlay and self.root_filesystem_has_write_partition is False ): device_map['readonly'] = device_map['root'] self.storage_map['luks_root'] = luks_root return device_map def _map_root_volume_manager( self, device_map: Dict, volume_manager: VolumeManagerBase ) -> Dict: # build system root volumes and filesystems volume_manager.setup(self.volume_group_name) volume_manager.create_volumes(self.requested_filesystem) volume_manager.mount_volumes() device_map['root'] = volume_manager.get_device().get('root') device_map['swap'] = volume_manager.get_device().get('swap') self.storage_map['system'] = volume_manager return device_map def _map_root_filesystem( self, device_map: Dict, filesystem: FileSystemBase ) -> None: # build system root filesystem log.info( 'Creating root({0}) filesystem on {1}'.format( self.requested_filesystem, device_map['root'].get_device() ) ) if self.root_filesystem_embed_integrity_metadata: filesystem.create_on_device( label=self.disk_setup.get_root_label(), size=-defaults.DM_METADATA_OFFSET, unit=defaults.UNIT.byte, ) else: filesystem.create_on_device( label=self.disk_setup.get_root_label(), ) self.storage_map['system'] = filesystem def _build_main_system( self, stack: ExitStack, device_map: Dict, disk: Disk, system: Optional[Union[FileSystemBase, VolumeManagerBase]], system_boot: Optional[FileSystemBase], system_efi: Optional[FileSystemBase], system_spare: Optional[FileSystemBase], system_custom_parts: Dict[str, FileSystemBase], luks_root: Optional[LuksDevice] = None, raid_root: Optional[RaidDevice] = None, integrity_root: Optional[IntegrityDevice] = None, ) -> None: # create swap on current root device if requested if self.swap_mbytes: with FileSystem.new('swap', device_map['swap']) as swap: swap.create_on_device(label='SWAP') # store root partition/filesystem uuid for profile self._preserve_root_partition_uuid(device_map) self._preserve_root_filesystem_uuid(device_map) # create a random image identifier self.mbrid = SystemIdentifier() self.mbrid.calculate_id() # create first stage metadata to boot image self._write_partition_id_config_to_boot_image(disk) self._write_recovery_metadata_to_boot_image() self._write_raid_config_to_boot_image(raid_root) self._write_generic_fstab_to_boot_image(device_map, system) self.system_setup.export_modprobe_setup( self.boot_image.boot_root_directory ) # create first stage metadata to system image self._write_image_identifier_to_system_image() self._write_crypttab_to_system_image(luks_root) if 'rd.kiwi.oem.luks.reencrypt' in self.cmdline: self._write_luks_header_checksum_to_boot_image(luks_root) self._write_integritytab_to_system_image(integrity_root) self._write_generic_fstab_to_system_image(device_map, system) if self.initrd_system == 'dracut': for module in self.dracut_add_modules: self.boot_image.include_module(module) for module in self.dracut_omit_modules: self.boot_image.omit_module(module) for driver in self.dracut_add_drivers: self.boot_image.include_driver(driver) for driver in self.dracut_omit_drivers: self.boot_image.omit_driver(driver) if self.root_filesystem_is_multipath is False: self.boot_image.omit_module('multipath') if self.root_filesystem_is_overlay: self.boot_image.include_module('kiwi-overlay') self.boot_image.write_system_config_file( config={'modules': ['kiwi-overlay']} ) if self.disk_resize_requested: self.boot_image.include_module('kiwi-repart') # create initrd(s) and second stage metadata to system image self._create_boot_files() with self._bootloader_instance(disk) as bootloader_config: # write bootloader meta data to system image if self.bootloader != 'custom': self._write_bootloader_meta_data_to_system_image( device_map, disk, system, bootloader_config ) # call edit_boot_config script partition_id_map = disk.get_public_partition_id_map() boot_partition_id = partition_id_map['kiwi_RootPart'] if 'kiwi_BootPart' in partition_id_map: boot_partition_id = partition_id_map['kiwi_BootPart'] self.system_setup.call_edit_boot_config_script( self.requested_filesystem, int(boot_partition_id) ) # write MBR id self.mbrid.write_to_disk(disk.storage_provider) # run pre sync script hook if self.system_setup.script_exists(defaults.PRE_DISK_SYNC_SCRIPT): disk_system = SystemSetup(self.xml_state, self.root_dir) disk_system.call_pre_disk_script() # syncing system data to disk image system = self._sync_system_to_image( stack, device_map, system, system_boot, system_efi, system_spare, system_custom_parts, integrity_root, ) # run post sync actions... if self.veritysetup: self._write_veritytab_to_boot_image( device_map, self.veritysetup ) if system: with ImageSystem( device_map, self.root_dir, system if self.volume_manager_name else None, self.custom_partitions if self.custom_partitions else {}, ) as image_system: image_system.mount() root = image_system.mountpoint() # rebuild initrd with veritytab included if ( self.veritysetup and self.boot_image.has_initrd_support() and not self.dracut_setup.uefi ): self._update_boot_files(root) disk_system = SystemSetup( self.xml_state, image_system.mountpoint() ) if self.system_setup.script_exists( defaults.POST_DISK_SYNC_SCRIPT ): # run post sync script hook and security context disk_system.call_disk_script() elif not self.root_filesystem_is_overlay: # setup security context disk_system.setup_selinux_file_contexts() # install boot loader if self.bootloader != 'custom': self._install_bootloader( device_map, disk, system, bootloader_config, self.boot_image, ) # initrd update on the ESP for systemd for read-only boot/ if ( self.veritysetup and self.boot_image.has_initrd_support() and not self.dracut_setup.uefi and not os.access(f'{root}/boot/', os.W_OK) and self.bootloader == 'systemd_boot' ): for flavor in self.kernel_flavors: boot_names = self.boot_image.get_boot_names(flavor) Command.run( [ 'cp', f'{self.root_dir}/boot/{boot_names.initrd_name}', f'{root}/boot/efi/os/{boot_names.initrd_name}', ] ) # cleanup build metadata if os.access(f'{root}/', os.W_OK): disk_system.cleanup() # set root filesystem properties if system: self._setup_property_root_is_readonly_snapshot(system) # set btrfs seed if ( self._install_image_requested() and self._partition_based_install_requested() and self.volume_manager_name == 'btrfs' ): Command.run( ['btrfstune', '-S', '1', device_map['root'].get_device()] ) # call edit_boot_install script boot_device = device_map['root'] if 'boot' in device_map: boot_device = device_map['boot'] self.system_setup.call_edit_boot_install_script( self.diskname, boot_device.get_device() ) def _install_image_requested(self) -> bool: return bool(self.install_iso or self.install_stick or self.install_pxe) def _partition_based_install_requested(self) -> bool: # follow the dracut getargbool semantics, the last # occurrence of the option wins requested = False for option in self.cmdline.split(): name, _, value = option.partition('=') if name == 'rd.kiwi.install.partition_based': requested = value not in ('0', 'no', 'off') return requested def _get_exclude_list_for_root_data_sync(self, device_map: Dict) -> list: exclude_list = ( Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files(self.root_dir) ) if 'spare' in device_map and self.spare_part_mountpoint: exclude_list.append( '{0}/*'.format(self.spare_part_mountpoint.lstrip(os.sep)) ) exclude_list.append( '{0}/.*'.format(self.spare_part_mountpoint.lstrip(os.sep)) ) if ( 'boot' in device_map and 's390' in self.arch and self.bootloader == 'grub2_s390x_emu' ): exclude_list.append('boot/zipl/*') exclude_list.append('boot/zipl/.*') elif 'boot' in device_map: exclude_list.append('boot/*') exclude_list.append('boot/.*') if 'efi' in device_map: exclude_list.append('boot/efi/*') exclude_list.append('boot/efi/.*') if self.custom_partitions: for map_name in sorted(self.custom_partitions.keys()): if ( map_name in device_map and self.custom_partitions[map_name].mountpoint ): mountpoint = os.path.normpath( self.custom_partitions[map_name].mountpoint ).lstrip(os.sep) exclude_list.append(f'{mountpoint}/*') exclude_list.append(f'{mountpoint}/.*') return exclude_list @staticmethod def _get_exclude_list_for_boot_data_sync() -> list: return ['efi/*'] def _build_custom_parts_filesystem( self, device_map: Dict, custom_partitions: Dict['str', ptable_entry_type], ) -> Dict[str, FileSystemBase]: filesystem_dict = {} partitions = custom_partitions or {} for map_name in sorted(partitions.keys()): if map_name in device_map: ptable_entry = partitions[map_name] if ptable_entry.filesystem: with FileSystem.new( ptable_entry.filesystem, device_map[map_name], f'{self.root_dir}{ptable_entry.mountpoint}/', ) as filesystem: if ptable_entry.filesystem == 'squashfs': squashed_root_file = Temporary().new_file() filesystem.create_on_file( filename=squashed_root_file.name, label=ptable_entry.label, exclude=[Defaults.get_shared_cache_location()], ) readonly_target = device_map[map_name].get_device() readonly_target_bytesize = device_map[ map_name ].get_byte_size(readonly_target) log.info( '--> {} {!r} file({} {}) -> {}({} {})'.format( 'Dumping', map_name, os.path.getsize(squashed_root_file.name), 'bytes', readonly_target, readonly_target_bytesize, 'bytes', ) ) Command.run( [ 'dd', f'if={squashed_root_file.name}', f'of={readonly_target}', ] ) else: filesystem.create_on_device( label=ptable_entry.label or map_name.upper() ) filesystem_dict[map_name] = filesystem return filesystem_dict def _build_spare_filesystem( self, device_map: Dict ) -> Optional[FileSystemBase]: if 'spare' in device_map and self.spare_part_fs: spare_part_data_path = None spare_part_custom_parameters = { 'fs_attributes': self.xml_state.get_build_type_spare_part_fs_attributes() } if self.spare_part_mountpoint: spare_part_data_path = self.root_dir + '{0}/'.format( self.spare_part_mountpoint ) with FileSystem.new( self.spare_part_fs, device_map['spare'], spare_part_data_path, spare_part_custom_parameters, ) as filesystem: filesystem.create_on_device(label='SPARE') return filesystem return None def _build_boot_filesystems( self, device_map: Dict ) -> Tuple[Optional[FileSystemBase], Optional[FileSystemBase]]: system_boot = None system_efi = None if 'efi' in device_map: log.info( 'Creating EFI(fat16) filesystem on %s', device_map['efi'].get_device(), ) with FileSystem.new( 'fat16', device_map['efi'], self.root_dir + '/boot/efi/' ) as filesystem: filesystem.create_on_device( label=self.disk_setup.get_efi_label() ) system_efi = filesystem if 'boot' in device_map: boot_filesystem = self.requested_boot_filesystem if not boot_filesystem: boot_filesystem = self.requested_filesystem boot_directory = self.root_dir + '/boot/' if 's390' in self.arch and self.bootloader == 'grub2_s390x_emu': boot_directory = self.root_dir + '/boot/zipl/' log.info( 'Creating boot(%s) filesystem on %s', boot_filesystem, device_map['boot'].get_device(), ) with FileSystem.new( boot_filesystem, device_map['boot'], boot_directory ) as filesystem: filesystem.create_on_device( label=self.disk_setup.get_boot_label() ) system_boot = filesystem return system_boot, system_efi @staticmethod def _custom_partition_id(part_id: Optional[str]) -> Optional[int]: """ Convert a custom partition number from the image description into the integer the partitioner expects The schema declares the *part_id attributes as a number type, but the attribute value arrives from the XML parser as a string. The partitioner stores the given value as the last used partition number and derives the next one from it by adding one, so passing the string on makes the first partition without an explicit number fail with TypeError: can only concatenate str (not "int") to str :param str part_id: partition number from the image description :return: partition number, or None if the description sets none :rtype: int """ return int(part_id) if part_id else None def _build_and_map_disk_partitions( self, disk: Disk, disksize_mbytes: int ) -> Dict: disk.wipe() disksize_used_mbytes = 0 if self.firmware.get_legacy_bios_partition_size(): log.info('--> creating EFI CSM(legacy bios) partition') partition_mbsize = self.firmware.get_legacy_bios_partition_size() disk.create_efi_csm_partition( partition_mbsize, self._custom_partition_id( self.xml_state.build_type.get_eficsmpart_id() ), ) disksize_used_mbytes += partition_mbsize if self.firmware.efi_mode(): log.info('--> creating EFI partition') partition_mbsize = self.firmware.get_efi_partition_size() disk.create_efi_partition( partition_mbsize, self._custom_partition_id( self.xml_state.build_type.get_efipart_id() ), ) disksize_used_mbytes += partition_mbsize if self.firmware.ofw_mode(): log.info('--> creating PReP partition') partition_mbsize = self.firmware.get_prep_partition_size() disk.create_prep_partition(partition_mbsize) disksize_used_mbytes += partition_mbsize if self.disk_setup.need_boot_partition(): log.info( '--> creating boot partition [with {0} clone(s)]'.format( self.boot_clone_count ) ) partition_mbsize = self.disk_setup.boot_partition_size() disk.create_boot_partition( format(partition_mbsize), self.boot_clone_count, self._custom_partition_id( self.xml_state.build_type.get_bootpart_id() ), ) disksize_used_mbytes += ( (self.boot_clone_count + 1) * partition_mbsize if self.boot_clone_count else partition_mbsize ) if self.swap_mbytes: if ( not self.volume_manager_name or self.volume_manager_name != 'lvm' ): log.info('--> creating SWAP partition') disk.create_swap_partition(f'{self.swap_mbytes}') disksize_used_mbytes += self.swap_mbytes if self.custom_partitions: log.info( '--> creating custom partition(s): {0}'.format( sorted(self.custom_partitions.keys()) ) ) disk.create_custom_partitions(self.custom_partitions) if self.spare_part_mbsize and not self.spare_part_is_last: log.info('--> creating spare partition') disk.create_spare_partition(f'{self.spare_part_mbsize}') if self.root_filesystem_is_overlay: log.info('--> creating readonly root partition') squashed_rootfs_mbsize = self.root_filesystem_read_only_partsize if not self.root_filesystem_read_only_partsize: squashed_root_file = Temporary().new_file() with FileSystem.new( self.root_filesystem_read_only_type or 'squashfs', device_provider=DeviceProvider(), root_dir=self.root_dir, custom_args={ 'compression': self._get_root_readonly_compression(), 'create_options': self.custom_root_readonly_creation_args, }, ) as squashed_root: squashed_root.create_on_file( filename=squashed_root_file.name, exclude=[Defaults.get_shared_cache_location()], ) squashed_rootfs_mbsize = ( int(os.path.getsize(squashed_root_file.name) / 1048576) + Defaults.get_min_partition_mbytes() ) disk.create_root_readonly_partition( squashed_rootfs_mbsize, self.root_clone_count ) disksize_used_mbytes += ( (self.root_clone_count + 1) * squashed_rootfs_mbsize if self.root_clone_count else squashed_rootfs_mbsize ) root_clone_count = self.root_clone_count if self.root_filesystem_is_overlay: # in overlay mode an eventual root clone is created from # the root readonly partition and not from the root (rw) # partition. Thus no further action needed here in this # case root_clone_count = 0 rootfs_mbsize: Union[int, str] if self.spare_part_mbsize and self.spare_part_is_last: rootfs_mbsize = ( disksize_mbytes - disksize_used_mbytes - self.spare_part_mbsize - Defaults.get_min_partition_mbytes() ) if root_clone_count: rootfs_mbsize = int(rootfs_mbsize / (root_clone_count + 1)) else: if self.oem_systemsize and ( not self.oem_resize or self.spare_part_is_last ): # if resize on boot is switched off, we have to take # the given size into account at build time. # if resize is active but the rootfs is not the last # partition we also have to take the given size into # account, because only the last can resize rootfs_mbsize = self.oem_systemsize else: rootfs_mbsize = 'all_free' log.info( '--> Using {0}MB for the root(rw) partition if present'.format( rootfs_mbsize ) ) if ( self.root_filesystem_is_overlay and self.root_filesystem_has_write_partition is False ): log.warning( '--> overlayroot explicitly requested no write partition' ) else: if root_clone_count: if rootfs_mbsize == 'all_free': clone_rootfs_mbsize = ( disksize_mbytes - disksize_used_mbytes - Defaults.get_min_partition_mbytes() ) clone_rootfs_mbsize = int( clone_rootfs_mbsize / (root_clone_count + 1) ) rootfs_mbsize = ( f'clone:{clone_rootfs_mbsize}:{clone_rootfs_mbsize}' ) else: rootfs_mbsize = f'clone:{rootfs_mbsize}:{rootfs_mbsize}' rootpart_id = self._custom_partition_id( self.xml_state.build_type.get_rootpart_id() ) if self.volume_manager_name and self.volume_manager_name == 'lvm': log.info( '--> creating {0} partition [with {1} clone(s)]'.format( 'root(LVM)', root_clone_count ) ) disk.create_root_lvm_partition( rootfs_mbsize, root_clone_count, rootpart_id ) elif self.mdraid: log.info( '--> creating {0} partition [with {1} clone(s)]'.format( f'root(mdraid={self.mdraid})', root_clone_count ) ) disk.create_root_raid_partition( rootfs_mbsize, root_clone_count, rootpart_id ) else: log.info( '--> creating root partition [with {0} clone(s)]'.format( root_clone_count ) ) disk.create_root_partition( rootfs_mbsize, root_clone_count, rootpart_id ) if self.spare_part_is_last: log.info('--> creating spare partition') disk.create_spare_partition('all_free') if self.firmware.bios_mode(): log.info('--> setting active flag to primary boot partition') disk.activate_boot_partition() if self.firmware.ofw_mode(): log.info('--> setting active flag to primary PReP partition') disk.activate_boot_partition() if ( self.firmware.get_partition_table_type() == 'msdos' and self.disk_start_sector ): log.info(f'--> setting start sector to: {self.disk_start_sector}') disk.set_start_sector(self.disk_start_sector) if self.firmware.efi_mode(): if self.force_mbr: log.info('--> converting partition table to MBR') disk.create_mbr() elif self.hybrid_mbr: log.info('--> converting partition table to hybrid GPT/MBR') disk.create_hybrid_mbr() disk.map_partitions() device_map = disk.get_device() device_map['origin_root'] = ( device_map.get('readonly') or device_map['root'] ) return device_map def _write_partition_id_config_to_boot_image(self, disk: Disk) -> None: """ Write the partition name to partition number map to config.partids in the boot system. With dracut the boot system is the system image, applications can use the file there, e.g for partition clones. The dracut initrd code looks up the partitions at boot time and does not use the file, it is only included into the kiwi initrd """ log.info('Creating config.partids in boot system') filename = ''.join( [self.boot_image.boot_root_directory, '/config.partids'] ) partition_id_map = disk.get_public_partition_id_map() with open(filename, 'w') as partids: for id_name, id_value in list(partition_id_map.items()): partids.write( '{0}="{1}"{2}'.format(id_name, id_value, os.linesep) ) if self.initrd_system == 'kiwi': self.boot_image.include_file(os.sep + os.path.basename(filename)) def _write_raid_config_to_boot_image( self, raid_root: Optional[RaidDevice] ) -> None: if raid_root is not None: log.info('Creating etc/mdadm.conf in boot system') filename = ''.join( [self.boot_image.boot_root_directory, '/etc/mdadm.conf'] ) raid_root.create_raid_config(filename) self.boot_image.include_file( os.sep + os.sep.join(['etc', os.path.basename(filename)]) ) def _write_integritytab_to_system_image( self, integrity_root: Optional[IntegrityDevice] ) -> None: if integrity_root: log.info('Creating etc/integritytab') filename = ''.join([self.root_dir, '/etc/integritytab']) integrity_root.create_integritytab(filename) self.boot_image.include_file( os.sep + os.sep.join(['etc', os.path.basename(filename)]) ) def _write_crypttab_to_system_image( self, luks_root: Optional[LuksDevice] ) -> None: if luks_root is not None: log.info('Creating etc/crypttab') filename = ''.join([self.root_dir, '/etc/crypttab']) luks_root.create_crypttab(filename) self.boot_image.include_file( os.sep + os.sep.join(['etc', os.path.basename(filename)]) ) def _write_luks_header_checksum_to_boot_image( self, luks_root: Optional[LuksDevice] ) -> None: if luks_root is not None: log.info( 'Including origin LUKS header checksum and key slot number' ) filenames = [ ''.join([self.root_dir, '/root/.luks.header']), ''.join([self.root_dir, '/root/.luks.slot']), ''.join([self.root_dir, '/root/.slotpass']), ] for filename in filenames: self.boot_image.include_file( filename=os.sep + os.sep.join(['root', os.path.basename(filename)]), delete_after_include=True, ) def _write_generic_fstab_to_system_image( self, device_map: Dict, system: Optional[Union[FileSystemBase, VolumeManagerBase]], ) -> None: log.info('Creating generic system etc/fstab') self._write_generic_fstab(device_map, self.system_setup, system) def _write_veritytab_to_boot_image( self, device_map: Dict, veritysetup: VeritySetup ) -> None: log.info('Creating generic boot image etc/veritytab') veritytab_filename = ''.join([self.root_dir, '/etc/veritytab']) block_operation = BlockID( device_map['readonly'].get_device() if device_map.get('readonly') else device_map['root'].get_device() ) filesystem = block_operation.get_filesystem() if filesystem == 'squashfs': # squashfs does not provide a label or uuid # veritytab does not support PARTUUID=... syntax device_id = '/dev/disk/by-partuuid/{}'.format( block_operation.get_blkid('PARTUUID') ) else: device_id = 'UUID={}'.format(block_operation.get_blkid('UUID')) with open(veritytab_filename, 'w') as veritytab: veritytab.write( 'verityroot {0} {0} {1} {2},{3}{4}'.format( device_id, veritysetup.verity_dict.get('Roothash'), f'hash-offset={veritysetup.verity_hash_offset}', f'hash-block-size={defaults.VERITY_HASH_BLOCKSIZE}', os.linesep, ) ) self.boot_image.include_file( filename=os.sep + os.sep.join(['etc', os.path.basename(veritytab_filename)]), delete_after_include=True, ) def _write_generic_fstab_to_boot_image( self, device_map: Dict, system: Optional[Union[FileSystemBase, VolumeManagerBase]], ) -> None: if self.initrd_system == 'kiwi': log.info('Creating generic boot image etc/fstab') # the kiwi boot image is prepared at this point assert self.boot_image.setup is not None self._write_generic_fstab(device_map, self.boot_image.setup, system) def _write_generic_fstab( self, device_map: Dict, setup: SystemSetup, system: Optional[Union[FileSystemBase, VolumeManagerBase]], ) -> None: root_is_snapper_snapshot = ( self.xml_state.build_type.get_btrfs_root_is_snapper_snapshot() ) root_is_readonly_snapshot = ( self.xml_state.build_type.get_btrfs_root_is_readonly_snapshot() ) fs_check_interval = '0 1' custom_root_mount_args = list(self.custom_root_mount_args) if root_is_snapper_snapshot and root_is_readonly_snapshot: custom_root_mount_args += ['ro'] fs_check_interval = '0 0' if ( system and self.volume_manager_name and self.volume_manager_name == 'btrfs' and not self.btrfs_default_volume_requested ): root_volume_name = system.get_root_volume_name() if root_volume_name != '/': custom_root_mount_args += [ f'defaults,subvol={root_volume_name}' ] self._add_fstab_entry( device_map['root'].get_device(), '/', custom_root_mount_args, fs_check_interval, ) if device_map.get('boot'): if 's390' in self.arch and self.bootloader == 'grub2_s390x_emu': boot_mount_point = '/boot/zipl' else: boot_mount_point = '/boot' self._add_fstab_entry( device_map['boot'].get_device(), boot_mount_point ) if device_map.get('efi'): self._add_fstab_entry( device_map['efi'].get_device(), '/boot/efi', self.custom_efi_mount_args, ) if system and self.volume_manager_name: volume_fstab_entries = system.get_fstab( self.persistency_type, self.requested_filesystem ) for volume_fstab_entry in volume_fstab_entries: self.fstab.add_entry(volume_fstab_entry) if ( device_map.get('spare') and self.spare_part_fs and self.spare_part_mountpoint ): self._add_fstab_entry( device_map['spare'].get_device(), self.spare_part_mountpoint ) if device_map.get('swap'): self._add_fstab_entry(device_map['swap'].get_device(), 'swap') if self.custom_partitions: for map_name in sorted(self.custom_partitions.keys()): if ( device_map.get(map_name) and self.custom_partitions[map_name].mountpoint ): self._add_fstab_entry( device_map[map_name].get_device(), self.custom_partitions[map_name].mountpoint, ) setup.create_fstab(self.fstab) def _add_fstab_entry( self, device: str, mount_point: str, options: Optional[List] = None, check: str = '0 0', ) -> None: if not options: options = ['defaults'] block_operation = BlockID(device) filesystem = block_operation.get_filesystem() if ( self.volume_manager_name and self.volume_manager_name == 'lvm' and (mount_point == '/' or mount_point == 'swap') ): fstab_entry = ' '.join( [device, mount_point, filesystem, ','.join(options), check] ) else: if filesystem == 'squashfs': # squashfs does not provide a label or uuid blkid_type = 'PARTUUID' elif self.persistency_type == 'by-label': blkid_type = 'LABEL' elif self.persistency_type == 'by-partuuid': blkid_type = 'PARTUUID' else: blkid_type = 'UUID' device_id = block_operation.get_blkid(blkid_type) fstab_entry = ' '.join( [ blkid_type + '=' + device_id, mount_point, filesystem, ','.join(options), check, ] ) self.fstab.add_entry(fstab_entry) def _preserve_root_partition_uuid(self, device_map: Dict) -> None: root_part_device = ( device_map.get('raid_root') or device_map.get('luks_root') or device_map.get('integrity_root') or device_map['root'] ) block_operation = BlockID(root_part_device.get_device()) partition_uuid = block_operation.get_blkid('PARTUUID') if partition_uuid: self.xml_state.set_root_partition_uuid(partition_uuid) def _preserve_root_filesystem_uuid(self, device_map: Dict) -> None: block_operation = BlockID(device_map['root'].get_device()) rootfs_uuid = block_operation.get_blkid('UUID') if rootfs_uuid: self.xml_state.set_root_filesystem_uuid(rootfs_uuid) def _write_image_identifier_to_system_image(self) -> None: log.info('Creating image identifier: %s', self.mbrid.get_id()) self.mbrid.write(self.root_dir + '/boot/mbrid') def _write_recovery_metadata_to_boot_image(self) -> None: if os.path.exists(self.root_dir + '/recovery.partition.size'): log.info('Copying recovery metadata to boot image') recovery_metadata = ''.join( [self.root_dir, '/recovery.partition.size'] ) Command.run( ['cp', recovery_metadata, self.boot_image.boot_root_directory] ) self.boot_image.include_file( os.sep + os.path.basename(recovery_metadata) ) def _write_bootloader_meta_data_to_system_image( self, device_map: Dict, disk: Disk, system: Optional[Union[FileSystemBase, VolumeManagerBase]], bootloader_config: BootLoaderConfigBase, ) -> None: log.info('Creating %s bootloader configuration', self.bootloader) boot_options = [] if self.mdraid: boot_options.append('rd.auto') if ( system and self.volume_manager_name and self.volume_manager_name == 'btrfs' and not self.btrfs_default_volume_requested and system.get_root_volume_name() != '/' ): boot_options.append( f'rootflags=subvol={system.get_root_volume_name()}' ) ro_device = device_map.get('readonly') root_device = device_map['root'] boot_device = root_device if 'boot' in device_map: boot_device = device_map['boot'] efi_uuid = ( disk.get_uuid(device_map['efi'].get_device()) if device_map.get('efi') else None ) boot_uuid = disk.get_uuid(boot_device.get_device()) boot_uuid_unmapped = ( disk.get_uuid(device_map['luks_root'].get_device()) if self.luks and self.boot_is_crypto else boot_uuid ) bootloader_config.setup_disk_boot_images(boot_uuid_unmapped, efi_uuid) bootloader_config.write_meta_data( root_device=( ro_device.get_device() if ro_device else root_device.get_device() ), write_device=root_device.get_device(), boot_options=' '.join(boot_options), ) log.info('Creating config.bootoptions') filename = ''.join( [self.boot_image.boot_root_directory, '/config.bootoptions'] ) kexec_boot_options = ' '.join( [ bootloader_config.get_boot_cmdline( ( ro_device.get_device() if ro_device else root_device.get_device() ), device_map['root'].get_device(), ) ] + boot_options ) with open(filename, 'w') as boot_optionsfp: boot_optionsfp.write( '{0}{1}'.format(kexec_boot_options, os.linesep) ) def _sync_system_to_image( self, stack: ExitStack, device_map: Dict, system: Optional[Union[FileSystemBase, VolumeManagerBase]], system_boot: Optional[FileSystemBase], system_efi: Optional[FileSystemBase], system_spare: Optional[FileSystemBase], system_custom_parts: Dict[str, FileSystemBase], integrity_root: Optional[IntegrityDevice], ) -> Optional[Union[FileSystemBase, VolumeManagerBase]]: log.info('Syncing system to image') if system_spare: log.info('--> Syncing spare partition data') stack.push(system_spare.sync_data()) for map_name in sorted(system_custom_parts.keys()): system_custom_part = system_custom_parts[map_name] log.info('--> Syncing custom partition(s) data') if not system_custom_part.filename: stack.push(system_custom_part.sync_data()) if device_map.get(f'{map_name}clone1'): log.info( f'--> Dumping {map_name!r} clone data at extra partition' ) system_custom_part.umount() system_custom_part_clone = CloneDevice( system_custom_part.device_provider, self.root_dir ) system_custom_part_clone.clone( self._get_clone_devices(f'{map_name}clone', device_map) ) system_custom_part.mount() if system_efi: log.info('--> Syncing EFI boot data to EFI partition') stack.push(system_efi.sync_data()) if system_boot: log.info('--> Syncing boot data at extra partition') stack.push( system_boot.sync_data( self._get_exclude_list_for_boot_data_sync() ) ) if device_map.get('bootclone1'): log.info('--> Dumping boot clone data at extra partition') system_boot.umount() CloneDevice(system_boot.device_provider, self.root_dir).clone( self._get_clone_devices('bootclone', device_map) ) system_boot.mount() log.info('--> Syncing root filesystem data') if self.root_filesystem_is_overlay: squashed_root_file = Temporary().new_file() with FileSystem.new( self.root_filesystem_read_only_type or 'squashfs', device_provider=DeviceProvider(), root_dir=self.root_dir, custom_args={ 'compression': self._get_root_readonly_compression(), 'create_options': self.custom_root_readonly_creation_args, }, ) as squashed_root: exclude_list = self._get_exclude_list_for_root_data_sync( device_map ) # To allow running custom scripts in a read-only root # it's required to keep the /image mountpoint directory # such that it can be bind mounted from the unpacked # root tree exclude_list.remove('image') exclude_list.append('image/*') with MountManager( device='tmpfs', mountpoint=os.path.join(self.root_dir, 'boot'), ) as overlay_boot: if system_boot: overlay_boot.tmpfs_mount() # make sure to keep boot/efi mountpoints # as they can't be created later Path.create(f'{self.root_dir}/boot/efi') for entry in [ 'boot/*', 'boot/.*', 'boot/efi/*', 'boot/efi/.*', ]: if entry in exclude_list: exclude_list.remove(entry) squashed_root.create_on_file( filename=squashed_root_file.name, exclude=exclude_list ) self.storage_map['system'] = squashed_root if self.root_filesystem_verity_blocks: squashed_root.create_verity_layer( self.root_filesystem_verity_blocks if self.root_filesystem_verity_blocks != 'all' else None ) self.veritysetup = squashed_root.veritysetup readonly_target = device_map['readonly'].get_device() readonly_target_bytesize = device_map['readonly'].get_byte_size( readonly_target ) log.info( '--> Dumping rootfs file({0} {1}) -> {2}({3} {1})'.format( os.path.getsize(squashed_root_file.name), 'bytes', readonly_target, readonly_target_bytesize, ) ) Command.run( [ 'dd', f'if={squashed_root_file.name}', f'of={readonly_target}', ] ) if self.root_filesystem_embed_verity_metadata: squashed_root.create_verification_metadata(readonly_target) if device_map.get('rootclone1'): log.info( '--> Dumping readonly root clone data at extra partition' ) CloneDevice(device_map['origin_root'], self.root_dir).clone( self._get_clone_devices('rootclone', device_map) ) elif self.root_filesystem_verity_blocks: root_target = device_map['root'].get_device() root_target_bytesize = device_map['root'].get_byte_size(root_target) verity_root_file_bytes = ( root_target_bytesize - VeritySetup( device_map['root'].get_device(), ( self.root_filesystem_verity_blocks if self.root_filesystem_verity_blocks != 'all' else None ), ).get_hash_byte_size() ) if self.root_filesystem_embed_verity_metadata: verity_root_file_bytes -= defaults.DM_METADATA_OFFSET verity_root_file = Temporary().new_file() with LoopDevice( verity_root_file.name, int(verity_root_file_bytes / 1048576) ) as loop_provider: loop_provider.create() filesystem_custom_parameters = { 'mount_options': self.custom_root_mount_args, 'create_options': self.custom_root_creation_args, } with FileSystem.new( self.requested_filesystem, loop_provider, self.root_dir + '/', filesystem_custom_parameters, ) as filesystem: filesystem.create_on_device( label=self.disk_setup.get_root_label(), uuid=BlockID(root_target).get_uuid(), ) self.storage_map['system'] = filesystem filesystem.sync_data( self._get_exclude_list_for_root_data_sync(device_map) ) filesystem.create_verity_layer( ( self.root_filesystem_verity_blocks if self.root_filesystem_verity_blocks != 'all' else None ), verity_root_file.name, ) self.veritysetup = filesystem.veritysetup log.info( '--> Dumping rootfs file({0} bytes) -> {1}({2} bytes)'.format( os.path.getsize(verity_root_file.name), root_target, root_target_bytesize, ) ) Command.run( ['dd', f'if={verity_root_file.name}', f'of={root_target}'] ) if self.root_filesystem_embed_verity_metadata: filesystem.create_verification_metadata(root_target) if device_map.get('rootclone1'): log.info('--> Dumping root clone data at extra partition') CloneDevice(device_map['origin_root'], self.root_dir).clone( self._get_clone_devices('rootclone', device_map) ) elif system: system_mount = system.sync_data( self._get_exclude_list_for_root_data_sync(device_map) ) if system_mount: stack.push(system_mount) if device_map.get('rootclone1'): log.info('--> Dumping root clone data at extra partition') if self.volume_manager_name: system.umount_volumes() else: system.umount() CloneDevice(device_map['origin_root'], self.root_dir).clone( self._get_clone_devices('rootclone', device_map) ) if self.volume_manager_name: system.mount_volumes() else: system.mount() if integrity_root and self.root_filesystem_embed_integrity_metadata: log.info('--> Creating integrity metadata...') integrity_root.create_integrity_metadata() log.info('--> Signing integrity metadata...') integrity_root.sign_integrity_metadata() integrity_root.write_integrity_metadata() return self.storage_map['system'] def _install_bootloader( self, device_map: Dict, disk: Disk, system: Optional[Union[FileSystemBase, VolumeManagerBase]], bootloader_config: BootLoaderConfigBase, boot_image: BootImageBase, ) -> None: root_device = device_map['root'] boot_device = root_device readonly_device = None if 'readonly' in device_map: readonly_device = device_map['readonly'] boot_device = readonly_device if 'boot' in device_map: boot_device = device_map['boot'] custom_install_arguments = { 'device_map': device_map, 'boot_image': boot_image, 'boot_device': boot_device.get_device(), 'root_device': ( readonly_device.get_device() if readonly_device else root_device.get_device() ), 'write_device': root_device.get_device(), 'firmware': self.firmware, 'target_removable': self.target_removable, 'install_options': self.xml_state.get_bootloader_install_options(), 'shim_options': self.xml_state.get_bootloader_shim_options(), } if 'efi' in device_map: efi_device = device_map['efi'] custom_install_arguments.update( {'efi_device': efi_device.get_device()} ) if 'prep' in device_map: prep_device = device_map['prep'] custom_install_arguments.update( {'prep_device': prep_device.get_device()} ) if system and self.volume_manager_name: custom_install_arguments.update( { 'system_volumes': system.get_volumes(), 'system_root_volume': ( system.get_root_volume_name() if self.volume_manager_name == 'btrfs' else None ), } ) if system and self.custom_partitions: custom_install_arguments.update( {'system_partitions': self.custom_partitions} ) # create bootloader config prior bootloader installation try: bootloader_config.setup_disk_image_config( boot_options=custom_install_arguments ) if 's390' in self.arch: bootloader_config.write() finally: # cleanup bootloader config resources taken prior to next steps del bootloader_config if self.root_filesystem_has_write_partition is not False: log.debug( "custom arguments for bootloader installation %s", custom_install_arguments, ) bootloader = BootLoaderInstall.new( self.bootloader, self.xml_state, self.root_dir, disk.storage_provider, custom_install_arguments, ) if bootloader.install_required(): bootloader.install() bootloader.secure_boot_install() if self.use_disk_password and self.storage_map['luks_root']: bootloader.set_disk_password( self.storage_map['luks_root'].passphrase ) else: log.warning('No install of bootcode on read-only root possible') def _setup_property_root_is_readonly_snapshot( self, system: Union[FileSystemBase, VolumeManagerBase] ) -> None: if self.volume_manager_name: root_is_snapper_snapshot = ( self.xml_state.build_type.get_btrfs_root_is_snapper_snapshot() ) root_is_readonly_snapshot = ( self.xml_state.build_type.get_btrfs_root_is_readonly_snapshot() ) if root_is_snapper_snapshot and root_is_readonly_snapshot: log.info('Setting root filesystem into read-only mode') system.set_property_readonly_root() def _create_boot_files(self) -> None: """ Create the initrd for the kernel of each kernel flavor and copy the boot files to the system image. Files included into the initrd with delete_after_include are deleted once the initrd of the last flavor is created """ for flavor in self.kernel_flavors: if ( self.boot_image.has_initrd_support() and not self.dracut_setup.uefi ): self.boot_image.create_initrd( self.mbrid, flavor=flavor, delete_included_files=flavor == self.kernel_flavors[-1], ) self._copy_first_boot_files_to_system_image(flavor) def _update_boot_files(self, root: str) -> None: """ Create the initrd for the kernel of each kernel flavor again and update it in the system image and, if writable, in the given mounted disk image system. Files included into the initrd with delete_after_include are deleted once the initrd of the last flavor is created :param str root: mountpoint of the disk image system """ for flavor in self.kernel_flavors: self.boot_image.create_initrd( self.mbrid, flavor=flavor, delete_included_files=flavor == self.kernel_flavors[-1], ) self._copy_first_boot_files_to_system_image(flavor) boot_names = self.boot_image.get_boot_names(flavor) if os.access(f'{root}/boot/', os.W_OK): Command.run( [ 'cp', f'{self.root_dir}/boot/{boot_names.initrd_name}', f'{root}/boot/{boot_names.initrd_name}', ] ) def _copy_first_boot_files_to_system_image(self, flavor: str = '') -> None: boot_names = self.boot_image.get_boot_names(flavor) if self.initrd_system == 'kiwi': log.info('Copy boot files to system image') kernel = Kernel(self.boot_image.boot_root_directory) log.info('--> boot image kernel as %s', boot_names.kernel_name) kernel.copy_kernel( self.root_dir, ''.join(['/boot/', boot_names.kernel_name]) ) if self.xen_server: if kernel.get_xen_hypervisor(): log.info('--> boot image Xen hypervisor as xen.gz') kernel.copy_xen_hypervisor(self.root_dir, '/boot/xen.gz') else: raise KiwiDiskBootImageError( 'No hypervisor in boot image tree %s found' % self.boot_image.boot_root_directory ) if self.boot_image.initrd_filename: log.info('--> initrd archive as %s', boot_names.initrd_name) Command.run( [ 'mv', self.boot_image.initrd_filename, self.root_dir + ''.join(['/boot/', boot_names.initrd_name]), ] ) def _get_clone_devices( self, match: str, device_map: Dict[str, DeviceProvider] ) -> List[DeviceProvider]: result = [] for map_name in sorted(device_map.keys()): if map_name.startswith(match): result.append(device_map[map_name]) return result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/enclave.py0000644000000000000000000001441715273675667014112 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict, Optional # project from kiwi.defaults import Defaults from kiwi.boot.image import BootImage from kiwi.system.setup import SystemSetup from kiwi.system.kernel import Kernel from kiwi.system.result import Result from kiwi.runtime_config import RuntimeConfig from kiwi.xml_state import XMLState from kiwi.command import Command from kiwi.exceptions import KiwiEnclaveFormatError, KiwiEnclaveBootImageError log = logging.getLogger('kiwi') class EnclaveBuilder: """ **Enclave Builder** Enclaves defines initrd-only image types. :param object xml_state: instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: system image root directory :param dict custom_args: Custom processing arguments defined as hash keys: * signing_keys: list of package signing keys * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.target_dir = target_dir self.custom_cmdline = xml_state.build_type.get_kernelcmdline() self.format = xml_state.build_type.get_enclave_format() self.system_setup = SystemSetup(xml_state=xml_state, root_dir=root_dir) xml_state.build_type.set_initrd_system('kiwi') xml_state.build_type.set_boot(f'{root_dir}/image') self.boot_signing_keys = ( custom_args['signing_keys'] if custom_args and 'signing_keys' in custom_args else None ) self.xz_options = ( custom_args['xz_options'] if custom_args and 'xz_options' in custom_args else None ) self.boot_image_task = BootImage.new( xml_state, target_dir, root_dir, signing_keys=self.boot_signing_keys ) # Force BootImageKiwi instance to use existing root_dir self.boot_image_task.boot_root_directory = root_dir self.bundle_format = xml_state.get_build_type_bundle_format() self.image_name = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + xml_state.get_image_version(), ] ) self.image: str = '' self.initrd: str = '' self.kernel_filename: str = '' self.enclave: str = '' self.result = Result(xml_state) self.runtime_config = RuntimeConfig() def create(self) -> Result: """ Build an eif image using the eif-cli Image types which triggers this builder are: * image="enclave" :return: result :rtype: instance of :class:`Result` """ if not self.format: raise KiwiEnclaveFormatError( 'No enclave_format= specified in build type' ) # Create initrd self.boot_image_task.create_initrd() # extract kernel from boot system kernel = Kernel(self.boot_image_task.boot_root_directory) kernel_data = kernel.get_kernel() if kernel_data: self.kernel_filename = ''.join( [ os.path.basename(self.image_name), '-', kernel_data.version, '.kernel', ] ) kernel.copy_kernel(self.target_dir, self.kernel_filename) else: raise KiwiEnclaveBootImageError( 'No kernel in boot image tree %s found' % self.boot_image_task.boot_root_directory ) self.initrd = os.path.basename(self.boot_image_task.initrd_filename) if self.format == 'aws-nitro': self.enclave = self.image_name + ".eif" Command.run( [ 'eif_build', '--kernel', '/'.join([self.target_dir, self.kernel_filename]), '--ramdisk', '/'.join([self.target_dir, self.initrd]), '--cmdline', self.custom_cmdline, '--output', self.enclave, ] ) Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.initrd ) # store image bundle_format in result if self.bundle_format: self.result.add_bundle_format(self.bundle_format) self.result.add( key='enclave', filename=self.enclave, use_for_bundle=True, compress=self.runtime_config.get_bundle_compression(default=False), shasum=True, ) # create image root metadata self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes(self.target_dir), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/filesystem.py0000644000000000000000000001666015273675667014663 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging import os from typing import Dict, Optional # project from kiwi.filesystem import FileSystem from kiwi.filesystem.setup import FileSystemSetup from kiwi.storage.loop_device import LoopDevice from kiwi.storage.device_provider import DeviceProvider from kiwi.system.setup import SystemSetup from kiwi.defaults import Defaults from kiwi.system.result import Result from kiwi.runtime_config import RuntimeConfig from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiFileSystemSetupError log = logging.getLogger('kiwi') class FileSystemBuilder: """ **Filesystem image builder** :param obsject xml_state: Instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: * None """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.label = None self.root_uuid = '' self.root_dir = root_dir self.target_dir = target_dir self.bundle_format = xml_state.get_build_type_bundle_format() self.requested_image_type = xml_state.get_build_type_name() if self.requested_image_type in Defaults.get_kis_image_types(): self.requested_filesystem = xml_state.build_type.get_filesystem() else: self.requested_filesystem = self.requested_image_type if not self.requested_filesystem: raise KiwiFileSystemSetupError( f'No filesystem configured in {self.requested_image_type} type' ) self.filesystem_custom_parameters = { 'mount_options': xml_state.get_fs_mount_option_list(), 'create_options': xml_state.get_fs_create_option_list(), } if self.requested_filesystem == 'squashfs': self.filesystem_custom_parameters['compression'] = ( xml_state.build_type.get_squashfscompression() ) elif self.requested_filesystem == 'erofs': self.filesystem_custom_parameters['compression'] = ( xml_state.build_type.get_erofscompression() ) self.system_setup = SystemSetup( xml_state=xml_state, root_dir=self.root_dir ) self.filename = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + xml_state.get_image_version(), '.', self.requested_filesystem, ] ) self.blocksize = xml_state.build_type.get_target_blocksize() self.filesystem_setup = FileSystemSetup(xml_state, root_dir) self.filesystems_no_device_node = ['squashfs', 'erofs'] self.luks = xml_state.get_luks_credentials() self.result = Result(xml_state) self.runtime_config = RuntimeConfig() def create(self) -> Result: """ Build a mountable filesystem image Image types which triggers this builder are: * image="ext2" * image="ext3" * image="ext4" * image="btrfs" * image="xfs" :return: result :rtype: instance of :class:`Result` """ log.info('Creating %s filesystem', self.requested_filesystem) supported_filesystems = Defaults.get_filesystem_image_types() if self.requested_filesystem not in supported_filesystems: raise KiwiFileSystemSetupError( f'Unknown filesystem: {self.requested_filesystem}' ) if self.requested_filesystem not in self.filesystems_no_device_node: self._operate_on_loop() else: self._operate_on_file() Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.filename ) if self.bundle_format: self.result.add_bundle_format(self.bundle_format) compression = self.runtime_config.get_bundle_compression(default=True) if self.luks is not None: compression = False self.result.add( key='filesystem_image', filename=self.filename, use_for_bundle=True, compress=compression, shasum=True, ) self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes(self.target_dir), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result def _operate_on_loop(self) -> None: with LoopDevice( self.filename, self.filesystem_setup.get_size_mbytes(), self.blocksize, ) as loop_provider: loop_provider.create() with FileSystem.new( self.requested_filesystem, loop_provider, self.root_dir + os.sep, self.filesystem_custom_parameters, ) as filesystem: filesystem.create_on_device(self.label) self.root_uuid = loop_provider.get_uuid( loop_provider.get_device() ) log.info( '--> Syncing data to filesystem on {0}'.format( loop_provider.get_device() ) ) filesystem.sync_data( Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ) ) def _operate_on_file(self) -> None: default_provider = DeviceProvider() with FileSystem.new( self.requested_filesystem, default_provider, self.root_dir, self.filesystem_custom_parameters, ) as filesystem: filesystem.create_on_file( self.filename, self.label, Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ), ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/install.py0000644000000000000000000005332715273675667014146 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict, Optional, Union, cast import shutil # project from kiwi.runtime_config import RuntimeConfig from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.storage.device_provider import DeviceProvider from kiwi.boot.image.base import BootImageBase from kiwi.boot.image import BootImage from kiwi.bootloader.config import create_boot_loader_config from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot from kiwi.filesystem.squashfs import FileSystemSquashFs from kiwi.filesystem.isofs import FileSystemIsoFs from kiwi.firmware import FirmWare from kiwi.system.identifier import SystemIdentifier from kiwi.path import Path from kiwi.utils.block import BlockID from kiwi.defaults import Defaults from kiwi.system.kernel import Kernel from kiwi.utils.compress import Compress from kiwi.archive.tar import ArchiveTar from kiwi.system.setup import SystemSetup from kiwi.iso_tools.base import IsoToolsBase from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiInstallBootImageError log = logging.getLogger('kiwi') class InstallImageBuilder: """ **Installation image builder** :param object xml_state: instance of :class:`XMLState` :param str root_dir: system image root directory :param str target_dir: target directory path name :param object boot_image_task: instance of :class:`BootImage` :param dict custom_args: Custom processing arguments defined as hash keys: * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, root_dir: str, target_dir: str, boot_image_task: Optional[BootImageBase], custom_args: Optional[Dict] = None, ) -> None: self.runtime_config = RuntimeConfig() self.arch = Defaults.get_platform_name() self.bootloader = xml_state.get_build_type_bootloader_name() if self.bootloader != 'systemd_boot': self.bootloader = 'grub2' self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state self.root_filesystem_is_multipath = ( xml_state.get_oemconfig_oem_multipath_scan() ) self.initrd_system = xml_state.get_initrd_system() # an install boot menu entry and initrd is created for the # kernel of each flavor, the empty flavor stands for the # latest kernel. The first flavor is the one of the default # menu entry and of the system booted after the installation self.kernel_flavors = ( xml_state.get_build_type_bootloader_kernel_flavors() or [''] ) self.firmware = FirmWare(xml_state) self.setup = SystemSetup(self.xml_state, self.root_dir) self.iso_volume_id = ( self.xml_state.build_type.get_volid() or Defaults.get_install_volume_id() ) self.diskname = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + self.arch, '-' + xml_state.get_image_version(), '.raw', ] ) self.isoname = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + self.arch, '-' + xml_state.get_image_version(), '.install.iso', ] ) self.pxename = ''.join( [ xml_state.xml_data.get_name(), '.' + self.arch, '-' + xml_state.get_image_version(), ] ) self.pxetarball = ''.join( [target_dir, '/', self.pxename, '.install.tar'] ) self.dracut_config_file = ''.join( [self.root_dir, Defaults.get_dracut_conf_name()] ) self.squashed_diskname = ''.join( [xml_state.xml_data.get_name(), '.raw'] ) self.xz_options = ( custom_args['xz_options'] if custom_args and 'xz_options' in custom_args else None ) self.mbrid = SystemIdentifier() self.mbrid.calculate_id() self.custom_iso_args: Dict = {} if not boot_image_task: self.boot_image_task = BootImage.new( xml_state, target_dir, root_dir ) self.boot_image_task.prepare() else: self.boot_image_task = boot_image_task def create_install_iso(self) -> None: """ Create an install ISO from the disk_image as hybrid ISO bootable via legacy BIOS, EFI and as disk from Stick Image types which triggers this builder are: * installiso="true|false" * installstick="true|false" """ self.media_dir = Temporary( prefix='kiwi_install_media.', path=self.target_dir ).new_dir() # unpack cdroot user files to media dir self.setup.import_cdroot_files(self.media_dir.name) # custom iso metadata self.custom_iso_args = { 'meta_data': { 'volume_id': self.iso_volume_id, 'mbr_id': self.mbrid.get_id(), 'application_id': self.xml_state.build_type.get_application_id(), 'efi_mode': self.firmware.efi_mode(), 'efi_partition_table': self.firmware.get_partition_table_type(), 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr, 'ofw_mode': self.firmware.ofw_mode(), 'legacy_bios_mode': self.firmware.legacy_bios_mode(), } } # the system image transfer is checked against a checksum self.squashed_contents = Temporary( prefix='kiwi_install_squashfs.', path=self.target_dir ).new_dir() shasum = self.runtime_config.get_checksum_handler( source_filename=self.diskname, target_filename='{}/{}{}'.format( self.squashed_contents.name, self.xml_state.xml_data.get_name(), self.runtime_config.get_checksum_handler(self.diskname).suffix, ), ) log.info(f'Creating disk image {shasum.suffix} checksum') shasum.digest() # the system image name is stored in a config file self._write_install_image_info_to_iso_image() if self.initrd_system == 'kiwi': self._write_install_image_info_to_boot_image() # the system image is stored as squashfs embedded file log.info('Creating squashfs embedded disk image') Command.run( [ 'cp', '-l', self.diskname, self.squashed_contents.name + '/' + self.squashed_diskname, ] ) squashed_image_file = ''.join( [self.target_dir, '/', self.squashed_diskname, '.squashfs'] ) with FileSystemSquashFs( device_provider=DeviceProvider(), root_dir=self.squashed_contents.name, custom_args={ 'compression': self.xml_state.build_type.get_squashfscompression() }, ) as squashed_image: squashed_image.create_on_file(squashed_image_file) Command.run(['mv', squashed_image_file, self.media_dir.name]) log.info('Setting up install image bootloader configuration') with self._create_bootloader_instance() as bootloader_config: bootloader_config.setup_install_boot_images( mbrid=self.mbrid, lookup_path=self.boot_image_task.boot_root_directory, ) IsoToolsBase.setup_media_loader_directory( self.boot_image_task.boot_root_directory, self.media_dir.name, bootloader_config.get_boot_theme(), ) bootloader_config.write_meta_data() bootloader_config.setup_install_image_config(mbrid=self.mbrid) bootloader_config.write() # create initrd for install image log.info('Creating install image boot image') self._create_iso_install_kernel_and_initrd() # the system image initrd is stored to allow kexec self._copy_system_image_initrd_to_iso_image() if self.firmware.efi_mode(): efi_loader = Temporary( prefix='efi-loader.', path=self.target_dir ).new_file() bootloader_config._create_embedded_fat_efi_image( efi_loader.name ) self.custom_iso_args['meta_data'][ 'efi_loader' ] = efi_loader.name # create iso filesystem from media_dir log.info('Creating ISO filesystem') with FileSystemIsoFs( device_provider=DeviceProvider(), root_dir=self.media_dir.name, custom_args=self.custom_iso_args, ) as iso_image: iso_image.create_on_file(self.isoname) self.boot_image_task.cleanup() def create_install_pxe_archive(self) -> None: """ Create an oem install tar archive suitable for installing a disk image via the network using the PXE boot protocol. The archive contains: * The raw system image xz compressed * The raw system image checksum metadata file * The append file template for the boot server * The system image initrd for kexec * The install initrd * The kernel Image types which triggers this builder are: * installpxe="true|false" """ self.pxe_dir = Temporary( prefix='kiwi_pxe_install_media.', path=self.target_dir ).new_dir() # the system image is transfered as xz compressed variant log.info('xz compressing disk image') pxe_image_filename = ''.join( [self.pxe_dir.name, '/', self.pxename, '.xz'] ) compress = Compress( source_filename=self.diskname, keep_source_on_compress=True ) xz_archive = compress.xz(self.xz_options) Command.run(['mv', xz_archive, pxe_image_filename]) # the system image transfer is checked against a checksum shasum = self.runtime_config.get_checksum_handler( source_filename=self.diskname, target_filename='{}/{}{}'.format( self.pxe_dir.name, self.pxename, self.runtime_config.get_checksum_handler(self.diskname).suffix, ), ) log.info(f'Creating disk image {shasum.suffix} checksum') shasum.digest() # the install image name is stored in a config file if self.initrd_system == 'kiwi': self._write_install_image_info_to_boot_image() # the kexec required system image initrd is stored for dracut kiwi-dump if self.initrd_system == 'dracut': boot_names = self.boot_image_task.get_boot_names( self.kernel_flavors[0] ) system_image_initrd = os.sep.join( [self.root_dir, 'boot', boot_names.initrd_name] ) target_initrd_name = '{0}/{1}.initrd'.format( self.pxe_dir.name, self.pxename ) shutil.copy(system_image_initrd, target_initrd_name) os.chmod(target_initrd_name, 420) # create pxe config append information # this information helps to configure the boot server correctly append_filename = ''.join( [self.pxe_dir.name, '/', self.pxename, '.append'] ) if self.initrd_system == 'kiwi': cmdline = 'pxe=1' else: cmdline = ' '.join( [ 'rd.kiwi.install.pxe', 'rd.kiwi.install.image=http://example.com/image.xz', ] ) custom_cmdline = self.xml_state.build_type.get_kernelcmdline() if custom_cmdline: cmdline += ' ' + custom_cmdline with open(append_filename, 'w') as append: append.write(f'{cmdline}\n') # create initrd for pxe install log.info('Creating pxe install boot image') self._create_pxe_install_kernel_and_initrd() # create pxe image bound boot config file, contents can be # changed but presence is required. log.info('Creating pxe install boot options file') configname = '{0}.config.bootoptions'.format(self.pxename) shutil.copy( os.sep.join([self.root_dir, 'config.bootoptions']), os.sep.join([self.pxe_dir.name, configname]), ) # create pxe install tarball log.info('Creating pxe install archive') archive = ArchiveTar(self.pxetarball) archive.create(self.pxe_dir.name) self.boot_image_task.cleanup() def _create_bootloader_instance( self, ) -> Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot]: # ISO images boot with grub2 or systemd-boot only, any other # bootloader name is turned into grub2, see __init__ return cast( Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot], create_boot_loader_config( name=self.bootloader, xml_state=self.xml_state, root_dir=self.root_dir, boot_dir=self.media_dir.name, custom_args=( Defaults.get_grub_custom_arguments(self.root_dir) if self.bootloader.startswith('grub') else {} ), ), ) def _create_pxe_install_kernel_and_initrd(self) -> None: # The pxe boot uses the kernel of the first flavor flavor = self.kernel_flavors[0] kernelname = 'pxeboot.{0}.kernel'.format(self.pxename) initrdname = 'pxeboot.{0}.initrd'.format(self.pxename) kernel = Kernel(self.boot_image_task.boot_root_directory) if kernel.get_kernel(flavor=flavor): kernel.copy_kernel(self.pxe_dir.name, kernelname, flavor=flavor) os.symlink( kernelname, ''.join([self.pxe_dir.name, '/', self.pxename, '.kernel']), ) else: raise KiwiInstallBootImageError( 'No kernel in boot image tree %s found' % self.boot_image_task.boot_root_directory ) if self.xml_state.is_xen_server(): if kernel.get_xen_hypervisor(): kernel.copy_xen_hypervisor( self.pxe_dir.name, '/pxeboot.{0}.xen.gz'.format(self.pxename), ) else: raise KiwiInstallBootImageError( 'No hypervisor in boot image tree %s found' % self.boot_image_task.boot_root_directory ) if self.initrd_system == 'dracut': self.boot_image_task.include_module('kiwi-dump') self.boot_image_task.include_module('kiwi-dump-reboot') self.boot_image_task.omit_module('kiwi-repart') if self.root_filesystem_is_multipath is False: self.boot_image_task.omit_module('multipath') for mod in self.xml_state.get_installmedia_initrd_modules('add'): self.boot_image_task.include_module(mod) for mod in self.xml_state.get_installmedia_initrd_modules('omit'): self.boot_image_task.omit_module(mod) self.boot_image_task.set_static_modules( self.xml_state.get_installmedia_initrd_modules('set') ) for drv in self.xml_state.get_installmedia_initrd_drivers('add'): self.boot_image_task.include_driver(drv) for drv in self.xml_state.get_installmedia_initrd_drivers('omit'): self.boot_image_task.omit_driver(drv) self.boot_image_task.create_initrd( self.mbrid, 'initrd_kiwi_install', install_initrd=True, flavor=flavor, ) Command.run( [ 'mv', self.boot_image_task.initrd_filename, self.pxe_dir.name + '/{0}'.format(initrdname), ] ) os.chmod(self.pxe_dir.name + '/{0}'.format(initrdname), 420) def _create_iso_install_kernel_and_initrd(self) -> None: boot_path = self.media_dir.name + '/boot/' + self.arch + '/loader' Path.create(boot_path) # see Defaults.get_iso_kernel_flavor_file_name() for the # names of the kernel and initrd files of each flavor kernel = Kernel(self.boot_image_task.boot_root_directory) for index, flavor in enumerate(self.kernel_flavors): if kernel.get_kernel(flavor=flavor): kernel.copy_kernel( boot_path, '/' + Defaults.get_iso_kernel_flavor_file_name( 'linux', index, flavor ), flavor=flavor, ) else: raise KiwiInstallBootImageError( 'No kernel{0} in boot image tree {1} found'.format( f' of flavor {flavor}' if flavor else '', self.boot_image_task.boot_root_directory, ) ) if self.xml_state.is_xen_server(): if kernel.get_xen_hypervisor(): kernel.copy_xen_hypervisor(boot_path, '/xen.gz') else: raise KiwiInstallBootImageError( 'No hypervisor in boot image tree %s found' % self.boot_image_task.boot_root_directory ) if self.initrd_system == 'dracut': self.boot_image_task.include_module('kiwi-dump') self.boot_image_task.include_module('kiwi-dump-reboot') self.boot_image_task.omit_module('kiwi-repart') if self.root_filesystem_is_multipath is False: self.boot_image_task.omit_module('multipath') for mod in self.xml_state.get_installmedia_initrd_modules('add'): self.boot_image_task.include_module(mod) for mod in self.xml_state.get_installmedia_initrd_modules('omit'): self.boot_image_task.omit_module(mod) self.boot_image_task.set_static_modules( self.xml_state.get_installmedia_initrd_modules('set') ) for drv in self.xml_state.get_installmedia_initrd_drivers('add'): self.boot_image_task.include_driver(drv) for drv in self.xml_state.get_installmedia_initrd_drivers('omit'): self.boot_image_task.omit_driver(drv) self._add_system_image_boot_options_to_boot_image() self._add_system_identifier_to_boot_image() for index, flavor in enumerate(self.kernel_flavors): self.boot_image_task.create_initrd( self.mbrid, 'initrd_kiwi_install', install_initrd=True, flavor=flavor, ) Command.run( [ 'mv', self.boot_image_task.initrd_filename, boot_path + '/' + Defaults.get_iso_kernel_flavor_file_name( 'initrd', index, flavor ), ] ) def _add_system_identifier_to_boot_image(self) -> None: filename = ''.join( [self.boot_image_task.boot_root_directory, '/system_identifier'] ) blockid = BlockID(self.diskname) with open(filename, 'w') as system_identifier: system_identifier.write(blockid.get_ptuuid()) self.boot_image_task.include_file(os.sep + os.path.basename(filename)) def _add_system_image_boot_options_to_boot_image(self) -> None: filename = ''.join( [self.boot_image_task.boot_root_directory, '/config.bootoptions'] ) self.boot_image_task.include_file(os.sep + os.path.basename(filename)) def _copy_system_image_initrd_to_iso_image(self) -> None: # The installed system is booted by kexec with the kernel of # the first flavor, see Defaults.get_iso_kernel_flavor_file_name() boot_names = self.boot_image_task.get_boot_names(self.kernel_flavors[0]) system_image_initrd = os.sep.join( [self.root_dir, 'boot', boot_names.initrd_name] ) shutil.copy( system_image_initrd, self.media_dir.name + '/initrd.system_image' ) def _write_install_image_info_to_iso_image(self) -> None: iso_trigger = self.media_dir.name + '/config.isoclient' with open(iso_trigger, 'w') as iso_system: iso_system.write(f'IMAGE="{self.squashed_diskname}\"\n') def _write_install_image_info_to_boot_image(self) -> None: initrd_trigger = ( self.boot_image_task.boot_root_directory + '/config.vmxsystem' ) with open(initrd_trigger, 'w') as vmx_system: vmx_system.write(f'IMAGE="{self.squashed_diskname}\"\n') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/kis.py0000644000000000000000000002723515273675667013265 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict, Optional # project from kiwi.defaults import Defaults from kiwi.boot.image import BootImage from kiwi.builder.filesystem import FileSystemBuilder from kiwi.utils.compress import Compress from kiwi.system.setup import SystemSetup from kiwi.system.kernel import Kernel from kiwi.system.result import Result from kiwi.runtime_config import RuntimeConfig from kiwi.archive.tar import ArchiveTar from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiKisBootImageError log = logging.getLogger('kiwi') class KisBuilder: """ **Filesystem based image builder.** :param object xml_state: instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: system image root directory :param dict custom_args: Custom processing arguments defined as hash keys: * signing_keys: list of package signing keys * xz_options: string of XZ compression parameters """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.runtime_config = RuntimeConfig() self.target_dir = target_dir self.compressed = xml_state.build_type.get_compressed() self.xen_server = xml_state.is_xen_server() self.custom_cmdline = xml_state.build_type.get_kernelcmdline() self.filesystem = ( FileSystemBuilder(xml_state, target_dir, root_dir + '/') if xml_state.build_type.get_filesystem() else None ) self.archive = ( xml_state.build_type.get_archive() if xml_state.build_type.get_archive() is not None else True ) self.system_setup = SystemSetup(xml_state=xml_state, root_dir=root_dir) self.initrd_system = xml_state.get_initrd_system() self.boot_signing_keys = ( custom_args['signing_keys'] if custom_args and 'signing_keys' in custom_args else None ) self.xz_options = ( custom_args['xz_options'] if custom_args and 'xz_options' in custom_args else None ) self.boot_image_task = BootImage.new( xml_state, target_dir, root_dir, signing_keys=self.boot_signing_keys ) self.bundle_format = xml_state.get_build_type_bundle_format() self.image_name = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + xml_state.get_image_version(), ] ) self.image: str = '' self.append_file = ''.join([self.image_name, '.append']) self.archive_name = ''.join([self.image_name, '.tar']) self.kernel_filename: str = '' self.hypervisor_filename: str = '' self.result = Result(xml_state) if not self.boot_image_task.has_initrd_support(): log.warning('Building without initrd support !') def create(self) -> Result: """ Build a component image consisting out of a boot image(initrd) plus its appropriate kernel files and the root filesystem image with a checksum. Image types which triggers this builder are: * image="kis" * image="pxe" :raises KiwiKisBootImageError: if no kernel or hipervisor is found in boot image tree :return: result :rtype: instance of :class:`Result` """ if self.filesystem: log.info('Creating root filesystem image') self.filesystem.create() os.rename(self.filesystem.filename, self.image_name) self.image = self.image_name if self.compressed: log.info('xz compressing root filesystem image') compress = Compress(self.image) self.image = compress.xz(self.xz_options) shasum = self.runtime_config.get_checksum_handler( source_filename=self.image, target_filename='{}{}'.format( self.image_name, self.runtime_config.get_checksum_handler(self.image).suffix, ), ) log.info(f'Creating root filesystem {shasum.suffix} checksum') shasum.digest() # prepare initrd if self.boot_image_task.has_initrd_support(): log.info('Creating boot image') self.boot_image_task.prepare() # export modprobe configuration to boot image self.system_setup.export_modprobe_setup( self.boot_image_task.boot_root_directory ) # extract kernel from boot system kernel = Kernel(self.boot_image_task.boot_root_directory) kernel_data = kernel.get_kernel() if kernel_data: self.kernel_filename = ''.join( [ os.path.basename(self.image_name), '-', kernel_data.version, '.kernel', ] ) kernel.copy_kernel(self.target_dir, self.kernel_filename) else: raise KiwiKisBootImageError( 'No kernel in boot image tree %s found' % self.boot_image_task.boot_root_directory ) # extract hypervisor from boot(initrd) root system if self.xen_server: hypervisor_data = kernel.get_xen_hypervisor() if hypervisor_data: self.hypervisor_filename = ''.join( [ os.path.basename(self.image_name), '-', hypervisor_data.name, ] ) kernel.copy_xen_hypervisor( self.target_dir, self.hypervisor_filename ) self.result.add( key='xen_hypervisor', filename=self.target_dir + '/' + self.hypervisor_filename, use_for_bundle=True, compress=False, shasum=True, ) else: raise KiwiKisBootImageError( 'No hypervisor in boot image tree %s found' % self.boot_image_task.boot_root_directory ) # create initrd if self.boot_image_task.has_initrd_support(): self.boot_image_task.create_initrd() # create append information # this information helps to configure the deployment infrastructure if ( self.filesystem and self.filesystem.root_uuid and self.initrd_system == 'dracut' ): cmdline = 'root=UUID={}'.format(self.filesystem.root_uuid) if self.custom_cmdline: cmdline += ' {}'.format(self.custom_cmdline) with open(self.append_file, 'w') as append: append.write(cmdline) # put results into a tarball if not self.xz_options: self.xz_options = Defaults.get_xz_compression_options() kis_tarball_files = [ self.kernel_filename, os.path.basename( '{}{}'.format( self.image_name, self.runtime_config.get_checksum_handler( f'{self.target_dir}/{self.kernel_filename}' ).suffix, ) ), ] if self.boot_image_task.initrd_filename: kis_tarball_files.append( os.path.basename(self.boot_image_task.initrd_filename) ) if self.image: kis_tarball_files.append(os.path.basename(self.image)) if ( self.filesystem and self.filesystem.root_uuid and self.initrd_system == 'dracut' ): kis_tarball_files.append(os.path.basename(self.append_file)) if self.archive: # Put all result files into a tar archive and add this # one file to the result instance kis_tarball = ArchiveTar( self.archive_name, create_from_file_list=True, file_list=kis_tarball_files, ) if self.compressed: self.archive_name = kis_tarball.create(self.target_dir) else: self.archive_name = kis_tarball.create_xz_compressed( self.target_dir, xz_options=self.xz_options ) Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.archive_name ) # store archive file name in result self.result.add( key='kis_archive', filename=self.archive_name, use_for_bundle=True, compress=self.runtime_config.get_bundle_compression( default=False ), shasum=True, ) else: # Treat all result files individually and add them to the # result instance. An eventually configured max size constraint # applies to the rootfs image only if self.image: Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.image ) for result_file in kis_tarball_files: compress_file = False filename = f'{self.target_dir}/{result_file}' keyname = f'kis_{result_file.split(".").pop()}' if self.image and filename == self.image: keyname = 'kis_rootfs' compress_file = self.runtime_config.get_bundle_compression( default=False ) self.result.add( key=keyname, filename=filename, use_for_bundle=True, compress=compress_file, shasum=False, ) # store image bundle_format in result if self.bundle_format: self.result.add_bundle_format(self.bundle_format) # create image root metadata self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes(self.target_dir), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/live.py0000644000000000000000000006322015273675667013430 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from contextlib import ExitStack import os import logging from typing import Dict, List, Union, cast, Optional import shutil # project import kiwi.defaults as defaults from kiwi.utils.veritysetup import VeritySetup from kiwi.utils.block import BlockID from kiwi.utils.temporary import Temporary from kiwi.bootloader.config import create_boot_loader_config from kiwi.bootloader.config.grub2 import BootLoaderConfigGrub2 from kiwi.bootloader.config.systemd_boot import BootLoaderSystemdBoot from kiwi.filesystem import FileSystem from kiwi.filesystem.isofs import FileSystemIsoFs from kiwi.filesystem.setup import FileSystemSetup from kiwi.storage.loop_device import LoopDevice from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.luks_device import LuksDevice from kiwi.boot.image.dracut import BootImageDracut from kiwi.system.size import SystemSize from kiwi.system.setup import SystemSetup from kiwi.firmware import FirmWare from kiwi.defaults import Defaults from kiwi.path import Path from kiwi.system.result import Result from kiwi.iso_tools.iso import Iso from kiwi.system.identifier import SystemIdentifier from kiwi.system.kernel import Kernel from kiwi.runtime_config import RuntimeConfig from kiwi.iso_tools.base import IsoToolsBase from kiwi.xml_state import XMLState from kiwi.command import Command from kiwi.exceptions import KiwiLiveBootImageError log = logging.getLogger('kiwi') class LiveImageBuilder: """ **Live image builder** :param object xml_state: instance of :class:`XMLState` :param str target_dir: target directory path name :param str root_dir: root directory path name :param dict custom_args: Custom processing arguments """ def __init__( self, xml_state: XMLState, target_dir: str, root_dir: str, custom_args: Optional[Dict] = None, ): self.bootloader = xml_state.get_build_type_bootloader_name() if self.bootloader != 'systemd_boot': self.bootloader = 'grub2' self.root_filesystem_verity_blocks = ( xml_state.build_type.get_verity_blocks() ) self.arch = Defaults.get_platform_name() self.root_dir = root_dir self.target_dir = target_dir self.xml_state = xml_state self.live_type = xml_state.build_type.get_flags() # a boot menu entry and an initrd is created for the kernel of # each flavor, the empty flavor stands for the latest kernel self.kernel_flavors = ( xml_state.get_build_type_bootloader_kernel_flavors() or [''] ) # initrd file per kernel flavor, see create_live_iso_boot_images() self.initrd_files: Dict[str, str] = {} self.volume_id = ( xml_state.build_type.get_volid() or Defaults.get_volume_id() ) self.mbrid = SystemIdentifier() self.mbrid.calculate_id() self.application_id = ( self.xml_state.build_type.get_application_id() or self.mbrid.get_id() ) self.publisher = ( xml_state.build_type.get_publisher() or Defaults.get_publisher() ) self.custom_args = custom_args self.luks = xml_state.get_luks_credentials() self.luks_os = xml_state.build_type.get_luksOS() self.luks_format_options = xml_state.get_luks_format_options() self.luks_randomize = ( xml_state.build_type.get_luks_randomize() if xml_state.build_type.get_luks_randomize() is not None else True ) if not self.live_type: self.live_type = Defaults.get_default_live_iso_type() self.boot_image = BootImageDracut( xml_state, ( f'{root_dir}/boot' if self.bootloader == 'systemd_boot' else target_dir ), self.root_dir, ) self.firmware = FirmWare(xml_state) self.system_setup = SystemSetup( xml_state=xml_state, root_dir=self.root_dir ) self.bundle_format = xml_state.get_build_type_bundle_format() self.isoname = ''.join( [ target_dir, '/', xml_state.xml_data.get_name(), '.' + Defaults.get_platform_name(), '-' + xml_state.get_image_version(), '.iso', ] ) self.result = Result(xml_state) self.runtime_config = RuntimeConfig() self.custom_iso_args = { 'meta_data': { 'publisher': self.publisher, 'preparer': Defaults.get_preparer(), 'volume_id': self.volume_id, 'mbr_id': self.mbrid.get_id(), 'application_id': self.application_id, 'efi_mode': self.firmware.efi_mode(), 'efi_partition_table': self.firmware.get_partition_table_type(), 'gpt_hybrid_mbr': self.firmware.gpt_hybrid_mbr, 'legacy_bios_mode': self.firmware.legacy_bios_mode(), } } def create(self) -> Result: """ Build a bootable hybrid live ISO image Image types which triggers this builder are: * image="iso" :raises KiwiLiveBootImageError: if no kernel or hipervisor is found in boot image tree :return: result :rtype: instance of :class:`Result` """ # media dir to store CD contents self.media_dir = Temporary( prefix='live-media.', path=self.target_dir ).new_dir() # unpack cdroot user files to media dir self.system_setup.import_cdroot_files(self.media_dir.name) rootsize = SystemSize(self.media_dir.name) # custom iso metadata log.info('Using following live ISO metadata:') log.info('--> Application id: {0}'.format(self.application_id)) log.info('--> Publisher: {0}'.format(self.publisher)) log.info('--> Volume id: {0}'.format(self.volume_id)) log.info('Setting up live image bootloader configuration') with self._create_bootloader_instance() as bootloader_config: bootloader_config.setup_live_boot_images( mbrid=self.mbrid, lookup_path=self.root_dir ) IsoToolsBase.setup_media_loader_directory( self.boot_image.boot_root_directory, self.media_dir.name, bootloader_config.get_boot_theme(), ) bootloader_config.write_meta_data() bootloader_config.setup_live_image_config(mbrid=self.mbrid) bootloader_config.write() # call custom editbootconfig script if present self.system_setup.call_edit_boot_config_script( filesystem='iso:{0}'.format(self.media_dir.name), boot_part_id=1, working_directory=self.root_dir, ) self.system_setup.setup_selinux_file_contexts() # prepare dracut initrd call self.boot_image.prepare() # create dracut initrd for live image log.info('Creating live ISO boot image(s)') self.create_live_iso_boot_images( bootloader_config, modules=['pollcdrom'] ) # setup kernel file(s) and initrd in ISO boot layout if self.bootloader != 'systemd_boot': log.info( 'Setting up kernel file(s) and boot image in ISO boot layout' ) self._setup_live_iso_kernel_and_initrd() # calculate size and decide if we need UDF if rootsize.accumulate_mbyte_file_sizes() > 4096: log.info('ISO exceeds 4G size, using UDF filesystem') self.custom_iso_args['meta_data']['udf'] = True # pack system into live boot structure as expected by dracut log.info( 'Packing system into dracut live ISO type: {0}'.format( self.live_type ) ) root_filesystem = self.xml_state.build_type.get_filesystem() root_filesystem = ( root_filesystem if root_filesystem else Defaults.get_default_live_iso_root_filesystem() ) filesystem_custom_parameters = { 'mount_options': self.xml_state.get_fs_mount_option_list(), 'create_options': self.xml_state.get_fs_create_option_list(), } self.system_setup.setup_selinux_file_contexts() filesystem_setup = FileSystemSetup(self.xml_state, self.root_dir) if root_filesystem not in ['squashfs', 'erofs']: # Create a filesystem image of the specified type # and put it into a SquashFS container root_image = Temporary().new_file() with ExitStack() as stack: loop_provider = LoopDevice( root_image.name, filesystem_setup.get_size_mbytes(root_filesystem), self.xml_state.build_type.get_target_blocksize(), ) stack.push(loop_provider) loop_provider.create() luks_provider = None if self.luks is not None: luks_provider = LuksDevice(loop_provider) stack.push(luks_provider) luks_provider.create_crypto_luks( passphrase=self.luks or '', osname=self.luks_os, options=self.luks_format_options, keyfile='', randomize=self.luks_randomize, root_dir=self.root_dir, ) device_provider = ( luks_provider.get_device() or DeviceProvider() if luks_provider else loop_provider ) live_filesystem = FileSystem.new( name=root_filesystem, device_provider=device_provider, root_dir=self.root_dir + os.sep, custom_args=filesystem_custom_parameters, ) stack.push(live_filesystem) live_filesystem.create_on_device() log.info( '--> Syncing data to {0} root image'.format(root_filesystem) ) live_filesystem.sync_data( Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ) ) log.info('--> Creating squashfs container for root image') self.live_container_dir = Temporary( prefix='live-container.', path=self.target_dir ).new_dir() Path.create(self.live_container_dir.name + '/LiveOS') shutil.copy( root_image.name, self.live_container_dir.name + '/LiveOS/rootfs.img', ) with FileSystem.new( name='squashfs', device_provider=DeviceProvider(), root_dir=self.live_container_dir.name, custom_args={ 'compression': self.xml_state.build_type.get_squashfscompression() }, ) as live_container_image: container_image = Temporary().new_file() live_container_image.create_on_file(container_image.name) if self.root_filesystem_verity_blocks: live_container_image.create_verity_layer( self.root_filesystem_verity_blocks if self.root_filesystem_verity_blocks != 'all' else None ) # set by create_verity_layer() assert live_container_image.veritysetup is not None self._write_veritytab_to_boot_image( container_image.name, live_container_image.veritysetup ) Path.create(self.media_dir.name + '/LiveOS') os.chmod(container_image.name, 0o644) shutil.copy( container_image.name, self.media_dir.name + '/LiveOS/squashfs.img', ) else: # Put the root filesystem into SquashFS/EroFS directly filesystem_custom_parameters.update( { 'compression': self.xml_state.build_type.get_squashfscompression() } if root_filesystem == 'squashfs' else { 'compression': self.xml_state.build_type.get_erofscompression() } ) with ExitStack() as stack: live_container_image = FileSystem.new( name=root_filesystem, device_provider=DeviceProvider(), root_dir=self.root_dir + os.sep, custom_args=filesystem_custom_parameters, ) stack.push(live_container_image) container_image = Temporary().new_file() live_container_image.create_on_file( filename=container_image.name, exclude=Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files( self.root_dir ), ) if self.root_filesystem_verity_blocks: live_container_image.create_verity_layer( self.root_filesystem_verity_blocks if self.root_filesystem_verity_blocks != 'all' else None ) # set by create_verity_layer() assert live_container_image.veritysetup is not None self._write_veritytab_to_boot_image( container_image.name, live_container_image.veritysetup ) luks_image = None if self.luks is not None: # dump root filesystem blob on top of a LUKS blob # we are adding 20MB LUKS overhead luks_size_mb = ( os.path.getsize(container_image.name) / 1048576 ) luks_image = Temporary().new_file() loop_provider = LoopDevice( luks_image.name, int(luks_size_mb) + 20, self.xml_state.build_type.get_target_blocksize(), ) stack.push(loop_provider) loop_provider.create() luks_provider = LuksDevice(loop_provider) stack.push(luks_provider) luks_provider.create_crypto_luks( passphrase=self.luks or '', osname=self.luks_os, options=self.luks_format_options, keyfile='', randomize=self.luks_randomize, root_dir=self.root_dir, ) luks_device = luks_provider.get_device() or DeviceProvider() Command.run( [ 'dd', f'if={container_image.name}', f'of={luks_device.get_device()}', ] ) Path.create(self.media_dir.name + '/LiveOS') os.chmod(container_image.name, 0o644) # Note: we keep the filename of the read-only image as it is # even if another read-only filesystem not matching this # filename is used. This is because the following filename # is also used in the initrd code for the kiwi-live and # dmsquash dracut modules. The name can be overwritten # with the rd.live.squashimg boot option though. shutil.copy( luks_image.name if luks_image else container_image.name, self.media_dir.name + '/LiveOS/squashfs.img', ) if self.root_filesystem_verity_blocks: log.info('Rebuild live ISO boot image(s) to include veritysetup') with self._create_bootloader_instance() as bootloader_config: self.create_live_iso_boot_images( bootloader_config, modules=['pollcdrom'] ) if self.bootloader != 'systemd_boot': self._setup_live_iso_kernel_and_initrd() # create iso filesystem from media_dir log.info('Creating live ISO image') with FileSystemIsoFs( device_provider=DeviceProvider(), root_dir=self.media_dir.name, custom_args=self.custom_iso_args, ) as iso_image: iso_image.create_on_file(self.isoname) # include metadata for checkmedia tool if self.xml_state.build_type.get_mediacheck() is True: Iso.set_media_tag(self.isoname) Result.verify_image_size( self.runtime_config.get_max_size_constraint(), self.isoname ) if self.bundle_format: self.result.add_bundle_format(self.bundle_format) self.result.add( key='live_image', filename=self.isoname, use_for_bundle=True, compress=False, shasum=True, ) self.result.add( key='image_packages', filename=self.system_setup.export_package_list(self.target_dir), use_for_bundle=True, compress=False, shasum=False, ) self.result.add( key='image_changes', filename=self.system_setup.export_package_changes(self.target_dir), use_for_bundle=True, compress=True, shasum=False, ) self.result.add( key='image_verified', filename=self.system_setup.export_package_verification( self.target_dir ), use_for_bundle=True, compress=False, shasum=False, ) return self.result def create_live_iso_boot_images( self, bootloader_config: Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot], modules: List[str] = [], ) -> None: live_dracut_modules = ( Defaults.get_live_dracut_modules_from_flag(self.live_type) + modules ) for dracut_module in live_dracut_modules: self.boot_image.include_module(dracut_module) self.boot_image.write_system_config_file( config={'modules': live_dracut_modules}, config_file=self.root_dir + '/etc/dracut.conf.d/02-livecd.conf', ) for flavor in self.kernel_flavors: # files included with delete_after_include are deleted # once the initrd of the last flavor is created self.boot_image.create_initrd( self.mbrid, basename=( f'{self.boot_image.initrd_base_name}-{flavor}' if flavor else None ), flavor=flavor, delete_included_files=flavor == self.kernel_flavors[-1], ) self.initrd_files[flavor] = self.boot_image.initrd_filename if self.bootloader == 'systemd_boot': # make sure the initrd name follows the dracut # naming conventions boot_names = self.boot_image.get_boot_names(flavor) if self.boot_image.initrd_filename: Command.run( [ 'mv', self.boot_image.initrd_filename, self.root_dir + ''.join(['/boot/', boot_names.initrd_name]), ] ) # Clean up leftover dracut config file (which can break installs) os.unlink(self.root_dir + '/etc/dracut.conf.d/02-livecd.conf') # create EFI FAT image if self.firmware.efi_mode(): self.efi_loader = Temporary( prefix='efi-loader.', path=self.target_dir ).new_file() bootloader_config._create_embedded_fat_efi_image( self.efi_loader.name ) self.custom_iso_args['meta_data'][ 'efi_loader' ] = self.efi_loader.name def _write_veritytab_to_boot_image( self, container_image_name: str, veritysetup: VeritySetup ) -> None: log.info('Creating generic boot image etc/veritytab') veritytab_filename = ''.join([self.root_dir, '/etc/veritytab']) block_operation = BlockID(container_image_name) filesystem = block_operation.get_filesystem() if filesystem == 'squashfs': # squashfs does not provide a label or uuid # The real device name is not known at this point, and # will be replaced in the initrd code with a device name # based on the loopsetup result of the compressed image file device_id = 'PLACEHOLDER' else: device_id = 'UUID={}'.format(block_operation.get_blkid('UUID')) with open(veritytab_filename, 'w') as veritytab: veritytab.write( 'verityroot {0} {0} {1} {2},{3}{4}'.format( device_id, veritysetup.verity_dict.get('Roothash'), f'hash-offset={veritysetup.verity_hash_offset}', f'hash-block-size={defaults.VERITY_HASH_BLOCKSIZE}', os.linesep, ) ) self.boot_image.include_file( filename=os.sep + os.sep.join(['etc', os.path.basename(veritytab_filename)]), delete_after_include=True, ) def _create_bootloader_instance( self, ) -> Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot]: # ISO images boot with grub2 or systemd-boot only, any other # bootloader name is turned into grub2, see __init__ return cast( Union[BootLoaderConfigGrub2, BootLoaderSystemdBoot], create_boot_loader_config( name=self.bootloader, xml_state=self.xml_state, root_dir=self.root_dir, boot_dir=self.media_dir.name, custom_args=( Defaults.get_grub_custom_arguments(self.root_dir) if self.bootloader.startswith('grub') else {} ), ), ) def _setup_live_iso_kernel_and_initrd(self) -> None: """ Copy kernel and initrd from the root tree into the iso boot structure """ boot_path = ''.join( [self.media_dir.name, '/boot/', self.arch, '/loader'] ) Path.create(boot_path) # Move kernel files to iso filesystem structure, see # Defaults.get_iso_kernel_flavor_file_name() for their names kernel = Kernel(self.boot_image.boot_root_directory) for index, flavor in enumerate(self.kernel_flavors): if kernel.get_kernel(flavor=flavor): kernel.copy_kernel( boot_path, '/' + Defaults.get_iso_kernel_flavor_file_name( 'linux', index, flavor ), flavor=flavor, ) else: raise KiwiLiveBootImageError( 'No kernel{0} in boot image tree {1} found'.format( f' of flavor {flavor}' if flavor else '', self.boot_image.boot_root_directory, ) ) if self.xml_state.is_xen_server(): if kernel.get_xen_hypervisor(): kernel.copy_xen_hypervisor(boot_path, '/xen.gz') else: raise KiwiLiveBootImageError( 'No hypervisor in boot image tree {0} found'.format( self.boot_image.boot_root_directory ) ) # Move initrd(s) to iso filesystem structure for index, flavor in enumerate(self.kernel_flavors): initrd_file = self.initrd_files.get(flavor) or '' if os.path.exists(initrd_file): os.chmod(initrd_file, 0o644) shutil.move( initrd_file, boot_path + '/' + Defaults.get_iso_kernel_flavor_file_name( 'initrd', index, flavor ), ) else: raise KiwiLiveBootImageError( 'No boot image {0} in boot image tree {1} found'.format( initrd_file, self.boot_image.boot_root_directory ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7431872 kiwi-11.1.8/kiwi/builder/template/container_import.py0000644000000000000000000000463015273675667017660 0ustar00# Copyright (c) 2024 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from typing import List from string import Template from textwrap import dedent class BuilderTemplateSystemdUnit: """ **systemd unit file templates** """ def __init__(self) -> None: self.unit = ( dedent( ''' # kiwi generated unit file [Unit] Description=Import Local Container(s) ''' ).strip() + os.linesep ) self.service = ( dedent( ''' [Service] Type=oneshot ''' ).strip() + os.linesep ) self.install = ( dedent( ''' [Install] WantedBy=multi-user.target ''' ).strip() + os.linesep ) def get_container_import_template( self, container_files: List[str], load_commands: List[List[str]], after: List[str], ) -> Template: template_data = self.unit for container_file in container_files: template_data += 'ConditionPathExists={0}{1}'.format( container_file, os.linesep ) if after: template_data += 'After={0}{1}'.format(' '.join(after), os.linesep) template_data += self.service for load_command in load_commands: template_data += 'ExecStart={0}{1}'.format( ' '.join(load_command), os.linesep ) for container_file in container_files: template_data += 'ExecStartPost=/bin/rm -f {0}{1}'.format( container_file, os.linesep ) template_data += self.install return Template(template_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7433155 kiwi-11.1.8/kiwi/chroot_manager.py0000644000000000000000000001035015273675667014027 0ustar00# Copyright (c) 2025 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from types import TracebackType from typing import List, Optional, NamedTuple, Type # project from kiwi.mount_manager import MountManager from kiwi.command import Command, CommandT, MutableMapping from kiwi.exceptions import KiwiUmountBusyError log = logging.getLogger('kiwi') class ChrootMount(NamedTuple): target: str source: Optional[str] = None class ChrootManager: """ **Implements methods for setting and unsetting a chroot environment** The caller is responsible for cleaning up bind mounts if the ChrootManager is used as is, without a context. The class also supports to be used as a context manager, where any bind or kernel filesystem mount is unmounted once the context manager's with block is left * :param string root_dir: path to change the root to * :param list binds: current root paths to bind to the chrooted path """ def __init__(self, root_dir: str, binds: List[ChrootMount] = []): self.root_dir = root_dir self.mounts: List[MountManager] = [] for bind in binds: self.mounts.append( MountManager( device=bind.source if bind.source else bind.target, mountpoint=os.path.normpath( os.sep.join([root_dir, bind.target]) ), ) ) def __enter__(self) -> "ChrootManager": try: self.mount() except Exception as e: try: self.umount() except Exception: pass raise e return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.umount() def mount(self) -> None: """ Mounts binds to the chroot path """ for mnt in self.mounts: mnt.bind_mount() def umount(self) -> None: """ Unmounts all binds from the chroot path If any unmount raises a KiwiUmountBusyError this is trapped and kept until the iteration over all bind mounts is over. """ errors = [] for mnt in reversed(self.mounts): try: mnt.umount() except KiwiUmountBusyError as e: errors.append(e) if errors: raise KiwiUmountBusyError(errors) def run( self, command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, raise_on_error: bool = True, stderr_to_stdout: bool = False, raise_on_command_not_found: bool = True, ) -> Optional[CommandT]: """ This is a wrapper for Command.run method but pre-appending the chroot call at the command list :param list command: command and arguments :param dict custom_env: custom os.environ :param bool raise_on_error: control error behaviour :param bool stderr_to_stdout: redirects stderr to stdout :return: Contains call results in command type .. code:: python CommandT(output='string', error='string', returncode=int) :rtype: CommandT """ chroot_cmd = ['chroot', self.root_dir] chroot_cmd = chroot_cmd + command return Command.run( chroot_cmd, custom_env, raise_on_error, stderr_to_stdout, raise_on_command_not_found, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7433155 kiwi-11.1.8/kiwi/cli.py0000644000000000000000000017464715273675667011632 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import typer import logging import sys import os from importlib.metadata import entry_points, EntryPoint from pathlib import Path from typer.core import TyperGroup from typing import Annotated, Any, Dict, Optional, List if sys.version_info >= (3, 8): from typing import Literal # pragma: no cover else: # pragma: no cover from typing_extensions import Literal # pragma: no cover # project from kiwi.exceptions import ( KiwiUnknownServiceName, KiwiCommandNotLoaded, KiwiLoadCommandUndefined, KiwiLoadPluginError, ) from kiwi.version import __version__ # from kiwi.help import Help from kiwi.defaults import Defaults log = logging.getLogger('kiwi') class PassThroughGroup(TyperGroup): """ **Command group for commands passing arguments to kiwi** Used by the system boxbuild and system stackbuild commands. Accepts the '--' separator as an alias for the kiwi subcommand, which keeps command lines written for the former kiwi-boxed-plugin and kiwi-stackbuild-plugin working """ def parse_args(self, ctx: Any, args: List[str]) -> List[str]: # ctx is a click Context, click is vendored by newer typer # versions such that no common import location exists if '--' in args: separator = args.index('--') args = args[:separator] + ['kiwi'] + args[separator + 1 :] return super().parse_args(ctx, args) class ManualPageGroup(TyperGroup): """ **Command group for commands providing a manual page** Used by the system, result and image services. Accepts the positional argument 'help' after a command, e.g. "kiwi-ng system build help", which shows the manual page of that command. typer cannot express a trailing positional argument which replaces all options of a command, in particular not if the command has required options. Thus the request is recognized before the command line of the command gets parsed and the command itself is not invoked. The task of the command shows the manual page instead """ def parse_args(self, ctx: Any, args: List[str]) -> List[str]: # ctx is a click Context, click is vendored by newer typer # versions such that no common import location exists if ( len(args) == 2 and args[1] == 'help' and self.get_command(ctx, args[0]) ): Cli.request_manual_page(ctx.info_name, args[0]) raise typer.Exit(0) return super().parse_args(ctx, args) class Cli: """ **Implements the main command line interface** An instance of the Cli class builds the entry point for the application and implements methods to load further command plugins which itself provides their own command line interface """ global_args: Dict = {} subcommand_args: Dict = {} plugins: Dict[str, typer.Typer] = {} cli_ok = False # plugins which provided commands that are now part of kiwi obsolete_plugins = ['kiwi_boxed_plugin', 'kiwi_stackbuild_plugin'] # system system = typer.Typer( cls=ManualPageGroup, help='system command for building images. The system space ' 'can also be extended by custom command plugins.', ) # result result = typer.Typer( cls=ManualPageGroup, help='result command for listing image result information ' 'and create result bundles.', ) # image image = typer.Typer( cls=ManualPageGroup, help='image command for retrieving image information ' 'prior building.', ) # system boxbuild boxbuild = typer.Typer( cls=PassThroughGroup, add_completion=True, invoke_without_command=True ) # system stackbuild stackbuild = typer.Typer( cls=PassThroughGroup, add_completion=True, invoke_without_command=True ) system.add_typer(boxbuild, name='boxbuild') system.add_typer(stackbuild, name='stackbuild') cli = typer.Typer() cli.add_typer(system, name='system') cli.add_typer(result, name='result') cli.add_typer(image, name='image') def __init__(self) -> None: Cli.cli_ok = False exit_code = 1 # load plugins... Cli.plugins.update(self.load_plugin_cli()) # add plugin cli's if there are any loaded for system_subcommand in sorted(Cli.plugins.keys()): Cli.system.add_typer( Cli.plugins[system_subcommand], name=system_subcommand, context_settings={'obj': Cli}, ) try: Cli.cli() except SystemExit as e: # avoid standalone mode: typer always exits here once the # command line is parsed. Keep the status of a usage error # and of a clean exit without a command to run, such as # --help, --version or a manual page request if e.code: sys.exit(e.code) if not Cli.cli_ok: sys.exit(0) if not Cli.cli_ok: sys.exit(exit_code) self.command_args = self.get_command_args(raise_if_no_command=False) self.command_loaded = None def version(self, perform: bool) -> None: if perform: print(f'KIWI (next generation) version {__version__}') raise typer.Exit(0) @staticmethod def _as_path_name(path: Optional[Path]) -> Optional[str]: """ Convert an optional Path option value into its string representation. Options annotated with a Path type are provided as PosixPath instance which cannot be used in a string context. An unset option stays unset :param Path path: option value or None :return: path name or None :rtype: str|None """ return f'{path}' if path is not None else None @staticmethod def _as_path_names(paths: Optional[List[Path]]) -> List[str]: """ Convert an optional list of Path option values into a list of their string representations :param list paths: list of option values or None :return: list of path names :rtype: list """ return [f'{path}' for path in paths or []] @staticmethod @cli.callback() def main( color_output: Annotated[ bool, typer.Option( '--color-output', help='use colors for warning and error messages', ), ] = False, config: Annotated[ Optional[Path], typer.Option( help='use specified runtime configuration file. If ' 'not specified the runtime configuration is looked ' 'up at ~/.config/kiwi/config.yml or /etc/kiwi.yml' ), ] = None, debug: Annotated[ bool, typer.Option( '--debug', help='print debug information, same as: --loglevel 10', ), ] = False, debug_run_scripts_in_screen: Annotated[ bool, typer.Option( '--debug-run-scripts-in-screen', help='run scripts called by kiwi in a screen session', ), ] = False, kiwi_file: Annotated[ Optional[str], typer.Option( help=' Basename of kiwi file which contains ' 'the main image configuration elements. If not specified ' 'kiwi searches for a file named config.xml or a file ' 'matching *.kiwi' ), ] = None, logfile: Annotated[ Optional[Path], typer.Option( help=' create a log file containing all log ' 'information including debug information even if this ' 'was not requested by the debug switch. The special ' 'call: "--logfile stdout" sends all information to ' 'standard out instead of writing to a file' ), ] = None, logsocket: Annotated[ Optional[Path], typer.Option( help=' send log data to the given Unix ' 'Domain socket in the same format as with --logfile' ), ] = None, loglevel: Annotated[ Optional[int], typer.Option( help=' specify logging level as number. ' 'Details about the available log levels can be found at: ' 'https://docs.python.org/3/library/logging.html#logging-levels ' 'Setting a log level causes all message >= level to be ' 'displayed.' ), ] = None, profile: Annotated[ Optional[List[str]], typer.Option( help=' Profile name, multiple profiles can be selected ' 'by passing this option multiple times' ), ] = [], setenv: Annotated[ Optional[List[str]], typer.Option( help=' export environment variable and its ' 'value into the caller environment. This option can be ' 'specified multiple times ' ), ] = [], shared_cache_dir: Annotated[ Optional[Path], typer.Option( help=' An alternative shared cache directory. ' 'The directory is shared via bind mount between the ' 'build host and image root system and contains ' 'information about package repositories and their ' 'cache and meta data.' ), ] = Path(os.sep + Defaults.get_shared_cache_location()), target_arch: Annotated[ Optional[str], typer.Option( help=' set the image architecture. By default the host ' 'architecture is used as the image architecture. If the ' 'specified architecture name does not match the host ' 'architecture and is therefore requesting a cross ' 'architecture image build, it is important to understand ' 'that for this process to work a preparatory step to ' 'support the image architecture and binary format on the ' 'building host is required first.' ), ] = None, temp_dir: Annotated[ Optional[Path], typer.Option( help=' An alternative base temporary directory. ' 'The provided path is used as base directory to store ' 'temporary files and directories.' ), ] = Path(Defaults.get_temp_location()), type: Annotated[ Optional[str], typer.Option( help=' Image build type. If not set the ' 'default XML specified build type will be used' ), ] = None, version: Annotated[ Optional[bool], typer.Option( '--version', help='show program version', callback=version ), ] = None, ) -> None: """ KIWI - Appliance Builder """ # start from a clean state, get_command_args() merges the # subcommand arguments into the global arguments Cli.global_args = {} Cli.global_args['--color-output'] = color_output Cli.global_args['--config'] = Cli._as_path_name(config) Cli.global_args['--debug'] = debug Cli.global_args['--debug-run-scripts-in-screen'] = ( debug_run_scripts_in_screen ) Cli.global_args['--kiwi-file'] = kiwi_file Cli.global_args['--logfile'] = Cli._as_path_name(logfile) Cli.global_args['--loglevel'] = loglevel Cli.global_args['--logsocket'] = Cli._as_path_name(logsocket) Cli.global_args['--profile'] = profile Cli.global_args['--setenv'] = setenv Cli.global_args['--shared-cache-dir'] = Cli._as_path_name( shared_cache_dir ) Cli.global_args['--target-arch'] = target_arch Cli.global_args['--temp-dir'] = Cli._as_path_name(temp_dir) Cli.global_args['--type'] = type Cli.global_args['command'] = None Cli.global_args['image'] = False Cli.global_args['result'] = False Cli.global_args['system'] = False @staticmethod def request_manual_page(service: str, command: str) -> None: """ Select the given command for showing its manual page Called for e.g. "kiwi-ng system build help". The command is selected like on a regular call but only carries the help flag. The task of the command checks for this flag and shows the manual page instead of running the command :param str service: service name, e.g. system :param str command: command name, e.g. build """ Cli.subcommand_args[command] = {'help': True} Cli.global_args[command] = True Cli.global_args['command'] = command Cli.global_args[service] = True Cli.cli_ok = True @staticmethod @image.command() def info( description: Annotated[ Path, typer.Option( help=' The description must be a directory ' 'containing a kiwi XML description and ' 'optional metadata files' ), ], resolve_package_list: Annotated[ Optional[bool], typer.Option( '--resolve-package-list', help='solve package dependencies and return a ' 'list of all packages including their attributes ' 'e.g. size, shasum, etc...', ), ] = False, list_profiles: Annotated[ Optional[bool], typer.Option( '--list-profiles', help='list profiles available for the selected/default type', ), ] = False, print_kiwi_env: Annotated[ Optional[bool], typer.Option( '--print-kiwi-env', help='list profiles available for the selected/default type', ), ] = False, ignore_repos: Annotated[ Optional[bool], typer.Option( '--ignore-repos', help='ignore all repos from the XML configuration', ), ] = False, add_repo: Annotated[ Optional[List[str]], typer.Option( help=' Add repository source, ' 'type, alias, priority, imageinclude(true|false), ' 'package_gpgcheck(true|false), list of signing_keys ' 'enclosed in curly brackets delimited by a colon, ' 'component list for debian based repos as string ' 'delimited by a space, main distribution name for ' 'debian based repos, repo_gpgcheck(true|false) and ' 'repo_sourcetype(metalink|baseurl|mirrorlist). The ' 'option can be specified multiple times' ), ] = [], add_repo_credentials: Annotated[ Optional[List[str]], typer.Option( help=' ' 'For repo sources of the form: uri://user:pass@location, ' 'set the user and password connected to the set-repo ' 'specification. If the provided value describes a ' 'filename in the filesystem, the first line of that ' 'file is read and used as credentials information.' '--add-repo-credentials is connected with the first ' '--add-repo specification and so on. The option can be ' 'specified multiple times' ), ] = [], print_xml: Annotated[ Optional[bool], typer.Option('--print-xml', help='print image description in XML'), ] = False, print_yaml: Annotated[ Optional[bool], typer.Option( '--print-yaml', help='print image description in YAML' ), ] = False, print_toml: Annotated[ Optional[bool], typer.Option( '--print-toml', help='print image description in TOML' ), ] = False, ) -> None: """ Provide information about the specified image description """ Cli.subcommand_args['info'] = { '--description': Cli._as_path_name(description), '--resolve-package-list': resolve_package_list, '--list-profiles': list_profiles, '--print-kiwi-env': print_kiwi_env, '--ignore-repos': ignore_repos, '--add-repo': add_repo, '--add-repo-credentials': add_repo_credentials, '--print-xml': print_xml, '--print-yaml': print_yaml, '--print-toml': print_toml, 'help': False, } Cli.global_args['info'] = True Cli.global_args['command'] = 'info' Cli.global_args['image'] = True Cli.cli_ok = True @staticmethod @image.command() def resize( target_dir: Annotated[ Path, typer.Option( help=' The target directory ' 'to expect image build results' ), ], size: Annotated[ str, typer.Option( help=' New size of the image. The value is either ' 'a size in bytes or can be specified with m=MB ' 'or g=GB. Example: 20g' ), ], root: Annotated[ Optional[Path], typer.Option( help=' The path to the root directory, if not ' 'specified kiwi searches the root directory ' 'in build/image-root below the specified target ' 'directory' ), ] = None, ) -> None: """ For disk based images, allow to resize the image to a new disk geometry. The additional space is free and not in use by the image. In order to make use of the additional free space a repartition process is required like it is provided by kiwi's oem boot code. Therefore the resize operation is useful for oem image builds most of the time """ Cli.subcommand_args['resize'] = { '--target-dir': Cli._as_path_name(target_dir), '--size': size, '--root': Cli._as_path_name(root), 'help': False, } Cli.global_args['resize'] = True Cli.global_args['command'] = 'resize' Cli.global_args['image'] = True Cli.cli_ok = True @staticmethod @result.command() def list( target_dir: Annotated[ Path, typer.Option( help='the target directory to expect image build results' ), ], ) -> None: """ List result information from a previously built image """ Cli.subcommand_args['list'] = { '--target-dir': Cli._as_path_name(target_dir), 'help': False, } Cli.global_args['list'] = True Cli.global_args['command'] = 'list' Cli.global_args['result'] = True Cli.cli_ok = True @staticmethod @result.command() def bundle( target_dir: Annotated[ Path, typer.Option( help=' The target directory to ' 'expect image build results' ), ], id: Annotated[ str, typer.Option( help=' The bundle id. A free form text ' 'appended to the version information of the result ' 'image filename' ), ], bundle_dir: Annotated[ str, typer.Option( help=' Directory to store the bundle results' ), ], bundle_format: Annotated[ Optional[str], typer.Option( help=' The bundle format to create the bundle. ' 'If provided this setting will overwrite an eventually ' 'provided bundle_format attribute from the main ' 'image description' ), ] = None, zsync_source: Annotated[ Optional[str], typer.Option( help=' Specify the download ' 'location from which the bundle file(s) can be ' 'fetched from. The information is effective if zsync ' 'is used to sync the bundle. The zsync control file ' 'is only created for those bundle files which are ' 'marked for compression because in a kiwi build ' 'only those are meaningful for a partial binary ' 'file download. It is expected that all files from a ' 'bundle are placed to the same download location' ), ] = None, package_as_rpm: Annotated[ Optional[bool], typer.Option( '--package-as-rpm', help='Take all result files and create an rpm package out of it', ), ] = False, no_compress: Annotated[ Optional[bool], typer.Option( '--no-compress', help='Do not compress the result image file(s)' ), ] = False, ) -> None: """ Create result bundle from the image build results in the specified target directory. Each result image will contain the specified bundle identifier as part of its filename. Uncompressed image files will also become xz compressed and a sha sum will be created from every result image. """ Cli.subcommand_args['bundle'] = { '--target-dir': Cli._as_path_name(target_dir), '--id': id, '--bundle-dir': bundle_dir, '--bundle-format': bundle_format, '--zsync-source': zsync_source, '--package-as-rpm': package_as_rpm, '--no-compress': no_compress, 'help': False, } Cli.global_args['bundle'] = True Cli.global_args['command'] = 'bundle' Cli.global_args['result'] = True Cli.cli_ok = True @staticmethod @system.command() def build( description: Annotated[ Path, typer.Option( help=' The description must be a ' 'directory containing a kiwi XML description ' 'and optional metadata files' ), ], target_dir: Annotated[ Path, typer.Option( help=' The target directory to ' 'store the system image file(s)' ), ], allow_existing_root: Annotated[ Optional[bool], typer.Option( '--allow-existing-root', help='Allow to use an existing root directory ' 'from an earlier build attempt. Use with caution ' 'this could cause an inconsistent root tree if the ' 'existing contents does not fit to the former ' 'image type setup', ), ] = False, clear_cache: Annotated[ Optional[bool], typer.Option( '--clear-cache', help='Delete repository cache for each of the ' 'used repositories before installing any package', ), ] = False, ignore_repos: Annotated[ Optional[bool], typer.Option( '--ignore-repos', help='Ignore all repos from the XML configuration', ), ] = False, ignore_repos_used_for_build: Annotated[ Optional[bool], typer.Option( '--ignore-repos-used-for-build', help='Ignore all repos from the XML configuration ' 'except the ones marked as imageonly', ), ] = False, set_repo: Annotated[ Optional[str], typer.Option( help=' Overwrite the first XML ' 'listed repository source, type, alias, priority, ' 'imageinclude(true|false), package_gpgcheck(true|false), ' 'list of signing_keys enclosed in curly brackets delimited ' 'by a colon, component list for debian based repos as string ' 'delimited by a space, main distribution name for ' 'debian based repos, repo_gpgcheck(true|false) and ' 'repo_sourcetype(metalink|baseurl|mirrorlist)' ), ] = None, set_repo_credentials: Annotated[ Optional[str], typer.Option( help=' ' 'For repo sources of the form: uri://user:pass@location, ' 'set the user and password connected to the set-repo ' 'specification. If the provided value describes a ' 'filename in the filesystem, the first line of that ' 'file is read and used as credentials information.' ), ] = None, add_repo: Annotated[ Optional[List[str]], typer.Option( help='Same as --set-repo, but it adds the repo to the ' 'current list of repositories. The option can be specified ' 'multiple times' ), ] = [], add_repo_credentials: Annotated[ Optional[List[str]], typer.Option( help='Same as --set-repo-credentials, but The first ' '--add-repo-credentials is connected with the first ' '--add-repo specification and so on. The option can be ' 'specified multiple times' ), ] = [], add_package: Annotated[ Optional[List[str]], typer.Option( help=' Install the given package name ' 'The option can be specified multiple times' ), ] = [], add_bootstrap_package: Annotated[ Optional[List[str]], typer.Option( help=' Install the given package name as ' 'part of the early bootstrap process. The option ' 'can be specified multiple times' ), ] = [], delete_package: Annotated[ Optional[List[str]], typer.Option( help=' Delete the given package name ' 'The option can be specified multiple times' ), ] = [], set_container_derived_from: Annotated[ Optional[str], typer.Option( help=' Overwrite the source location of the ' 'base container for the selected image type. ' 'The setting is only effective if the configured ' 'image type is setup with an initial derived_from reference' ), ] = None, set_container_tag: Annotated[ Optional[str], typer.Option( help=' Overwrite the container tag in the ' 'container configuration. The setting is only ' 'effective if the container configuraiton provides ' 'an initial tag value' ), ] = None, add_container_label: Annotated[ Optional[List[str]], typer.Option( help=' Add a container label in the ' 'container configuration metadata. The label with ' 'the provided key-value pair will be overwritten ' 'in case it was already defined in the XML description. ' 'The option can be specified multiple times' ), ] = [], set_type_attr: Annotated[ Optional[List[str]], typer.Option( help=' Overwrite/set the attribute ' 'with the provided value in the selected build type ' 'section. The option can be specified multiple times' ), ] = [], set_release_version: Annotated[ Optional[str], typer.Option( help=' Overwrite/set the release-version ' 'element in the selected build type preferences section' ), ] = None, signing_key: Annotated[ Optional[List[Path]], typer.Option( help=' Includes the given key-file as a trusted ' 'key for package manager validations. The option can be ' 'specified multiple times' ), ] = [], ca_cert: Annotated[ Optional[List[Path]], typer.Option( help='include additional CA certificate to import immediately ' 'after bootstrap and make available during the build process.' ), ] = [], ca_target_distribution: Annotated[ Optional[Literal['suse', 'redhat', 'debian', 'archlinux']], typer.Option( help='Specify target distribution for the import of ' 'certificates via the --ca-cert options(s) and/or the ' 'provided from the image description. ' 'The selected distribution is used in KIWI to map the ' 'distribution specific CA storage path and update tool ' 'for the import process.' ), ] = None, ) -> None: """ Build a system image from the specified description. The build command combines the prepare and create commands. """ Cli.subcommand_args['build'] = { '--description': Cli._as_path_name(description), '--target-dir': Cli._as_path_name(target_dir), '--allow-existing-root': allow_existing_root, '--clear-cache': clear_cache, '--ignore-repos': ignore_repos, '--ignore-repos-used-for-build': ignore_repos_used_for_build, '--set-repo': set_repo, '--set-repo-credentials': set_repo_credentials, '--add-repo': add_repo, '--add-repo-credentials': add_repo_credentials, '--add-package': add_package, '--add-bootstrap-package': add_bootstrap_package, '--delete-package': delete_package, '--set-container-derived-from': set_container_derived_from, '--set-container-tag': set_container_tag, '--add-container-label': add_container_label, '--set-type-attr': set_type_attr, '--set-release-version': set_release_version, '--signing-key': Cli._as_path_names(signing_key), '--ca-cert': Cli._as_path_names(ca_cert), '--ca-target-distribution': ca_target_distribution, 'help': False, } Cli.global_args['build'] = True Cli.global_args['command'] = 'build' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @system.command() def prepare( description: Annotated[ Path, typer.Option( help=' The description must be a ' 'directory containing a kiwi XML description ' 'and optional metadata files' ), ], root: Annotated[ Path, typer.Option( help=' The path to the new root ' 'directory of the system ' ), ], allow_existing_root: Annotated[ Optional[bool], typer.Option( '--allow-existing-root', help='Allow to use an existing root directory ' 'from an earlier build attempt. Use with caution ' 'this could cause an inconsistent root tree if the ' 'existing contents does not fit to the former ' 'image type setup', ), ] = False, clear_cache: Annotated[ Optional[bool], typer.Option( '--clear-cache', help='Delete repository cache for each of the ' 'used repositories before installing any package', ), ] = False, ignore_repos: Annotated[ Optional[bool], typer.Option( '--ignore-repos', help='Ignore all repos from the XML configuration', ), ] = False, ignore_repos_used_for_build: Annotated[ Optional[bool], typer.Option( '--ignore-repos-used-for-build', help='Ignore all repos from the XML configuration ' 'except the ones marked as imageonly', ), ] = False, set_repo: Annotated[ Optional[str], typer.Option( help=' Overwrite the first XML ' 'listed repository source, type, alias, priority, ' 'imageinclude(true|false), package_gpgcheck(true|false), ' 'list of signing_keys enclosed in curly brackets delimited ' 'by a colon, component list for debian based repos as string ' 'delimited by a space, main distribution name for ' 'debian based repos, repo_gpgcheck(true|false) and ' 'repo_sourcetype(metalink|baseurl|mirrorlist)' ), ] = None, set_repo_credentials: Annotated[ Optional[str], typer.Option( help=' ' 'For repo sources of the form: uri://user:pass@location, ' 'set the user and password connected to the set-repo ' 'specification. If the provided value describes a ' 'filename in the filesystem, the first line of that ' 'file is read and used as credentials information.' ), ] = None, add_repo: Annotated[ Optional[List[str]], typer.Option( help='Same as --set-repo, but it adds the repo to the ' 'current list of repositories. The option can be specified ' 'multiple times' ), ] = [], add_repo_credentials: Annotated[ Optional[List[str]], typer.Option( help='Same as --set-repo-credentials, but The first ' '--add-repo-credentials is connected with the first ' '--add-repo specification and so on. The option can be ' 'specified multiple times' ), ] = [], add_package: Annotated[ Optional[List[str]], typer.Option( help=' Install the given package name ' 'The option can be specified multiple times' ), ] = [], add_bootstrap_package: Annotated[ Optional[List[str]], typer.Option( help=' Install the given package name as ' 'part of the early bootstrap process. The option ' 'can be specified multiple times' ), ] = [], delete_package: Annotated[ Optional[List[str]], typer.Option( help=' Delete the given package name ' 'The option can be specified multiple times' ), ] = [], set_container_derived_from: Annotated[ Optional[str], typer.Option( help=' Overwrite the source location of the ' 'base container for the selected image type. ' 'The setting is only effective if the configured ' 'image type is setup with an initial derived_from reference' ), ] = None, set_container_tag: Annotated[ Optional[str], typer.Option( help=' Overwrite the container tag in the ' 'container configuration. The setting is only ' 'effective if the container configuraiton provides ' 'an initial tag value' ), ] = None, add_container_label: Annotated[ Optional[List[str]], typer.Option( help=' Add a container label in the ' 'container configuration metadata. The label with ' 'the provided key-value pair will be overwritten ' 'in case it was already defined in the XML description. ' 'The option can be specified multiple times' ), ] = [], set_type_attr: Annotated[ Optional[List[str]], typer.Option( help=' Overwrite/set the attribute ' 'with the provided value in the selected build type ' 'section. The option can be specified multiple times' ), ] = [], set_release_version: Annotated[ Optional[str], typer.Option( help=' Overwrite/set the release-version ' 'element in the selected build type preferences section' ), ] = None, signing_key: Annotated[ Optional[List[Path]], typer.Option( help=' Includes the given key-file as a trusted ' 'key for package manager validations. The option can be ' 'specified multiple times' ), ] = [], ca_cert: Annotated[ Optional[List[Path]], typer.Option( help='include additional CA certificate to import immediately ' 'after bootstrap and make available during the build process.' ), ] = [], ca_target_distribution: Annotated[ Optional[Literal['suse', 'redhat', 'debian', 'archlinux']], typer.Option( help='Specify target distribution for the import of ' 'certificates via the --ca-cert options(s) and/or the ' 'provided from the image description. ' 'The selected distribution is used in KIWI to map the ' 'distribution specific CA storage path and update tool ' 'for the import process.' ), ] = None, ) -> None: """ Prepare and install a new system root tree for chroot access. """ Cli.subcommand_args['prepare'] = { '--description': Cli._as_path_name(description), '--root': Cli._as_path_name(root), '--allow-existing-root': allow_existing_root, '--clear-cache': clear_cache, '--ignore-repos': ignore_repos, '--ignore-repos-used-for-build': ignore_repos_used_for_build, '--set-repo': set_repo, '--set-repo-credentials': set_repo_credentials, '--add-repo': add_repo, '--add-repo-credentials': add_repo_credentials, '--add-package': add_package, '--add-bootstrap-package': add_bootstrap_package, '--delete-package': delete_package, '--set-container-derived-from': set_container_derived_from, '--set-container-tag': set_container_tag, '--add-container-label': add_container_label, '--set-type-attr': set_type_attr, '--set-release-version': set_release_version, '--signing-key': Cli._as_path_names(signing_key), '--ca-cert': Cli._as_path_names(ca_cert), '--ca-target-distribution': ca_target_distribution, 'help': False, } Cli.global_args['prepare'] = True Cli.global_args['command'] = 'prepare' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @system.command() def update( root: Annotated[ Path, typer.Option(help=' The path to the root directory'), ], add_package: Annotated[ Optional[List[str]], typer.Option( help=' Install the given package name ' 'The option can be specified multiple times' ), ] = [], delete_package: Annotated[ Optional[List[str]], typer.Option( help=' Delete the given package name ' 'The option can be specified multiple times' ), ] = [], ) -> None: """ Update root system with latest repository updates and optionally allow to add or delete packages. """ Cli.subcommand_args['update'] = { '--root': Cli._as_path_name(root), '--add-package': add_package, '--delete-package': delete_package, 'help': False, } Cli.global_args['update'] = True Cli.global_args['command'] = 'update' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @system.command() def create( root: Annotated[ Path, typer.Option(help=' The path to the root directory'), ], target_dir: Annotated[ Path, typer.Option( help=' The target directory to ' 'store the system image file(s)' ), ], signing_key: Annotated[ Optional[List[Path]], typer.Option( help=' Includes the given key-file as a trusted ' 'key for package manager validations. The option can be ' 'specified multiple times' ), ] = [], ) -> None: """ Create an image from the specified root directory. """ Cli.subcommand_args['create'] = { '--root': Cli._as_path_name(root), '--target-dir': Cli._as_path_name(target_dir), '--signing-key': Cli._as_path_names(signing_key), 'help': False, } Cli.global_args['create'] = True Cli.global_args['command'] = 'create' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @boxbuild.callback( help='build a system image in a self contained VM or container', subcommand_metavar='kiwi [OPTIONS]', ) def boxbuild_options( box: Annotated[ Optional[str], typer.Option( help=' Name of the box to use for the build process.' ), ] = None, list_boxes: Annotated[ Optional[bool], typer.Option('--list-boxes', help='show available build boxes.'), ] = False, box_memory: Annotated[ Optional[str], typer.Option( help=' Number of GBs to reserve as main memory ' 'for the virtual machine. By default 8GB will be used.' ), ] = None, box_console: Annotated[ Optional[str], typer.Option( help=' Name of console in the kernel settings ' 'for the virtual machine. By default set to hvc0.' ), ] = None, box_smp_cpus: Annotated[ Optional[int], typer.Option( help=' Number of CPUs to use in the SMP setup. ' 'By default 4 CPUs will be used.' ), ] = 4, box_debug: Annotated[ Optional[bool], typer.Option( '--box-debug', help='In debug mode the started virtual machine will be kept open.', ), ] = False, container: Annotated[ Optional[bool], typer.Option( '--container', help='Build in container instead of a VM. Options related to ' 'building in a VM will have no effect.', ), ] = False, kiwi_version: Annotated[ Optional[str], typer.Option( help=' Specify a KIWI version to use for ' 'the build. The referenced KIWI will be fetched from ' 'pip and replaces the box installed KIWI version. ' 'Note: If --no-snapshot is used in combination ' 'with this option, the change of the KIWI version will ' 'be permanently stored in the used box.' ), ] = None, shared_path: Annotated[ Optional[Path], typer.Option( help=' Optional host path to share with the box. ' 'The same path as it is present on the host will also ' 'be available inside of the box during build time.' ), ] = None, no_update_check: Annotated[ Optional[bool], typer.Option( '--no-update-check', help='Skip check for available box update. The option ' 'has no effect if the selected box does not yet exist ' 'on the host.', ), ] = False, no_snapshot: Annotated[ Optional[bool], typer.Option( '--no-snapshot', help='Run box with snapshot mode switched off. This ' 'causes the box disk file to be modified by the build ' 'process and allows to keep a persistent package cache ' 'as part of the box. The option can be used to increase ' 'the build performance due to data stored in the box ' 'which does not have to be reloaded from the network. ' 'On the contrary this option invalidates the immutable ' 'box attribute and should be used with care. On update ' 'of the box all data stored will be wiped. To prevent ' 'this combine the option with the --no-update-check option.', ), ] = False, no_accel: Annotated[ Optional[bool], typer.Option( '--no-accel', help='Run box without hardware acceleration. By default ' 'KVM acceleration is activated', ), ] = False, qemu_9p_sharing: Annotated[ Optional[bool], typer.Option( '--9p-sharing', help='Select 9p backend to use for sharing data ' 'between the host and the box.', ), ] = False, virtiofs_sharing: Annotated[ Optional[bool], typer.Option( '--virtiofs-sharing', help='Select virtiofsd backend to use for sharing data ' 'between the host and the box.', ), ] = False, sshfs_sharing: Annotated[ Optional[bool], typer.Option( '--sshfs-sharing', help='Select sshfs backend to use for sharing data ' 'between the host and the box.', ), ] = False, ssh_key: Annotated[ Optional[str], typer.Option( help=' Name of ssh key to authorize for ' 'connection. By default id_rsa is used.' ), ] = 'id_rsa', ssh_port: Annotated[ Optional[int], typer.Option( help=' Port number to use to forward the ' 'guest SSH port to the host By default 10022 is used.' ), ] = 10022, x86_64: Annotated[ Optional[bool], typer.Option( '--x86_64', help='Select box for the x86_64 architecture. If no ' 'architecture is selected the host architecture is ' 'used for selecting the box. The selected box ' 'architecture also specifies the target architecture ' 'for the image build with that box.', ), ] = False, aarch64: Annotated[ Optional[bool], typer.Option( '--aarch64', help='Select box for the aarch64 architecture. If no ' 'architecture is selected the host architecture is ' 'used for selecting the box. The selected box ' 'architecture also specifies the target architecture ' 'for the image build with that box.', ), ] = False, machine: Annotated[ Optional[str], typer.Option( help=' Machine name used ' 'by QEMU. By default no specific value is used here ' 'and qemu selects its default machine type. For cross ' 'arch builds or for system architectures for which ' 'QEMU defines no default like for Arm, it is required ' 'to specify a machine name. If you do not care about ' 'reproducing the idiosyncrasies of a particular bit ' 'of hardware, the best option is to use the virt ' 'machine type.' ), ] = None, cpu: Annotated[ Optional[str], typer.Option( help=' CPU type used by QEMU. By default ' 'the host CPU type is used which is only a good ' 'selection if the host and the selected box are from ' 'the same architecture. On cross arch builds it is ' 'required to specify the CPU emulation the box should use' ), ] = None, ) -> None: Cli.subcommand_args['boxbuild'] = { '--box': box, '--list-boxes': list_boxes, '--box-memory': box_memory, '--box-console': box_console, '--box-smp-cpus': f'{box_smp_cpus}', '--box-debug': box_debug, '--container': container, '--kiwi-version': kiwi_version, '--shared-path': Cli._as_path_name(shared_path), '--no-update-check': no_update_check, '--no-snapshot': no_snapshot, '--no-accel': no_accel, '--9p-sharing': qemu_9p_sharing, '--virtiofs-sharing': virtiofs_sharing, '--sshfs-sharing': sshfs_sharing, '--ssh-key': ssh_key, '--ssh-port': f'{ssh_port}', '--x86_64': x86_64, '--aarch64': aarch64, '--machine': machine, '--cpu': cpu, 'system_build': [], 'help': False, } Cli.global_args['boxbuild'] = True Cli.global_args['command'] = 'boxbuild' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @boxbuild.command( name='kiwi', context_settings={ 'allow_extra_args': True, 'ignore_unknown_options': True, }, ) def boxbuild_kiwi(ctx: typer.Context) -> None: """ List of command parameters as supported by the kiwi-ng build command. The information given here is passed along to the kiwi-ng system build command running in the virtual machine or container. """ Cli.subcommand_args['boxbuild']['system_build'] = ctx.args @staticmethod @stackbuild.callback( help='build an image based on a given stash container root.\n\n' 'If no --description is provided, stackbuild rebuilds the ' 'image from ' 'the stash container and passes the kiwi subcommand arguments ' 'to the kiwi-ng system create command. If a --description is ' 'provided, this description takes over precedence and a new ' 'image from this description based on the given stash container ' 'root will be built. In this case the kiwi subcommand arguments ' 'are passed to the kiwi-ng system build command.', subcommand_metavar='kiwi [OPTIONS]', ) def stackbuild_options( stash: Annotated[ List[str], typer.Option( help=' Name of the stash container. See system stash ' '--list for available stashes. Multiple --stash options will ' 'be stacked together in the given order' ), ], target_dir: Annotated[ Path, typer.Option( help=' The target directory to store the ' 'system image file(s)' ), ], description: Annotated[ Optional[Path], typer.Option(help=' Path to KIWI image description'), ] = None, from_registry: Annotated[ Optional[str], typer.Option( help=' Pull given stash container name from the ' 'provided registry URI' ), ] = None, ) -> None: Cli.subcommand_args['stackbuild'] = { '--stash': stash, '--target-dir': Cli._as_path_name(target_dir), '--description': Cli._as_path_name(description), '--from-registry': from_registry, 'system_build_or_create': [], 'help': False, } Cli.global_args['stackbuild'] = True Cli.global_args['command'] = 'stackbuild' Cli.global_args['system'] = True Cli.cli_ok = True @staticmethod @stackbuild.command( name='kiwi', context_settings={ 'allow_extra_args': True, 'ignore_unknown_options': True, }, ) def stackbuild_kiwi(ctx: typer.Context) -> None: """ List of command parameters as supported by the kiwi-ng build or create command. The information given here is passed along to the kiwi-ng system build or the kiwi-ng system create command depending on the presence of the --description option. """ Cli.subcommand_args['stackbuild']['system_build_or_create'] = ctx.args @staticmethod @system.command() def stash( root: Annotated[ Optional[Path], typer.Option( help=' The path to the root directory, ' 'usually the result of a former system prepare or ' 'build call' ), ] = None, tag: Annotated[ Optional[str], typer.Option( help=' The tag name for the container. ' 'By default set to: latest' ), ] = None, container_name: Annotated[ Optional[str], typer.Option( help=' The name of the container. By default ' 'set to the image name of the stash' ), ] = None, stash_list: Annotated[ Optional[bool], typer.Option('--list', help='List the available stashes'), ] = False, ) -> None: """ Create a container from the given root directory. """ Cli.subcommand_args['stash'] = { '--root': Cli._as_path_name(root), '--tag': tag, '--container-name': container_name, '--list': stash_list, 'help': False, } Cli.global_args['stash'] = True Cli.global_args['command'] = 'stash' Cli.global_args['system'] = True Cli.cli_ok = True def get_servicename(self) -> str: """ Extract service name from argument parse result :return: service name :rtype: str """ if Cli.global_args.get('image') is True: return 'image' elif Cli.global_args.get('system') is True: return 'system' elif Cli.global_args.get('result') is True: return 'result' else: raise KiwiUnknownServiceName('Unknown/Invalid Servicename') def get_command(self) -> str: """ Extract selected command name :return: command name :rtype: str """ return Cli.global_args['command'] def get_command_args(self, raise_if_no_command: bool = True) -> Dict: """ Get argument dict for selected command including global options :return: Contains dictionary of command arguments .. code:: python { '--some-option-name': 'value' } :rtype: dict """ result = Cli.global_args command = self.get_command() if Cli.subcommand_args.get(command): result.update(Cli.subcommand_args.get(command) or {}) elif raise_if_no_command: raise KiwiCommandNotLoaded(f'{command} command not loaded') return result def get_global_args(self) -> Dict: """ Get argument dict for global arguments :return: Contains dictionary of global arguments .. code:: python { '--some-global-option': 'value' } :rtype: dict """ result = {} for arg, value in list(Cli.global_args.items()): if not arg == 'command': if arg == '--type' and value == 'vmx': log.warning( 'vmx type is now a subset of oem, --type set to oem' ) value = 'oem' if arg == '--shared-cache-dir' and value: Defaults.set_shared_cache_location(value) if arg == '--temp-dir' and value: Defaults.set_temp_location(value) if arg == '--target-arch' and value: Defaults.set_platform_name(value) if arg == '--config' and value: # pragma: no cover Defaults.set_custom_runtime_config_file(value) result[arg] = value return result def load_plugin_cli(self) -> Dict[str, typer.Typer]: """ Loads plugin command line interface The loading is based on the plugin entry point name and requires the presence of a Typer based cli.py. The implementation of cli.py in the plugin also requires to provide a dict named typers. The matching of an entry point to be a valid kiwi plugin requires the entry point group to be set to kiwi.tasks and the entry point value must container the _plugin substring. :return: list of typer.Typer instances :rtype: list """ plugin_typers = {} for entry in self._get_module_entries(): module_name = entry.value.split('.')[0] if module_name in Cli.obsolete_plugins: continue if '_plugin' in entry.value: plugin_entry = EntryPoint( name='cli', value=f'{module_name}.cli', group='kiwi.tasks' ) try: plugin = plugin_entry.load() plugin_typers.update(plugin.typers) except ModuleNotFoundError: # plugin gets skipped if it does not provide # a typer based cli pass except Exception as issue: raise KiwiLoadPluginError(f'{plugin_entry}: {issue}') return plugin_typers def load_command(self) -> Any: """ Loads task class plugin according to service and command name :return: loaded task module :rtype: object """ discovered_tasks = {} for entry in self._get_module_entries(): if entry.value.split('.')[0] in Cli.obsolete_plugins: continue discovered_tasks[entry.name] = entry.load() service = self.get_servicename() command = self.get_command() if not command: raise KiwiLoadCommandUndefined( 'No command specified for {0} service'.format(service) ) self.command_loaded = discovered_tasks.get(f'{service}_{command}') if not self.command_loaded: prefix = 'usage:' discovered_tasks_for_service = '' for task in discovered_tasks: if task.startswith(service): discovered_tasks_for_service += '{0} kiwi-ng {1}\n'.format( prefix, task.replace('_', ' ') ) prefix = ' ' raise KiwiCommandNotLoaded( 'Command "{0}" not found\n\n{1}'.format( command, discovered_tasks_for_service ) ) return self.command_loaded def _get_module_entries(self) -> List[EntryPoint]: """ Lookup module entries matching the kiwi.tasks entry group """ entries: List[EntryPoint] = [] if sys.version_info >= (3, 12): for entry in list(entry_points()): # pragma: no cover if entry.group == 'kiwi.tasks': entries.append(entry) else: # pragma: no cover for entry in dict.get(entry_points(), 'kiwi.tasks') or {}: entries.append(entry) return entries ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7433155 kiwi-11.1.8/kiwi/command.py0000644000000000000000000002014315273675667012456 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import ( IO, Callable, Literal, List, MutableMapping, NamedTuple, Optional, overload, ) import logging import os import select import subprocess # project from kiwi.utils.codec import Codec from kiwi.exceptions import KiwiCommandError, KiwiCommandNotFound log = logging.getLogger('kiwi') class CommandT(NamedTuple): output: str error: str returncode: int class CommandCallT(NamedTuple): output: IO[bytes] output_available: Callable[[], bool] error: IO[bytes] error_available: Callable[[], bool] process: subprocess.Popen class Command: """ **Implements command invocation** An instance of Command provides methods to invoke external commands in blocking and non blocking mode. Control of stdout and stderr is given to the caller """ @overload @staticmethod def run( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, raise_on_error: bool = True, stderr_to_stdout: bool = False, raise_on_command_not_found: Literal[False] = False, ) -> CommandT: ... # pragma: no cover @overload @staticmethod def run( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, raise_on_error: bool = True, stderr_to_stdout: bool = False, raise_on_command_not_found: bool = True, ) -> Optional[CommandT]: ... # pragma: no cover @staticmethod def run( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, raise_on_error: bool = True, stderr_to_stdout: bool = False, raise_on_command_not_found: bool = True, ) -> Optional[CommandT]: """ Execute a program and block the caller. The return value is a CommandT namedtuple containing the stdout, stderr and return code information. Unless raise_on_error is set to `False` an exception is thrown if the command exits with an error code not equal to zero. If raise_on_command_not_found is `False` and the command is not found, then `None` is returned. Example: .. code:: python result = Command.run(['ls', '-l']) :param list command: command and arguments :param dict custom_env: custom os.environ :param bool raise_on_error: control error behaviour :param bool stderr_to_stdout: redirects stderr to stdout :return: Contains call results in command type .. code:: python CommandT(output='string', error='string', returncode=int) :rtype: CommandT """ from .path import Path environment = custom_env or os.environ cmd_abspath: Optional[str] if command[0].startswith("/"): cmd_abspath = command[0] if not os.path.exists(cmd_abspath): cmd_abspath = None else: cmd_abspath = Path.which( command[0], custom_env=environment, access_mode=os.X_OK ) if not cmd_abspath: message = f'Command "{command[0]}" not found in the environment' if raise_on_command_not_found: raise KiwiCommandNotFound(message) log.debug('EXEC: %s', message) return None stderr = subprocess.STDOUT if stderr_to_stdout else subprocess.PIPE log.debug('EXEC: [%s]', ' '.join(command)) try: process = subprocess.Popen( [cmd_abspath] + command[1:], stdout=subprocess.PIPE, stderr=stderr, env=environment, ) except (OSError, subprocess.SubprocessError) as e: raise KiwiCommandError( f'{command[0]}: {type(e).__name__}: {format(e)}' ) from e output, error = process.communicate() if process.returncode != 0 and raise_on_error: if not error: error = bytes(b'(no output on stderr)') if not output: output = bytes(b'(no output on stdout)') log.debug( 'EXEC: Failed with stderr: {0}, stdout: {1}'.format( Codec.decode(error), Codec.decode(output) ) ) raise KiwiCommandError( '{0}: stderr: {1}, stdout: {2}'.format( command[0], Codec.decode(error), Codec.decode(output) ) ) return CommandT( output=Codec.decode(output), error=Codec.decode(error), returncode=process.returncode, ) @staticmethod def call( command: List[str], custom_env: Optional[MutableMapping[str, str]] = None, ) -> CommandCallT: """ Execute a program and return an io file handle pair back. stdout and stderr are both on different channels. The caller must read from the output file handles in order to actually run the command. This can be done using the CommandIterator from command_process Example: .. code:: python process = Command.call(['ls', '-l']) :param list command: command and arguments :param list custom_env: custom os.environ :return: Contains process results in command type .. code:: python command( output='string', output_available=bool, error='string', error_available=bool, process=subprocess ) :rtype: namedtuple """ from .path import Path log.debug('EXEC: [%s]', ' '.join(command)) environment = custom_env or os.environ if not Path.which( command[0], custom_env=environment, access_mode=os.X_OK ): raise KiwiCommandNotFound( f'Command "{command[0]}" not found in the environment' ) try: process = subprocess.Popen( command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=environment, ) except Exception as e: raise KiwiCommandError(f'{type(e).__name__}: {format(e)}') from e # guaranteed to be true as stdout & stderr equal subprocess.PIPE assert process.stdout and process.stderr # the narrowed types are kept in the select closures below stdout = process.stdout stderr = process.stderr def output_available() -> Callable[[], bool]: def _select() -> bool: readable, _, exceptional = select.select( [stdout], [], [stdout], 1e-4 ) if readable and not exceptional: return True return False return _select def error_available() -> Callable[[], bool]: def _select() -> bool: readable, _, exceptional = select.select( [stderr], [], [stderr], 1e-4 ) if readable and not exceptional: return True return False return _select return CommandCallT( output=process.stdout, output_available=output_available(), error=process.stderr, error_available=error_available(), process=process, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7433155 kiwi-11.1.8/kiwi/command_process.py0000644000000000000000000001706015273675667014220 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from collections import namedtuple from kiwi.command import CommandCallT from typing import Any, NamedTuple, List, Callable # project from kiwi.utils.codec import Codec from kiwi.logger import Logger from kiwi.exceptions import KiwiCommandError log = logging.getLogger('kiwi') class PollT(NamedTuple): stdout_line: str stderr_line: str class CommandProcess: """ **Implements processing of non blocking Command calls** Provides methods to iterate over non blocking instances of the Command class with and without progress information :param subprocess command: instance of subprocess :param string log_topic: topic string for logging """ def __init__( self, command: CommandCallT, log_topic: str = 'system' ) -> None: self.command = CommandIterator(command) self.log_topic = log_topic self.items_processed = 0 def poll(self) -> None: """ Iterate over process, raise on error and log output """ for lineT in self.command: line = lineT.stdout_line if line: log.debug('%s: %s', self.log_topic, line) if self.command.get_error_code() != 0: raise KiwiCommandError(self.command.get_error_output()) def poll_show_progress( self, items_to_complete: List[str], match_method: Callable, with_stderr: bool = False, ) -> None: """ Iterate over process and show progress in percent raise on error and log output :param list items_to_complete: all items :param function match_method: method matching item """ self._init_progress() for lineT in self.command: lines = [lineT.stdout_line] if with_stderr: lines.append(lineT.stderr_line) for line in lines: if line: log.debug('%s: %s', self.log_topic, line) self._update_progress(match_method, items_to_complete, line) self._stop_progress() if self.command.get_error_code() != 0: raise KiwiCommandError(self.command.get_error_output()) def poll_and_watch(self) -> Any: """ Iterate over process don't raise on error and log stdout and stderr """ log.info(self.log_topic) log.debug('--------------out start-------------') for lineT in self.command: line = lineT.stdout_line if line: log.debug(line) log.debug('--------------out stop--------------') error_code = self.command.get_error_code() error_output = self.command.get_error_output() result = namedtuple('result', ['stderr', 'returncode']) if error_output: log.debug('--------------err start-------------') for line in error_output.split(os.linesep): log.debug(line) log.debug('--------------err stop--------------') return result(stderr=error_output, returncode=error_code) def create_match_method( self, method: Callable[[str, str], bool] ) -> Callable[[str, str], bool]: """ create a matcher function pointer which calls the given method as method(item_to_match, data) on dereference :param function method: function reference :return: function pointer :rtype: object """ def create_method(item_to_match: str, data: str) -> bool: return method(item_to_match, data) return create_method def returncode(self) -> int: return self.command.get_error_code() def _init_progress(self) -> None: Logger.progress(0, 100, '[ INFO ]: Processing') def _stop_progress(self) -> None: Logger.progress(100, 100, '[ INFO ]: Processing') def _update_progress( self, match_method: Callable, items_to_complete: List[str], command_output: str, ) -> None: items_count = len(items_to_complete) for item in items_to_complete: if match_method(item, command_output): self.items_processed += 1 if self.items_processed <= items_count: Logger.progress( self.items_processed, items_count, '[ INFO ]: Processing', ) class CommandIterator: """ **Implements an Iterator for Instances of Command** :param subprocess command: instance of subprocess """ def __init__(self, command: CommandCallT) -> None: self.command = command self.command_error_output = bytes(b'') self.command_output_line = bytes(b'') self.command_error_line = bytes(b'') self.output_eof_reached = False self.errors_eof_reached = False def __next__(self) -> PollT: line_stdout = '' line_stderr = '' if self.command.process.poll() is not None: if self.output_eof_reached and self.errors_eof_reached: raise StopIteration() if self.command.output_available(): byte_read = self.command.output.read(1) if not byte_read: self.output_eof_reached = True elif byte_read == bytes(b'\n'): line_stdout = Codec.decode(self.command_output_line) self.command_output_line = bytes(b'') else: self.command_output_line += byte_read if self.command.error_available(): byte_read = self.command.error.read(1) if not byte_read: self.errors_eof_reached = True elif byte_read == bytes(b'\n'): line_stderr = Codec.decode(self.command_error_line) self.command_error_line = bytes(b'') self.command_error_output += byte_read else: self.command_error_line += byte_read self.command_error_output += byte_read return PollT(stdout_line=line_stdout, stderr_line=line_stderr) def get_error_output(self) -> str: """ Provide data which was sent to the stderr channel :return: stderr data :rtype: str """ return Codec.decode(self.command_error_output) def get_error_code(self) -> int: """ Provide return value from processed command :return: errorcode :rtype: int """ return self.command.process.returncode def get_pid(self) -> int: """ Provide process ID of command while running :return: pid :rtype: int """ return self.command.process.pid def kill(self) -> None: """ Send kill signal SIGTERM to command process """ self.command.process.kill() def __iter__(self) -> 'CommandIterator': return self ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7433155 kiwi-11.1.8/kiwi/config/functions.sh0000644000000000000000000007231615273675667014310 0ustar00#!/bin/bash # Copyright (c) 2021 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # #====================================== # work in POSIX environment #-------------------------------------- export LANG=C export LC_ALL=C #====================================== # Common Functions #-------------------------------------- function mountProc { if ! mountpoint -q /proc; then mount -t proc proc /proc trap "umount /proc" EXIT fi } function mountCgroups { mount -t tmpfs cgroup_root /sys/fs/cgroup for group in devices blkio cpuset pids; do mkdir -p /sys/fs/cgroup/"${group}" mount -t cgroup "${group}" -o "${group}" /sys/fs/cgroup/"${group}" done } function umountCgroups { for group in devices blkio cpuset pids; do umount /sys/fs/cgroup/"${group}" done umount /sys/fs/cgroup } function setupContainerRuntime { cat >/etc/containers/containers.conf <<- EOF [engine] no_pivot_root = true events_logger = "none" cgroup_manager = "cgroupfs" EOF cat >/etc/containers/storage.conf <<- EOF [storage] driver = "vfs" graphroot = "/var/lib/containers/storage" runroot = "/var/run/containers/storage" EOF } function baseSystemdServiceInstalled { local service=$1 local sd_dirs=" /usr/lib/systemd/system /etc/systemd/system /run/systemd/system " local dir for dir in ${sd_dirs} ; do if [ -f "${dir}/${service}.service" ];then echo "${dir}/${service}.service" return fi if [ -f "${dir}/${service}.mount" ];then echo "${dir}/${service}.mount" return fi done } function baseSysVServiceInstalled { local service=$1 if [ -x "/etc/init.d/${service}" ];then echo "${service}" fi } function baseSystemdCall { local service_name=$1; shift local service local legacy_service service=$(baseSystemdServiceInstalled "${service_name}") if [ -n "${service}" ];then systemctl "$@" "${service_name}" else legacy_service=$(baseSysVServiceInstalled "${service_name}") if [ -n "${legacy_service}" ];then # systemd is sysV init compatible and still allows # to enable those type of services systemctl "$@" "${legacy_service}" fi fi } function baseInsertService { # /.../ # Enable a system service using systemctl # Examples: # # baseInsertService sshd # --> enable sshd service # # ---- local service=$1 baseSystemdCall "${service}" "enable" } function baseRemoveService { # /.../ # Disable a system service using systemctl # Example: # # baseRemoveService sshd # --> disable sshd service # ---- local service=$1 baseSystemdCall "${service}" "disable" } function baseService { # /.../ # Enable or Disable a service # Examples: # # baseService sshd on # --> enable sshd service # # baseService sshd off # --> disable sshd service # # ---- local service=$1 local target=$2 if [ -z "${target}" ];then echo "baseService: no target specified" return fi if [ -z "${service}" ];then echo "baseService: no service name specified" return fi if [ "${target}" = off ];then baseRemoveService "${service}" else baseInsertService "${service}" fi } #====================================== # suseImportBuildKey #-------------------------------------- function suseImportBuildKey { # /.../ # Add missing gpg keys to rpm database # ---- local KEY local TDIR local KFN local dumpsigs=/usr/lib/rpm/gnupg/dumpsigs TDIR=$(mktemp -d) if [ ! -d "${TDIR}" ]; then echo "suseImportBuildKey: Failed to create temp dir" return fi if [ -d "/usr/lib/rpm/gnupg/keys" ];then pushd "/usr/lib/rpm/gnupg/keys" || return else pushd "${TDIR}" || return if [ -x "${dumpsigs}" ];then ${dumpsigs} /usr/lib/rpm/gnupg/suse-build-key.gpg fi fi for KFN in gpg-pubkey-*.asc; do if [ ! -e "${KFN}" ];then # # check if file exists because if the glob match did # not find files bash will use the glob string as # result and we just continue in this case # continue fi KEY=$(basename "${KFN}" .asc) if rpm -q "${KEY}" >/dev/null; then continue fi echo "Importing ${KEY} to rpm database" rpm --import "${KFN}" done popd || return rm -rf "${TDIR}" } function baseStripLocales { local keepLocales="$*" find /usr/lib/locale -mindepth 1 -maxdepth 1 -type d 2>/dev/null |\ baseStripAndKeep "${keepLocales}" } function baseStripTranslations { local keepMatching="$*" find /usr/share/locale -name "*.mo" |\ grep -v "${keepMatching}" | xargs rm -f } function baseStripUnusedLibs { # /.../ # Remove libraries which are not directly linked # against applications in the bin directories # ---- local needlibs local found local dir local lib local lddref # /.../ # Find directly used libraries, by calling ldd # on files in *bin* # --- ldconfig rm -f /tmp/needlibs for i in /usr/bin/* /bin/* /sbin/* /usr/sbin/* /lib/systemd/systemd-*;do for n in $(ldd "$i" 2>/dev/null | cut -f2- -d "/" | cut -f1 -d " ");do if [ ! -e "/$n" ];then continue fi lddref="/$n" while true;do if lib=$(readlink "${lddref}"); then lddref="${lib}" continue fi break done lddref=$(basename "${lddref}") echo "${lddref}" >> /tmp/needlibs done done count=0 for i in $(sort /tmp/needlibs | uniq);do for d in \ /lib /lib64 /usr/lib /usr/lib64 \ /usr/X11R6/lib /usr/X11R6/lib64 \ /lib/x86_64-linux-gnu /usr/lib/x86_64-linux-gnu do if [ -e "$d/$i" ];then needlibs[${count}]="$d/$i" count=$((count + 1)) fi done done # /.../ # add exceptions # ---- for libname in $1; do for libfile in \ /lib*/"$libname"* /usr/lib*/"$libname"* \ /lib/x86_64-linux-gnu/"$libname"* \ /usr/lib/x86_64-linux-gnu/"$libname"* \ /usr/X11R6/lib*/"$libname"* do if [ -e "$libfile" ];then needlibs[$count]=$libfile count=$((count + 1)) fi done done # /.../ # find unused libs and remove it, dl loaded libs # seems not to be that important within the initrd # ---- rm -f /tmp/needlibs for i in \ /lib/lib* /lib64/lib* /usr/lib/lib* \ /usr/lib64/lib* /usr/X11R6/lib*/lib* \ /lib/x86_64-linux-gnu/lib* /usr/lib/x86_64-linux-gnu/lib* do found=0 if [ ! -e "$i" ];then continue fi if [ -d "$i" ];then continue fi if [ -L "$i" ];then continue fi for n in ${needlibs[*]};do if [ "$i" = "$n" ];then found=1; break fi done if [ "${found}" -eq 0 ];then echo "Removing library: $i" rm "$i" fi done } function baseUpdateSysConfig { # /.../ # Update the contents of a sysconfig variable # ---- local FILE=$1 local VAR local VAL if [ -f "${FILE}" ];then VAR=$2 VAL=$3 eval sed -i "s'@^\($VAR=\).*\$@\1\\\"$VAL\\\"@'" "${FILE}" else echo "warning: config file $FILE not found" fi } function baseStripInitrd { declare kiwi_initrd_system=${kiwi_initrd_system} declare kiwi_strip_tools=${kiwi_strip_tools} declare kiwi_strip_libs=${kiwi_strip_libs} #========================================== # Check for initrd system #------------------------------------------ if [ "${kiwi_initrd_system}" = "dracut" ]; then echo "dracut initrd system requested, initrd strip skipped" return fi #========================================== # remove unneeded tools #------------------------------------------ local tools="${kiwi_strip_tools}" tools="${tools} $*" for path in /sbin /usr/sbin /usr/bin /bin;do baseStripTools "${path}" "${tools}" done } function baseStripFirmware { # /.../ # check all kernel modules if they require a firmware and # strip out all firmware files which are not referenced # by a kernel module # ---- local IFS local base=/lib/modules local name local bmdir local kernel_module local firmware mkdir -p /lib/firmware-required find "${base}" \( -name "*.ko" -o -name "*.ko.xz" -o -name "*.ko.zst" -o -name "*.ko.gz" \) -print0 | \ while IFS= read -r -d $'\0' kernel_module; do firmware=$(modinfo "${kernel_module}" | grep ^firmware || :) if [ -z "${firmware}" ];then continue fi name=$(echo "$firmware" | cut -f2 -d:) if [ -z "${name}" ];then continue fi # could be more than one, loop for fname in $name ; do for match in /lib/firmware/"${fname}" \ /lib/firmware/"${fname}".xz \ /lib/firmware/"${fname}".gz \ /lib/firmware/"${fname}".zst \ /lib/firmware/*/"${fname}" \ /lib/firmware/*/"${fname}".xz \ /lib/firmware/*/"${fname}".gz \ /lib/firmware/*/"${fname}".zst ;do if [ -e "${match}" ];then match="${match//\/lib\/firmware\//}" bmdir=$(dirname "${match}") mkdir -p "/lib/firmware-required/${bmdir}" mv "/lib/firmware/${match}" \ "/lib/firmware-required/${bmdir}" fi done done done # Preserve licenses and txt files (which are needed for some firmware blobs) find /lib/firmware \( -name 'LICENSE*' -o -name '*txt' \) -print |\ while read -r match; do if [ -e "${match}" ];then match="${match//\/lib\/firmware\//}" bmdir=$(dirname "${match}") mkdir -p "/lib/firmware-required/${bmdir}" mv "/lib/firmware/${match}" "/lib/firmware-required/${bmdir}" fi done rm -rf /lib/firmware mv /lib/firmware-required /lib/firmware } function baseStripModules { # /.../ # search for updated modules and remove the old version # which might be provided by the standard kernel # ---- local kernel=/lib/modules local files local mlist local count=1 local mosum=1 local modup files=$(find ${kernel} -type f \( -name "*.ko" -o -name "*.ko.xz" \) ) mlist=$(for i in ${files};do echo "$i";done | sed -e "s@.*/@@g" | sort) #====================================== # create sorted module array #-------------------------------------- for mod in ${mlist};do name_list[$count]=${mod} count=$((count + 1)) done count=1 #====================================== # find duplicate modules by their name #-------------------------------------- while [ ${count} -lt ${#name_list[*]} ];do mod=${name_list[${count}]} mod_next=${name_list[$((count + 1))]} if [ "${mod}" = "$mod_next" ];then mosum=$((mosum + 1)) else if [ ${mosum} -gt 1 ];then modup="${modup} ${mod}" fi mosum=1 fi count=$((count + 1)) done #====================================== # sort out duplicates prefer updates #-------------------------------------- if [ -z "${modup}" ];then echo "baseStripModules: No old versions for update drivers found" return fi for file in ${files};do for mod in $modup;do if [[ ${file} =~ ${mod} ]] && [[ ! ${file} =~ "updates" ]];then echo "baseStripModules: Update driver found for ${mod}" echo "baseStripModules: Removing old version: ${file}" rm -f "${file}" fi done done } function baseStripKernelModules { for kernel_dir in /kernel-tree/*;do kernel_version=$(/usr/bin/basename "${kernel_dir}") if [ ! -d "/kernel-tree/${kernel_version}/kernel" ]; then continue fi echo "Downsizing kernel modules for ${kernel_dir}" for module in $( find "/kernel-tree/${kernel_version}/kernel" \ -name "*.ko" -o -name "*.ko.xz" | sort ); do if ! baseKernelDriverMatches "${module}"; then echo "Deleting unwanted module: ${module}" rm -f "${module}" fi done done } function baseUpdateModuleDependencies { local kernel_dir local kernel_version for kernel_dir in /lib/modules/*;do [ -d "${kernel_dir}" ] || continue kernel_version=$(/usr/bin/basename "${kernel_dir}") if [ -f "/boot/System.map-${kernel_version}" ];then /sbin/depmod -F \ "/boot/System.map-${kernel_version}" "${kernel_version}" fi done } function baseStripKernel { # /.../ # this function will strip the kernel # # 1. handle requests. Because this # information is generic not only files of the kernel # are affected but also other data which is unwanted # gets deleted here # # 2. only keep kernel modules matching the # patterns from the kiwi boot image description # # 3. lookup kernel module dependencies and bring back # modules which were removed but still required by # other modules kept in the system to stay consistent # # 4. lookup for duplicate kernel modules due to kernel # module updates and keep only the latest version # # 5. lookup for kernel firmware files and keep only those # for which a kernel driver is still present in the # system # ---- declare kiwi_initrd_system=${kiwi_initrd_system} declare kiwi_strip_delete=${kiwi_strip_delete} if [ "${kiwi_initrd_system}" = "dracut" ]; then echo "dracut initrd system requested, kernel strip skipped" else for delete in ${kiwi_strip_delete};do echo "Removing file/directory: ${delete}" rm -rf "${delete}" done baseCreateKernelTree baseStripKernelModules baseFixupKernelModuleDependencies baseSyncKernelTree baseStripModules baseStripFirmware baseUpdateModuleDependencies fi } function baseSetRunlevel { # /.../ # Set the systemd default target link according to the # specified value Examples: # # baseSetRunlevel 5 # --> set graphical.target as systemd default # # baseSetRunlevel shutdown.target # --> set shutdown.target as systemd default # # ---- local target=$1 local systemd_system=/usr/lib/systemd/system local systemd_default=/etc/systemd/system/default.target local systemd_consoles=$systemd_system/multi-user.target local systemd_graphics=$systemd_system/graphical.target case "${target}" in 1|2|3|5) # /.../ # Given target is a number, map the number to a service # We only allow console or graphics mode # ---- if [ "${target}" -lt 5 ];then ln -sf "${systemd_consoles}" "${systemd_default}" else ln -sf "${systemd_graphics}" "${systemd_default}" fi ;; *) # /.../ # Given target is a raw name; use this as systemd target # name and setup this target name as the default target # ---- if [ -e "${systemd_system}/${target}" ]; then ln -sf "${systemd_system}/${target}" "${systemd_default}" else echo "Can't find systemd target: ${target}" fi ;; esac } function baseSetupBuildDay { local buildDay buildDay="$(LC_ALL=C date -u '+%Y%m%d')" echo "build_day=${buildDay}" > /build_day } function baseVagrantSetup { # insert the default insecure ssh key from here: # https://github.com/hashicorp/vagrant/blob/master/keys/vagrant.pub mkdir -p /home/vagrant/.ssh/ chmod 0700 /home/vagrant/.ssh/ echo "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6NF8iallvQVp22WDkTkyrtvp9eWW6A8YVr+kz4TjGYe7gHzIw+niNltGEFHzD8+v1I2YJ6oXevct1YeS0o9HZyN1Q9qgCgzUFtdOKLv6IedplqoPkcmF0aYet2PkEDo3MlTBckFXPITAMzF8dJSIFo9D8HfdOV0IAdx4O7PtixWKn5y2hMNG0zQPyUecp4pzC6kivAIhyfHilFR61RGL+GPXQ2MWZWFYbAGjyiYJnAmCP3NOTd0jMZEnDkbUvxhMmBYSdETk1rRgm+R4LOzFUGaHqHDLKLX+FIPKcF96hrucXzcWyLbIbEgE98OHlnVYCzRdK8jlqm8tehUc9c9WhQ== vagrant insecure public key" > /home/vagrant/.ssh/authorized_keys chmod 0600 /home/vagrant/.ssh/authorized_keys chown -R vagrant:vagrant /home/vagrant/ # apply recommended ssh settings for vagrant boxes SSHD_CONFIG=/etc/ssh/sshd_config.d/00-vagrant.conf if [[ ! -d "$(dirname ${SSHD_CONFIG})" ]]; then SSHD_CONFIG=/etc/ssh/sshd_config # prepend the settings, so that they take precedence echo -e "UseDNS no\nGSSAPIAuthentication no\n$(cat ${SSHD_CONFIG})" > ${SSHD_CONFIG} else echo -e "UseDNS no\nGSSAPIAuthentication no" > ${SSHD_CONFIG} fi # vagrant assumes that it can sudo without a password # => add the vagrant user to the sudoers list SUDOERS_LINE="vagrant ALL=(ALL) NOPASSWD: ALL" if [ -d /etc/sudoers.d ]; then echo "$SUDOERS_LINE" >| /etc/sudoers.d/vagrant visudo -cf /etc/sudoers.d/vagrant chmod 0440 /etc/sudoers.d/vagrant else echo "$SUDOERS_LINE" >> /etc/sudoers visudo -cf /etc/sudoers fi # the default shared folder mkdir -p /vagrant chown -R vagrant:vagrant /vagrant # SSH service baseInsertService sshd } #====================================== # Common Functions (SUSE) #-------------------------------------- function suseRemoveService { # function kept for compatibility baseRemoveService "$@" } function suseInsertService { # function kept for compatibility baseInsertService "$@" } function suseService { # function kept for compatibility baseService "$@" } function suseStripInitrd { baseStripInitrd "$@" } function suseStripKernel { baseStripKernel } function suseSetupProduct { # /.../ # This function creates the /etc/products.d/baseproduct # link pointing to the product referenced by either # /etc/SuSE-brand or /etc/os-release or the latest .prod file # available in /etc/products.d # ---- local prod=undef if [ -f /etc/SuSE-brand ];then prod=$(head /etc/SuSE-brand -n 1) elif [ -f /etc/os-release ];then prod=$(while read -r line; do if [[ $line =~ ^NAME ]]; then echo "$line"; fi; done < /etc/os-release | cut -d '=' -f 2 | cut -d '"' -f 2) fi if [ -d /etc/products.d ];then pushd /etc/products.d || return if [ -f "${prod}.prod" ];then ln -sf "${prod}.prod" baseproduct elif [ -f "SUSE_${prod}.prod" ];then ln -sf "SUSE_${prod}.prod" baseproduct else find_prod() { for f in *; do if [[ $f =~ \.prod$ ]]; then echo "$f" fi done } prod=$(find_prod | tail -n 1) if [ -f "${prod}" ];then ln -sf "${prod}" baseproduct fi fi popd || return fi } #====================================== # Helper methods #-------------------------------------- function Echo { # /.../ # print a message to the controling terminal # ---- local option="" local prefix="----->" local optn="" local opte="" while getopts "bne" option;do case ${option} in b) prefix=" " ;; n) optn="-n" ;; e) opte="-e" ;; *) echo "Invalid argument: ${option}" ;; esac done shift $((OPTIND - 1)) echo "${optn}" "${opte}" "${prefix} $1" OPTIND=1 } function Debug { # /.../ # print message if variable DEBUG is set to 1 # ----- if test "${DEBUG:-0}" = 1;then echo "+++++> (caller:${FUNCNAME[1]}:${FUNCNAME[2]} ) $*" fi } function baseQuoteFile { # /.../ # Quote file to be shell sourceable # ----- local file=$1 local conf=$file.quoted # create clean input, no empty lines and comments grep -v '^$' "${file}" | grep -v '^[ \t]*#' > "${conf}" # remove start/stop quoting from values sed -E -i -e s"#(^[a-zA-Z0-9_]+)=[\"'](.*)[\"']#\1=\2#" "${conf}" # remove backslash quotes if any sed -E -i -e s"#\\\\(.)#\1#g" "${conf}" # quote simple quotation marks sed -E -i -e s"#'+#'\\\\''#g" "${conf}" # add '...' quoting to values sed -E -i -e s"#(^[a-zA-Z0-9_]+)=(.*)#\1='\2'#" "${conf}" mv "${conf}" "${file}" } function baseCreateKernelTree { # /.../ # Create a copy of the kernel source tree under # /kernel-tree/ for stripping operations # ---- echo "Creating copy of kernel tree for strip operations" mountProc mkdir -p /kernel-tree cp -a /lib/modules/* /kernel-tree/ } function baseSyncKernelTree { # /.../ # Overwrite the original kernel tree with a minimized # version from /kernel-tree/. # ---- echo "Replace kernel tree with downsized version" mountProc rm -rf /lib/modules/* cp -a /kernel-tree/* /lib/modules/ rm -rf /kernel-tree } function baseFixupKernelModuleDependencies { # /.../ # Browse through the kernel tree and check if kernel # module dependencies are broken, if yes fix them # ---- local kernel_dir local kernel_version local module local module_name local module_info local dependency local module_files mountProc for kernel_dir in /kernel-tree/*;do echo "Checking kernel dependencies for ${kernel_dir}" kernel_version=$(/usr/bin/basename "${kernel_dir}") module_files=$( find "/kernel-tree/${kernel_version}" \ -name "*.ko" -o -name "*.ko.xz" ) for module in ${module_files};do module_u=${module%.xz} module_name=$(/usr/bin/basename "${module_u}") module_info=$(/sbin/modprobe \ --set-version "${kernel_version}" --ignore-install \ --show-depends "${module_name%.ko}" |\ sed -ne 's:.*insmod /\?::p' ) for dependency in ${module_info}; do if [ ! -f "/${dependency}" ]; then continue fi dependency_module=${dependency/lib\/modules/kernel-tree} if [ ! -f "${dependency_module}" ];then echo -e "Fix ${module}:\n --> needs: ${dependency_module}" mkdir -p "$(/usr/bin/dirname "${dependency_module}")" cp -a "${dependency}" "${dependency_module}" fi done done done } function baseKernelDriverMatches { # /.../ # Check if the provided kernel module name matches # the kiwi driver pattern # ---- declare kiwi_drivers=${kiwi_drivers} module=$1 for pattern in $(echo "${kiwi_drivers}" | tr , ' '); do if [[ ${module} =~ ${pattern} ]];then return 0 fi done return 1 } function baseStripAndKeep { # /.../ # helper function for the baseStrip* functions # reads the list of files to check from stdin for removing # - params - files which should be kept # ---- local keepFiles="$*" local found local baseFile local keep local file while read -r file; do baseFile=$(/usr/bin/basename "${file}") found=0 for keep in ${keepFiles};do if echo "${baseFile}" | grep -q "${keep}"; then found=1 break fi done if [ "${found}" = 0 ]; then Rm -rf "${file}" fi done } function baseStripTools { # /.../ # baseStripTools {list of toolpath} {list of tools} # Helper function for suseStripInitrd, only keep # the provided tools from the list local tpath=$1 local tools=$2 local found local file local IFS find "${tpath}" -print0 | \ while IFS= read -r -d $'\0' file; do found=0 base=$(/usr/bin/basename "${file}") for need in ${tools};do if [ "${base}" = "$need" ];then found=1 break fi done if [ "${found}" = 0 ] && [ ! -d "${file}" ];then Rm -fv "${file}" fi done } function Rm { # /.../ # delete files & anounce it to log # ---- Debug "rm $*" rm "$@" } function deprecated { { echo "DEPRECATED: $1() is obsolete" echo "[" cat echo "]" } >&2 exit 1 } #====================================== # Deprecated methods #-------------------------------------- function baseCleanMount { deprecated "${FUNCNAME[0]}" <<- EOF Cleanup operations are a responsibility of the kiwi code EOF } function baseMount { deprecated "${FUNCNAME[0]}" <<- EOF Mounting of kernel and other base filesystems are a responsibility of the kiwi code EOF } function suseActivateDefaultServices { deprecated "${FUNCNAME[0]}" <<- EOF There is no generic applicable list of default services It is expected that the installation of software handles this properly. Optional services should be handled explicitly EOF } function baseSetupUserPermissions { deprecated "${FUNCNAME[0]}" <<- EOF This is done in kiwi by chkstat EOF } function suseConfig { deprecated "${FUNCNAME[0]}" <<- EOF suse script no longer present EOF } function baseGetPackagesForDeletion { deprecated "${FUNCNAME[0]}" <<- EOF Provided by the kiwi_delete environment variable EOF } function baseGetProfilesUsed { deprecated "${FUNCNAME[0]}" <<- EOF Provided by the kiwi_profiles environment variable EOF } function baseStripMans { deprecated "${FUNCNAME[0]}" <<- EOF Man pages are marked as documentation by the package manager and can be excluded via rpm-excludedocs EOF } function baseStripDocs { deprecated "${FUNCNAME[0]}" <<- EOF Documentation lives at different places and a method to do this in a common way should not be a responsibility of kiwi EOF } function baseStripInfos { deprecated "${FUNCNAME[0]}" <<- EOF Info files are marked as documentation by the package manager and can be excluded via rpm-excludedocs EOF } function Rpm { deprecated "${FUNCNAME[0]}" <<- EOF Please call rpm as it fits in the caller scope EOF } function rhelStripInitrd { deprecated "${FUNCNAME[0]}" <<- EOF Useful only in the context of a kiwi created initrd This concept was never exposed to RHEL/Fedora EOF } function debianStripInitrd { deprecated "${FUNCNAME[0]}" <<- EOF Useful only in the context of a kiwi created initrd This concept was never exposed to Debian/Ubuntu EOF } function rhelStripKernel { deprecated "${FUNCNAME[0]}" <<- EOF Useful only in the context of a kiwi created initrd This concept was never exposed to RHEL/Fedora EOF } function debianStripKernel { deprecated "${FUNCNAME[0]}" <<- EOF Useful only in the context of a kiwi created initrd This concept was never exposed to Debian/Ubuntu EOF } function rhelSplashToGrub { deprecated "${FUNCNAME[0]}" <<- EOF Code based on distros out of support and no longer using kernel splash EOF } function suseSetupProductInformation { deprecated "${FUNCNAME[0]}" <<- EOF Utilizes zypper to install a product information. Product handling in zypper has changed. The method here was based on distros out of support EOF } function baseCleanup { deprecated "${FUNCNAME[0]}" <<- EOF Cleanup operations are a responsibility of the kiwi code EOF } function suseCleanup { deprecated "${FUNCNAME[0]}" <<- EOF Cleanup operations are a responsibility of the kiwi code EOF } function suseRemoveYaST { deprecated "${FUNCNAME[0]}" <<- EOF There is no standard uninstall method for YaST which is common across distributions. To prevent installation of YaST make sure it does not get pulled in with the packages. If this is inevitable delete it based on packages in the kiwi delete/uninstall sections EOF } # vim: set noexpandtab: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7445486 kiwi-11.1.8/kiwi/config/kiwi_boxed_plugin.yml0000644000000000000000000001747215273675667016173 0ustar00box: - name: tumbleweed mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda3 - rd.plymouth=0 source: obs://Virtualization:Appliances:SelfContained:tumbleweed/images packages_file: TumbleWeed-Box.x86_64-System.json boxfiles: - TumbleWeed-Box.x86_64-Kernel.tar.xz - TumbleWeed-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/tumbleweed/images/tumbleweed:latest use_initrd: true - name: aarch64 cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:tumbleweed/images packages_file: TumbleWeed-Box.aarch64-System.json boxfiles: - TumbleWeed-Box.aarch64-Kernel.tar.xz - TumbleWeed-Box.aarch64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/tumbleweed/images/tumbleweed:latest use_initrd: true - name: s390x cmdline: - root=/dev/vda1 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:tumbleweed/images packages_file: TumbleWeed-Box.s390x-System.json boxfiles: - TumbleWeed-Box.s390x-Kernel.tar.xz - TumbleWeed-Box.s390x-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/tumbleweed/images/tumbleweed:latest use_initrd: true - name: leap mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda1 - rd.plymouth=0 source: obs://Virtualization:Appliances:SelfContained:leap/images packages_file: Leap-Box.x86_64-System.json boxfiles: - Leap-Box.x86_64-Kernel.tar.xz - Leap-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/leap/images/leap:latest use_initrd: true - name: fedora mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda3 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:fedora/images packages_file: Fedora-Box.x86_64-System.json boxfiles: - Fedora-Box.x86_64-Kernel.tar.xz - Fedora-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/fedora/images/fedora:latest use_initrd: true - name: ubuntu mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda3 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:ubuntu/images packages_file: Ubuntu-Box.x86_64-System.json boxfiles: - Ubuntu-Box.x86_64-Kernel.tar.xz - Ubuntu-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/ubuntu/images/ubuntu:latest use_initrd: true - name: aarch64 cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:ubuntu/images packages_file: Ubuntu-Box.aarch64-System.json boxfiles: - Ubuntu-Box.aarch64-Kernel.tar.xz - Ubuntu-Box.aarch64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/ubuntu/images/ubuntu:latest use_initrd: true - name: universal mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda3 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:universal/images packages_file: Universal-Box.x86_64-System.json boxfiles: - Universal-Box.x86_64-Kernel.tar.xz - Universal-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/universal/images/universal:latest use_initrd: true - name: aarch64 cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:universal/images packages_file: Universal-Box.aarch64-System.json boxfiles: - Universal-Box.aarch64-Kernel.tar.xz - Universal-Box.aarch64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/universal/images/universal:latest use_initrd: true - name: s390x cmdline: - root=/dev/vda1 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:universal/images packages_file: Universal-Box.s390x-System.json boxfiles: - Universal-Box.s390x-Kernel.tar.xz - Universal-Box.s390x-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/universal/images/universal:latest use_initrd: true - name: ppc64le cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:universal/images packages_file: Universal-Box.ppc64le-System.json boxfiles: - Universal-Box.ppc64le-Kernel.tar.xz - Universal-Box.ppc64le-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/universal/images/universal:latest use_initrd: true - name: staging mem_mb: 8096M processors: 4 console: hvc0 arch: - name: x86_64 cmdline: - root=/dev/vda3 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:staging/images packages_file: Staging-Box.x86_64-System.json boxfiles: - Staging-Box.x86_64-Kernel.tar.xz - Staging-Box.x86_64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/staging/images/staging:latest use_initrd: true - name: aarch64 cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:staging/images packages_file: Staging-Box.aarch64-System.json boxfiles: - Staging-Box.aarch64-Kernel.tar.xz - Staging-Box.aarch64-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/staging/images/staging:latest use_initrd: true - name: s390x cmdline: - root=/dev/vda1 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:staging/images packages_file: Staging-Box.s390x-System.json boxfiles: - Staging-Box.s390x-Kernel.tar.xz - Staging-Box.s390x-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/staging/images/staging:latest use_initrd: true - name: ppc64le cmdline: - root=/dev/vda2 - rd.plymouth=0 - selinux=0 source: obs://Virtualization:Appliances:SelfContained:staging/images packages_file: Staging-Box.ppc64le-System.json boxfiles: - Staging-Box.ppc64le-Kernel.tar.xz - Staging-Box.ppc64le-System.qcow2 container: registry.opensuse.org/virtualization/appliances/selfcontained/staging/images/staging:latest use_initrd: true ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7445486 kiwi-11.1.8/kiwi/config/strip.xml0000644000000000000000000002603615273675667013625 0ustar00 Marcus Schäfer ms@suse.com default items to keep or delete from the kiwi initrd 2.7.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7445486 kiwi-11.1.8/kiwi/container/__init__.py0000644000000000000000000000417015273675667014563 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from abc import ABCMeta, abstractmethod from typing import Dict, Optional from kiwi.container.base import ContainerImageBase from kiwi.exceptions import KiwiContainerImageSetupError class ContainerImage(metaclass=ABCMeta): """ **Container Image factory** :param string name: container system name :param string root_dir: root directory path name :param dict custom_args: custom arguments """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( name: str, root_dir: str, custom_args: Optional[Dict] = None ) -> ContainerImageBase: # noqa: E252 name_map = {'docker': 'OCI', 'oci': 'OCI', 'appx': 'Appx', 'wsl': 'Wsl'} args_map = { 'docker': [root_dir, 'docker-archive', custom_args], 'oci': [root_dir, 'oci-archive', custom_args], 'appx': [root_dir, custom_args], 'wsl': [root_dir, custom_args], } try: container_image = importlib.import_module( 'kiwi.container.{}'.format('oci' if name == 'docker' else name) ) module_name = 'ContainerImage{}'.format(name_map[name]) return container_image.__dict__[module_name](*args_map[name]) except Exception: raise KiwiContainerImageSetupError( 'Support for {0} container not implemented'.format(name) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7447097 kiwi-11.1.8/kiwi/container/appx.py0000644000000000000000000001114215273675667013771 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict # project from kiwi.utils.temporary import Temporary from kiwi.archive.tar import ArchiveTar from kiwi.defaults import Defaults from kiwi.utils.compress import Compress from kiwi.runtime_config import RuntimeConfig from kiwi.command import Command from kiwi.container.base import ContainerImageBase from kiwi.exceptions import KiwiContainerSetupError log = logging.getLogger('kiwi') class ContainerImageAppx(ContainerImageBase): """ Create Appx container from a root directory for WSL(Windows Subsystem Linux) :param string root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: Example .. code:: python { 'metadata_path': 'directory' } """ def __init__(self, root_dir: str, custom_args: Dict[str, str] = {}): self.root_dir = root_dir self.wsl_config = custom_args or {} self.runtime_config = RuntimeConfig() self.meta_data_path = format(self.wsl_config.get('metadata_path') or '') if not self.meta_data_path: raise KiwiContainerSetupError( 'No metadata path specified to build appx container' ) if not os.path.exists(self.meta_data_path): raise KiwiContainerSetupError( 'Specified metadata path {0} does not exist'.format( self.meta_data_path ) ) def create( self, filename: str, base_image: str = '', ensure_empty_tmpdirs: bool = False, compress_archive: bool = False, ) -> str: """ Create WSL/Appx archive :param string filename: archive file name :param string base_image: not-supported :param bool ensure_empty_tmpdirs: not-supported :param bool compress_archive: compress container archive """ exclude_list = ( Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files(self.root_dir) ) exclude_list.append('boot') exclude_list.append('dev') exclude_list.append('sys') exclude_list.append('proc') # The C code of WSL-DistroLauncher harcodes the name for the # root tarball to be install.tar.gz. Thus we have to use this # name for the root tarball archive_file_name = os.sep.join([self.meta_data_path, 'install.tar']) archive = ArchiveTar(archive_file_name) archive_file_name = archive.create(self.root_dir, exclude=exclude_list) compressor = Compress(archive_file_name) archive_file_name = compressor.gzip() filemap_file = Temporary().new_file() with open(filemap_file.name, 'w') as filemap: filemap.write('[Files]{0}'.format(os.linesep)) for topdir, dirs, files in sorted(os.walk(self.meta_data_path)): for entry in sorted(dirs + files): if entry in files: mapfile = os.sep.join([topdir, entry]) mapfile_relative = os.path.relpath( mapfile, start=self.meta_data_path ) log.info( 'Adding {0} to Appx filemap as relative path {1}'.format( mapfile, mapfile_relative ) ) filemap.write( '"{0}" "{1}"{2}'.format( mapfile, mapfile_relative, os.linesep ) ) Command.run(['appx', '-o', filename, '-f', filemap_file.name]) if compress_archive: compress = Compress(filename) filename = compress.xz(self.runtime_config.get_xz_options()) return filename ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7447097 kiwi-11.1.8/kiwi/container/base.py0000644000000000000000000000170415273675667013736 0ustar00# Copyright (c) 2023 SUSE LLC # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from abc import ABC, abstractmethod class ContainerImageBase(ABC): @abstractmethod def create( self, filename: str, base_image: str, ensure_empty_tmpdirs: bool, compress_archive: bool = False, ) -> str: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7447097 kiwi-11.1.8/kiwi/container/oci.py0000644000000000000000000002023015273675667013571 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict, List, Optional, TypedDict # project from kiwi.utils.compress import Compress from kiwi.runtime_config import RuntimeConfig from kiwi.defaults import Defaults from kiwi.oci_tools import OCI from kiwi.container.base import ContainerImageBase log = logging.getLogger('kiwi') class OciConfig(TypedDict, total=False): container_name: str container_tag: str additional_names: List[str] entry_command: List[str] entry_subcommand: List[str] maintainer: str user: str workingdir: str expose_ports: List[str] volumes: List[str] environment: Dict[str, str] labels: Dict[str, str] history: Dict[str, str] class ContainerImageOCI(ContainerImageBase): """ Create oci container from a root directory :param string root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys: Example .. code:: python { 'container_name': 'name', 'container_tag': '1.0', 'additional_names': ['current', 'foobar'], 'entry_command': ['/bin/bash', '-x'], 'entry_subcommand': ['ls', '-l'], 'maintainer': 'tux', 'user': 'root', 'workingdir': '/root', 'expose_ports': ['80', '42'], 'volumes': ['/var/log', '/tmp'], 'environment': {'PATH': '/bin'}, 'labels': {'name': 'value'}, 'history': { 'created_by': 'some explanation here', 'comment': 'some comment here', 'author': 'tux' } } """ def __init__( self, root_dir: str, transport: str, custom_args: Optional[OciConfig] = None, ) -> None: self.root_dir = root_dir self.archive_transport = transport self.oci_config = custom_args or {} # for builds inside the buildservice we include a reference to the # specific build. Thus disturl label only exists inside the # buildservice. if Defaults.is_buildservice_worker(): bs_label = 'org.openbuildservice.disturl' # Do not label anything if the build service label is # already present if ( 'labels' not in self.oci_config or bs_label not in self.oci_config['labels'] ): self._append_buildservice_disturl_label() if 'container_name' not in self.oci_config: log.info( 'No container configuration provided, ' 'using default container name "kiwi-container:latest"' ) self.oci_config['container_name'] = ( Defaults.get_default_container_name() ) self.oci_config['container_tag'] = ( Defaults.get_default_container_tag() ) if 'container_tag' not in self.oci_config: self.oci_config['container_tag'] = ( Defaults.get_default_container_tag() ) if 'history' not in self.oci_config: self.oci_config['history'] = {} if 'created_by' not in self.oci_config['history']: self.oci_config['history'][ 'created_by' ] = Defaults.get_default_container_created_by() def create( self, filename: str, base_image: str, ensure_empty_tmpdirs: bool = True, compress_archive: bool = False, ) -> str: """ Create compressed oci system container tar archive :param string filename: archive file name :param string base_image: archive used as a base image :param bool ensure_empty_tmpdirs: exclude system tmp directories :param bool compress_archive: compress container archive """ exclude_list = Defaults.get_exclude_list_for_root_data_sync( ensure_empty_tmpdirs ) + Defaults.get_exclude_list_from_custom_exclude_files(self.root_dir) exclude_list.append('dev/*') exclude_list.append('sys/*') exclude_list.append('proc/*') with OCI.new() as oci: if base_image: oci.import_container_image( 'oci-archive:{0}:{1}'.format( base_image, Defaults.get_container_base_image_tag() ) ) else: # Apply default subcommand only for base images if ( 'entry_command' not in self.oci_config and 'entry_subcommand' not in self.oci_config ): self.oci_config['entry_subcommand'] = ( Defaults.get_default_container_subcommand() ) oci.init_container() image_ref = '{0}:{1}'.format( self.oci_config['container_name'], self.oci_config['container_tag'], ) oci.unpack() oci.sync_rootfs(self.root_dir, exclude_list) oci.repack(self.oci_config) oci.set_config(self.oci_config) oci.post_process() image_ref = '{0}:{1}'.format( self.oci_config['container_name'], self.oci_config['container_tag'], ) additional_refs: List[str] = [] if self.archive_transport == 'docker-archive': if 'additional_names' in self.oci_config: additional_refs = [] for name in self.oci_config['additional_names']: name_parts = name.partition(':') if not name_parts[0]: additional_refs.append( '{0}:{1}'.format( self.oci_config['container_name'], name_parts[2], ) ) elif not name_parts[2]: additional_refs.append( '{0}:{1}'.format( name_parts[0], self.oci_config['container_tag'], ) ) else: additional_refs.append( '{0}:{1}'.format(name_parts[0], name_parts[2]) ) oci.export_container_image( filename, self.archive_transport, image_ref, additional_refs ) if compress_archive: compress = Compress(filename) filename = compress.xz(RuntimeConfig().get_xz_options()) return filename def _append_buildservice_disturl_label(self) -> None: with open(os.sep + Defaults.get_buildservice_env_name()) as env: for line in env: if line.startswith('BUILD_DISTURL') and '=' in line: disturl = line.split('=')[1].lstrip('\'\"').rstrip('\n\'\"') if disturl: label = {'org.openbuildservice.disturl': disturl} if self.oci_config.get('labels'): self.oci_config['labels'].update(label) else: self.oci_config['labels'] = label return log.warning('Could not find BUILD_DISTURL inside .buildenv') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7447097 kiwi-11.1.8/kiwi/container/setup/__init__.py0000644000000000000000000000342115273675667015721 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from abc import ABCMeta, abstractmethod from typing import Dict, Optional # project from kiwi.container.setup.base import ContainerSetupBase from kiwi.exceptions import KiwiContainerSetupError class ContainerSetup(metaclass=ABCMeta): """ container setup factory """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( name: str, root_dir: str, custom_args: Optional[Dict] = None ) -> ContainerSetupBase: name_map = { 'docker': 'Docker', 'oci': 'OCI', 'appx': 'Appx', 'wsl': 'Wsl', } try: container_setup = importlib.import_module( 'kiwi.container.setup.{}'.format(name) ) module_name = 'ContainerSetup{}'.format(name_map[name]) return container_setup.__dict__[module_name](root_dir, custom_args) except Exception: raise KiwiContainerSetupError( 'Support for {0} container not implemented'.format(name) ) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/setup/appx.py0000644000000000000000000001247215273675667015140 0ustar00# Copyright (c) 2020 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from typing import List, Optional, cast from lxml import etree from xml.dom import minidom # project from kiwi.container.setup.base import ContainerSetupBase from kiwi.exceptions import KiwiContainerSetupError from kiwi.defaults import Defaults class ContainerSetupAppx(ContainerSetupBase): """ WSL/Appx container setup """ def setup(self) -> None: """ Setup container metadata """ self.appx_at_microsoft = 'http://schemas.microsoft.com/appx' self.appx_manifest_file = os.sep.join( [ cast(str, self.custom_args.get('metadata_path')), 'AppxManifest.xml', ] ) self.appx_namespace_map = dict( wsl='{0}/manifest/foundation/windows10'.format( self.appx_at_microsoft ), uap='{0}/manifest/uap/windows10'.format(self.appx_at_microsoft), desktop='{0}/manifest/desktop/windows10'.format( self.appx_at_microsoft ), uap3='{0}/manifest/uap/windows10/3'.format(self.appx_at_microsoft), mp='{0}/2014/phone/manifest'.format(self.appx_at_microsoft), ) if not os.path.exists(self.appx_manifest_file): raise KiwiContainerSetupError( 'Appx Manifest not found at: {0}'.format( self.appx_manifest_file ) ) try: self.appx_manifest = etree.parse(self.appx_manifest_file) self._update_identity() self._update_desktop_visual() self._update_application_metadata() xml_data_unformatted = etree.tostring(self.appx_manifest.getroot()) xml_data_domtree = minidom.parseString(xml_data_unformatted) xml_data_raw = xml_data_domtree.toprettyxml( indent=' ', encoding="utf-8" ) with open(self.appx_manifest_file, 'wb') as appx_manifest: for line in xml_data_raw.split(b'\n'): if line.split(): appx_manifest.write(line + b'\n') except Exception as issue: raise KiwiContainerSetupError( 'Failed to update Appx Manifest: {0}'.format(issue) ) def _update_identity(self) -> None: identities = self._get_xpath('//wsl:Identity') history = self.custom_args.get('history') or {} for identity in identities: if self._get_microsoft_processor_architecture(): identity.set( 'ProcessorArchitecture', self._get_microsoft_processor_architecture(), ) if history.get('package_version'): identity.set('Version', history.get('package_version')) if history.get('created_by'): identity.set('Publisher', history.get('created_by')) def _update_desktop_visual(self) -> None: history = self.custom_args.get('history') or {} visuals = self._get_xpath( '//wsl:Applications/wsl:Application/uap:VisualElements' ) for visual in visuals: if history.get('comment'): visual.set('Description', history.get('comment')) def _update_application_metadata(self) -> None: history = self.custom_args.get('history') or {} applications = self._get_xpath('//wsl:Applications/wsl:Application') extensions = self._get_xpath( '//wsl:Applications/wsl:Application/' 'wsl:Extensions/uap3:Extension' ) aliases = self._get_xpath( '//wsl:Applications/wsl:Application/' 'wsl:Extensions/uap3:Extension/uap3:AppExecutionAlias/' 'desktop:ExecutionAlias' ) for application in applications: if history.get('application_id'): application.set('Id', history.get('application_id')) if history.get('launcher'): application.set('Executable', history.get('launcher')) for extension in extensions: if history.get('launcher'): extension.set('Executable', history.get('launcher')) for alias in aliases: if history.get('launcher'): alias.set('Alias', history.get('launcher')) def _get_xpath(self, expression: str) -> List[etree._Element]: return self.appx_manifest.getroot().xpath( expression, namespaces=self.appx_namespace_map ) def _get_microsoft_processor_architecture(self) -> Optional[str]: windows = {'x86_64': 'x64'} return windows.get(Defaults.get_platform_name()) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/setup/base.py0000644000000000000000000001244115273675667015076 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import collections import os from typing import Any, Dict, Optional # project from kiwi.command import Command from kiwi.exceptions import KiwiContainerSetupError class ContainerSetupBase: """ Base class for setting up the root system to create a container image from for e.g docker. The methods here are generic to linux systems following the FHS standard and modern enough e.g based on systemd Attributes * :attr:`root_dir` root directory path name * :attr:`custom_args` dict of custom arguments """ def __init__( self, root_dir: str, custom_args: Optional[Dict] = None ) -> None: if not os.path.exists(root_dir): raise KiwiContainerSetupError( f'Container root directory {root_dir} does not exist' ) self.root_dir = root_dir self.custom_args: Dict[str, Any] = custom_args or {} if 'container_name' not in self.custom_args: self.custom_args['container_name'] = 'system-container' self.post_init(custom_args) def post_init(self, custom_args: Optional[Dict]) -> None: """ Post initialization method Implementation in specialized container setup class :param list custom_args: unused """ pass def setup(self) -> None: """ Setup container metadata Implementation in specialized bootloader class required """ raise NotImplementedError def deactivate_bootloader_setup(self) -> None: """ Container bootloader setup Tell the system there is no bootloader configuration it needs to care for. A container does not boot """ bootloader_setup = self.root_dir + '/etc/sysconfig/bootloader' if os.path.exists(bootloader_setup): self._update_config( bootloader_setup, { 'LOADER_LOCATION': 'LOADER_LOCATION="none"', 'LOADER_TYPE': 'LOADER_TYPE="none"', }, ) def deactivate_root_filesystem_check(self) -> None: """ Container filesystem check setup The root filesystem of a container could be an overlay or a mapped device. In any case it should not be checked for consistency as this is should be done by the container infrastructure """ boot_setup = self.root_dir + '/etc/sysconfig/boot' if os.path.exists(boot_setup): self._update_config( boot_setup, {'ROOTFS_BLKDEV': 'ROOTFS_BLKDEV="/dev/null"'} ) def deactivate_systemd_service(self, name: str) -> None: """ Container system services setup Init systems among others also controls services which starts at boot time. A container does not really boot. Thus some services needs to be deactivated :param string name: systemd service name """ service_file = self.root_dir + '/usr/lib/systemd/system/' + name if os.path.exists(service_file): try: Command.run(['ln', '-s', '-f', '/dev/null', service_file]) except Exception as e: raise KiwiContainerSetupError( f'Failed to deactivate service {name}: {format(e)}' ) def setup_root_console(self) -> None: """ Container console setup /dev/console should be allowed to login by root """ securetty = self.root_dir + '/etc/securetty' if not os.path.exists(securetty): with open(securetty, 'w'): pass self._update_config(securetty, {'console': 'console'}) def get_container_name(self) -> str: """ Container name :return: name :rtype: str """ return self.custom_args['container_name'] def _update_config( self, filename: str, update_record: Dict[str, str] ) -> None: data = [] with open(filename, 'r') as config: data = config.read().rsplit('\n') sorted_record = collections.OrderedDict(sorted(update_record.items())) for current_value, new_value in list(sorted_record.items()): entry_found = False for index in range(0, len(data)): line = data[index] if line.startswith(current_value): entry_found = True data[index] = new_value if not entry_found: data.append(new_value) with open(filename, 'w') as config: config.write('%s\n' % '\n'.join(data)) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/setup/docker.py0000644000000000000000000000153615273675667015436 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project from kiwi.container.setup.oci import ContainerSetupOCI class ContainerSetupDocker(ContainerSetupOCI): """ Docker container setup """ ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/setup/oci.py0000644000000000000000000000326215273675667014737 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project from typing import Dict, Optional from kiwi.container.setup.base import ContainerSetupBase class ContainerSetupOCI(ContainerSetupBase): """ Oci container setup """ def post_init(self, custom_args: Optional[Dict]) -> None: """ Post initialization method Store custom arguments :param list custom_args: custom bootloader arguments """ if custom_args: self.custom_args = custom_args def setup(self) -> None: """ Setup system for use with docker """ services_to_deactivate = [ 'device-mapper.service', 'kbd.service', 'swap.service', 'udev.service', 'proc-sys-fs-binfmt_misc.automount', ] self.deactivate_bootloader_setup() self.deactivate_root_filesystem_check() self.setup_root_console() for service in services_to_deactivate: self.deactivate_systemd_service(service) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/setup/wsl.py0000644000000000000000000000343615273675667014775 0ustar00# Copyright (c) 2025 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os # project from kiwi.container.setup.base import ContainerSetupBase from kiwi.exceptions import KiwiContainerSetupError class ContainerSetupWsl(ContainerSetupBase): """ WSL new tar setup """ def setup(self) -> None: """ Setup container metadata """ # It's expected that the container metadata for the # new WSL format is provided by a package or via overlay # data. Thus this setup routine only checks for the # presence of the data and fails when missing distribution_conf = os.path.normpath( os.sep.join([self.root_dir, 'etc', 'wsl-distribution.conf']) ) wsl_conf = os.path.normpath( os.sep.join([self.root_dir, 'etc', 'wsl.conf']) ) if not os.path.exists(distribution_conf): raise KiwiContainerSetupError( f'Mandatory WSL {distribution_conf} not found in root tree' ) if not os.path.exists(wsl_conf): raise KiwiContainerSetupError( f'Mandatory WSL {wsl_conf} not found in root tree' ) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/container/wsl.py0000644000000000000000000000477015273675667013637 0ustar00# Copyright (c) 2025 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Dict # project from kiwi.archive.tar import ArchiveTar from kiwi.defaults import Defaults from kiwi.utils.compress import Compress from kiwi.command import Command from kiwi.container.base import ContainerImageBase class ContainerImageWsl(ContainerImageBase): """ Create new WSL root tar WSL(Windows Subsystem Linux >= 2.4.4) :param string root_dir: root directory path name :param dict custom_args: Custom processing arguments defined as hash keys """ def __init__(self, root_dir: str, custom_args: Dict[str, str] = {}): self.root_dir = root_dir self.wsl_config = custom_args or {} def create( self, filename: str, base_image: str = '', ensure_empty_tmpdirs: bool = False, compress_archive: bool = True, ) -> str: """ Create WSL root tar :param string filename: archive file name :param string base_image: not-supported :param bool ensure_empty_tmpdirs: not-supported :param bool compress_archive: not-supported """ exclude_list = ( Defaults.get_exclude_list_for_root_data_sync() + Defaults.get_exclude_list_from_custom_exclude_files(self.root_dir) ) exclude_list.append('boot') exclude_list.append('dev') exclude_list.append('sys') exclude_list.append('proc') archive_file_name = filename archive = ArchiveTar(archive_file_name) archive_file_name = archive.create( self.root_dir, exclude=exclude_list, options=['--numeric-owner', '--absolute-names'], ) compressor = Compress(archive_file_name) archive_file_name = compressor.gzip() Command.run(['mv', archive_file_name, filename]) return filename ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/defaults.py0000644000000000000000000017632015273675667012660 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging import os import glob import importlib from importlib.resources import as_file from importlib.util import find_spec from importlib.machinery import ModuleSpec from collections import namedtuple import platform import yaml from typing import ( TYPE_CHECKING, Any, Dict, List, Literal, NamedTuple, Optional, Union, ) # project from kiwi.path import Path from kiwi.version import __githash__, __version__ from kiwi.exceptions import KiwiBootLoaderGrubDataError if TYPE_CHECKING: # pragma: nocover from kiwi.system.profile import Profile shim_loader_type = NamedTuple( 'shim_loader_type', [('filename', str), ('binaryname', str)] ) grub_loader_type = NamedTuple( 'grub_loader_type', [('filename', str), ('binaryname', str), ('targetname', str)], ) unit_type = NamedTuple( 'unit_type', [('byte', str), ('kb', str), ('mb', str), ('gb', str)] ) # Default module variables DM_METADATA_FORMAT_VERSION = '1' DM_METADATA_OFFSET = 4096 # 4kb VERITY_DATA_BLOCKSIZE = 4096 # 4kb VERITY_HASH_BLOCKSIZE = 4096 # 4kb INTEGRITY_SECTOR_SIZE = 512 INTEGRITY_ALGORITHM = 'sha256' INTEGRITY_KEY_ALGORITHM = 'hmac-sha256' UNIT = unit_type(byte='b', kb='k', mb='m', gb='g') POST_DISK_SYNC_SCRIPT = 'disk.sh' PRE_DISK_SYNC_SCRIPT = 'pre_disk_sync.sh' POST_BOOTSTRAP_SCRIPT = 'post_bootstrap.sh' POST_PREPARE_SCRIPT = 'config.sh' POST_PREPARE_OVERLAY_SCRIPT = 'config-overlay.sh' POST_HOST_PREPARE_OVERLAY_SCRIPT = 'config-host-overlay.sh' PRE_CREATE_SCRIPT = 'images.sh' EDIT_BOOT_CONFIG_SCRIPT = 'edit_boot_config.sh' EDIT_BOOT_INSTALL_SCRIPT = 'edit_boot_install.sh' IMAGE_METADATA_DIR = 'image' ROOT_VOLUME_NAME = 'LVRoot' SHARED_CACHE_DIR = '/var/cache/kiwi' MODULE_SPEC: Optional[ModuleSpec] = find_spec('kiwi') RUNTIME_CHECKER_METADATA = ( '{}/runtime_checker_metadata.yml'.format( os.path.dirname(MODULE_SPEC.origin or 'unknown') ) if MODULE_SPEC else 'unknown' ) TEMP_DIR = '/var/tmp' LOCAL_CONTAINERS = '/var/tmp/kiwi_containers' CUSTOM_RUNTIME_CONFIG_FILE: Optional[str] = None ETC_RUNTIME_CONFIG_FILE = '/etc/kiwi.yml' ETC_RUNTIME_CONFIG_DIR = '/etc/kiwi.yml.d' USR_RUNTIME_CONFIG_FILE = '/usr/share/kiwi/kiwi.yml' USR_RUNTIME_CONFIG_DIR = '/usr/share/kiwi/kiwi.yml.d' PLATFORM_MACHINE = platform.machine() EFI_FAT_IMAGE_SIZE = 20 # kernel flavor name of the kernels without flavor, see Kernel.is_flavor KERNEL_FLAVOR_NONE = 'none' # optional package manager environment variables PACKAGE_MANAGER_ENV_VARS = '/.kiwi.package_manager.env' # host proxy configuration taken into account for package managers HOST_SYSCONFIG_PROXY = '/etc/sysconfig/proxy' HOST_DNF_CONFIG = '/etc/dnf/dnf.conf' # Distribution specific CA store and tooling CA_UPDATE_MAP = { 'suse': { 'tool': 'update-ca-certificates', 'destination_path': '/etc/pki/trust/anchors', }, 'redhat': { 'tool': 'update-ca-trust', 'destination_path': '/etc/pki/ca-trust/source/anchors/', }, 'debian': { 'tool': 'update-ca-certificates', 'destination_path': '/usr/local/share/ca-certificates/', }, 'archlinux': { 'tool': 'update-ca-trust', 'destination_path': '/etc/ca-certificates/trust-source/anchors/', }, } log = logging.getLogger('kiwi') class Defaults: """ **Implements default values** Provides static methods for default values and state information """ def __init__(self) -> None: self.defaults: Dict[str, Any] = { # alignment in bytes 'kiwi_align': 1048576, # start sector number 'kiwi_startsector': 2048, # sectorsize in bytes 'kiwi_sectorsize': 512, # inode size in bytes for inode based filesystems 'kiwi_inode_size': 256, # inode ratio for inode based filesystems 'kiwi_inode_ratio': 16384, # minimum inode number for inode based filesystems 'kiwi_min_inodes': 20000, # kiwi git revision 'kiwi_revision': __githash__, } self.profile_key_list = [ 'kiwi_align', 'kiwi_startsector', 'kiwi_sectorsize', 'kiwi_revision', ] @staticmethod def get_luks_key_length() -> int: """ Provides key length to use for random luks keys """ return 256 @staticmethod def get_swapsize_mbytes() -> int: """ Provides swapsize in MB """ return 128 @staticmethod def get_xz_compression_options() -> List[str]: """ Provides compression options for the xz compressor :return: Contains list of options .. code:: python ['--option=value'] :rtype: list """ return ['--threads=0'] @staticmethod def get_platform_name() -> str: """ Provides the machine architecture name as used by KIWI This is the architecture name as it is returned by 'uname -m' with one exception for the 32bit x86 architecture which is handled as 'ix86' in general :return: architecture name :rtype: str """ arch = PLATFORM_MACHINE if arch == 'i686' or arch == 'i586': arch = 'ix86' return arch @staticmethod def set_platform_name(name: str) -> None: """ Sets the platform architecture once :param str name: an architecture name """ global PLATFORM_MACHINE PLATFORM_MACHINE = name @staticmethod def is_x86_arch(arch: str) -> bool: """ Checks if machine architecture is x86 based Any arch that matches 32bit and 64bit x86 architecture causes the method to return True. Anything else will cause the method to return False :rtype: bool """ x86_arch_names = ['x86_64', 'i686', 'i586', 'ix86'] if arch in x86_arch_names: return True return False @staticmethod def is_ppc64_arch(arch: str) -> bool: """ Checks if machine architecture is ppc64 based Any arch that matches little endian or big endian ppc64 architecture causes the method to return True. Anything else will cause the method to return False :rtype: bool """ ppc64_arch_names = ['ppc64', 'ppc64le'] if arch in ppc64_arch_names: return True return False @staticmethod def is_buildservice_worker() -> bool: """ Checks if build host is an open buildservice machine The presence of /.buildenv on the build host indicates we are building inside of the open buildservice :return: True if obs worker, else False :rtype: bool """ return os.path.exists(os.sep + Defaults.get_buildservice_env_name()) @staticmethod def get_buildservice_env_name() -> str: """ Provides the base name of the environment file in a buildservice worker :return: file basename :rtype: str """ return '.buildenv' @staticmethod def get_obs_download_server_url() -> str: """ Provides the default download server url hosting the public open buildservice repositories :return: url path :rtype: str """ return 'http://download.opensuse.org/repositories' @staticmethod def get_obs_api_server_url() -> str: """ Provides the default API server url to access the public open buildservice API :return: url path :rtype: str """ return 'https://api.opensuse.org' @staticmethod def get_solvable_location() -> str: """ Provides the directory to store SAT solvables for repositories. The solvable files are used to perform package dependency and metadata resolution :return: directory path :rtype: str """ return '/var/tmp/kiwi/satsolver' @staticmethod def set_runtime_checker_metadata(filename: str) -> None: """ Sets the runtime checker metadata filename :param str filename: a file path name """ global RUNTIME_CHECKER_METADATA RUNTIME_CHECKER_METADATA = filename @staticmethod def set_shared_cache_location(location: str) -> None: """ Sets the shared cache location once :param str location: a location path """ global SHARED_CACHE_DIR SHARED_CACHE_DIR = location @staticmethod def set_custom_runtime_config_file(filename: str) -> None: """ Sets the runtime config file once :param str filename: a file path name """ global CUSTOM_RUNTIME_CONFIG_FILE CUSTOM_RUNTIME_CONFIG_FILE = filename @staticmethod def set_temp_location(location: str) -> None: """ Sets the temp directory location once :param str location: a location path """ global TEMP_DIR TEMP_DIR = location @staticmethod def get_shared_cache_location() -> str: """ Provides the shared cache location This is a directory which shares data from the image buildsystem host with the image root system. The location is returned as an absolute path stripped off by the leading '/'. This is because the path is transparently used on the host / and inside of the image imageroot/ :return: directory path :rtype: str """ return os.path.abspath(os.path.normpath(SHARED_CACHE_DIR)).lstrip( os.sep ) @staticmethod def get_temp_location() -> str: """ Provides the base temp directory location This is the directory used to store any temporary files and directories created by kiwi during runtime :return: directory path :rtype: str """ return os.path.abspath(os.path.normpath(TEMP_DIR)) @staticmethod def get_sync_options() -> List[str]: """ Provides list of default data sync options :return: list of rsync options :rtype: list """ return [ '--archive', '--hard-links', '--xattrs', '--acls', '--one-file-system', '--inplace', ] @staticmethod def get_sync_sparse() -> bool: """ Provides default setting for sparse data sync If enabled, rsync is called with the --sparse option which turns sequences of null bytes in the source data into holes on the target. :return: True or False :rtype: bool """ return False @staticmethod def get_removed_files_name() -> str: """ Provides base file name to store removed files in a delta root build """ return 'removed' @staticmethod def get_system_files_name() -> str: """ Provides base file name to store system files in a container build """ return 'systemfiles' @staticmethod def get_exclude_list_for_removed_files_detection() -> List[str]: """ Provides list of files/dirs to exclude from the removed files detection in a delta root build """ return [ 'etc/hosts.kiwi', 'etc/hosts.sha', 'etc/resolv.conf.kiwi', 'etc/resolv.conf.sha', 'etc/sysconfig/proxy.kiwi', 'etc/sysconfig/proxy.sha', 'usr/lib/sysimage/rpm', ] @staticmethod def get_exclude_list_for_root_data_sync( no_tmpdirs: bool = True, ) -> List[str]: """ Provides the list of files or folders that are created by KIWI for its own purposes. Those files should be not be included in the resulting image. :return: list of file and directory names :rtype: list """ exclude_list = ['image', '.kconfig'] if no_tmpdirs: exclude_list += ['run/*', 'tmp/*'] exclude_list += [ Defaults.get_buildservice_env_name(), Defaults.get_shared_cache_location(), ] return exclude_list @staticmethod def get_runtime_checker_metadata() -> Dict: with open(RUNTIME_CHECKER_METADATA) as meta: return yaml.safe_load(meta) @staticmethod def _parse_exclude_file(root_dir: str, exclude_filename: str) -> List: """ Retrieves an exclusion list from the provided metadata file The file should contain a YAML dictionary with a top-level key named 'exclude' and the list of exclusions as its value. The list of exclusions may include: * file paths * folder paths * glob patterns Paths and patterns should be relative to the filesystem or directory that they're being excluded from. :return: list of paths and glob patterns :param string root_dir: image root directory :param string exclude_filename: file exclusion YAML metadata file :rtype: list """ exclude_file = os.sep.join([root_dir, 'image', exclude_filename]) exclude_list = [] if os.path.isfile(exclude_file): with open(exclude_file) as exclude: exclude_dict = yaml.safe_load(exclude) exclude_data = exclude_dict.get('exclude') if exclude_data and isinstance(exclude_data, list): for exclude_file in exclude_data: exclude_list.append(exclude_file.lstrip(os.sep)) else: log.warning( f'invalid yaml structure in {exclude_file}, ignored' ) return exclude_list @staticmethod def get_exclude_list_from_custom_exclude_files(root_dir: str) -> List: """ Gets the list of excluded items for the root filesystem from the optional metadata file image/exclude_files.yaml :return: list of paths and glob patterns :param string root_dir: image root directory :rtype: list """ return Defaults._parse_exclude_file(root_dir, 'exclude_files.yaml') @staticmethod def get_exclude_list_from_custom_exclude_files_for_efifatimage( root_dir: str, ) -> List: """ Gets the list of excluded items for the ESP's EFI folder from the optional metadata file image/exclude_files_efifatimage.yaml Excluded items must be relative to the ESP's /EFI directory. :return: list of paths and glob patterns :param string root_dir: EFI root directory :rtype: list """ return Defaults._parse_exclude_file( root_dir, 'exclude_files_efifatimage.yaml' ) @staticmethod def get_exclude_list_for_non_physical_devices() -> List[str]: """ Provides the list of folders that are not associated with a physical device. KIWI returns the basename of the folders typically used as mountpoint for those devices. :return: list of file and directory names :rtype: list """ exclude_list = ['proc', 'sys', 'dev'] return exclude_list @staticmethod def get_failsafe_kernel_options() -> str: """ Provides failsafe boot kernel options :return: list of kernel options .. code:: python ['option=value', 'option'] :rtype: list """ return ' '.join( ['ide=nodma', 'apm=off', 'noresume', 'edd=off', 'nomodeset', '3'] ) @staticmethod def get_volume_id() -> str: """ Provides default value for ISO volume ID :return: name :rtype: str """ return 'CDROM' @staticmethod def get_install_volume_id() -> str: """ Provides default value for ISO volume ID for install media :return: name :rtype: str """ return 'INSTALL' @staticmethod def get_snapper_config_template_file(root: str) -> str: """ Provides the default configuration template file for snapper. The location in etc/ are preferred over files in usr/ :return: file path :rtype: str """ snapper_templates = [ 'etc/snapper/config-templates/default', 'usr/share/snapper/config-templates/default', ] snapper_default_conf = '' for snapper_template in snapper_templates: template_config = os.path.join(root, snapper_template) if os.path.exists(template_config): snapper_default_conf = template_config break return snapper_default_conf @staticmethod def get_default_bootloader() -> str: """ Return default bootloader name which is grub2 :return: bootloader name :rtype: str """ return 'grub2' @staticmethod def get_grub_custom_arguments(root_dir: str) -> Dict[str, str]: return { 'grub_directory_name': Defaults.get_grub_boot_directory_name( root_dir ), 'grub_load_command': 'configfile', } @staticmethod def get_grub_boot_directory_name(lookup_path: str) -> str: """ Provides grub2 data directory name in boot/ directory Depending on the distribution the grub2 boot path could be either boot/grub2 or boot/grub. The method will decide for the correct base directory name according to the name pattern of the installed grub2 tools :return: directory basename :rtype: str """ if Path.which(filename='grub2-install', root_dir=lookup_path): # the presence of grub2-install is an indicator to put all # grub2 data below boot/grub2 return 'grub2' else: # in any other case the assumption is made that all grub # boot data should live below boot/grub return 'grub' @staticmethod def get_grub_basic_modules(multiboot: bool) -> List[str]: """ Provides list of basic grub modules :param bool multiboot: grub multiboot mode :return: list of module names :rtype: list """ modules = [ 'ext2', 'iso9660', 'linux', 'echo', 'configfile', 'search_label', 'search_fs_file', 'search', 'search_fs_uuid', 'ls', 'normal', 'gzio', 'png', 'fat', 'gettext', 'font', 'minicmd', 'gfxterm', 'gfxmenu', 'all_video', 'xfs', 'btrfs', 'squash4', 'lvm', 'luks', 'gcry_rijndael', 'gcry_sha256', 'gcry_sha512', 'crypto', 'cryptodisk', 'test', 'true', 'loadenv', ] if multiboot: modules.append('multiboot') return modules @staticmethod def get_grub_efi_modules(multiboot: bool = False) -> List[str]: """ Provides list of grub efi modules :param bool multiboot: grub multiboot mode :return: list of module names :rtype: list """ modules = Defaults.get_grub_basic_modules(multiboot) + [ 'part_gpt', 'part_msdos', 'efi_gop', ] return modules @staticmethod def get_grub_platform_modules(multiboot: bool = False) -> List[str]: """ Provides list of platform specific grub modules :param bool multiboot: grub multiboot mode :return: list of module names :rtype: list """ modules = Defaults.get_grub_basic_modules(multiboot) if Defaults.is_ppc64_arch(Defaults.get_platform_name()): return Defaults.get_grub_ofw_modules() else: modules += [ 'part_gpt', 'part_msdos', 'biosdisk', 'vga', 'vbe', 'chain', 'boot', ] return modules @staticmethod def get_grub_ofw_modules() -> List[str]: """ Provides list of grub ofw modules (ppc) :return: list of module names :rtype: list """ modules = Defaults.get_grub_basic_modules(multiboot=False) + [ 'part_gpt', 'part_msdos', 'boot', ] return modules @staticmethod def get_grub_s390_modules() -> List[str]: """ Provides list of grub ofw modules (s390) :return: list of module names :rtype: list """ modules = Defaults.get_grub_basic_modules(multiboot=False) + [ 'part_gpt', 'part_msdos', 'boot', ] return modules @staticmethod def get_grub_path( root_path: str, filename: str, raise_on_error: bool = True ) -> str: """ Provides grub path to given search file Depending on the distribution grub could be installed below a grub2 or grub directory. grub could also reside in /usr/lib as well as in /usr/share. Therefore this information needs to be dynamically looked up :param string root_path: root path to start the lookup from :param string filename: filename to search :param bool raise_on_error: raise on not found, defaults to True The method returns the path to the given grub search file. By default it raises a KiwiBootLoaderGrubDataError exception if the file could not be found in any of the search locations. If raise_on_error is set to False and no file could be found the function returns None :return: filepath :rtype: str """ log.debug(f'Searching grub file: {filename}') install_dirs = ['usr/share', 'usr/lib'] lookup_list = [] for grub_name in ['grub2', 'grub']: for install_dir in install_dirs: grub_path = os.path.join( root_path, install_dir, grub_name, filename ) log.debug(f'--> {grub_path}') if os.path.exists(grub_path): log.debug(f'--> Found in: {grub_path}') return grub_path lookup_list.append(grub_path) if raise_on_error: raise KiwiBootLoaderGrubDataError( 'grub path {0} not found in {1}'.format(filename, lookup_list) ) return '' @staticmethod def get_preparer() -> str: """ Provides ISO preparer name :return: name :rtype: str """ return 'KIWI - https://github.com/OSInside/kiwi' @staticmethod def get_publisher() -> str: """ Provides ISO publisher name :return: name :rtype: str """ return 'SUSE LINUX GmbH' @staticmethod def get_shim_loader(root_path: str) -> List[shim_loader_type]: """ Provides shim loader file path Searches distribution specific locations to find shim.efi below the given root path :param string root_path: image root path :return: list of shim_loader_type :rtype: list """ result = [] shim_pattern_type = namedtuple( 'shim_pattern_type', ['pattern', 'binaryname'] ) shim_file_patterns = [ shim_pattern_type( '/usr/lib/shim/shim*.efi.signed.latest', 'bootx64.efi' ), shim_pattern_type('/usr/lib/shim/shim*.efi.signed', 'bootx64.efi'), shim_pattern_type('/usr/lib/grub/*-efi-signed', 'bootx64.efi'), shim_pattern_type('/usr/share/efi/x86_64/shim.efi', 'bootx64.efi'), shim_pattern_type( '/usr/share/efi/aarch64/shim.efi', 'bootaa64.efi' ), shim_pattern_type('/usr/lib64/efi/shim.efi', 'bootx64.efi'), shim_pattern_type('/boot/efi/EFI/*/shimx64.efi', 'bootx64.efi'), shim_pattern_type('/boot/efi/EFI/*/shimia32.efi', 'bootia32.efi'), shim_pattern_type('/boot/efi/EFI/*/shimaa64.efi', 'bootaa64.efi'), shim_pattern_type( '/boot/efi/EFI/*/shimriscv64.efi', 'bootriscv64.efi' ), shim_pattern_type('/boot/efi/EFI/*/shim.efi', 'bootx64.efi'), shim_pattern_type('/usr/lib/shim/shim*.efi', 'bootx64.efi'), ] for shim_file_pattern in shim_file_patterns: for shim_file in sorted( glob.iglob(root_path + shim_file_pattern.pattern), key=len ): result.append( shim_loader_type(shim_file, shim_file_pattern.binaryname) ) # one match only expected, per pattern break return result @staticmethod def get_mok_manager(root_path: str) -> List[str]: """ Provides Mok Manager file path Searches distribution specific locations to find the Mok Manager EFI binary :param str root_path: image root path :return: file path or None :rtype: str """ result = [] mok_manager_file_patterns = [ '/usr/share/efi/*/MokManager.efi', '/usr/lib64/efi/MokManager.efi', '/boot/efi/EFI/*/mm*.efi', '/usr/lib/shim/mm*.efi', ] for mok_manager_file_pattern in mok_manager_file_patterns: for mm_file in glob.iglob(root_path + mok_manager_file_pattern): result.append(mm_file) return result @staticmethod def get_grub_efi_font_directory(root_path: str) -> Optional[str]: """ Provides distribution specific EFI font directory used with grub. :param string root_path: image root path :return: file path or None :rtype: str """ font_dir_patterns = ['/boot/efi/EFI/*/fonts'] for font_dir_pattern in font_dir_patterns: for font_dir in glob.iglob(root_path + font_dir_pattern): return font_dir return None @staticmethod def get_unsigned_grub_loader( root_path: str, target_type: str = 'disk' ) -> List[grub_loader_type]: """ Provides unsigned grub efi loader file path Searches distribution specific locations to find a distro grub EFI binary within the given root path :param string root_path: image root path :return: list of grub_loader_type :rtype: list """ result = [] grub_pattern_type = namedtuple( 'grub_pattern_type', ['pattern', 'binaryname', 'targetname'] ) unsigned_grub_file_patterns = { 'disk': [ grub_pattern_type( '/usr/share/grub*/x86_64-efi/grub.efi', 'grub.efi', 'bootx64.efi', ), grub_pattern_type( '/usr/lib/grub*/x86_64-efi/grub.efi', 'grub.efi', 'bootx64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubia32.efi', 'grubia32.efi', 'bootia32.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubaa64.efi', 'grubaa64.efi', 'bootaa64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubriscv64.efi', 'grubriscv64.efi', 'bootriscv64.efi', ), ], 'iso': [ grub_pattern_type( '/boot/efi/EFI/*/gcdx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/usr/share/grub*/x86_64-efi/grub.efi', 'grub.efi', 'bootx64.efi', ), grub_pattern_type( '/usr/lib/grub*/x86_64-efi/grub.efi', 'grub.efi', 'bootx64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubia32.efi', 'grubia32.efi', 'bootia32.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubaa64.efi', 'grubaa64.efi', 'bootaa64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubriscv64.efi', 'grubriscv64.efi', 'bootriscv64.efi', ), ], } for unsigned_grub_file_pattern in unsigned_grub_file_patterns[ target_type ]: for unsigned_grub_file in glob.iglob( root_path + unsigned_grub_file_pattern.pattern ): result.append( grub_loader_type( unsigned_grub_file, unsigned_grub_file_pattern.binaryname, unsigned_grub_file_pattern.targetname, ) ) # one match only expected, per pattern break return result @staticmethod def get_grub_chrp_loader(boot_path: str) -> str: """ Lookup CHRP boot loader (ppc) :param string boot_path: boot path :return: file base name :rtype: str """ for chrp_loader in ['grub.elf', 'core.elf']: for grub_chrp in glob.iglob( os.sep.join( [boot_path, 'boot/grub*/powerpc-ieee1275', chrp_loader] ) ): log.info(f'Found CHRP loader at: {grub_chrp}') return os.path.basename(grub_chrp) raise KiwiBootLoaderGrubDataError( f'CHRP loader not found in {boot_path}' ) @staticmethod def get_grub_platform_core_loader(root_path: str) -> Optional[str]: """ Provides grub bios image Searches distribution specific locations to find the core bios image below the given root path :param string root_path: image root path :return: file path or None :rtype: str """ bios_grub_core_patterns = [ '/usr/share/grub*/{0}/{1}'.format( Defaults.get_grub_platform_module_directory_name(), Defaults.get_grub_platform_image_name(), ), '/usr/lib/grub*/{0}/{1}'.format( Defaults.get_grub_platform_module_directory_name(), Defaults.get_grub_platform_image_name(), ), ] for bios_grub_core_pattern in bios_grub_core_patterns: for bios_grub_core in glob.iglob( root_path + bios_grub_core_pattern ): return bios_grub_core return None @staticmethod def get_iso_grub_loader() -> str: """ Return name of eltorito grub image used as ISO loader :return: file base name :rtype: str """ return 'eltorito.img' @staticmethod def get_iso_grub_mbr() -> str: """ Return name of hybrid MBR image used as ISO boot record :return: file base name :rtype: str """ return 'boot_hybrid.img' @staticmethod def get_signed_grub_loader( root_path: str, target_type: str = 'disk' ) -> List[grub_loader_type]: """ Provides shim signed grub loader file path Searches distribution specific locations to find a grub EFI binary within the given root path :param str root_path: image root path :return: list of grub_loader_type :rtype: list """ result = [] grub_pattern_type = namedtuple( 'grub_pattern_type', ['pattern', 'binaryname', 'targetname'] ) signed_grub_file_patterns = { 'disk': [ grub_pattern_type( '/usr/share/efi/*/grub.efi', 'grub.efi', 'bootx64.efi' ), grub_pattern_type( '/usr/lib64/efi/grub.efi', 'grub.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubia32.efi', 'grubia32.efi', 'bootia32.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubaa64.efi', 'grubaa64.efi', 'bootaa64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubriscv64.efi', 'grubriscv64.efi', 'bootriscv64.efi', ), grub_pattern_type( '/usr/share/grub*/*-efi/grub.efi', 'grub.efi', 'bootx64.efi' ), grub_pattern_type( '/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed', 'grubx64.efi', 'bootx64.efi', ), ], 'iso': [ grub_pattern_type( '/boot/efi/EFI/*/gcdx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/gcdaa64.efi', 'grubaa64.efi', 'bootaa64.efi', ), grub_pattern_type( '/usr/share/efi/x86_64/grub.efi', 'grub.efi', 'grubx64.efi' ), grub_pattern_type( '/usr/share/efi/aarch64/grub.efi', 'grub.efi', 'grubaa64.efi', ), grub_pattern_type( '/usr/lib64/efi/grub.efi', 'grub.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubx64.efi', 'grubx64.efi', 'bootx64.efi' ), grub_pattern_type( '/boot/efi/EFI/*/grubia32.efi', 'grubia32.efi', 'bootia32.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubaa64.efi', 'grubaa64.efi', 'bootaa64.efi', ), grub_pattern_type( '/boot/efi/EFI/*/grubriscv64.efi', 'grubriscv64.efi', 'bootriscv64.efi', ), grub_pattern_type( '/usr/share/grub*/x86_64-efi/grub.efi', 'grub.efi', 'bootx64.efi', ), grub_pattern_type( '/usr/lib/grub/x86_64-efi-signed/grubx64.efi.signed', 'grubx64.efi', 'bootx64.efi', ), ], } for signed_grub in signed_grub_file_patterns[target_type]: for signed_grub_file in glob.iglob(root_path + signed_grub.pattern): result.append( grub_loader_type( signed_grub_file, signed_grub.binaryname, signed_grub.targetname, ) ) # one match only expected, per pattern break return result @staticmethod def get_efi_vendor_directory(efi_path: str) -> Optional[str]: """ Provides EFI vendor directory if present Looks up distribution specific EFI vendor directory :param string root_path: path to efi mountpoint :return: directory path or None :rtype: str """ efi_vendor_directories = [ 'EFI/fedora', 'EFI/redhat', 'EFI/centos', 'EFI/almalinux', 'EFI/opensuse', 'EFI/ubuntu', 'EFI/debian', ] for efi_vendor_directory in efi_vendor_directories: efi_vendor_directory = os.sep.join([efi_path, efi_vendor_directory]) if os.path.exists(efi_vendor_directory): return efi_vendor_directory return None @staticmethod def get_vendor_grubenv(efi_path: str) -> Optional[str]: efi_vendor_directory = Defaults.get_efi_vendor_directory(efi_path) if efi_vendor_directory: grubenv = os.sep.join([efi_vendor_directory, 'grubenv']) if os.path.exists(grubenv): return grubenv return None @staticmethod def get_shim_vendor_directories(root_path: str) -> List[str]: """ Provides list of shim vendor directories Searches distribution specific locations to find shim.efi below the given root path and return the directory names to the file found :param string root_path: image root path :return: list of directory names or empty list :rtype: list """ shim_vendor_patterns = ['/boot/efi/EFI/*/shim*.efi', '/EFI/*/shim*.efi'] vendor_paths = [] for shim_vendor_pattern in shim_vendor_patterns: for shim_file in glob.iglob(root_path + shim_vendor_pattern): shim_path = os.path.dirname(shim_file) if shim_path not in vendor_paths: vendor_paths.append(shim_path) return vendor_paths @staticmethod def get_default_volume_group_name() -> str: """ Provides default LVM volume group name :return: name :rtype: str """ return 'systemVG' @staticmethod def get_min_partition_mbytes() -> int: """ Provides default minimum partition size in mbytes :return: mbsize value :rtype: int """ return 10 @staticmethod def get_min_volume_mbytes(filesystem: str) -> int: """ Provides default minimum LVM volume size in mbytes per filesystem :return: mbsize value :rtype: int """ if filesystem == 'btrfs': return 120 elif filesystem == 'xfs': return 300 else: return 30 @staticmethod def get_lvm_overhead_mbytes() -> int: """ Provides empiric LVM overhead size in mbytes :return: mbsize value :rtype: int """ return 80 @staticmethod def get_default_boot_mbytes() -> int: """ Provides default boot partition size in mbytes :return: mbsize value :rtype: int """ return 300 @staticmethod def get_default_efi_boot_mbytes() -> int: """ Provides default EFI partition size in mbytes :return: mbsize value :rtype: int """ return 20 @staticmethod def get_recovery_spare_mbytes() -> int: """ Provides spare size of recovery partition in mbytes :return: mbsize value :rtype: int """ return 300 @staticmethod def get_default_legacy_bios_mbytes() -> int: """ Provides default size of bios_grub partition in mbytes :return: mbsize value :rtype: int """ return 2 @staticmethod def get_default_prep_mbytes() -> int: """ Provides default size of prep partition in mbytes :return: mbsize value :rtype: int """ return 8 @staticmethod def get_disk_format_types() -> List[str]: """ Provides supported disk format types :return: disk types :rtype: list """ return [ 'gce', 'qcow2', 'vmdk', 'ova', 'meta', 'vmx', 'vhd', 'vhdx', 'vhdfixed', 'vdi', 'vagrant.libvirt.box', 'vagrant.virtualbox.box', 'oci', ] @staticmethod def get_vagrant_config_virtualbox_guest_additions() -> bool: """ Provides the default value for ``vagrantconfig.virtualbox_guest_additions_present`` :return: whether guest additions are expected to be present in the vagrant box :rtype: bool """ return False @staticmethod def get_firmware_types() -> Dict[str, List[str]]: """ Provides supported architecture specific firmware types :return: firmware types per architecture :rtype: dict """ return { 'x86_64': ['efi', 'uefi', 'bios', 'ec2'], 'i586': ['bios'], 'i686': ['bios'], 'ix86': ['bios'], 'aarch64': ['efi', 'uefi'], 'arm64': ['efi', 'uefi'], 'armv5el': ['efi', 'uefi'], 'armv5tel': ['efi', 'uefi'], 'armv6hl': ['efi', 'uefi'], 'armv6l': ['efi', 'uefi'], 'armv7hl': ['efi', 'uefi'], 'armv7l': ['efi', 'uefi'], 'armv8l': ['efi', 'uefi'], 'loongarch64': ['efi', 'uefi'], 'ppc': ['ofw'], 'ppc64': ['ofw', 'opal'], 'ppc64le': ['ofw', 'opal'], 'riscv64': ['efi', 'uefi'], 's390': [], 's390x': [], } @staticmethod def get_default_firmware(arch: str) -> Optional[str]: """ Provides default firmware for specified architecture :param string arch: machine architecture name :return: firmware name :rtype: str """ default_firmware = { 'x86_64': 'bios', 'i586': 'bios', 'i686': 'bios', 'ix86': 'bios', 'loongarch64': 'efi', 'ppc': 'ofw', 'ppc64': 'ofw', 'ppc64le': 'ofw', 'arm64': 'efi', 'aarch64': 'efi', 'armv5el': 'efi', 'armv5tel': 'efi', 'armv6hl': 'efi', 'armv6l': 'efi', 'armv7hl': 'efi', 'armv7l': 'efi', 'armv8l': 'efi', 'riscv64': 'efi', } if arch in default_firmware: return default_firmware[arch] return None @staticmethod def get_efi_capable_firmware_names() -> List[str]: """ Provides list of EFI capable firmware names. These are those for which kiwi supports the creation of an EFI bootable disk image :return: firmware names :rtype: list """ return ['efi', 'uefi'] @staticmethod def get_ec2_capable_firmware_names() -> List[str]: """ Provides list of EC2 capable firmware names. These are those for which kiwi supports the creation of disk images bootable within the Amazon EC2 public cloud :return: firmware names :rtype: list """ return ['ec2'] @staticmethod def get_efi_module_directory_name(arch: str) -> Optional[str]: """ Provides architecture specific EFI directory name which stores the EFI binaries for the desired architecture. :param string arch: machine architecture name :return: directory name :rtype: str """ default_module_directory_names = { 'x86_64': 'x86_64-efi', 'i386': 'i386-efi', # There is no dedicated xen architecture but there are # modules provided for xen. Thus we treat it as an # architecture 'x86_64_xen': 'x86_64-xen', 'aarch64': 'arm64-efi', 'arm64': 'arm64-efi', 'armv5el': 'arm-efi', 'armv5tel': 'arm-efi', 'armv6l': 'arm-efi', 'armv7l': 'arm-efi', 'armv8l': 'arm-efi', 'loongarch64': 'loongarch64-efi', 'riscv64': 'riscv64-efi', } return default_module_directory_names.get(arch) @staticmethod def get_grub_platform_module_directory_name() -> str: """ Provides grub platform specific directory name which stores the grub module binaries :return: directory name :rtype: str """ return ( 'powerpc-ieee1275' if Defaults.is_ppc64_arch(Defaults.get_platform_name()) else 'i386-pc' ) @staticmethod def get_efi_image_name(arch: str) -> Optional[str]: """ Provides architecture specific EFI boot binary name :param string arch: machine architecture name :return: name :rtype: str """ default_efi_image_names = { 'x86_64': 'bootx64.efi', 'i386': 'bootia32.efi', 'aarch64': 'bootaa64.efi', 'arm64': 'bootaa64.efi', 'armv5el': 'bootarm.efi', 'armv5tel': 'bootarm.efi', 'armv6l': 'bootarm.efi', 'armv7l': 'bootarm.efi', 'armv8l': 'bootarm.efi', 'loongarch64': 'bootloongarch64.efi', 'riscv64': 'bootriscv64.efi', } return default_efi_image_names.get(arch) @staticmethod def get_grub_platform_image_name() -> str: """ Provides platform specific core boot binary name :return: name :rtype: str """ return ( 'grub.elf' if Defaults.is_ppc64_arch(Defaults.get_platform_name()) else 'core.img' ) @staticmethod def get_default_boot_timeout_seconds() -> int: """ Provides default boot timeout in seconds :return: seconds :rtype: int """ return 10 @staticmethod def get_default_disk_start_sector() -> int: """ Provides the default initial disk sector for the first disk partition. :return: sector value :rtype: int """ return Defaults().defaults['kiwi_startsector'] @staticmethod def get_default_efi_partition_table_type() -> str: """ Provides the default partition table type for efi firmwares. :return: partition table type name :rtype: str """ return 'gpt' @staticmethod def get_default_inode_size() -> int: """ Provides default size of inodes in bytes. This is only relevant for inode based filesystems :return: bytesize value :rtype: int """ return Defaults().defaults['kiwi_inode_size'] @staticmethod def get_archive_image_types() -> List[str]: """ Provides list of supported archive image types :return: archive names :rtype: list """ return ['tbz', 'cpio'] @staticmethod def get_container_image_types() -> List[str]: """ Provides list of supported container image types :return: container names :rtype: list """ return ['docker', 'oci', 'appx', 'wsl'] @staticmethod def get_filesystem_image_types() -> List[str]: """ Provides list of supported filesystem image types :return: filesystem names :rtype: list """ return [ 'ext2', 'ext3', 'ext4', 'btrfs', 'squashfs', 'xfs', 'fat16', 'fat32', 'erofs', ] @staticmethod def get_default_live_iso_type() -> str: """ Provides default live iso union type :return: live iso type :rtype: str """ return 'overlay' @staticmethod def get_default_uri_type() -> str: """ Provides default URI type Absolute path specifications used in the context of an URI will apply the specified default mime type :return: URI mime type :rtype: str """ return 'dir:/' @staticmethod def get_dracut_conf_name() -> str: """ Provides file path of dracut config file to be used with KIWI :return: file path name :rtype: str """ return '/etc/dracut.conf.d/02-kiwi.conf' @staticmethod def get_live_dracut_modules_from_flag(flag_name: str) -> List[str]: """ Provides flag_name to dracut modules name map Depending on the value of the flag attribute in the KIWI image description specific dracut modules need to be selected :return: dracut module names as list :rtype: list """ live_modules = { 'overlay': ['kiwi-live'], 'dmsquash': ['dmsquash-live', 'livenet'], } if flag_name in live_modules: return live_modules[flag_name] else: return ['kiwi-live'] @staticmethod def get_default_live_iso_root_filesystem() -> str: """ Provides default live iso root filesystem type :return: filesystem name :rtype: str """ return 'ext4' @staticmethod def get_live_iso_persistent_boot_options( persistent_filesystem: Optional[str] = None, ) -> List[str]: """ Provides list of boot options passed to the dracut kiwi-live module to setup persistent writing :return: list of boot options :rtype: list """ live_iso_persistent_boot_options = ['rd.live.overlay.persistent'] if persistent_filesystem: live_iso_persistent_boot_options.append( 'rd.live.overlay.cowfs={0}'.format(persistent_filesystem) ) return live_iso_persistent_boot_options @staticmethod def get_disk_image_types() -> List[str]: """ Provides supported disk image types :return: disk image type names :rtype: list """ return ['oem'] @staticmethod def get_live_image_types() -> List[str]: """ Provides supported live image types :return: live image type names :rtype: list """ return ['iso'] @staticmethod def get_kis_image_types() -> List[str]: """ Provides supported kis image types :return: kis image type names :rtype: list """ return ['kis', 'pxe'] @staticmethod def get_enclaves_image_types() -> List[str]: """ Provides supported enclave(initrd-only) image types :return: enclave image type names :rtype: list """ return ['enclave'] @staticmethod def get_boot_image_description_path() -> str: """ Provides the path to find custom kiwi boot descriptions :return: directory path :rtype: str """ return '/usr/share/kiwi/custom_boot' @staticmethod def get_boot_image_strip_file() -> str: """ Provides the file path to bootloader strip metadata. This file contains information about the files and directories automatically striped out from the kiwi initrd :return: file path :rtype: str """ return Defaults.project_file('config/strip.xml') @staticmethod def get_schema_file() -> str: """ Provides file path to kiwi RelaxNG schema :return: file path :rtype: str """ return Defaults.project_file('schema/kiwi.rng') @staticmethod def get_common_functions_file() -> str: """ Provides the file path to config functions metadata. This file contains bash functions used for system configuration or in the boot code from the kiwi initrd :return: file path :rtype: str """ return Defaults.project_file('config/functions.sh') @staticmethod def get_xsl_stylesheet_file() -> str: """ Provides the file path to the KIWI XSLT style sheets :return: file path :rtype: str """ return Defaults.project_file('xsl/master.xsl') @staticmethod def get_schematron_module_name() -> str: """ Provides module name for XML SchemaTron validations :return: python module name :rtype: str """ return 'lxml.isoschematron' @staticmethod def project_file(filename: str) -> str: """ Provides the python module base directory search path The method uses the importlib.resources.path method to identify files and directories from the application :param string filename: relative project file :return: absolute file path name :rtype: str """ with as_file(importlib.resources.files('kiwi')) as path: return f'{path}/{filename}' @staticmethod def get_imported_root_image(root_dir: str) -> str: """ Provides the path to an imported root system image If the image description specified a derived_from attribute the file from this attribute is copied into the root_dir using the name as provided by this method :param string root_dir: image root directory :return: file path name :rtype: str """ return os.sep.join([root_dir, 'image', 'imported_root']) @staticmethod def get_iso_boot_path() -> str: """ Provides arch specific relative path to boot files on kiwi iso filesystems :return: relative path name :rtype: str """ return os.sep.join(['boot', Defaults.get_platform_name()]) @staticmethod def get_iso_kernel_flavor_file_name( name: str, index: int, flavor: str ) -> str: """ Provides the name of the kernel or initrd file of the kernel of the given flavor in the boot directory of ISO images The files of the first flavor, which provides the default boot menu entry, keep the given name, e.g linux. This is also the name the install image boots the installed system from with kexec. The files of each further flavor have the flavor appended, e.g linux-rt :param str name: base file name, e.g linux or initrd :param int index: index of the flavor in the kernel flavor list :param str flavor: kernel flavor name :return: file name :rtype: str """ return f'{name}-{flavor}' if index else name @staticmethod def get_iso_tool_category() -> str: """ Provides default iso tool category :return: name :rtype: str """ return 'xorriso' @staticmethod def get_iso_media_tag_tool() -> Literal['checkmedia']: """ Provides default iso media tag tool :return: name :rtype: str """ return 'checkmedia' @staticmethod def get_container_compression() -> bool: """ Provides default container compression :return: True :rtype: bool """ return True @staticmethod def get_default_container_name() -> str: """ Provides the default container name. :return: name :rtype: str """ return 'kiwi-container' @staticmethod def get_container_base_image_tag() -> str: """ Provides the tag used to identify base layers during the build of derived images. :return: tag :rtype: str """ return 'base_layer' @staticmethod def get_oci_archive_tool() -> str: """ Provides the default OCI archive tool name. :return: name :rtype: str """ return 'umoci' @staticmethod def get_part_mapper_tool() -> str: """ Provides the default partition mapper tool name. :return: name :rtype: str """ host_architecture = Defaults.get_platform_name() if 's390' in host_architecture: return 'kpartx' return 'partx' @staticmethod def get_default_container_tag() -> str: """ Provides the default container tag. :return: tag :rtype: str """ return 'latest' @staticmethod def get_default_container_subcommand() -> List[str]: """ Provides the default container subcommand. :return: command as a list of arguments :rtype: list """ return ['/bin/bash'] @staticmethod def get_default_container_created_by() -> str: """ Provides the default 'created by' history entry for containers. :return: the specific kiwi version used for the build :rtype: str """ return 'KIWI {0}'.format(__version__) @staticmethod def get_custom_rpm_macros_path() -> str: """ Returns the custom macros directory for the rpm database. :return: path name :rtype: str """ return 'usr/lib/rpm/macros.d' @staticmethod def get_custom_rpm_bootstrap_macro_name() -> str: """ Returns the rpm bootstrap macro file name created in the custom rpm macros path :return: filename :rtype: str """ return 'macros.kiwi-bootstrap-config' @staticmethod def get_custom_rpm_image_macro_name() -> str: """ Returns the rpm image macro file name created in the custom rpm macros path :return: filename :rtype: str """ return 'macros.kiwi-image-config' @staticmethod def get_default_package_manager() -> str: """ Returns the default package manager name if none is configured in the image description :return: package manager name :rtype: str """ return 'dnf4' @staticmethod def get_default_packager_tool(package_manager: str) -> Optional[str]: """ Provides the packager tool according to the package manager :param string package_manager: package manger name :return: packager tool binary name :rtype: str """ rpm_based = ['zypper', 'dnf4', 'dnf5', 'microdnf'] deb_based = ['apt'] if package_manager in rpm_based: return 'rpm' elif package_manager in deb_based: return 'dpkg' elif package_manager == 'pacman': return 'pacman' return None @staticmethod def get_discoverable_partition_ids() -> Dict[str, str]: """ Provides arch specific partition UUIDs as defined by the UAPI group :return: partition UUIDs :rtype: dict """ arch = Defaults.get_platform_name() part_uuids_archs = { 'x86_64': { 'root': '4f68bce3e8cd4db196e7fbcaf984b709', 'usr': '8484680c952148c69c11b0720656f69e', 'usr-verity': '77ff5f63e7b64633acf41565b864c0e6', }, 'ix86': { 'root': '44479540f29741b29af7d131d5f0458a', 'usr': '75250d768cc6458ebd66bd47cc81a812', 'usr-verity': '8f461b0d14ee4e819aa9049b6fb97abd', }, 'aarch64': { 'root': 'b921b0451df041c3af444c6f280d3fae', 'usr': 'b0e01050ee5f4390949a9101b17104e9', 'usr-verity': '6e11a4e7fbca4dedb9e9e1a512bb664e', }, 'riscv64': { 'root': '72ec70a6cf7440e6bd494bda08e8f224', 'usr': 'beaec34b8442439ba40b984381ed097d', 'usr-verity': '8f1056be9b0547c481d6be53128e5b54', }, } part_uuids_arch = part_uuids_archs.get(arch) or {} return { 'root': part_uuids_arch.get('root') or '', 'usr': part_uuids_arch.get('usr') or '', 'usr-verity': part_uuids_arch.get('usr-verity') or '', 'usr-secondary': '75250d768cc6458ebd66bd47cc81a812', 'usr-secondary-verity': '8f461b0d14ee4e819aa9049b6fb97abd', 'esp': 'c12a7328f81f11d2ba4b00a0c93ec93b', 'xbootldr': 'bc13c2ff59e64262a352b275fd6f7172', 'swap': '0657fd6da4ab43c484e50933c84b4f4f', 'home': '933ac7e12eb44f13b8440e14e2aef915', 'srv': '3b8f842520e04f3b907f1a25a76f98e8', 'var': '4d21b016b53445c2a9fb5c16e091fd2d', 'tmp': '7ec6f5573bc54acab29316ef5df639d1', 'user-home': '773f91ef66d449b5bd83d683bf40ad16', 'linux-generic': '0fc63daf848347728e793d69d8477de4', } @staticmethod def get_bls_loader_entries_dir() -> str: """ Provide default loader entries directory for BLS loaders :return: directory name :rtype: str """ return '/boot/loader/entries' @staticmethod def get_apk_repo_config() -> str: """ Repository file for apk :return: file path name :rtype: str """ return '/etc/apk/repositories' @staticmethod def get_ca_update_map(target_distribution: str) -> Optional[Dict[str, str]]: return CA_UPDATE_MAP.get(target_distribution) @staticmethod def get_ca_target_distributions() -> List[str]: return sorted(CA_UPDATE_MAP.keys()) def get(self, key: str) -> Optional[Union[int, str]]: """ Implements get method for profile elements :param string key: profile keyname :return: key value :rtype: str """ return self.defaults.get(key) @staticmethod def get_profile_file(root_dir: str) -> str: """ Return name of profile file for given root directory """ return root_dir + '/.profile' def to_profile(self, profile: 'Profile') -> None: """ Implements method to add list of profile keys and their values to the specified instance of a Profile class :param object profile: Profile instance """ for key in sorted(self.profile_key_list): # Do not apply default values to any variable that was # already defined in the profile instance. cur_profile = profile.dot_profile if key not in cur_profile or cur_profile[key] is None: profile.add(key, self.get(key)) ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/exceptions.py0000644000000000000000000005610115273675667013224 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Sequence, Union class KiwiError(Exception): """ **Base class to handle all known exceptions** Specific exceptions are implemented as sub classes of KiwiError Attributes :param string message: Exception message text """ def __init__( self, message: Union[str, Exception, Sequence[Exception]] ) -> None: self.message = message def __str__(self) -> str: return format(self.message) class KiwiArchiveSetupError(KiwiError): """ Exception raised if an unsupported image archive type is used. """ class KiwiArchiveTarError(KiwiError): """ Exception raised if impossible to determine which tar command version is installed on the underlying system """ class KiwiBootImageSetupError(KiwiError): """ Exception raised if an unsupported initrd system type is used. """ class KiwiBootLoaderConfigSetupError(KiwiError): """ Exception raised if a configuration for an unsupported bootloader is requested. """ class KiwiBootLoaderGrubDataError(KiwiError): """ Exception raised if no grub installation was found. """ class KiwiBootLoaderGrubFontError(KiwiError): """ Exception raised if no grub unicode font was found. """ class KiwiBootLoaderGrubInstallError(KiwiError): """ Exception raised if grub install to master boot record has failed. """ class KiwiBootLoaderGrubMenuEntryError(KiwiError): """ Exception raised if the menu entry of a kernel to be selected as the default entry was not found in the grub configuration. """ class KiwiBootLoaderGrubModulesError(KiwiError): """ Exception raised if the synchronisation of modules from the grub installation to the boot space has failed. """ class KiwiBootLoaderGrubPlatformError(KiwiError): """ Exception raised if an attempt was made to use grub on an unsupported platform. """ class KiwiBootLoaderGrubSecureBootError(KiwiError): """ Exception raised if the Microsoft signed shim loader or grub2 loader could not be found in the image root system """ class KiwiBootLoaderInstallSetupError(KiwiError): """ Exception raised if an installation for an unsupported bootloader is requested. """ class KiwiBootLoaderTargetError(KiwiError): """ Exception raised if the target to read the bootloader path from is not a disk or an iso image. """ class KiwiBootLoaderZiplInstallError(KiwiError): """ Exception raised if the installation of zipl has failed. """ class KiwiBootLoaderZiplPlatformError(KiwiError): """ Exception raised if a configuration for an unsupported zipl architecture is requested. """ class KiwiBootLoaderZiplSetupError(KiwiError): """ Exception raised if the data set to configure the zipl bootloader is incomplete. """ class KiwiBootStrapPhaseFailed(KiwiError): """ Exception raised if the bootstrap phase of the system prepare command has failed. """ class KiwiBundleError(KiwiError): """ Exception raised if the system bundle command has failed. """ class KiwiCommandError(KiwiError): """ Exception raised if an external command called via a Command instance has returned with an exit code != 0 or could not be called at all. """ class KiwiCommandNotFound(KiwiError): """ Exception raised if any executable command cannot be found in the evironment PATH variable. """ class KiwiCommandNotLoaded(KiwiError): """ Exception raised if a kiwi command task module could not be loaded. """ class KiwiCompressionFormatUnknown(KiwiError): """ Exception raised if the compression format of the data could not be detected. """ class KiwiConfigFileNotFound(KiwiError): """ Exception raised if no kiwi XML description was found. """ class KiwiConfigFileFormatNotSupported(KiwiError): """ Exception raised if kiwi description file format is not supported. """ class KiwiContainerSetupError(KiwiError): """ Exception raised if an error in the creation of the container archive happened. """ class KiwiContainerImageSetupError(KiwiError): """ Exception raised if an attempt to create a container instance for an unsupported container type is performed. """ class KiwiDataStructureError(KiwiError): """ Exception raised if the XML description failed to parse the data structure. """ class KiwiDebianBootstrapError(KiwiError): """ Exception raised if the bootstrap installation for Debian based systems has failed """ class KiwiDescriptionInvalid(KiwiError): """ Exception raised if the XML description failed to validate the XML schema. """ class KiwiDeviceProviderError(KiwiError): """ Exception raised if a storage provide is asked for its managed device but no such device exists. """ class KiwiDiskBootImageError(KiwiError): """ Exception raised if a kiwi boot image does not provide the requested data, e.g kernel, or hypervisor files. """ class KiwiDiskFormatSetupError(KiwiError): """ Exception raised if an attempt was made to create a disk format instance of an unsupported disk format. """ class KiwiDiskGeometryError(KiwiError): """ Exception raised if the disk geometry (partition table) could not be read or evaluated against their expected geometry and capabilities. """ class KiwiDistributionNameError(KiwiError): """ Exception raised if the distribution name could not be found. The information is extracted from the boot attribute of the XML description. If no boot attribute is present or does not match the naming conventions the exception is raised. """ class KiwiExtensionError(KiwiError): """ Exception raised if an extension section of the same namespace is used multiple times as toplevel section within the extension section. Each extension must have a single toplevel entry point qualified by its namespace """ class KiwiFileNotFound(KiwiError): """ Exception raised if the requested file could not be found. """ class KiwiFileSystemSetupError(KiwiError): """ Exception raised if an attempt was made to build an unsupported or unspecified filesystem. """ class KiwiFileSystemSyncError(KiwiError): """ Exception raised if the data sync from the system into the loop mounted filesystem image failed. """ class KiwiFormatSetupError(KiwiError): """ Exception raised if the requested disk format could not be created. """ class KiwiHelpNoCommandGiven(KiwiError): """ Exception raised if the request for the help page is executed without a command to show the help for. """ class KiwiImageResizeError(KiwiError): """ Exception raised if the request to resize a disk image failed. Reasons could be a missing raw disk reference or a wrong size specification. """ class KiwiImportDescriptionError(KiwiError): """ Exception raised if the XML description data and scripts could not be imported into the root of the image. """ class KiwiInstallBootImageError(KiwiError): """ Exception raised if the required files to boot an installation image could not be found, e.g kernel or hypervisor. """ class KiwiInstallMediaError(KiwiError): """ Exception raised if a request for an installation media is made but the system image type is not an oem type. """ class KiwiInstallPhaseFailed(KiwiError): """ Exception raised if the install phase of a system prepare command has failed. """ class KiwiPackagesDeletePhaseFailed(KiwiError): """ Exception raised if the packages deletion phase in system prepare fails. """ class KiwiIsoMetaDataError(KiwiError): """ Exception raised if an inconsistency in the ISO header was found such like invalid eltorito specification or a broken path table. """ class KiwiIsoToolError(KiwiError): """ Exception raised if an iso helper tool such as isoinfo could not be found on the build system. """ class KiwiKernelLookupError(KiwiError): """ Exception raised if the search for the kernel image file failed """ class KiwiLiveBootImageError(KiwiError): """ Exception raised if an attempt was made to use an unsupported live iso type. """ class KiwiLuksSetupError(KiwiError): """ Exception raised if not enough user data is provided to setup the luks encryption on the given device. """ class KiwiLoadCommandUndefined(KiwiError): """ Exception raised if no command is specified for a given service on the commandline. """ class KiwiLoadPluginError(KiwiError): """ Exception raised if loading the plugin has failed """ class KiwiLogFileSetupFailed(KiwiError): """ Exception raised if the log file could not be created. """ class KiwiLogSocketSetupFailed(KiwiError): """ Exception raised if the Unix Domain log socket could not be created. """ class KiwiLoopSetupError(KiwiError): """ Exception raised if not enough user data to create a loop device is specified. """ class KiwiMappedDeviceError(KiwiError): """ Exception raised if the device to become mapped does not exist. """ class KiwiMountKernelFileSystemsError(KiwiError): """ Exception raised if a kernel filesystem such as proc or sys could not be mounted. """ class KiwiMountSharedDirectoryError(KiwiError): """ Exception raised if the host <-> image shared directory could not be mounted. """ class KiwiNotImplementedError(KiwiError): """ Exception raised if a functionality is not yet implemented. """ class KiwiOCIArchiveToolError(KiwiError): """ Exception raised if the requested OCI archive tool is not supported """ class KiwiOSReleaseImportError(KiwiError): """ Exception raised if reading etc/os-release caused an issue """ class KiwiEnvImportError(KiwiError): """ Exception raised if extending os.environ with another env file caused an issue """ class KiwiPackageManagerSetupError(KiwiError): """ Exception raised if an attempt was made to create a package manager instance for an unsupported package manager. """ class KiwiPartitionerGptFlagError(KiwiError): """ Exception raised if an attempt was made to set an unknown partition flag for an entry in the GPT table. """ class KiwiPartitionerMsDosFlagError(KiwiError): """ Exception raised if an attempt was made to set an unknown partition flag for an entry in the MSDOS table. """ class KiwiPartitionerSetupError(KiwiError): """ Exception raised if an attempt was made to create an instance of a partitioner for an unsupporte partitioner. """ class KiwiPrivilegesError(KiwiError): """ Exception raised if root privileges are required but not granted. """ class KiwiProfileNotFound(KiwiError): """ Exception raised if a specified profile does not exist in the XML configuration. """ class KiwiKisBootImageError(KiwiError): """ Exception raised if a required boot file e.g the kernel could not be found in the process of building a kis image. """ class KiwiEnclaveBootImageError(KiwiError): """ Exception raised if no kernel image was found while building an enclave image. """ class KiwiRaidSetupError(KiwiError): """ Exception raised if invalid or not enough user data is provided to create a raid array on the specified storage device. """ class KiwiRepositorySetupError(KiwiError): """ Exception raised if an attempt was made to create an instance of a repository for an unsupported package manager. """ class KiwiRequestedTypeError(KiwiError): """ Exception raised if an attempt was made to build an image for an unsupported image type. """ class KiwiRequestError(KiwiError): """ Exception raised if a package request could not be processed by the corresponding package manager instance. """ class KiwiResizeRawDiskError(KiwiError): """ Exception raised if an attempt was made to resize the image disk to a smaller size than the current one. Simply shrinking a disk image file is not possible without data corruption because the partitions were setup to use the entire disk geometry as it fits into the file. A successful shrinking operation would require the filesystems and the partition table to be reduced which is not done by the provided simple storage resize method. In addition without the user overwriting the disk size in the XML setup, kiwi will calculate the minimum required size in order to store the data. Thus in almost all cases it will not be possible to store the data in a smaller disk. """ class KiwiResultError(KiwiError): """ Exception raised if the image build result pickle information could not be created or loaded. """ class KiwiRootDirExists(KiwiError): """ Exception raised if the specified image root directory already exists and should not be re-used. """ class KiwiRootInitCreationError(KiwiError): """ Exception raised if the initialization of a new image root directory has failed. """ class KiwiRpmDirNotRemoteError(KiwiError): """ Exception raised if the provided rpm-dir repository is not local """ class KiwiRuntimeConfigFileError(KiwiError): """ Exception raised if the provided custom runtime config file could not be found """ class KiwiRuntimeConfigFormatError(KiwiError): """ Exception raised if the expected format in the yaml KIWI runtime config file does not match """ class KiwiRuntimeError(KiwiError): """ Exception raised if a runtime check has failed. """ class KiwiSatSolverJobError(KiwiError): """ Exception raised if a sat solver job can not be done, e.g because the requested package or collection does not exist in the registered repository metadata """ class KiwiSatSolverJobProblems(KiwiError): """ Exception raised if the sat solver operations returned with solver problems e.g package conflicts """ class KiwiSatSolverPluginError(KiwiError): """ Exception raised if the python solv module failed to load. The solv module is provided by SUSE's rpm package python-solv and provides a python binding to the libsolv C library """ class KiwiAnyMarkupPluginError(KiwiError): """ Exception raised if the python anymarkup module failed to load. """ class KiwiMarkupConversionError(KiwiError): """ Exception raised if the markup format conversion is not possible. """ class KiwiSchemaImportError(KiwiError): """ Exception raised if the schema file could not be read by lxml.RelaxNG. """ class KiwiScriptFailed(KiwiError): """ Exception raised if a user script returned with an exit code != 0. """ class KiwiSetupIntermediateConfigError(KiwiError): """ Exception raised if the setup of the temporary image system configuration for the duration of the build process has failed. """ class KiwiSolverRepositorySetupError(KiwiError): """ Exception raised if the repository type is not supported for the creation of a SAT solvable """ class KiwiSystemDeletePackagesFailed(KiwiError): """ Exception raised if the deletion of a package has failed in the corresponding package manager instance. """ class KiwiSystemInstallPackagesFailed(KiwiError): """ Exception raised if the installation of a package has failed in the corresponding package manager instance. """ class KiwiSystemUpdateFailed(KiwiError): """ Exception raised if the package upgrade has failed in the corresponding package manager instance. """ class KiwiTargetDirectoryNotFound(KiwiError): """ Exception raised if the specified target directory to store the image results was not found. """ class KiwiTemplateError(KiwiError): """ Exception raised if the substitution of variables in a configuration file template has failed. """ class KiwiTypeNotFound(KiwiError): """ Exception raised if no build type was found in the XML description. """ class KiwiUnknownServiceName(KiwiError): """ Exception raised if an unknown service name was provided on the commandline. """ class KiwiUriOpenError(KiwiError): """ Exception raised if the urllib urlopen request has failed """ class KiwiUriStyleUnknown(KiwiError): """ Exception raised if an unsupported URI style was used in the source definition of a repository. """ class KiwiUriTypeUnknown(KiwiError): """ Exception raised if the protocol type of an URI is unknown in the source definition of a repository. """ class KiwiValidationError(KiwiError): """ Exception raised if the XML validation against the schema has failed. """ class KiwiVhdTagError(KiwiError): """ Exception raised if the GUID tag is not provided in the expected format. """ class KiwiVolumeGroupConflict(KiwiError): """ Exception raised if the requested LVM volume group already is in use on the build system. """ class KiwiVolumeManagerSetupError(KiwiError): """ Exception raised if the preconditions for volume mangement support are not met or an attempt was made to create an instance of a volume manager for an unsupported volume management system. """ class KiwiSnapshotManagerSetupError(KiwiError): """ Exception raised if an attempt was made to create an instance of a snapshot manager for an unsupported snapshot management system. """ class KiwiVolumeRootIDError(KiwiError): """ Exception raised if the root volume can not be found. This concept currently exists only for the btrfs subvolume system. """ class KiwiRootImportError(KiwiError): """ Exception is raised when something fails during the root import procedure. """ class KiwiContainerBuilderError(KiwiError): """ Exception is raised when something fails during a container image build procedure. """ class KiwiSizeError(KiwiError): """ Exception is raised when the convertion from a given size in string format to a number. """ class KiwiCommandCapabilitiesError(KiwiError): """ Exception is raised when some the CommandCapabilities methods fails, usually meaning there is some issue trying to parse some command output. """ class KiwiDecodingError(KiwiError): """ Exception is raised on decoding literals failure """ class KiwiBuildahError(KiwiError): """ Exception raised on inconsistent buildah class calls """ class KiwiFileAccessError(KiwiError): """ Exception raised if accessing a file or its metadata failed """ class KiwiShellVariableValueError(KiwiError): """ Exception raised if a given python value cannot be converted into a string representation for use in shell scripts """ class KiwiIncludFileNotFoundError(KiwiError): """ Exception raised if the file reference in an statement could not be found """ class KiwiUmountBusyError(KiwiError): """ Exception raised if the attempt to umount a resource has failed """ class KiwiCustomPartitionConflictError(KiwiError): """ Exception raised if the entry in a custom partition setup conflicts with an existing partition table layout setting """ class KiwiVolumeTooSmallError(KiwiError): """ Exception raised if the specified volume size is smaller than the required bytes to store the data """ class KiwiPartitionTooSmallError(KiwiError): """ Exception raised if the specified partition size is smaller than the required bytes to store the data """ class KiwiCredentialsError(KiwiError): """ Exception raised if required credentials information is missing """ class KiwiOffsetError(KiwiError): """ Exception raised if the offset for a seek operation does not match the expected data to write """ class KiwiBootLoaderDiskPasswordError(KiwiError): """ Exception raised if the disk password could not be set """ class KiwiEnclaveFormatError(KiwiError): """ Exception raised if no enclave_format attribute specified for the selected build type """ class KiwiCATargetDistributionError(KiwiError): """ Exception raised if no CA target distribution can be found but the request to import custom CA certificates was issued """ class KiwiBoxConfigError(KiwiError): """ Exception raised if the box config yaml file is invalid """ class KiwiBoxNameError(KiwiError): """ Exception raised if the boxname could not be found """ class KiwiBoxVirtioFsError(KiwiError): """ Exception raised if virtiofsd does not start """ class KiwiBoxArchNotFoundError(KiwiError): """ Exception raised if the selected architecture has no box configuration """ class KiwiBoxQEMUBinaryNotFound(KiwiError): """ Exception raised if no QEMU binary for the desired box architecture could be found """ class KiwiBoxSSHPortInvalid(KiwiError): """ Exception raised if an invalid SSH port is passed as an argument """ class KiwiBoxSSHKeyNotFound(KiwiError): """ Exception raised if the SSH pkey file could not be found """ class KiwiBoxChecksumError(KiwiError): """ Exception raised in case of a box file checksum error """ class KiwiBoxDownloadError(KiwiError): """ Exception raised in case of a box download issue """ class KiwiBoxTargetPathError(KiwiError): """ Exception raised in case of an issue creating or accessing the box build target dir """ class KiwiBoxBuildError(KiwiError): """ Exception raised if the build process in a box failed or could not be started """ class KiwiStackBuildTargetDirExists(KiwiError): """ Exception raised if the stackbuild image root directory already exists """ class KiwiStackBuildContainerNameInvalid(KiwiError): """ Exception raised if the image name cannot be used as stash container name """ class KiwiStackBuildRootSyncFailed(KiwiError): """ Exception raised if the rsync process to sync the stash into the root-tree failed """ ././@PaxHeader0000000000000000000000000000003200000000000010210 xustar0026 mtime=1794079670.74488 kiwi-11.1.8/kiwi/filesystem/__init__.py0000644000000000000000000000665015273675667014772 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import Dict, List, Optional from abc import ABCMeta, abstractmethod # project from kiwi.storage.device_provider import DeviceProvider from kiwi.filesystem.base import FileSystemBase from kiwi.exceptions import KiwiFileSystemSetupError filesystem_class_map = { 'ext2': 'Ext2', 'ext3': 'Ext3', 'ext4': 'Ext4', 'btrfs': 'Btrfs', 'xfs': 'Xfs', 'fat16': 'Fat16', 'fat32': 'Fat32', 'squashfs': 'SquashFs', 'swap': 'Swap', 'erofs': 'EroFs', } class FileSystem(metaclass=ABCMeta): """ **FileSystem factory** :param string name: filesystem name :param object device_provider: Instance of DeviceProvider :param string root_dir: root directory path name :param dict custom_args: dict of custom filesystem arguments """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( name: str, device_provider: DeviceProvider, root_dir: Optional[str] = None, custom_args: Optional[Dict] = None, # noqa: E252 ) -> FileSystemBase: try: filesystem = importlib.import_module( 'kiwi.filesystem.{0}'.format(name) ) return filesystem.__dict__[ 'FileSystem{0}'.format(filesystem_class_map[name]) ](device_provider, root_dir, custom_args) except Exception as issue: raise KiwiFileSystemSetupError( 'Support for {0} filesystem not implemented: {1}'.format( name, issue ) ) @staticmethod def sparse_install_mount_options(name: str) -> List[str]: """ Provide the mount options that the filesystem implementation of the given name declares as needed at install time for a sparse data sync to be effective. Reading the declaration does not require an instance of the filesystem. This allows callers that mount a filesystem they did not create, as the volume managers do, to apply the same options as the filesystem implementation would. :param string name: filesystem name :return: list of mount options :rtype: list """ try: filesystem = importlib.import_module( 'kiwi.filesystem.{0}'.format(name) ) return filesystem.__dict__[ 'FileSystem{0}'.format(filesystem_class_map[name]) ].sparse_install_mount_options except Exception: # A filesystem that cannot be looked up declares nothing. # Reporting an unsupported filesystem is the task of new(), # which is where one gets created. return [] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/base.py0000644000000000000000000004106415273675667014143 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import uuid import random import logging import copy from functools import reduce from types import TracebackType from typing import Dict, List, Optional, Type # project import kiwi.defaults as defaults from kiwi.defaults import Defaults from kiwi.runtime_config import RuntimeConfig from kiwi.utils.sync import DataSync from kiwi.mount_manager import MountManager from kiwi.command import Command from kiwi.storage.device_provider import DeviceProvider from kiwi.utils.veritysetup import VeritySetup from kiwi.exceptions import KiwiFileSystemSyncError log = logging.getLogger('kiwi') class FileSystemBase: """ **Implements base class for filesystem interface** :param object device_provider: Instance of a class based on DeviceProvider required for filesystems which needs a block device for creation. In most cases the DeviceProvider is a LoopDevice :param string root_dir: root directory path name :param dict custom_args: custom filesystem arguments """ # Mount options applied only to the install time mount of a newly created # filesystem. They are deliberately not part of the mount options from the # image description and therefore never end up in the fstab of the # image. A filesystem implementation declares options here when sparse # sync cannot work without them, see the xfs implementation for an # example. sparse_install_mount_options: List[str] = [] def __init__( self, device_provider: DeviceProvider, root_dir: str = '', custom_args: Dict = {}, ) -> None: # filesystems created with a block device stores the mountpoint # here. The file name of the file containing the filesystem is # stored in the device_provider if the filesystem is represented # as a file there self.filesystem_mount: Optional[MountManager] = None # the underlaying device provider. This is only required if the # filesystem required a block device to become created self.device_provider = device_provider self.root_dir = root_dir # filesystems created without a block device stores the result # filesystem file name here self.filename = '' self.custom_args: Dict = {} self.post_init(custom_args) self.veritysetup: Optional[VeritySetup] = None def __enter__(self) -> 'FileSystemBase': return self def post_init(self, custom_args: Dict) -> None: """ Post initialization method Store dictionary of custom arguments if not empty. This overrides the default custom argument hash :param dict custom_args: custom arguments .. code:: python { 'create_options': ['option'], 'mount_options': ['option'], 'meta_data': { 'key': 'value' } } """ if custom_args: self.custom_args = copy.deepcopy(custom_args) if not self.custom_args.get('create_options'): self.custom_args['create_options'] = [] if not self.custom_args.get('meta_data'): self.custom_args['meta_data'] = {} if not self.custom_args.get('mount_options'): self.custom_args['mount_options'] = [] if not self.custom_args.get('fs_attributes'): self.custom_args['fs_attributes'] = [] def set_uuid(self) -> None: """ Create new random filesystem UUID Implement in specialized filesystem class for filesystems which supports the concept of an UUID and allows to change it """ log.warning( 'Instance {0} has no support for setting a new UUID label'.format( type(self).__name__ ) ) def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create filesystem on block device Implement in specialized filesystem class for filesystems which requires a block device for creation, e.g ext4. :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ raise NotImplementedError def create_on_file( self, filename: str, label: Optional[str] = None, exclude: Optional[List[str]] = None, ) -> None: """ Create filesystem from root data tree Implement in specialized filesystem class for filesystems which requires a data tree for creation, e.g squashfs. :param string filename: result file path name :param string label: label name :param list exclude: list of exclude dirs/files """ raise NotImplementedError def get_mountpoint(self) -> Optional[str]: """ Provides mount point directory Effective use of the directory is guaranteed only after sync_data :return: directory path name :rtype: string """ if self.filesystem_mount: return self.filesystem_mount.mountpoint return None def sync_data(self, exclude: List[str] = []) -> MountManager: """ Copy data tree into filesystem :param list exclude: list of exclude dirs/files :return: The mount created for syncing data. It should be used to un-mount the filesystem again. """ if not self.root_dir: raise KiwiFileSystemSyncError('no root directory specified') if not os.path.exists(self.root_dir): raise KiwiFileSystemSyncError( f'given root directory {self.root_dir} does not exist' ) self.filesystem_mount = MountManager( device=self.device_provider.get_device() ) self.filesystem_mount.mount(self._get_install_mount_options()) self._apply_attributes() data = DataSync(self.root_dir, self.filesystem_mount.mountpoint) data.sync_data(exclude=exclude, options=Defaults.get_sync_options()) return self.filesystem_mount def _get_install_mount_options(self) -> List[str]: """ Provide mount options for the install time mount of the filesystem the image payload is synced into. These are the mount options from the image description, plus any options the filesystem implementation declares as needed for a sparse data sync. The latter apply to this mount only and are not passed on to the fstab of the image. As everywhere else in kiwi the result is a list of at most one element, holding the comma separated option string that is passed along to the -o mount option. :return: max one element list with mount option string :rtype: list """ return FileSystemBase.merge_install_mount_options( self.sparse_install_mount_options, self.custom_args['mount_options'] ) @staticmethod def merge_install_mount_options( sparse_options: List[str], given_options: List[str] ) -> List[str]: """ Merge the mount options declared for a sparse data sync into the mount options from the image description. The declared options are only used if a sparse data sync was requested, and only for an option the image description does not set itself. The result describes an install time mount and is never used to build the fstab of the image. This is a static method because a filesystem is not always mounted by its own implementation. The volume managers mount the volumes they created themselves and use this to arrive at the same options. :param list sparse_options: declared sparse sync mount options :param list given_options: mount options from the image description :return: max one element list with mount option string :rtype: list """ if not sparse_options or not RuntimeConfig().get_sync_sparse(): return list(given_options) options = ','.join(given_options).split(',') if given_options else [] given_names = [option.split('=')[0] for option in options if option] for option in sparse_options: if option.split('=')[0] in given_names: # an explicit setting from the image description wins continue log.info( 'Using install time mount option {0} for sparse sync'.format( option ) ) options.append(option) return [','.join(options)] if options else [] def create_verity_layer( self, blocks: Optional[int] = None, filename: Optional[str] = None ) -> None: """ Create veritysetup on device :param int block: Number of blocks to use for veritysetup. If not specified the entire root device is used :param str filename: Target filename to use for VeritySetup. If not specified the filename or block special provided at object construction time is used """ on_file_name = filename or self.filename self.veritysetup = VeritySetup( on_file_name or self.device_provider.get_device(), blocks ) log.info( '--> Creating dm verity hash ({0} blocks)...'.format( blocks or 'all' ) ) log.debug( '--> dm verity metadata: {0}'.format(self.veritysetup.format()) ) def create_verification_metadata(self, device_node: str = '') -> None: """ Write verification block at the end of the device :param str device_node: Target device node, if not specified the root device from this instance is used """ if self.veritysetup: log.info('--> Creating verification metadata...') self.veritysetup.create_verity_verification_metadata() log.info('--> Signing verification metadata...') self.veritysetup.sign_verification_metadata() self.veritysetup.write_verification_metadata( device_node or self.device_provider.get_device() ) def mount(self) -> None: """ Mount the filesystem """ if self.filesystem_mount: self.filesystem_mount.mount(self.custom_args['mount_options']) def umount(self) -> None: """ Umounts the filesystem in case it is mounted, does nothing otherwise """ if self.filesystem_mount: log.info('umount %s instance', type(self).__name__) self.filesystem_mount.umount() def umount_volumes(self) -> None: """ Consistency layer with regards to VolumeManager classes Invokes umount """ self.umount() def mount_volumes(self) -> None: """ Consistency layer with regards to VolumeManager classes Invokes mount """ self.mount() def get_volumes(self) -> Dict: """ Consistency layer with regards to VolumeManager classes Raises """ raise NotImplementedError def get_root_volume_name(self) -> None: """ Consistency layer with regards to VolumeManager classes Raises """ raise NotImplementedError def get_fstab( self, persistency_type: str = 'by-label', filesystem_name: str = '' ) -> List[str]: """ Consistency layer with regards to VolumeManager classes Raises """ raise NotImplementedError def set_property_readonly_root(self) -> None: """ Consistency layer with regards to VolumeManager classes Raises """ raise NotImplementedError def _generate_seed_uuid(self, label: str, random_bits: int = 128) -> str: """ Create random UUID. If SOURCE_DATE_EPOCH is present use SOURCE_DATE_EPOCH + label name as seed """ sde = os.environ.get('SOURCE_DATE_EPOCH') if sde: label_seed = reduce(lambda x, y: x + y, map(ord, label)) epoch_seed = label_seed + int(sde) log.info( 'Using UUID seed SOURCE_DATE_EPOCH:{0} + LABEL:{1}={2}'.format( sde, label, label_seed ) ) rd = random.Random() rd.seed(epoch_seed) return format(uuid.UUID(int=rd.getrandbits(random_bits))) else: return format(uuid.uuid4()) def _map_size(self, size: float, from_unit: str, to_unit: str) -> float: """ Return byte size value for given size and unit :param float size: requested filesystem size. The value is interpreted by the given from_unit. :param str from_unit: source unit :param str to_unit: target unit :return: size value in unit: to_unit :rtype: float """ unit_map = { defaults.UNIT.byte: 1, defaults.UNIT.kb: 1024, defaults.UNIT.mb: 1048576, defaults.UNIT.gb: 1073741824, } byte_size = size * unit_map[from_unit] return byte_size / unit_map[to_unit] def _fs_size( self, size: float, blocksize: int = 1, unit: str = defaults.UNIT.kb ) -> str: """ Calculate filesystem size parameter in number of blocksize blocks. If the given size is <= 0 the calculation is done from the actual size of the block device reduced by the given size :param float size: requested filesystem size. The value is interpreted by the given unit. :param int blocksize: blocksize as requested from the filesystem creation tool for specifying the filesystem size. The value is interpreted by the given unit. By default set to: 1 :param str unit: Unit to use for calculations and return value Default unit is set to: defaults.UNIT.kb :return: an int block count of the specified unit as str :rtype: str """ if size > 0: result_size = size / blocksize else: device_name = self.device_provider.get_device() device_byte_size = self.device_provider.get_byte_size(device_name) requested_byte_size = self._map_size(size, unit, defaults.UNIT.byte) result_size = self._map_size( (device_byte_size + requested_byte_size) / blocksize, from_unit=defaults.UNIT.byte, to_unit=unit, ) return format(int(result_size)) def _apply_attributes(self) -> None: """ Apply filesystem attributes """ attribute_map = {'synchronous-updates': '+S', 'no-copy-on-write': '+C'} for attribute in self.custom_args['fs_attributes']: if attribute_map.get(attribute): assert self.filesystem_mount is not None log.info( '--> setting {0} for {1}'.format( attribute, self.filesystem_mount.mountpoint ) ) Command.run( [ 'chattr', attribute_map[attribute], self.filesystem_mount.mountpoint, ] ) def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.umount() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/btrfs.py0000644000000000000000000000470715273675667014354 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Optional # project import kiwi.defaults as defaults from kiwi.command import Command from kiwi.filesystem.base import FileSystemBase class FileSystemBtrfs(FileSystemBase): """ **Implements creation of btrfs filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create btrfs filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ device = self.device_provider.get_device() call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-L') call_args.append(label) if uuid: call_args.append('-U') call_args.append(uuid) if size: call_args.append('--byte-count') call_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.byte ), unit=defaults.UNIT.byte, ) ) Command.run(['mkfs.btrfs'] + call_args + [device]) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['btrfstune', '-f', '-u', device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/erofs.py0000644000000000000000000000440215273675667014342 0ustar00# Copyright (c) 2024 SUSE Software Solutions Germany GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Optional # project from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemEroFs(FileSystemBase): """ **Implements creation of erofs filesystem** """ def create_on_file( self, filename: str, label: Optional[str] = None, exclude: Optional[List[str]] = None, ) -> None: """ Create erofs filesystem from data tree :param string filename: result file path name :param string label: volume label :param list exclude: list of exclude dirs/files """ self.filename = filename exclude_options = [] compression = self.custom_args.get('compression') call_args = self.custom_args['create_options'].copy() if compression: call_args.append('-z') call_args.append(compression) if exclude: for item in exclude: # item is a glob, but erofs requires a POSIX extended regex. # Translate the glob to a regex for correct behaviour. # # We can't use fnmatch.translate, as that produces a Python regex. as_regex = '^' + item.replace('*', '.*') + '$' exclude_options.append(f'--exclude-regex={as_regex}') if label: call_args.append('-L') call_args.append(label) Command.run( ['mkfs.erofs'] + call_args + exclude_options + [self.filename, self.root_dir] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/ext2.py0000644000000000000000000000475715273675667014123 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemExt2(FileSystemBase): """ **Implements creation of ext2 filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create ext2 filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ device_args = [self.device_provider.get_device()] call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-L') call_args.append(label) if uuid: call_args.append('-U') call_args.append(uuid) if size: device_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) Command.run(['mkfs.ext2'] + call_args + device_args) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['e2fsck', '-y', '-f', device], raise_on_error=False) Command.run(['tune2fs', '-f', '-U', 'random', device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/ext3.py0000644000000000000000000000475715273675667014124 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemExt3(FileSystemBase): """ **Implements creation of ext3 filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create ext3 filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ device_args = [self.device_provider.get_device()] call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-L') call_args.append(label) if uuid: call_args.append('-U') call_args.append(uuid) if size: device_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) Command.run(['mkfs.ext3'] + call_args + device_args) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['e2fsck', '-y', '-f', device], raise_on_error=False) Command.run(['tune2fs', '-f', '-U', 'random', device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/ext4.py0000644000000000000000000000475715273675667014125 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemExt4(FileSystemBase): """ **Implements creation of ext4 filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create ext4 filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ device_args = [self.device_provider.get_device()] call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-L') call_args.append(label) if uuid: call_args.append('-U') call_args.append(uuid) if size: device_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) Command.run(['mkfs.ext4'] + call_args + device_args) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['e2fsck', '-y', '-f', device], raise_on_error=False) Command.run(['tune2fs', '-f', '-U', 'random', device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/fat16.py0000644000000000000000000000477615273675667014163 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project from uuid import UUID import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemFat16(FileSystemBase): """ **Implements creation of fat16 filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create fat16 filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: Volume Label, there is no real UUID on fat """ device_args = [self.device_provider.get_device()] call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-n') call_args.append(label) if uuid: call_args.append('-i') call_args.append(f'{UUID(uuid).time_low:08X}') if size: device_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) Command.run(['mkdosfs', '-F16', '-I'] + call_args + device_args) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['mlabel', '-n', '-i', device, '::']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/fat32.py0000644000000000000000000000477615273675667014161 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project from uuid import UUID import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemFat32(FileSystemBase): """ **Implements creation of fat32 filesystem** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create fat32 filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: Volume Label, there is no real UUID on fat """ device_args = [self.device_provider.get_device()] call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-n') call_args.append(label) if uuid: call_args.append('-i') call_args.append(f'{UUID(uuid).time_low:08X}') if size: device_args.append( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) Command.run(['mkdosfs', '-F32', '-I'] + call_args + device_args) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['mlabel', '-n', '-i', device, '::']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/isofs.py0000644000000000000000000000343315273675667014352 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import List, Optional # project from kiwi.filesystem.base import FileSystemBase from kiwi.iso_tools import IsoTools log = logging.getLogger('kiwi') class FileSystemIsoFs(FileSystemBase): """ **Implements creation of iso filesystem** """ def create_on_file( self, filename: str, label: Optional[str] = None, exclude: Optional[List[str]] = None, ) -> None: """ Create iso filesystem from data tree There is no label which could be set for iso filesystem thus this parameter is not used :param string filename: result file path name :param string label: unused :param list exclude: unused """ self.filename = filename meta_data = self.custom_args['meta_data'] efi_loader = meta_data.get('efi_loader') iso_tool = IsoTools.new(self.root_dir) iso_tool.init_iso_creation_parameters(meta_data) if efi_loader: iso_tool.add_efi_loader_parameters(efi_loader, meta_data) iso_tool.create_iso(self.filename) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/setup.py0000644000000000000000000000667615273675667014403 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Optional import logging # project from kiwi.system.size import SystemSize from kiwi.defaults import Defaults from kiwi.xml_state import XMLState log = logging.getLogger('kiwi') class FileSystemSetup: """ **Implement filesystem setup methods** Methods from this class provides information from the root directory required before building a filesystem image :param object xml_state: Instance of XMLState :param string root_dir: root directory path """ def __init__(self, xml_state: XMLState, root_dir: str): self.configured_size = xml_state.get_build_type_size( include_unpartitioned=True ) if xml_state.get_build_type_unpartitioned_bytes() > 0: log.warning( 'Unpartitoned size attribute is ignored for filesystem images' ) self.size = SystemSize(root_dir) self.requested_image_type = xml_state.get_build_type_name() if self.requested_image_type in Defaults.get_filesystem_image_types(): self.requested_filesystem = self.requested_image_type else: self.requested_filesystem = xml_state.build_type.get_filesystem() def get_size_mbytes(self, filesystem: Optional[str] = None) -> int: """ Precalculate the requires size in mbytes to store all data from the root directory in the requested filesystem. Return the configured value if present, if not return the calculated result :param string filesystem: name :return: mbytes :rtype: int """ fstype = self.requested_filesystem if not filesystem else filesystem root_dir_mbytes = self.size.accumulate_mbyte_file_sizes() filesystem_mbytes = self.size.customize(root_dir_mbytes, fstype) if not self.configured_size: log.info('Using calculated size: %d MB', filesystem_mbytes) return filesystem_mbytes elif self.configured_size.additive: result_filesystem_mbytes = ( self.configured_size.mbytes + filesystem_mbytes ) log.info( 'Using configured size: %d MB + %d MB calculated = %d MB', self.configured_size.mbytes, filesystem_mbytes, result_filesystem_mbytes, ) return result_filesystem_mbytes else: log.info( 'Using configured size: %d MB', self.configured_size.mbytes ) if self.configured_size.mbytes < filesystem_mbytes: log.warning( '--> Configured size smaller than calculated size: %d MB', filesystem_mbytes, ) return self.configured_size.mbytes ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/squashfs.py0000644000000000000000000000600515273675667015062 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Optional # project from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command from kiwi.defaults import Defaults class FileSystemSquashFs(FileSystemBase): """ **Implements creation of squashfs filesystem** """ def create_on_file( self, filename: str, label: Optional[str] = None, exclude: Optional[List[str]] = None, ) -> None: """ Create squashfs filesystem from data tree There is no label which could be set for squashfs thus this parameter is not used :param string filename: result file path name :param string label: unused :param list exclude: list of exclude dirs/files """ self.filename = filename exclude_options = [] compression = self.custom_args.get('compression') call_args = self.custom_args['create_options'].copy() if compression is None or compression == 'xz': if '-comp' not in self.custom_args['create_options']: call_args.append('-comp') call_args.append('xz') if '-Xbcj' not in self.custom_args['create_options']: host_architecture = Defaults.get_platform_name() if Defaults.is_x86_arch(host_architecture): call_args.append('-Xbcj') call_args.append('x86') if 'ppc' in host_architecture: call_args.append('-Xbcj') call_args.append('powerpc') elif compression != 'uncompressed': if '-comp' not in self.custom_args['create_options']: call_args.append('-comp') call_args.append(compression) else: for flag in ['-noI', '-noD', '-noF', '-noX']: if flag not in self.custom_args['create_options']: call_args.append(flag) if exclude: exclude_options.extend(['-wildcards', '-e']) for item in exclude: exclude_options.append(item) Command.run( [ 'mksquashfs', self.root_dir, self.filename, '-noappend', '-b', '1M', ] + call_args + exclude_options ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/swap.py0000644000000000000000000000310515273675667014175 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see from typing import Optional # project import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemSwap(FileSystemBase): """ **Implements creation of swap space** """ def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create swap space on block device :param string label: label name :param int size: unused :param str unit: unused :param str uuid: unused """ device = self.device_provider.get_device() call_args = self.custom_args['create_options'].copy() if label: call_args.append('-L') call_args.append(label) Command.run(['mkswap'] + call_args + [device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/filesystem/xfs.py0000644000000000000000000000764515273675667014040 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Optional # project import kiwi.defaults as defaults from kiwi.filesystem.base import FileSystemBase from kiwi.command import Command class FileSystemXfs(FileSystemBase): """ **Implements creation of xfs filesystem** """ # Minimize speculative preallocation when a sparse data sync is requested. # # rsync --sparse only seeks forward across a sequence of null bytes, it # does not punch a hole. On xfs most small skips lead to speculative # preallocation (optimized for a fast-changing runtime) instead of the # desired holes (as desired during one-time image construction). # rsync therefore fails to save most space without this setting. sparse_install_mount_options = ['allocsize=4k'] def create_on_device( self, label: Optional[str] = None, size: int = 0, unit: str = defaults.UNIT.kb, uuid: Optional[str] = None, ) -> None: """ Create xfs filesystem on block device :param str label: label name :param int size: size value, can also be counted from the end via -X The value is interpreted in units of: unit :param str unit: unit name. Default unit is set to: defaults.UNIT.kb :param str uuid: UUID name """ device = self.device_provider.get_device() call_args = self.custom_args['create_options'].copy() if not uuid and label: uuid = self._generate_seed_uuid(label) if label: call_args.append('-L') call_args.append(label) if uuid: call_args.append('-m') call_args.append(f'uuid={uuid}') if size: call_args.append('-d') call_args.append( 'size={0}k'.format( self._fs_size( size=self._map_size( size, from_unit=unit, to_unit=defaults.UNIT.kb ), unit=defaults.UNIT.kb, ) ) ) Command.run(['mkfs.xfs', '-f'] + call_args + [device]) # Materialize log on disk. Recent mkfs.xfs versions (v 5.5+) do not # write zeros into the log, but FALLOC_FL_ZERO_RANGE that space. # This leads to sparse holes in the underlying image file. Image # upload tools may skip these holes when creating EBS snapshots. # EBS is usually returning 0 when reading from such sparse areas, # but not when the image is server-side encrypted, where you get # decrypt(0) == noise instead. When mounting file system with # partially garbled logs, XFS trips and cannot find its log head, # and ultimately gives up ("XFS (nvme0n1p1): log mount/recovery # failed: error -117"). Jumping to cycle count 2 forces real, # non-zero writes throughout the log and sidesteps the problem. Command.run(['xfs_db', '-x', '-c', 'logformat -c 2', device]) def set_uuid(self) -> None: """ Create new random filesystem UUID """ device = self.device_provider.get_device() Command.run(['xfs_repair', '-L', device]) Command.run(['xfs_admin', '-U', 'generate', device]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/firmware.py0000644000000000000000000001425115273675667012657 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re from typing import Optional # project from .defaults import Defaults from .xml_state import XMLState from .exceptions import KiwiNotImplementedError class FirmWare: """ **Implements firmware specific methods** According to the selected firmware some parameters in a disk image changes. This class provides methods to provide firmware dependant information * :param object xml_state: instance of :class:`XMLState` """ def __init__(self, xml_state: XMLState) -> None: self.arch = Defaults.get_platform_name() self.zipl_target_type = xml_state.get_build_type_bootloader_targettype() self.firmware = xml_state.build_type.get_firmware() self.efipart_mbytes = xml_state.build_type.get_efipartsize() self.efi_partition_table = xml_state.build_type.get_efiparttable() self.gpt_hybrid_mbr = xml_state.build_type.get_gpt_hybrid_mbr() self.efi_csm = ( True if xml_state.build_type.get_eficsm() is None else xml_state.build_type.get_eficsm() ) if not self.firmware: self.firmware = Defaults.get_default_firmware(self.arch) if self.firmware and self.firmware != 'custom': firmware_types = Defaults.get_firmware_types() if self.firmware not in firmware_types[self.arch]: raise KiwiNotImplementedError( 'support for firmware %s for arch %s not implemented' % (self.firmware, self.arch) ) def get_partition_table_type(self) -> str: """ Provides partition table type according to architecture and firmware :return: partition table name :rtype: str """ if 's390' in self.arch: if self.zipl_target_type and 'CDL' in self.zipl_target_type: return 'dasd' elif self.zipl_target_type and 'GPT' in self.zipl_target_type: return 'gpt' else: return 'msdos' elif 'ppc64' in self.arch: return 'gpt' elif self.efi_mode(): default_efi_table = Defaults.get_default_efi_partition_table_type() return self.efi_partition_table or default_efi_table else: return 'msdos' def legacy_bios_mode(self) -> bool: """ Check if the legacy boot from BIOS systems should be activated :return: True or False :rtype: bool """ if self.get_partition_table_type() == 'gpt': if ( (self.arch == 'x86_64' or re.match('i.86', self.arch)) and (self.firmware == 'efi' or self.firmware == 'uefi') and self.efi_csm ): return True else: return False elif self.get_partition_table_type() == 'msdos': if self.arch == 'x86_64' or re.match('i.86', self.arch): return True else: return False else: return False def efi_mode(self) -> str: """ Check if EFI mode is requested :return: The requested EFI mode or None if no EFI mode requested :rtype: str """ if self.firmware in Defaults.get_efi_capable_firmware_names(): return self.firmware return '' def ec2_mode(self) -> Optional[str]: """ Check if EC2 mode is requested :return: True or False :rtype: bool """ if self.firmware in Defaults.get_ec2_capable_firmware_names(): return self.firmware return None def bios_mode(self) -> bool: """ Check if BIOS mode is requested :return: True or False :rtype: bool """ if self.firmware == 'bios': return True else: return False def ofw_mode(self) -> bool: """ Check if OFW mode is requested :return: True or False :rtype: bool """ if self.firmware == 'ofw': return True else: return False def opal_mode(self) -> bool: """ Check if Opal mode is requested :return: True or False :rtype: bool """ if self.firmware == 'opal': return True else: return False def get_legacy_bios_partition_size(self) -> int: """ Size of legacy bios_grub partition if legacy BIOS mode is required. Returns 0 if no such partition is needed :return: mbsize value :rtype: int """ if self.legacy_bios_mode() and self.efi_mode(): return Defaults.get_default_legacy_bios_mbytes() else: return 0 def get_efi_partition_size(self) -> int: """ Size of EFI partition. Returns 0 if no such partition is needed :return: mbsize value :rtype: int """ if self.efi_mode(): if self.efipart_mbytes: return self.efipart_mbytes else: return Defaults.get_default_efi_boot_mbytes() else: return 0 def get_prep_partition_size(self) -> int: """ Size of Prep partition if OFW mode is requested. Returns 0 if no such partition is needed :return: mbsize value :rtype: int """ if self.ofw_mode(): return Defaults.get_default_prep_mbytes() else: return 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/help.py0000644000000000000000000000276315273675667012000 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import subprocess from typing import Optional # project from .exceptions import KiwiHelpNoCommandGiven class Help: """ **Implements man page help for kiwi commands** Each kiwi command implements their own manual page, which is shown if the positional argument 'help' is passed to the command. """ def show(self, command: Optional[str] = None) -> None: """ Call man to show the command specific manual page All kiwi commands store their manual page in the section '8' of the man system. The calling process is replaced by the man process :param string command: man page name """ if not command: raise KiwiHelpNoCommandGiven("No help context specified") subprocess.call('man 8 ' + command, shell=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7454617 kiwi-11.1.8/kiwi/iso_tools/__init__.py0000644000000000000000000000311015273675667014604 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import Any from abc import ABCMeta, abstractmethod # project from kiwi.exceptions import KiwiIsoToolError from kiwi.runtime_config import RuntimeConfig class IsoTools(metaclass=ABCMeta): """ **IsoTools factory** """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new(source_dir: str) -> Any: name_map = {'xorriso': 'XorrIso'} runtime_config = RuntimeConfig() tool = runtime_config.get_iso_tool_category() try: iso_tool = importlib.import_module('kiwi.iso_tools.{}'.format(tool)) module_name = 'IsoTools{}'.format(name_map[tool]) return iso_tool.__dict__[module_name](source_dir) except Exception: raise KiwiIsoToolError( 'No support for {} tool available'.format(tool) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/iso_tools/base.py0000644000000000000000000001272515273675667013773 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import shutil import logging from typing import Dict, List, Optional, Union # project from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.utils.sync import DataSync from kiwi.path import Path log = logging.getLogger('kiwi') class IsoToolsBase: """ **Base Class for Parameter API for iso creation tools** """ def __init__(self, source_dir: str) -> None: """ Base class for IsoTools :param string source_dir: data source dir, usually root_dir """ self.arch = Defaults.get_platform_name() self.source_dir = source_dir self.boot_path = Defaults.get_iso_boot_path() self.iso_parameters: List[str] = [] self.iso_loaders: List[str] = [] def get_tool_name(self) -> str: """ Return caller name for iso creation tool Implementation in specialized tool class :return: tool name :rtype: str """ raise NotImplementedError def init_iso_creation_parameters( self, custom_args: Optional[Dict[str, Union[str, bool]]] = None ) -> None: """ Create a set of standard parameters for the main loader Implementation in specialized tool class :param list custom_args: unused """ raise NotImplementedError def add_efi_loader_parameters( self, loader_file: str, custom_args: Optional[Dict[str, Union[str, bool]]] = None, ) -> None: """ Add ISO creation parameters to embed the EFI loader Implementation in specialized tool class """ raise NotImplementedError def create_iso( self, filename: str, hidden_files: Optional[List[str]] = None ) -> None: """ Create iso file Implementation in specialized tool class :param str filename: unused :param list hidden_files: unused """ raise NotImplementedError def list_iso(self, isofile: str) -> None: """ List contents of an ISO image :param str isofile: unused """ raise NotImplementedError def has_iso_hybrid_capability(self) -> bool: """ Indicate if the iso tool has the capability to embed a partition table into the iso such that it can be used as both; an iso and a disk Implementation in specialized tool class """ raise NotImplementedError @staticmethod def setup_media_loader_directory( lookup_path: str, media_path: str, boot_theme: Optional[str] ) -> None: loader_data = lookup_path + '/image/loader/' media_boot_path = os.sep.join( [media_path, Defaults.get_iso_boot_path(), 'loader'] ) Path.wipe(loader_data) Path.create(loader_data) grub_image_file_names = [ Defaults.get_iso_grub_loader(), Defaults.get_iso_grub_mbr(), ] loader_files = [] for grub_image_file_name in grub_image_file_names: grub_file = Defaults.get_grub_path( lookup_path, 'i386-pc/{0}'.format(grub_image_file_name), raise_on_error=False, ) if grub_file and os.path.exists(grub_file): loader_files.append(grub_file) log.debug('Copying loader files to {0}'.format(loader_data)) for loader_file in loader_files: log.debug('--> Copying {0}'.format(loader_file)) shutil.copy(loader_file, loader_data) bash_command = ' '.join( ['cp', lookup_path + '/boot/memtest*', loader_data + '/memtest'] ) Command.run(command=['bash', '-c', bash_command], raise_on_error=False) if boot_theme: theme_path = ''.join( [lookup_path, '/etc/bootsplash/themes/', boot_theme] ) if os.path.exists(theme_path + '/cdrom/gfxboot.cfg'): bash_command = ' '.join( ['cp', theme_path + '/cdrom/*', loader_data] ) Command.run(['bash', '-c', bash_command]) # don't move down one menu entry the first time a F-key is used Command.run( [ 'gfxboot', '--config-file', loader_data + '/gfxboot.cfg', '--change-config', 'install::autodown=0', ] ) if os.path.exists(theme_path + '/bootloader/message'): Command.run( ['cp', theme_path + '/bootloader/message', loader_data] ) Path.create(media_boot_path) data = DataSync(loader_data, media_boot_path) data.sync_data(options=['-a']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/iso_tools/iso.py0000644000000000000000000000364615273675667013655 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.runtime_config import RuntimeConfig class Iso: """ **Implements helper methods around the creation of ISO filesystems** """ def __init__(self, source_dir: str) -> None: """ Create an instance of Iso helpers :param str source_dir: Directory path for iso source dir, root of ISO """ self.source_dir = source_dir self.boot_path = Defaults.get_iso_boot_path() @staticmethod def set_media_tag(isofile: str) -> None: """ Include checksum tag in the ISO so it can be verified with the mediacheck program. :param str isofile: path to the ISO file """ media_tagger = RuntimeConfig().get_iso_media_tag_tool() if media_tagger == 'checkmedia': Command.run( [ 'tagmedia', '--digest', 'sha256', '--check', '--pad', '0', isofile, ] ) elif media_tagger == 'isomd5sum': Command.run(['implantisomd5', '--force', isofile]) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/iso_tools/xorriso.py0000644000000000000000000002057715273675667014572 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import Dict, List, Optional, Union # project from kiwi.iso_tools.base import IsoToolsBase from kiwi.path import Path from kiwi.command import Command from kiwi.exceptions import KiwiIsoToolError from kiwi.defaults import Defaults log = logging.getLogger('kiwi') class IsoToolsXorrIso(IsoToolsBase): """ **xorriso wrapper class** Implementation of Parameter API for iso creation tools using the libburnia project. Addressed here is the tool xorriso """ def has_iso_hybrid_capability(self) -> bool: """ Indicate if the iso tool has the capability to embed a partition table into the iso such that it can be used as both; an iso and a disk :return: True or False :rtype: bool """ return True def get_tool_name(self) -> str: """ Lookup xorriso in search path :raises KiwiIsoToolError: if xorriso tool is not found :return: xorriso tool path :rtype: str """ xorriso = Path.which('xorriso') if xorriso: return xorriso raise KiwiIsoToolError('xorriso tool not found') def init_iso_creation_parameters( self, custom_args: Optional[Dict[str, Union[str, bool]]] = None ) -> None: """ Create a set of standard parameters :param list custom_args: custom ISO meta data """ legacy_bios_mode = True if custom_args: application_id = custom_args.get( 'application_id' ) or custom_args.get('mbr_id') if application_id: self.iso_parameters += [ '-application_id', format(application_id), ] if 'publisher' in custom_args: self.iso_parameters += [ '-publisher', format(custom_args['publisher']), ] if 'preparer' in custom_args: self.iso_parameters += [ '-preparer_id', format(custom_args['preparer']), ] if 'volume_id' in custom_args: self.iso_parameters += [ '-volid', format(custom_args['volume_id']), ] if 'legacy_bios_mode' in custom_args: legacy_bios_mode = bool(custom_args['legacy_bios_mode']) catalog_file = self.boot_path + '/boot.catalog' self.iso_parameters += ['-joliet', 'on', '-padding', '0'] if Defaults.is_ppc64_arch(self.arch): self.iso_parameters += ['-compliance', 'untranslated_names'] else: self.iso_parameters += [ # https://lists.gnu.org/archive/html/bug-xorriso/2024-11/msg00012.html '-compliance', 'no_emul_toc', ] if Defaults.is_x86_arch(self.arch) and legacy_bios_mode: mbr_file = os.sep.join( [ self.source_dir, self.boot_path, 'loader', Defaults.get_iso_grub_mbr(), ] ) loader_file = os.sep.join( [self.boot_path, 'loader', Defaults.get_iso_grub_loader()] ) self.iso_loaders += [ '-boot_image', 'grub', 'bin_path={0}'.format(loader_file), ] if os.path.exists(mbr_file): self.iso_loaders += [ '-boot_image', 'grub', 'grub2_mbr={0}'.format(mbr_file), ] else: log.warning(f'No hybrid MBR file found: {mbr_file}: skipped') self.iso_loaders += ['-boot_image', 'grub', 'grub2_boot_info=on'] if Defaults.is_ppc64_arch(self.arch): self.iso_loaders += ['-boot_image', 'any', 'chrp_boot_part=on'] else: self.iso_loaders += [ '-boot_image', 'any', 'partition_offset=16', '-boot_image', 'any', 'cat_path={0}'.format(catalog_file), '-boot_image', 'any', 'cat_hidden=on', '-boot_image', 'any', 'boot_info_table=on', '-boot_image', 'any', 'platform_id=0x00', '-boot_image', 'any', 'emul_type=no_emulation', '-boot_image', 'any', 'load_size=2048', ] def add_efi_loader_parameters( self, loader_file: str, custom_args: Optional[Dict[str, Union[str, bool]]] = None, ) -> None: """ Add ISO creation parameters to embed the EFI loader In order to boot the ISO from EFI, the EFI binary is added as alternative loader to the ISO creation parameter list. The EFI binary must be included into a fat filesystem in order to become recognized by the firmware. For details about this file refer to _create_embedded_fat_efi_image() from bootloader/config/grub2.py """ if custom_args: efi_partition_table = custom_args.get('efi_partition_table', '') legacy_bios_mode = custom_args.get('legacy_bios_mode', False) gpt_hybrid_mbr = custom_args.get('gpt_hybrid_mbr', False) if efi_partition_table == 'gpt': self.iso_loaders += [ '-boot_image', 'any', 'appended_part_as=gpt', ] if gpt_hybrid_mbr and legacy_bios_mode: self.iso_loaders += [ '-boot_image', 'any', 'mbr_force_bootable=on', ] self.iso_loaders += [ '-append_partition', '2', '0xef', loader_file, '-boot_image', 'any', 'next', '-boot_image', 'any', 'efi_path=--interval:appended_partition_2:all::', '-boot_image', 'any', 'platform_id=0xef', '-boot_image', 'any', 'emul_type=no_emulation', ] def create_iso( self, filename: str, hidden_files: Optional[List[str]] = None ) -> None: """ Creates the iso file with the given filename using xorriso :param str filename: output filename :param list hidden_files: list of hidden files """ hidden_files_parameters = [] if hidden_files: for hidden_file in hidden_files: hidden_files_parameters += [ '--', '-find', hidden_file, '-exec', 'hide', 'on', ] Path.wipe(filename) Command.run( [self.get_tool_name()] + self.iso_parameters + [ '-outdev', filename, '-map', self.source_dir, '/', '-chmod', '0755', '/', '--', ] + self.iso_loaders + hidden_files_parameters ) report_call = Command.run( [ self.get_tool_name(), '-indev', filename, '-report_system_area', 'plain', ] ) log.debug(report_call.output) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/kiwi.py0000644000000000000000000000325415273675667012007 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import sys import logging # project from kiwi.app import App from kiwi.exceptions import KiwiError log = logging.getLogger('kiwi') def main() -> None: """ kiwi - main application entry point Initializes a global log object and handles all errors of the application. Every known error is inherited from KiwiError, everything else is passed down until the generic Exception which is handled as unexpected error including the python backtrace """ try: App() except KiwiError as e: # known exception, log information and exit log.error('%s: %s', type(e).__name__, format(e)) sys.exit(1) except KeyboardInterrupt: log.error('kiwi aborted by keyboard interrupt') sys.exit(1) except SystemExit as e: # user exception, program aborted by user sys.exit(e.code) except Exception: # exception we did no expect, show python backtrace log.error('Unexpected error:') raise ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/logger.py0000644000000000000000000002036215273675667012322 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Dict, Optional, List, TextIO import logging from kiwi.logger_socket import PlainTextSocketHandler import sys # project from kiwi.logger_color_formatter import ColorFormatter from kiwi.logger_filter import ( LoggerSchedulerFilter, InfoFilter, DebugFilter, ErrorFilter, WarningFilter, ) from kiwi.exceptions import KiwiLogFileSetupFailed, KiwiLogSocketSetupFailed class Logger(logging.Logger): """ **Extended logging facility based on Python logging** :param str name: name of the logger """ def __init__(self, name: str): logging.Logger.__init__(self, name) self.log_handlers: Dict = {} self.logfile: Optional[str] = None # log INFO to stdout self._add_stream_handler( 'info', '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', [InfoFilter(), LoggerSchedulerFilter()], ) # log WARNING messages to stdout self._add_stream_handler( 'warning', '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', [WarningFilter()], ) # log DEBUG messages to stdout self._add_stream_handler( 'debug', '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', [DebugFilter()], ) # log ERROR messages to stderr self._add_stream_handler( 'error', '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', [ErrorFilter()], sys.__stderr__, ) self.log_level = self.level self.log_flags: Dict[str, bool] = {} def getLogLevel(self) -> int: """ Return currently used log level :return: log level number :rtype: int """ return self.log_level def getLogFlags(self) -> Dict[str, bool]: """ Return logging flags :return: Dictionary with flags and their activation status :rtype: dict """ return self.log_flags def setLogLevel( self, level: int, except_for: List[str] = [], only_for: List[str] = [] ) -> None: """ Set custom log level for all console handlers :param int level: log level number :param list except_for: set log level to all handlers except for the given list :param list only_for: set log level to the given handlers only if both except_for and only_for handlers are specified, the except_for list will be ignored """ self.log_level = level if only_for: for handler_type in self.log_handlers: if handler_type in only_for: self.log_handlers[handler_type].setLevel(level) return for handler_type in self.log_handlers: if handler_type not in except_for: self.log_handlers[handler_type].setLevel(level) def setLogFlag(self, flag: str, value: bool = True) -> None: """ Set logging flag for further properties of the logging facility Available flags are: * run-scripts-in-screen :param str flag: name """ self.log_flags[flag] = value def set_color_format(self) -> None: """ Set color format for all console handlers """ for handler_type in self.log_handlers: message_format = None if handler_type == 'debug': message_format = ( '$LIGHTCOLOR[ %(levelname)-8s]: %(asctime)-8s | %(message)s' ) elif handler_type == 'warning' or handler_type == 'error': message_format = ( '$COLOR[ %(levelname)-8s]: %(asctime)-8s | %(message)s' ) if message_format: self.log_handlers[handler_type].setFormatter( ColorFormatter(message_format, '%H:%M:%S') ) def set_logfile(self, filename: str) -> None: """ Set logfile handler :param str filename: logfile file path """ try: if filename == 'stdout': # special case, log usual log file contents to stdout handler = logging.StreamHandler(sys.__stdout__) else: handler = logging.FileHandler( filename=filename, encoding='utf-8' ) self.logfile = filename handler.setFormatter( logging.Formatter( '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', '%H:%M:%S', ) ) handler.addFilter(LoggerSchedulerFilter()) self.addHandler(handler) self.log_handlers['file'] = handler except Exception as e: raise KiwiLogFileSetupFailed(f'{type(e).__name__}: {format(e)}') def set_log_socket(self, filename: str) -> None: """ Set log socket handler :param str filename: UDS socket file path. Note if there is no server listening on the socket the log handler setup will fail """ try: handler = PlainTextSocketHandler(filename, None) handler.makeSocket() handler.setFormatter( logging.Formatter( '[ %(levelname)-8s]: %(asctime)-8s | %(message)s', '%H:%M:%S', ) ) handler.addFilter(LoggerSchedulerFilter()) self.addHandler(handler) self.log_handlers['socket'] = handler except Exception as e: raise KiwiLogSocketSetupFailed( 'UDS socket: {0}:{1}: {2}'.format(filename, type(e).__name__, e) ) def get_logfile(self) -> Optional[str]: """ Return file path name of logfile :return: file path :rtype: str """ return self.logfile @staticmethod def progress( current: int, total: int, prefix: str, bar_length: int = 40 ) -> None: """ Custom progress log information. progress information is intentionally only logged to stdout and will bypass any handlers. We don't want this information to show up in the log file :param int current: current item :param int total: total number of items :param string prefix: prefix name :param int bar_length: length of progress bar """ try: percent = float(current) / total except Exception: # we don't want the progress to raise an exception # In case of any error e.g division by zero the current # way out is to skip the progress update return hashes = '#' * int(round(percent * bar_length)) spaces = ' ' * (bar_length - len(hashes)) sys.stdout.write( '\r{0}: [{1}] {2}%'.format( prefix, hashes + spaces, int(round(percent * 100)) ) ) if current == 100: sys.stdout.write('\n') sys.stdout.flush() def _add_stream_handler( self, handler_type: str, message_format: str, message_filter: List[logging.Filter], channel: Optional[TextIO] = sys.__stdout__, ) -> None: handler = logging.StreamHandler(channel) handler.setFormatter(logging.Formatter(message_format, '%H:%M:%S')) for rule in message_filter: handler.addFilter(rule) self.addHandler(handler) self.log_handlers[handler_type] = handler ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/logger_color_formatter.py0000644000000000000000000000731215273675667015603 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging class ColorMessage: """ **Implements color messages for Python logging facility** Has to implement the format_message method to serve as message formatter """ def __init__(self) -> None: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = list(range(8)) self.color = { 'BLACK': BLACK, 'WARNING': YELLOW, 'INFO': WHITE, 'DEBUG': WHITE, 'CRITICAL': YELLOW, 'ERROR': RED, 'RED': RED, 'GREEN': GREEN, 'YELLOW': YELLOW, 'BLUE': BLUE, 'MAGENTA': MAGENTA, 'CYAN': CYAN, 'WHITE': WHITE, } self.esc = { 'reset': '\033[0m', 'color': '\033[3;%dm', 'color_light': '\033[2;%dm', 'bold': '\033[1m', } def format_message(self, level: str, message: str) -> str: """ Message formatter with support for embedded color sequences The Message is allowed to contain the following color metadata: * $RESET, reset to no color mode * $BOLD, bold * $COLOR, color the following text * $LIGHTCOLOR, light color the following text The color of the message depends on the level and is defined in the ColorMessage constructor :param str level: color level name :param str message: text :return: color message with escape sequences :rtype: str """ message = ( message.replace('$RESET', self.esc['reset']) .replace('$BOLD', self.esc['bold']) .replace('$COLOR', self.esc['color'] % (30 + self.color[level])) .replace( '$LIGHTCOLOR', self.esc['color_light'] % (30 + self.color[level]), ) ) for color_name, color_id in list(self.color.items()): message = ( message.replace( '$' + color_name, self.esc['color'] % (color_id + 30) ) .replace( '$BG' + color_name, self.esc['color'] % (color_id + 40) ) .replace( '$BG-' + color_name, self.esc['color'] % (color_id + 40) ) ) return message + self.esc['reset'] class ColorFormatter(logging.Formatter): """ **Extended standard logging Formatter** Extended format supporting text with color metadata Example: .. code:: python ColorFormatter(message_format, '%H:%M:%S') """ def format(self, record: logging.LogRecord) -> str: """ Creates a logging Formatter with support for color messages :param tuple record: logging message record :return: result from format_message :rtype: str """ color = ColorMessage() levelname = record.levelname message = logging.Formatter.format(self, record) return color.format_message(levelname, message) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/logger_filter.py0000644000000000000000000000612215273675667013665 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging class LoggerSchedulerFilter(logging.Filter): """ **Extended standard logging Filter** """ def filter(self, record: logging.LogRecord) -> bool: """ Messages from apscheduler scheduler instances are filtered out They conflict with console progress information :param tuple record: logging message record :return: True|False :rtype: bool """ ignorables = ['apscheduler.scheduler', 'apscheduler.executors.default'] return bool(record.name not in ignorables) class InfoFilter(logging.Filter): """ **Extended standard logging Filter** """ def filter(self, record: logging.LogRecord) -> bool: """ Only messages with record level INFO can pass for messages with another level an extra handler is used :param tuple record: logging message record :return: True|False :rtype: bool """ return True if record.levelno == logging.INFO else False class DebugFilter(logging.Filter): """ **Extended standard debug logging Filter** """ def filter(self, record: logging.LogRecord) -> bool: """ Only messages with record level DEBUG can pass for messages with another level an extra handler is used :param tuple record: logging message record :return: True|False :rtype: bool """ return True if record.levelno == logging.DEBUG else False class ErrorFilter(logging.Filter): """ **Extended standard error logging Filter** """ def filter(self, record: logging.LogRecord) -> bool: """ Only messages with record level DEBUG can pass for messages with another level an extra handler is used :param tuple record: logging message record :return: True|False :rtype: bool """ return True if record.levelno == logging.ERROR else False class WarningFilter(logging.Filter): """ **Extended standard warning logging Filter** """ def filter(self, record: logging.LogRecord) -> bool: """ Only messages with record level WARNING can pass for messages with another level an extra handler is used :param tuple record: logging message record :return: True|False :rtype: bool """ return True if record.levelno == logging.WARNING else False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/logger_socket.py0000644000000000000000000000366315273675667013677 0ustar00# Copyright (c) 2022 Marcus Schäfer. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging.handlers class PlainTextSocketHandler(logging.handlers.SocketHandler): def makePickle(self, record: logging.LogRecord) -> bytes: """ Custom makePickle method which actually does not pickle the messages into the pickle binary format but just sends the log message as plain text. A simple server listening could then look like the following example .. code:: python sock_file = '/tmp/log_socket' buffer = 1024 if os.path.exists(sock_file): os.unlink(sock_file) sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) sock.bind(sock_file) sock.listen(1) while True: connection, client_address = sock.accept() try: while True: data = connection.recv(buffer) if not data: break print(data.decode()) finally: connection.close() """ # the formatter is always set by Logger.set_log_socket assert self.formatter message = self.formatter.format(record) return message.encode() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7460392 kiwi-11.1.8/kiwi/markup/__init__.py0000644000000000000000000000350615273675667014102 0ustar00# Copyright (c) 2020 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from abc import ABCMeta, abstractmethod import logging # project from kiwi.markup.base import MarkupBase from kiwi.exceptions import KiwiAnyMarkupPluginError log = logging.getLogger('kiwi') class Markup(metaclass=ABCMeta): """ **Markup factory** :param str description: path to description file """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new(description: str, name: str = 'any') -> MarkupBase: # noqa: E252 try: markup = Markup._load_markup_by_name(name, description) log.info('Support for multiple markup descriptions available') except KiwiAnyMarkupPluginError: markup = Markup._load_markup_by_name('xml', description) log.info('Support for XML markup available') return markup @staticmethod def _load_markup_by_name(name: str, description: str) -> MarkupBase: name_map = {'any': 'Any', 'xml': 'XML'} markup = importlib.import_module('kiwi.markup.{0}'.format(name)) return markup.__dict__['Markup{0}'.format(name_map[name])](description) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746392 kiwi-11.1.8/kiwi/markup/any.py0000644000000000000000000001013215273675667013123 0ustar00# Copyright (c) 2020 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import Any import importlib import importlib.util # project from kiwi.utils.temporary import Temporary from kiwi.markup.base import MarkupBase from kiwi.exceptions import KiwiDescriptionInvalid, KiwiAnyMarkupPluginError class MarkupAny(MarkupBase): """ **Implements markup handling for XML, YAML, JSON and INI** """ def post_init(self) -> None: """ Convert given description file into XML The anymarkup module supports auto detection of the given input format and can convert YAML, JSON and INI to XML """ try: anymarkup_mod = 'anymarkup' if importlib.util.find_spec('anymarkup_core'): anymarkup_mod = 'anymarkup_core' self.anymarkup: Any = importlib.import_module(anymarkup_mod) except Exception as issue: raise KiwiAnyMarkupPluginError(issue) try: self.description_markup_processed = Temporary().new_file() markup = self.anymarkup.parse_file( self.description, force_types=None ) self.anymarkup.serialize_file( markup, self.description_markup_processed.name, format='xml' ) except Exception as issue: raise KiwiDescriptionInvalid(issue) def get_xml_description(self) -> str: """ Return XML description file name :return: file path name :rtype: str """ return self.apply_xslt_stylesheets( self.description_markup_processed.name ) def get_toml_description(self) -> str: """ Return TOML description file name :return: file path name :rtype: str """ xml_description_xslt_transformed = self.apply_xslt_stylesheets( self.description_markup_processed.name ) markup = self.anymarkup.parse_file( xml_description_xslt_transformed, force_types=None ) self.anymarkup.serialize_file( markup, xml_description_xslt_transformed, format='toml' ) return xml_description_xslt_transformed def get_yaml_description(self) -> str: """ Return YAML description file name :return: file path name :rtype: str """ xml_description_xslt_transformed = self.apply_xslt_stylesheets( self.description_markup_processed.name ) markup = self.anymarkup.parse_file( xml_description_xslt_transformed, force_types=None ) # The translation to yaml runs from a translation to json first # This is done because a direct translation from xml to yaml # causes the ordered mapping to be included. See # http://yaml.org/type/omap.html. In the context of kiwi this # ordering information is however not needed. The order of # sections and attributes doesn't play a role. In json no # such ordering concept exists and this is way to convert to # yaml without the !!omap definitions. self.anymarkup.serialize_file( markup, xml_description_xslt_transformed, format='json' ) markup = self.anymarkup.parse_file( xml_description_xslt_transformed, force_types=None ) self.anymarkup.serialize_file( markup, xml_description_xslt_transformed, format='yaml' ) return xml_description_xslt_transformed ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746392 kiwi-11.1.8/kiwi/markup/base.py0000644000000000000000000001073715273675667013261 0ustar00# Copyright (c) 2020 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from typing import Any, Optional from lxml import etree from urllib.parse import urlparse # project from kiwi.utils.temporary import Temporary from kiwi.defaults import Defaults from kiwi.exceptions import ( KiwiConfigFileFormatNotSupported, KiwiDescriptionInvalid, KiwiIncludFileNotFoundError, ) class MarkupBase: """ **Implements base class for Markup interface** Attributes :param str description: description path """ def __init__(self, description: str): self.description = description self.post_init() def post_init(self) -> None: """ Post initialization method """ pass def apply_xslt_stylesheets(self, description: str) -> str: """ Apply XSLT style sheet rules to an xml file The result of the XSLT processing is stored in a named temporary file and returned to the caller :param str description: path to an XML description file """ # Parse the provided description, raising the appropriate # exception if parsing fails. try: parsed_description = etree.parse(description) except etree.XMLSyntaxError: raise KiwiConfigFileFormatNotSupported( 'Configuration file could not be parsed. ' 'In case your configuration file is XML it most likely ' 'contains a syntax error. For other formats the ' 'Python anymarkup module is required.' ) xslt_transform_parser = etree.XMLParser() xslt_transform_parser.resolvers.add( FileResolver(os.path.dirname(self.description)) ) xslt_transform = etree.XSLT( etree.parse( Defaults.get_xsl_stylesheet_file(), xslt_transform_parser ) ) self.description_xslt_processed = Temporary( prefix='kiwi_xslt-' ).new_file() try: with open(self.description_xslt_processed.name, "wb") as xsltout: xsltout.write( etree.tostring( xslt_transform(parsed_description), pretty_print=True ) ) except etree.XMLSyntaxError as issue: raise KiwiDescriptionInvalid(issue) except etree.XSLTApplyError as issue: raise KiwiDescriptionInvalid(issue) return self.description_xslt_processed.name def get_xml_description(self) -> str: """ Return XML description file name Implementation in specialized Markup class """ raise NotImplementedError def get_yaml_description(self) -> str: """ Return YAML description file name Implementation in specialized Markup class """ raise NotImplementedError def get_toml_description(self) -> str: """ Return TOML description file name Implementation in specialized Markup class """ raise NotImplementedError class FileResolver(etree.Resolver): def __init__(self, description_dir: str) -> None: self.description_dir = description_dir def resolve(self, url: str, pubid: Optional[str], context: Any) -> Any: if url.startswith('this://'): url = url.replace('this://', '') url = 'dir://{0}'.format( os.path.realpath(os.path.join(self.description_dir, url)) ) uri = urlparse(url) if uri.path and uri.netloc: url = ''.join([uri.netloc, uri.path]) elif uri.path: url = uri.path if os.path.exists(url): return self.resolve_filename(url, context) else: raise KiwiIncludFileNotFoundError( f'include reference {url!r} does not exist' ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746392 kiwi-11.1.8/kiwi/markup/xml.py0000644000000000000000000000316415273675667013143 0ustar00# Copyright (c) 2020 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # # project from kiwi.markup.base import MarkupBase from kiwi.exceptions import KiwiMarkupConversionError class MarkupXML(MarkupBase): """ **Implements plain XML markup handling** """ def get_xml_description(self) -> str: """ Return XML description file name :return: file path name :rtype: string """ return self.apply_xslt_stylesheets(self.description) def get_yaml_description(self) -> str: """ Conversion into YAML format not supported by base XML markup """ raise KiwiMarkupConversionError( 'Conversion to YAML not supported, install anymarkup-core module' ) def get_toml_description(self) -> str: """ Conversion into TOML format not supported by base XML markup """ raise KiwiMarkupConversionError( 'Conversion to TOML not supported, install anymarkup-core module' ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746392 kiwi-11.1.8/kiwi/mount_manager.py0000644000000000000000000001771215273675667013704 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import time import logging from textwrap import dedent from types import TracebackType from typing import List, Dict, Optional, Type # project from kiwi.path import Path from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.exceptions import KiwiCommandError, KiwiUmountBusyError from kiwi.utils.block import BlockID log = logging.getLogger('kiwi') class MountManager: """ **Implements methods for mounting, umounting and mount checking** The caller is responsible for unmounting the device if the MountManager is used as is. The class also supports to be used as a context manager, where the device is unmounted once the context manager's with block is left * :param string device: device node name * :param string mountpoint: mountpoint directory name * :param dict attributes: optional attributes to store """ def __init__( self, device: str, mountpoint: str = '', attributes: Dict[str, str] = {} ): self.device = device self.attributes = attributes if not mountpoint: self.mountpoint_tempdir = Temporary( prefix='kiwi_mount_manager.' ).new_dir() self.mountpoint = self.mountpoint_tempdir.name else: Path.create(mountpoint) self.mountpoint = mountpoint def __enter__(self) -> "MountManager": return self def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self.umount() def get_attributes(self) -> Dict[str, str]: """ Return attributes dict for this mount manager """ return self.attributes def bind_mount(self) -> None: """ Bind mount the device to the mountpoint """ if self.device and not self.is_mounted(): Command.run(['mount', '-n', '--bind', self.device, self.mountpoint]) def overlay_mount(self, lower: str) -> None: self.device = 'overlay' self.lower = lower self.upper = f'{self.mountpoint}_cow' self.work = f'{self.mountpoint}_work' Path.create(self.upper) Path.create(self.work) if not self.is_mounted(): Command.run( [ 'mount', '-t', 'overlay', self.device, self.mountpoint, '-o', 'lowerdir={0},upperdir={1},workdir={2}'.format( lower, self.upper, self.work ), ] ) def tmpfs_mount(self) -> None: """ tmpfs mount the device to the mountpoint """ if not self.is_mounted(): Command.run(['mount', '-t', 'tmpfs', 'tmpfs', self.mountpoint]) def mount(self, options: List[str] = []) -> None: """ Standard mount the device to the mountpoint :param list options: mount options """ if self.device and not self.is_mounted(): option_list = [] if options: option_list = ['-o'] + options type_list = [] filesystem = BlockID(self.device).get_filesystem() if filesystem: type_list = ['-t', filesystem] Command.run( ['mount'] + type_list + option_list + [self.device, self.mountpoint] ) def umount_lazy(self) -> None: """ Umount by the mountpoint directory in lazy mode Release the mount in any case, however the time when the mounted resource is released by the kernel depends on when the resource enters the non busy state """ if self.is_mounted(): Command.run(['umount', '-l', self.mountpoint]) def umount(self, raise_on_busy: bool = True) -> bool: """ Umount by the mountpoint directory Wait up to 10sec trying to umount. If the resource stays busy the call will raise an exception unless raise_on_busy is set to False. In case the umount failed and raise_on_busy is set to False, the method returns False to indicate the error condition. :return: True or False :rtype: bool """ if self.is_mounted(): umounted_successfully = False for busy in range(0, 5): try: Command.run(['umount', self.mountpoint]) umounted_successfully = True break except KiwiCommandError as err: log.warning( f'{busy} umount of {self.mountpoint} failed with: {err}' ) time.sleep(1) if not umounted_successfully: try: Command.run(['umount', '--lazy', self.mountpoint]) umounted_successfully = True except KiwiCommandError as err: log.error(f'umount of {self.mountpoint} failed with: {err}') if not umounted_successfully: if raise_on_busy: lsof = Path.which('lsof', access_mode=os.X_OK) if lsof: open_files = Command.run( [lsof, '+c', '0', self.mountpoint], raise_on_error=False, ) open_files_info = 'Open files status:{0}{1}'.format( os.linesep, open_files.output ) else: open_files_info = 'For further details install: lsof' message = dedent( '''\n Failed to umount: {0}. Your build host system is in an inconsistent state. The cleanup of the created resource was not possible because it is still busy. This resource and all nested resources stays active on your host and needs a manual cleanup. Please do not use the intermediate state of the image files created so far. There is no guarantee that the produced results are valid. {1} ''' ) raise KiwiUmountBusyError( message.format(self.mountpoint, open_files_info) ) else: log.warning( '{0} still busy at {1}'.format( self.mountpoint, type(self).__name__ ) ) # skip removing the mountpoint directory return False return True def is_mounted(self) -> bool: """ Check if mounted :return: True or False :rtype: bool """ mountpoint_call = Command.run( command=['mountpoint', '-q', self.mountpoint], raise_on_error=False ) return mountpoint_call.returncode == 0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746392 kiwi-11.1.8/kiwi/oci_tools/__init__.py0000644000000000000000000000301115273675667014564 0ustar00# Copyright (c) 2018 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from abc import ABCMeta, abstractmethod from kiwi.oci_tools.base import OCIBase # project from kiwi.runtime_config import RuntimeConfig from kiwi.exceptions import KiwiOCIArchiveToolError class OCI(metaclass=ABCMeta): """ **OCI Factory** """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new() -> OCIBase: tool_name = RuntimeConfig().get_oci_archive_tool() if tool_name == "umoci": from kiwi.oci_tools.umoci import OCIUmoci return OCIUmoci() elif tool_name == "buildah": from kiwi.oci_tools.buildah import OCIBuildah return OCIBuildah() else: raise KiwiOCIArchiveToolError( 'No support for {0} tool available'.format(tool_name) ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746591 kiwi-11.1.8/kiwi/oci_tools/base.py0000644000000000000000000001445415273675667013754 0ustar00# Copyright (c) 2018 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from datetime import datetime, timezone from types import TracebackType from typing import Any, List, Mapping, Optional, Type # project from kiwi.path import Path from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.utils.sync import DataSync class OCIBase: """ **Base Class for Open Container Interface operations** An initiative to formulate industry standards around container formats and runtime is available at https://www.opencontainers.org Different tools to implement the specifications had been created. The purpose of this class and its sub-classes is to provide a common interface in kiwi to allow using all tools such that the container support in kiwi covers every linux distribution no matter what tooling was preferred """ def __init__(self) -> None: self.oci_root_dir: Optional[str] = None sde = os.environ.get('SOURCE_DATE_EPOCH') if sde: self.creation_date = datetime.fromtimestamp( int(sde), tz=timezone.utc ).strftime('%Y-%m-%dT%H:%M:%S+00:00') else: self.creation_date = datetime.utcnow().strftime( '%Y-%m-%dT%H:%M:%S+00:00' ) self.post_init() def __enter__(self) -> 'OCIBase': return self def post_init(self) -> None: """ Post initialization method Implementation in specialized OCI tool class if required """ pass def import_container_image(self, container_image_ref: str) -> None: """ Imports container image reference to a working space Implementation in specialized tool class :param string container_image_ref: image reference to import """ raise NotImplementedError def export_container_image( self, filename: str, transport: str, image_ref: str, additional_refs: Optional[List[str]] = None, ) -> None: """ Exports the working container to a container image archive Implementation in specialized tool class :param str filename: The resulting filename :param str transport: The archive format :param str image_ref: Image reference of the exported image :param list additional_names: List of additional references """ raise NotImplementedError def init_container(self) -> None: """ Initialize a new container Implementation in specialized tool class """ raise NotImplementedError def unpack(self) -> None: """ Unpack current container root data Implementation in specialized tool class """ raise NotImplementedError def sync_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the image root with the rootfs of the container Implementation in specialized tool class :param string root_dir: root directory of the prepare step :param list exclude_list: list of paths to exclude """ raise NotImplementedError def import_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the container rootfs with the root tree of the build Implementation in specialized tool class :param string root_dir: root directory used in prepare step :param list exclude_list: list of paths to exclude """ raise NotImplementedError def repack(self, oci_config: Mapping[str, Any]) -> None: """ Pack root data directory into container image Implementation in specialized tool class :param list oci_config: meta data list """ raise NotImplementedError def set_config(self, oci_config: Mapping[str, Any]) -> None: """ Set list of meta data information such as entry_point, maintainer, etc... to the container. The validation of the list content is handled by the underlaying toolkit Implementation in specialized tool class :param list oci_config: meta data list """ raise NotImplementedError def post_process(self) -> None: """ Performs latest steps after the container layers is added and configured. Implementation in specialized tool class """ raise NotImplementedError @staticmethod def _sync_data( origin: str, destination: str, exclude_list: Optional[List[str]] = None, options: Optional[List[str]] = None, ) -> None: """ Synchronizes the origin and destination paths to be equivalent :param string origin: the source path :param string destination: the destination path """ sync = DataSync(origin, destination) sync.sync_data(options=options, exclude=exclude_list) @staticmethod def _skopeo_provides_tmpdir_option() -> bool: """ Check if skopeo provides the --tmpdir option Beginning with version >= 0.2 skopeo provides it. """ tool = 'skopeo' expected_version = (0, 2, 0) if Path.which(filename=tool, access_mode=os.X_OK): if CommandCapabilities.check_version( tool, expected_version, raise_on_error=False ): return True return False # pragma: no cover def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: pass ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746591 kiwi-11.1.8/kiwi/oci_tools/buildah.py0000644000000000000000000003146215273675667014450 0ustar00# Copyright (c) 2019 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi is free software: you can redistribute it and/or modify # it under the terms owf the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging import os import random import string from types import TracebackType from typing import Any, List, Mapping, Optional, Type # project from kiwi.oci_tools.base import OCIBase from kiwi.command import Command from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.exceptions import KiwiBuildahError log = logging.getLogger('kiwi') class OCIBuildah(OCIBase): """ **Open Container Operations using buildah** """ def post_init(self) -> None: """ Initializes some default parameters """ self.working_image: Optional[str] = None self.imported_image: Optional[str] = None self.working_container: Optional[str] = None def __enter__(self) -> 'OCIBuildah': return self def import_container_image(self, container_image_ref: str) -> None: """ Imports container image reference to the OCI containers storage. :param str container_image_ref: container image reference """ if not self.imported_image: self.imported_image = 'kiwi-image-{0}:{1}'.format( self._random_string_generator(), Defaults.get_container_base_image_tag(), ) else: raise KiwiBuildahError( "Image already imported, called: '{0}'".format( self.imported_image ) ) # We are making use of skopeo instead of only calling 'buildah from' # because we want to control the image name loaded into the containers # storage. This way we are certain to not leave any left over after the # build. Command.run( [ 'skopeo', 'copy', container_image_ref, 'containers-storage:{0}'.format(self.imported_image), ] + ( ['--tmpdir', Defaults.get_temp_location()] if self._skopeo_provides_tmpdir_option() else [] ) ) if not self.working_container: self.working_container = 'kiwi-container-{0}'.format( self._random_string_generator() ) else: raise KiwiBuildahError( "Container already initated, called: '{0}'".format( self.working_container ) ) Command.run( [ 'buildah', 'from', '--name', self.working_container, 'containers-storage:{0}'.format(self.imported_image), ] ) def export_container_image( self, filename: str, transport: str, image_ref: str, additional_names: Optional[List[str]] = None, ) -> None: """ Exports the working container to a container image archive :param str filename: The resulting filename :param str transport: The archive format :param str image_ref: Reference of the exported image :param list additional_names: List of additional references """ extra_tags_opt = [] if additional_names: for ref in additional_names: extra_tags_opt.extend(['--additional-tag', ref]) # make sure the target tar file does not exist # skopeo doesn't support force overwrite Path.wipe(filename) if self.working_image: export_image = self.working_image elif self.imported_image: export_image = self.imported_image else: raise KiwiBuildahError("There is no image to export defined") # we are using 'skopeo copy' to export images instead of 'buildah push' # because buildah does not support multiple tags Command.run( [ 'skopeo', 'copy', 'containers-storage:{0}'.format(export_image), '{0}:{1}:{2}'.format(transport, filename, image_ref), ] + extra_tags_opt + ( ['--tmpdir', Defaults.get_temp_location()] if self._skopeo_provides_tmpdir_option() else [] ) ) def init_container(self) -> None: """ Initialize a new container in OCI containers storage """ if not self.working_container: self.working_container = 'kiwi-container-{0}'.format( self._random_string_generator() ) else: raise KiwiBuildahError( "Image already imported or initated at '{0}' container".format( self.working_container ) ) Command.run( ['buildah', 'from', '--name', self.working_container, 'scratch'] ) def unpack(self) -> None: """ Mounts current container root data to a directory """ assert self.working_container cmd = Command.run(['buildah', 'mount', self.working_container]) self.oci_root_dir = cmd.output.rstrip() def sync_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the image root with the rootfs of the container :param string root_dir: root directory of the prepare step :param list exclude_list: list of paths to exclude """ assert self.oci_root_dir self._sync_data( ''.join([root_dir, os.sep]), self.oci_root_dir, exclude_list=exclude_list, options=Defaults.get_sync_options() + [ '--filter', '-x! user.*', '--filter', '-x! security.ima*', '--filter', '-x! security.capability*', '--delete', ], ) def import_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the container rootfs with the root tree of the build :param string root_dir: root directory used in prepare step :param list exclude_list: list of paths to exclude """ assert self.oci_root_dir self._sync_data( os.sep.join([self.oci_root_dir, '']), root_dir, exclude_list=exclude_list, options=Defaults.get_sync_options(), ) def repack(self, oci_config: Mapping[str, Any]) -> None: """ Pack root data directory into container image :param list oci_config: unused parameter """ assert self.working_container Command.run(['buildah', 'umount', self.working_container]) def set_config(self, oci_config: Mapping[str, Any]) -> None: """ Set list of meta data information such as entry_point, maintainer, etc... to the container. :param list oci_config: meta data list :param bool base_image: True|False """ assert self.working_container config_args = self._process_oci_config_to_arguments(oci_config) Command.run( ['buildah', 'config'] + config_args + [self.working_container] ) def post_process(self) -> None: """ Commits the OCI container into an OCI image """ if not self.working_image and self.working_container: self.working_image = 'kiwi-image-{0}:{1}'.format( self._random_string_generator(), 'tag-{0}'.format(self._random_string_generator()), ) else: raise KiwiBuildahError( "No container to commit or container already committed" ) output = Command.run( [ 'buildah', 'commit', '--rm', '--format', 'oci', self.working_container, self.working_image, ] ) self.working_image = output.output.rstrip() self.working_container = None @classmethod def _process_oci_config_to_arguments( self, oci_config: Mapping[str, Any] ) -> List[str]: """ Process the oci configuration dictionary into a list of arguments for the 'buildah config' command :param list oci_config: meta data list :return: List of buildah config arguments :rtype: list """ arguments = [] if 'maintainer' in oci_config: arguments.append('--author={0}'.format(oci_config['maintainer'])) if 'user' in oci_config: arguments.append('--user={0}'.format(oci_config['user'])) if 'workingdir' in oci_config: arguments.append( '--workingdir={0}'.format(oci_config['workingdir']) ) if 'entry_command' in oci_config: arguments.append( '--entrypoint=[{0}]'.format( ','.join( ['"{0}"'.format(x) for x in oci_config['entry_command']] ) ) ) if 'entry_subcommand' in oci_config: arguments.append( '--cmd={0}'.format(' '.join(oci_config['entry_subcommand'])) ) if 'volumes' in oci_config: for vol in oci_config['volumes']: arguments.append('--volume={0}'.format(vol)) if 'stopsignal' in oci_config: arguments.append( '--stop-signal={0}'.format(oci_config['stopsignal']) ) if 'expose_ports' in oci_config: for port in oci_config['expose_ports']: arguments.append('--port={0}'.format(port)) if 'environment' in oci_config: for name in sorted(oci_config['environment']): arguments.append( '--env={0}={1}'.format( name, oci_config['environment'][name] ) ) if 'labels' in oci_config: for name in sorted(oci_config['labels']): arguments.append( '--label={0}={1}'.format(name, oci_config['labels'][name]) ) if 'history' in oci_config: if 'comment' in oci_config['history']: arguments.append( '--history-comment={0}'.format( oci_config['history']['comment'] ) ) if 'created_by' in oci_config['history']: arguments.append( '--created-by={0}'.format( oci_config['history']['created_by'] ) ) if 'author' in oci_config['history']: log.warning('Author field in history is ignored using buildah') return arguments @classmethod def _random_string_generator( cls, chars_num: int = 6, allchars: str = string.ascii_lowercase + string.digits, ) -> str: """ Creates a random string with the given length of characters choosen randomly from a given list of possible characters. Buildah makes use of the hosts configured containers storage of OCI images. This method is used to avoid name collisions with any previous image or container present in the host. :param int chars_num: Lenght of the generated random string :param list allchars: List of possible characters :return: generated random string :rtype: string """ return "".join(random.choice(allchars) for x in range(chars_num)) def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: if self.working_container: Command.run(['buildah', 'umount', self.working_container]) Command.run(['buildah', 'rm', self.working_container]) if self.working_image: Command.run(['buildah', 'rmi', self.working_image]) if self.imported_image: Command.run(['buildah', 'rmi', self.imported_image]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746591 kiwi-11.1.8/kiwi/oci_tools/umoci.py0000644000000000000000000002450115273675667014150 0ustar00# Copyright (c) 2018 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from typing import Any, List, Mapping, Optional # project from kiwi.utils.temporary import Temporary from kiwi.oci_tools.base import OCIBase from kiwi.command import Command from kiwi.path import Path from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.defaults import Defaults class OCIUmoci(OCIBase): """ **Open Container Operations using umoci** """ def post_init(self) -> None: """ Initializes some umoci parameters and options """ self.oci_dir_tempfile = Temporary(prefix='kiwi_oci_dir.').new_dir() self.oci_dir = self.oci_dir_tempfile.name self.container_dir = os.sep.join([self.oci_dir, 'oci_layout']) self.working_image = '{0}:{1}'.format( self.container_dir, Defaults.get_container_base_image_tag() ) if CommandCapabilities.has_option_in_help( 'umoci', '--no-history', ['config', '--help'], raise_on_error=False ): self.no_history_flag = ['--no-history'] else: self.no_history_flag = [] def import_container_image(self, container_image_ref: str) -> None: """ Imports container image reference to an OCI layout :param str container_image_ref: container image reference """ Command.run( [ 'skopeo', 'copy', container_image_ref, 'oci:{0}:{1}'.format( self.container_dir, Defaults.get_container_base_image_tag() ), ] + ( ['--tmpdir', Defaults.get_temp_location()] if self._skopeo_provides_tmpdir_option() else [] ) ) def export_container_image( self, filename: str, transport: str, image_ref: str, additional_names: Optional[List[str]] = None, ) -> None: """ Exports the working container to a container image archive :param str filename: The resulting filename :param str transport: The archive format :param str image_name: Name of the exported image :param str image_tag: Tag of the exported image :param list additional_names: List of additional references """ extra_tags_opt = [] if additional_names: for ref in additional_names: extra_tags_opt.extend(['--additional-tag', ref]) # make sure the target tar file does not exist # skopeo doesn't support force overwrite Path.wipe(filename) Command.run( [ 'skopeo', 'copy', 'oci:{0}'.format(self.working_image), '{0}:{1}:{2}'.format(transport, filename, image_ref), ] + extra_tags_opt + ( ['--tmpdir', Defaults.get_temp_location()] if self._skopeo_provides_tmpdir_option() else [] ) ) def init_container(self) -> None: """ Initialize a new container layout :param bool base_image: True|False """ Command.run(['umoci', 'init', '--layout', self.container_dir]) Command.run(['umoci', 'new', '--image', self.working_image]) def unpack(self) -> None: """ Unpack current container root data """ self.oci_root_dir_tempdir = Temporary( prefix='kiwi_oci_root_dir.' ).new_dir() self.oci_root_dir = self.oci_root_dir_tempdir.name Command.run( [ 'umoci', 'unpack', '--image', self.working_image, self.oci_root_dir, ] ) def sync_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the image root with the rootfs of the container :param string root_dir: root directory of the prepare step :param list exclude_list: list of paths to exclude """ assert self.oci_root_dir self._sync_data( ''.join([root_dir, os.sep]), os.sep.join([self.oci_root_dir, 'rootfs']), exclude_list=exclude_list, options=Defaults.get_sync_options() + ['--delete'], ) def import_rootfs( self, root_dir: str, exclude_list: Optional[List[str]] = None ) -> None: """ Synchronizes the container rootfs with the root tree of the build :param string root_dir: root directory used in prepare step :param list exclude_list: list of paths to exclude """ assert self.oci_root_dir self._sync_data( os.sep.join([self.oci_root_dir, 'rootfs', '']), root_dir, exclude_list=exclude_list, options=Defaults.get_sync_options(), ) def repack(self, oci_config: Mapping[str, Any]) -> None: """ Pack root data directory into container image :param list oci_config: meta data list """ assert self.oci_root_dir history_flags = self._process_oci_history_to_arguments(oci_config) history_flags.extend(['--history.created', self.creation_date]) Command.run( ['umoci', 'repack'] + history_flags + ['--image', self.working_image, self.oci_root_dir] ) def set_config(self, oci_config: Mapping[str, Any]) -> None: """ Set list of meta data information such as entry_point, maintainer, etc... to the container. :param list oci_config: meta data list """ config_args = self._process_oci_config_to_arguments(oci_config) Command.run( ['umoci', 'config'] + config_args + self.no_history_flag + [ '--image', self.working_image, '--tag', oci_config['container_tag'], '--created', self.creation_date, ] ) self.working_image = '{0}:{1}'.format( self.container_dir, oci_config['container_tag'] ) @staticmethod def _process_oci_config_to_arguments( oci_config: Mapping[str, Any], ) -> List[str]: """ Process the oci configuration dictionary into a list of arguments for the 'umoci config' command :param list oci_config: meta data list :return: List of umoci config arguments :rtype: list """ arguments = [] if 'maintainer' in oci_config: arguments.append('--author={0}'.format(oci_config['maintainer'])) if 'user' in oci_config: arguments.append('--config.user={0}'.format(oci_config['user'])) if 'workingdir' in oci_config: arguments.append( '--config.workingdir={0}'.format(oci_config['workingdir']) ) if 'entry_command' in oci_config: if len(oci_config['entry_command']) == 0: arguments.append('--clear=config.entrypoint') else: for arg in oci_config['entry_command']: arguments.append('--config.entrypoint={0}'.format(arg)) if 'entry_subcommand' in oci_config: if len(oci_config['entry_subcommand']) == 0: arguments.append('--clear=config.cmd') else: for arg in oci_config['entry_subcommand']: arguments.append('--config.cmd={0}'.format(arg)) if 'volumes' in oci_config: for vol in oci_config['volumes']: arguments.append('--config.volume={0}'.format(vol)) if 'stopsignal' in oci_config: arguments.append( '--config.stopsignal={0}'.format(oci_config['stopsignal']) ) if 'expose_ports' in oci_config: for port in oci_config['expose_ports']: arguments.append('--config.exposedports={0}'.format(port)) if 'environment' in oci_config: for name in sorted(oci_config['environment']): arguments.append( '--config.env={0}={1}'.format( name, oci_config['environment'][name] ) ) if 'labels' in oci_config: for name in sorted(oci_config['labels']): arguments.append( '--config.label={0}={1}'.format( name, oci_config['labels'][name] ) ) return arguments @staticmethod def _process_oci_history_to_arguments( oci_config: Mapping[str, Any], ) -> List[str]: history_args = [] if 'history' in oci_config: if 'comment' in oci_config['history']: history_args.append( '--history.comment={0}'.format( oci_config['history']['comment'] ) ) if 'created_by' in oci_config['history']: history_args.append( '--history.created_by={0}'.format( oci_config['history']['created_by'] ) ) if 'author' in oci_config['history']: history_args.append( '--history.author={0}'.format( oci_config['history']['author'] ) ) return history_args def post_process(self) -> None: """ Cleanup unused data from operations """ Command.run(['umoci', 'gc', '--layout', self.container_dir]) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.746591 kiwi-11.1.8/kiwi/package_manager/__init__.py0000644000000000000000000000551615273675667015673 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import TYPE_CHECKING, List from abc import ABCMeta, abstractmethod import logging # project from kiwi.exceptions import KiwiPackageManagerSetupError if TYPE_CHECKING: # pragma: nocover from kiwi.package_manager.base import PackageManagerBase log = logging.getLogger('kiwi') class PackageManager(metaclass=ABCMeta): """ **Package manager factory** :param object repository: instance of :class:`Repository` :param str package_manager: package manager name :param list custom_args: custom package manager arguments list :raises KiwiPackageManagerSetupError: if the requested package manager type is not supported :return: package manager :rtype: PackageManagerBase subclass """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( repository: object, package_manager_name: str, custom_args: List = [], release_version: str = '', ) -> 'PackageManagerBase': name_map = { 'zypper': ['zypper', 'Zypper'], 'dnf': ['dnf', 'Dnf'], 'dnf5': ['dnf5', 'Dnf5'], 'dnf4': ['dnf4', 'Dnf4'], 'microdnf': ['microdnf', 'MicroDnf'], 'pacman': ['pacman', 'Pacman'], 'apt': ['apt', 'Apt'], 'apk': ['apk', 'Apk'], } try: module_namespace, module_name = name_map[package_manager_name] package_manager = importlib.import_module( 'kiwi.package_manager.{0}'.format(module_namespace) ) module_name = 'PackageManager{0}'.format(module_name) manager = package_manager.__dict__[module_name]( repository, custom_args, release_version ) except Exception as issue: raise KiwiPackageManagerSetupError( 'Support for package manager {0} not implemented {1}'.format( package_manager_name, issue ) ) log.info( 'Using package manager backend: {0}'.format(package_manager_name) ) return manager ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/apk.py0000644000000000000000000001727515273675667014714 0ustar00# Copyright (c) 2025 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import logging from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.exceptions import KiwiBootStrapPhaseFailed log = logging.getLogger('kiwi') class PackageManagerApk(PackageManagerBase): """ **Implements Installation/Deletion of packages/collections with apk** :param dict command_env: apk command environment from repository runtime configuration """ def post_init(self, custom_args: Optional[List[str]] = None) -> None: """ Post initialization method Store custom apk arguments :param list custom_args: custom apk arguments """ self.custom_args: List[str] = custom_args or [] runtime_config = self.repository.runtime_config() self.command_env = runtime_config['command_env'] self.bootstrap_repo = runtime_config['bootstrap_repo'] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request apk does not distinguish the installation of packages and collections. Because of that collections are listed together with package requests and a warning message is shown. :param str name: apk group name """ log.warning( ( 'apk treats collection installations as regular packages.' 'It is preferred to list them as regular packages' ) ) self.package_requests.append(name) def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the Alpine Linux repo data :param str name: unused """ pass def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Repository modules not supported for apk. The method does nothing in this scope :param dict collection_modules: unused """ pass def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ if not self.bootstrap_repo: raise KiwiBootStrapPhaseFailed( 'No bootstrap repository URI available' ) if not self.package_requests: self.package_requests.append('alpine-base') apk_command = ( ['apk'] + ['--root', self.root_dir] + self.custom_args + [ '-X', self.bootstrap_repo, '-U', '--allow-untrusted', '--initdb', 'add', ] + self.package_requests ) self.cleanup_requests() return Command.call(apk_command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ Command.run( ['chroot', self.root_dir, 'apk', 'update'], self.command_env ) apk_command = ( ['chroot', self.root_dir, 'apk'] + self.custom_args + ['add'] + self.package_requests ) self.cleanup_requests() return Command.call(apk_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: unused :raises KiwiRequestError: if none of the packages to delete is installed :return: process results in command type :rtype: namedtuple """ Command.run( ['chroot', self.root_dir, 'apk', 'update'], self.command_env ) apk_command = ( ['chroot', self.root_dir, 'apk'] + self.custom_args + ['del'] + self.package_requests ) self.cleanup_requests() return Command.call(apk_command, self.command_env) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ Command.run( ['chroot', self.root_dir, 'apk', 'update'], self.command_env ) return Command.call( ['chroot', self.root_dir, 'apk', 'upgrade'] + self.custom_args, self.command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages. This is the only option in apk, nothing to do here. """ pass def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. There is no such concept of recommended dependencies in apk. """ pass def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the apk command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.* Installing {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: apk status line :returns: True|False :rtype: bool """ return bool( re.match( '.* Removing {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/apt.py0000644000000000000000000005307215273675667014720 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import os import glob import logging import pathlib from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.path import Path from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.repository.apt import RepositoryApt from kiwi.utils.temporary import Temporary import kiwi.defaults as defaults from kiwi.exceptions import ( KiwiDebianBootstrapError, KiwiRequestError, KiwiFileNotFound, ) log = logging.getLogger('kiwi') class PackageManagerApt(PackageManagerBase): """ **Implements Installation/Deletion of packages/collections with apt-get** :param list apt_get_args: apt-get arguments from repository runtime configuration :param dict command_env: apt-get command environment from repository runtime configuration """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom apt-get arguments :param list custom_args: custom apt-get arguments """ self.repository: RepositoryApt = self.repository self.custom_args = custom_args self.deboostrap_minbase: bool = True runtime_config = self.repository.runtime_config() self.apt_get_args = runtime_config['apt_get_args'] self.command_env = runtime_config['command_env'] self.distribution = runtime_config['distribution'] self.distribution_path = runtime_config['distribution_path'] self.bootstrap_packages: List[str] = [] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request There is no collection definition in the deb repo data :param str name: unused """ log.warning('Collection(%s) handling not supported for apt-get', name) def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the deb repo data :param str name: unused """ log.warning('Product(%s) handling not supported for apt-get', name) def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request Package exclusion for apt package manager not yet implemented :param str name: unused """ log.warning( 'Package exclusion for (%s) not supported for apt-get', name ) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Repository modules not supported for apt-get The method does nothing in this scope :param dict collection_modules: unused """ pass def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) Either a manual unpacking strategy or a prebuilt bootstrap package can be used to bootstrap a new system. :param object root_bind: unused :param str bootstrap_package: package name of a bootstrap package :return: process results as command instance :rtype: command_call_type """ if bootstrap_package: return self._process_install_requests_bootstrap_prebuild_root( bootstrap_package ) else: return self._process_install_requests_bootstrap() def _process_install_requests_bootstrap_prebuild_root( self, bootstrap_package: str ) -> CommandCallT: """ Process bootstrap phase (no chroot) using a prebuilt bootstrap package. The package has to provide a tarball below the directory /var/cache/kiwi/bootstrap/PACKAGE_NAME.ARCH.tar.xz and will be unpacked to serve as the bootstrap root. The optionally listed packages in the kiwi bootstrap section will be installed as part of a chroot install and returned as command instance :param str bootstrap_package: package name of the bootstrap package containing the bootstrap root as a tarball :return: process results as command instance :rtype: command_call_type """ # Install prebuilt bootstrap package on build host update_command = ( ['apt-get'] + self.apt_get_args + self.custom_args + ['update'] ) install_command = ( ['apt-get'] + self.apt_get_args + self.custom_args + ['install', bootstrap_package] ) Command.run(update_command, self.command_env) Command.run(install_command, self.command_env) # Unpack prebuilt bootstrap root tarball as new root bootstrap_root_tarball = '/var/lib/bootstrap/{0}.{1}.tar.xz'.format( bootstrap_package, defaults.PLATFORM_MACHINE ) if not os.path.isfile(bootstrap_root_tarball): raise KiwiFileNotFound( f'bootstrap tarball: {bootstrap_root_tarball!r} not found' ) Command.run(['tar', '-C', self.root_dir, '-xf', bootstrap_root_tarball]) # Install eventual bootstrap packages as standard system install return self.process_install_requests() def _process_install_requests_bootstrap(self) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot). :raises KiwiDebianBootstrapError :return: process results in command type :rtype: CommandCallT """ # we invoke apt-get install to download all the essential packages. # With DPkg::Pre-Install-Pkgs, we specify a shell command that will # receive the list of packages that will be installed on stdin. # By configuring Debug::pkgDpkgPm=1, apt-get install will not # actually execute any dpkg commands, so all it does is download # the essential debs and tell us their full in the apt cache without # actually installing them. try: # TODO: Drop once apt 2.5.4 is widely available. pathlib.Path(f'{self.root_dir}/var/lib/dpkg').mkdir( parents=True, exist_ok=True ) # Older versions of apt look for the status file both within and # without the tree, but do not create the status file within the # tree. pathlib.Path(f'{self.root_dir}/var/lib/dpkg/status.kiwi').touch() # Some versions of apt don't create the status file outside the # tree, e.g. 2.4.14. pathlib.Path('/var/lib/dpkg/status.kiwi').touch() pathlib.Path(f'{self.root_dir}/var/lib/dpkg/available').touch() if 'apt' not in self.package_requests: self.package_requests.append('apt') update_cache = ( ['apt-get'] + self.apt_get_args + self.custom_args + ['update'] ) result = Command.run(update_cache, self.command_env) log.debug('Apt update: {0} {1}'.format(result.output, result.error)) package_names = Temporary(prefix='kiwi_debs_').new_file() self.bootstrap_packages = ['?essential'] + self.package_requests download_bootstrap = ( ['apt-get'] + self.apt_get_args + self.custom_args + [ 'install', '-oDebug::pkgDPkgPm=1', f'-oDPkg::Pre-Install-Pkgs::=cat >{package_names.name}', ] + self.bootstrap_packages ) # Download solved bootstrap packages result = Command.run(download_bootstrap, self.command_env) log.debug( 'Apt download: {0} {1}'.format(result.output, result.error) ) # Extract bootstrap packages with open(package_names.name) as packages: solved_debootstrap_packages = [p.rstrip() for p in packages] self.command_env['PATH'] = '$PATH:/usr/bin:/bin:/usr/sbin:/sbin' for package in solved_debootstrap_packages: Command.run( [ 'bash', '-c', 'dpkg-deb --fsys-tarfile {0} | tar -C {1} -x'.format( package, self.root_dir ), ], self.command_env, ) # Run package scripts. Unfortuantely Debian based systems # requires special sauce for bootstrap. See the exceptions # we have to apply below: # # * manual order is required to make sure users(root) exists # * the usr-merge strategy only works after bootstrap # # 1. Pass: Run package scripts, manual order self._run_bootstrap_scripts( solved_debootstrap_packages, only_for=['base-passwd'], skip=['usrmerge'], ) # 2. Pass: Run package scripts in apt order self._run_bootstrap_scripts( solved_debootstrap_packages, skip=['usrmerge'] ) self.cleanup_requests() return Command.call(update_cache, self.command_env) except Exception as e: raise KiwiDebianBootstrapError(f'{type(e).__name__}: {format(e)}') def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ update_command = ['chroot', self.root_dir, 'apt-get'] update_command.extend( Path.move_to_root(self.root_dir, self.apt_get_args) ) update_command.extend(self.custom_args) update_command.append('update') Command.run(update_command, self.command_env) apt_get_command = ['chroot', self.root_dir, 'apt-get'] apt_get_command.extend( Path.move_to_root(self.root_dir, self.apt_get_args) ) apt_get_command.extend(self.custom_args) apt_get_command.append('install') # Due to the special bootstrap process, the packages unpacked # during bootstrap are not properly listed in the apt index. # Therefore the bootstrap packages are added to the install # phase which causes an install of this packages again to # fix the apt index and provide a consistent system from # an apt perspective. apt_get_command.extend(self.bootstrap_packages) apt_get_command.extend(self._package_requests()) return Command.call(apt_get_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed :return: process results in command type :rtype: namedtuple """ delete_items = [] for delete_item in self._package_requests(): try: Command.run( ['chroot', self.root_dir, 'dpkg', '-l', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) # deleting debs for some reason can also trigger package # scripts from the libc-bin (glibc) package. This often # results in calls like ldconfig which failed unless the # system would be really running. Since the deletion # happens via chroot and with the system in offline mode # many attempts to uninstall a package, even cracefully # failed for this reason. So far I only saw the workaround # to make ldconfig a noop during the process of uninstall. # This is not a nice solution and I dislike it. However, # the only one I could come up with to turn the package # uninstall in a useful feature in kiwi. Command.run( [ 'cp', f'{self.root_dir}/usr/sbin/ldconfig', f'{self.root_dir}/usr/sbin/ldconfig.orig', ] ) Command.run( [ 'cp', f'{self.root_dir}/usr/bin/true', f'{self.root_dir}/usr/sbin/ldconfig', ] ) if force: # force deleting debs only worked well for me when ignoring # the pre/post-inst and pre/post-remove codings. No idea why it # ends in so many conflicts but if you want to force get rid # of stuff this was the only way I could come up with. There # are still cases when it does not work depending on the many # code that runs on deleting for package in delete_items: pre_delete_pattern = ( f'{self.root_dir}/var/lib/dpkg/info/{package}*.pre*' ) post_delete_pattern = ( f'{self.root_dir}/var/lib/dpkg/info/{package}*.post*' ) for delete_file in glob.iglob(pre_delete_pattern): Path.wipe(delete_file) for delete_file in glob.iglob(post_delete_pattern): Path.wipe(delete_file) apt_get_command = ['chroot', self.root_dir, 'dpkg'] apt_get_command.extend( [ '--remove', '--force-remove-reinstreq', '--force-remove-essential', '--force-depends', ] ) apt_get_command.extend(delete_items) else: apt_get_command = ['chroot', self.root_dir, 'apt-get'] apt_get_command.extend( Path.move_to_root(self.root_dir, self.apt_get_args) ) apt_get_command.extend(self.custom_args) apt_get_command.extend(['--auto-remove', 'remove']) apt_get_command.extend(delete_items) return Command.call(apt_get_command, self.command_env) def post_process_delete_requests( self, root_bind: Optional[RootBind] = None ) -> None: """ Revert system changes done prior deleting packages :param object root_bind: unused """ Command.run( [ 'mv', f'{self.root_dir}/usr/sbin/ldconfig.orig', f'{self.root_dir}/usr/sbin/ldconfig', ] ) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ apt_get_command = ['chroot', self.root_dir, 'apt-get'] apt_get_command.extend( Path.move_to_root(self.root_dir, self.apt_get_args) ) apt_get_command.extend(self.custom_args) apt_get_command.append('upgrade') return Command.call(apt_get_command, self.command_env) def process_only_required(self) -> None: """ Setup package processing only for required packages """ if '--no-install-recommends' not in self.custom_args: self.custom_args.append('--no-install-recommends') self.deboostrap_minbase = True def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. """ if '--no-install-recommends' in self.custom_args: self.custom_args.remove('--no-install-recommends') self.deboostrap_minbase = False def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the apt-get command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: apt-get status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Unpacking {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: apt-get status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Removing {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as the repository keyring files used at build time. Keyring files of repositories marked with imageinclude or imageonly are stored in /usr/share/keyrings and stay in the image """ self.repository.delete_trusted_keys() def _package_requests(self) -> List: items = self.package_requests[:] self.cleanup_requests() return items def _run_bootstrap_scripts( self, solved_debootstrap_packages: List[str], only_for: List[str] = [], skip: List[str] = [], ) -> None: # TODO: this should not be needed but without setting # the following environment variables no package pre/post # script completes its task. I leave it up to the Debian # experts to provide a fix if needed. self.command_env['DPKG_MAINTSCRIPT_NAME'] = 'true' self.command_env['DPKG_MAINTSCRIPT_PACKAGE'] = 'libc6' post_script_dir = Temporary( prefix='kiwi_debpost.', path=self.root_dir ).new_dir() for package in solved_debootstrap_packages: package_base_name = os.path.basename(package) go_ahead = False if only_for else True for name in only_for: if name in package_base_name: go_ahead = True break for name in skip: if name in package_base_name: go_ahead = False break if not go_ahead: continue log.debug(f'Running pre/post scripts for: {package_base_name}') package_metadata_dir = ( f'{post_script_dir.name}/{os.path.basename(package)}' ) Command.run(['dpkg', '-e', package, package_metadata_dir]) script_pre = f'{package_metadata_dir}/preinst' script_post = f'{package_metadata_dir}/postinst' # 1. preinst if os.path.exists(script_pre): Command.run(['chmod', '755', script_pre]) Command.run( [ 'chroot', self.root_dir, f'{script_pre.replace(self.root_dir, "")}', 'install', ], self.command_env, ) # 2. postinst if os.path.exists(script_post): Command.run(['chmod', '755', script_post]) try: Command.run( [ 'chroot', self.root_dir, f'{script_post.replace(self.root_dir, "")}', 'configure', ], self.command_env, ) except Exception: # Some newer postinst scripts require a non-empty # version number as second parameter. Command.run( [ 'chroot', self.root_dir, f'{script_post.replace(self.root_dir, "")}', 'configure', "0", ], self.command_env, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/base.py0000644000000000000000000002012015273675667015032 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from types import TracebackType from typing import List, Dict, Optional, Type from kiwi.api_helper import decommissioned from kiwi.command import CommandCallT from kiwi.system.root_bind import RootBind from kiwi.repository.base import RepositoryBase class PackageManagerBase: """ **Implements base class for Package Management** :param object repository: instance of :class:`Repository` :param str root_dir: root directory path name :param list package_requests: list of packages to install or delete :param list collection_requests: list of collections to install :param list product_requests: list of products to install """ def __init__( self, repository: RepositoryBase, custom_args: List = [], release_version: str = '', ) -> None: self.repository = repository self.root_dir = repository.root_dir self.package_requests: List[str] = [] self.collection_requests: List[str] = [] self.product_requests: List[str] = [] self.exclude_requests: List[str] = [] self.release_version = release_version or '0' self.post_init(custom_args or []) def __enter__(self) -> 'PackageManagerBase': return self def post_init(self, custom_args: List = []) -> None: """ Post initialization method Implementation in specialized package manager class :param list custom_args: unused """ pass def request_package(self, name: str) -> None: """ Queue a package request Implementation in specialized package manager class :param str name: unused """ raise NotImplementedError def request_collection(self, name: str) -> None: """ Queue a package collection Implementation in specialized package manager class :param str name: unused """ raise NotImplementedError def request_product(self, name: str) -> None: """ Queue a product request Implementation in specialized package manager class :param str name: unused """ raise NotImplementedError @decommissioned def request_package_lock(self, name: str) -> None: pass # pragma: no cover def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request Implementation in specialized package manager class :param str name: unused """ raise NotImplementedError def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Setup repository modules and streams Implementation in specialized package manager class :param dict collection_modules: unused """ raise NotImplementedError def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) Implementation in specialized package manager class """ raise NotImplementedError def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) Implementation in specialized package manager class """ raise NotImplementedError def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) Implementation in specialized package manager class :param bool force: unused """ raise NotImplementedError def update(self) -> CommandCallT: """ Process package update requests (chroot) Implementation in specialized package manager class """ raise NotImplementedError def process_only_required(self) -> None: """ Setup package processing only for required packages Implementation in specialized package manager class """ raise NotImplementedError def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies Implementation in specialized package manager class """ raise NotImplementedError def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed Implementation in specialized package manager class :param str package_name: unused :param str package_manager_output: unused :return: True|False :rtype: bool """ raise NotImplementedError def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted Implementation in specialized package manager class :param str package_name: unused :param str package_manager_output: unused :return: True|False :rtype: bool """ raise NotImplementedError @decommissioned def database_consistent(self) -> None: pass # pragma: no cover @decommissioned def dump_reload_package_database(self, version: int = 45) -> None: pass # pragma: no cover def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: """ Process extra code required after bootstrapping Implementation in specialized package manager class """ pass def post_process_delete_requests( self, root_bind: Optional[RootBind] = None ) -> None: """ Process extra code required after deleting packages Implementation in specialized package manager class """ pass @staticmethod def has_failed(returncode: int) -> bool: """ Evaluate given result return code Any returncode != 0 is considered an error unless overwritten in specialized package manager class :param int returncode: return code number :return: True|False :rtype: boolean """ return returncode != 0 def get_error_details(self) -> str: """ Provide further error details In case the package manager call failed this method will return package manager specific error information if there is any :return: further error data as str or empty str :rtype: str """ return '' def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as custom macros Implementation in specialized package manager class """ pass def cleanup_requests(self) -> None: """ Cleanup request queues """ del self.package_requests[:] del self.collection_requests[:] del self.product_requests[:] del self.exclude_requests[:] def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: if self.repository: self.repository.cleanup() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/dnf.py0000644000000000000000000000621415273675667014677 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Dict, no_type_check, Optional # project from kiwi.command import CommandCallT from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.api_helper import decommissioned class PackageManagerDnf(PackageManagerBase): """ decommissioned, moved to PackageManagerDnf4 """ @decommissioned def post_init(self, custom_args: List = []) -> None: pass # pragma: no cover @decommissioned def request_package(self, name: str) -> None: pass # pragma: no cover @decommissioned def request_collection(self, name: str) -> None: pass # pragma: no cover @decommissioned def request_product(self, name: str) -> None: pass # pragma: no cover @decommissioned def request_package_exclusion(self, name: str) -> None: pass # pragma: no cover @decommissioned def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: pass # pragma: no cover @no_type_check @decommissioned def process_install_requests_bootstrap( self, root_bind: RootBind = None, bootstrap_package: str = None ) -> CommandCallT: pass # pragma: no cover @no_type_check @decommissioned def process_install_requests(self) -> CommandCallT: pass # pragma: no cover @no_type_check @decommissioned def process_delete_requests(self, force: bool = False) -> CommandCallT: pass # pragma: no cover @no_type_check @decommissioned def update(self) -> CommandCallT: pass # pragma: no cover @decommissioned def process_only_required(self) -> None: pass # pragma: no cover @decommissioned def process_plus_recommended(self) -> None: pass # pragma: no cover @no_type_check @decommissioned def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: pass # pragma: no cover @no_type_check @decommissioned def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: pass # pragma: no cover @decommissioned def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: pass # pragma: no cover @decommissioned def clean_leftovers(self) -> None: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/dnf4.py0000644000000000000000000003043215273675667014762 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import re from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.rpm import Rpm from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.exceptions import KiwiRequestError class PackageManagerDnf4(PackageManagerBase): """ ***Implements base class for installation/deletion of packages and collections using dnf*** :param doct dnf_args: dnf arguments from repository runtime configuration :param dict command_env: dnf command environment from repository runtime configuration """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method :param list custom_args: custom dnf arguments """ self.custom_args = custom_args runtime_config = self.repository.runtime_config() self.dnf_args = runtime_config['dnf_args'] self.command_env = runtime_config['command_env'] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request :param str name: dnf group ID name """ self.collection_requests.append(f'@{name}') def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the fedora repo data :param str name: unused """ pass def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def _get_dnf4_binary_name(self, root: Optional[str] = None) -> str: """ Identify whether dnf is 'dnf4' or 'dnf-3' :param str root: lookup binary name below this root directory :return: name of dnf4 command :rtype: str """ dnf4_binary = 'dnf-3' dnf4_search_env = ( {'PATH': os.sep.join([root, 'usr', 'bin'])} if root else None ) # Python interpreter specific path if Path.which( filename='dnf4', custom_env=dnf4_search_env, access_mode=os.X_OK ): dnf4_binary = 'dnf4' return dnf4_binary def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Setup repository modules and streams :param dict collection_modules: Expect dict of the form: .. code:: python { 'enable': [ "module:stream", "module" ], 'disable': [ "module" ] } """ dnf_module_command = ( [self._get_dnf4_binary_name()] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', ] + self.custom_args + ['module'] ) for disable_module in collection_modules['disable']: Command.run( dnf_module_command + ['disable', disable_module], self.command_env, ) for enable_module in collection_modules['enable']: Command.run( dnf_module_command + ['reset', enable_module.split(':')[0]], self.command_env, ) Command.run( dnf_module_command + ['enable', enable_module], self.command_env ) def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ dnf4 = self._get_dnf4_binary_name() exclude_args = [] if self.exclude_requests: # For DNF, excluding a package means removing it from # the solver operation. This is done by adding --exclude # to the command line. This means that if the package is # hard required by another package, it will break the transaction. for package in self.exclude_requests: exclude_args.append('--exclude=' + package) Command.run( [dnf4] + self.dnf_args + [f'--releasever={self.release_version}'] + ['makecache'], self.command_env, ) dnf_command = ( [dnf4] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', ] + self.custom_args + exclude_args + ['install'] + self.package_requests + self.collection_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ dnf4 = self._get_dnf4_binary_name(self.root_dir) exclude_args = [] if self.exclude_requests: # For DNF, excluding a package means removing it from # the solver operation. This is done by adding --exclude # to the command line. This means that if the package is # hard required by another package, it will break the transaction. for package in self.exclude_requests: exclude_args.append('--exclude=' + package) chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) dnf_command = ( ['chroot', self.root_dir, dnf4] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + exclude_args + ['install'] + self.package_requests + self.collection_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed. :return: process results in command type :rtype: namedtuple """ if force: delete_items = [] for delete_item in self.package_requests: try: Command.run( ['chroot', self.root_dir, 'rpm', '-q', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) self.cleanup_requests() delete_options = ['--nodeps', '--allmatches', '--noscripts'] return Command.call( ['chroot', self.root_dir, 'rpm', '-e'] + delete_options + delete_items, self.command_env, ) else: dnf4 = self._get_dnf4_binary_name(self.root_dir) chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) dnf_command = ( ['chroot', self.root_dir, dnf4] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + ['autoremove'] + self.package_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ dnf4 = self._get_dnf4_binary_name(self.root_dir) chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) return Command.call( ['chroot', self.root_dir, dnf4] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + ['upgrade'], self.command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages """ if '--setopt=install_weak_deps=False' not in self.custom_args: self.custom_args.append('--setopt=install_weak_deps=False') def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. """ if '--setopt=install_weak_deps=False' in self.custom_args: self.custom_args.remove('--setopt=install_weak_deps=False') def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the dnf command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Installing.*: {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Removing.*: {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: """ Move the rpm database to the place as it is expected by the rpm package installed during bootstrap phase :param object root_bind: unused :param bool delta_root: unused """ rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.rebuild_database() rpmdb.set_database_to_image_path() def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as custom macros """ Rpm( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ).wipe_config() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/dnf5.py0000644000000000000000000003070415273675667014765 0ustar00# Copyright (c) 2023 Marcus Schäfer. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.rpm import Rpm from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.exceptions import KiwiRequestError class PackageManagerDnf5(PackageManagerBase): """ ***Implements base class for installation/deletion of packages and collections using the next generation dnf5*** :param doct dnf_args: dnf arguments from repository runtime configuration :param dict command_env: dnf command environment from repository runtime configuration """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method :param list custom_args: custom dnf arguments """ self.custom_args = custom_args runtime_config = self.repository.runtime_config() self.dnf_args = runtime_config['dnf_args'] self.command_env = runtime_config['command_env'] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request :param str name: dnf group ID name """ self.collection_requests.append(f'@{name}') def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the fedora repo data :param str name: unused """ pass def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Setup repository modules and streams :param dict collection_modules: Expect dict of the form: .. code:: python { 'enable': [ "module:stream", "module" ], 'disable': [ "module" ] } """ dnf_module_command = ( ['dnf5'] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', ] + self.custom_args + ['module'] ) for disable_module in collection_modules['disable']: Command.run( dnf_module_command + ['disable', disable_module], self.command_env, ) for enable_module in collection_modules['enable']: Command.run( dnf_module_command + ['reset', enable_module.split(':')[0]], self.command_env, ) Command.run( dnf_module_command + ['enable', enable_module], self.command_env ) def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ exclude_args = [] if self.exclude_requests: # For DNF, excluding a package means removing it from # the solver operation. This is done by adding --exclude # to the command line. This means that if the package is # hard required by another package, it will break the transaction. for package in self.exclude_requests: exclude_args.append('--exclude=' + package) Command.run( ['dnf5'] + self.dnf_args + [f'--releasever={self.release_version}'] + ['makecache'], self.command_env, ) dnf_command = ( ['dnf5'] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', ] + self.custom_args + exclude_args + ['install'] + self.package_requests + self.collection_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ exclude_args = [] if self.exclude_requests: # For DNF, excluding a package means removing it from # the solver operation. This is done by adding --exclude # to the command line. This means that if the package is # hard required by another package, it will break the transaction. for package in self.exclude_requests: exclude_args.append('--exclude=' + package) chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) dnf_command = ( ['chroot', self.root_dir, 'dnf5'] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + exclude_args + ['install'] + self.package_requests + self.collection_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed. :return: process results in command type :rtype: namedtuple """ if force: delete_items = [] for delete_item in self.package_requests: try: Command.run( ['chroot', self.root_dir, 'rpm', '-q', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) self.cleanup_requests() delete_options = ['--nodeps', '--allmatches', '--noscripts'] return Command.call( ['chroot', self.root_dir, 'rpm', '-e'] + delete_options + delete_items, self.command_env, ) else: chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) dnf_command = ( ['chroot', self.root_dir, 'dnf5'] + chroot_dnf_args + [ f'--releasever={self.release_version}', '--setopt=clean_requirements_on_remove=true', ] + self.custom_args + ['remove'] + self.package_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) return Command.call( ['chroot', self.root_dir, 'dnf5'] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + ['upgrade'], self.command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages """ self.__clean_pattern_type() self.custom_args.append('--setopt=install_weak_deps=False') self.custom_args.append('--setopt=group_package_types=mandatory') def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. """ self.__clean_pattern_type() self.custom_args.append('--setopt=install_weak_deps=True') self.custom_args.append( '--setopt=group_package_types=default,mandatory,conditional' ) def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the dnf command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Installing.* {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Removing.* {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: """ Move the rpm database to the place as it is expected by the rpm package installed during bootstrap phase :param object root_bind: unused :param bool delta_root: unused """ rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.rebuild_database() rpmdb.set_database_to_image_path() def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as custom macros """ Rpm( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ).wipe_config() def __clean_pattern_type(self) -> None: """ Setup package processing only for required packages """ if '--setopt=install_weak_deps=True' in self.custom_args: self.custom_args.remove('--setopt=install_weak_deps=True') if '--setopt=install_weak_deps=False' in self.custom_args: self.custom_args.remove('--setopt=install_weak_deps=False') if ( '--setopt=group_package_types=default,mandatory,conditional' in self.custom_args ): self.custom_args.remove( '--setopt=group_package_types=default,mandatory,conditional' ) if '--setopt=group_package_types=mandatory' in self.custom_args: self.custom_args.remove('--setopt=group_package_types=mandatory') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/microdnf.py0000644000000000000000000002753215273675667015737 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import logging from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.rpm import Rpm from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.repository.dnf4 import RepositoryDnf4 from kiwi.exceptions import KiwiRequestError log = logging.getLogger('kiwi') class PackageManagerMicroDnf(PackageManagerBase): """ **Implements Installation/Deletion of packages/collections with microdnf*** :param doct dnf_args: microdnf arguments from repository runtime configuration :param dict command_env: microdnf command environment from repository runtime configuration """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method :param list custom_args: custom microdnf arguments """ self.repository: RepositoryDnf4 = self.repository self.custom_args = custom_args runtime_config = self.repository.runtime_config() self.dnf_args = runtime_config['dnf_args'] self.command_env = runtime_config['command_env'] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request :param str name: dnf group name """ log.warning('Group(%s) handling not yet supported for microdnf', name) def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the fedora repo data :param str name: unused """ pass def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Setup repository modules and streams :param dict collection_modules: Expect dict of the form: .. code:: python { 'enable': [ "module:stream", "module" ], 'disable': [ "module" ] } """ microdnf_module_command = ( ['microdnf'] + ['--refresh'] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', '--noplugins', '--setopt=cachedir={0}'.format( self.repository.shared_dnf_dir['cache-dir'] ), '--setopt=reposdir={0}'.format( self.repository.shared_dnf_dir['reposd-dir'] ), '--setopt=varsdir={0}'.format( self.repository.shared_dnf_dir['vars-dir'] ), ] + self.custom_args + ['module'] ) for disable_module in collection_modules['disable']: Command.run( microdnf_module_command + ['disable', disable_module], self.command_env, ) for enable_module in collection_modules['enable']: Command.run( microdnf_module_command + ['reset', enable_module.split(':')[0]], self.command_env, ) Command.run( microdnf_module_command + ['enable', enable_module], self.command_env, ) def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ microdnf_command = ( ['microdnf'] + ['--refresh'] + self.dnf_args + [ '--installroot', self.root_dir, f'--releasever={self.release_version}', '--noplugins', '--setopt=cachedir={0}'.format( self.repository.shared_dnf_dir['cache-dir'] ), '--setopt=reposdir={0}'.format( self.repository.shared_dnf_dir['reposd-dir'] ), '--setopt=varsdir={0}'.format( self.repository.shared_dnf_dir['vars-dir'] ), ] + self.custom_args + ['install'] + self.package_requests ) self.cleanup_requests() return Command.call(microdnf_command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ exclude_args = [] if self.exclude_requests: # For DNF, excluding a package means removing it from # the solver operation. This is done by adding --exclude # to the command line. This means that if the package is # hard required by another package, it will break the transaction. for package in self.exclude_requests: exclude_args.append('--exclude=' + package) chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) microdnf_command = ( ['chroot', self.root_dir, 'microdnf'] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + exclude_args + ['install'] + self.package_requests ) self.cleanup_requests() return Command.call(microdnf_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed. :return: process results in command type :rtype: namedtuple """ if force: delete_items = [] for delete_item in self.package_requests: try: Command.run( ['chroot', self.root_dir, 'rpm', '-q', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) self.cleanup_requests() delete_options = ['--nodeps', '--allmatches', '--noscripts'] return Command.call( ['chroot', self.root_dir, 'rpm', '-e'] + delete_options + delete_items, self.command_env, ) else: chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) dnf_command = ( ['chroot', self.root_dir, 'microdnf'] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + ['remove'] + self.package_requests ) self.cleanup_requests() return Command.call(dnf_command, self.command_env) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ chroot_dnf_args = Path.move_to_root(self.root_dir, self.dnf_args) return Command.call( ['chroot', self.root_dir, 'microdnf'] + chroot_dnf_args + [f'--releasever={self.release_version}'] + self.custom_args + ['upgrade'], self.command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages """ if '--setopt=install_weak_deps=0' not in self.custom_args: self.custom_args.append('--setopt=install_weak_deps=0') def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. """ if '--setopt=install_weak_deps=0' in self.custom_args: self.custom_args.remove('--setopt=install_weak_deps=0') def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the microdnf command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: microdnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Installing : {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: microdnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Removing: {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: """ Move the rpm database to the place as it is expected by the rpm package installed during bootstrap phase :param object root_bind: unused :param bool delta_root: unused """ rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.rebuild_database() rpmdb.set_database_to_image_path() def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as custom macros """ Rpm( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ).wipe_config() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/pacman.py0000644000000000000000000002047715273675667015376 0ustar00# Copyright (c) 2019 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import logging from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.exceptions import KiwiRequestError from kiwi.path import Path log = logging.getLogger('kiwi') class PackageManagerPacman(PackageManagerBase): """ **Implements Installation/Deletion of packages/collections with pacman** :param list pacman_args: pacman arguments from repository runtime configuration :param dict command_env: pacman command environment from repository runtime configuration """ def post_init(self, custom_args: Optional[List[str]] = None) -> None: """ Post initialization method Store custom pacman arguments :param list custom_args: custom pacman arguments """ self.custom_args: List[str] = custom_args or [] runtime_config = self.repository.runtime_config() self.pacman_args = runtime_config['pacman_args'] self.command_env = runtime_config['command_env'] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request Pacman does not distinguish the installation of pacakges and collections. Because of that collections are listed together with package requests and a warning message is shown. :param str name: pacman group name """ log.warning( ( 'Pacman treats collection installations as regular packages.' 'It is preferred to list them as regular packages' ) ) self.package_requests.append(name) def request_product(self, name: str) -> None: """ Queue a product request There is no product definition in the Arch Linux repo data :param str name: unused """ pass def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Repository modules not supported for pacman. The method does nothing in this scope :param dict collection_modules: unused """ pass def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ Command.run( ['pacman'] + self.pacman_args + ['--root', self.root_dir, '-Sy'], self.command_env, ) pacman_command = ( ['pacman'] + self.pacman_args + ['--root', self.root_dir] + self.custom_args + [ '-S', '--needed', '--overwrite', '{}/var/run'.format(self.root_dir), ] + self.package_requests ) self.cleanup_requests() return Command.call(pacman_command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) pacman_command = ( ['chroot', self.root_dir, 'pacman'] + chroot_pacman_args + self.custom_args + ['-S', '--needed'] + self.package_requests ) self.cleanup_requests() return Command.call(pacman_command, self.command_env) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed :return: process results in command type :rtype: namedtuple """ delete_items = [] for delete_item in self.package_requests: try: Command.run( ['chroot', self.root_dir, 'pacman', '-Qi', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) self.cleanup_requests() chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) return Command.call( ['chroot', self.root_dir, 'pacman'] + chroot_pacman_args + self.custom_args + ['-Rdd' if force else '-Rs'] + delete_items, self.command_env, ) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ chroot_pacman_args = Path.move_to_root(self.root_dir, self.pacman_args) return Command.call( ['chroot', self.root_dir, 'pacman'] + chroot_pacman_args + self.custom_args + ['-Su'], self.command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages. This is the only option in pacman, nothing to do here. """ pass def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. There is no such concept of recommended dependencies in pacman. """ pass def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the pacman command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: dnf status line :returns: True|False :rtype: bool """ return bool( re.match( '.* installing {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: pacman status line :returns: True|False :rtype: bool """ return bool( re.match( '.* removing {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/package_manager/zypper.py0000644000000000000000000003000715273675667015456 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import re import os from typing import List, Dict, Optional # project from kiwi.command import CommandCallT from kiwi.command import Command from kiwi.package_manager.base import PackageManagerBase from kiwi.system.root_bind import RootBind from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.rpm import Rpm from kiwi.path import Path from kiwi.exceptions import KiwiRequestError from kiwi.defaults import Defaults class PackageManagerZypper(PackageManagerBase): """ **Implements Installation/Deletion of packages/collections with zypper** :param list zypper_args: zypper arguments from repository runtime configuration :param dict command_env: zypper command environment from repository runtime configuration """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom zypper arguments :param list custom_args: custom zypper arguments """ self.anonymousId_file = '/var/lib/zypp/AnonymousUniqueId' self.custom_args = custom_args runtime_config = self.repository.runtime_config() self.zypper_args = runtime_config['zypper_args'] self.chroot_zypper_args = Path.move_to_root( self.root_dir, self.zypper_args ) self.command_env = runtime_config['command_env'] self.chroot_command_env = dict(self.command_env) if 'ZYPP_CONF' in self.command_env: self.chroot_command_env['ZYPP_CONF'] = Path.move_to_root( self.root_dir, [self.command_env['ZYPP_CONF']] )[0] def request_package(self, name: str) -> None: """ Queue a package request :param str name: package name """ self.package_requests.append(name) def request_collection(self, name: str) -> None: """ Queue a collection request :param str name: zypper pattern name """ self.collection_requests.append('pattern:' + name) def request_product(self, name: str) -> None: """ Queue a product request :param str name: zypper product name """ self.product_requests.append('product:' + name) def request_package_exclusion(self, name: str) -> None: """ Queue a package exclusion(skip) request :param str name: package name """ self.exclude_requests.append(name) def setup_repository_modules( self, collection_modules: Dict[str, List[str]] ) -> None: """ Repository modules not supported for zypper. The method does nothing in this scope :param dict collection_modules: unused """ pass def process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, bootstrap_package: Optional[str] = None, ) -> CommandCallT: """ Process package install requests for bootstrap phase (no chroot) :param object root_bind: unused :param str bootstrap_package: unused :return: process results in command type :rtype: namedtuple """ command = ( ['zypper'] + self.zypper_args + [ '--root', self.root_dir, 'install', '--download', 'in-advance', '--auto-agree-with-licenses', ] + self.custom_args + ['--'] + self._install_items() ) return Command.call(command, self.command_env) def process_install_requests(self) -> CommandCallT: """ Process package install requests for image phase (chroot) :return: process results in command type :rtype: namedtuple """ if self.exclude_requests: # For zypper excluding a package means, removing it from # the solver operation. This is done by adding a package # lock. This means that if the package is hard required # by another package, it will break the transaction. metadata_dir = ''.join([self.root_dir, '/etc/zypp']) if not os.path.exists(metadata_dir): Path.create(metadata_dir) for package in self.exclude_requests: Command.run( ['chroot', self.root_dir, 'zypper'] + self.chroot_zypper_args + ['al'] + [package], self.chroot_command_env, ) return Command.call( ['chroot', self.root_dir, 'zypper'] + self.chroot_zypper_args + [ 'install', '--download', 'in-advance', '--auto-agree-with-licenses', ] + self.custom_args + ['--'] + self._install_items(), self.chroot_command_env, ) def process_delete_requests(self, force: bool = False) -> CommandCallT: """ Process package delete requests (chroot) :param bool force: force deletion: true|false :raises KiwiRequestError: if none of the packages to delete is installed :return: process results in command type :rtype: namedtuple """ if force: delete_items = [] for delete_item in self._delete_items(): try: Command.run( ['chroot', self.root_dir, 'rpm', '-q', delete_item] ) delete_items.append(delete_item) except Exception: # ignore packages which are not installed pass if not delete_items: raise KiwiRequestError( 'None of the requested packages to delete are installed' ) self.cleanup_requests() force_options = ['--nodeps', '--allmatches', '--noscripts'] return Command.call( ['chroot', self.root_dir, 'rpm', '-e'] + force_options + delete_items, self.chroot_command_env, ) else: zypper_command = ( ['chroot', self.root_dir, 'zypper'] + self.chroot_zypper_args + ['remove', '-u', '--force-resolution'] + self._delete_items() ) self.cleanup_requests() return Command.call(zypper_command, self.chroot_command_env) def update(self) -> CommandCallT: """ Process package update requests (chroot) :return: process results in command type :rtype: namedtuple """ return Command.call( ['chroot', self.root_dir, 'zypper'] + self.chroot_zypper_args + [ 'update', '--download', 'in-advance', '--auto-agree-with-licenses', ] + self.custom_args, self.chroot_command_env, ) def process_only_required(self) -> None: """ Setup package processing only for required packages """ if '--no-recommends' not in self.custom_args: self.custom_args.append('--no-recommends') def process_plus_recommended(self) -> None: """ Setup package processing to also include recommended dependencies. """ if '--no-recommends' in self.custom_args: self.custom_args.remove('--no-recommends') def match_package_installed( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been installed This match for the package to be installed in the output of the zypper command is not 100% accurate. There might be false positives due to sub package names starting with the same base package name :param str package_name: package_name :param str package_manager_output: zypper status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Installing: {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def match_package_deleted( self, package_name: str, package_manager_output: str ) -> bool: """ Match expression to indicate a package has been deleted :param str package_name: package_name :param str package_manager_output: zypper status line :returns: True|False :rtype: bool """ return bool( re.match( '.*Removing: {0}.*'.format(re.escape(package_name)), package_manager_output, ) ) def post_process_install_requests_bootstrap( self, root_bind: Optional[RootBind] = None, delta_root: bool = False ) -> None: """ Move the rpm database to the place as it is expected by the rpm package installed during bootstrap phase :param object root_bind: unused :param bool delta_root: unused """ rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.rebuild_database() rpmdb.set_database_to_image_path() @staticmethod def has_failed(returncode: int) -> bool: """ Evaluate given result return code In zypper any return code == 0 or >= 100 is considered success. Any return code different from 0 and < 100 is treated as an error we care for. Return codes >= 100 indicates an issue like 'new kernel needs reboot of the system' or similar which we don't care in the scope of image building :param int returncode: return code number :return: True|False :rtype: boolean """ error_codes = [ 104, # - ZYPPER_EXIT_INF_CAP_NOT_FOUND 105, # - ZYPPER_EXIT_ON_SIGNAL 106, # - ZYPPER_EXIT_INF_REPOS_SKIPPED 127, # - Command Not Found ] if returncode == 0: # All is good return False elif returncode in error_codes: # Treat matching exit code as error return True elif returncode >= 100: # Treat all other 100 codes as non error codes return False # Treat any other error code as error return True def clean_leftovers(self) -> None: """ Cleans package manager related data not needed in the resulting image such as custom macros """ Rpm( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ).wipe_config() id_file = os.path.normpath( os.sep.join([self.root_dir, self.anonymousId_file]) ) if os.path.exists(id_file): os.unlink(id_file) def _install_items(self) -> List: items = ( self.package_requests + self.collection_requests + self.product_requests ) self.cleanup_requests() return items def _delete_items(self) -> List: # collections and products can't be deleted items = [] items += self.package_requests self.cleanup_requests() return items ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7467964 kiwi-11.1.8/kiwi/partitioner/__init__.py0000644000000000000000000000465415273675667015150 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import TYPE_CHECKING, Optional from abc import ABCMeta, abstractmethod import logging # project from kiwi.exceptions import KiwiPartitionerSetupError if TYPE_CHECKING: # pragma: nocover from kiwi.partitioner.base import PartitionerBase log = logging.getLogger('kiwi') class Partitioner(metaclass=ABCMeta): """ **Partitioner factory** :param string table_type: Table type name :param object storage_provider: Instance of class based on DeviceProvider :param int start_sector: sector number :param bool extended_layout: support extended layout for msdos table """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( table_type: str, storage_provider: object, start_sector: Optional[int] = None, extended_layout: bool = False, ) -> 'PartitionerBase': name_map = {'msdos': 'MsDos', 'gpt': 'Gpt', 'dasd': 'Dasd'} try: partitioner = importlib.import_module( 'kiwi.partitioner.{0}'.format(table_type) ) module_name = 'Partitioner{0}'.format(name_map[table_type]) if table_type == 'dasd' and start_sector: log.warning( 'disk start_sector value is ignored for dasd partitions' ) start_sector = None return partitioner.__dict__[module_name]( storage_provider, start_sector, extended_layout ) except Exception as issue: raise KiwiPartitionerSetupError( 'Support for {0} partitioner not implemented: {1}'.format( table_type, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/partitioner/base.py0000644000000000000000000001032515273675667014313 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Dict, Union, Optional # project from kiwi.storage.device_provider import DeviceProvider class PartitionerBase: """ **Base class for partitioners** """ def __init__( self, disk_provider: DeviceProvider, start_sector: Optional[int] = None, extended_layout: bool = False, ) -> None: """ Base class constructor for partitioners :param object disk_provider: Instance of DeviceProvider :param int start_sector: sector number :param bool extended_layout: If set to true and on msdos table type when creating more than 4 partitions, this will cause the fourth partition to be an extended partition and all following partitions will be placed as logical partitions inside of that extended partition """ self.disk_device = disk_provider.get_device() self.partition_id = 0 self.partition_count = 0 self.start_sector = start_sector self.extended_layout = extended_layout self.flag_map: Dict[str, Union[bool, str, None]] = {} self.post_init() def post_init(self) -> None: """ Post initialization method Implementation in specialized partitioner class """ pass def get_id(self) -> int: """ Current partition number Zero indicates no partition has been created so far :return: partition number :rtype: int """ return self.partition_id def create( self, name: str, mbsize: Union[int, str], type_name: str, flags: List[str] = [], partition_id: Optional[int] = None, ) -> None: """ Create partition Implementation in specialized partitioner class :param string name: unused :param int mbsize: unused :param string type_name: unused :param list flags: unused :param int partition_id: If provided, use this exact partition ID instead of auto-incrementing. """ raise NotImplementedError def set_flag(self, partition_id: Union[int, str], flag_name: str) -> None: """ Set partition flag Implementation in specialized partitioner class :param int partition_id: unused :param string flag_name: unused """ raise NotImplementedError def set_uuid(self, partition_id: Union[int, str], uuid: str) -> None: """ Set partition UUID Implementation in specialized partitioner class :param int partition_id: unused :param string uuid: unused """ raise NotImplementedError def set_hybrid_mbr(self) -> None: """ Turn partition table into hybrid table if supported Implementation in specialized partitioner class """ raise NotImplementedError def set_mbr(self) -> None: """ Turn partition table into MBR (msdos table) Implementation in specialized partitioner class """ raise NotImplementedError def set_start_sector(self, start_sector: int) -> None: """ Set start sector of first partition as configured :param int start_sector: unused Does nothing by default """ pass def resize_table(self, entries: int = 0) -> None: """ Resize partition table :param int entries: unused """ raise NotImplementedError ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/partitioner/dasd.py0000644000000000000000000000713715273675667014323 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import List, Optional, Union # project from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.partitioner.base import PartitionerBase log = logging.getLogger('kiwi') class PartitionerDasd(PartitionerBase): """ **Implements DASD partition setup** """ def post_init(self) -> None: """ Post initialization method Setup fdasd partition type/flag map """ self.flag_map = { 'f.active': None, 't.linux': '1', 't.swap': '1', 't.lvm': '1', 't.raid': '1', 't.efi': None, 't.csm': None, } def create( self, name: str, mbsize: Union[int, str], type_name: str, flags: Optional[List[str]] = None, partition_id: Optional[int] = None, ) -> None: """ Create DASD partition :param string name: partition name :param int mbsize: partition size :param string type_name: unused :param list flags: unused :param int partition_id: unused """ self.partition_id += 1 fdasd_input = Temporary().new_file() with open(fdasd_input.name, 'w') as partition: log.debug( '%s: fdasd: n p cur_position +%sM w q', name, format(mbsize) ) if mbsize == 'all_free': partition.write('n\np\n\n\nw\nq\n') else: partition.write(f'n\np\n\n+{mbsize}M\nw\nq\n') bash_command = ' '.join( ['cat', fdasd_input.name, '|', 'fdasd', '-f', self.disk_device] ) try: Command.run(['bash', '-c', bash_command]) except Exception: # unfortunately fdasd reports that it can't read in the partition # table which I consider a bug in fdasd. However the table was # correctly created and therefore we continue. Problem is that we # are not able to detect real errors with the fdasd operation at # that point. log.debug('potential fdasd errors were ignored') def set_uuid(self, partition_id: Union[int, str], uuid: str) -> None: """ Set partition UUID Nothing to be done here for DASD devices :param int partition_id: unused :param string uuid: unused """ pass # pragma: nocover def set_flag(self, partition_id: Union[int, str], flag_name: str) -> None: """ Set partition flag Nothing to be done here for DASD devices :param int partition_id: unused :param string flag_name: unused """ pass # pragma: nocover def resize_table(self, entries: Optional[int] = None) -> None: """ Resize partition table Nothing to be done here for DASD devices :param int entries: unused """ pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/partitioner/gpt.py0000644000000000000000000001503315273675667014174 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import List, Optional, Union # project from kiwi.command import Command from kiwi.partitioner.base import PartitionerBase from kiwi.exceptions import KiwiPartitionerGptFlagError log = logging.getLogger('kiwi') class PartitionerGpt(PartitionerBase): """ **Implements GPT partition setup** """ def post_init(self) -> None: """ Post initialization method Setup gdisk partition type/flag map """ self.flag_map = { 'f.active': None, 't.csm': 'EF02', 't.linux': '8300', 't.swap': '8200', 't.lvm': '8E00', 't.raid': 'FD00', 't.efi': 'EF00', 't.prep': '4100', } self.partition_map: dict[int, int] = {} def create( self, name: str, mbsize: Union[int, str], type_name: str, flags: Optional[List[str]] = None, partition_id: Optional[int] = None, ) -> None: """ Create GPT partition :param string name: partition name :param int mbsize: partition size :param string type_name: partition type :param list flags: additional flags :param int partition_id: If provided, use this exact partition ID instead of auto-incrementing """ self.partition_id = ( partition_id if partition_id else self.partition_id + 1 ) self.partition_count += 1 self.partition_map[self.partition_count] = self.partition_id if mbsize == 'all_free': partition_end = '0' else: partition_end = '+' + format(mbsize) + 'M' if self.partition_count > 1 or not self.start_sector: # A start sector value of 0 specifies the default value # defined in sgdisk self.start_sector = 0 Command.run( [ 'sgdisk', '--align-end', '-n', ':'.join( [ format(self.partition_id), format(self.start_sector), partition_end, ] ), '-c', ':'.join([format(self.partition_id), name]), self.disk_device, ] ) self.set_flag(self.partition_id, type_name) if flags: for flag_name in flags: self.set_flag(self.partition_id, flag_name) def set_flag(self, partition_id: Union[int, str], flag_name: str) -> None: """ Set GPT partition flag :param int partition_id: partition number :param string flag_name: name from flag map """ if flag_name not in self.flag_map: raise KiwiPartitionerGptFlagError( f'Unknown partition flag {flag_name}' ) if self.flag_map[flag_name]: Command.run( [ 'sgdisk', '-t', ':'.join( [format(partition_id), format(self.flag_map[flag_name])] ), self.disk_device, ] ) else: log.warning('Flag %s ignored on GPT', flag_name) def set_uuid(self, partition_id: Union[int, str], uuid: str) -> None: """ Set partition UUID (TypeCode) :param int partition_id: partition number :param string uuid: UUID """ Command.run( ['sgdisk', '--typecode', f'{partition_id}:{uuid}', self.disk_device] ) def set_hybrid_mbr(self) -> None: """ Turn partition table into hybrid GPT/MBR table """ partition_ids = [] partition_number_to_embed = self.partition_count if partition_number_to_embed > 3: # the max number of partitions to embed is 3 # for details see man sgdisk log.debug( 'maximum number of GPT hybrid MBR partitions is 3, got %d', partition_number_to_embed, ) partition_number_to_embed = 3 log.debug( 'reduced GPT hybrid MBR partition count to %d', partition_number_to_embed, ) for number in range(1, partition_number_to_embed + 1): if self.partition_map.get(number): partition_ids.append(format(self.partition_map[number])) Command.run(['sgdisk', '-h', ':'.join(partition_ids), self.disk_device]) def set_mbr(self) -> None: """ Turn partition table into MBR (msdos table) """ partition_ids = [] efi_partition_number = None for number in range(1, self.partition_id + 1): if self.partition_map.get(number): partition_info = Command.run( [ 'sgdisk', '-i={0}'.format(self.partition_map[number]), self.disk_device, ] ) if '(EFI System)' in partition_info.output: efi_partition_number = self.partition_map[number] partition_ids.append(format(self.partition_map[number])) if efi_partition_number: # turn former EFI partition into standard linux partition self.set_flag(efi_partition_number, 't.linux') Command.run(['sgdisk', '-m', ':'.join(partition_ids), self.disk_device]) def resize_table(self, entries: int = 128) -> None: """ Resize partition table :param int entries: number of default entries """ Command.run( [ 'sgdisk', '--resize-table', '{0}'.format(entries), self.disk_device, ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/partitioner/msdos.py0000644000000000000000000002406415273675667014533 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import logging from typing import List, Optional, Union # project from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.partitioner.base import PartitionerBase from kiwi.utils.block import BlockID from kiwi.exceptions import KiwiPartitionerMsDosFlagError log = logging.getLogger('kiwi') class PartitionerMsDos(PartitionerBase): """ **Implement old style msdos partition setup** """ def post_init(self) -> None: """ Post initialization method Setup sfdisk partition type/flag map """ self.default_start = 2048 self.sector_size = 512 self.flag_map = { 'f.active': True, 't.linux': '83', 't.swap': '82', 't.lvm': '8e', 't.raid': 'fd', 't.efi': None, 't.csm': None, 't.prep': '41', 't.extended': '5', } def create( self, name: str, mbsize: Union[int, str], type_name: str, flags: List[str] = [], partition_id: Optional[int] = None, ) -> None: """ Create msdos partition :param string name: partition name :param int mbsize: partition size :param string type_name: partition type :param list flags: additional flags :param int partition_id: If provided, use this exact partition ID instead of auto-incrementing. """ if self.extended_layout: if self.partition_count < 3: # in primary boundary self._create_primary( name, mbsize, type_name, flags, partition_id ) elif self.partition_count == 3: # at primary boundary, create extended + logical self._create_extended(name) self._create_logical( name, mbsize, type_name, flags, partition_id ) elif self.partition_count > 3: # in logical boundary self._create_logical( name, mbsize, type_name, flags, partition_id ) else: self._create_primary(name, mbsize, type_name, flags, partition_id) def set_flag(self, partition_id: Union[int, str], flag_name: str) -> None: """ Set msdos partition flag :param int partition_id: partition number :param string flag_name: name from flag map """ partition_id = partition_id if partition_id else self.partition_id if flag_name not in self.flag_map: raise KiwiPartitionerMsDosFlagError( f'Unknown partition flag {flag_name}' ) flag_val = self.flag_map[flag_name] if flag_val: if flag_name == 'f.active': Command.run( [ 'sfdisk', '--activate', self.disk_device, format(partition_id), ] ) elif isinstance(flag_val, str): Command.run( [ 'sfdisk', '-c', self.disk_device, format(partition_id), flag_val, ] ) else: log.warning('Flag %s ignored on msdos', flag_name) def resize_table(self, entries: Optional[int] = None) -> None: """ Resize partition table Nothing to be done here for msdos table :param int entries: unused """ pass def set_uuid(self, partition_id: Union[int, str], uuid: str) -> None: """ Set partition UUID Nothing to be done here for MSDOS devices :param int partition_id: unused :param string uuid: unused """ pass # pragma: nocover def set_start_sector(self, start_sector: int) -> None: """ Set start sector of first partition as configured. fdisk and friends are not able to work correctly if the start sector of the first partition is any different from 2048. :param int start_sector: sector size """ block_operation = BlockID(self.disk_device) fdisk_input = Temporary().new_file() with open(fdisk_input.name, 'w') as partition: if block_operation.get_partition_count() >= 4: # if the partition table is full fdisk does not ask # for the partition number when there is only one choice log.debug(f'fdisk: d 1 n p {start_sector} w q') partition.write( 'd\n1\nn\np\n{0}\n\nw\nq\n'.format(start_sector) ) else: # if the partition table has less than 4 partitions # fdisk will ask for the partition number to change log.debug(f'fdisk: d 1 n p 1 {start_sector} w q') partition.write( 'd\n1\nn\np\n1\n{0}\n\nw\nq\n'.format(start_sector) ) self._call_fdisk(fdisk_input.name) def _create_primary( self, name: str, mbsize: Union[int, str], type_name: str, flags: List[str] = [], partition_id: Optional[int] = None, ) -> None: """ Create primary msdos partition """ self.partition_id = ( partition_id if partition_id else self.partition_id + 1 ) self.partition_count += 1 fdisk_input = Temporary().new_file() if self.partition_id == 1 and self.start_sector: if self.start_sector > self.default_start and mbsize != 'all_free': # fdisk default start sector is self.default_start, increase # the partition size such that after set_start_sector() # the requested partition size is present. The size # can also be given as numeric string, e.g for clones mbsize = int(mbsize) + int( (self.start_sector - self.default_start) * self.sector_size / (1024 * 1024) ) with open(fdisk_input.name, 'w') as partition: log.debug( '%s: fdisk: n p %d cur_position +%sM w q', name, self.partition_id, format(mbsize), ) partition.write( 'n\np\n{0}\n\n{1}\nw\nq\n'.format( self.partition_id, '' if mbsize == 'all_free' else '+{0}M'.format(mbsize), ) ) self._call_fdisk(fdisk_input.name) self._set_all_flags(type_name, flags) def _create_extended( self, name: str, partition_id: Optional[int] = None ) -> None: """ Create extended msdos partition """ self.partition_id = ( partition_id if partition_id else self.partition_id + 1 ) self.partition_count += 1 fdisk_input = Temporary().new_file() with open(fdisk_input.name, 'w') as partition: log.debug( '%s: fdisk: n e %d cur_position +all_freeM w q', name, self.partition_id, ) partition.write( 'n\ne\n{0}\n{1}\n{2}\nw\nq\n'.format(self.partition_id, '', '') ) self._call_fdisk(fdisk_input.name) def _create_logical( self, name: str, mbsize: Union[int, str], type_name: str, flags: List[str] = [], partition_id: Optional[int] = None, ) -> None: """ Create logical msdos partition """ self.partition_id = ( partition_id if partition_id else self.partition_id + 1 ) self.partition_count += 1 fdisk_input = Temporary().new_file() with open(fdisk_input.name, 'w') as partition: log.debug( '%s: fdisk: n %d cur_position +%sM w q', name, self.partition_id, format(mbsize), ) partition.write( 'n\n{0}\n{1}\n{2}\nw\nq\n'.format( self.partition_id, '', '' if mbsize == 'all_free' else '+{0}M'.format(mbsize), ) ) self._call_fdisk(fdisk_input.name) self._set_all_flags(type_name, flags) def _set_all_flags(self, type_name: str, flags: List[str]) -> None: self.set_flag(self.partition_id, type_name) if flags: for flag_name in flags: self.set_flag(self.partition_id, flag_name) def _call_fdisk(self, fdisk_config_file_path: str) -> None: bash_command = ' '.join( ['cat', fdisk_config_file_path, '|', 'fdisk', self.disk_device] ) try: Command.run(['bash', '-c', bash_command]) except Exception: # unfortunately fdisk reports that it can't read in the partition # table which I consider a bug in fdisk. However the table was # correctly created and therefore we continue. Problem is that we # are not able to detect real errors with the fdisk operation at # that point. log.debug('potential fdisk errors were ignored') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/path.py0000644000000000000000000002014515273675667011776 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging import collections import pathlib import shutil from typing import Any, Dict, List, MutableMapping, Optional # project from kiwi.command import Command from kiwi.exceptions import KiwiFileAccessError log = logging.getLogger('kiwi') class Path: """ **Directory path helpers** """ @staticmethod def sort_by_hierarchy(path_list: List[str]) -> List[str]: """ Sort given list of path names by their hierachy in the tree Example: .. code:: python result = Path.sort_by_hierarchy(['/var/lib', '/var']) :param list path_list: list of path names :return: hierachy sorted path_list :rtype: list """ paths_at_depth: Dict[int, List[str]] = {} for path in path_list: path_elements = path.split('/') path_depth = len(path_elements) if path_depth not in paths_at_depth: paths_at_depth[path_depth] = [] paths_at_depth[path_depth].append(path) ordered_paths_at_depth = collections.OrderedDict( sorted(paths_at_depth.items()) ) ordered_paths = [] for path_depth in ordered_paths_at_depth: for path in ordered_paths_at_depth[path_depth]: ordered_paths.append(path) return ordered_paths @staticmethod def access(path: str, mode: int, **kwargs: Any) -> bool: """ Check whether path can be accessed with the given mode. :param str path: The path that should be checked for access. :param int mode: Which access mode should be checked. This value must be a bit-wise or of one or more of the following constants: :py:const:`os.F_OK` (note that this one is zero), :py:const:`os.X_OK`, :py:const:`os.R_OK` and :py:const:`os.W_OK` :param kwargs: further keyword arguments are forwarded to :func:`os.access` :return: Boolean value whether this access mode is allowed :rtype: bool :raises ValueError: if the supplied mode is invalid :raises kiwi.exceptions.KiwiFileNotFound: if the path does not exist or is not accessible by the current user """ if mode & ~(os.F_OK | os.X_OK | os.R_OK | os.W_OK) != 0: raise ValueError('Invalid mode 0x{:X}'.format(mode)) try: os.stat(path) except Exception as exc: raise KiwiFileAccessError( 'Error accessing path {0} failed with: {1}'.format(path, exc) ) return os.access(path, mode, **kwargs) @staticmethod def create(path: str) -> None: """ Create path and all sub directories to target :param string path: path name """ log.debug("Creating directory %s", path) try: pathlib.Path(path).mkdir(parents=True, exist_ok=True) except Exception as issue: raise KiwiFileAccessError( f'Cannot create directory: {path}: {issue}' ) @staticmethod def wipe(path: str) -> None: """ Delete path and all contents :param string path: path name """ if os.path.exists(path): Command.run(['rm', '-r', '-f', path]) @staticmethod def remove_hierarchy(root: str, path: str) -> None: """ Recursively remove an empty path and its sub directories starting at a given root directory. Ignore non empty or protected paths and leave them untouched :param string root: start at directory :param string path: path name below root """ Command.run( [ 'rmdir', '--ignore-fail-on-non-empty', os.path.normpath(os.sep.join([root, path])), ] ) path_elements = path.split(os.sep) protected_elements = [ 'boot', 'dev', 'proc', 'run', 'sys', 'tmp', 'home', 'mnt', ] for path_index in reversed(range(0, len(path_elements))): sub_path = os.sep.join(path_elements[0:path_index]) if sub_path: if path_elements[path_index - 1] in protected_elements: log.warning( 'remove_hierarchy: path {0} is protected'.format( os.path.normpath(os.sep.join([root, sub_path])) ) ) return Command.run( [ 'rmdir', '--ignore-fail-on-non-empty', os.path.normpath(os.sep.join([root, sub_path])), ] ) @staticmethod def move_to_root(root: str, elements: List[str]) -> List[str]: """ Change the given path elements to a new root directory :param str root: the root path to trim :param list elements: list of path names :return: changed elements :rtype: list """ result = [] for element in elements: normalized_element = os.path.normpath(element) result.append( normalized_element.replace( os.path.normpath(root), os.sep ).replace('{0}{0}'.format(os.sep), os.sep) ) return result @staticmethod def rebase_to_root(root: str, elements: List[str]) -> List[str]: """ Include the root prefix for the given paths elements :param str root: the new root path :param list elements: list of path names :return: changed elements :rtype: list """ result = [] for element in elements: result.append(os.path.normpath(os.sep.join([root, element]))) return result @staticmethod def which( filename: str, custom_env: Optional[MutableMapping[str, str]] = None, access_mode: int = os.F_OK | os.X_OK, root_dir: Optional[str] = None, ) -> Optional[str]: """ Lookup file name in PATH :param string filename: file base name :param list alternative_lookup_paths: list of additional lookup paths :param list custom_env: a custom os.environ used to obtain ``$PATH`` :param int access_mode: one of the os access modes or a combination of them (os.R_OK, os.W_OK and os.X_OK). If the provided access mode does not match the file is considered not existing :param str root_dir: the root path to look at :return: absolute path to file or None :rtype: str """ system_path = ( custom_env.get("PATH") if custom_env else os.environ.get("PATH") ) or os.defpath lookup_paths = system_path.split(os.pathsep) if root_dir: lookup_paths = Path.rebase_to_root(root_dir, lookup_paths) log.debug(f"Looking for {filename} in {os.pathsep.join(lookup_paths)}") return shutil.which( filename, access_mode, path=os.pathsep.join(lookup_paths) ) @staticmethod def first_exists(path: str) -> str: """ Lookup first path that exists in the given path hierarchy """ p = pathlib.Path(path) if p.exists(): return path else: return Path.first_exists(format(p.parent)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/privileges.py0000644000000000000000000000231315273675667013210 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os # project from .exceptions import KiwiPrivilegesError class Privileges: """ **Implements check for root privileges** """ @staticmethod def check_for_root_permissions() -> bool: """ Check if we are effectively root on the system. If not an exception is thrown :return: True or raise an Exception :rtype: bool """ if os.geteuid() != 0: raise KiwiPrivilegesError('operation requires root permissions') return True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7472916 kiwi-11.1.8/kiwi/repository/__init__.py0000644000000000000000000000463115273675667015022 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import importlib from typing import TYPE_CHECKING, List, Optional from abc import ABCMeta, abstractmethod # project from kiwi.exceptions import KiwiRepositorySetupError if TYPE_CHECKING: # pragma: nocover from kiwi.repository.base import RepositoryBase class Repository(metaclass=ABCMeta): """ **Repository factory** :param object root_bind: instance of RootBind :param str package_manager: package manager name :param list custom_args: list of custom package manager arguments to setup the repository :raises KiwiRepositorySetupError: if package_manager is not supported """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new( root_bind: object, package_manager: str, custom_args: Optional[List] = None, ) -> 'RepositoryBase': name_map = { 'zypper': ['zypper', 'Zypper'], 'dnf': ['dnf', 'Dnf'], 'dnf5': ['dnf5', 'Dnf5'], 'dnf4': ['dnf4', 'Dnf4'], 'microdnf': ['dnf4', 'Dnf4'], 'apt': ['apt', 'Apt'], 'pacman': ['pacman', 'Pacman'], 'apk': ['apk', 'Apk'], } try: repository = importlib.import_module( 'kiwi.repository.{0}'.format(name_map[package_manager][0]) ) module_name = 'Repository{0}'.format(name_map[package_manager][1]) return repository.__dict__[module_name](root_bind, custom_args) except Exception as issue: raise KiwiRepositorySetupError( 'Support for {0} repository not implemented: {1}'.format( package_manager, issue ) ) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/apk.py0000644000000000000000000001173115273675667014035 0ustar00# Copyright (c) 2025 SUSE LLC. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from typing import List, Dict, Optional # project from kiwi.defaults import Defaults from kiwi.repository.base import RepositoryBase from kiwi.path import Path from kiwi.utils.proxy import HostProxy log = logging.getLogger('kiwi') class RepositoryApk(RepositoryBase): """ **Implements repository handling for apk (Alpine) package manager** """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom apk arguments and create runtime configuration and environment :param list custom_args: apk arguments """ self.bootstrap_repo = '' self.custom_args = custom_args self.repo_dict: Dict[str, str] = {} self.signing_keys: List = [] self.repo_config = os.sep.join( [self.root_dir, Defaults.get_apk_repo_config()] ) self.command_env = self._create_apk_get_runtime_environment() Path.create(os.path.dirname(self.repo_config)) def setup_package_database_configuration(self) -> None: """ Setup package database configuration No special database configuration required for apk """ pass def use_default_location(self) -> None: """ Call repository operations with default repository manager setup No special database configuration required for apk """ pass def runtime_config(self) -> Dict: """ apk runtime configuration and environment """ return { 'bootstrap_repo': self.bootstrap_repo, 'command_env': self.command_env, } def add_repo( self, name: str, uri: str, repo_type: str = 'apk', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: Optional[bool] = None, pkg_gpgcheck: Optional[bool] = None, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add apk repository :param str name: repository name :param str uri: repository URI :param str repo_type: unused :param int prio: unused :param str dist: unused :param str components: unused :param str user: unused :param str secret: unused :param str credentials_file: unused :param bool repo_gpgcheck: unused :param bool pkg_gpgcheck: unused :param str sourcetype: unused :param str customization_script: unused :param str architectures: unused """ self.repo_dict[name] = uri if not self.bootstrap_repo: # The first repo added is used for bootstrap self.bootstrap_repo = uri with open(self.repo_config, 'a') as repos: repos.write(f'{uri}{os.linesep}') def import_trusted_keys(self, signing_keys: List) -> None: """ Imports trusted keys into the image :param list signing_keys: list of the key files to import """ pass def delete_repo(self, name: str) -> None: """ Delete apk repository :param str name: repository base file name """ if self.repo_dict.get(name): self.delete_all_repos() del self.repo_dict[name] for name in self.repo_dict: self.add_repo(name, self.repo_dict[name]) def delete_all_repos(self) -> None: """ Delete all apk repositories """ if os.path.isfile(self.repo_config): os.unlink(self.repo_config) def delete_repo_cache(self, name: str) -> None: """ Delete repository cache No special cache cleanup required for apk :param str name: unused """ pass def cleanup_unused_repos(self) -> None: """ Cleanup/Delete unused repositories No special cleanup required for apk """ pass def _create_apk_get_runtime_environment(self) -> Dict: return dict(os.environ, **HostProxy().get_env(), LANG='C') ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/apt.py0000644000000000000000000005424715273675667014057 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import glob import shutil import hashlib import logging from base64 import b64encode from urllib.parse import urlparse, unquote from urllib.request import Request, urlopen from typing import List, Dict, Optional # project import kiwi.defaults as defaults from kiwi.utils.temporary import Temporary, TmpT from kiwi.repository.template.apt import PackageManagerTemplateAptGet from kiwi.repository.base import RepositoryBase from kiwi.path import Path from kiwi.utils.toenv import ToEnv from kiwi.system.uri import Uri from kiwi.utils.proxy import HostProxy from kiwi.exceptions import KiwiUriOpenError log = logging.getLogger('kiwi') class RepositoryApt(RepositoryBase): """ **Implements repository handling for apt-get package manager** :param str shared_apt_get_dir: shared directory between image root and build system root :param str runtime_apt_get_config_file: apt-get runtime config file name :param list apt_get_args: apt-get caller arguments :param dict command_env: customized os.environ for apt-get :param manager_base: shared location for apt-get repodata between host and image :param str keyrings_dir: location of the repository keyring files :param str keyring_prefix: build specific keyring file name prefix :param list keyrings: keyring files referenced via Signed-By :param str image_keyrings_dir: location of the keyring files permanently stored in the image :param dict repository_keyrings: keyring files referenced via Signed-By per repository name :param list repo_files: sources, auth and preferences files written for this build """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom apt-get arguments and create runtime configuration and environment :param list custom_args: apt-get arguments """ self.runtime_apt_get_config_file = TmpT(name='') self.custom_args = custom_args self.exclude_docs = False # delete custom arguments not used for apt for argument in self.custom_args: if '_install_langs' in argument or '_target_arch' in argument: log.warning( f'Custom argument {argument} will be ignored for apt' ) self.custom_args.remove(argument) # extract custom arguments used for apt config only if 'exclude_docs' in self.custom_args: self.custom_args.remove('exclude_docs') self.exclude_docs = True if 'check_signatures' in self.custom_args: self.custom_args.remove('check_signatures') self.unauthenticated = 'false' else: self.unauthenticated = 'true' self.distribution: str = '' self.distribution_path: str = '' self.repo_files: List[str] = [] self.components: List = [] # apt-get support is based on creating a sources file which # contains path names to the repo and its cache. In order to # allow a persistent use of the files in and outside of a chroot # call an active bind mount from RootBind::mount_shared_directory # is expected and required self.manager_base = self.shared_location + '/apt-get' self.shared_apt_get_dir = { 'sources-dir': self.manager_base + '/sources.list.d', 'netrcparts-dir': self.manager_base + '/auth.conf.d', 'preferences-dir': self.manager_base + '/preferences.d', } # Each signing key is stored as keyring file in keyrings_dir # and referenced via Signed-By from the repository sources # files. apt is called on the host for the bootstrap phase and # chrooted for the image build phase. Thus the keyring files # must exist at the same location on the host and in the image # root. The build specific prefix prevents concurrent builds # from using each others keyring files on the host self.keyrings_dir = '/etc/apt/keyrings' self.keyring_prefix = 'kiwi-{0}-'.format( hashlib.sha256( os.path.realpath(self.root_dir).encode() ).hexdigest()[:8] ) self.keyrings: List[str] = [] # Signing keys of repositories configured in the image are # stored permanently as recommended for third party repositories # in https://wiki.debian.org/DebianRepository/UseThirdParty self.image_keyrings_dir = '/usr/share/keyrings' self.repository_keyrings: Dict[str, List[str]] = {} # former kiwi versions created a global keyring in the # shared location, which would still be trusted by apt legacy_keyring = '{}/trusted.gpg'.format(self.manager_base) if os.path.exists(legacy_keyring): os.unlink(legacy_keyring) self.runtime_apt_get_config_file = Temporary( path=self.root_dir, prefix='kiwi_apt.config' ).unmanaged_file() self.apt_get_args = [ '-q', '-c', self.runtime_apt_get_config_file.name, '-y', ] + self.custom_args self.command_env = self._create_apt_get_runtime_environment() # config file for apt-get tool self.apt_conf = PackageManagerTemplateAptGet() self._write_runtime_config() def setup_package_database_configuration(self) -> None: """ Setup package database configuration No special database configuration required for apt """ pass def use_default_location(self) -> None: """ Setup apt-get repository operations to store all data in the default places """ self.manager_base = os.sep.join([self.root_dir, 'etc/apt']) self.shared_apt_get_dir['sources-dir'] = os.sep.join( [self.manager_base, 'sources.list.d'] ) self.shared_apt_get_dir['preferences-dir'] = os.sep.join( [self.manager_base, 'preferences.d'] ) self.shared_apt_get_dir['netrcparts-dir'] = os.sep.join( [self.manager_base, 'auth.conf.d'] ) self._write_runtime_config(system_default=True) def runtime_config(self) -> Dict: """ apt-get runtime configuration and environment """ return { 'apt_get_args': self.apt_get_args, 'command_env': self.command_env, 'distribution': self.distribution, 'distribution_path': self.distribution_path, } def add_repo( self, name: str, uri: str, repo_type: str = 'deb', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: Optional[bool] = None, pkg_gpgcheck: Optional[bool] = None, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add apt_get repository :param str name: repository base file name :param str uri: repository URI :param str repo_type: unused :param int prio: apt Pin-Priority of the repository, see apt_preferences(5) :param str dist: distribution name for non flat deb repos :param str components: distribution categories :param str user: username :param str secret: password_or_token :param str credentials_file: unused :param bool repo_gpgcheck: enable repository signature validation :param bool pkg_gpgcheck: unused :param str sourcetype: unused :param str customization_script: custom script called after the repo file was created :param str architectures: identifies which architectures are supported by this repository """ sources_file = '/'.join( [self.shared_apt_get_dir['sources-dir'], name + '.sources'] ) auth_file = '/'.join( [self.shared_apt_get_dir['netrcparts-dir'], name + '.conf'] ) pref_file = '/'.join( [self.shared_apt_get_dir['preferences-dir'], name + '.pref'] ) self.repo_files.append(sources_file) if os.path.exists(uri): # apt-get requires local paths to take the file: type uri = 'file:/' + uri uri = uri.replace('file://', 'file:/') if not components: components = 'main' self._add_components(components) with open(sources_file, 'w') as repo: repo_details = 'Types: deb' + os.linesep repo_details += 'URIs: ' + uri + os.linesep if architectures: repo_details += 'Architectures: {}{}'.format( architectures.replace(',', ' '), os.linesep ) if not dist: # create a debian flat repository setup. We consider the # repository metadata to exist on the toplevel of the # specified uri. This applies to the way the open build # service creates debian repositories and should be # done in the same way for other repositories when used # with kiwi repo_details += 'Suites: ./' + os.linesep else: # create a debian distributon repository setup for the # specified distributon name and components self.distribution = dist self.distribution_path = uri repo_details += 'Suites: ' + dist + os.linesep repo_details += 'Components: ' + components + os.linesep keyrings = self.repository_keyrings.get(name) or self.keyrings if keyrings: repo_details += 'Signed-By: {0}{1}'.format( ' '.join(keyrings), os.linesep ) if repo_gpgcheck is False: repo_details += 'trusted: yes' + os.linesep repo_details += 'check-valid-until: no' + os.linesep repo.write(repo_details) if secret: self.repo_files.append(auth_file) with open(auth_file, 'w') as auth: if user: auth.write( 'machine {} login {} password {}{}'.format( uri, user, secret, os.linesep ) ) else: auth.write( 'machine {} login {}{}'.format(uri, secret, os.linesep) ) if customization_script: self.run_repo_customize(customization_script, sources_file) if prio: self.repo_files.append(pref_file) uri_parsed = urlparse(uri.replace('file://', 'file:/')) with open(pref_file, 'w') as pref: pref.write('Package: *{0}'.format(os.linesep)) if not uri_parsed.hostname: pref.write('Pin: origin ""{0}'.format(os.linesep)) else: pref.write( 'Pin: origin "{0}"{1}'.format( uri_parsed.hostname, os.linesep ) ) pref.write('Pin-Priority: {0}{1}'.format(prio, os.linesep)) if customization_script: self.run_repo_customize(customization_script, pref_file) def import_trusted_keys(self, signing_keys: List) -> None: """ Stores each provided key as keyring file in /etc/apt/keyrings on the host and in the image root. The keyring files are referenced via Signed-By from the repository sources files added afterwards :param list signing_keys: list of the key files to import. A key can be a local file path or a remote http, https or ftp location. Remote keys are downloaded prior to the import :raises KiwiUriOpenError: if the download of a remote key fails """ self.delete_trusted_keys() with Temporary(prefix='kiwi_apt_keys.').new_dir() as key_dir: for index, key in enumerate(signing_keys): key = self._fetch_key(key, key_dir, index) keyring_name = '{0}{1}.{2}'.format( self.keyring_prefix, index, self._get_keyring_extension(key) ) for keyrings_dir in self._get_keyrings_dirs(): self._install_keyring(key, keyrings_dir, keyring_name) keyring = os.sep.join([self.keyrings_dir, keyring_name]) self.keyrings.append(keyring) log.info(f'Keyring for APT created: {keyring}') def import_repository_signing_keys( self, name: str, signing_keys: List ) -> None: """ Permanently stores the signing keys of the repository with the given name as keyring files in /usr/share/keyrings of the image root. The keyring files are referenced via Signed-By from the sources file of this repository added afterwards :param str name: repository base file name :param list signing_keys: list of the key files to store. A key can be a local file path or a remote http, https or ftp location. Remote keys are downloaded prior to the import :raises KiwiUriOpenError: if the download of a remote key fails """ image_keyrings_dir = os.path.normpath( os.sep.join([self.root_dir, self.image_keyrings_dir]) ) keyring_basename = f'{name}-archive-keyring' # delete keyring files of a former import for this repository for keyring in glob.glob( os.sep.join([image_keyrings_dir, f'{keyring_basename}*']) ): os.unlink(keyring) keyrings = [] with Temporary(prefix='kiwi_apt_keys.').new_dir() as key_dir: for index, key in enumerate(signing_keys): key = self._fetch_key(key, key_dir, index) keyring_name = '{0}{1}.{2}'.format( keyring_basename, f'-{index}' if index else '', self._get_keyring_extension(key), ) self._install_keyring(key, image_keyrings_dir, keyring_name) keyring = os.sep.join([self.image_keyrings_dir, keyring_name]) keyrings.append(keyring) log.info(f'--> Keyring: {keyring}') self.repository_keyrings[name] = keyrings def delete_trusted_keys(self) -> None: """ Delete the keyring files created by import_trusted_keys on the host and in the image root """ for keyrings_dir in self._get_keyrings_dirs(): self._delete_keyrings(keyrings_dir) self.keyrings = [] def _fetch_key(self, key: str, key_dir: str, index: int) -> str: if urlparse(key).scheme in ('http', 'https', 'ftp'): key = self._download_key( key, os.sep.join([key_dir, f'key.{index}']) ) return key @staticmethod def _get_keyring_extension(key: str) -> str: # apt expects ASCII armored keys to use the .asc # extension, any other key file is used as binary keyring with open(key, 'rb') as key_file: is_armored = ( b'-----BEGIN PGP PUBLIC KEY BLOCK-----' in key_file.read() ) return 'asc' if is_armored else 'gpg' @staticmethod def _install_keyring( key: str, keyrings_dir: str, keyring_name: str ) -> None: Path.create(keyrings_dir) target = os.sep.join([keyrings_dir, keyring_name]) shutil.copy(key, target) # apt verifies signatures as _apt user os.chmod(target, 0o644) @staticmethod def _download_key(key_url: str, target: str) -> str: log.info(f'Downloading signing key: {Uri.print_sensitive(key_url)}') uri = urlparse(key_url) request = Request(key_url) if uri.username and uri.scheme != 'ftp': # urllib handles credentials as part of the URL for # ftp only, pass them as basic auth header for http(s) netloc = uri.netloc.rpartition('@')[2] request = Request(uri._replace(netloc=netloc).geturl()) credentials = b64encode( ':'.join( [unquote(uri.username), unquote(uri.password or '')] ).encode() ).decode() request.add_header('Authorization', f'Basic {credentials}') try: with urlopen(request) as location: with open(target, 'wb') as key_file: key_file.write(location.read()) except Exception as issue: raise KiwiUriOpenError( 'Failed to download signing key {0}: {1}: {2}'.format( Uri.print_sensitive(key_url), type(issue).__name__, issue ) ) return target def delete_repo(self, name: str) -> None: """ Delete apt-get repository :param str name: repository base file name """ Path.wipe( self.shared_apt_get_dir['sources-dir'] + '/' + name + '.sources' ) Path.wipe( self.shared_apt_get_dir['netrcparts-dir'] + '/' + name + '.conf' ) Path.wipe( self.shared_apt_get_dir['preferences-dir'] + '/' + name + '.pref' ) def delete_all_repos(self) -> None: """ Delete all apt-get repositories """ for dirname in ['sources-dir', 'netrcparts-dir', 'preferences-dir']: Path.wipe(self.shared_apt_get_dir[dirname]) Path.create(self.shared_apt_get_dir[dirname]) def delete_repo_cache(self, name: str) -> None: """ Delete apt-get repository cache Apt stores the package cache in a collection of binary files and deb archives. As of now I couldn't came across a solution which allows for deleting only the cache data for a specific repository. Thus the repo cache cleanup affects all cache data :param str name: unused """ for cache_file in ['archives', 'pkgcache.bin', 'srcpkgcache.bin']: Path.wipe(os.sep.join([self.manager_base, cache_file])) def cleanup_unused_repos(self) -> None: """ Delete unused apt_get repositories Repository sources, auth and preferences files which were not written for this build must be removed otherwise they are taken into account for the package installations. This also applies to e.g. a preferences file of a repository which was set up with a priority in a former build but is set up without a priority in this build """ for dirname in ['sources-dir', 'netrcparts-dir', 'preferences-dir']: config_dir = self.shared_apt_get_dir[dirname] for config_file in list(os.walk(config_dir))[0][2]: config_file = '/'.join([config_dir, config_file]) if config_file not in self.repo_files: Path.wipe(config_file) def _add_components(self, components: str) -> None: for component in components.split(): if component not in self.components: self.components.append(component) def _create_apt_get_runtime_environment(self) -> Dict: for apt_get_dir in list(self.shared_apt_get_dir.values()): Path.create(apt_get_dir) ToEnv(self.root_dir, defaults.PACKAGE_MANAGER_ENV_VARS) return dict( os.environ, **HostProxy().get_env(), LANG='C', DEBIAN_FRONTEND='noninteractive', ) def _write_runtime_config(self, system_default: bool = False) -> None: parameters = { 'apt_shared_base': self.manager_base, 'unauthenticated': self.unauthenticated, } if not system_default: template = self.apt_conf.get_host_template(self.exclude_docs) apt_conf_data = template.substitute(parameters) else: template = self.apt_conf.get_image_template(self.exclude_docs) apt_conf_data = template.substitute(parameters) # The runtime config redirects Dir::Etc away from the host # apt configuration. Thus a proxy setup in the host apt # configuration needs to be added explicitly for proxy_config in HostProxy.get_apt_config(): apt_conf_data += proxy_config + os.linesep with open(self.runtime_apt_get_config_file.name, 'w') as config: config.write(apt_conf_data) def _get_keyrings_dirs(self) -> List[str]: return [ self.keyrings_dir, os.path.normpath(os.sep.join([self.root_dir, self.keyrings_dir])), ] def _delete_keyrings(self, keyrings_dir: str) -> None: for keyring in glob.glob( os.sep.join([keyrings_dir, f'{self.keyring_prefix}*']) ): os.unlink(keyring) def cleanup(self) -> None: """ Delete intermediate apt config file and the keyring files on the host. The keyring files in the image root are still referenced by the repository sources files and are deleted via delete_trusted_keys at the end of the build """ if os.path.isfile(self.runtime_apt_get_config_file.name): os.unlink(self.runtime_apt_get_config_file.name) self._delete_keyrings(self.keyrings_dir) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/base.py0000644000000000000000000001367015273675667014200 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from types import TracebackType from typing import List, Dict, Optional, Type from kiwi.system.root_bind import RootBind from kiwi.command import Command class RepositoryBase: """ Implements base class for package manager repository handling Attributes :param object root_bind: instance of :class:`RootBind` :param str root_dir: root directory path name :param str shared_location: shared directory between image root and build system root """ def __init__(self, root_bind: RootBind, custom_args: List = []) -> None: self.root_bind = root_bind self.root_dir = root_bind.root_dir self.shared_location = root_bind.shared_location self.post_init(custom_args or []) def __enter__(self) -> 'RepositoryBase': return self def post_init(self, custom_args: List = []) -> None: """ Post initialization method Implementation in specialized repository class :param list custom_args: unused """ pass def use_default_location(self) -> None: """ Call repository operations with default repository manager setup Implementation in specialized repository class """ raise NotImplementedError def runtime_config(self) -> Dict: """ Repository runtime configuration and environment Implementation in specialized repository class """ raise NotImplementedError def add_repo( self, name: str, uri: str, repo_type: str, prio: int, dist: str, components: str, user: str, secret: str, credentials_file: str, repo_gpgcheck: bool, pkg_gpgcheck: bool, sourcetype: str, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add repository Implementation in specialized repository class :param str name: unused :param str uri: unused :param str repo_type: unused :param int prio: unused :param str dist: unused :param str components: unused :param str user: unused :param str secret: unused :param str credentials_file: unused :param bool repo_gpgcheck: unused :param bool pkg_gpgcheck: unused :param str sourcetype: unused :param str customization_script: unused :param str architectures: unused """ raise NotImplementedError def setup_package_database_configuration(self) -> None: """ Setup package database configuration Implementation in specialized repository class """ raise NotImplementedError def import_trusted_keys(self, signing_keys: List) -> None: """ Imports trusted keys into the image Implementation in specialized repository class :param list signing_keys: list of the key files to import """ raise NotImplementedError def import_repository_signing_keys( self, name: str, signing_keys: List ) -> None: """ Permanently stores the signing keys of the repository with the given name in the image, such that the repository added afterwards via add_repo can be validated in the image. The default implementation does nothing. Package managers which keep the keys imported via import_trusted_keys as part of the image, e.g. in the rpm database, don't need this. :param str name: repository base file name :param list signing_keys: list of the key files to store """ pass def cleanup_unused_repos(self) -> None: """ Cleanup/Delete unused repositories Only configured repositories according to the image configuration are allowed to be active when building Implementation in specialized repository class """ raise NotImplementedError def delete_repo(self, name: str) -> None: """ Delete repository Implementation in specialized repository class :param str name: unused """ raise NotImplementedError def delete_all_repos(self) -> None: """ Delete all repositories Implementation in specialized repository class """ raise NotImplementedError def delete_repo_cache(self, name: str) -> None: """ Delete repository cache Implementation in specialized repository class :param str name: unused """ raise NotImplementedError @staticmethod def run_repo_customize(script_path: str, repo_file: str) -> None: """ Run an optional customization script :param str script_path: unused :param str repo_file: unused """ Command.run(['bash', '--norc', script_path, repo_file]) def cleanup(self) -> None: """ Cleanup method Implementation in specialized repository class """ pass # pragma: no cover def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/dnf.py0000644000000000000000000000553515273675667014036 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # from typing import List, Dict, no_type_check, Optional # project from kiwi.repository.base import RepositoryBase from kiwi.api_helper import decommissioned class RepositoryDnf(RepositoryBase): """ decommissioned """ @decommissioned def post_init(self, custom_args: List = []) -> None: pass # pragma: no cover @decommissioned def setup_package_database_configuration(self) -> None: pass # pragma: no cover @decommissioned def use_default_location(self) -> None: pass # pragma: no cover @no_type_check @decommissioned def runtime_config(self) -> Dict: pass # pragma: no cover @decommissioned def add_repo( self, name: str, uri: str, repo_type: str = 'rpm-md', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, ) -> None: pass # pragma: no cover @decommissioned def import_trusted_keys(self, signing_keys: List) -> None: pass # pragma: no cover @decommissioned def delete_repo(self, name: str) -> None: pass # pragma: no cover @decommissioned def delete_all_repos(self) -> None: pass # pragma: no cover @decommissioned def delete_repo_cache(self, name: str) -> None: pass # pragma: no cover @decommissioned def cleanup_unused_repos(self) -> None: pass # pragma: no cover @no_type_check @decommissioned def _create_dnf_runtime_environment(self) -> Dict: pass # pragma: no cover @decommissioned def _create_runtime_config_parser(self) -> None: pass # pragma: no cover @decommissioned def _create_runtime_plugin_config_parsers(self) -> None: pass # pragma: no cover @decommissioned def _write_runtime_config(self) -> None: pass # pragma: no cover ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/dnf4.py0000644000000000000000000003252215273675667014116 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import glob from configparser import ConfigParser from typing import List, Dict, Optional # project import kiwi.defaults as defaults from kiwi.utils.temporary import Temporary, TmpT from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.repository.base import RepositoryBase from kiwi.path import Path from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.toenv import ToEnv from kiwi.utils.proxy import HostProxy class RepositoryDnf4(RepositoryBase): """ **Implements repository handling for dnf package manager** :param str shared_dnf_dir: shared directory between image root and build system root :param str runtime_dnf_config_file: dnf runtime config file name :param dict command_env: customized os.environ for dnf :param str runtime_dnf_config: instance of :class:`ConfigParser` """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom dnf arguments and create runtime configuration and environment :param list custom_args: dnf arguments """ self.runtime_dnf_config_file = TmpT(name='') self.custom_args = custom_args self.exclude_docs = False self.locale = None self.target_arch = None # extract custom arguments not used in dnf call if 'exclude_docs' in self.custom_args: self.custom_args.remove('exclude_docs') self.exclude_docs = True if 'check_signatures' in self.custom_args: self.custom_args.remove('check_signatures') self.gpg_check = '1' else: self.gpg_check = '0' for argument in self.custom_args: if '_install_langs' in argument: self.locale = argument elif '_target_arch' in argument: self.target_arch = argument if self.locale: self.custom_args.remove(self.locale) if self.target_arch: self.custom_args.remove(self.target_arch) self.target_arch = self.target_arch.split('%')[1] self.repo_names: List = [] # dnf support is based on creating repo files which contains # path names to the repo and its cache. In order to allow a # persistent use of the files in and outside of a chroot call # an active bind mount from RootBind::mount_shared_directory # is expected and required manager_base = self.shared_location + '/dnf' self.shared_dnf_dir = { 'reposd-dir': manager_base + '/repos', 'cache-dir': manager_base + '/cache', 'pluginconf-dir': manager_base + '/pluginconf', 'vars-dir': manager_base + '/vars', } self.runtime_dnf_config_file = Temporary( path=self.root_dir, prefix='kiwi_dnf4.config' ).unmanaged_file() self.dnf_args = [ '--config', self.runtime_dnf_config_file.name, '-y', '--enableplugin=priorities', '--disableplugin=versionlock', ] + self.custom_args self.command_env = self._create_dnf_runtime_environment() # config file parameters for dnf tool self._create_runtime_config_parser() self._write_runtime_config() def setup_package_database_configuration(self) -> None: """ Setup rpm macros for bootstrapping and image building 1. Create the rpm image macro which persists during the build 2. Create the rpm bootstrap macro to make sure for bootstrapping the rpm database location matches the host rpm database setup. This macro only persists during the bootstrap phase. If the image was already bootstrapped a compat link is created instead. """ rpmdb = RpmDataBase( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ) if self.locale: rpmdb.set_macro_from_string(self.locale) rpmdb.write_config() rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.link_database_to_host_path() else: rpmdb.set_database_to_host_path() # SUSE compat code: # # Manually adding the compat link /var/lib/rpm that points to the # rpmdb location as it is configured in the host rpm setup. DNF makes # use of the host setup to bootstrap or to read the signing keys # from the rpm database setup provisioned by rpm itself (macro level). # It assumes the host setup is the default for the system being built. # # For SUSE this is causing a mismatch as the host setup is not the RPM # default (/var/lib/rpm) and SUSE distros proactively relocate the rpm # database from the default location to a custom one when the # rpm package and related macros are installed for the first time. rpmdb.init_database() image_rpm_compat_link = '/var/lib/rpm' host_rpm_dbpath = rpmdb.rpmdb_host.expand_query('%_dbpath') if host_rpm_dbpath != image_rpm_compat_link: Path.create(self.root_dir + os.path.dirname(image_rpm_compat_link)) Command.run( [ 'ln', '-s', '--no-target-directory', ''.join(['../..', host_rpm_dbpath]), self.root_dir + image_rpm_compat_link, ], raise_on_error=False, ) def use_default_location(self) -> None: """ Setup dnf repository operations to store all data in the default places """ self.shared_dnf_dir['reposd-dir'] = self.root_dir + '/etc/yum.repos.d' self.shared_dnf_dir['cache-dir'] = self.root_dir + '/var/cache/dnf' self.shared_dnf_dir['pluginconf-dir'] = ( self.root_dir + '/etc/dnf/plugins' ) self.shared_dnf_dir['vars-dir'] = self.root_dir + '/etc/dnf/vars' self._create_runtime_config_parser() self._write_runtime_config() def runtime_config(self) -> Dict: """ dnf runtime configuration and environment :return: dnf_args:list, command_env:dict :rtype: dict """ return {'dnf_args': self.dnf_args, 'command_env': self.command_env} def add_repo( self, name: str, uri: str, repo_type: str = 'rpm-md', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add dnf repository :param str name: repository base file name :param str uri: repository URI :param str repo_type: repostory type name :param int prio: dnf repostory priority :param str dist: unused :param str components: unused :param str user: unused :param str secret: unused :param str credentials_file: unused :param bool repo_gpgcheck: enable repository signature validation :param bool pkg_gpgcheck: enable package signature validation :param str sourcetype: source type, one of 'baseurl', 'metalink' or 'mirrorlist' :param str customization_script: custom script called after the repo file was created :param str architectures: unused """ repo_file = self.shared_dnf_dir['reposd-dir'] + '/' + name + '.repo' self.repo_names.append(name + '.repo') if os.path.exists(uri): # dnf requires local paths to take the file: type uri = 'file://' + uri repo_config = ConfigParser(interpolation=None) repo_config.add_section(name) repo_config.set(name, 'name', name) repo_config.set(name, sourcetype if sourcetype else 'baseurl', uri) if prio: repo_config.set(name, 'priority', format(prio)) repo_config.set(name, 'repo_gpgcheck', '1' if repo_gpgcheck else '0') repo_config.set(name, 'gpgcheck', '1' if pkg_gpgcheck else '0') if Defaults.is_buildservice_worker(): # when building in the build service, modular metadata is # inaccessible. In order to use modular content in the build # service, we need to disable modular filtering, which is # done with module_hotfixes option repo_config.set(name, 'module_hotfixes', '1') if not os.path.isdir(self.shared_dnf_dir['reposd-dir']): Path.create(self.shared_dnf_dir['reposd-dir']) with open(repo_file, 'w') as repo: repo_config.write(repo) if customization_script: self.run_repo_customize(customization_script, repo_file) def import_trusted_keys(self, signing_keys: List) -> None: """ Imports trusted keys into the image :param list signing_keys: list of the key files to import """ rpmdb = RpmDataBase(self.root_dir) for key in signing_keys: rpmdb.import_signing_key_to_image(key) def delete_repo(self, name: str) -> None: """ Delete dnf repository :param str name: repository base file name """ Path.wipe(self.shared_dnf_dir['reposd-dir'] + '/' + name + '.repo') def delete_all_repos(self) -> None: """ Delete all dnf repositories """ Path.wipe(self.shared_dnf_dir['reposd-dir']) Path.create(self.shared_dnf_dir['reposd-dir']) def delete_repo_cache(self, name: str) -> None: """ Delete dnf repository cache The cache data for each repository is stored in a directory and additional files all starting with the repository name. The method glob deletes all files and directories matching the repository name followed by any characters to cleanup the cache information :param str name: repository name """ dnf_cache_glob_pattern = ''.join( [self.shared_dnf_dir['cache-dir'], os.sep, name, '*'] ) for dnf_cache_file in glob.iglob(dnf_cache_glob_pattern): Path.wipe(dnf_cache_file) def cleanup_unused_repos(self) -> None: """ Delete unused dnf repositories Repository configurations which are not used for this build must be removed otherwise they are taken into account for the package installations """ repos_dir = self.shared_dnf_dir['reposd-dir'] repo_files = list(os.walk(repos_dir))[0][2] for repo_file in repo_files: if repo_file not in self.repo_names: Path.wipe(repos_dir + '/' + repo_file) def _create_dnf_runtime_environment(self) -> Dict: for dnf_dir in list(self.shared_dnf_dir.values()): Path.create(dnf_dir) ToEnv(self.root_dir, defaults.PACKAGE_MANAGER_ENV_VARS) return dict(os.environ, **HostProxy().get_env(), LANG='C') def _create_runtime_config_parser(self) -> None: self.runtime_dnf_config = ConfigParser(interpolation=None) self.runtime_dnf_config["main"] = { "cachedir": self.shared_dnf_dir['cache-dir'], "reposdir": self.shared_dnf_dir['reposd-dir'], "varsdir": self.shared_dnf_dir['vars-dir'], "pluginconfpath": self.shared_dnf_dir['pluginconf-dir'], "keepcache": "1", "debuglevel": "2", "best": "1", "obsoletes": "1", "plugins": "1", "gpgcheck": self.gpg_check, } # The runtime config replaces the host dnf configuration. # Thus a proxy setup in the host dnf configuration needs # to be added explicitly self.runtime_dnf_config["main"].update(HostProxy.get_dnf_config()) if self.exclude_docs: self.runtime_dnf_config["main"]["tsflags"] = "nodocs" if self.target_arch: self.runtime_dnf_config["main"].update( { "arch": self.target_arch, "ignorearch": "1", } ) def _write_runtime_config(self) -> None: with open(self.runtime_dnf_config_file.name, 'w') as config: self.runtime_dnf_config.write(config) def cleanup(self) -> None: """ Delete intermediate dnf config file """ if os.path.isfile(self.runtime_dnf_config_file.name): os.unlink(self.runtime_dnf_config_file.name) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/dnf5.py0000644000000000000000000003253115273675667014117 0ustar00# Copyright (c) 2023 Marcus Schäfer. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import glob from configparser import ConfigParser from typing import List, Dict, Optional # project import kiwi.defaults as defaults from kiwi.utils.temporary import Temporary, TmpT from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.repository.base import RepositoryBase from kiwi.path import Path from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.toenv import ToEnv from kiwi.utils.proxy import HostProxy class RepositoryDnf5(RepositoryBase): """ **Implements repository handling for next generation dnf5 package manager** :param str shared_dnf_dir: shared directory between image root and build system root :param str runtime_dnf_config_file: dnf runtime config file name :param dict command_env: customized os.environ for dnf :param str runtime_dnf_config: instance of :class:`ConfigParser` """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom dnf arguments and create runtime configuration and environment :param list custom_args: dnf arguments """ self.runtime_dnf_config_file = TmpT(name='') self.custom_args = custom_args self.exclude_docs = False self.locale = None self.target_arch = None # extract custom arguments not used in dnf call if 'exclude_docs' in self.custom_args: self.custom_args.remove('exclude_docs') self.exclude_docs = True if 'check_signatures' in self.custom_args: self.custom_args.remove('check_signatures') self.gpg_check = '1' else: self.gpg_check = '0' for argument in self.custom_args: if '_install_langs' in argument: self.locale = argument elif '_target_arch' in argument: self.target_arch = argument if self.locale: self.custom_args.remove(self.locale) if self.target_arch: self.custom_args.remove(self.target_arch) self.target_arch = self.target_arch.split('%')[1] self.repo_names: List = [] # dnf support is based on creating repo files which contains # path names to the repo and its cache. In order to allow a # persistent use of the files in and outside of a chroot call # an active bind mount from RootBind::mount_shared_directory # is expected and required manager_base = self.shared_location + '/dnf' self.shared_dnf_dir = { 'reposd-dir': manager_base + '/repos', 'cache-dir': manager_base + '/cache', 'pluginconf-dir': manager_base + '/pluginconf', 'vars-dir': manager_base + '/vars', } self.runtime_dnf_config_file = Temporary( path=self.root_dir, prefix='kiwi_dnf5.config' ).unmanaged_file() self.dnf_args = [ '--config', self.runtime_dnf_config_file.name, '-y', '--disable-plugin=priorities,versionlock', ] + self.custom_args self.command_env = self._create_dnf_runtime_environment() # config file parameters for dnf tool self._create_runtime_config_parser() self._write_runtime_config() def setup_package_database_configuration(self) -> None: """ Setup rpm macros for bootstrapping and image building 1. Create the rpm image macro which persists during the build 2. Create the rpm bootstrap macro to make sure for bootstrapping the rpm database location matches the host rpm database setup. This macro only persists during the bootstrap phase. If the image was already bootstrapped a compat link is created instead. """ rpmdb = RpmDataBase( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ) if self.locale: rpmdb.set_macro_from_string(self.locale) rpmdb.write_config() rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.link_database_to_host_path() else: rpmdb.set_database_to_host_path() # SUSE compat code: # # Manually adding the compat link /var/lib/rpm that points to the # rpmdb location as it is configured in the host rpm setup. DNF makes # use of the host setup to bootstrap or to read the signing keys # from the rpm database setup provisioned by rpm itself (macro level). # It assumes the host setup is the default for the system being built. # # For SUSE this is causing a mismatch as the host setup is not the RPM # default (/var/lib/rpm) and SUSE distros proactively relocate the rpm # database from the default location to a custom one when the # rpm package and related macros are installed for the first time. rpmdb.init_database() image_rpm_compat_link = '/var/lib/rpm' host_rpm_dbpath = rpmdb.rpmdb_host.expand_query('%_dbpath') if host_rpm_dbpath != image_rpm_compat_link: Path.create(self.root_dir + os.path.dirname(image_rpm_compat_link)) Command.run( [ 'ln', '-s', '--no-target-directory', ''.join(['../..', host_rpm_dbpath]), self.root_dir + image_rpm_compat_link, ], raise_on_error=False, ) def use_default_location(self) -> None: """ Setup dnf repository operations to store all data in the default places """ self.shared_dnf_dir['reposd-dir'] = self.root_dir + '/etc/yum.repos.d' self.shared_dnf_dir['cache-dir'] = self.root_dir + '/var/cache/libdnf5' self.shared_dnf_dir['pluginconf-dir'] = ( self.root_dir + '/etc/dnf/libdnf5-plugins' ) self.shared_dnf_dir['vars-dir'] = self.root_dir + '/etc/dnf/vars' self._create_runtime_config_parser() self._write_runtime_config() def runtime_config(self) -> Dict: """ dnf runtime configuration and environment :return: dnf_args:list, command_env:dict :rtype: dict """ return {'dnf_args': self.dnf_args, 'command_env': self.command_env} def add_repo( self, name: str, uri: str, repo_type: str = 'rpm-md', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add dnf repository :param str name: repository base file name :param str uri: repository URI :param str repo_type: repostory type name :param int prio: dnf repostory priority :param str dist: unused :param str components: unused :param str user: unused :param str secret: unused :param str credentials_file: unused :param bool repo_gpgcheck: enable repository signature validation :param bool pkg_gpgcheck: enable package signature validation :param str sourcetype: source type, one of 'baseurl', 'metalink' or 'mirrorlist' :param str customization_script: custom script called after the repo file was created :param str architectures: unused """ repo_file = self.shared_dnf_dir['reposd-dir'] + '/' + name + '.repo' self.repo_names.append(name + '.repo') if os.path.exists(uri): # dnf requires local paths to take the file: type uri = 'file://' + uri repo_config = ConfigParser(interpolation=None) repo_config.add_section(name) repo_config.set(name, 'name', name) repo_config.set(name, sourcetype if sourcetype else 'baseurl', uri) if prio: repo_config.set(name, 'priority', format(prio)) repo_config.set(name, 'repo_gpgcheck', '1' if repo_gpgcheck else '0') repo_config.set(name, 'gpgcheck', '1' if pkg_gpgcheck else '0') if Defaults.is_buildservice_worker(): # when building in the build service, modular metadata is # inaccessible. In order to use modular content in the build # service, we need to disable modular filtering, which is # done with module_hotfixes option repo_config.set(name, 'module_hotfixes', '1') if not os.path.isdir(self.shared_dnf_dir['reposd-dir']): Path.create(self.shared_dnf_dir['reposd-dir']) with open(repo_file, 'w') as repo: repo_config.write(repo) if customization_script: self.run_repo_customize(customization_script, repo_file) def import_trusted_keys(self, signing_keys: List) -> None: """ Imports trusted keys into the image :param list signing_keys: list of the key files to import """ rpmdb = RpmDataBase(self.root_dir) for key in signing_keys: rpmdb.import_signing_key_to_image(key) def delete_repo(self, name: str) -> None: """ Delete dnf repository :param str name: repository base file name """ Path.wipe(self.shared_dnf_dir['reposd-dir'] + '/' + name + '.repo') def delete_all_repos(self) -> None: """ Delete all dnf repositories """ Path.wipe(self.shared_dnf_dir['reposd-dir']) Path.create(self.shared_dnf_dir['reposd-dir']) def delete_repo_cache(self, name: str) -> None: """ Delete dnf repository cache The cache data for each repository is stored in a directory and additional files all starting with the repository name. The method glob deletes all files and directories matching the repository name followed by any characters to cleanup the cache information :param str name: repository name """ dnf_cache_glob_pattern = ''.join( [self.shared_dnf_dir['cache-dir'], os.sep, name, '*'] ) for dnf_cache_file in glob.iglob(dnf_cache_glob_pattern): Path.wipe(dnf_cache_file) def cleanup_unused_repos(self) -> None: """ Delete unused dnf repositories Repository configurations which are not used for this build must be removed otherwise they are taken into account for the package installations """ repos_dir = self.shared_dnf_dir['reposd-dir'] repo_files = list(os.walk(repos_dir))[0][2] for repo_file in repo_files: if repo_file not in self.repo_names: Path.wipe(repos_dir + '/' + repo_file) def _create_dnf_runtime_environment(self) -> Dict: for dnf_dir in list(self.shared_dnf_dir.values()): Path.create(dnf_dir) ToEnv(self.root_dir, defaults.PACKAGE_MANAGER_ENV_VARS) return dict(os.environ, **HostProxy().get_env(), LANG='C') def _create_runtime_config_parser(self) -> None: self.runtime_dnf_config = ConfigParser(interpolation=None) self.runtime_dnf_config["main"] = { "system_cachedir": self.shared_dnf_dir['cache-dir'], "reposdir": self.shared_dnf_dir['reposd-dir'], "varsdir": self.shared_dnf_dir['vars-dir'], "pluginconfpath": self.shared_dnf_dir['pluginconf-dir'], "keepcache": "1", "debuglevel": "2", "best": "1", "obsoletes": "1", "plugins": "0", "gpgcheck": self.gpg_check, } # The runtime config replaces the host dnf configuration. # Thus a proxy setup in the host dnf configuration needs # to be added explicitly self.runtime_dnf_config["main"].update(HostProxy.get_dnf_config()) if self.exclude_docs: self.runtime_dnf_config["main"]["tsflags"] = "nodocs" if self.target_arch: self.runtime_dnf_config["main"].update( { "arch": self.target_arch, "ignorearch": "1", } ) def _write_runtime_config(self) -> None: with open(self.runtime_dnf_config_file.name, 'w') as config: self.runtime_dnf_config.write(config) def cleanup(self) -> None: """ Delete intermediate dnf config file """ if os.path.isfile(self.runtime_dnf_config_file.name): os.unlink(self.runtime_dnf_config_file.name) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/pacman.py0000644000000000000000000002316715273675667014527 0ustar00# Copyright (c) 2019 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging from configparser import ConfigParser from typing import List, Dict, Optional # project import kiwi.defaults as defaults from kiwi.utils.temporary import Temporary, TmpT from kiwi.repository.base import RepositoryBase from kiwi.path import Path from kiwi.command import Command from kiwi.utils.toenv import ToEnv from kiwi.utils.proxy import HostProxy log = logging.getLogger('kiwi') class RepositoryPacman(RepositoryBase): """ **Implements repo handling for pacman package manager** :param str shared_pacman_dir: shared directory between image root and build system root :param str runtime_pacman_config_file: pacman runtime config file name :param list pacman_args: pacman caller args plus additional custom args :param dict command_env: customized os.environ for pacman """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom pacman arguments and create runtime configuration and environment :param list custom_args: pacman arguments """ self.runtime_pacman_config_file = TmpT(name='') self.custom_args = custom_args self.check_signatures = False self.repo_names: List = [] # delete custom arguments not used for pacman for argument in self.custom_args: if ( '_install_langs' in argument or '_target_arch' in argument or 'exclude_docs' in argument ): log.warning( f'Custom argument {argument} will be ignored for pacman' ) self.custom_args.remove(argument) self.runtime_pacman_config_file = Temporary( path=self.root_dir, prefix='kiwi_pacman.config' ).unmanaged_file() if 'check_signatures' in self.custom_args: self.custom_args.remove('check_signatures') self.check_signatures = True manager_base = self.shared_location + '/pacman' self.shared_pacman_dir = { 'cache-dir': manager_base + '/cache', 'repos-dir': manager_base + '/repos', } Path.create(self.shared_pacman_dir['repos-dir']) self.pacman_args = [ '--config', self.runtime_pacman_config_file.name, '--noconfirm', ] self._write_runtime_config() def use_default_location(self) -> None: """ Setup pacman repository operations to store all data in the default places """ self.shared_pacman_dir['repos-dir'] = self.root_dir + '/etc/pacman.d' def runtime_config(self) -> Dict: """ pacman runtime configuration and environment """ ToEnv(self.root_dir, defaults.PACKAGE_MANAGER_ENV_VARS) return { 'pacman_args': self.pacman_args, 'command_env': dict(os.environ, **HostProxy().get_env()), } def _add_repo_section( self, config_parser: ConfigParser, name: str, uri: str, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, ) -> None: config_parser.add_section(name) if uri.startswith(os.sep) and os.path.exists(uri): uri = 'file://{}'.format(uri) config_parser.set(name, 'Server', uri) config_parser.set( name, 'SigLevel', '{0} {1}'.format( 'Required' if pkg_gpgcheck else 'Never', 'DatabaseRequired' if repo_gpgcheck else 'DatabaseNever', ), ) def add_repo( self, name: str, uri: str, repo_type: Optional[str] = None, prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add pacman repository :param str name: repository base file name :param str uri: repository URI :param str repo_type: unused :param int prio: :param dist: unused :param str components: components to be used from this repository :param str user: unused :param str secret: unused :param credentials_file: unused :param bool repo_gpgcheck: enable database signature validation :param bool pkg_gpgcheck: enable package signature validation :param str sourcetype: unused :param str customization_script: custom script called after the repo file was created :param str architectures: unused """ repo_file = '{0}/{1}.repo'.format( self.shared_pacman_dir['repos-dir'], name ) self.repo_names.append(name + '.repo') repo_config = ConfigParser() repo_config.optionxform = str # type: ignore[method-assign, assignment] if not components: self._add_repo_section( repo_config, name, uri, repo_gpgcheck, pkg_gpgcheck ) else: for component in components.split(): self._add_repo_section( repo_config, component, uri, repo_gpgcheck, pkg_gpgcheck ) with open(repo_file, 'w') as config: repo_config.write(config) if customization_script: self.run_repo_customize(customization_script, repo_file) def import_trusted_keys(self, signing_keys: List) -> None: """ pacman runtime configuration and environment """ for signing_key in signing_keys: if os.path.exists(signing_key): Command.run(['pacman-key', '--recv-keys', signing_key]) else: Command.run(['pacman-key', '--add', signing_key]) def delete_repo(self, name: str) -> None: """ Delete pacman repository :param str name: repository name """ Path.wipe( '{0}/{1}.repo'.format(self.shared_pacman_dir['repos-dir'], name) ) def delete_all_repos(self) -> None: """ Delete all pacman repositories """ Path.wipe(self.shared_pacman_dir['repos-dir']) Path.create(self.shared_pacman_dir['repos-dir']) def delete_repo_cache(self, name: str) -> None: """ Delete pacman repository cache The method deletes these directories to cleanup the cache information :param str name: repository name """ Path.wipe(self.shared_pacman_dir['cache-dir']) def setup_package_database_configuration(self) -> None: """ Creates the folder that will contain the package manager database """ Path.create(os.sep.join([self.root_dir, 'var/lib/pacman'])) def cleanup_unused_repos(self) -> None: """ Delete unused pacman repositories Repository configurations which are not used for this build must be removed otherwise they are taken into account for the package installations """ repos_dir = self.shared_pacman_dir['repos-dir'] repo_files = list(os.walk(repos_dir))[0][2] for repo_file in repo_files: if repo_file not in self.repo_names: Path.wipe(repos_dir + '/' + repo_file) def _write_runtime_config(self) -> None: runtime_pacman_config = ConfigParser() runtime_pacman_config.optionxform = str # type: ignore[method-assign, assignment] runtime_pacman_config.add_section('options') runtime_pacman_config.set('options', 'Architecture', 'auto') runtime_pacman_config.set( 'options', 'CacheDir', self.shared_pacman_dir['cache-dir'] ) if self.check_signatures: runtime_pacman_config.set( 'options', 'SigLevel', 'Required DatabaseRequired' ) runtime_pacman_config.set( 'options', 'LocalFileSigLevel', 'Required DatabaseRequired' ) else: runtime_pacman_config.set( 'options', 'SigLevel', 'Never DatabaseNever' ) runtime_pacman_config.set( 'options', 'LocalFileSigLevel', 'Never DatabaseNever' ) runtime_pacman_config.set( 'options', 'Include', '{0}/*.repo'.format(self.shared_pacman_dir['repos-dir']), ) with open(self.runtime_pacman_config_file.name, 'w') as config: runtime_pacman_config.write(config) def cleanup(self) -> None: """ Delete intermediate pacman config file """ if os.path.isfile(self.runtime_pacman_config_file.name): os.unlink(self.runtime_pacman_config_file.name) ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1794079670.747587 kiwi-11.1.8/kiwi/repository/template/__init__.py0000644000000000000000000000000015273675667016617 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/repository/template/apt.py0000644000000000000000000000602515273675667015661 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os from string import Template from textwrap import dedent class PackageManagerTemplateAptGet: """ apt-get configuration file template """ def __init__(self) -> None: self.host_header = ( dedent( ''' # kiwi generated apt-get config file Dir "/"; Dir::State "${apt_shared_base}/"; Dir::State::status "/var/lib/dpkg/status.kiwi"; Dir::Cache "${apt_shared_base}/"; Dir::Etc "${apt_shared_base}/"; ''' ).strip() + os.linesep ) self.image_header = ( dedent( ''' # kiwi generated apt-get config file Dir "/"; ''' ).strip() + os.linesep ) self.apt = ( dedent( ''' APT { Get { AllowDowngrades "true"; AllowRemoveEssential "true"; AllowChangeHeldPackages "true"; AllowUnauthenticated "${unauthenticated}"; } }; ''' ).strip() + os.linesep ) self.dpkg_exclude_docs = ( dedent( ''' DPkg::Options { "--path-exclude=/usr/share/man/*"; "--path-exclude=/usr/share/doc/*"; "--path-include=/usr/share/doc/*/copyright"; }; ''' ).strip() + os.linesep ) def get_host_template(self, exclude_docs: bool = False) -> Template: """ apt-get package manager template for apt-get called outside of the image, not chrooted :rtype: Template """ template_data = self.host_header + self.apt if exclude_docs: template_data += self.dpkg_exclude_docs return Template(template_data) def get_image_template(self, exclude_docs: bool = False) -> Template: """ apt-get package manager template for apt-get called inside of the image, chrooted :rtype: Template """ template_data = self.image_header + self.apt if exclude_docs: template_data += self.dpkg_exclude_docs return Template(template_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/repository/zypper.py0000644000000000000000000004630115273675667014614 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import re import logging from configparser import ConfigParser from types import TracebackType from typing import List, Dict, Optional, Type # project import kiwi.defaults as defaults from kiwi.utils.temporary import Temporary, TmpT from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.repository.base import RepositoryBase from kiwi.system.uri import Uri from kiwi.path import Path from kiwi.utils.rpm_database import RpmDataBase from kiwi.utils.toenv import ToEnv from kiwi.utils.proxy import HostProxy log = logging.getLogger('kiwi') class RepositoryZypper(RepositoryBase): """ **Implements repo handling for zypper package manager** :param str shared_zypper_dir: shared directory between image root and build system root :param str runtime_zypper_config_file: zypper runtime config file name :param str runtime_zypp_config_file: libzypp runtime config file name :param list zypper_args: zypper caller args plus additional custom args :param dict command_env: customized os.environ for zypper :param object runtime_zypper_config: instance of :class:`ConfigParser` """ def post_init(self, custom_args: List = []) -> None: """ Post initialization method Store custom zypper arguments and create runtime configuration and environment :param list custom_args: zypper arguments """ self.runtime_zypper_config_file = TmpT(name='') self.runtime_zypp_config_file = TmpT(name='') self.custom_args = custom_args self.exclude_docs = False self.gpgcheck = False self.locale = None self.target_arch = None # extract custom arguments used for zypp config only if 'exclude_docs' in self.custom_args: self.custom_args.remove('exclude_docs') self.exclude_docs = True if 'check_signatures' in self.custom_args: self.custom_args.remove('check_signatures') self.gpgcheck = True for argument in self.custom_args: if '_install_langs' in argument: self.locale = argument elif '_target_arch' in argument: self.target_arch = argument if self.locale: self.custom_args.remove(self.locale) if self.target_arch: self.custom_args.remove(self.target_arch) self.target_arch = self.target_arch.split('%')[1] self.repo_names: List = [] # zypper support by default point all actions into the root # directory of the image system. This information is passed # as arguments to zypper and adapted if the call runs as # chrooted operation. Therefore the use of the shared location # via RootBind::mount_shared_directory is optional but # recommended to make use of the repo cache manager_base = self.root_dir + self.shared_location self.shared_zypper_dir = { 'pkg-cache-dir': os.sep.join([manager_base, 'packages']), 'reposd-dir': os.sep.join([manager_base, 'zypper/repos']), 'credentials-dir': os.sep.join( [manager_base, 'zypper/credentials'] ), 'solv-cache-dir': os.sep.join([manager_base, 'zypper/solv']), 'raw-cache-dir': os.sep.join([manager_base, 'zypper/raw']), 'cache-dir': os.sep.join([manager_base, 'zypper']), } self.runtime_zypper_config_file = Temporary( path=self.root_dir, prefix='kiwi_zypper.config' ).unmanaged_file() self.runtime_zypp_config_file = Temporary( path=self.root_dir, prefix='kiwi_zypp.config' ).unmanaged_file() self.zypper_args = [ '--non-interactive', '--gpg-auto-import-keys', '--pkg-cache-dir', self.shared_zypper_dir['pkg-cache-dir'], '--reposd-dir', self.shared_zypper_dir['reposd-dir'], '--solv-cache-dir', self.shared_zypper_dir['solv-cache-dir'], '--cache-dir', self.shared_zypper_dir['cache-dir'], '--raw-cache-dir', self.shared_zypper_dir['raw-cache-dir'], '--config', self.runtime_zypper_config_file.name, ] + self.custom_args self.command_env = self._create_zypper_runtime_environment() self._setup_intermediate_proxy_config() # config file parameters for zypper tool self.runtime_zypper_config = ConfigParser(interpolation=None) self.runtime_zypper_config.add_section('main') # config file parameters for libzypp library self.runtime_zypp_config = ConfigParser(interpolation=None) self.runtime_zypp_config.add_section('main') self.runtime_zypp_config.set( 'main', 'credentials.global.dir', self.shared_zypper_dir['credentials-dir'], ) if self.target_arch: self.runtime_zypp_config.set('main', 'arch', self.target_arch) if self.exclude_docs: self.runtime_zypp_config.set( 'main', 'rpm.install.excludedocs', 'yes' ) if self.gpgcheck: self.runtime_zypp_config.set('main', 'gpgcheck', '1') else: self.runtime_zypp_config.set('main', 'gpgcheck', '0') self._write_runtime_config() def setup_package_database_configuration(self) -> None: """ Setup rpm macros for bootstrapping and image building 1. Create the rpm image macro which persists during the build 2. Create the rpm bootstrap macro to make sure for bootstrapping the rpm database location matches the host rpm database setup. This macro only persists during the bootstrap phase. If the image was already bootstrapped a compat link is created instead. 3. Create zypper compat link """ rpmdb = RpmDataBase( self.root_dir, Defaults.get_custom_rpm_image_macro_name() ) if self.locale: rpmdb.set_macro_from_string(self.locale) rpmdb.write_config() rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): rpmdb.link_database_to_host_path() else: rpmdb.set_database_to_host_path() # Zypper compat code: # # Manually adding the compat link /var/lib/rpm that points to the # rpmdb location as it is configured in the host rpm setup. The # host rpm setup is taken into account because import_trusted_keys # is called during the bootstrap phase where rpm (respectively zypper) # is called from the host # # Usually it is expected that the package manager reads the # signing keys from the rpm database setup provisioned by rpm # itself (macro level) but zypper doesn't take the rpm macro # setup into account and relies on a hard coded path which we # can only provide as a symlink. # # That symlink is usually created by the rpm package when it gets # installed. However at that early phase when we import the # signing keys no rpm is installed yet nor any symlink exists. # Thus we have to create it here and hope to get rid of it in the # future. # # For further details on the motivation in zypper please # refer to bsc#1112357 rpmdb.init_database() image_rpm_compat_link = '/var/lib/rpm' host_rpm_dbpath = rpmdb.rpmdb_host.expand_query('%_dbpath') if host_rpm_dbpath != image_rpm_compat_link: Path.create(self.root_dir + os.path.dirname(image_rpm_compat_link)) Command.run( [ 'ln', '-s', '--no-target-directory', ''.join(['../..', host_rpm_dbpath]), self.root_dir + image_rpm_compat_link, ], raise_on_error=False, ) def use_default_location(self) -> None: """ Setup zypper repository operations to store all data in the default places """ self.shared_zypper_dir['reposd-dir'] = ( self.root_dir + '/etc/zypp/repos.d' ) self.shared_zypper_dir['credentials-dir'] = ( self.root_dir + '/etc/zypp/credentials.d' ) self.zypper_args = [ '--non-interactive', ] + self.custom_args self.command_env = dict(os.environ, **HostProxy().get_env(), LANG='C') def runtime_config(self) -> Dict: """ zypper runtime configuration and environment """ return { 'zypper_args': self.zypper_args, 'command_env': self.command_env, } def add_repo( self, name: str, uri: str, repo_type: str = 'rpm-md', prio: Optional[int] = None, dist: Optional[str] = None, components: Optional[str] = None, user: Optional[str] = None, secret: Optional[str] = None, credentials_file: Optional[str] = None, repo_gpgcheck: bool = False, pkg_gpgcheck: bool = False, sourcetype: Optional[str] = None, customization_script: Optional[str] = None, architectures: Optional[str] = None, ) -> None: """ Add zypper repository :param str name: repository name :param str uri: repository URI :param str repo_type: repostory type name :param int prio: zypper repostory priority :param str dist: unused :param str components: unused :param str user: credentials username :param str secret: credentials password :param str credentials_file: zypper credentials file :param bool repo_gpgcheck: enable repository signature validation :param bool pkg_gpgcheck: enable package signature validation :param str sourcetype: unused :param str customization_script: custom script called after the repo file was created :param str architectures: unused """ if credentials_file: repo_secret = os.sep.join( [self.shared_zypper_dir['credentials-dir'], credentials_file] ) if os.path.exists(repo_secret): Path.wipe(repo_secret) if user and secret: uri = ''.join([uri, '?credentials=', credentials_file]) with open(repo_secret, 'w') as credentials: credentials.write( 'username={0}{1}'.format(user, os.linesep) ) credentials.write( 'password={0}{1}'.format(secret, os.linesep) ) repo_file = ''.join( [self.shared_zypper_dir['reposd-dir'], '/', name, '.repo'] ) self.repo_names.append(''.join([name, '.repo'])) if os.path.exists(repo_file): Path.wipe(repo_file) self._backup_package_cache() zypper_addrepo_command = ( ['zypper'] + self.zypper_args + [ '--root', self.root_dir, 'addrepo', '--refresh', ( '--keep-packages' if Uri(uri).is_remote() else '--no-keep-packages' ), '--no-check', uri, name, ] ) try: Command.run(zypper_addrepo_command, self.command_env) except Exception: # for whatever reason zypper sometimes failes with # a 'failed to cache rpm database' error. I could not # find any reason why and a simple recall of the exact # same command in the exact same environment works. # Thus the stupid but simple workaround to this problem # is try one recall before really failing Command.run(zypper_addrepo_command, self.command_env) repo_config = ConfigParser(interpolation=None) repo_config.read(repo_file) baseurl = uri uri_with_credentials_pattern = '^(.*):(.*)@(.*)$' sensitive_match = re.match(uri_with_credentials_pattern, baseurl) if sensitive_match: # rewrite baseurl with credentials information because zypper # addrepo does strange things with the provided url encoded data repo_config.set(name, 'baseurl', baseurl) repo_config.set(name, 'repo_gpgcheck', '1' if repo_gpgcheck else '0') repo_config.set(name, 'pkg_gpgcheck', '1' if pkg_gpgcheck else '0') if prio: repo_config.set(name, 'priority', format(prio)) with open(repo_file, 'w') as repo: repo_config.write(repo) if customization_script: self.run_repo_customize(customization_script, repo_file) self._restore_package_cache() def import_trusted_keys(self, signing_keys: List) -> None: """ Imports trusted keys into the image :param list signing_keys: list of the key files to import """ rpmdb = RpmDataBase(self.root_dir) for key in signing_keys: rpmdb.import_signing_key_to_image(key) def delete_repo(self, name: str) -> None: """ Delete zypper repository :param str name: repository name """ Command.run( ['zypper'] + self.zypper_args + ['--root', self.root_dir, 'removerepo', name], self.command_env, ) def delete_all_repos(self) -> None: """ Delete all zypper repositories """ Path.wipe(self.shared_zypper_dir['reposd-dir']) Path.create(self.shared_zypper_dir['reposd-dir']) def delete_repo_cache(self, name: str) -> None: """ Delete zypper repository cache The cache data for each repository is stored in a list of directories of the same name as the repository name. The method deletes these directories to cleanup the cache information :param str name: repository name """ Path.wipe(os.sep.join([self.shared_zypper_dir['pkg-cache-dir'], name])) Path.wipe(os.sep.join([self.shared_zypper_dir['solv-cache-dir'], name])) Path.wipe(os.sep.join([self.shared_zypper_dir['raw-cache-dir'], name])) def cleanup_unused_repos(self) -> None: """ Delete unused zypper repositories zypper creates a system solvable which is unwanted for the purpose of building images. In addition zypper fails with an error message 'Failed to cache rpm database' if such a system solvable exists and a new root system is created All other repository configurations which are not used for this build must be removed too, otherwise they are taken into account for the package installations """ solv_dir = self.shared_zypper_dir['solv-cache-dir'] Path.wipe(solv_dir + '/@System') repos_dir = self.shared_zypper_dir['reposd-dir'] repo_files = list(os.walk(repos_dir))[0][2] for repo_file in repo_files: if repo_file not in self.repo_names: Path.wipe(repos_dir + '/' + repo_file) def _create_zypper_runtime_environment(self) -> Dict: for zypper_dir in list(self.shared_zypper_dir.values()): Path.create(zypper_dir) ToEnv(self.root_dir, defaults.PACKAGE_MANAGER_ENV_VARS) return dict( os.environ, **HostProxy().get_env(), LANG='C', ZYPP_CONF=self.runtime_zypp_config_file.name, ) def _setup_intermediate_proxy_config(self) -> None: """ libzypp reads the proxy setup from /etc/sysconfig/proxy only and ignores proxy environment variables. If the host does not provide this file, RootBind could not copy it as intermediate config into the image root. Thus a host proxy setup from the environment would not be taken into account by zypper calls inside of the image root. For this case create the intermediate config file from the host proxy setup """ sysconfig_proxy = '/etc/sysconfig/proxy' if os.path.exists(defaults.HOST_SYSCONFIG_PROXY): return if os.path.islink(self.root_dir + sysconfig_proxy): # intermediate config already created return host_proxy = HostProxy() if host_proxy.has_proxy(): log.info( f'Creating intermediate {sysconfig_proxy} from host proxy setup' ) self.root_bind.add_intermediate_config( sysconfig_proxy, host_proxy.get_sysconfig_data() ) def _write_runtime_config(self) -> None: with open(self.runtime_zypper_config_file.name, 'w') as config: self.runtime_zypper_config.write(config) with open(self.runtime_zypp_config_file.name, 'w') as config: self.runtime_zypp_config.write(config) def _backup_package_cache(self) -> None: """ preserve package cache which otherwise will be removed by zypper if no repo file is found. But this situation is normal for an image build process which setup and remove repos for building at runtime """ self._move_package_cache(backup=True) def _restore_package_cache(self) -> None: """ restore preserved package cache at the location passed to zypper """ self._move_package_cache(restore=True) def _move_package_cache( self, backup: bool = False, restore: bool = False ) -> None: package_cache = self.shared_location + '/packages' package_cache_moved = package_cache + '.moved' if backup and os.path.exists(package_cache): Command.run(['mv', '-f', package_cache, package_cache_moved]) elif restore and os.path.exists(package_cache_moved): Command.run(['mv', '-f', package_cache_moved, package_cache]) def cleanup(self) -> None: """ Delete intermediate zypp and zypper config files """ if os.path.isfile(self.runtime_zypper_config_file.name): os.unlink(self.runtime_zypper_config_file.name) if os.path.isfile(self.runtime_zypp_config_file.name): os.unlink(self.runtime_zypp_config_file.name) def __exit__( self, exc_type: Optional[Type[BaseException]], exc_value: Optional[BaseException], traceback: Optional[TracebackType], ) -> None: self._restore_package_cache() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/runtime_checker.py0000644000000000000000000014346715273675667014226 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import json import re import logging from textwrap import dedent from typing import NamedTuple, Dict, List, Any # project from kiwi.version import __version__ from io import StringIO from kiwi.xml_description import XMLDescription from kiwi.firmware import FirmWare from kiwi.xml_state import XMLState from kiwi.system.uri import Uri from kiwi.defaults import Defaults from kiwi.command import Command from kiwi.path import Path from kiwi.system.kernel import Kernel from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.runtime_config import RuntimeConfig from kiwi.exceptions import KiwiRuntimeError import kiwi.defaults as defaults dracut_module_type = NamedTuple( 'dracut_module_type', [('package', str), ('min_version', str)] ) log = logging.getLogger('kiwi') class RuntimeChecker: """ **Implements build consistency checks at runtime** """ def __init__(self, xml_state: XMLState) -> None: """ The schema of an image description covers structure and syntax of the provided data. The RuntimeChecker provides methods to perform further semantic checks which allows to recognize potential build or boot problems early. :param object xml_state: Instance of XMLState """ self.xml_state = xml_state def check_repositories_configured(self) -> None: """ Verify that there are repositories configured """ if not self.xml_state.get_repository_sections(): raise KiwiRuntimeError('No repositories configured') @staticmethod def check_target_dir_on_unsupported_filesystem(target_dir: str) -> None: """ Raise if the given target dir does not reside on a filesystem that supports all important features like extended permissions(fscaps), ACLs or xattrs. """ message = dedent( '''\n Target root/image directory is lacking filesystem features The filesystem {0} in the target path {1} does not support important features like extended permissions, ACLs or xattrs. The image build may fail or the resulting image misbehave. ''' ) target_dir = Path.first_exists(target_dir) stat = Command.run(['stat', '-f', '-c', '%T', target_dir]) if stat: target_fs = stat.output.strip() supported_target_filesystem = ( 'btrfs', 'ext2', 'ext2/ext3', 'ext3', 'ext4', 'overlayfs', 'tmpfs', 'xfs', ) if target_fs not in supported_target_filesystem: raise KiwiRuntimeError(message.format(target_fs, target_dir)) def check_include_references_unresolvable(self) -> None: """ Raise for still included statements as not resolvable. The KIWI XSLT processing replaces the specified include directive(s) with the given file reference(s). If this action did not happen for example on nested includes, it can happen that they stay in the document as sort of waste. """ message = dedent( '''\n One ore more statements are unresolvable The following include references could not be resolved. Please verify the specified location(s) and/or delete the broken include directive(s) from the description. Please also note, nested includes from other include files are not supported: {0} ''' ) include_files = ( self.xml_state.get_include_section_reference_file_names() ) if include_files: raise KiwiRuntimeError( message.format(json.dumps(include_files, indent=4)) ) def check_image_include_repos_publicly_resolvable(self) -> None: """ Verify that all repos marked with the imageinclude attribute can be resolved into a http based web URL """ message = dedent( '''\n The use of imageinclude="true" in the repository definition for the Repository: {0} requires the repository to by publicly available. The source locator of the repository however indicates it is private to your local system. Therefore it can't be included into the system image repository configuration. Please define a publicly available repository in your image XML description. ''' ) repository_sections = self.xml_state.get_repository_sections() for xml_repo in repository_sections: repo_marked_for_image_include = xml_repo.get_imageinclude() if repo_marked_for_image_include: repo_source = xml_repo.get_source().get_path() repo_type = xml_repo.get_type() uri = Uri(repo_source, repo_type) if not uri.is_public(): raise KiwiRuntimeError(message.format(repo_source)) @staticmethod def check_target_directory_not_in_shared_cache(target_dir: str) -> None: """ The target directory must be outside of the kiwi shared cache directory in order to avoid busy mounts because kiwi bind mounts the cache directory into the image root tree to access host caching information :param string target_dir: path name """ message = dedent( '''\n Target directory %s conflicts with kiwi's shared cache directory %s. This is going to create a busy loop mount. Please choose another target directory. ''' ) shared_cache_location = Defaults.get_shared_cache_location() target_dir_stack = ( os.path.abspath(os.path.normpath(target_dir)) .replace(os.sep + os.sep, os.sep) .split(os.sep) ) if target_dir_stack[1:4] == shared_cache_location.split(os.sep): raise KiwiRuntimeError( message % (target_dir, shared_cache_location) ) def check_volume_label_used_with_lvm(self) -> None: """ The optional volume label in a systemdisk setup is only effective if the LVM, logical volume manager system is used. In any other case where the filesystem itself offers volume management capabilities there are no extra filesystem labels which can be applied per volume """ message = dedent( '''\n Custom volume label setup used without LVM The optional volume label in a systemdisk setup is only effective if the LVM, logical volume manager system is used. Your setup uses the {0} filesystem which itself offers volume management capabilities. Extra filesystem labels cannot be applied in this case. If you want to force LVM over the {0} volume management system you can do so by specifying the following in your KIWI XML description: ''' ) volume_management = self.xml_state.get_volume_management() if volume_management != 'lvm': for volume in self.xml_state.get_volumes(): if volume.label and volume.label != 'SWAP': raise KiwiRuntimeError(message.format(volume_management)) def check_partuuid_persistency_type_used_with_mbr(self) -> None: """ The devicepersistency setting by-partuuid can only be used in combination with a partition table type that supports UUIDs. In any other case Linux creates artificial values for PTUUID and PARTUUID from the disk signature which can change without touching the actual partition table. We consider this unsafe and only allow the use of by-partuuid in combination with partition tables that actually supports it properly. """ message = dedent( '''\n devicepersistency={0!r} used with non UUID capable partition table PTUUID and PARTUUID exists in the GUID (GPT) partition table. According to the firmware setting: {1!r}, the selected partition table type is: {2!r}. This table type does not natively support UUIDs. In such a case Linux creates artificial values for PTUUID and PARTUUID from the disk signature which can change without touching the actual partition table. This is considered unsafe and KIWI only allows the use of by-partuuid in combination with partition tables that actually supports UUIDs properly. Please make sure to use one of the following firmware settings which leads to an image using an UUID capable partition table and therefore supporting consistent by-partuuid device names: ''' ) persistency_type = self.xml_state.build_type.get_devicepersistency() if persistency_type and persistency_type == 'by-partuuid': supported_table_types = ['gpt'] firmware = FirmWare(self.xml_state) table_type = firmware.get_partition_table_type() if table_type not in supported_table_types: raise KiwiRuntimeError( message.format( persistency_type, firmware.firmware, table_type ) ) def check_swap_name_used_with_lvm(self) -> None: """ The optional oem-swapname is only effective if used together with the LVM volume manager. A name for the swap space can only be set if it is created as a LVM volume. In any other case the name does not apply to the system """ message = dedent( '''\n Specified swap space name: {0} will not be used The specified oem-swapname is used without the LVM volume manager. This means the swap space will be created as simple partition for which no name assignment can take place. The name specified in oem-swapname is used to give the LVM swap volume a name. Outside of LVM the setting is meaningless and should be removed. Please delete the following setting from your image description: {0} ''' ) volume_management = self.xml_state.get_volume_management() if volume_management != 'lvm': oemconfig = self.xml_state.get_build_type_oemconfig_section() if oemconfig and oemconfig.get_oem_swapname(): raise KiwiRuntimeError( message.format(oemconfig.get_oem_swapname()[0]) ) def check_volume_setup_defines_reserved_labels(self) -> None: message = dedent( '''\n Reserved label name used in LVM volume setup The label setup for volume {0} uses the reserved label {1}. Reserved labels used by KIWI internally are {2}. Please choose another label name for this volume. ''' ) reserved_labels = [ self.xml_state.build_type.get_rootfs_label() or 'ROOT', 'SWAP', 'SPARE', ] volume_management = self.xml_state.get_volume_management() if volume_management == 'lvm': for volume in self.xml_state.get_volumes(): # A swap volume is created implicitly if oem-swap is # requested. This volume detected via realpath set to # swap is skipped from the reserved label check as it # intentionally uses the reserved label named SWAP if volume.realpath != 'swap': if volume.label and volume.label in reserved_labels: raise KiwiRuntimeError( message.format( volume.name, volume.label, reserved_labels ) ) def check_volume_setup_defines_multiple_fullsize_volumes(self) -> None: """ The volume size specification 'all' makes this volume to take the rest space available on the system. It's only allowed to specify one all size volume """ message = dedent( '''\n Multiple all size volumes found but only one is allowed The volume size specification 'all' makes this volume to take the rest space available on the system. It's only allowed to specify one all size volume ''' ) systemdisk_section = self.xml_state.get_build_type_system_disk_section() if systemdisk_section: all_size_volume_count = 0 volumes = systemdisk_section.get_volume() or [] for volume in volumes: size = volume.get_size() or volume.get_freespace() if size and 'all' in size: all_size_volume_count += 1 if all_size_volume_count > 1: raise KiwiRuntimeError(message) def check_volume_setup_has_no_root_definition(self) -> None: """ The root volume in a systemdisk setup is handled in a special way. It is not allowed to setup a custom name or mountpoint for the root volume. Therefore the size of the root volume can be setup via the @root volume name. This check looks up the volume setup and searches if there is a configuration for the '/' mountpoint which would cause the image build to fail """ message = dedent( '''\n Volume setup for "/" found. The size of the root volume must be specified via the @root volume name like the following example shows: A custom name or mountpoint for the root volume is not allowed. ''' ) for volume in self.xml_state.get_volumes(): if volume.mountpoint == '/': raise KiwiRuntimeError(message) def check_container_tool_chain_installed(self) -> None: """ When creating container images the specific tools are used in order to import and export OCI or Docker compatible images. This check searches for those tools to be installed in the build system and fails if it can't find them """ message_tool_not_found = dedent( '''\n Required tool {name} not found in caller environment Creation of OCI or Docker images requires the tools {name} and skopeo to be installed on the build system. For SUSE based systems you can find the tools at: http://download.opensuse.org/repositories/Virtualization:/containers ''' ) message_version_unsupported = dedent( '''\n {name} tool found with unknown version ''' ) message_unknown_tool = dedent( '''\n Unknown tool: {0}. Please configure KIWI with an appropriate value (umoci or buildah). Consider this runtime configuration file syntax (/etc/kiwi.yml): oci: - archive_tool: umoci | buildah ''' ) expected_version = (0, 1, 0) if self.xml_state.get_build_type_name() in ['docker', 'oci']: runtime_config = RuntimeConfig() tool_name = runtime_config.get_oci_archive_tool() if tool_name == 'buildah': oci_tools = ['buildah', 'skopeo'] elif tool_name == 'umoci': oci_tools = ['umoci', 'skopeo'] else: raise KiwiRuntimeError(message_unknown_tool.format(tool_name)) for tool in oci_tools: if not Path.which(filename=tool, access_mode=os.X_OK): raise KiwiRuntimeError( message_tool_not_found.format(name=tool) ) elif not CommandCapabilities.check_version( tool, expected_version, raise_on_error=False ): raise KiwiRuntimeError( message_version_unsupported.format(name=tool) ) self._check_multitag_support() def _check_multitag_support(self) -> None: message = dedent( '''\n Using additionaltags attribute requires skopeo tool to be capable to handle it, it must include the '--additional-tag' option for copy subcommand (check it running 'skopeo copy --help').\n It is known to be present since v0.1.30 ''' ) if 'additional_names' in self.xml_state.get_container_config(): if not CommandCapabilities.has_option_in_help( 'skopeo', '--additional-tag', ['copy', '--help'], raise_on_error=False, ): raise KiwiRuntimeError(message) def check_luksformat_options_valid(self) -> None: """ Options set via the luksformat element are passed along to the cryptsetup tool. Only options that are known to the tool should be allowed. Thus this runtime check looks up the provided option names if they exist in the cryptsetup version used on the build host """ message = dedent( '''\n Option {0!r} not found in cryptsetup The Option {0!r} could not be found in the help output of the cryptsetup tool. ''' ) luksformat = self.xml_state.build_type.get_luksformat() if luksformat: for option in luksformat[0].get_option(): argument = option.get_name() if not CommandCapabilities.has_option_in_help( 'cryptsetup', argument, ['--help'], raise_on_error=False ): raise KiwiRuntimeError(message.format(argument)) def check_appx_naming_conventions_valid(self) -> None: """ When building wsl images there are some naming conventions that must be fulfilled to run the container on Microsoft Windows """ launcher_pattern = r'[^\\]+(\.[Ee][Xx][Ee])$' message_container_launcher_invalid = dedent( '''\n Invalid WSL launcher name: {0} WSL launcher name must match the pattern: {1} ''' ) id_pattern = r'^[a-zA-Z0-9]+$' message_container_id_invalid = dedent( '''\n Invalid WSL container application id: {0} WSL container id must match the pattern: {1} ''' ) build_type = self.xml_state.get_build_type_name() container_config = self.xml_state.get_container_config() container_history = container_config.get('history') or {} if build_type == 'appx' and container_config: launcher = container_history.get('launcher') if launcher and not re.match(launcher_pattern, launcher): raise KiwiRuntimeError( message_container_launcher_invalid.format( launcher, launcher_pattern ) ) application_id = container_history.get('application_id') if application_id and not re.match(id_pattern, application_id): raise KiwiRuntimeError( message_container_id_invalid.format( application_id, id_pattern ) ) def check_initrd_selection_required(self) -> None: """ If the boot attribute is used without selecting kiwi as the initrd_system, the setting of the boot attribute will not have any effect. We assume that configurations which explicitly specify the boot attribute wants to use the custom kiwi initrd system and not dracut. """ message_kiwi_initrd_system_not_selected = dedent( '''\n Missing initrd_system selection for boot attribute The selected boot="'{0}'" boot description indicates the custom kiwi initrd system should be used instead of dracut. If this is correct please explicitly request the kiwi initrd system as follows: If this is not want you want and dracut should be used as initrd system, please delete the boot attribute as it is obsolete in this case. ''' ) initrd_system = self.xml_state.get_initrd_system() boot_image_reference = self.xml_state.build_type.get_boot() if initrd_system != 'kiwi' and boot_image_reference: raise KiwiRuntimeError( message_kiwi_initrd_system_not_selected.format( boot_image_reference ) ) def check_boot_description_exists(self) -> None: """ If a kiwi initrd is used, a lookup to the specified boot description is done and fails early if it does not exist """ message_no_boot_reference = dedent( '''\n Boot description missing for '{0}' type The selected '{1}' initrd_system requires a boot description reference. Please update your type setup as follows A collection of custom boot descriptions can be found in the kiwi-boot-descriptions package ''' ) message_boot_description_not_found = dedent( '''\n Boot description '{0}' not found The selected boot description could not be found on the build host. A collection of custom boot descriptions can be found in the kiwi-boot-descriptions package ''' ) image_types_supporting_custom_boot_description = ['oem', 'pxe'] build_type = self.xml_state.get_build_type_name() initrd_system = self.xml_state.get_initrd_system() if ( initrd_system == 'kiwi' and build_type in image_types_supporting_custom_boot_description ): boot_image_reference = self.xml_state.build_type.get_boot() if not boot_image_reference: raise KiwiRuntimeError( message_no_boot_reference.format(build_type, initrd_system) ) if not boot_image_reference[0] == os.sep: boot_image_reference = os.sep.join( [ Defaults.get_boot_image_description_path(), boot_image_reference, ] ) if not os.path.exists(boot_image_reference): raise KiwiRuntimeError( message_boot_description_not_found.format( boot_image_reference ) ) def check_consistent_kernel_in_boot_and_system_image(self) -> None: """ If a kiwi initrd is used, the kernel used to build the kiwi initrd and the kernel used in the system image must be the same in order to avoid an inconsistent boot setup """ message = dedent( '''\n Possible kernel mismatch between kiwi initrd and system image The selected '{0}' boot image kernel is '{1}'. However this kernel package was not explicitly listed in the package list of the system image. Please fixup your system image description: 1) Add to your system XML description 2) Inherit kernel from system description to initrd via the custom kernel profile: ''' ) boot_image_reference = self.xml_state.build_type.get_boot() boot_kernel_package_name = None if boot_image_reference: if not boot_image_reference[0] == '/': boot_image_reference = os.sep.join( [ Defaults.get_boot_image_description_path(), boot_image_reference, ] ) boot_config_file = os.sep.join([boot_image_reference, 'config.xml']) if os.path.exists(boot_config_file): boot_description = XMLDescription( description=boot_config_file, derived_from=self.xml_state.xml_data.description_dir, ) boot_kernel_profile = self.xml_state.build_type.get_bootkernel() if not boot_kernel_profile: boot_kernel_profile = 'std' boot_xml_state = XMLState( boot_description.load(), [boot_kernel_profile] ) kernel_package_sections = [] for packages_section in boot_xml_state.xml_data.get_packages(): # lookup package sections matching kernel profile in kiwi # boot description. By definition this must be a packages # section with a single profile name whereas the default # profile name is 'std'. The section itself must contain # one matching kernel package name for the desired # architecture if packages_section.get_profiles() == boot_kernel_profile: for package in packages_section.get_package(): kernel_package_sections.append(package) for package in kernel_package_sections: if boot_xml_state.package_matches_host_architecture( package ): boot_kernel_package_name = package.get_name() if boot_kernel_package_name: # A kernel package name was found in the kiwi boot image # description. Let's check if this kernel is also used # in the system image image_package_names = self.xml_state.get_system_packages() if boot_kernel_package_name not in image_package_names: raise KiwiRuntimeError( message.format( self.xml_state.build_type.get_boot(), boot_kernel_package_name, ) ) def check_dracut_module_versions_compatible_to_kiwi( self, root_dir: str ) -> None: """ KIWI images which makes use of kiwi dracut modules has to use module versions compatible with the version of this KIWI builder code base. This is important to avoid inconsistencies between the way how kiwi includes its own dracut modules and former version of those dracut modules which could be no longer compatible with the builder. Therefore this runtime check maintains a min_version constraint for which we know this KIWI builder to be compatible with. """ message = dedent( '''\n Incompatible dracut-kiwi module(s) found The image was build with KIWI version={0}. The system root tree has the following dracut-kiwi-* module packages installed which are too old to work with this version of KIWI. Please make sure to use dracut-kiwi-* module packages which are >= than the versions listed below. {1} ''' ) kiwi_dracut_modules = { '55kiwi-dump': dracut_module_type('dracut-kiwi-oem-dump', '9.20.1'), '55kiwi-live': dracut_module_type('dracut-kiwi-live', '9.20.1'), '55kiwi-overlay': dracut_module_type( 'dracut-kiwi-overlay', '9.20.1' ), '55kiwi-repart': dracut_module_type( 'dracut-kiwi-oem-repart', '9.20.1' ), '59kiwi-dump-reboot': dracut_module_type( 'dracut-kiwi-oem-dump', '9.20.1' ), '59kiwi-lib': dracut_module_type('dracut-kiwi-lib', '9.20.1'), } dracut_module_dir = os.sep.join([root_dir, '/usr/lib/dracut/modules.d']) if not os.path.isdir(dracut_module_dir): # no dracut module dir present return incompatible_modules = {} for module in os.listdir(dracut_module_dir): module_meta = kiwi_dracut_modules.get(module) if module_meta: module_version = self._get_dracut_module_version_from_pdb( self.xml_state.get_package_manager(), module_meta.package, root_dir, ) if module_version: module_version_nr = tuple( int(it) for it in module_version.split('.') ) module_min_version_nr = tuple( int(it) for it in module_meta.min_version.split('.') ) if module_version_nr < module_min_version_nr: incompatible_modules[module_meta.package] = ( 'got:{0}, need:>={1}'.format( module_version, module_meta.min_version ) ) if incompatible_modules: raise KiwiRuntimeError( message.format(__version__, incompatible_modules) ) def check_dracut_module_for_oem_install_in_package_list(self) -> None: """ OEM images if configured to use dracut as initrd system and configured with one of the installiso, installstick or installpxe attributes requires the KIWI provided dracut-kiwi-oem-dump module to be installed at the time dracut is called. Thus this runtime check examines if the required package is part of the package list in the image description. """ message = dedent( '''\n Required dracut module package missing in package list One of the packages '{0}' is required to build an installation image for the selected oem image type. Depending on your distribution, add the following in the section: ''' ) meta = Defaults.get_runtime_checker_metadata() required_dracut_packages = meta['package_names']['dracut_oem_dump'] initrd_system = self.xml_state.get_initrd_system() build_type = self.xml_state.get_build_type_name() if build_type == 'oem' and initrd_system == 'dracut': install_iso = self.xml_state.build_type.get_installiso() install_stick = self.xml_state.build_type.get_installstick() install_pxe = self.xml_state.build_type.get_installpxe() if install_iso or install_stick or install_pxe: package_names = ( self.xml_state.get_bootstrap_packages() + self.xml_state.get_system_packages() ) if not RuntimeChecker._package_in_list( package_names, required_dracut_packages ): raise KiwiRuntimeError( message.format(required_dracut_packages) ) def check_dracut_module_for_disk_oem_in_package_list(self) -> None: """ OEM images if configured to use dracut as initrd system requires the KIWI provided dracut-kiwi-oem-repart module to be installed at the time dracut is called. Thus this runtime check examines if the required package is part of the package list in the image description. """ message = dedent( '''\n Required dracut module package missing in package list One of the packages '{0}' is required for the selected oem image type. Depending on your distribution, add the following in the section: ''' ) meta = Defaults.get_runtime_checker_metadata() required_dracut_packages = meta['package_names']['dracut_oem_repart'] initrd_system = self.xml_state.get_initrd_system() disk_resize_requested = self.xml_state.get_oemconfig_oem_resize() build_type = self.xml_state.get_build_type_name() if ( build_type == 'oem' and initrd_system == 'dracut' and disk_resize_requested ): package_names = ( self.xml_state.get_bootstrap_packages() + self.xml_state.get_system_packages() ) if not RuntimeChecker._package_in_list( package_names, required_dracut_packages ): raise KiwiRuntimeError(message.format(required_dracut_packages)) def check_dracut_module_for_live_iso_in_package_list(self) -> None: """ Live ISO images uses a dracut initrd to boot and requires the KIWI provided kiwi-live dracut module to be installed at the time dracut is called. Thus this runtime check examines if the required package is part of the package list in the image description. """ message = dedent( '''\n Required dracut module package missing in package list One of the packages '{0}' is required for the selected live iso image type. Depending on your distribution, add the following in your section: ''' ) meta = Defaults.get_runtime_checker_metadata() required_dracut_packages = meta['package_names']['dracut_live'] type_name = self.xml_state.get_build_type_name() type_flag = self.xml_state.build_type.get_flags() if type_name == 'iso' and type_flag != 'dmsquash': package_names = ( self.xml_state.get_bootstrap_packages() + self.xml_state.get_system_packages() ) if not RuntimeChecker._package_in_list( package_names, required_dracut_packages ): raise KiwiRuntimeError(message.format(required_dracut_packages)) def check_dracut_module_for_disk_overlay_in_package_list(self) -> None: """ Disk images configured to use a root filesystem overlay requires the KIWI provided kiwi-overlay dracut module to be installed at the time dracut is called. Thus this runtime check examines if the required package is part of the package list in the image description. """ message = dedent( '''\n Required dracut module package missing in package list The package '{0}' is required for the selected overlayroot activated image type. Depending on your distribution, add the following in your section: ''' ) initrd_system = self.xml_state.get_initrd_system() meta = Defaults.get_runtime_checker_metadata() required_dracut_packages = meta['package_names']['dracut_overlay'] if ( initrd_system == 'dracut' and self.xml_state.build_type.get_overlayroot() ): package_names = ( self.xml_state.get_bootstrap_packages() + self.xml_state.get_system_packages() ) if not RuntimeChecker._package_in_list( package_names, required_dracut_packages ): raise KiwiRuntimeError(message.format(required_dracut_packages)) def check_xen_uniquely_setup_as_server_or_guest(self) -> None: """ If the image is classified to be used as Xen image, it can be either a Xen Server(dom0) or a Xen guest. The image configuration is checked if the information uniquely identifies the image as such """ xen_message = dedent( '''\n Inconsistent Xen setup found: The use of the 'xen_server' or 'xen_loader' attributes indicates the target system for this image is Xen. However the image specifies both attributes at the same time which classifies the image to be both, a Xen Server(dom0) and a Xen guest at the same time, which is not supported. Please cleanup your image description. Setup only one of 'xen_server' or 'xen_loader'. ''' ) ec2_message = dedent( '''\n Inconsistent Amazon EC2 setup found: The firmware setup indicates the target system for this image is Amazon EC2, which uses a Xen based virtualisation technology. Therefore the image must be classified as a Xen guest and can not be a Xen server as indicated by the 'xen_server' attribute Please cleanup your image description. Delete the 'xen_server' attribute for images used with Amazon EC2. ''' ) if self.xml_state.is_xen_server() and self.xml_state.is_xen_guest(): firmware = self.xml_state.build_type.get_firmware() ec2_firmware_names = Defaults.get_ec2_capable_firmware_names() if firmware and firmware in ec2_firmware_names: raise KiwiRuntimeError(ec2_message) else: raise KiwiRuntimeError(xen_message) def check_mediacheck_installed(self) -> None: """ If the image description enables the mediacheck attribute the required tools to run this check must be installed on the image build host """ message_tool_not_found = dedent( '''\n Required tool {name} not found in caller environment The attribute 'mediacheck' is set to 'true' which requires the above tool to be installed on the build system ''' ) if self.xml_state.build_type.get_mediacheck() is True: tool = 'tagmedia' media_tagger = RuntimeConfig().get_iso_media_tag_tool() if media_tagger == 'checkmedia': tool = 'tagmedia' elif media_tagger == 'isomd5sum': tool = 'implantisomd5' if not Path.which(filename=tool, access_mode=os.X_OK): raise KiwiRuntimeError(message_tool_not_found.format(name=tool)) def check_checkmedia_used_with_msdos_table(self) -> None: """ The checkmedia/tagmedia tools in versions < v6.6 only supports plain MBR partition tables. """ message = dedent( '''\n ISO media tag tool tagmedia < v6.6 does not support {table} partition tables The attribute 'mediacheck' is set to 'true' and the selected media tag tool only supports plain MBR (msdos) partition tables. Attributes influencing the partition table type are firmware, efiparttable and targettype. Please check your image description for the setup of these attributes. ''' ) if self.xml_state.build_type.get_mediacheck() is True: media_tagger = RuntimeConfig().get_iso_media_tag_tool() firmware = FirmWare(self.xml_state) table_type = firmware.get_partition_table_type() if media_tagger == 'checkmedia' and table_type != 'msdos': self.check_mediacheck_installed() # checkmedia in versions >= 6.6 supports GPT, only for # older versions the runtime check applies expected_version = (6, 6) if not CommandCapabilities.check_version( media_tagger, expected_version, raise_on_error=False ): raise KiwiRuntimeError( message.format(tool=media_tagger, table=table_type) ) def check_image_version_provided(self) -> None: """ Kiwi requires a element to be specified as part of at least one section. """ message_missing_version = dedent( '''\n No version is defined in any of the sections. Please add image_version inside the section. ''' ) if not self.xml_state.get_image_version(): raise KiwiRuntimeError(message_missing_version) def check_image_type_unique(self) -> None: """ Verify that the selected image type is unique within the range of the configured types and profiles. """ message = dedent( '''\n Conflicting image type setup detected The selected image type '{0}' in the {1} profile selection is not unique. There are the following type settings which overrides each other: {2} To solve this conflict please move the image type setup into its own profile and select them using the --profile option at call time. ''' ) image_type_sections = [] type_dict: Dict[str, List[Any]] = {} for preferences in self.xml_state.get_preferences_sections(): image_type_sections += preferences.get_type() for image_type in image_type_sections: type_name = image_type.get_image() if type_dict.get(type_name): type_dict[type_name].append(image_type) else: type_dict[type_name] = [image_type] for type_name, type_list in list(type_dict.items()): if len(type_list) > 1: type_export = StringIO() for image_type in type_list: type_export.write(os.linesep) image_type.export(type_export, 0) raise KiwiRuntimeError( message.format( type_name, self.xml_state.profiles or ['Default'], type_export.getvalue(), ) ) def check_efi_fat_image_has_correct_size(self) -> None: """ Verify that the efifatimagesize does not exceed the max El Torito load size of 65535 * 512 bytes """ message = dedent( '''\n El Torito max load size exceeded The configured efifatimagesize of '{0}MB' exceeds the El Torito max load size of 65535 * 512 bytes (~31MB). ''' ) fat_image_mbsize = int( self.xml_state.build_type.get_efifatimagesize() or defaults.EFI_FAT_IMAGE_SIZE ) if fat_image_mbsize > 31: raise KiwiRuntimeError(message.format(fat_image_mbsize)) def check_bootloader_env_compatible_with_loader(self) -> None: """ If there is an environment section as part of the bootloader section, check if the selected loader supports custom environment blobs """ message = dedent( '''\n Selected loader does not support custom environments The selected bootloader {} does not support custom environment settings. Please drop the setting from for this loader ''' ) env_supported_loaders = ['grub2', 'grub2_s390x_emu', 'custom'] bootloader = self.xml_state.get_build_type_bootloader_name() variable_list = ( self.xml_state.get_build_type_bootloader_environment_variables() ) if variable_list and bootloader not in env_supported_loaders: raise KiwiRuntimeError(message.format(bootloader)) def check_kernel_flavor_supported(self) -> None: """ If kernel flavors are configured in the bootloader section, check that the image setup can provide a boot menu entry for the kernel of each flavor. This requires an initrd for each kernel, which is only created with the dracut initrd system, and separate kernel and initrd files, which are not used if the kernel and the initrd are combined into one UKI binary """ message_initrd_system = dedent( '''\n Kernel flavors require the dracut initrd system The bootloader kernel_flavor setting creates an initrd for the kernel of each flavor. This is only supported with the dracut initrd system but the selected initrd system is {} ''' ) message_uki = dedent( '''\n Kernel flavors are not supported with UKI The bootloader kernel_flavor setting creates a boot menu entry for the kernel of each flavor. This is not supported in combination with a UKI binary as configured by the setting ''' ) message_secure_execution = dedent( '''\n Kernel flavors are not supported with zipl secure execution The bootloader kernel_flavor setting creates a boot menu entry for the kernel of each flavor. This is not supported in combination with the zipl secure execution image as configured by the setting ''' ) if self.xml_state.get_build_type_bootloader_kernel_flavors(): if ( self.xml_state.get_build_type_bootloader_name() == 'zipl' and self.xml_state.get_host_key_certificates() ): raise KiwiRuntimeError(message_secure_execution) initrd_system = self.xml_state.get_initrd_system() if initrd_system != 'dracut': raise KiwiRuntimeError( message_initrd_system.format(initrd_system) ) if self.xml_state.get_dracut_config('setup').uefi: raise KiwiRuntimeError(message_uki) def check_kernel_flavors_installed(self, root_dir: str) -> None: """ If kernel flavors are configured in the bootloader section, check that a kernel of each flavor is installed in the given root tree :param str root_dir: root directory path name """ message = dedent( '''\n No kernel installed for kernel flavor(s): {0} The bootloader kernel_flavor setting requires a kernel of each flavor to be installed. Please add the package providing the kernel of the missing flavor(s) to the image description. The installed kernel versions are: {1} ''' ) kernel_flavors = ( self.xml_state.get_build_type_bootloader_kernel_flavors() ) if kernel_flavors: kernel = Kernel(root_dir) missing_flavors = [ flavor for flavor in kernel_flavors if not kernel.get_kernel(flavor=flavor) ] if missing_flavors: raise KiwiRuntimeError( message.format( ', '.join(missing_flavors), ', '.join(kernel.get_kernel_versions()) or 'none', ) ) @staticmethod def _package_in_list( package_list: List[str], search_list: List[str] ) -> str: result = '' for search in search_list: if search in package_list: result = search break return result @staticmethod def _get_dracut_module_version_from_pdb( package_manager: str, package_name: str, root_dir: str ) -> str: tool = Defaults.get_default_packager_tool(package_manager) package_query = None package_manager_query = None package_version = '' if tool == 'rpm': package_manager_query = [ 'chroot', root_dir, tool, '-q', '--qf', '%{VERSION}', package_name, ] elif tool == 'dpkg': package_manager_query = [ 'chroot', root_dir, 'dpkg-query', '-W', '-f', '${Version}', package_name, ] if package_manager_query: try: package_query = Command.run(package_manager_query) if package_query: package_version = package_query.output.split('-', 1)[0] except Exception as issue: log.debug(f'Package manager query failed with: {issue}') return package_version ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/runtime_checker_metadata.yml0000644000000000000000000000045215273675667016221 0ustar00package_names: dracut_oem_dump: - dracut-kiwi-oem-dump - kiwi-dracut-oem-dump dracut_oem_repart: - dracut-kiwi-oem-repart - kiwi-dracut-oem-repart dracut_live: - dracut-kiwi-live - kiwi-dracut-live dracut_overlay: - dracut-kiwi-overlay - kiwi-dracut-overlay ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/runtime_config.py0000644000000000000000000004726315273675667014064 0ustar00# Copyright (c) 2015 SUSE Linux GmbH. All rights reserved. # # This file is part of kiwi. # # kiwi 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 3 of the License, or # (at your option) any later version. # # kiwi 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 kiwi. If not, see # import os import logging import functools from typing import Literal, List, Optional, NamedTuple, Callable, Dict, Any import yaml # project import kiwi.defaults as defaults from kiwi.defaults import Defaults from kiwi.utils.size import StringToSize from kiwi.utils.checksum import Checksum from kiwi.exceptions import ( KiwiRuntimeConfigFormatError, KiwiRuntimeConfigFileError, ) log = logging.getLogger('kiwi') RUNTIME_CONFIG: Optional[Dict[str, Any]] = None # Maximum nesting level the merge of two runtime config files # is allowed to descend into. The runtime config format only # uses a nesting of element -> attribute. The limit prevents an # endless recursion in _merge() on config data that references # itself, which the yaml anchor and alias syntax allows to create MERGE_MAX_DEPTH = 20 class ShasumT(NamedTuple): suffix: str digest: Callable class RuntimeConfig: """ **Implements reading of runtime configuration files:** 1. vendor: /usr/share/kiwi/kiwi.yml + /usr/share/kiwi/kiwi.yml.d/*.yml 2. admin: /etc/kiwi.yml + /etc/kiwi.yml.d/*.yml 3. ~/.config/kiwi/config.yml 4. Check for --config provided from the CLI The KIWI runtime configuration file is a yaml formatted file containing information to control the behavior of the tools used by KIWI on the build host. :param bool reread: reread runtime config """ def __init__(self, reread: bool = False): global RUNTIME_CONFIG if RUNTIME_CONFIG is None or reread: custom_config_file = defaults.CUSTOM_RUNTIME_CONFIG_FILE config_files = [] # 1. vendor config_file = defaults.USR_RUNTIME_CONFIG_FILE if os.path.isfile(config_file): config_files.append(config_file) config_files += self._read_drop_ins_dir( defaults.USR_RUNTIME_CONFIG_DIR ) # 2. admin config_file = defaults.ETC_RUNTIME_CONFIG_FILE if os.path.isfile(config_file): config_files.append(config_file) config_files += self._read_drop_ins_dir( defaults.ETC_RUNTIME_CONFIG_DIR ) # 3. home if self._home_path(): config_file = os.sep.join( [self._home_path(), '.config', 'kiwi', 'config.yml'] ) if os.path.isfile(config_file): config_files.append(config_file) # 4. cmdline if custom_config_file: config_file = custom_config_file if not os.path.isfile(config_file): raise KiwiRuntimeConfigFileError( f'Custom config file {config_file!r} not found' ) config_files.append(config_file) # read and merge all config files... RUNTIME_CONFIG = {} for config_file in config_files: log.info(f'Reading runtime config file: {config_file!r}') with open(config_file, 'r') as config: next_config = yaml.safe_load(config) or {} RUNTIME_CONFIG = RuntimeConfig._merge( RUNTIME_CONFIG, next_config ) def get_credentials_verification_metadata_signing_key_file(self) -> str: """ Return verification metadata signing key file, used for signature creation of rootfs verification metadata: credentials: verification_metadata_signing_key_file: ... There is no default value for this setting available :return: file path name or '' :rtype: str """ signing_key_file = self._get_attribute( element='credentials', attribute='verification_metadata_signing_key_file', ) return signing_key_file if signing_key_file else '' def get_obs_download_server_url(self) -> str: """ Return URL of buildservice download server in: obs: download_url: ... if no configuration exists the downloadserver from the Defaults class is returned :return: URL type data :rtype: str """ obs_download_server_url = self._get_attribute( element='obs', attribute='download_url' ) return ( obs_download_server_url if obs_download_server_url else Defaults.get_obs_download_server_url() ) def get_obs_api_server_url(self) -> str: """ Return URL of buildservice API server in: obs: api_url: ... if no configuration exists the API server from the Defaults class is returned :return: URL type data :rtype: str """ obs_api_server_url = self._get_attribute( element='obs', attribute='api_url' ) return ( obs_api_server_url if obs_api_server_url else Defaults.get_obs_api_server_url() ) def is_obs_public(self) -> bool: """ Check if the buildservice configuration is public or private in: obs: public: true|false if no configuration exists we assume to be public :return: True or False :rtype: bool """ obs_public = self._get_attribute(element='obs', attribute='public') if obs_public is None: # if the privacy attribute is not set we assume to be public obs_public = True return bool(obs_public) def get_package_changes(self, default: bool = True) -> bool: """ Return boolean value to express if the image build and bundle should contain a .changes file. The .changes file contains the package changelog information from all packages installed into the image. bundle: has_package_changes: true|false By default the creation is switched on. When building in the Open Build Service the default is switched off because obs provides a .report file containing the same information. :param bool default: Default value :return: True or False :rtype: bool """ bundle_package_changes = self._get_attribute( element='bundle', attribute='has_package_changes' ) if bundle_package_changes is None: if Defaults.is_buildservice_worker(): bundle_package_changes = False else: bundle_package_changes = default return bool(bundle_package_changes) def get_bundle_compression(self, default: bool = True) -> bool: """ Return boolean value to express if the image bundle should contain XZ compressed image results or not. bundle: compress: true|false If compression of image build results is activated the size of the bundle is smaller and the download speed increases. However the image must be uncompressed before use If no compression is explicitly configured, the provided default value applies :param bool default: Default value :return: True or False :rtype: bool """ bundle_compress = self._get_attribute( element='bundle', attribute='compress' ) if bundle_compress is None: bundle_compress = default return bool(bundle_compress) def get_checksum_handler( self, source_filename: str, target_filename: Optional[str] = None, default: str = '256', bundle_lookup: bool = False, ) -> ShasumT: """ Return a ShasumT with information about the configured shasum suffix name and the digest(Checksum) callable. The following configuration setting allows to configure the size of the checksum: shasum: size: 256 bundle: shasum_size: "256" Instructs kiwi to use the provided shasum size. Supported values are 256 (default) and 512. In case of an unsupported value the default is used. A value from the bundle section takes precedence over the global shasum size specified in the shasum section for creating bundle results. If no information for bundle results is specified, the global shasum size or the default applies. :param str source_filename: filename to calculate checksum for :param str target_filename: filename to write checksum to :param str default: default size set to 256 :rtype: ShasumT """ supported_shasums = { '256': ShasumT( suffix='.sha256', digest=functools.partial( Checksum(source_filename).sha256, target_filename ), ), '512': ShasumT( suffix='.sha512', digest=functools.partial( Checksum(source_filename).sha512, target_filename ), ), } shasum_size = self._get_attribute(element='shasum', attribute='size') if bundle_lookup: bundle_shasum_size = self._get_attribute( element='bundle', attribute='shasum_size' ) if bundle_shasum_size: shasum_size = bundle_shasum_size if supported_shasums.get(shasum_size): return supported_shasums[shasum_size] return supported_shasums['256'] def get_xz_options(self) -> Optional[List[str]]: """ Return list of XZ compression options in: xz: options: ... if no configuration exists None is returned :return: Contains list of options .. code:: python ['--option=value'] :rtype: list """ xz_options = self._get_attribute(element='xz', attribute='options') return xz_options.split() if xz_options else None def get_sync_sparse(self) -> bool: """ Return if data sync operations should turn sequences of null bytes into holes on the target. sync: sparse: true|false If no configuration exists the default from the Defaults class is returned. Enabling this adds the --sparse option to the rsync call used to install the image payload. On filesystems that perform speculative preallocation the option alone does not reclaim the space, because the skipped range is recorded as an allocated but unwritten extent rather than as a hole. Such filesystems can declare additional install time mount options via the sparse_install_mount_options attribute of their FileSystemBase implementation. :return: True or False :rtype: bool """ sync_sparse = self._get_attribute(element='sync', attribute='sparse') if sync_sparse is None: return Defaults.get_sync_sparse() return bool(sync_sparse) def get_container_compression(self) -> bool: """ Return compression for container images container: compress: xz|none|true|false if no or invalid configuration data is provided, the default compression from the Defaults class is returned :return: True or False :rtype: bool """ container_compression = self._get_attribute( element='container', attribute='compress' ) if container_compression is None: return Defaults.get_container_compression() elif 'xz' == container_compression or container_compression is True: return True elif 'none' == container_compression or container_compression is False: return False else: log.warning( 'Skipping invalid container compression: {0}'.format( container_compression ) ) return Defaults.get_container_compression() def get_iso_tool_category(self) -> str: """ Return tool category which should be used to build iso images iso: tool_category: xorriso if no or invalid configuration exists the default tool category from the Defaults class is returned :return: A name :rtype: str """ iso_tool_category = self._get_attribute( element='iso', attribute='tool_category' ) if not iso_tool_category: return Defaults.get_iso_tool_category() elif 'xorriso' in iso_tool_category: return iso_tool_category else: log.warning( 'Skipping invalid iso tool category: {0}'.format( iso_tool_category ) ) return Defaults.get_iso_tool_category() def get_iso_media_tag_tool(self) -> Literal['checkmedia', 'isomd5sum']: """ Return media tag tool used to checksum iso images iso: media_tag_tool: checkmedia if no or invalid configuration exists the default media tagger from the Defaults class is returned :return: A name :rtype: str """ iso_media_tag_tool = self._get_attribute( element='iso', attribute='media_tag_tool' ) if not iso_media_tag_tool: return Defaults.get_iso_media_tag_tool() elif 'checkmedia' in iso_media_tag_tool: return iso_media_tag_tool elif 'isomd5sum' in iso_media_tag_tool: return iso_media_tag_tool else: log.warning( 'Skipping invalid iso media tag tool: {0}'.format( iso_media_tag_tool ) ) return Defaults.get_iso_media_tag_tool() def get_oci_archive_tool(self) -> str: """ Return OCI archive tool which should be used on creation of container archives for OCI compliant images, e.g docker oci: archive_tool: umoci if no configuration exists the default tool from the Defaults class is returned :return: A name :rtype: str """ oci_archive_tool = self._get_attribute( element='oci', attribute='archive_tool' ) return oci_archive_tool or Defaults.get_oci_archive_tool() def get_mapper_tool(self) -> str: """ Return partition mapper tool mapper: part_mapper: partx if no configuration exists the default tool from the Defaults class is returned :return: A name :rtype: str """ part_mapper_tool = self._get_attribute( element='mapper', attribute='part_mapper' ) return part_mapper_tool or Defaults.get_part_mapper_tool() def get_max_size_constraint(self) -> Optional[int]: """ Returns the maximum allowed size of the built image. The value is returned in bytes and it is specified in build_constraints element with the max_size attribute. The value can be specified in bytes or it can be specified with m=MB or g=GB. build_constraints: max_size: 700m if no configuration exists None is returned :return: byte value or None :rtype: int """ max_size = self._get_attribute( element='build_constraints', attribute='max_size' ) return StringToSize.to_bytes(max_size) if max_size else None def get_disabled_runtime_checks(self) -> List[str]: """ Returns disabled runtime checks. Checks can be disabled with: runtime_checks: disable: - check_container_tool_chain_installed if the provided string does not match any RuntimeChecker method it is just ignored. """ disabled_checks = ( self._get_attribute(element='runtime_checks', attribute='disable') or [] ) for check in disabled_checks: log.warning(f'Runtime check: {check}: disabled') return disabled_checks def _get_attribute(self, element: str, attribute: str) -> Any: if RUNTIME_CONFIG: try: for key, value in RUNTIME_CONFIG.get(element, {}).items(): if key == attribute: return value except Exception as issue: raise KiwiRuntimeConfigFormatError( f'{type(issue).__name__}: {issue}' ) def _home_path(self) -> str: return os.environ.get('HOME') or '' def _read_drop_ins_dir(self, config_dir: str) -> List[str]: config_files = [] if os.path.isdir(config_dir): for config_file in sorted(os.listdir(config_dir)): if config_file.endswith('.yml'): config_file_path = os.path.normpath( os.sep.join([config_dir, config_file]) ) config_files.append(config_file_path) return config_files @staticmethod def _merge(master: Dict, slave: Dict, depth: int = 0) -> Dict: if depth > MERGE_MAX_DEPTH: # self referencing config data would let the merge # recurse forever, stop it with an error message raise KiwiRuntimeConfigFormatError( f'Nesting level of {MERGE_MAX_DEPTH} exceeded on merge. ' 'Please check the runtime config file(s) for data ' 'structures referencing themselves' ) if slave: if ( not master or isinstance(master, str) or isinstance(master, int) or isinstance(master, float) ): # border case for first run or if slave is a # primitive type in the recursive iteration master = slave elif isinstance(master, list): if isinstance(slave, list): # list on list gets a merged list master.extend(slave) else: raise NotImplementedError( 'No strategy for merge non-list into list' ) # no duplicates master = sorted(set(master)) elif isinstance(master, dict): # dict on dict gets a merged dict by copy # slave on master recursively if isinstance(slave, dict): for key in slave: if key in master: master[key] = RuntimeConfig._merge( master[key], slave[key], depth + 1 ) else: master[key] = slave[key] else: raise NotImplementedError( 'No strategy for merge non-dict into dict' ) else: # No merge strategy available pass return master ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1794079670.7479773 kiwi-11.1.8/kiwi/schema/kiwi.rnc0000644000000000000000000045160215273675667013405 0ustar00#================ # FILE : kiwi.rnc #**************** # PROJECT : KIWI - Appliance Builder # COPYRIGHT : (c) 2006 SUSE LINUX Products GmbH, Germany # : # AUTHOR : Thomas Schraitle # AUTHOR : Marcus Schaefer # : # BELONGS TO : Operating System images # : # DESCRIPTION : This is the RELAX NG Schema for KIWI # : configuration files. The schema is maintained # : in the relax compact syntax. Any changes should # : made in !! *** kiwi.rnc *** !! # : # : # STATUS : Development #**************** namespace rng = "http://relaxng.org/ns/structure/1.0" namespace xsi = "http://www.w3.org/2001/XMLSchema-instance" datatypes xsd = "http://www.w3.org/2001/XMLSchema-datatypes" namespace db = "http://docbook.org/ns/docbook" namespace sch = "http://purl.oclc.org/dsdl/schematron" namespace nul = "" safe-posix-name = xsd:token {pattern = "[a-zA-Z0-9_\-\.]+"} safe-posix-short-name = xsd:token {pattern = "[a-zA-Z0-9_\-\.]{1,32}"} ecma-119-achar-128-text = xsd:token {pattern = "[a-zA-Z0-9!-/:-?_ ]{1,128}"} locale-name = xsd:token {pattern = "(C|POSIX|[a-z]{2,3}_[A-Z]{2})(,[a-z]{2,3}_[A-Z]{2})*"} mac-address-type = xsd:token {pattern = "([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2}"} size-type = xsd:token {pattern = "(\d*|image)"} number-type = xsd:token {pattern = "\d+"} blocks-type = xsd:token {pattern = "(\d*|all)"} volume-size-type = xsd:token {pattern = "(\d+|\d+M|\d+G|all)"} partition-size-type = xsd:token {pattern = "(\d+|\d+M|\d+G)"} vhd-tag-type = xsd:token {pattern = "[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}"} groups-list = xsd:token {pattern = "[a-zA-Z0-9_\-\.:]+(,[a-zA-Z0-9_\-\.:]+)*"} kernel-flavor-list = xsd:token {pattern = "[a-zA-Z0-9_\-\.]+(,[a-zA-Z0-9_\-\.]+)*"} arch-name = xsd:token {pattern = "(x86_64|i586|i686|ix86|aarch64|arm64|amd64|armv5el|armv5tel|armv6hl|armv6l|armv7hl|armv7l|ppc|ppc64|ppc64le|s390|s390x|riscv64)(,(x86_64|i586|i686|ix86|aarch64|arm64|amd64|armv5el|armv5tel|armv6hl|armv6l|armv7hl|armv7l|ppc|ppc64|ppc64le|s390|s390x|riscv64))*"} portnum-type = xsd:token {pattern = "(\d+|\d+/(udp|tcp))"} grub_console_output = xsd:token {pattern = "(none|console|gfxterm|serial|vga_text|mda_text|morse|spkmodem)( (console|gfxterm|serial|vga_text|mda_text|morse|spkmodem))*"} grub_console_input = xsd:token {pattern = "(none|console|serial|at_keyboard|usb_keyboard)( (console|serial|at_keyboard|usb_keyboard))*"} fs_attributes = xsd:token {pattern = "(no-copy-on-write|synchronous-updates)(,(no-copy-on-write|synchronous-updates))*"} package-version-type = xsd:token {pattern = "(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])(\.(0|[1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])){3}"} simple-uri-type = xsd:token {pattern = "(file:|this:|https:|http:|ftp:).*"} oci_format_type = xsd:token {pattern = "(oci|docker):(gce|ova|qcow2|vmdk|vdi|vhd|vhdx|vhd-fixed|raw)"} #========================================== # start with image description # start = ## The start pattern of an image k.image #========================================== # main block: # div { k.image.name.attribute = k.imagename.attribute k.image.displayname.attribute = k.displayname.attribute k.image.noNamespaceSchemaLocation.attribute = ## The location of the XSD Schema (not relevant for RELAX NG or DTD) attribute xsi:noNamespaceSchemaLocation { xsd:anyURI } k.image.schemaLocation.attribute = ## A pair of URI references: First is a namespace name, ## second the location of the XSD Schema ## (not relevant for RELAX NG or DTD) attribute xsi:schemaLocation { xsd:anyURI } k.image.schemaversion.attribute = ## The allowed Schema version (fixed value) attribute schemaversion { "8.5" } k.image.id = ## An identification number which is represented in a file ## named /etc/ImageID attribute id { text } k.image.attlist = k.image.name.attribute & k.image.displayname.attribute? & k.image.id? & k.image.schemaversion.attribute & ( k.image.noNamespaceSchemaLocation.attribute? | k.image.schemaLocation.attribute? )? k.image = ## The root element of the configuration file element image { k.image.attlist & k.include* & k.certificates? & k.description & k.preferences+ & k.profiles* & k.users* & k.drivers* & k.strip* & k.repository* & k.containers* & k.packages* & k.extension? } } #========================================== # common attributes # k.id.attribute = ## An ID attribute id { xsd:ID } k.name.attribute = ## A name attribute name { text } k.bootinclude.attribute = ## Indicates that this package should be part of ## the boot image (initrd) too. This attribute ## can be used to include for example branding packages ## specified in the system image description to become ## part of the boot image also attribute bootinclude { xsd:boolean } k.bootdelete.attribute = ## Indicates that this package should be removed from ## the boot image (initrd). the attribute is only ## evaluated if the bootinclude attribute is specified ## along with it too attribute bootdelete { xsd:boolean } k.displayname.attribute = ## A friendly display name. Used in the boot menu attribute displayname { text } k.arch.attribute = ## A system architecture name, matching the 'uname -m' information ## Multiple architectures can be combined as comma separated list ## e.g arch="x86_64,ix86" attribute arch { arch-name } k.description.attribute = ## A short description attribute description { text } k.path.attribute = ## A path attribute path { text } k.profiles.attribute = ## A profile name which binds the section to this name attribute profiles { text } k.password.attribute = ## The password attribute password { text } k.script.attribute = ## A script hook for meta files to be called after the ## file was fetched attribute script { text } k.source.attribute = ## A source location where a package or configuration file can be found attribute source { text } k.size.attribute = ## A partition size or optional image size attribute size { size-type } k.dest.attribute = ## Destination of a resource attribute dest { text } k.username.attribute = ## A name of a user attribute username { text } k.imagename.attribute = ## An image name without / and spaces attribute name { safe-posix-name } k.clear.attribute = ## Specifies to clear or not some data or configurations attribute clear { xsd:boolean } div { k._any.attribute = ## Any attribute including in any attribute in any namespace. attribute * { text } k._any = ## Any element from almost any namespace except empty ones element * - (nul:*) { (k._any.attribute | text | k._any)* } } #========================================== # common element # div { k.extension.attlist = empty k.extension = ## Define custom XML extensions element extension { k.extension.attlist, k._any+ } } #========================================== # common element # div { k.archive.name.attribute = k.name.attribute k.archive.bootinclude.attribute = k.bootinclude.attribute k.archive.target_dir.attribute = # Extract tarball to the specified target directory. # By default the target directory is the root(/) of # the image. attribute target_dir { text } k.archive.attlist = k.archive.name.attribute & k.archive.bootinclude.attribute? & k.archive.target_dir.attribute? k.archive = ## Name of an image archive file (tarball) element archive { k.archive.attlist, empty } } #========================================== # common element # div { k.author.attlist = empty k.author = ## Author of the image element author { k.author.attlist, text } } #========================================== # common element # div { k.license.attlist = empty k.license = ## License of the image element license { k.license.attlist, text } } #========================================== # common element # div { k.bootloader-theme.attlist = empty k.bootloader-theme = ## Image bootloader theme setup. element bootloader-theme { k.bootloader-theme.attlist, text } } #========================================== # common element # div { k.bootsplash-theme.attlist = empty k.bootsplash-theme = ## Image bootsplash theme setup. element bootsplash-theme { k.bootsplash-theme.attlist, text } } #========================================== # common element # div { k.contact.attlist = empty k.contact = ## Contact Information from the Author, like Email etc. element contact { k.contact.attlist, text } } #========================================== # common element # div { k.file.name.attribute = k.name.attribute k.file.arch.attribute = k.arch.attribute k.file.target.attribute = # include file as target to the root(/) of the image. attribute target { text } k.file.owner.attribute = # set ownership to the file. The value is passed to chown attribute owner { text } k.file.permissions.attribute = # set file permissions. The value is passed to chmod attribute permissions { text } k.file.attlist = k.file.name.attribute & k.file.target.attribute? & k.file.owner.attribute? & k.file.permissions.attribute? & k.file.arch.attribute? k.file = ## A Pointer to a File element file { k.file.attlist, empty } } #========================================== # common element # div { k.ignore.name.attribute = k.name.attribute k.ignore.arch.attribute = k.arch.attribute k.ignore.attlist = k.ignore.name.attribute & k.ignore.arch.attribute? k.ignore = ## Ignores a Package element ignore { k.ignore.attlist, empty } } #========================================== # common element # div { k.keytable.attlist = empty k.keytable = ## Image keytable setup. element keytable { k.keytable.attlist, text } } #========================================== # common element # div { # locale k.locale.attlist = empty k.locale = ## Image locale setup. element locale { k.locale.attlist, locale-name } } #========================================== # common element # div { k.rpm-locale-filtering.content = xsd:boolean k.rpm-locale-filtering.attlist = empty k.rpm-locale-filtering = # locale-filtering sets the install_lang macro for rpm based # installations to the configured locale list. This results # in language specific files to become filtered out by rpm # if they don't match the configured list. Please note it # depends on the package design if the install_lang macro # contents apply to the package or not. element rpm-locale-filtering { k.rpm-locale-filtering.attlist, k.rpm-locale-filtering.content } } #========================================== # common element # div { k.namedCollection.name.attribute = k.name.attribute k.namedCollection.arch.attribute = k.arch.attribute k.namedCollection.attlist = k.namedCollection.name.attribute & k.namedCollection.arch.attribute? k.namedCollection = ## Name of a Pattern for SUSE or a Group for RH element namedCollection { k.namedCollection.attlist, empty } } #========================================== # common element # div { sch:pattern [ id = "collection_module_scope" sch:rule [ context = "collectionModule" sch:assert [ test = "../@type='bootstrap'" "collectionModule is only available in the bootstrap "~ "packages section" ] ] ] sch:pattern [ id = "collection_module_disable_constraint" sch:rule [ context = "collectionModule" sch:assert [ test = "@enable='true' or not(@stream)" "@stream attribute is not allowed to disable a module" ] ] ] k.collectionModule.name.attribute = attribute name { safe-posix-name } k.collectionModule.stream.attribute = attribute stream { safe-posix-name } k.collectionModule.arch.attribute = k.arch.attribute k.collectionModule.enable.attribute = ## Specify the module to become enabled (for the optionally ## given stream) or disabled. attribute enable { xsd:boolean } k.collectionModule.attlist = k.collectionModule.name.attribute & k.collectionModule.enable.attribute & k.collectionModule.stream.attribute? & k.collectionModule.arch.attribute? k.collectionModule = element collectionModule { k.collectionModule.attlist, empty } } #========================================== # common element # div { k.oem-boot-title.attlist = empty k.oem-boot-title = ## For oemboot driven images: setup of the boot menu text ## displayed within the square brackets after first reboot ## of the OEM image element oem-boot-title { k.oem-boot-title.attlist, text } } #========================================== # common element # div { k.oem-bootwait.content = xsd:boolean k.oem-bootwait.attlist = empty k.oem-bootwait = ## For oemboot driven images: halt system after image dump true/false element oem-bootwait { k.oem-bootwait.attlist, k.oem-bootwait.content } } #========================================== # common element # div { k.oem-resize.content = xsd:boolean k.oem-resize.attlist = empty k.oem-resize = ## activate/deactivate disk resize on first boot: true/false ## By default the repart/resize is activated when creating ## an oem image. If the disk image should be more simple ## and should not react on storage geometry changes at all ## this option allows to switch off the use and inclusion ## of the kiwi oem dracut module that implements the resize element oem-resize { k.oem-resize.attlist, k.oem-resize.content } } #========================================== # common element # div { k.oem-resize-once.content = xsd:boolean k.oem-resize-once.attlist = empty k.oem-resize-once = ## For oem images: repart/resize only on first boot: true/false ## By default the repart/resize happens on every reboot and ## therefore also allows for disk geometry changes during the ## livetime of the machine. If set to false the repart/resize ## operation happens only once and then never again element oem-resize-once { k.oem-resize-once.attlist, k.oem-resize-once.content } } #========================================== # common element # div { k.oem-ramdisk-size.content = text k.oem-ramdisk-size.attlist = empty k.oem-ramdisk-size = ## For oemboot driven images: specify the size of the ramdisk ## The value is taken into account if the deployment of the ## image happens into a ramdisk. The value can be overwritten ## with the kernel boot parameter: ramdisk_size element oem-ramdisk-size { k.oem-ramdisk-size.attlist, k.oem-ramdisk-size.content } } #========================================== # common element # div { k.oem-device-filter.content = text k.oem-device-filter.attlist = empty k.oem-device-filter = ## For oemboot driven images: filter install devices by given ## regular expression. The expression is handled by the bash regexp ## operator element oem-device-filter { k.oem-device-filter.attlist, k.oem-device-filter.content } } #========================================== # common element # div { k.oem-nic-filter.content = text k.oem-nic-filter.attlist = empty k.oem-nic-filter = ## For oemboot driven images: filter network interface names by given ## regular expression. The expression is handled by the bash regexp ## operator. Interface names matching the rule will be skipped. All ## other interface names stay in the list. It is also possible to ## pass the variable kiwi_oemnicfilter as kernel command ## line in a netboot deployment element oem-nic-filter { k.oem-nic-filter.attlist, k.oem-nic-filter.content } } #========================================== # common element # div { k.oem-inplace-recovery.content = xsd:boolean k.oem-inplace-recovery.attlist = empty k.oem-inplace-recovery = ## For oemboot driven images: Specify whether the ## recovery archive should be stored as part of the image ## or not. If it's not stored it's created during install ## of the oem image element oem-inplace-recovery { k.oem-inplace-recovery.attlist, k.oem-inplace-recovery.content } } #========================================== # common element # div { k.oem-kiwi-initrd.content = xsd:boolean k.oem-kiwi-initrd.attlist = empty k.oem-kiwi-initrd = ## For oemboot driven images: use kiwi initrd in any case ## and don't replace it with mkinitrd created initrd element oem-kiwi-initrd { k.oem-kiwi-initrd.attlist, k.oem-kiwi-initrd.content } } #========================================== # common element # div { k.oem-partition-install.content = xsd:boolean k.oem-partition-install.attlist = empty k.oem-partition-install = ## For oemboot driven images: install the system not as ## disk but into a free partition. If this option is set ## all other oem-* options concerning the partition table ## will not have any effect element oem-partition-install { k.oem-partition-install.attlist, k.oem-partition-install.content } } #========================================== # common element # div { k.oem-reboot.content = xsd:boolean k.oem-reboot.attlist = empty k.oem-reboot = ## For oemboot driven images: reboot after first deployment true/false element oem-reboot { k.oem-reboot.attlist, k.oem-reboot.content } } #========================================== # common element # div { k.oem-reboot-interactive.content = xsd:boolean k.oem-reboot-interactive.attlist = empty k.oem-reboot-interactive = ## For oemboot driven images: reboot after first deployment ## with an interactive dialog true/false element oem-reboot-interactive { k.oem-reboot-interactive.attlist, k.oem-reboot-interactive.content } } #========================================== # common element # div { k.oem-recovery.content = xsd:boolean k.oem-recovery.attlist = empty k.oem-recovery = ## For oemboot driven images: create a recovery archive yes/no element oem-recovery { k.oem-recovery.attlist, k.oem-recovery.content } } #========================================== # common element # div { k.oem-recoveryID.attlist = empty k.oem-recoveryID = ## For oemboot driven images: Set the partition ID of ## recovery partition. Default value is 83 (Linux) element oem-recoveryID { k.oem-recoveryID.attlist, xsd:nonNegativeInteger } } #========================================== # common element # div { k.oem-recovery-part-size.attlist = empty k.oem-recovery-part-size = ## For oemboot driven images: Set the size of the ## recovery partition. Value is interpreted as MB element oem-recovery-part-size { k.oem-recovery-part-size.attlist, xsd:nonNegativeInteger } } #========================================== # common element # div { k.oem-shutdown.content = xsd:boolean k.oem-shutdown.attlist = empty k.oem-shutdown = ## For oemboot driven images: shutdown after first deployment ## true/false element oem-shutdown { k.oem-shutdown.attlist, k.oem-shutdown.content } } #========================================== # common element # div { k.oem-shutdown-interactive.content = xsd:boolean k.oem-shutdown-interactive.attlist = empty k.oem-shutdown-interactive = ## For oemboot driven images: shutdown after first deployment ## true/false element oem-shutdown-interactive { k.oem-shutdown-interactive.attlist, k.oem-shutdown-interactive.content } } #========================================== # common element # div { k.oem-silent-boot.content = xsd:boolean k.oem-silent-boot.attlist = empty k.oem-silent-boot = ## For oemboot driven images: boot silently during the initial boot ## true/false element oem-silent-boot { k.oem-silent-boot.attlist, k.oem-silent-boot.content } } #========================================== # common element # div { k.oem-silent-install.content = xsd:boolean k.oem-silent-install.attlist = empty k.oem-silent-install = ## For oemboot driven images: do not show progress of the image ## dump process, true/false element oem-silent-install { k.oem-silent-install.attlist, k.oem-silent-install.content } } #========================================== # common element # div { k.oem-skip-verify.content = xsd:boolean k.oem-skip-verify.attlist = empty k.oem-skip-verify = ## For oemboot driven images: do not perform the checksum ## verification process, true/false element oem-skip-verify { k.oem-skip-verify.attlist, k.oem-skip-verify.content } } #========================================== # common element # div { k.oem-vmcp-parmfile.content = text k.oem-vmcp-parmfile.attlist = empty k.oem-vmcp-parmfile = ## For oemboot driven images: provide the name of a parmfile ## which is loaded via cmsfscat on s390 systems. Default value ## is set to: PARM-S11 element oem-vmcp-parmfile { k.oem-vmcp-parmfile.attlist, k.oem-vmcp-parmfile.content } } #========================================== # common element # div { k.oem-multipath-scan.content = xsd:boolean k.oem-multipath-scan.attlist = empty k.oem-multipath-scan = ## For oemboot driven images: turn on or off the search ## for multipath devices: true/false (default is true) element oem-multipath-scan { k.oem-multipath-scan.attlist, k.oem-multipath-scan.content } } #========================================== # common element # div { k.oem-silent-verify.content = xsd:boolean k.oem-silent-verify.attlist = empty k.oem-silent-verify = ## For oemboot driven images: do not show progress of the image ## verification process, true/false element oem-silent-verify { k.oem-silent-verify.attlist, k.oem-silent-verify.content } } #========================================== # common element # div { k.oem-swap.content = xsd:boolean k.oem-swap.attlist = empty k.oem-swap = ## For oemboot driven images: use a swap partition yes/no element oem-swap { k.oem-swap.attlist, k.oem-swap.content } } #========================================== # common element # div { k.oem-swapsize.attlist = empty k.oem-swapsize = ## For oemboot driven images: Set the size of the swap ## partition in MB element oem-swapsize { k.oem-swapsize.attlist, xsd:nonNegativeInteger } } #========================================== # common element # div { k.oem-swapname.attlist = empty k.oem-swapname = ## For oem images: Set the name of the swap space ## The name of the swap space is used only if the ## image is configured to use the LVM volume manager. ## In this case swap is a volume and the volume takes ## a name. In any other case the given name will have ## no effect. element oem-swapname { k.oem-swapname.attlist, text } } #========================================== # common element # div { k.oem-systemsize.attlist = empty k.oem-systemsize = ## For oemboot driven images: Set the size of the system (root) ## partition or LVM partition in MB. If not provided the partition ## will grow to the maximum available free space on the disk element oem-systemsize { k.oem-systemsize.attlist, xsd:nonNegativeInteger } } #========================================== # common element # div { k.oem-unattended.content = xsd:boolean k.oem-unattended.attlist = empty k.oem-unattended = ## For oemboot driven images: don't ask questions if possible ## true/false element oem-unattended { k.oem-unattended.attlist, k.oem-unattended.content } } #========================================== # common element # div { k.oem-unattended-id.attlist = empty k.oem-unattended-id = ## For oemboot driven images: use the specified disk id ## the device is looked up in /dev/disk/by-* and /dev/mapper/* element oem-unattended-id { k.oem-unattended-id.attlist, text } } #========================================== # common element # div { k.product.name.attribute = k.name.attribute k.product.arch.attribute = k.arch.attribute k.product.attlist = k.product.name.attribute & k.product.arch.attribute? k.product = ## Name of a Product From openSUSE element product { k.product.attlist, empty } } #========================================== # common element