././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1748336309.304142 kiwi-10.2.24/.bumpversion.cfg0000644000000000000000000000025715015277265012677 0ustar00[bumpversion] current_version = 10.2.24 commit = True tag = True [bumpversion:file:pyproject.toml] [bumpversion:file:kiwi/version.py] [bumpversion:file:doc/source/conf.py] ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1748336309.304142 kiwi-10.2.24/LICENSE0000644000000000000000000010451315015277265010574 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 . ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1748336309.304142 kiwi-10.2.24/Makefile0000644000000000000000000001327215015277265011230 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 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 $(python) helper/completion_generator.py \ > ${buildroot}usr/share/bash-completion/completions/kiwi-ng # kiwi default configuration install -d -m 755 ${buildroot}etc install -m 644 kiwi.yml ${buildroot}etc/kiwi.yml # kiwi old XSL stylesheets for upgrade install -d -m 755 ${buildroot}usr/share/kiwi 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 git_attributes: # the following is required to update the $Format:%H$ git attribute # for details on when this target is called see setup.py git archive HEAD kiwi/version.py | tar -x clean_git_attributes: # cleanup version.py to origin state # for details on when this target is called see setup.py git checkout kiwi/version.py setup: poetry install --all-extras docs: setup poetry run make -C doc man html docs_suse: setup poetry run make -C doc xml rm -rf doc/build/restxml mv doc/build/xml doc/build/restxml 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 \ 'pip install pytest-container && pushd test/scripts && pytest -s -vv' check: setup # shell code checks 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 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 5 \ --doctest-modules --no-cov-on-fail --cov=kiwi \ --cov-report=term-missing --cov-fail-under=100 \ --cov-config .coveragerc' 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 patches cp package/*.patch 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: clean_git_attributes rm -rf dist rm -rf doc/build rm -rf doc/dist ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1748336309.304142 kiwi-10.2.24/README.rst0000644000000000000000000000442415015277265011256 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=1748336309.3051422 kiwi-10.2.24/build-tests.sh0000755000000000000000000001052615015277265012365 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 the kiwi box plugin # # $ pip install --upgrade kiwi-boxed-plugin # 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} --" 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} --" 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=1748336309.3371441 kiwi-10.2.24/doc/Makefile0000644000000000000000000001727615015277265012005 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 " 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: 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=1748336309.3371441 kiwi-10.2.24/doc/source/.images/intro.png0000644000000000000000000004454115015277265015024 0ustar00PNG  IHDR R/sBITUF pHYsTT(tEXtSoftwarewww.inkscape.org<HIDATxgTGCGEQQ,`kFcoD4h^w{EX#DQivca1;^es}ٽ;3;}a޹C|,,r ?}阴G!Ԃ~ae<㸇۸kaf<eҎT.͡ g,r.йwc%:s;3#h@$U&Hr!H# G2α4 ec'1aM%8lI]y;؁xKpk@$uMHog'p:FDRæmQ2 چ[<)"l:q!{$xImcw{??1k?+60;lņ@I8896#tGxFَQqg21g,[.|$hh .za-,C'"i~>i~\>/H:Gf^6'\h&r;/ӿ҃ˑȓ=]ս\2*8גVzLӤ=W_+7WŽ87O$C"I 78 j%5'P6tos K@*50c@4mlGAs|=ؙ?(؁Ľ .ؾԆ'n'p4«~#M.t4K9b?qp6T~1P+s W|iLjSypԪZ<>DjyuԊk1.c_:f4v|.=DjO0vjL^dƳ[NishԤ[pțe, G0 Sϩ0uYv9&Eoa: ߭0}W,fb,ABwzsϘXbŘ}g ',4p ^K.?Ž(F3=I@#]v9w.\U)Vh8M%a\GXy7#Bju@، #&k~?F% s?%Uy ! )} Wpy8EKD[΄@ByC`Y|J< {vuk7F`3܎T2 !BX:HKBxS*~vay@XcSvp]_"4:j)!|i3S\n^~ =``]vX3FRd*t5Af%FZ(ڀG2'?20 awaOZ@ > B8^/ 3kq O K)nb #|N.}/p8|`Izq!<ۀ.aؑlW[ыux_@kU'chs4Tߗ<̀\G+xزk&sQ`G`q^#n Ԟ#K!lBօ3ȣe >,n!͋KM!|Aw+2\ 9n&|V/aV f5F-{*9GM)q%ϕ^:0ʳe^rGKVRꥀ\EuץٛR|{pv#gB i'=!֩ 9Yoi*ɾv~"z;؉؉ >;?ȈR@Dxux}τ6TL;0s3=oړΜHw^#RIXn'z|=B8z[tFܧtJmu/ɍ| B8KH;NĒE@>aCn'!y/a3 aYǓywko7b*d|!-ZGH?B1:tƴ ҋH@%BR@ IkLwQOzI) Ɓ]) !0/;>xqZ#B`e &.Ž) `5.:IlpO!֦S[3NSNs$ [ Ţ[>%NӽB8t`h.v#{syH^ |jݗ)F@>d|OaWG\NpkiI]EX^pN=3ɰݮI'˜R۝5R;Źp{G҅*u@cXo;KT݉+K׾iZ@ViLf@6&qs2- 6gQb8F^*\tgba;nh#CRgu_cЗm3[|8$U؊9h;Ԑp#p {"Qc8ɸVΌaY"o4g%G# F/uZuė1Is5CVN<ܖ_2!Tx޶?ˇHRnwZqWbL}ʀHjbo~(Wc֩ o.&x*VrB>L*XlyB<<\DRؤj}*{{%M~z+nӇJl7]CMo@$u `j^;=48xVDRؠ1B+t6 :އv^}TFb?rZqw"#l:7u߭m~|MĖ(I0 lx[8<3U )[90;IyCvD{8#Syc@$#tȯp"n:wȑ_h@$|-l©\hV1zpvo ;_O;%l魘lƵ81 $fW6fybJC@gg)a'ne ϝܖs Jz5I"i=i .ADvNg?6eYi߶0[p(W3qtks#Oe;ZC$fy8mDd0˷_փ 9 ,F7Xj_wHah c"Wۜ*CF;]ٛBn qHam1L|Vic I:'>>$dv8I$bCH&wPzQ'gv+:}L?[Sy]"N/!qfaVn*.geW{Iyگ? o-,Ϫ 8'4I,{,hȀHt/k82 *}! IN-<ɀH| INdnGCDR{ŸѐT^ 5u4d@$U:WBҎ J'&‡hȀHtZeB;2 *}x>c+GCDRS—hȀHtBw92 *Ν^|+GCDR3 IO3/.w,d@$U>]|q+ IN/!"i;_r4d@$U:3Bx IN /!Ž J'jf!" hȀHtuBu IN !|掆 J'<+tLd@$U2!GCDRç&>؉g_hȀH|2 L^L Oœ|!"7{; 2 $"I$I$I$ɀH $ɀH $ɀHd@$IDd@$IDd@$I2 $"I2 $"I2 $ɀH ~\]KīҌyQ}aSz;W 4]vuV oye@d@Kr^ 8 Ԙ2 2 Rc2,""5< ﳚ3ˀȀH 2 2 1 1 1 1 DDj;a6goN~l\3q%"RrNE#(}ʇL~D2 2 um(f1u?fKDdOn^Kp ȴMd^#FӀHDYpUO ɀ+}n@$*xQWwɀr5r 4 MX'W lɀȀ̼i|I+\h@$* aWw߀HDۚ:`c0Yo1C`I:o`6f0{7 \ߏ;g*8Rwes.?Âf ƭ^bvb7 ha- ze9  o{OeyU H[ ҁά'cٟZ|uyӍ'ϴtLI`9=YRY@>j) ݘ$b&9y{Yt/`"o ]Kr(x˘Gĉes٬?H[}C%JXx)<9_8S$&{3])!0I\˲ <!07a 2Jw/1]$d)nUg$fvumZ@VyeO&kySC.i-!9;ZzU:n!ΗŏC}"N<0ݕ/nҒ|L!_egٍ˦{B!t|])[z] !_3GKXL!KSưL _p)!G9»a{B;#J /p$g!_?Kptg lKXDd*?2 !!%qR`k1eBnBX,p/!yR@v$B`qBf6Y a2Љ8g- Zd6 Bٻ/n;yVeR@bUXc ^gR@ܕr{*~zF97'Znќ3"W32#Fksi<- lRA6Q H|+gSqC3:gtqQYtr`vf@tܟy3X|[1 ӭ?o4w + o]8دnė{KQgU& t*}N ;CXd[ ˥NWu/2B5W!gҩš]@g|/3&E@MsC Y/C6<|ȐbwC-{,ey59*ls%rYFN 2_y"{Hl;l_\frcϐ,NilVdrV5#@M ȤLiL@7ťg (nm3SỬQ2} a@ ke2}Fe@V3>!ГOO 鞁DN4"٘yBV/-_6dtrbY5Ԥ&˥G!;/jJ/tmr) e_~e٩ɚ!&-bB!)2d⥫BVLMjtryt29-5Qeͬ -}Uk y v*—Uڜd"!,di9tf?bY9?9(C a 0+ mgtfϯ_^ceNm@R{5=S@FY,e@jE 3 /Ԛ9b?!w^--*dԤ&ϖ^̪]!۔wei9 w]6ٷ==gVS3C)߬ Jepj#8 d3v(쎲־,]7'sV3*C>/f>˦ז¢\8X?iw> 3 ە]eRL=kgR@V eW%ee#NMv y7K@Fdt MuC6!/Nؤ|U1+ a0!ŋNs3!ņ/CR{;8o;6dCž!ЍSyWVF9fܱ؛!ٟM?ropN؆gZs&SL 0CRBΙn_) _: +CJM٥TOg5aGlRe+y"gedjrnA 2rFi鹥} =--?C*{uv~2:7 0\)svHlSr}N!KR: ]C~rqV fjR!}sQ==B+q)7dyuwߕNY0MMOt3"CUZ,3sgdo Y-t~okȎy55[(wl!.[>d\[tͼpJ4 T|o_։9븐C&rs9hT<} ߵ);f]sp:[=$rMX1q.jrw=d<_z6wK3l^mdz,U,_%wQɳ٨t3ŀ̰No|un@YHq ٪)[3KO-5<2٥ܜy35y>O795wp欺99MwUMjd<\z1)8)Jg!7rt/˽r^Nɭ H=k V:|3 Rǣ901Ae}+jg֮Tsx\ʣsa|Y7#vU靁l}} 'à%k!K'7 MZ7f;맼+\U p3F8s)آ9oulv({EGtO9$ŀ4y_8븇8ݪyQ1G7͉xlm@d@ :r<DĀH(Kp՞؀ȀȀÀȀux\΃lˀȀux<& H s45Z7D}\!`(h@d@| K2 2 D2 2 2 2 2 2 2 1 1 1     sˀȀHĀHDĀH ϻTg@ w#&1 ڕq#FP1 ڕTc Q֫nH21 ^wC7jX]1 /IWb@T+

Q#C^E#A0!0 r%s ; Dy/U}`@u~a2򩃡Y?xء0 +k'V ό]K_!)Dx+: 2 $"ɀHD2 d@d@$ID2  d@$"ɀȀH2 2 d@$"ɀHDd@d@$ɀHD2  $""ɀȀH2  d@$"IDDɀHD2  d@$ID2  d@$"ɀH2 2 d@$"ɀHDr:IDD d@$"ɀH2 2 $"ɀHD2 4m"SvDd@$U6ò9,ȀHl"οX+X!"҉%ms!"҉؇wo5 ʧ!"8w4d@$U>O,L'GCDRSq3>"82 2{!XȀHjnב԰]HȀHjd<ő԰ɸ "qnpADRçtd@X={M`Ϫ1m]Xַ\7]|/R'OWTGgw}k]B"qS#e)Yc@$BM}4Rfj_! |y7j.ǀ-2np`Qs~)TcSX9d@TOB5S2 ֕qATc<Qծ;_7j<QZ; v~UGDUiFPMp<2 ΀<A5_ȀȀHĀHD㴩< p.a rz8 ԰FvF =vN o2ʀȀH 2 2 R+""5< j?9 ԘL3ˀȀH ;,2 2 Rk8 d@ d@d@ d@d@ d@d@d@d@d@     1 2 R +~,a@$"RrnMӲ^\V4 kC.Yx*r/KɀȀo${P=(t6 D.ͫ|Y8zɀȀ 4 _ ɀӈ@/">eιyQBٍ?ʑu,=*I7( 덺e;HjRw΋B73=݊LQ٠36Fթ͖:a}sUl(]rݐSCV+sh) WkK?;,5y3 kJMzw^.{A[zIsw k-]dQv=+dߵ}-Y?ǔW|5y Ԯ\*K[a9Ml5 !rhȊ97[[C~Xg@V++~7dL}q,@6٬t" ?67*rN 2{=}$.qDY*]KɷL YR[fZv֟'Vv١9;o&gnj)diw[Yȹ=_z3 uW*0 ȝu->SmJ,P3D'ҥrogxy!+tZ۸6䛩IMQ_`<雿/zgL(?y=/]SJfk"ug xOt 0o7΢3&͐Sf.]9, Kq3WȞ)fMkrleTLtM\?¯fDd gjriṊy)5y-W<_dBE}j'ݦ 9!CFYp|$y!fw3w9fKUobxJ˜ 뎺A>J l?ӎiGs" GeyfwJn7 iCJY(S[IK}s3/|.Irv*XWKaκْiW^?dHr4Z2 /Z S35 QU$_/14 ꘳nd@d@8RU}~oY7[2 2 uoP+M9zɀX §gκْ&? /uwJ7=f{/x㟾݀ȀȀ4tC"cywXwoκْdc2_!Sx#Y:笛-7%rp<{I5ĀȀȀȀȀHĀHDĀHDĀHDDDDDDD2 D2 2 D2 2 D2 2 2 2 2 ĀȀHĀHDĀHDĀHDDDDĀɀɀȀɀȀȀȀȀȀ1nj5O/ҍob@T+ڻFP1 ڕrTcMʂĀz-r_R#.ĀWB5MS*e@ <C5 u0 DU2ͻ~D5,]WĀ 9,od@ n\pGV}G^gDNofTwXoU2 DUjˠX0 IuX8 W#b@TĀɀ!K]Vt sAPL`@TaPU6; j[j]=3aP}]AàFU*:]p {]w5GA0 r%]?Bt D  {iᩎ O'fZ""i|IoGED63,c""i8/_NDDRe}#""xt.ȀHl"X J'L-}s!"җ82 *hȀH|*^|c!"!S_o I /A Mƫ M>#!"aq.ADd@$I2 $"I2 $"I2 $I$I$I $ɀH $ɀH $IDd@$IDd@j> ۳[w@2."J|ɕ݆=q4s4r5)'i%mLrr3=[Ér7YhH E"3)&736ag9}{?׃o߻}<ߕ"ΏХۤAQ)X8c}{KC RcpqEا3NӧϔHQ)]8oMhLuEsy:ۑ :t7bc7_,0 :/oFe(X+Di=W3tE,If "bxȧuÆL%ks&wî!~r+*G-VG8_Ģpw-["N> R)HQ™lmMsE#X{;TG{ws◡xO_MMRxk4a7vt#GKE6g#|-ѯ_ET)J E1^K9ִ!=F W5cK,7Cea?}u (x+IXaEe7Y0 bbpĆaV)J E1f-b럻^qDَב# dKv}^@sqat_ЩRpk6=">)p-Z(v.Ӽp7VS*W֡S(qHX)\SQ)W8S}V'GS/6Xk$'ElivR7~q_f.U"ua (XT[~a6WBڏ垭ehiY9,23=:|}u3"̴nqyxHKKgX/ҲT@bPFzv^;f&سgs"E;?UBIENDB`././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3371441 kiwi-10.2.24/doc/source/.images/kiwi-logo.png0000644000000000000000000004165215015277265015572 0ustar00PNG  IHDRaaϣbKGD pHYs.#.#x?vtIME  IDATxܽy\}۪h @jq%EI۲lɱ=>>dIډsΜ23ؓr<ؓۉ=8lM-B-/wWuo BmS@uAxiƿgOk<!' l "@N,^OG63yǟo!S'~iZ+58 Gpbhru! -Sk!,I2+EȜsq8 Eo&>'cӓ8OXI)m14vHGi$VH!~s@Cd;dblqIq̹cǟ/WOg!>~o*ZcfsΓ XGfr2c!@ VO{HݶĢVs1Nk ~w'_&m[_&+K\n&:s,Y5X8u8Wa!Zj<>H'7 nXp! =T j%CfJ{ _~|f_~%cO1$%4V.X"(f8\oE XY#=<%5_"b6\$¤Y&F+U< 3pGO7`<r~!WwY``r.#I CbX0#w6 @JAO{J$Ykid9ɱaC_kO{ևw c+t-NrR/=~!~cS tKcsq.ap aP&t6i!¡ȭ#7sދ~H))A GɏD&4pΒCYFrkp2-5Z Zz\5/ts_zGœ35a8~ -J)/oL)t^V@H|]2uv8Vb{! AC sJR }BϧDWFqHޙdyFn /()=򵇧u@H Α[COl9-ӏϼx^(ty^SD:!C,!(*!A =Nȍq'isє!4b(̟ iL1H҄vҥݍc"]Ӝ$;~}/<8_L|fCΗ.욢cuXW0qZ)C7h=Dmʉ sZn }l*ZW텺 z@0X$k sKnmτ~GSkgfO~|Lf>0||'~6:2.l\Ͼ[+ɍ !kqJ3&1GXgQ؇J䁀v;0αnF{Rxx&^~DO?AJ@u99cm1g6} (~k#z3sxnV:DKO(P nalNbNMI )5PRB+C`XKn{ɭce-gH QDU βX[a~E-BTZIZ,$a/$ K#DAXkXkQ c@!{ZOk*Qs% $YN\R)C8lU~Ӎ=.~7?|_&ft`0>ᇯ|lqraA 2rRBBת;56Z|י]Ya6Yȍ%M-8RZ"$$BvPѴ)$+BWB)Ec,AQJzJu"=[{/G>>oִ~ \>_;< a)%DAH) (!RH_Gi9'3ֺ*/{vgr0B*u,7INhH#{U2CX5DcAu[f8d%737IGi G)(6MAHD:D_h+1Rۑg]?~`{?3=zR?L! Xu93YAJNWѪF.Rd:  ع*CB 0:A_)@+BU$D 43@IQJC̐8hk Iӊ( < (E%0$#{2fO g=rLlHM1pZ jK֖[;:2C䬬uٳcOnv" ZEB=-dR <ƇUfut_czZDaCJRЫBι"0 !$iYMzyp7ֹ<Ο}7PxpÀdt) <z8ZiUֵ9]e乥R)"InzU*t:.?W&vL?tum' Ոz3& iю hƘIuAra]2R pM4>̪S)kZy(Rh.ZȲqO+hԣ2V[ <z:Юk6SV֘?eVDYsɖ7Ơ&s9}j|ܙ\|er/p˽;9if.=R i`lb0 H9ևCܺ&pvR}ӳ5kmJ!ZEnLNy@PA8G(UJi@-h{0̿Bq#[wfp|@Hwt]~Ŗvk1K9Uh5[̟1zsuXol\$ jFx/3yqqF:ȥ9Rab@ZLlsܰiRWҞ/kD <|ϻѺDwyW–FMr֚1If(I\oaatOEhtt6 tx#ȷһ蓏MB `t=n)BQE9((FfS6Fi,aTk:#4V[̝q,{'*)Mt{&{ep$2a9s/,/rͷUB|-L4B4U")&Gv‗^|s4[ OUxJq j͘$ Ialh_ɍa@nD̈́qtGd#PM/g葩Sj3+y2%" d]Wmp5ꋅi~췺Ubbjr/&u\8wپJ2{]sIǾ6҅cysͶ=|5 W6 &3V]I̸q~XcivV4ZB`-$#ӂd=8OXo}='Ϟ}5͙"^SVi5ABV!%222w1|S_9;^4[G|C\*9j%:f~Zju~҈;{ 'nZ̞W@;nrk[)Y~ k,OhtHa (I[%ZwfYփEzЈ}>ى}ݩה-{O6eZw)WF qܦHa Bڟ>ZXZYdj&tN{G.Ø>e$SyoFkkysN'CE&,`rGҼHSgnJCf b@%`/$39i4'9Q(G~Q=O#p|꣏O<է>#Hq5%0wݼj!QqgƷ}dσ˺"4Da %3|]"4U*S4:R fT뜟5^yk/Btu۩<_%.-5&9@_Q5̭4iuiC,G)+~R"BI0LR#WHذF-.*WB ]+-߰5UZnʢ e'BJ<Px2퐥>J;3ͳ\6|$/xy+1J}%ٽG}?S4Y^?{/qqA[|29Vv% J@Hd% {aT;C7|t'f_B&.c6ߥH62l*IQ2;LnߡzBIy\zV-A6C_aN0)߾m9/q*V8Ɉo];&{җsr9^=qskE@s˅Nspc-J@hz:oVb#;rvI,LMwK]sY\`;\\NE užwk#{dyf7Y,_~ttIoh>p=J rpb̮[Gx{ع柞[5з{Ƹ}ϲ蠄 g0}Z!ZKŤ9H!z%0:Qa/}>B )$kYlxN czؖ4FC1q]UTG#Q0OzO6WI3+-N)@9'`g.Q&pe`[Ğۆ9 V(vq̧>t?؉Bkk5B/B_U y@>pkaoXZpM{x+_z nF{S _4$KhwfpU,IR ѯ3Ow/źz뛧־ o5Iz8RyPwL*asli[BqfV bs;x9mwu!}c3/ڠ.o8Z|48S3r)EO8yFㄥ4 w;GY.79zzq&96J)gw&-Z5qb# ||-|REXl]9(C~)\ Rjf6RdZě +eU_"<{e_^`mT< lCS][(GL:đ/_!'O(-5;z:ŵ !y3RcFHkJܶ>o|oSsGhwju4,x{ݫM )P<(EeJƙ^.;d!%EM"TiH!nnνl{88+~`KjwŎ} {y!Qex擧'9˩l?g7BmŎ6GWfP"7]T(,֦H((RehW=?rZ/$ +[tR$d!J~VYP+K& =>Z n&E ^ 5 " FZ Dk5i^ԁS;'Hj)ոzwg-^1u0QI~Оdph/n\WoҰ^;m 5\$]"v3JT%5g;"p⬥IYwHRCx_+ 1k G0H#1PbBxmjMOTX]EiGG*7jhO*G4F8)`m!.z;sd2C7ijS6x1y5L8/!ʕ€3T &2:%J~?.IS/4v\ 6`f6P^z,thJpL%DBx#EiQYEIXt $6첖r{,(=ŕP_с&\ofxg[ uxq~ Il9D%X)P3eїqTYN6B71Z·[f@:R)F$IN%uH-{ =s*֦lz= 8-BYLs)u!F78$^!f9Y 3yhw1BB\5!Qd\&O}t'KiyQ)n@o?{WC7si4vo|8s-vTq$BJ\E J# <!Ejb%I҂,cVڬ>]RInBvThWI!٢QQ,^T8x}&76c@>+ZC \3[z [kX3_hnly8^^yhx~bfJ@K*ר-q,7۬6:46yb>oW{R TBƇ+W"qMKrA<É Y+8o%(l[@#fIN_[ *Ce8$1|v/ ˴Pam\~d-K q+6W&Z30qm?|O#MS <4'?5p-v?ȭfQN(?  3ZKۿjZnSCYqrM_9`tD_䣄!+P.#e :WTk9RYL)o\"V }9ln3 Tˉf5r;:P{Dʃ>ɒZ/yvis[rmɛOQY%PJm>7Ïޱ:92E$YY-O0<ϵ_ǭBjWcnu5ڝ.YEfLjɧD_BHV44LW+<-F 7"jn-@pnlb)bgE*zҐ'=MvK`f_;3<5Ώ4o{G?qÓ|wg݆yr1;.^ i1Pct@8)4j1#ーwr}`6KŬ6,Xcդ'a$CkxE*qV8R!El FxebyhP]$7 24Y?9g5Fj{Iio7>8)ᅪȏ/"/y>KGxwN_:w{ZٛVPX{dtp]U?w_F'dl`j?c$YZAYYi҉8kQd!+YM µ)FXcO/6^iM=b-qශ=}ߣN?5\"2v} A0rsrYT4|7qC{+ZǎCgןЂq/yY.-qfvpoW^Ypv"/>}eF62P+UhuZZkMLn1(!Hph K]~suE#\+͓v~cyן^T(}K qvlƁbjŅY_?wϮ)rP嵓oы\|~/o7޵PGhQ<|S`D1+r ݲ9{@~ M.w5loWc^qwRPhuq1Oo玃>B f\am\}711؏E ϋځBz(0G86.$dC>Q̔zQCSMgJ3w^3vN^/O= a7*Bꢠ!M-OFJ}˩yp5jE~h{O~nϗbI1S/rĝ?ګ;A).\Ki7|dF|i(PڤI5`#Ie4䦘 Q?L|뽧*uӔ.d*?N*}g_kZِ/7_,4Z;-g>dN}yOҠ'!_n4Zcv9E 1&c[8Bn (!:TԠoh)彪ʦ}zH< N~\KsgfwQ㕣Gx~K4Ǭ94N0PVw& Vؾ࿚}}p{w]G$sT$cf3X'~֯|?*~񅲞/L0R*;mcqyi׋ zf0rofG)ދS4~Y#ŘO&a^{Sh˝3OywspAA977WS >Ymrkq_Wԅ.@d|`G__{ۿ-'''o|Y-+_S?J `'#*`\pk)^te BbeDZLZҒ/,Ӯ8 ^~k!X㰙EjRlq*d{su6me&nƼ~q=15@t4+q7uU3)wrC5r)3ywKyY?z<6U964Ӷɛ-Q Pu̡}g-7ܿhbpj=+x`2#See֞zmffHjY[Rx/-, Oe9Ξ"3)w˶1!'Ο0r͸Ixhٵ_űُN'f ƀzmb}#aTJE %yYCTsV[̬IذZ|Y̅"65F.!(%EDȍnԳtgUr1JeZ:F9qѿo}ZcN_ngbp$ ;E'n[ȏBʢYdsj9m>?ЛDtibVɛ]sYljזly%&u+q.)bv1$@P*Y9&c\LJ(+_zYJ8 W?R[ mD)zd%t VuÃy`KKJgX\sǐvsG! upkuDGK(!\eݔ'\J'w #CUigێSj[$dGusϽw%$!| I Y8;Li 6 Pϴ?!Cqfiv3. ئ=`Sl !!#iW}!i 6t3wv{ ⚦#Br¼DY (3`Sz2*1mM_(\P`!6h^(WW}UK>I珕e&c2m6!11ܦ8$6Q/4,!7 H`ɂ6, a]2#!'ٝ2ݢ=?8Q?B*1)¸BgZlJ5𫟜#Ogblaߥ^<N~1n0H,OE$$'.&0TU{ BUtGQ]hmLaFDǿ`ru6"3@zسgohJ$K|4wK~   yaZmoL؜=Fpr!Qy5:ia &Q錆YN\: QNϔ`7PH獕G,,*`2'ܗ4ӎB--/Tk.PJ1 0322`ydv2xvOʶ3*15zUnFh#vwo_m+$K Q*E,x-OPɊ4&Ԛ[is}y SZL\ VX+YI- _}g d\6l-y2 Cy䩣s?,Q8a P1' J}U*$edR۶QE % ycHA  ʷf$p eIQԪ\E ar8 8f.\]+xkO?5kL}˖-n'm$Go $E,.U.tAJò'##n^vbQnOlUMdq~y#߽HV%pJ Q_i;,` "?Rb0w5۔Fj ~/fW\ h,MS;(hw1%K XqY_-y߀t tY|Ŧ>1Ү1I*}ϸ J6fw]}^9'\*v[>Mɂ.g/h^dڃ pd{kF^pG!n^D\Z*en"D|k&7#*/൰RF1@<p6Un_]!ܫS N,zyG˒-_n[2 Sԉ+TU [\+]: 4 !avki *KT<)R; "A"-Gg;: մV Ө tGG n}g=톚"LTgͱ<ۚi J&x58!;Gf,ྖĘ")ȸ %MAޘEZ{yqƷ}Q B9ۉ?:/v٠()8+qrV(y%ط.՟@So^jW]QF|uD=YIDATբâŪ'ꑌ3ᨲ:}nK_*77`$ꝟo_]Go0)Ψrjb C#2*W̼#9Þk'$ zUn|@9aNP 8c8+ns <9T߮aSz["ʫ¨Ɇ%ߺyɥ{;UJ9ԑ%s9R~lsRJēGGsBaضm8Y8_ CJc9@d/8q(jR"ɬz 7Ȍ}C^x]V^=/ۅB!{H7>p1xXĴl Pk.۶܉w@?0{=\*RQ *8EQm۶zs^ BAc:o$7 Tlx&uVIDJ)QFesΛJ`~~EK&=+W\gϞs ͛Oԓ̲,֚ 7Nlذ[n#dv˖-~g#ئMg(PP*ą m˲֞={ 0;qZl۶mX~e[)eRuwwŋci` 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. * ``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 of the `USER` directive of a :file:`Dockerfile`. * ``workingdir``: Sets the working directory to be used when running `cmd` and `entrypoint`. Equivalent of the `WORKDIR` directive in a :file:`Dockerfile`. `containerconfig` child tags: * ``subcommand``: Provides the default execution parameters of the container. Equivalent of 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 of the `ENTRYPOINT` directive in 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. Make sure you have checked out the example image descriptions (see :ref:`example-descriptions`). #. Include the ``Virtualization/containers`` repository into your list (replace the placeholder `` with the name of the desired distribution): .. code:: bash $ zypper addrepo http://download.opensuse.org/repositories/Virtualization:/containers/ container-tools #. Install :command:`umoci` and :command:`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=1748336309.3381443 kiwi-10.2.24/doc/source/building_images/build_enclave.rst0000644000000000000000000000641515015277265020306 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 which includes the kernel, initrd and the kernel commandline 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}.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 AF_VSOCK socket. This can be done via a ProxyCommand setup of the host 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 login to the enclave with a custom vsock port as follows: .. code:: bash $ ssh root@21.vsock ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3381443 kiwi-10.2.24/doc/source/building_images/build_expandable_disk.rst0000644000000000000000000003753515015277265022015 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: * build an expandable disk image * deploy an expandable disk image * run 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. Make sure you have checked out the example image descriptions (see :ref:`example-descriptions`). 2. 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 which contains the disk image and is capable to install 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 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 a 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 equals 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 disk image on 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 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 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 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 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 setup 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.xz` file to a temporary directory, and copy the initrd and kernel images to the PXE server. a) Unpack installation tarball: .. code:: bash mkdir /tmp/pxe && cd /tmp/pxe tar -xf {exc_image_base_name_disk}.x86_64-{exc_image_version}.install.tar.xz b) Copy 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 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 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 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 gets deployed into a ramdisk which 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 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 they 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 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`. 4. Create a target disk. Follow the steps above to create a virtual target disk. 5. 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 to setup a network bridge on the host system and connect 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 losr list of optional `oem` 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 (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 (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 (default value is false). Before the reboot, a message is displayed, and it and 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 (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 (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 (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 be created. By default, no swap partition is created. This value is represented by the ``kiwi_oemswap`` variable in the initrd. 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 a name of the swap volume. oemconfig.oem-swapsize Specifies the size of the swap partition. If a swap partition is created while the size of the swap partition is not specified, KIWI calculates the size of the swap partition, and creates a swap partition at initial boot time. In this case, the swap partition size equals the double amount of RAM of the system. This value is represented by the ``kiwi_oemswapMB`` variable in the initrd. oemconfig.oem-systemsize Specifies the 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, 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. 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. ``kiwi_oemunattended`` 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 of the image to the target disk. The verification process computes the checksum of the image installed to the target. This value is then compared to the initrd embedded checksum generated at 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=1748336309.3391442 kiwi-10.2.24/doc/source/building_images/build_kis.rst0000644000000000000000000000700115015277265017447 0ustar00.. _kis: Build KIS Image (Kernel, Initrd, System) ======================================== .. sidebar:: Abstract This page explains how to build an image that consists out 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 optional `system root filesystem`, {kiwi} provides this as the universal `KIS` type. The former `pxe` image type still exist, 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 the :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 file system. 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: - `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) then also include a `root=` parameter that references this filesystem image UUID. Whther 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 neither information nor the reference to the root UUID, if the `filesystem` attribute is used. All other attributes of the `type` element that applies 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_pxe} \ --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_pxe}.*-{exc_image_version},if=virtio,driver=raw \ -serial stdio .. note:: For testing the components of a KIS image normally requires a deployment infrastructure and a deployment process. 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 only kept for compatibility reasons. For details, see :ref:`build_legacy_pxe` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/building_images/build_live_iso.rst0000644000000000000000000001437115015277265020502 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 a 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 overayfs. If set to a value different from `squashfs`, the root filesystem is written into a filesystem image of the specified type, and the filesystem image 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 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 how to setup 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 how to setup RAM only deployments in {kiwi} see: :ref:`ramdisk_deployment` Overlay based on overlayfs Usable with both dracut modules. The readonly root filesystem is overlaid with a readwrite filesystem using the kernel overlayfs filesystem. Overlay based on device mapper snapshots Usable with the `dmsquash` module. A squashfs compressed readonly root is overlaid with a readwrite 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 only in combination with 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 auto detects if it is used on a disk or ISO scan loop booted from a file. If booted as disk, persistency is setup on a new partition of that disk. If loop booted from file, persistency is setup on a new cow file. The cow file/partition setup can be influenced with 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 setup `dmsquash` are marked with `rd.live` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/building_images/build_simple_disk.rst0000644000000000000000000002603415015277265021173 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: - define a simple disk image in the image description - build a simple disk image - run 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 disabled. An example configuration for a 42 GB large VMDK image with 512 MB RAM, an IDE controller and a bridged network interface is shown below: .. code:: xml 42 false The following attributes of the `type` element are 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`. - `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 setup 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 futher 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`: 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 distribute them in an archive called 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 values are `zvm`, `powervm`, `xen` and `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` (= `i585` and `i686`) and `x86_64`. - `xen_loader`: the Xen target loader which 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 VIM GuestOS. For instance, 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 is possible to add another bridged interface or to specify the driver wto 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} doesn **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 disks and disk controllers for the virtual machine. This element can be specified for each disk or disk controller present. 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` format only). Supported values are: `ide`, `buslogic`, `lsilogic`, `lsisas1068`, `legacyESX` and `pvscsi`. - `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 IDE and SCSCI CD/DVD drives to the virtual machine using the `vmdvd` element for the `vmdk` image format. The following example adds two drives: one with a SCSCI and another with a IDE 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=1748336309.3391442 kiwi-10.2.24/doc/source/building_images/build_wsl_container.rst0000644000000000000000000001341515015277265021536 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 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 :command:`appx` 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 :command:`WSL-DistroLauncher`. This component consists out 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 meta data required for the :command:`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` is changed by {kiwi} if the :file:`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 to be signed off with a digital signature. If the image is build in the Open Build Service (OBS), this is done automatically. Outside of OBS, you must o 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} doesn not validate the specified name string, because there is no common criteria for various the 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` ifor validity or completeness. The following example shows how to build a WSL image based on openSUSE Tumbleweed: 1. Check the example image descriptions, see :ref:`example-descriptions`. #. Include the ``Virtualization/WSL`` repository to the list ((replace `` with the desired distribution)): .. code:: bash $ zypper addrepo http://download.opensuse.org/repositories/Virtualization:/WSL/ WSL #. Install :command:`fb-util-for-appx` utility and the package that provides the :command:`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 Servic,e make sure to add the packages from the zypper command above to the project configuration via :command:`osc meta -e prjconf` along with the line :file:`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 :command:`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 setup the Windows machine, see: `Windows Subsystem for Linux `__ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3381443 kiwi-10.2.24/doc/source/building_images.rst0000644000000000000000000000106015015277265015501 0ustar00.. _building_types: Building Images for Supported Types =================================== .. note:: This document provides an overview how to build and use the {kiwi} supported image types. All images that we provide for testing uses the root password: `linux` .. toctree:: :maxdepth: 1 building_images/build_live_iso building_images/build_simple_disk building_images/build_expandable_disk building_images/build_container_image building_images/build_wsl_container building_images/build_kis building_images/build_enclave ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/commands/image_info.rst0000644000000000000000000000652115015277265016264 0ustar00kiwi-ng image info ================== .. _db_image_info_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng image info -h | --help kiwi-ng image info --description= [--resolve-package-list] [--list-profiles] [--print-kiwi-env] [--ignore-repos] [--add-repo=...] [--print-xml|--print-yaml] kiwi-ng image info help .. _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 to run through the list of packages, providing more detailed information about the image description. .. _db_image_info_opts: OPTIONS ------- --add-repo= Add repository with given source, type, alias and priority. --description= The description must be a directory containing a kiwi XML description and optional metadata files. --ignore-repos Ignore 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 the processing the requested image information, which could lead to an error. --list-profiles list 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 print 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 which are known at the beginning of a build process. --resolve-package-list Solve package dependencies and return a list of all packages including their attributes, for example size, shasum, and more. --print-xml Print 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 similar 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 loaded on demand. If the module is missing, requests to convert to other format than XML fail. * Update of an old image description to the latest schema ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/commands/image_resize.rst0000644000000000000000000000220415015277265016624 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 -h | --help kiwi-ng image resize --target-dir= --size= [--root=] kiwi-ng image resize help .. _db_kiwi_image_resize_desc: DESCRIPTION ----------- 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. 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 the root directory in build/image-root under the specified target directory. --size= 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= Directory containing the kiwi build results. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/commands/kiwi.rst0000644000000000000000000001375415015277265015140 0ustar00kiwi-ng ======= .. _db_commands_kiwi_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng -h | --help kiwi-ng [--profile=...] [--temp-dir=] [--type=] [--logfile=] [--logsocket=] [--loglevel=] [--debug] [--debug-run-scripts-in-screen] [--color-output] [--config=] [--kiwi-file=] image [...] kiwi-ng [--logfile=] [--logsocket=] [--loglevel=] [--debug] [--debug-run-scripts-in-screen] [--color-output] [--config=] result [...] kiwi-ng [--profile=...] [--shared-cache-dir=] [--temp-dir=] [--target-arch=] [--type=] [--logfile=] [--logsocket=] [--loglevel=] [--debug] [--debug-run-scripts-in-screen] [--color-output] [--config=] [--kiwi-file=] system [...] kiwi-ng -v | --version kiwi-ng help .. _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. Appliance is a small, text-based image including a predefined remote source setup to allow 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 - file system 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 Use 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 specified runtime configuration file. If not specified, the runtime configuration is expected to be in the :file:`~/.config/kiwi/config.yml` or :file:`/etc/kiwi.yml` files. --debug Print debug information on the command line. Same as: `--loglevel 10`. --debug-run-scripts-in-screen Run scripts called by {kiwi} in a screen session. --logfile= Specify 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= Send log data to the specified Unix Domain socket in the same format as with `--logfile`. --loglevel= Specify 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 above the specified level. .. code:: bash ---------------------------- | Level | Numeric value | ---------------------------- | CRITICAL | 50 | | ERROR | 40 | | WARNING | 30 | | INFO | 20 | | DEBUG | 10 | | NOTSET | 0 | ---------------------------- --profile= Select 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. --shared-cache-dir= Specify 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 meta data. The default location is `/var/cache/kiwi`. --temp-dir= Specify an alternative base temporary directory. The provided path is used as base directory to store temporary files and directories. Default is `/var/tmp`. --target-arch= Specify 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= Select an image build type. The specified build type must be configured as part of the XML description. --kiwi-file= Basename of 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 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=1748336309.3391442 kiwi-10.2.24/doc/source/commands/result_bundle.rst0000644000000000000000000000433515015277265017037 0ustar00kiwi-ng result bundle ===================== .. _db_kiwi_result_bundle_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng result bundle -h | --help kiwi-ng result bundle --target-dir= --id= --bundle-dir= [--bundle-format=] [--zsync_source=] [--package-as-rpm] [--no-compress] kiwi-ng result bundle help .. _db_kiwi_result_bundle_desc: DESCRIPTION ----------- Create 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= Directory containing the bundle results, compressed versions of image results, and SHA checksum files. --bundle-format= Specify 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 result image filename. --target-dir= Directory containing the {kiwi} build results. --zsync_source= 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 stays compressed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3391442 kiwi-10.2.24/doc/source/commands/result_list.rst0000644000000000000000000000132715015277265016537 0ustar00kiwi-ng result list =================== .. _db_kiwi_result_list_synopsis: SYNOPSIS -------- .. code:: bash kiwi-ng [global options] service [] kiwi-ng result list -h | --help kiwi-ng result list --target-dir= kiwi-ng result list help .. _db_kiwi_result_list_desc: DESCRIPTION ----------- List 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=1748336309.3391442 kiwi-10.2.24/doc/source/commands/system_build.rst0000644000000000000000000002004515015277265016667 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 -h | --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=...] [--delete-package=...] [--set-container-derived-from=] [--set-container-tag=] [--add-container-label=

The location of the XSD Schema (not relevant for RELAX NG or DTD) A pair of URI references: First is a namespace name, second the location of the XSD Schema (not relevant for RELAX NG or DTD) The allowed Schema version (fixed value) 8.3 An identification number which is represented in a file named /etc/ImageID The root element of the configuration file
An ID A name 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 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 A friendly display name. Used in the boot menu A system architecture name, matching the 'uname -m' information Multiple architectures can be combined as comma separated list e.g arch="x86_64,ix86" A short description A path A profile name which binds the section to this name The password A script hook for meta files to be called after the file was fetched A source location where a package or configuration file can be found A partition size or optional image size Destination of a resource A name of a user An image name without / and spaces Specifies to clear or not some data or configurations
Any attribute including in any attribute in any namespace. Any element from almost any namespace except empty ones
Define custom XML extensions
Name of an image archive file (tarball)
Author of the image
License of the image
Image bootloader theme setup.
Image bootsplash theme setup.
Contact Information from the Author, like Email etc.
A Pointer to a File
Ignores a Package
Image keytable setup.
Image locale setup.
Name of a Pattern for SUSE or a Group for RH
collectionModule is only available in the bootstrap packages section @stream attribute is not allowed to disable a module Specify the module to become enabled (for the optionally given stream) or disabled.
For oemboot driven images: setup of the boot menu text displayed within the square brackets after first reboot of the OEM image
For oemboot driven images: halt system after image dump true/false
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
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
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
For oemboot driven images: filter install devices by given regular expression. The expression is handled by the bash regexp operator
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
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
For oemboot driven images: use kiwi initrd in any case and don't replace it with mkinitrd created initrd
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
For oemboot driven images: reboot after first deployment true/false
For oemboot driven images: reboot after first deployment with an interactive dialog true/false
For oemboot driven images: create a recovery archive yes/no
For oemboot driven images: Set the partition ID of recovery partition. Default value is 83 (Linux)
For oemboot driven images: Set the size of the recovery partition. Value is interpreted as MB
For oemboot driven images: shutdown after first deployment true/false
For oemboot driven images: shutdown after first deployment true/false
For oemboot driven images: boot silently during the initial boot true/false
For oemboot driven images: do not show progress of the image dump process, true/false
For oemboot driven images: do not perform the checksum verification process, true/false
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
For oemboot driven images: turn on or off the search for multipath devices: true/false (default is true)
For oemboot driven images: do not show progress of the image verification process, true/false
For oemboot driven images: use a swap partition yes/no
For oemboot driven images: Set the size of the swap partition in MB
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.
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
For oemboot driven images: don't ask questions if possible true/false
For oemboot driven images: use the specified disk id the device is looked up in /dev/disk/by-* and /dev/mapper/*
Name of a Product From openSUSE
A commandline option specification
A shim setup option specification
A install command option specification
A config command option specification
Name of an image Package
apk apt zypper dnf4 dnf5 microdnf pacman Name of the Package Manager
Name of the Package Manager
Description of how this profiles influences the image Import profile by default if no profile was set on the command line Profiles creates a namespace on an image description and thus can be used to have one description with different profiles for example KDE and GNOME including different packages.
The profile name required as part of the current profile definition. Requires is used to set profiles dependencies, with it a profile definition can be composed by other existing profiles.
Name of registry source server Use container with specified container backend podman docker
Container name Container path, default to '/' if not specified Container tag, defaults to 'latest' if not specified Only fetch the container but do not activate the loading of the container at first boot
$attr attribute is only available for the following repository types: $types Type of repository apk apt-deb apt-rpm deb-dir mirrors rpm-md Alias name to be used for this repository. This is an optional free-form text restricted to characters from the POSIX standard. If not set the source attribute value is used and builds the alias name by running a checksum digest of the defined URI of the repository. An alias name should be set if the source argument doesn't really explain what this repository contains. Distribution components, used for deb repositories. If not set it defaults to main Distribution name information, used for deb repositories Specify whether or not this repository should be configured in the resulting image. Boolean value true or false, the default is false. Specify whether or not this repository should be configured in the resulting image without using it at build time. Boolean value true or false, the default is false. Specify whether or not this specific repository values the result of the repository signature validation. The default value is false. Specify 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. Specify the path to a customization script. The script receives the repo file according to the used package manager as a parameter such that custom modifications can be placed when needed Channel priority assigned to all packages available in this channel (0 if not set). If the exact same package is available in more than one channel, the highest priority is used Channel password if required. It depends on the url type whether and how this information is passed Channel username if required. It depends on the url type whether and how this information is passed Specify the source type of the repository path. Depending on if the source path is a simple url or a pointer to a metadata file or mirror list, the configured package manager needs to be setup appropriately. By default the source is expected to be a simple repository url baseurl metalink mirrorlist Specifies for which architecture(s) this repository is supposed to provide packages. Multiple architecture names needs to be separated by a comma The Name of the Repository
Specify path to a signing key for this repo The signing element holds information about repo/package signing keys
A Pointer to a data source. This can be a remote location as well as a path specification
Sets the used package manager to validate, or not, the repository and/or package signatures. The behavior can be slightly different depending on the used package manager. This is a system wide package manager option, so that, this value can be overwritten by repository specific configurations. The default value is false.
Setup if the package manager should exclude docs files during package installation. This option could be ignored according to the used package manager.
Image license setup. The specfied license name will be displayed in a dialog window on boot.
The unit of the image size M G Specifies the image empty space that will not be partitioned unpartitioned size must be smaller or equal to the defined size Specifies if the size value is absolute or added on top of the current data size Specifies the Size of an Image in (M)egabyte or (G)igabyte If the attribute additive is set the value will be added to the required size of the image
A detailed description of this image and what it can be used for.
Specify Volume group name, default is kiwiVG. This information is only used if the LVM volume management is used Prefer LVM even if the used filesystem has its own volume management system Specify volumes and size attributes
Setup Image Timezone setup
$attr attribute is only available for the following image types: $types $attr attribute must be set for the following image types: $types <vagrantconfig> section is required for the vagrant format $attr attribute also needs the setting: <oem-resize>false</oem-resize> in the <oemconfig> section of the selected <type> $attr attribute must be set for embed_verity_metadata $attr attribute must be set for embed_integrity_metadata Specifies the path of the boot image (initrd) description provided with KIWI Specifies the kernel boot profile defined in the boot image description. When kiwi builds the boot image the information is passed as add-profile option Specify the image is a Xen dom0 (Xen Server) image The information is used to create a correct bootloader configuration with regards to the required loading of the Xen Hypervisor activate the quota system if the filesystem is btrfs based. By default the quota system is inactive Tell kiwi to explicitly make a volume the default volume This can be either (/) or the root subvolume or the root snapshot depending on the specified btrfs configuration attributes. By default btrfs_set_default_volume is set to: true If no default volume should be set, this attribute can be used to turn it off 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 '<volume name="@root=NAME"/>' By default the creation of a root subvolume is set to: true Tell 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 no snapshots are used Tell kiwi to set the btrfs root filesystem snapshot read-only Once all data has been placed to the root filesystem snapshot it will be turned into read-only mode if this option is set to true. The option is only effective if btrfs_root_is_snapper_snapshot is also set to true. By default the root filesystem snapshot is writable Specifies the image blocksize in bytes which has to match the logical (SSZ) blocksize of the target storage device. By default 512 byte is used which works on many disks However 4096 byte disks are coming. You can check the desired target by calling: blockdev --report device 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 On systems using selinux use the given policy. by default 'targeted' is used targeted mls minimum 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 Specify mount point for spare partition in the system. Can only be configured for the disk image type oem Specify filesystem for spare partition in the system. Can only be configured for the disk image type oem btrfs ext2 ext3 ext4 xfs Specify filesystem attributes for the spare partition. Attributes can be specified as comma separated list. Can only be configured for the disk image type oem Specify if the spare partition should be the last one in the partition table. 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 For images with a separate boot partition this attribute specifies the size in MB. If not set the min bootpart size is set to 200 MB For images with an EFI fat partition this attribute specifies the size in MB. If not set the min efipart size is set to 20 MB For images with an EFI layout, specify if the legacy CSM (BIOS) mode should be supported or not. By default CSM mode is enabled 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 min efifatimage size is set to 20 MB Specify 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 needs to be created in an msdos table For images with an EFI firmware specifies the partition table type to use. If not set defaults to gpt partition table type. msdos gpt Specifies the boot profile defined in the boot image description. When kiwi builds the boot image the information is passed as add-profile option Specifies whether the image output file should be compressed or not. This makes only sense for filesystem only images. Specifies which method to use in order to get persistent storage device names. By default by-uuid is used. by-uuid by-label by-partuuid Specifies the path to a script which is called right before the bootloader is installed. The script runs relative to the directory which contains the image structure Specifies the path to a script which is called right after the bootloader is installed. The script runs relative to the directory which contains the image structure Specifies the root filesystem type btrfs ext2 ext3 ext4 squashfs erofs xfs Specifies the compression type for erofs Specifies the compression type for mksquashfs uncompressed gzip lzo lz4 xz zstd Specifies to create a dm verity hash from the number of given blocks and placed at the end of the root filesystem If the verity hash should be calculated from the complete file the value 'all' can be specified. In combination with the verity_blocks attribute, embed a verification metadata block at the end of the partition serving the root filesystem Specifies to create a standalone dm integrity layer on top of the root filesystem In combination with the standalone_integrity attribute, embed an integrity metadata block at the end of the partition serving the root filesystem In combination with the standalone_integrity attribute, Allow to use old flawed HMAC calculation (does not protect superblock) In combination with the standalone_integrity attribute, protect access to the integrity map using the given key. In combination with the embed_integrity_metadata attribute, allows to specify 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: ':basename_of_integrity_keyfile_no_file_extension' Specifies to use an overlay root system consisting out of a squashfs or erofs compressed read-only root system overlayed using the overlayfs filesystem into an extra read-write partition. Available for the disk image type oem Specifies the filesystem type to use as read-only filesystem in an overlayroot setup. By default squashfs is used squashfs erofs Specifies to create an extra read-write partition in combination with the overlayroot attribute on the target disk. By default the partition is created and the kiwi-overlay dracut module also expect it to be present. However, the overlayroot feature could also be used without dracut (initrd_system="none") and under certain circumstances it is handy to configure if the partition table should contain the read-write partition or not. Available for the disk image type oem Specifies the size in MB of the partition which 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 happens after this calculation, which cannot be guaranteed as there is at least one optional script hook which is allowed and applied after the calculation. Specifies the boot firmware of the system. Most systems uses a standard BIOS but there are also other firmware systems like efi, coreboot, etc.. This attribute is used to differentiate the image according to the firmware which boots up the system. It mostly has an impact on the disk layout and the partition table type. The default firmware setting is based on the architecture. If no action should be taken on behalf of a firmware setting or kiwi's default selection, set the firmware to: custom bios ec2 efi uefi ofw opal custom specify if an extra boot partition should be used or not. This will overwrite kiwi's default layout if an extra boot partition is required this attribute specify which filesystem should be used for it. The type of the bootloader might overwrite this setting btrfs ext2 ext3 ext4 xfs fat32 fat16 Specifies 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 squashfs and the device mapper. Please note both modules supports a different set of live features. overlay dmsquash Specifies the format of the virtual disk. gce ova qcow2 vagrant vmdk vdi vhd vhdx vhd-fixed Specifies the format of the virtual disk. aws-nitro Specifies additional format options passed on to qemu-img formatoptions is a comma separated list of format specific options in a name=value format like qemu-img expects it. kiwi will take the information and pass it as parameter to the -o option in the qemu-img call Force use of MBR (msdos table) partition table even if the use of the GPT would be the natural choice. On e.g some arm systems an EFI partition layout is required but must not be stored in a GPT. For those rare cases this attribute allows to force the use of the msdos table including all its restrictions in max partition size and amount of partitions Specifies the filesystem mount options which also ends up in fstab The string given here is passed as value to the -o option of mount Specifies options to create the filesystem Will trigger the creation of a partition for a COW file to keep data persistent over a reboot Set the filesystem to use for persistent writing if a hybrid image is used as disk on e.g a USB Stick. By default the ext4 filesystem is used ext4 xfs for gpt disk types only: create a hybrid GPT/MBR partition table specify which initrd builder to use, default is dracut initrd architecture. kiwi dracut none Specifies the image type btrfs cpio docker ext2 ext3 ext4 iso oem pxe kis squashfs erofs tbz xfs oci appx enclave wsl Specifies a path to additional metadata required for the selected image type or its tools used to create that image type Specifies the bootloader default boot entry for the initial boot of a kiwi install image. failsafe-install harddisk install 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. Specifies if the bootloader menu should provide an failsafe entry with special kernel parameters or not Specifies if a install iso should be created (oem only) Specifies if a install stick should be created (oem only) Specifies if all data for a pxe network installation should be created (oem only) Specifies if the bootloader menu should provide an mediacheck entry to verify ISO integrity or not. Disabled by default and only available for x86 arch family. The kernelcmdline element specifies additional kernel command line options Specify LUKS version. This can be either set to "luks", "luks1" or "luks2". By default "luks" is used. luks luks1 luks2 Setup cryptographic volume along with the given filesystem using the LUKS extension. The value of this attribute represents either the password string or the location of a key file if specified as file:// resource. When using a key file it is in the responsibility of the user how this key file is actually being used. By default any distribution will just open an interactive dialog asking for credentials at boot time. With the luksOS value a predefined set of cipher, keysize and hash format options is passed to the cryptsetup call in order to create a format compatible to the specified distribution sle12 By default, all blocks of a LUKS volume will be filled with pseudo-random data. If you're shipping an image with a well-known key, which is going to be re-encrypted at deployment time, you can decrease the size of the image by setting this attribute to false. When LUKS unlocks a key slot using a user provided password, it uses a so-called key derivation function to derive a symmetric encryption key from the password. Not all boot loaders support all KDF algorithms, hence this attribute can be used to select a specific algorithm. pbkdf2 argon2i argon2id Setup software raid in degraded mode with one disk Thus only mirroring and striping is possible mirroring striping Specifies the primary type (choose KIWI option type) for use with overlay filesystems only: will force any COW action to happen in RAM label to set for the root filesystem. By default ROOT is used Specifies the kernel framebuffer mode. More information about the possible values can be found by calling hwinfo --framebuffer or in /usr/src/linux/Documentation/fb/vesafb.txt Specifies the license tag in a GCE format Specifies the GUID in a fixed format VHD for the iso type only: Specifies the volume ID (volume name or label) to be written into the master block. There is space for 32 characters. 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. Specifies the wait period in seconds after launching the multipath daemon to wait until all presented devices are available on the host. Default timeout is 3 seconds 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. Create a systemfiles metadata file containing the file list of all packages of the system Include an existing systemfiles metadata file Together with derived_from create a delta root tree Whether to ensure /run and /tmp directories are empty in the container image created by Kiwi. Default is true. Specifies the publisher name of the ISO. Specifies the first disk sector for the first partition. Default value is 2048 and it can't be set to any number below the default. 2048 Clone root partition N times Clone boot partition N times. If no boot partition is used, the attribute has no effect. The use of a boot partition can be enforced through the bootpartition attribute or is implicitly activated according to the combination of type settings that makes the use of an extra boot partition a requirement Specifies the bundle format pattern 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 The specified bundle_format will be used as base name for all target image files at call time of: kiwi-ng result bundle ... Specifies the image type The Image Type of the Logical Extend
The user ID for this user The list of groups that the user belongs to. The first item in the list is used as the login group. If 'groups' is not present a default group is assigned to the user according to he specifing toolchain behaviour. The name of an user Format of the given password, encrypted is the default encrypted plain The home directory for this user The shell for this user A User with Name, Password, Path to Its Home And Shell
A Version Number for the Image, Consists of Major.Minor.Release
An entry for the VM configuration file
The type of the disk as it is internally handled by the VM (ova only) The disk controller used for the VM guest (vmdk only) ide buslogic lsilogic lsisas1068 legacyESX pvscsi The disk ID / device for the VM disk (vmdk only) The disk device to appear in the guest (xen only) The disk mode (vmdk only) monolithicSparse monolithicFlat twoGbMaxExtentSparse twoGbMaxExtentFlat streamOptimized The VM disk definition.
The CD/DVD controller used for the VM guest ide scsi The CD/DVD ID for the VM CD rom drive The VM CD/DVD drive definition. You can setup either a scsi CD or an ide CD drive
The driver used for the VM network interface The interface ID for the VM network interface The VM network mode The VM mac address The VM network interface definition
partition(name) is reserved Reserved names are '$reserved' Partition map name. The name of the partition as handled by KIWI. Note, that there are the following reserved names which cannot be used because they are already represented by existing KIWI attributes: root, readonly, boot, prep, spare, swap, efi_csm and efi. The filesystem created on the partition will also use this name in uppercase as its label Absolute size of the partition. The value is used as MB by default but you can add "M" and/or "G" as postfix Partition type name in the context of kiwi Allowed values are: t.linux t.linux t.raid Partition name as it appears in the table Mountpoint below which this partition should be mounted to Filesystem which should be created on the partition btrfs ext2 ext3 ext4 squashfs xfs fat32 fat16 Clone this partition N times Specify custom partition in the partition table
quota attribute is only available for the following image filesystem: btrfs free space to be added to this volume. The value is used as MB by default but you can add "M" and/or "G" as postfix volume name. The name of the volume. if mountpoint is not specified the name specifies a path which has to exist inside the root directory. The name/path of the parent volume. Evaluated only for the btrfs volume manager to allow specifying the parent subvolume to nest this volume in. If not specified the parent is always the volume set as the default volume volume path. The mountpoint specifies a path which has to exist inside the root directory. absolute size of the volume. If the size value is too small to store all data kiwi will exit. The value is used as MB by default but you can add "M" and/or "G" as postfix Apply the filesystem copy-on-write attribute for this volume Apply quota value to filesystem volume if supported Indicate that this filesystem should perform the validation to become filesystem checked. The actual constraints if 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 to be never checked. The latter is the default. filesystem label name of the volume. Specify which parts of the filesystem should be on an extra volume.
include given file and replace the include statement with its contents
Kiwi distinguishes between two basic image description types which uses the same format but one is created and provided by the kiwi developers and the other is created by the users of kiwi. The type=boot specifies a boot image (initrd) which should be provided by the kiwi developers wheras type=system specifies a standard image description created by a kiwi user. boot system A Short Description
A Collection of Driver Files
Specifies the strip data type. `delete` references a list of custom files and directories to delete, `tools` references file names in linux bin/sbin directories to keep, `libs` references file names in linux lib directories to keep. delete tools libs A Collection of files to strip
bootloader($attr) is only available for the following image types: $types bootloader($attr) attribute is only available for the following bootloader types: $types Specifies the bootloader used for booting the image. At the moment grub2, systemd_boot(EFI only), zipl and the combination of zipl plus userspace grub2(grub2_s390x_emu) are supported. The special custom entry allows 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 grub2 grub2_s390x_emu systemd_boot custom zipl Specifies a custom grub bootloader template file which 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. Specifies if the GRUB2 bootloader uses the boot loader specification Specifies the bootloader console. 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 Specifies the boot timeout in seconds prior to launching the default boot option. By default the timeout is set to 10sec 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 expired. In countdown mode an indication of the remaining time is displayed. The attribute is available for the grub loader. countdown hidden menu The device type of the disk on s390. For zFCP devices use SCSI, for emulated DASD devices use FBA, for 4k DASD devices use CDL FBA SCSI CDL GPT When /boot is encrypted, make the boot loader store the password in its configuration file (in cleartext). This is useful for full disk encryption images The bootloader section is used to select the bootloader and to provide configuration parameters for it
containerconfig($attr) is only available for the following image types: $types Specifies a name for the container. This is usually the the repository name of the container as read if the container image is imported via the docker load command Specifies a tag for the container. This is usually the the tag name of the container as read if the container image is imported via the docker load command Specifies additional names for the container using a comma separated values string. It supports '<repo1>:<tag2>,<repo2:tag2>' format. If any of the names does not include the repository or tag in that case `name` or `tag` attribute values from containerconfig will be used to build a complete image referrence. Specifies a maintainer for the container. Specifies a user for the container. Specifies the default working directory of the container The containerconfig element provides metadata information to setup a container in order to be prepared for use with the container engine tool chain. container specific data should be provided in an additional subsection whereas this section provides globally useful container information.
Specifies the entry point program name to execute Provides details for the entry point command. This includes the execution name and its parameters. Arguments can be optionally specified
Specifies the subcommand program name to execute Provides details for the subcommand command. This includes the execution name and its parameters. Arguments can be optionally specified. The subcommand is appended the command information from the entrypoint. It is in the responsibility of the author to make sure the combination of entrypoint and subcommand forms a valid execution command
Specifies a command argument name Provides details about a command argument
Provides details about network ports which should be exposed from the container. At least one port must be configured
Specifies the port number and transport protocol to expose. If no protocol is defined OCI defaults are applied. Provides details about an exposed port.
Provides details about storage volumes in the container At least one volume must be configured
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.
Partition table entries within the custom area of the storage device
luks attribute must be set when using luksformat option(s) luksFormat option settings
Additional bootloader settings
<securelinux> section only available for the bootloader types: zipl and grub2-s390x-emu securelinux contains all elements to describe data required to setup a secure linux execution process for the individual architecture in the scope of the bootloader process
Provides details about the container environment variables At least one environment variable must be configured
Specifies the environment variable name Specifies the environment variable value Provides details about an environment variable
Provides details about container labels At least one label must be configured
Specifies the label name Specifies the label value Provides details about a container label
Specifies the 'created by' history record. By default set to 'KIWI' Specifies the 'author' history record. Specifies a custom application ID, used for WSL containers Specifies a version information, used for WSL containers. The version given here follows the package numbering rules as documented in: https://docs.microsoft.com/en-us/windows/uwp/publish/package-version-numbering Specifies name of the container launcher program Used for WSL containers Provides details about the container history. Includes the 'created by', 'author' as attributes and its content represents the 'comment' entry.
The oemconfig element specifies the OEM image configuration options which are used to repartition and setup the system disk.
$attr attribute is only available for the following vagrant providers: $providers The vagrant provider for this box libvirt virtualbox Guest additions are present in this box Path to a Vagrantfile that will be embedded in the box Kiwi cannot check the Vagrantfile for correctnes, therefore use this at your own risk! virtualsize provides the value of the virtual_size key which is embedded in the metadata.json hash inside the box file, as described in http://docs.vagrantup.com/v2/boxes/format.html This tells the Vagrant provider how big to make the virtual disk when it creates the VM. The boxname as it's written into the json file If not specified the image name is used The vagrantconfig element specifies the Vagrant meta configuration options which are used inside a vagrant box
installmedia is only available for the oem type with 'installiso' or 'installpxe' set to 'true' The installmedia element defined the configuration parameters for the installation media of OEM images.
Sets weather the dracut modules list are meant to be appended, omitted or they define the static list of modules for the installation initrd. omit add set setup The initrd element defines the dracut modules configuration for the installation media.
A module name A dracut module
The OVF configuration type. The Open Virtualization Format is a standard for describing virtual appliances and distribute them in an archive also called Open Virtual Appliance(OVA). The standard describes major components associated with a disk image. The exact specification depends on the product using the format and is specified in KIWI as the OVF type. zvm powervm xen vmware The virtual HW version number for the VM configuration (vmdk and ova) the VM architecture type (vmdk only) ix86 x86_64 the Xen target loader which is expected to load this guest hvmloader pygrub pvgrub The virtual guestOS identification string for the VM (vmdk and ova, note the name designation is different for the two formats) The virtual machine min memory in MB (ova only) The virtual machine max memory in MB (ova only) The virtual machine min CPU count (ova only) The virtual machine max CPU count (ova only) The memory, in MB, setup for the guest VM (all formats) The number of virtual cpus for the guest VM (all formats) The machine element specifies VM guest configuration options which are used by the virtual machine when running the image.
$attr attribute is only available for the following packages type(s): $types Specifies package collection type. `bootstrap` packages gets installed in the very first phase of an image build in order to fill the empty root directory with bootstrap data. `image` packages are installed after the bootstrap phase as chroot operation. `delete` packages are uninstalled after the preparation phase is done. `image_type_name` packages are only installed if this build type is requested. bootstrap delete docker image iso oem pxe kis oci uninstall systemfiles Selection type for patterns. Can be onlyRequired or plusRecommended onlyRequired plusRecommended Specify bootstrap package providing a bootstrap tarball in /var/cache/kiwi/bootstrap/PACKAGE_NAME.ARCH.tar.xz The tarball will be unpacked and used as the bootstrap rootfs to begin with. The feature is currently only available with the apt package manager to allow an alternative bootstrap method bootstrap_package attribute only available for the apt packagemanager Specifies Packages/Patterns Used in Different Stages
Configuration Information Needed for Logical Extend All elements are optional since the combination of appropriate preference sections based on profiles combine to create on vaild definition
Namespace section which creates a namespace and the drivers can bind itself to one of the listed namespaces.
A List of Users
././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/__init__.py0000644000000000000000000000000015015277265014137 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/__init__.py0000644000000000000000000000403115015277265016366 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 ) # project from kiwi.exceptions import KiwiSolverRepositorySetupError from kiwi.system.uri import Uri class SolverRepository(metaclass=ABCMeta): """ **Repository factory for creation of SAT solvables** * :param object uri: Instance of :class:`Uri` """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new(uri: Uri, user: str = None, secret: str = None): name_map = { 'yast2': ['SolverRepositorySUSE', 'suse'], 'rpm-md': ['SolverRepositoryRpmMd', 'rpm_md'], 'rpm-dir': ['SolverRepositoryRpmDir', 'rpm_dir'], 'apt-deb': ['SolverRepositoryDeb', 'deb'] } try: module_name = name_map[uri.repo_type][0] module_namespace = name_map[uri.repo_type][1] repository = importlib.import_module( 'kiwi.solver.repository.{0}'.format(module_namespace) ) return repository.__dict__[module_name]( uri, user, secret ) except Exception as issue: raise KiwiSolverRepositorySetupError( 'Support for {0} solver repotype not implemented: {1}'.format( uri.repo_type, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/base.py0000644000000000000000000003122215015277265015543 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 base64 import b64encode from urllib.request import urlopen from urllib.request import Request from kiwi.utils.temporary import Temporary from lxml import etree import random import glob import os import re # project import kiwi.defaults as defaults from kiwi.exceptions import KiwiUriOpenError from kiwi.path import Path from kiwi.command import Command from kiwi.defaults import Defaults class SolverRepositoryBase: """ **Base class interface for SAT solvable creation.** :param object uri: Instance of :class:`Uri` :param string user: User name for uri authentication :param string secret: Secret token for uri authentication """ def __init__(self, uri, user=None, secret=None): self.uri = uri # check if the URI string contains credentials and # extract/trim them from the uri object. The urlparse # class does not recognize this information as a valid # URI and throws an exception secret_format = re.match(r'^(.*)://(.*):(.*)@(.*)', uri.uri) if secret_format: self.user = secret_format.group(2) self.secret = secret_format.group(3) self.uri.uri = \ f'{secret_format.group(1)}://{secret_format.group(4)}' else: self.user = user self.secret = secret self.repository_metadata_dirs = [] self.repository_solvable_dir = None def create_repository_solvable( self, target_dir=Defaults.get_solvable_location() ): """ Create SAT solvable for this repository from previously created intermediate solvables by merge and store the result solvable in the specified target_dir :param str target_dir: path name :return: file path to solvable :rtype: str """ Path.create(target_dir) solvable = os.sep.join( [target_dir, self.uri.alias()] ) if not self.is_uptodate(target_dir): self._setup_repository_metadata() solvable = self._merge_solvables(target_dir) return solvable def timestamp(self): """ Return repository timestamp The retrieval of the repository timestamp depends on the type of the repository and is therefore supposed to be implemented in the specialized Solver Repository classes. If no such implementation exists the method returns the value 'static' to indicate there is no timestamp information available. :rtype: str """ return 'static' def is_uptodate(self, target_dir=Defaults.get_solvable_location()): """ Check if repository metadata is up to date :return: True or False :rtype: bool """ solvable_time_file = ''.join( [target_dir, os.sep, self.uri.alias(), '.timestamp'] ) if os.path.exists(solvable_time_file): with open(solvable_time_file) as solvable_time: saved_time = solvable_time.read() if saved_time == self.timestamp() and not saved_time == 'static': return True return False def download_from_repository(self, repo_source, target): """ Download given source file from the repository and store it as target file The repo_source location is used relative to the repository location and will be part of a mime type source like: `file://repo_path/repo_source` :param str repo_source: source file in the repo :param str target: file path :raises KiwiUriOpenError: if the download fails """ download_link = None try: download_link = os.sep.join( [ self._get_mime_typed_uri(), repo_source ] ) request = Request(download_link) if self.user and self.secret: credentials = b64encode( format(':'.join([self.user, self.secret])).encode() ) request.add_header( 'Authorization', b'Basic ' + credentials ) location = urlopen(request) except Exception as e: raise KiwiUriOpenError( f'{type(e).__name__}: {e} {download_link}' ) with open(target, 'wb') as target_file: target_file.write(location.read()) def get_repo_type(self): try: if self._get_repomd_xml(): return 'rpm-md' except KiwiUriOpenError: pass try: if self._get_deb_packages(): return 'apt-deb' except KiwiUriOpenError: pass try: repo_listing = self._get_pacman_packages() if '.db.sig\"' in repo_listing: return 'pacman' except KiwiUriOpenError: pass return None def _get_pacman_packages(self): """ Download Arch repository listing for the current architecture :return: html directory listing :rtype: str """ dir_listing_download = Temporary().new_file() self.download_from_repository( defaults.PLATFORM_MACHINE, dir_listing_download.name ) if os.path.isfile(dir_listing_download.name): with open(dir_listing_download.name) as listing: return listing.read() def _get_deb_packages(self, download_dir=None): """ Download Packages.gz file from an apt repository and return its contents. If download_dir is set, download the file and return the file path name :param str download_dir: Download directory :return: Contents of Packages file or file path name :rtype: str """ repo_source = 'Packages.gz' if not download_dir: packages_download = Temporary().new_file() self.download_from_repository(repo_source, packages_download.name) if os.path.isfile(packages_download.name): with open(packages_download.name) as packages: return packages.read() else: packages_download = os.sep.join( [download_dir, repo_source.replace(os.sep, '_')] ) self.download_from_repository(repo_source, packages_download) return packages_download def _get_repomd_xml(self, lookup_path='repodata'): """ Parse repomd.xml file from lookup_path and return an etree This method only applies to rpm-md type repositories :param str lookup_path: relative path used to find repomd.xml file :return: Object with repomd.xml contents :rtype: XML etree """ xml_download = Temporary().new_file() xml_setup_file = os.sep.join([lookup_path, 'repomd.xml']) self.download_from_repository(xml_setup_file, xml_download.name) return etree.parse(xml_download.name) def _get_repomd_xpath(self, xml_data, expression): """ Call the provided xpath expression on the root element of the xml_data which must be an XML etree parsed document and return the result. This method only applies to repomd.xml files of the correct namespaces: * http://linux.duke.edu/metadata/repo * http://linux.duke.edu/metadata/rpm :param object xml_data: An XML etree object of a parsed XML file. :param str expression: An xpath expression to filder xml_data. :return: Elements matching the xpath expression :rtype: list """ namespace_map = dict( repo='http://linux.duke.edu/metadata/repo', rpm='http://linux.duke.edu/metadata/rpm' ) return xml_data.getroot().xpath( expression, namespaces=namespace_map ) def _setup_repository_metadata(self): """ Download all relevant repository metadata and create intermediate solvables from the result. The metadata structure depends on the type of the repository and must be implemented in the specialized Solver Repository classes. """ raise NotImplementedError def _create_solvables(self, metadata_dir, tool): """ Create intermediate (before merge) SAT solvables from the data given in the metadata_dir and store the result in the temporary repository_solvable_dir. The given tool must match the solvable data structure. There are the following tools to create a solvable from repository metadata: * rpmmd2solv solvable from repodata files * susetags2solv solvable from SUSE (yast2) repository files * comps2solv solvable from RHEL component files * rpms2solv solvable from rpm header files * deb2solv solvable from deb header files :param str metadata_dir: path name :param str tool: one of the above tools """ if not self.repository_solvable_dir: self.repository_solvable_dir = Temporary( prefix='kiwi_solvable_dir.' ).new_dir() if tool == 'rpms2solv': # solvable is created from a bunch of rpm files bash_command = [ tool, os.sep.join([metadata_dir, '*.rpm']), '>', self._get_random_solvable_name() ] Command.run(['bash', '-c', ' '.join(bash_command)]) else: # each file in the metadata_dir is considered a valid # solvable for the selected solv tool tool_options = [] if tool == 'deb2solv': tool_options.append('-r') for source in glob.iglob('/'.join([metadata_dir, '*'])): bash_command = [ 'gzip', '-cd', '--force', source, '|', tool ] + tool_options + [ '>', self._get_random_solvable_name() ] Command.run(['bash', '-c', ' '.join(bash_command)]) def _merge_solvables(self, target_dir): """ Merge all intermediate SAT solvables into one and store the result in the given target_dir. In addition an info file containing the repo url and a timestamp file is created :param str target_dir: path name """ if self.repository_solvable_dir: solvable = os.sep.join([target_dir, self.uri.alias()]) bash_command = [ 'mergesolv', '/'.join([self.repository_solvable_dir.name, '*']), '>', solvable ] Command.run(['bash', '-c', ' '.join(bash_command)]) with open('.'.join([solvable, 'info']), 'w') as solvable_info: solvable_info.write(''.join([self.uri.uri, os.linesep])) with open('.'.join([solvable, 'timestamp']), 'w') as solvable_time: solvable_time.write(self.timestamp()) return solvable def _get_mime_typed_uri(self): """ Adds `file` scheme for local URIs :return: A mime typed URI as string :rtype: str """ return self.uri.translate() if self.uri.is_remote() else ''.join( ['file://', self.uri.translate()] ) def _create_temporary_metadata_dir(self): """ Create and manage a temporary metadata directory :return: the path of the temporary directory just created :rtype: str """ metadata_dir = Temporary(prefix='kiwi_metadata_dir.').new_dir() self.repository_metadata_dirs.append(metadata_dir) return metadata_dir.name def _get_random_solvable_name(self): if self.repository_solvable_dir: return '{0}/solvable-{1}{2}{3}{4}'.format( self.repository_solvable_dir.name, self._rand(), self._rand(), self._rand(), self._rand() ) def _rand(self): return '%02x' % random.randrange(1, 0xfe) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/deb.py0000644000000000000000000000265115015277265015367 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.solver.repository.base import SolverRepositoryBase class SolverRepositoryDeb(SolverRepositoryBase): """ **Class for SAT solvable creation for apt-deb type repositories.** """ def _setup_repository_metadata(self): """ Download repo metadata for apt-deb specific repositories and create SAT solvables from all solver relevant files """ # Download Packages metadata for the deb2solv solvable # creation. This includes the files named Packages.gz in # the repo definition deb_dir = self._create_temporary_metadata_dir() self._get_deb_packages(download_dir=deb_dir) self._create_solvables( deb_dir, 'deb2solv' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/rpm_dir.py0000644000000000000000000000326515015277265016273 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 # project from kiwi.solver.repository.base import SolverRepositoryBase from kiwi.exceptions import KiwiRpmDirNotRemoteError class SolverRepositoryRpmDir(SolverRepositoryBase): """ **Class for SAT solvable creation for rpm_dir type repositories.** """ def _setup_repository_metadata(self): """ Download rpms from the repository and create a SAT solvable from the rpm header metadata """ if self.uri.is_remote(): raise KiwiRpmDirNotRemoteError( 'Only local rpm-dir repositories are supported' ) package_dir = self._create_temporary_metadata_dir() for package in glob.iglob('/'.join([self.uri.translate(), '*.rpm'])): package_name = os.path.basename(package) self.download_from_repository( package_name, os.sep.join([package_dir, package_name]) ) self._create_solvables( package_dir, 'rpms2solv' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/rpm_md.py0000644000000000000000000000754315015277265016120 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 collections import namedtuple # project from kiwi.solver.repository.base import SolverRepositoryBase class SolverRepositoryRpmMd(SolverRepositoryBase): """ **Class for SAT solvable creation for rpm-md type repositories.** """ def _setup_repository_metadata(self): """ Download repo metadata for rpm-md specific repositories and create SAT solvables from all solver relevant files """ # Download rpm-md metadata for the rpmmd2solv solvable # creation. This includes the files marked as primary # and pattern in the repo definition rpm_md_dir = self._create_temporary_metadata_dir() rpm_md_data = self._find_repomd_files( ['primary', 'patterns'], 'rpmmd2solv' ) for rpm_md_file in rpm_md_data.metadata_files: self.download_from_repository( rpm_md_file, os.sep.join([rpm_md_dir, os.path.basename(rpm_md_file)]) ) self._create_solvables( rpm_md_dir, rpm_md_data.solv_tool ) # Download rpm-md metadata for the comps2solv solvable # creation. This includes the files marked as group_gz # This component information is like the SUSE pattern # information but for RHEL. In order to allow resolving # group id names this information needs to be present rpm_comps_dir = self._create_temporary_metadata_dir() rpm_comps_data = self._find_repomd_files( ['group_gz'], 'comps2solv' ) for rpm_comps_file in rpm_comps_data.metadata_files: self.download_from_repository( rpm_comps_file, os.sep.join([rpm_comps_dir, os.path.basename(rpm_comps_file)]) ) self._create_solvables( rpm_comps_dir, rpm_comps_data.solv_tool ) def timestamp(self): """ Get timestamp from the first primary metadata :return: time value as text :rtype: str """ return self._get_repomd_xpath( self._get_repomd_xml(), 'repo:data[@type="primary"]/repo:timestamp' )[0].text def _find_repomd_files(self, type_list, tool): """ Lookup repodata/repomd.xml and the metadata files for the specified type list. Assign the found entries to the given tool :param list type_list: Value of type attribute in the repomd.xml definition :param str tool: Tool to create a solvable from this data :return: str:solv_tool, list:metadata_files :rtype: tuple """ result_type = namedtuple( 'result_type', ['solv_tool', 'metadata_files'] ) metadata_files = [] for metadata_type in type_list: metadata_locations = self._get_repomd_xpath( self._get_repomd_xml(), 'repo:data[@type="{0}"]/repo:location'.format(metadata_type) ) for location in metadata_locations: metadata_files.append(location.get('href')) return result_type( solv_tool=tool, metadata_files=metadata_files ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/repository/suse.py0000644000000000000000000000552615015277265015620 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 collections import namedtuple # project from kiwi.solver.repository.base import SolverRepositoryBase class SolverRepositorySUSE(SolverRepositoryBase): """ **Class for SAT solvable creation for SUSE type repositories.** """ def _setup_repository_metadata(self): """ Download repo metadata for SUSE specific repositories and create SAT solvables from all solver relevant files """ metadata_dir = self._create_temporary_metadata_dir() repo_data = self._find_primary_repository_files() for primary_file in repo_data.primary_files: self.download_from_repository( primary_file, os.sep.join([metadata_dir, os.path.basename(primary_file)]) ) self._create_solvables( metadata_dir, repo_data.solv_tool ) def _find_primary_repository_files(self): """ Lookup repodata/repomd.xml or alternative the packages.gz from the suse/setup/descr metadata. For online suse repos the repodata metadata exists and is preferred. On media like DVD there might be only the suse metadata. Depending on the result this also impacts which tool is required to create the solv data from the information :return: str:solv_tool, list:primary_files :rtype: tuple """ result_type = namedtuple( 'result_type', ['solv_tool', 'primary_files'] ) try: primary_files = [] primary_locations = self._get_repomd_xpath( self._get_repomd_xml('suse/repodata'), 'repo:data[@type="primary"]/repo:location' ) for location in primary_locations: primary_files.append( os.sep.join(['suse', location.get('href')]) ) return result_type( solv_tool='rpmmd2solv', primary_files=primary_files ) except Exception: return result_type( solv_tool='susetags2solv', primary_files=['suse/setup/descr/packages.gz'] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/solver/sat.py0000644000000000000000000002110515015277265013200 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 import logging from collections import namedtuple from xml.etree import ElementTree from xml.dom import minidom # project import kiwi.defaults as defaults from kiwi.exceptions import ( KiwiSatSolverPluginError, KiwiSatSolverJobError, KiwiSatSolverJobProblems ) log = logging.getLogger('kiwi') class Sat: """ **Sat Solver class to run package solver operations** The class uses SUSE's libsolv sat plugin """ def __init__(self): """ An instance of Sat auto loads the python solv plugin which is a python binding to the libsolv C library. An exception is raised if the module failed to load. On success a new solver pool is initialized for this instance :raises KiwiSatSolverPluginError: if libsolv module can't be loaded """ try: self.solv = importlib.import_module('solv') except Exception as e: raise KiwiSatSolverPluginError( '{0}: {1}'.format(type(e).__name__, format(e)) ) self.pool = self.solv.Pool() self.pool.setarch() def set_dist_type(self, dist, arch=None): if not arch: arch = defaults.PLATFORM_MACHINE dist_types = { 'deb-x86_64': { 'pool_dist': self.solv.Pool.DISTTYPE_DEB, 'arch': 'amd64' }, 'deb-aarch64': { 'pool_dist': self.solv.Pool.DISTTYPE_DEB, 'arch': 'arm64' } } dist_type = dist_types.get(f'{dist}-{arch}') if dist_type: if self.pool.setdisttype(dist_type['pool_dist']) < 0: raise KiwiSatSolverPluginError( f'Failed to set dist type for distribution: {dist!r}' ) self.pool.setarch( dist_type['arch'] if dist_type else arch ) return dist_type def add_repository(self, solver_repository): """ Add a repository solvable to the pool. This basically add the required repository metadata which is needed to run a solver operation later. :param object solver_repository: Instance of :class:`SolverRepository` """ solvable = solver_repository.create_repository_solvable() pool_repository = self.pool.add_repo(solver_repository.uri.uri) pool_repository.add_solv(solvable) self.pool.addfileprovides() self.pool.createwhatprovides() def solve(self, job_names, skip_missing=False, ignore_recommended=True): """ Solve dependencies for the given job list. The list is allowed to contain element names of the following format: * name describes a package name * pattern:name describes a package collection name whose metadata type is called 'pattern' and stored as such in the repository metadata. Usually SUSE repos uses that * group:name describes a package collection name whose metadata type is called 'group' and stored as such in the repository metadata. Usually RHEL/CentOS/Fedora repos uses that :param list job_names: list of strings :param bool skip_missing: skip job if not found :param bool ignore_recommended: do not include recommended packages :raises KiwiSatSolverJobProblems: if solver reports solving problems :return: Transaction result information :rtype: dict """ solver = self.pool.Solver() if ignore_recommended: solver.set_flag(self.solv.Solver.SOLVER_FLAG_IGNORE_RECOMMENDED, 1) solver_problems = solver.solve( self._setup_jobs(job_names, skip_missing) ) solver_problem_info = self._evaluate_solver_problems( solver_problems ) if solver_problem_info: raise KiwiSatSolverJobProblems(solver_problem_info) solver_transaction = solver.transaction() return self._evaluate_solver_result( solver_transaction ) def _evaluate_solver_problems(self, solver_problems): """ Iterate over solver problems and their solutions The method creates a pretty print XML information and returns that as a UTF-8 encoded string :param object solver_problems: result of :class:`Pool::Solver::solve()` :return: solver problem info and solutions :rtype: str """ if solver_problems: problems = ElementTree.Element('problems') for problem in solver_problems: problem_detail = ElementTree.SubElement( problems, 'problem', id=format(problem.id), message=problem.findproblemrule().info().problemstr() ) for solution in problem.solutions(): solution_detail = ElementTree.SubElement( problem_detail, 'solution', id=format(solution.id) ) for option in solution.elements(1): solution_option = ElementTree.SubElement( solution_detail, 'option' ) solution_option.text = option.str() xml_data_unformatted = ElementTree.tostring( problems, 'utf-8' ) xml_data_domtree = minidom.parseString(xml_data_unformatted) return xml_data_domtree.toprettyxml(indent=" ") def _evaluate_solver_result(self, solver_transaction): """ Iterate over solver result and return a data dictionary :param object solver_transaction: result of :class:`Pool::Solver::transaction()` :return: dict of packages and their details :rtype: dict """ result_type = namedtuple( 'result_type', [ 'uri', 'installsize_bytes', 'arch', 'version', 'checksum' ] ) result = {} for solvable in solver_transaction.newpackages(): name = solvable.lookup_str(self.solv.SOLVABLE_NAME) result[name] = result_type( uri=solvable.repo.name, installsize_bytes=solvable.lookup_num( self.solv.SOLVABLE_INSTALLSIZE ), arch=solvable.lookup_str( self.solv.SOLVABLE_ARCH ), version=solvable.lookup_str( self.solv.SOLVABLE_EVR ), checksum=solvable.lookup_checksum( self.solv.SOLVABLE_CHECKSUM ) ) return result def _setup_jobs(self, job_names, skip_missing): """ Create a solver job list from given list of job names :param list job_names: list of package,pattern,group names :param bool skip_missing: continue or raise if job selection failed :return: list of :class:`Pool.selection()` objects :rtype: list """ jobs = [] for job_name in job_names: selection_name = self.solv.Selection.SELECTION_NAME selection_provides = self.solv.Selection.SELECTION_PROVIDES selection = self.pool.select( job_name, selection_name | selection_provides ) if selection.flags & self.solv.Selection.SELECTION_PROVIDES: log.info('--> Using capability match for {0}'.format(job_name)) if selection.isempty(): if skip_missing: log.info( '--> Package {0} not found: skipped'.format(job_name) ) else: raise KiwiSatSolverJobError( 'Package {0} not found'.format(job_name) ) else: jobs += selection.jobs(self.solv.Job.SOLVER_INSTALL) return jobs ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/__init__.py0000644000000000000000000000000015015277265014271 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/clone_device.py0000644000000000000000000001155315015277265015170 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 uuid from typing import List from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.mapped_device import MappedDevice from kiwi.filesystem import FileSystem from kiwi.command import Command from kiwi.utils.block import BlockID from kiwi.defaults import Defaults from kiwi.exceptions import KiwiRaidSetupError class CloneDevice(DeviceProvider): """ **Implements device cloning** """ def __init__(self, source_provider: DeviceProvider, root_dir: str): """ Construct a new CloneDevice layout object :param object source_provider: Instance of class based on DeviceProvider :param str root_dir: Path to image root directory """ self.source_provider = source_provider self.root_dir = root_dir def clone(self, target_devices: List[DeviceProvider]): """ Clone source device to target device(s) :param list target_devices: List of target DeviceProvider instances """ for target_device in target_devices: Command.run( [ 'dd', 'if={0}'.format(self.source_provider.get_device()), 'of={0}'.format(target_device.get_device()), 'bs=1M' ] ) clone_id = BlockID(target_device.get_device()) target_filesystem = clone_id.get_filesystem() if target_filesystem in Defaults.get_filesystem_image_types(): # Simple filesystem clones needs to be unique on the UUID # to avoid conflicts on the running system with FileSystem.new(target_filesystem, target_device) as fs: fs.set_uuid() elif target_filesystem == 'LVM2_member': # Volume Group clones requires to be unique on the vgroup # name to avoid conflicts on the running system Command.run( ['vgimportclone', target_device.get_device()] ) elif target_filesystem == 'crypto_LUKS': # Device mapper clones based on the LUKS header needs to be # unique in the LUKS UUID to avoid conflicts on the running # system Command.run( [ 'cryptsetup', '-q', 'luksUUID', target_device.get_device(), '--uuid', format(uuid.uuid4()) ] ) elif target_filesystem == 'linux_raid_member': # Device mapper clones based on the RAID superblock needs # to be unique in the UUID stored in the raid superblock # to avoid conflicts on the running system try: mdadm_conf = f'{self.root_dir}/etc/mdadm.conf' with open(mdadm_conf) as mdadm: raid_config = mdadm.readline().split(' ') md_device = raid_config[1] md_name = raid_config[3].split('=')[1] Command.run( ['mdadm', '--stop', md_device] ) Command.run( [ 'mdadm', '--assemble', '--update=uuid', '--name', md_name, md_device, target_device.get_device() ] ) with FileSystem.new( BlockID(md_device).get_filesystem(), MappedDevice(md_device, target_device) ) as fs: fs.set_uuid() Command.run( ['mdadm', '--stop', md_device] ) Command.run( [ 'mdadm', '--assemble', md_device, self.source_provider.get_device() ] ) except Exception as issue: raise KiwiRaidSetupError( f'Failed to update mdraid UUID: {issue}' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/device_provider.py0000644000000000000000000000424415015277265015721 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.command import Command from kiwi.exceptions import ( KiwiDeviceProviderError ) class DeviceProvider: """ **Base class for any class providing storage devices** """ def get_device(self): """ Representation of device nodes Could provide one ore more devices representing the storage Implementation in specialized device provider class """ raise KiwiDeviceProviderError( 'No storage device(s) provided' ) def get_uuid(self, device: str) -> str: """ UUID of device :param string device: node name :return: UUID from blkid :rtype: str """ uuid_call = Command.run( ['blkid', device, '-s', 'UUID', '-o', 'value'] ) return uuid_call.output.rstrip('\n') def get_byte_size(self, device: str) -> int: """ Size of device in bytes :param string device: node name :return: byte value from blockdev :rtype: int """ blockdev_call = Command.run( ['blockdev', '--getsize64', device] ) return int(blockdev_call.output.rstrip('\n')) def is_loop(self) -> bool: """ Check if device provider is loop based By default this is always False and needs an implementation in the the specialized device provider class :return: True or False :rtype: bool """ return False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/disk.py0000644000000000000000000005136715015277265013512 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 OrderedDict from typing import ( Dict, NamedTuple, Tuple ) # project from kiwi.defaults import Defaults from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.mapped_device import MappedDevice from kiwi.partitioner import Partitioner from kiwi.runtime_config import RuntimeConfig from kiwi.exceptions import ( KiwiCustomPartitionConflictError, KiwiError ) ptable_entry_type = NamedTuple( 'ptable_entry_type', [ ('mbsize', int), ('clone', int), ('partition_name', str), ('partition_type', str), ('mountpoint', str), ('filesystem', str) ] ) log = logging.getLogger('kiwi') class Disk(DeviceProvider): """ **Implements storage disk and partition table setup** """ def __init__( self, table_type: str, storage_provider: DeviceProvider, start_sector: int = None, extended_layout: bool = False ): """ Construct a new Disk layout object :param string table_type: Partition table type name :param object storage_provider: Instance of class based on 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.partition_mapper = RuntimeConfig().get_mapper_tool() #: the underlaying device provider self.storage_provider = storage_provider #: list of protected map ids. If used in a custom partitions #: setup this will lead to a raise conditition in order to #: avoid conflicts with the existing partition layout and its #: customizaton capabilities self.protected_map_ids = [ 'root', 'readonly', 'boot', 'prep', 'spare', 'swap', 'efi_csm', 'efi' ] #: Unified partition UUIDs according to systemd self.gUID = self.get_discoverable_partition_ids() self.partition_map: Dict[str, str] = {} self.public_partition_id_map: Dict[str, str] = {} self.partition_id_map: Dict[str, str] = {} self.is_mapped = False self.partitioner = Partitioner.new( table_type, storage_provider, start_sector, extended_layout ) self.table_type = table_type def __enter__(self): return self def get_device(self) -> Dict[str, MappedDevice]: """ Names of partition devices Note that the mapping requires an explicit map() call :return: instances of MappedDevice :rtype: dict """ device_map = {} for partition_name, device_node in list(self.partition_map.items()): device_map[partition_name] = MappedDevice( device=device_node, device_provider=self ) return device_map def is_loop(self) -> bool: """ Check if storage provider is loop based The information is taken from the storage provider. If the storage provider is loop based the disk is it too :return: True or False :rtype: bool """ return self.storage_provider.is_loop() def create_custom_partitions( self, table_entries: Dict[str, ptable_entry_type] ) -> None: """ Create partitions from custom data set .. code:: python table_entries = { map_name: ptable_entry_type } :param dict table: partition table spec """ for map_name in table_entries: if map_name in self.protected_map_ids: raise KiwiCustomPartitionConflictError( f'Cannot use reserved table entry name: {map_name!r}' ) entry = table_entries[map_name] if entry.clone: self._create_clones( map_name, entry.clone, entry.partition_type, format(entry.mbsize) ) id_name = f'kiwi_{map_name.title()}Part' self.partitioner.create( entry.partition_name, entry.mbsize, entry.partition_type ) self._add_to_map(map_name) self._add_to_public_id_map(id_name) part_uuid = self.gUID.get(entry.partition_name) if part_uuid: self.partitioner.set_uuid( self.partition_id_map[map_name], part_uuid ) def create_root_partition(self, mbsize: str, clone: int = 0): """ Create root partition Populates kiwi_RootPart(id) and kiwi_BootPart(id) if no extra boot partition is requested :param str mbsize: partition size string :param int clone: create [clone] cop(y/ies) of the root partition """ (mbsize, mbsize_clone) = Disk._parse_size(mbsize) if clone: self._create_clones('root', clone, 't.linux', mbsize_clone) self.partitioner.create('p.lxroot', mbsize, 't.linux') self._add_to_map('root') self._add_to_public_id_map('kiwi_RootPart') if 'kiwi_ROPart' in self.public_partition_id_map: self._add_to_public_id_map('kiwi_RWPart') if 'kiwi_BootPart' not in self.public_partition_id_map: self._add_to_public_id_map('kiwi_BootPart') root_uuid = self.gUID.get('root') if root_uuid: self.partitioner.set_uuid( self.partition_id_map['root'], root_uuid ) def create_root_lvm_partition(self, mbsize: str, clone: int = 0): """ Create root partition for use with LVM Populates kiwi_RootPart(id) :param str mbsize: partition size string :param int clone: create [clone] cop(y/ies) of the lvm roo partition """ (mbsize, mbsize_clone) = Disk._parse_size(mbsize) if clone: self._create_clones('root', clone, 't.lvm', mbsize_clone) self.partitioner.create('p.lxlvm', mbsize, 't.lvm') self._add_to_map('root') self._add_to_public_id_map('kiwi_RootPart') root_uuid = self.gUID.get('root') if root_uuid: self.partitioner.set_uuid( self.partition_id_map['root'], root_uuid ) def create_root_raid_partition(self, mbsize: str, clone: int = 0): """ Create root partition for use with MD Raid Populates kiwi_RootPart(id) and kiwi_RaidPart(id) as well as the default raid device node at boot time which is configured to be kiwi_RaidDev(/dev/mdX) :param str mbsize: partition size string :param int clone: create [clone] cop(y/ies) of the raid root partition """ (mbsize, mbsize_clone) = Disk._parse_size(mbsize) if clone: self._create_clones('root', clone, 't.raid', mbsize_clone) self.partitioner.create('p.lxraid', mbsize, 't.raid') self._add_to_map('root') self._add_to_public_id_map('kiwi_RootPart') self._add_to_public_id_map('kiwi_RaidPart') root_uuid = self.gUID.get('root') if root_uuid: self.partitioner.set_uuid( self.partition_id_map['root'], root_uuid ) def create_root_readonly_partition(self, mbsize: str, clone: int = 0): """ Create root readonly partition for use with overlayfs Populates kiwi_ReadOnlyPart(id), the partition is meant to contain a squashfs readonly filesystem. The partition size should be the size of the squashfs filesystem in order to avoid wasting disk space :param str mbsize: partition size string :param int clone: create [clone] cop(y/ies) of the ro root partition """ (mbsize, mbsize_clone) = Disk._parse_size(mbsize) if clone: self._create_clones('root', clone, 't.linux', mbsize_clone) self.partitioner.create('p.lxreadonly', mbsize, 't.linux') self._add_to_map('readonly') self._add_to_public_id_map('kiwi_ROPart') root_uuid = self.gUID.get('root') if root_uuid: self.partitioner.set_uuid( self.partition_id_map['readonly'], root_uuid ) def create_boot_partition(self, mbsize: str, clone: int = 0): """ Create boot partition Populates kiwi_BootPart(id) and optional kiwi_BootPartClone(id) :param str mbsize: partition size string :param int clone: create [clone] cop(y/ies) of the boot partition """ (mbsize, mbsize_clone) = Disk._parse_size(mbsize) if clone: self._create_clones('boot', clone, 't.linux', mbsize_clone) self.partitioner.create('p.lxboot', mbsize, 't.linux') self._add_to_map('boot') self._add_to_public_id_map('kiwi_BootPart') boot_uuid = self.gUID.get('xbootldr') if boot_uuid: self.partitioner.set_uuid( self.partition_id_map['boot'], boot_uuid ) def create_prep_partition(self, mbsize: str): """ Create prep partition Populates kiwi_PrepPart(id) :param str mbsize: partition size string """ (mbsize, _) = Disk._parse_size(mbsize) self.partitioner.create('p.prep', mbsize, 't.prep') self._add_to_map('prep') self._add_to_public_id_map('kiwi_PrepPart') def create_spare_partition(self, mbsize: str): """ Create spare partition for custom use Populates kiwi_SparePart(id) :param str mbsize: partition size string """ (mbsize, _) = Disk._parse_size(mbsize) self.partitioner.create('p.spare', mbsize, 't.linux') self._add_to_map('spare') self._add_to_public_id_map('kiwi_SparePart') def create_swap_partition(self, mbsize: str): """ Create swap partition Populates kiwi_SwapPart(id) :param str mbsize: partition size string """ (mbsize, _) = Disk._parse_size(mbsize) self.partitioner.create('p.swap', mbsize, 't.swap') self._add_to_map('swap') self._add_to_public_id_map('kiwi_SwapPart') swap_uuid = self.gUID.get('swap') if swap_uuid: self.partitioner.set_uuid( self.partition_id_map['swap'], swap_uuid ) def create_efi_csm_partition(self, mbsize: str): """ Create EFI bios grub partition Populates kiwi_BiosGrub(id) :param str mbsize: partition size string """ (mbsize, _) = Disk._parse_size(mbsize) self.partitioner.create('p.legacy', mbsize, 't.csm') self._add_to_map('efi_csm') self._add_to_public_id_map('kiwi_BiosGrub') def create_efi_partition(self, mbsize: str): """ Create EFI partition Populates kiwi_EfiPart(id) :param str mbsize: partition size string """ (mbsize, _) = Disk._parse_size(mbsize) self.partitioner.create('p.UEFI', mbsize, 't.efi') self._add_to_map('efi') self._add_to_public_id_map('kiwi_EfiPart') esp_uuid = self.gUID.get('esp') if esp_uuid: self.partitioner.set_uuid( self.partition_id_map['efi'], esp_uuid ) def activate_boot_partition(self): """ Activate boot partition Note: not all Partitioner instances supports this """ partition_id = None if 'prep' in self.partition_id_map: partition_id = self.partition_id_map['prep'] elif 'boot' in self.partition_id_map: partition_id = self.partition_id_map['boot'] elif 'root' in self.partition_id_map: partition_id = self.partition_id_map['root'] if partition_id: self.partitioner.set_flag(partition_id, 'f.active') def create_hybrid_mbr(self): """ Turn partition table into a hybrid GPT/MBR table Note: only GPT tables supports this """ self.partitioner.set_hybrid_mbr() def create_mbr(self): """ Turn partition table into MBR (msdos table) Note: only GPT tables supports this """ self.partitioner.set_mbr() def set_start_sector(self, start_sector: int): """ Set start sector Note: only effective on DOS tables """ self.partitioner.set_start_sector(start_sector) def wipe(self): """ Zap (destroy) any GPT and MBR data structures if present For DASD disks create a new VTOC table """ if 'dasd' in self.table_type: log.debug('Initialize DASD disk with new VTOC table') fdasd_input = Temporary().new_file() with open(fdasd_input.name, 'w') as vtoc: vtoc.write('y\n\nw\nq\n') bash_command = ' '.join( [ 'cat', fdasd_input.name, '|', 'fdasd', '-f', self.storage_provider.get_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') else: log.debug('Initialize %s disk', self.table_type) Command.run( [ 'sgdisk', '--zap-all', self.storage_provider.get_device() ] ) def map_partitions(self): """ Map/Activate partitions In order to access the partitions through a device node it is required to map them if the storage provider is loop based """ if self.storage_provider.is_loop(): if self.partition_mapper == 'kpartx': Command.run( ['kpartx', '-s', '-a', self.storage_provider.get_device()] ) else: Command.run( ['partx', '--add', self.storage_provider.get_device()] ) self.is_mapped = True else: Command.run( ['partprobe', self.storage_provider.get_device()] ) def get_public_partition_id_map(self) -> Dict[str, str]: """ Populated partition name to number map """ return OrderedDict( sorted(self.public_partition_id_map.items()) ) def get_discoverable_partition_ids(self) -> Dict[str, str]: """ Ask systemd for a list of standardized GUIDs for the current architecture and return them in a dictionary. If there is no such information available an empty dictionary is returned :return: key:value dict from systemd-id128 :rtype: dict """ discoverable_ids = {} try: raw_lines = Command.run( ['systemd-id128', 'show'] ).output.split(os.linesep)[1:] for line in raw_lines: if line: line = ' '.join(line.split()) partition_name, uuid = line.split(' ') discoverable_ids[partition_name] = uuid except KiwiError as issue: log.warning( f'Failed to obtain discoverable partition IDs: {issue}' ) log.warning( 'Using built-in table' ) discoverable_ids = Defaults.get_discoverable_partition_ids() return discoverable_ids def _create_clones( self, name: str, clone: int, type_flag: str, mbsize: str ) -> None: """ Create [clone] cop(y/ies) of the given partition name The name of a clone partition uses the following name policy: * {name}clone{id} for the partition name * kiwi_{name}PartClone{id} for the kiwi map name :param str name: basename to use for clone partition names :param int clone: number of clones, >= 1 :param str type_flag: partition type name :param str mbsize: partition size string """ for clone_id in range(1, clone + 1): self.partitioner.create( f'p.lx{name}clone{clone_id}', mbsize, type_flag ) self._add_to_map(f'{name}clone{clone_id}') self._add_to_public_id_map(f'kiwi_{name}PartClone{clone_id}') @staticmethod def _parse_size(value: str) -> Tuple[str, str]: """ parse size value. This can be one of the following * A number_string * The string named: 'all_free' * The string formatted as: clone:{number_string_origin}:{number_string_clone} The method returns a tuple for size and optional clone size If no clone size exists both tuple values are the same The given number_string for the size of the partition is passed along to the actually used partitioner object and expected to be valid there. In case invalid size information is passed to the partitioner an exception will be raised in the scope of the partitioner interface and the selected partitioner class :param str value: size value :return: Tuple of strings :rtype: tuple """ if not format(value).startswith('clone:'): return (value, value) else: size_list = value.split(':') return (size_list[1], size_list[2]) def _add_to_public_id_map(self, name, value=None): if not value: value = self.partitioner.get_id() self.public_partition_id_map[name] = value def _add_to_map(self, name): device_node = None partition_number = format(self.partitioner.get_id()) if self.storage_provider.is_loop(): device_base = os.path.basename(self.storage_provider.get_device()) if self.partition_mapper == 'kpartx': device_node = ''.join( ['/dev/mapper/', device_base, 'p', partition_number] ) else: device_node = ''.join( ['/dev/', device_base, 'p', partition_number] ) else: device = self.storage_provider.get_device() if device[-1].isdigit(): device_node = ''.join( [device, 'p', partition_number] ) else: device_node = ''.join( [device, partition_number] ) if device_node: self.partition_map[name] = device_node self.partition_id_map[name] = partition_number def __exit__(self, exc_type, exc_value, traceback): if self.storage_provider.is_loop() and self.is_mapped: log.info('Cleaning up %s instance', type(self).__name__) try: if self.partition_mapper == 'kpartx': for device_node in self.partition_map.values(): Command.run(['dmsetup', 'remove', device_node]) Command.run( ['kpartx', '-d', self.storage_provider.get_device()] ) else: Command.run( ['partx', '--delete', self.storage_provider.get_device()] ) except Exception as issue: log.error( 'cleanup of partition maps on {} failed with: {}'.format( self.storage_provider.get_device(), issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/integrity_device.py0000644000000000000000000002727515015277265016116 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 os import logging from typing import ( Optional, List, Dict, IO, Union, NamedTuple ) # project import kiwi.defaults as defaults from kiwi.command import Command from kiwi.utils.temporary import Temporary from kiwi.utils.signature import Signature from kiwi.utils.block import BlockID from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.mapped_device import MappedDevice from kiwi.exceptions import KiwiOffsetError integrity_credentials_type = NamedTuple( 'integrity_credentials_type', [ ('keydescription', str), ('keyfile', str), ('keyfile_algorithm', str), ('options', List[str]) ] ) log = logging.getLogger('kiwi') class IntegrityDevice(DeviceProvider): """ **Implements dm_integrity setup on a storage device** :param object storage_provider: Instance of class based on DeviceProvider :param str integrity_algorithm: Default integrity algorithm used unless further credentials information is provided :param integrity_credentials_type credentials: Optional credentials specification to protect integrity blocks with security key(s) """ def __init__( self, storage_provider: DeviceProvider, integrity_algorithm: str, credentials: integrity_credentials_type = None ) -> None: #: the underlaying device provider self.storage_provider = storage_provider self.integrity_device: Optional[str] = None self.integrity_name = 'integrityRoot' self.integrity_algorithm = integrity_algorithm if credentials and \ credentials.keyfile and credentials.keyfile_algorithm: self.integrity_algorithm = credentials.keyfile_algorithm self.integrity_format_options = [ '--integrity', self.integrity_algorithm, '--sector-size', format(defaults.INTEGRITY_SECTOR_SIZE) ] self.integrity_open_options = [ '--integrity', self.integrity_algorithm ] if credentials and credentials.options: if 'legacy_hmac' in credentials.options: self.integrity_format_options.append( '--integrity-legacy-hmac' ) if credentials and credentials.keyfile: integrity_key_options = [ '--integrity-key-file', credentials.keyfile, '--integrity-key-size', format( os.path.getsize(credentials.keyfile) ) ] self.integrity_format_options += integrity_key_options self.integrity_open_options += integrity_key_options self.integrity_metadata_file: Optional[IO[bytes]] = None self.credentials = credentials def __enter__(self): return self def get_device(self) -> Optional[MappedDevice]: """ Instance of MappedDevice providing the dm_integrity device :return: mapped integrity device :rtype: MappedDevice """ if self.integrity_device: return MappedDevice( device=self.integrity_device, device_provider=self ) return None def create_dm_integrity(self, options: List[str] = []) -> None: """ Create dm_integrity device. :param list options: further integritysetup format options """ storage_device = self.storage_provider.get_device() log.info(f'Creating dm_integrity on {storage_device}') Command.run( [ 'integritysetup', '-v', '--batch-mode', 'format' ] + self.integrity_format_options + options + [ storage_device ] ) Command.run( [ 'integritysetup', '-v', '--batch-mode', 'open' ] + self.integrity_open_options + options + [ storage_device, self.integrity_name ] ) self.integrity_device = '/dev/mapper/' + self.integrity_name def create_integrity_metadata(self) -> None: """ Create a metadata block containing information for dm_integrity device map in the following format: |header_string|0xFF|dm_integrity_meta|0xFF|0x0| header_string: '{version} {fstype} {ro|rw} integrity' dm_integrity_meta: '{provided_data_sectors} {sector_size} {parameter_count} {parameters}' The information for dm_integrity_meta is taken from the dm_integrity superblock. From the flags field of the superblock a list of space separated parameters is created. The first element of the parameter list contains information about the used hash algorithm and secret, which are not part of the superblock and provided according to the parameters passed along with the integritysetup call. The number of parameters in the resulting parameter list is provided as value in parameter_count and prepended to the actual list of parameters. Please note, writing of the metadata block can destroy the filesystem on the device_node if it was not created with a smaller size than the device_node ! """ metadata_format_version = defaults.DM_METADATA_FORMAT_VERSION filesystem = BlockID(self.integrity_device).get_filesystem() integrity_superblock = self._get_integrity_superblock() if filesystem and integrity_superblock: filesystem_mode = 'ro' if filesystem == 'squashfs' else 'rw' header_string = '{0} {1} {2} integrity'.format( metadata_format_version, filesystem, filesystem_mode ) if self.credentials and self.credentials.keyfile and \ self.integrity_algorithm == defaults.INTEGRITY_KEY_ALGORITHM: # The internal_hash setup in case of a keyfile is currently # only done for the key based algorithm configured in the # kiwi defaults space. Thus the following split is safe as # we know the name. (keyformat, algorithm) = self.integrity_algorithm.split('-', 2) keytype = f'{keyformat}({algorithm})' keyfile_reference = \ self.credentials.keydescription or ':{0}'.format( os.path.basename(self.credentials.keyfile).replace( '.bin', '' ) ) parameters = [ f'internal_hash:{keytype}:{keyfile_reference}' ] else: parameters = [ f'internal_hash:{self.integrity_algorithm}' ] parameters += list(integrity_superblock['flags']) dm_integrity_meta = '{0} {1} {2} {3}'.format( integrity_superblock['provided_data_sectors'], integrity_superblock['sector_size'], len(parameters), ' '.join(parameters) ) self.integrity_metadata_file = Temporary().new_file() with open(self.integrity_metadata_file.name, 'wb') as meta: meta.write(header_string.encode("ascii")) meta.write(b'\xFF') meta.write(dm_integrity_meta.encode("ascii")) meta.write(b'\xFF') meta.write(b'\0') def sign_integrity_metadata(self) -> None: """ Create an openssl based signature from the metadata block and attach it at the end of the block. """ if self.integrity_metadata_file: Signature(self.integrity_metadata_file.name).sign() def write_integrity_metadata(self) -> None: """ Write metadata block beginning at getsize64() - defaults.DM_METADATA_OFFSET """ if self.integrity_metadata_file: meta_data_size = os.path.getsize( self.integrity_metadata_file.name ) if meta_data_size > defaults.DM_METADATA_OFFSET: raise KiwiOffsetError( 'Metadata size of {0}b exceeds {1}b limit'.format( meta_data_size, defaults.DM_METADATA_OFFSET ) ) with open(self.integrity_metadata_file.name, 'rb') as meta: with open(self.storage_provider.get_device(), 'r+b') as target: # seek --defaults.DM_METADATA_OFFSET from the # end to reach the metadata start # Please note, writing of the metadata block can destroy # the filesystem on the device_node if it was not created # with a smaller size than the device_node, you have been # warned. target.seek(-defaults.DM_METADATA_OFFSET, 2) target.write(meta.read()) def create_integritytab(self, filename: str) -> None: """ Create integritytab, setting the UUID and options of the storage device :param string filename: file path name integrity UUID={0} - integrity-algorithm=foo """ storage_device = self.storage_provider.get_device() key_file = '-' if self.credentials and self.credentials.keyfile: key_file = self.credentials.keyfile with open(filename, 'w') as integritytab: block_operation = BlockID(storage_device) integritytab.write( '{0} PARTUUID={1} {2} integrity-algorithm={3}{4}'.format( self.integrity_name, block_operation.get_blkid('PARTUUID'), key_file, self.integrity_algorithm, os.linesep ) ) def is_loop(self) -> bool: """ Check if storage provider is loop based Return loop status from base storage provider :return: True or False :rtype: bool """ return self.storage_provider.is_loop() def _get_integrity_superblock(self) -> Dict[str, Union[str, List[str]]]: integrity: Dict[str, Union[str, List[str]]] = {} dump_call = Command.run( ['integritysetup', 'dump', self.storage_provider.get_device()] ) for line in dump_call.output.strip().split(os.linesep): if line and not line.startswith('Info for'): entry = line.split(' ') if entry[0] == 'flags': integrity[entry[0]] = entry[1:] else: integrity[entry[0]] = entry[1] return integrity def __exit__(self, exc_type, exc_value, traceback): if self.integrity_device: try: Command.run( ['integritysetup', 'close', self.integrity_name] ) except Exception as issue: log.error( 'Shutdown of integrity map {0}:{1} failed with {2}'.format( self.integrity_name, self.integrity_device, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3571455 kiwi-10.2.24/kiwi/storage/loop_device.py0000644000000000000000000001060015015277265015031 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 time import pathlib # project from kiwi.command import Command from kiwi.storage.device_provider import DeviceProvider from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.exceptions import ( KiwiLoopSetupError, KiwiCommandNotFound, KiwiCommandError ) log = logging.getLogger('kiwi') class LoopDevice(DeviceProvider): """ **Create and manage loop device file for block operations** :param string filename: loop file name to create :param int filesize_mbytes: size of the loop file :param int blocksize_bytes: blocksize used in loop driver """ def __init__( self, filename: str, filesize_mbytes: int = None, blocksize_bytes: int = None ): self.node_name = '' if not os.path.exists(filename) and not filesize_mbytes: raise KiwiLoopSetupError( 'Can not create loop file without a size' ) self.filename = filename self.filesize_mbytes = filesize_mbytes self.blocksize_bytes = blocksize_bytes def __enter__(self): return self def get_device(self) -> str: """ Device node name :return: device node name :rtype: str """ return self.node_name def is_loop(self) -> bool: """ Always True :return: True :rtype: bool """ return True def create(self, overwrite: bool = True): """ Setup a loop device of the blocksize given in the constructor The file to loop is created with the size specified in the constructor unless an existing one should not be overwritten :param bool overwrite: overwrite existing file to loop """ if overwrite: qemu_img_size = format(self.filesize_mbytes) + 'M' Command.run( ['qemu-img', 'create', self.filename, qemu_img_size] ) loop_options = [] if self.blocksize_bytes and self.blocksize_bytes != 512: if CommandCapabilities.has_option_in_help( 'losetup', '--sector-size', raise_on_error=False ): loop_options.append('--sector-size') else: loop_options.append('--logical-blocksize') loop_options.append(format(self.blocksize_bytes)) loop_call = Command.run( ['losetup'] + loop_options + ['-f', '--show', self.filename] ) self.node_name = loop_call.output.rstrip(os.linesep) def __exit__(self, exc_type, exc_value, traceback): if self.node_name: try: Command.run(['losetup', '-d', self.node_name]) # loop detach is an async operation, re-use of the device # should not be done before the block device has been released loop_released = False sys_block_loop = pathlib.Path( '/sys/devices/virtual/block/{0}/loop'.format( os.path.basename(self.node_name) ) ) for busy in range(0, 100): if not sys_block_loop.is_block_device(): loop_released = True break time.sleep(0.1) if not loop_released: raise KiwiCommandError( f'Loop device {self.node_name} still attached' ) except (KiwiCommandError, KiwiCommandNotFound) as issue: log.error( 'loop cleanup on {0} failed with: {1}'.format( self.node_name, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/luks_device.py0000644000000000000000000002217315015277265015046 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 binascii from typing import Optional # project from kiwi.utils.temporary import Temporary from kiwi.utils.checksum import Checksum from kiwi.path import Path from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.mapped_device import MappedDevice from kiwi.exceptions import ( KiwiLuksSetupError ) log = logging.getLogger('kiwi') class LuksDevice(DeviceProvider): """ **Implements luks setup on a storage device** :param object storage_provider: Instance of class based on DeviceProvider """ def __init__(self, storage_provider: DeviceProvider) -> None: #: the underlaying device provider self.storage_provider = storage_provider self.luks_device: Optional[str] = None self.luks_keyfile: str = '' self.passphrase: str = '' self.luks_name = 'luksRoot' self.option_map = { 'sle12': [ '--cipher', 'aes-xts-plain64', '--key-size', '256', '--hash', 'sha1' ] } def __enter__(self): return self def get_device(self) -> Optional[MappedDevice]: """ Instance of MappedDevice providing the luks device :return: mapped luks device :rtype: MappedDevice """ if self.luks_device: return MappedDevice( device=self.luks_device, device_provider=self ) return None def create_crypto_luks( self, passphrase: str, osname: str = None, options: list = None, keyfile: str = '', randomize: bool = True, root_dir: str = '' ) -> None: """ Create luks device. Please note the passphrase is readable at creation time of this image. Make sure your host system is secure while this process runs :param string passphrase: credentials :param string osname: distribution name to match distribution specific options for cryptsetup :param list options: further cryptsetup options :param string keyfile: file path name file path name which contains an alternative key to unlock the luks device :param string root_dir: root dir path """ keyslot = '0' self.passphrase = passphrase if not options: options = [] if osname: if osname in self.option_map: options += self.option_map[osname] else: raise KiwiLuksSetupError( 'no custom option configuration found for OS %s' % osname ) extra_options = [] storage_device = self.storage_provider.get_device() log.info('Creating crypto LUKS on %s', storage_device) if not passphrase: log.warning('Using an empty passphrase for the key setup') if keyfile: self.luks_keyfile = keyfile keyfile_path = os.path.normpath( os.sep.join([root_dir, self.luks_keyfile]) ) random_passphrase = LuksDevice.create_random_keyfile(keyfile_path) if randomize: log.info('--> Randomizing...') storage_size_mbytes = self.storage_provider.get_byte_size( storage_device ) / 1048576 Command.run( [ 'dd', 'if=/dev/urandom', 'bs=1M', 'count=%d' % storage_size_mbytes, 'of=%s' % storage_device ] ) log.info('--> Creating LUKS map') if passphrase and passphrase == 'random': # In random mode use the generated keyfile as the only # key to decrypt. This is only secure if the generated # initrd also gets protected, e.g through encryption # like it is done with the secure linux execution on # zSystems self.passphrase = random_passphrase passphrase_file = keyfile_path # Do not add an additional keyfile keyfile = '' elif passphrase: # Setup a passphrase file for which the system will # ask for in an interactive dialog passphrase_file_tmp = Temporary().new_file() with open(passphrase_file_tmp.name, 'w') as credentials: credentials.write(passphrase) passphrase_file = passphrase_file_tmp.name else: # Setup an empty passphrase, insecure and only useful # for initial deployment which then applies a process # to secure the image e.g reencrypt passphrase_file_zero = '/dev/zero' extra_options = [ '--keyfile-size', '32' ] passphrase_file = passphrase_file_zero Command.run( [ 'cryptsetup', '-q', '--key-file', passphrase_file ] + options + extra_options + [ 'luksFormat', storage_device ] ) if keyfile: Command.run( [ 'cryptsetup', '--key-file', passphrase_file ] + extra_options + [ 'luksAddKey', storage_device, keyfile_path ] ) # Create backup header checksum as reencryption reference master_checksum = f'{root_dir}/root/.luks.header' Path.wipe(master_checksum) Command.run( [ 'cryptsetup', '--key-file', passphrase_file ] + extra_options + [ 'luksHeaderBackup', storage_device, '--header-backup-file', master_checksum ] ) checksum = Checksum(master_checksum).sha256() with open(master_checksum, 'w') as shasum: shasum.write(checksum) # Create key slot number as reencryption reference master_slot = f'{root_dir}/root/.luks.slot' with open(master_slot, 'w') as slot: slot.write(keyslot) # Create slot passphrase as reencryption reference master_slotpass = f'{root_dir}/root/.slotpass' with open(master_slotpass, 'w') as slotpass: slotpass.write(self.passphrase) # open the pool Command.run( [ 'cryptsetup', '--key-file', passphrase_file ] + extra_options + [ 'luksOpen', storage_device, self.luks_name ] ) self.luks_device = '/dev/mapper/' + self.luks_name def create_crypttab(self, filename: str) -> None: """ Create crypttab, setting the UUID of the storage device :param string filename: file path name """ storage_device = self.storage_provider.get_device() with open(filename, 'w') as crypttab: luks_uuid = self.storage_provider.get_uuid(storage_device) if self.luks_keyfile: crypttab.write( 'luks UUID={0} /{1}{2}'.format( luks_uuid, self.luks_keyfile.lstrip(os.sep), os.linesep ) ) else: crypttab.write( 'luks UUID={0}{1}'.format( luks_uuid, os.linesep ) ) def is_loop(self) -> bool: """ Check if storage provider is loop based Return loop status from base storage provider :return: True or False :rtype: bool """ return self.storage_provider.is_loop() @staticmethod def create_random_keyfile(filename: str) -> str: """ Create keyfile with random data :param string filename: file path name """ random_data = binascii.hexlify( os.urandom(Defaults.get_luks_key_length()) ).decode() with open(filename, 'w') as keyfile: keyfile.write(random_data) os.chmod(filename, 0o600) return random_data def __exit__(self, exc_type, exc_value, traceback): if self.luks_device: try: Command.run( ['cryptsetup', 'luksClose', self.luks_name] ) except Exception as issue: log.error( 'Shutdown of luks map {0}:{1} failed with: {2}'.format( self.luks_name, self.luks_device, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/mapped_device.py0000644000000000000000000000336615015277265015341 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 kiwi.storage.device_provider import DeviceProvider from kiwi.exceptions import ( KiwiMappedDeviceError ) class MappedDevice(DeviceProvider): """ **Hold a reference on a single device** :param object device_provider: Instance of class based on DeviceProvider :param string device: Device node name """ def __init__(self, device: str, device_provider: DeviceProvider): if not os.path.exists(device): raise KiwiMappedDeviceError( 'Device %s does not exist' % device ) self.device_provider = device_provider self.device = device def get_device(self) -> str: """ Mapped device node name :return: device node name :rtype: str """ return self.device def is_loop(self) -> bool: """ Check if storage provider is loop based Return loop status from base storage provider :return: True or False :rtype: bool """ return self.device_provider.is_loop() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/raid_device.py0000644000000000000000000001033215015277265015001 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 Optional # project from kiwi.command import Command from kiwi.storage.device_provider import DeviceProvider from kiwi.storage.mapped_device import MappedDevice from kiwi.exceptions import ( KiwiRaidSetupError ) log = logging.getLogger('kiwi') class RaidDevice(DeviceProvider): """ **Implement raid setup on a storage device** :param object storage_provider: Instance of class based on DeviceProvider """ def __init__(self, storage_provider: DeviceProvider): #: the underlaying device provider self.storage_provider = storage_provider self.raid_level_map = { 'mirroring': '1', 'striping': '0' } self.raid_device = None def __enter__(self): return self def get_device(self) -> Optional[MappedDevice]: """ Instance of MappedDevice providing the raid device :return: mapped raid device :rtype: MappedDevice """ if self.raid_device: return MappedDevice( device=self.raid_device, device_provider=self ) return None def create_degraded_raid(self, raid_level): """ Create a raid array in degraded mode with one device missing. This only works in the raid levels 0(striping) and 1(mirroring) :param string raid_level: raid level name """ if raid_level not in self.raid_level_map: raise KiwiRaidSetupError( 'Only raid levels 0(striping) and 1(mirroring) are supported' ) raid_device = None for raid_id in range(9): raid_device = '/dev/md' + format(raid_id) if os.path.exists(raid_device): raid_device = None else: break if not raid_device: raise KiwiRaidSetupError( 'Could not find free raid device in range md0-8' ) log.info( 'Creating raid array in %s mode as %s', raid_level, raid_device ) Command.run( [ 'mdadm', '--create', '--run', raid_device, '--level', self.raid_level_map[raid_level], '--raid-disks', '2', self.storage_provider.get_device(), 'missing' ] ) self.raid_device = raid_device def create_raid_config(self, filename: str) -> None: """ Create mdadm config file from mdadm request :param string filename: config file name """ if not self.raid_device: raise KiwiRaidSetupError("No raid device defined, cannot create raid config!") mdadm_call = Command.run( ['mdadm', '-Db', self.raid_device] ) with open(filename, 'w') as mdadmconf: mdadmconf.write(mdadm_call.output) def is_loop(self) -> bool: """ Check if storage provider is loop based Return loop status from base storage provider :return: True or False :rtype: bool """ return self.storage_provider.is_loop() def __exit__(self, exc_type, exc_value, traceback): if self.raid_device: try: Command.run( ['mdadm', '--stop', self.raid_device] ) except Exception as issue: log.error( 'Shutdown of raid device {0} failed with: {1}'.format( self.raid_device, issue ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/setup.py0000644000000000000000000004372715015277265013721 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 textwrap import dedent # project from kiwi.firmware import FirmWare from kiwi.system.size import SystemSize from kiwi.defaults import Defaults from kiwi.xml_state import XMLState from kiwi.exceptions import ( KiwiVolumeTooSmallError, KiwiPartitionTooSmallError ) log = logging.getLogger('kiwi') class DiskSetup: """ **Implements disk setup methods** Methods from this class provides information required before building a disk image :param object xml_state: Instance of XMLState :param string root_dir: root directory path name """ def __init__(self, xml_state: XMLState, root_dir: str): self.root_filesystem_is_overlay = xml_state.build_type.get_overlayroot() self.swap_mbytes = xml_state.get_oemconfig_swap_mbytes() self.configured_size = xml_state.get_build_type_size() self.disk_resize_requested = xml_state.get_oemconfig_oem_resize() self.filesystem = xml_state.build_type.get_filesystem() self.bootpart_requested = xml_state.build_type.get_bootpartition() self.bootpart_mbytes = xml_state.build_type.get_bootpartsize() self.spare_part_mbytes = xml_state.get_build_type_spare_part_size() self.mdraid = xml_state.build_type.get_mdraid() self.luks = xml_state.get_luks_credentials() self.volume_manager = xml_state.get_volume_management() self.bootloader = xml_state.get_build_type_bootloader_name() self.oemconfig = xml_state.get_build_type_oemconfig_section() self.volumes = xml_state.get_volumes() self.custom_partitions = xml_state.get_partitions() self.firmware = FirmWare( xml_state ) self.rootsize = SystemSize( root_dir ) self.root_dir = root_dir self.xml_state = xml_state def get_disksize_mbytes( self, root_clone: int = 0, boot_clone: int = 0 ) -> int: """ Precalculate disk size requirements in mbytes :param int root_clone: root partition gets cloned, N+1 times the size is needed :param int boot_clone: boot partition gets cloned, N+1 times the size is needed :return: disk size mbytes :rtype: int """ log.info('Precalculating required disk size') calculated_disk_mbytes = 0 root_filesystem_mbytes = self.rootsize.customize( self.rootsize.accumulate_mbyte_file_sizes(), self.filesystem ) if root_clone: root_clone += 1 log.info( '--> root partition is clone: {0}*{1} MB'.format( root_clone, root_filesystem_mbytes ) ) root_filesystem_mbytes *= root_clone calculated_disk_mbytes += root_filesystem_mbytes log.info( '--> system data with filesystem overhead needs %s MB', root_filesystem_mbytes ) if self.custom_partitions: partition_mbytes = self._accumulate_partitions_size() if partition_mbytes: calculated_disk_mbytes += partition_mbytes log.info( '--> partition(s) size setup adding %s MB', partition_mbytes ) if self.volume_manager and self.volume_manager == 'lvm': lvm_overhead_mbytes = Defaults.get_lvm_overhead_mbytes() log.info( '--> LVM overhead adding %s MB', lvm_overhead_mbytes ) calculated_disk_mbytes += lvm_overhead_mbytes volume_mbytes = self._accumulate_volume_size( root_filesystem_mbytes ) if volume_mbytes: calculated_disk_mbytes += volume_mbytes log.info( '--> volume(s) size setup adding %s MB', volume_mbytes ) elif self.swap_mbytes: calculated_disk_mbytes += self.swap_mbytes log.info( '--> swap partition adding %s MB', self.swap_mbytes ) legacy_bios_mbytes = self.firmware.get_legacy_bios_partition_size() if legacy_bios_mbytes: calculated_disk_mbytes += legacy_bios_mbytes log.info( '--> legacy bios boot partition adding %s MB', legacy_bios_mbytes ) boot_mbytes = self.boot_partition_size() if boot_mbytes: if boot_clone: boot_clone += 1 log.info( '--> boot partition is clone: {0}*{1} MB'.format( boot_clone, boot_mbytes ) ) boot_mbytes *= boot_clone calculated_disk_mbytes += boot_mbytes log.info( '--> boot partition adding %s MB', boot_mbytes ) if self.spare_part_mbytes: calculated_disk_mbytes += self.spare_part_mbytes log.info( '--> spare partition adding %s MB', self.spare_part_mbytes ) efi_mbytes = self.firmware.get_efi_partition_size() if efi_mbytes: calculated_disk_mbytes += efi_mbytes log.info( '--> EFI partition adding %s MB', efi_mbytes ) prep_mbytes = self.firmware.get_prep_partition_size() if prep_mbytes: calculated_disk_mbytes += prep_mbytes log.info( '--> PReP partition adding %s MB', prep_mbytes ) recovery_mbytes = self._inplace_recovery_partition_size() if recovery_mbytes: calculated_disk_mbytes += recovery_mbytes log.info( '--> In-place recovery partition adding: %s MB', recovery_mbytes ) if not self.configured_size: log.info( 'Using calculated disk size: %d MB', calculated_disk_mbytes ) return calculated_disk_mbytes elif self.configured_size.additive: result_disk_mbytes = \ self.configured_size.mbytes + calculated_disk_mbytes log.info( 'Using configured disk size: %d MB + %d MB calculated = %d MB', self.configured_size.mbytes, calculated_disk_mbytes, result_disk_mbytes ) return result_disk_mbytes else: log.info( 'Using configured disk size: %d MB', self.configured_size.mbytes ) if self.configured_size.mbytes < calculated_disk_mbytes: log.warning( '--> Configured size smaller than calculated size: %d MB', calculated_disk_mbytes ) return self.configured_size.mbytes def need_boot_partition(self): """ Decide if an extra boot partition is needed. This is done with the bootpartition attribute from the type, however if it is not set it depends on some other type configuration parameters if we need a boot partition or not :return: True or False :rtype: bool """ if self.bootpart_requested is True: return True if self.bootpart_requested is False: return False if self.mdraid: return True if self.volume_manager == 'lvm': return True if self.volume_manager == 'btrfs': return False if self.root_filesystem_is_overlay: return True return False @staticmethod def get_boot_label() -> str: """ Filesystem Label to use for the boot partition :return: label name :rtype: str """ return 'BOOT' def get_root_label(self) -> str: """ Filesystem Label to use for the root partition If not specified in the XML configuration the default root label is set to 'ROOT' :return: label name :rtype: str """ root_label = self.xml_state.build_type.get_rootfs_label() if not root_label: root_label = 'ROOT' return root_label @staticmethod def get_efi_label() -> str: """ Filesystem Label to use for the EFI partition :return: label name :rtype: str """ return 'EFI' def boot_partition_size(self) -> int: """ Size of the boot partition in mbytes :return: boot size mbytes :rtype: int """ if self.need_boot_partition(): if self.bootpart_mbytes: return self.bootpart_mbytes return Defaults.get_default_boot_mbytes() return 0 def _inplace_recovery_partition_size(self): """ In inplace recovery mode the recovery archive is created at install time. This requires free space on the disk. The amount of free space is specified with the oem-recovery-part-size attribute. If specified we add the given size to the disk. If not specified an inplace setup at install time will be moved to the first boot of an oem image when the recovery partition has been created """ if self.oemconfig and self.oemconfig.get_oem_inplace_recovery(): recovery_mbytes = self.oemconfig.get_oem_recovery_part_size() if recovery_mbytes: return int(recovery_mbytes[0] * 1.7) def _accumulate_partitions_size(self): """ Calculate number of mbytes to add to the disk to allow the creaton of the partitions with their configured size """ disk_partition_mbytes = 0 data_partition_mbytes = self._calculate_partition_mbytes() for map_name in sorted(self.custom_partitions.keys()): partition_mount_path = self.custom_partitions[map_name].mountpoint partition_filesystem = self.custom_partitions[map_name].filesystem partition_clone = self.custom_partitions[map_name].clone if partition_mount_path: partition_mbsize = self.custom_partitions[map_name].mbsize if partition_filesystem == 'squashfs': # cannot predict compressed size prior compressing # use size as configured disk_add_mbytes = int(partition_mbsize) else: disk_add_mbytes = int(partition_mbsize) - \ data_partition_mbytes.partition[partition_mount_path] if disk_add_mbytes > 0: if partition_clone: partition_clone += 1 log.info( '--> {0} partition is clone: {1}*{2} MB'.format( map_name, partition_clone, disk_add_mbytes ) ) disk_add_mbytes *= partition_clone disk_partition_mbytes += disk_add_mbytes else: message = dedent('''\n Requested partition size {0}MB for {1!r} is too small The minimum byte value to store the data below the {1!r} path was calculated to be {2}MB ''') raise KiwiPartitionTooSmallError( message.format( partition_mbsize, partition_mount_path, data_partition_mbytes.partition[partition_mount_path] ) ) return disk_partition_mbytes def _accumulate_volume_size(self, root_mbytes): """ Calculate number of mbytes to add to the disk to allow the creaton of the volumes with their configured size """ disk_volume_mbytes = 0 data_volume_mbytes = self._calculate_volume_mbytes() root_volume = self._get_root_volume_configuration() # If disk resize is requested we only add the default min # volume size because their target size request is handled # on first boot of the disk image in oemboot/repart if self.disk_resize_requested: for volume in self.volumes: disk_volume_mbytes += Defaults.get_min_volume_mbytes( self.filesystem ) return disk_volume_mbytes # For static disk(no resize requested) we need to add the # configured volume sizes because the image is used directly # as it is without being deployed and resized on a target disk for volume in self.volumes: if volume.realpath and not volume.realpath == '/' and volume.size: [size_type, req_size] = volume.size.split(':') disk_add_mbytes = 0 if size_type == 'freespace': disk_add_mbytes += int(req_size) else: disk_add_mbytes += int(req_size) - \ data_volume_mbytes.volume[volume.realpath] if disk_add_mbytes > 0: disk_volume_mbytes += disk_add_mbytes + \ Defaults.get_min_volume_mbytes(self.filesystem) else: message = dedent('''\n Requested volume size {0}MB for {1!r} is too small The minimum byte value to store the data below the {1!r} path was calculated to be {2}MB ''') raise KiwiVolumeTooSmallError( message.format( req_size, volume.realpath, data_volume_mbytes.volume[volume.realpath] ) ) if root_volume: if root_volume.size_type == 'freespace': disk_add_mbytes = root_volume.req_size else: disk_add_mbytes = root_volume.req_size - \ root_mbytes + data_volume_mbytes.total if disk_add_mbytes > 0: disk_volume_mbytes += disk_add_mbytes + \ Defaults.get_min_volume_mbytes(self.filesystem) else: log.warning( 'root volume size of %s MB is too small, skipped', root_volume.req_size ) return disk_volume_mbytes def _get_root_volume_configuration(self): """ Provide root volume configuration if present and in use according to the selected volume management. So far this only affects the LVM volume manager """ root_volume_type = namedtuple( 'root_volume_type', ['size_type', 'req_size'] ) for volume in self.volumes: if volume.is_root_volume: if volume.size: [size_type, req_size] = volume.size.split(':') return root_volume_type( size_type=size_type, req_size=int(req_size) ) def _calculate_volume_mbytes(self): """ Calculate the number of mbytes each volume path currently consumes and also provide a total number of these values. """ volume_mbytes_type = namedtuple( 'volume_mbytes_type', ['volume', 'total'] ) volume_mbytes = {} volume_total = 0 for volume in self.volumes: if volume.realpath and not volume.realpath == '/': path_to_volume = self.root_dir + '/' + volume.realpath if os.path.exists(path_to_volume): volume_size = SystemSize(path_to_volume) volume_mbytes[volume.realpath] = volume_size.customize( volume_size.accumulate_mbyte_file_sizes(), self.filesystem ) else: volume_mbytes[volume.realpath] = 0 volume_total += volume_mbytes[volume.realpath] return volume_mbytes_type( volume=volume_mbytes, total=volume_total ) def _calculate_partition_mbytes(self): """ Calculate the number of mbytes each partition path consumes """ partition_mbytes_type = namedtuple( 'partition_mbytes_type', ['partition'] ) partition_mbytes = {} for map_name in sorted(self.custom_partitions.keys()): partition_mount_path = self.custom_partitions[map_name].mountpoint if partition_mount_path: partition_filesystem = self.custom_partitions[map_name].filesystem path_to_partition = os.path.normpath( os.sep.join([self.root_dir, partition_mount_path]) ) if os.path.exists(path_to_partition): partition_size = SystemSize(path_to_partition) partition_mbytes[partition_mount_path] = partition_size.customize( partition_size.accumulate_mbyte_file_sizes(), partition_filesystem ) else: partition_mbytes[partition_mount_path] = 0 return partition_mbytes_type( partition=partition_mbytes ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/__init__.py0000644000000000000000000001004215015277265016302 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, Tuple # project from kiwi.storage.subformat.base import DiskFormatBase from kiwi.xml_state import XMLState from kiwi.exceptions import KiwiDiskFormatSetupError class DiskFormat: """ **DiskFormat factory** :param string name: Format name :param object xml_state: Instance of XMLState :param string root_dir: root directory path name :param string target_dir: target directory path name """ @staticmethod def new( name: str, xml_state: XMLState, root_dir: str, target_dir: str ) -> DiskFormatBase: name_map = { 'qcow2': 'Qcow2', 'vdi': 'Vdi', 'vhd': 'Vhd', 'vhdx': 'Vhdx', 'vhdfixed': 'VhdFixed', 'gce': 'Gce', 'vmdk': 'Vmdk', 'ova': 'Ova', 'vagrant_libvirt': 'VagrantLibVirt', 'vagrant_virtualbox': 'VagrantVirtualBox', 'base': 'Base' } module_namespace: Optional[str] = None try: custom_args, module_namespace = DiskFormat.\ _custom_args_for_format(name, xml_state) diskformat = importlib.import_module( f'kiwi.storage.subformat.{module_namespace}' ) module_name = f'DiskFormat{name_map[module_namespace]}' return diskformat.__dict__[module_name]( xml_state, root_dir, target_dir, custom_args ) except Exception as issue: raise KiwiDiskFormatSetupError( f'No support for {module_namespace} disk format: {issue}' ) @staticmethod def _custom_args_for_format(name: str, xml_state: XMLState) -> Tuple[Dict, str]: custom_args = xml_state.get_build_type_format_options() module_namespace = name if name == 'vhd-fixed': disk_tag = xml_state.build_type.get_vhdfixedtag() if disk_tag: custom_args.update( {'--tag': disk_tag} ) module_namespace = 'vhdfixed' elif name == 'gce': gce_license_tag = xml_state.build_type.get_gcelicense() if gce_license_tag: custom_args.update( {'--tag': gce_license_tag} ) elif name == 'vmdk' or name == 'ova': vmdisk_section = xml_state.get_build_type_vmdisk_section() if vmdisk_section: disk_mode = vmdisk_section.get_diskmode() disk_controller = vmdisk_section.get_controller() if disk_mode: custom_args.update( {'subformat={0}'.format(disk_mode): None} ) if disk_controller: custom_args.update( {'adapter_type={0}'.format(disk_controller): None} ) elif name == 'vagrant': vagrant_config = xml_state.get_build_type_vagrant_config_section() if vagrant_config: custom_args.update( {'vagrantconfig': vagrant_config} ) module_namespace = '{0}_{1}'.format( name, vagrant_config.get_provider() ) elif name == 'raw': module_namespace = 'base' return custom_args, module_namespace ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/base.py0000644000000000000000000001471515015277265015470 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 OrderedDict # project from kiwi.command import Command from kiwi.runtime_config import RuntimeConfig from kiwi.defaults import Defaults from kiwi.path import Path from kiwi.xml_state import XMLState from kiwi.system.result import Result from kiwi.exceptions import ( KiwiFormatSetupError, KiwiResizeRawDiskError ) log = logging.getLogger('kiwi') class DiskFormatBase: """ **Base class to create disk formats from a raw disk image** :param object xml_state: Instance of XMLState :param string root_dir: root directory path name :param string arch: Defaults.get_platform_name :param string target_dir: target directory path name :param dict custom_args: custom format options dictionary """ def __init__( self, xml_state: XMLState, root_dir: str, target_dir: str, custom_args: dict = None ): self.xml_state = xml_state self.root_dir = root_dir self.arch = Defaults.get_platform_name() self.target_dir = target_dir self.temp_image_dir: str = '' self.image_format: str = '' self.diskname = self.get_target_file_path_for_format('raw') self.runtime_config = RuntimeConfig() self.post_init(custom_args or {}) def __enter__(self): return self def post_init(self, custom_args: dict) -> None: """ Post initialization method Implementation in specialized disk format class if required :param dict custom_args: unused """ pass def has_raw_disk(self) -> bool: """ Check if the base raw disk image exists :return: True or False :rtype: bool """ return os.path.exists(self.diskname) def resize_raw_disk(self, size_bytes, append=False): """ Resize raw disk image to specified size. If the request would actually shrink the disk an exception is raised. If the disk got changed the method returns True, if the new size is the same as the current size nothing gets resized and the method returns False :param int size: size in bytes :return: True or False :rtype: bool """ if not append: current_byte_size = os.path.getsize(self.diskname) size_bytes = int(size_bytes) if size_bytes < current_byte_size: raise KiwiResizeRawDiskError( 'shrinking {0} disk to {1} bytes corrupts the image'.format( self.diskname, size_bytes ) ) elif size_bytes == current_byte_size: return False Command.run( [ 'qemu-img', 'resize', self.diskname, '+{0}'.format(size_bytes) if append else format(size_bytes) ] ) return True def create_image_format(self) -> None: """ Create disk format Implementation in specialized disk format class required """ raise NotImplementedError @staticmethod def get_qemu_option_list(custom_args: dict) -> list: """ Create list of qemu options from custom_args dict :param dict custom_args: arguments :return: qemu option list :rtype: list """ options = [] if custom_args: ordered_args = OrderedDict(sorted(custom_args.items())) for key, value in list(ordered_args.items()): if key == 'adapter_type=pvscsi': # Building for the pvscsi ddb.adapterType only # affects the guest configuration (vmx). For the # vmdk disk format this is the same as lsilogic key = 'adapter_type=lsilogic' options.append('-o') if value: options.append('{0}={1}'.format(key, value)) else: options.append(key) return options def get_target_file_path_for_format(self, format_name: str) -> str: """ Create target file path name for specified format :param string format_name: disk format name :return: file path name :rtype: str """ if format_name != 'raw': if format_name not in Defaults.get_disk_format_types(): raise KiwiFormatSetupError( 'unsupported disk format %s' % format_name ) return ''.join( [ self.target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + self.xml_state.get_image_version(), '.' + format_name ] ) def store_to_result(self, result: Result): """ Store result file of the format conversion into the provided result instance. By default only the converted image file will be stored as compressed file. Subformats which creates additional metadata files or want to use other result flags needs to overwrite this method :param object result: Instance of Result """ compression = self.runtime_config.get_bundle_compression(default=True) if self.xml_state.get_luks_credentials() is not None: compression = False result.add( key='disk_format_image', filename=self.get_target_file_path_for_format( self.image_format ), use_for_bundle=True, compress=compression, shasum=True ) def __exit__(self, exc_type, exc_value, traceback): if self.temp_image_dir and os.path.exists(self.temp_image_dir): Path.wipe(self.temp_image_dir) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/gce.py0000644000000000000000000001053615015277265015311 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 collections import OrderedDict # project from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.storage.subformat.base import DiskFormatBase from kiwi.archive.tar import ArchiveTar from kiwi.system.result import Result class DiskFormatGce(DiskFormatBase): """ **Create GCE - Google Compute Engine image format** """ def post_init(self, custom_args: dict) -> None: """ GCE disk format post initialization method Store disk tag from custom args :param dict custom_args: custom gce argument dictionary .. code:: python {'--tag': 'billing_code'} """ self.image_format = 'gce' self.tag = None if custom_args: ordered_args = OrderedDict(list(custom_args.items())) for key, value in list(ordered_args.items()): if key == '--tag': self.tag = value def create_image_format(self) -> None: """ Create GCE disk format and manifest """ gce_tar_ball_file_list = [] temp_image_dir = Temporary( prefix='kiwi_gce_subformat.', path=self.target_dir ).new_dir() diskname = ''.join( [ self.target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + self.xml_state.get_image_version(), '.raw' ] ) if self.tag: with open(temp_image_dir.name + '/manifest.json', 'w') as manifest: manifest.write('{"licenses": ["%s"]}' % self.tag) gce_tar_ball_file_list.append('manifest.json') Command.run( ['cp', diskname, temp_image_dir.name + '/disk.raw'] ) gce_tar_ball_file_list.append('disk.raw') archive_name = os.path.basename( self.get_target_file_path_for_format(self.image_format) ) # delete the '.gz' suffix from the name. The suffix is appended by # the archive creation method depending on the creation type. archive_name = archive_name.replace('.gz', '') archive = ArchiveTar( filename=self.target_dir + '/' + archive_name, file_list=gce_tar_ball_file_list ) archive.create_gnu_gzip_compressed( temp_image_dir.name ) def store_to_result(self, result: Result) -> None: """ Store result file of the gce format conversion into the provided result instance. In this case compression is unwanted because the gce tarball is already created as a compressed archive :param object result: Instance of Result """ result.add( key='disk_format_image', filename=self.get_target_file_path_for_format( self.image_format ), use_for_bundle=True, compress=False, shasum=True ) def get_target_file_path_for_format(self, format_name): """ Google requires the image name to follow their naming convetion. Therefore it's required to provide a suitable name by overriding the base class method :param string format_name: gce :return: file path name :rtype: str """ if format_name == 'gce': format_name = 'tar.gz' return ''.join( [ self.target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + self.xml_state.get_image_version(), '.' + format_name ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/ova.py0000644000000000000000000001032015015277265015327 0ustar00# Copyright (c) 2018 Eaton. 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 stat from textwrap import dedent # project from kiwi.storage.subformat.vmdk import DiskFormatVmdk from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.path import Path from kiwi.system.result import Result from kiwi.exceptions import ( KiwiFormatSetupError, KiwiCommandNotFound ) class DiskFormatOva(DiskFormatBase): """ **Create ova disk format, based on vmdk** """ def post_init(self, custom_args: dict) -> None: """ vmdk disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ ovftype = self.xml_state.get_build_type_machine_section().get_ovftype() if ovftype != 'vmware': raise KiwiFormatSetupError('Unsupported ovftype %s' % ovftype) self.image_format = 'ova' self.options = self.get_qemu_option_list(custom_args) self.vmdk = DiskFormatVmdk( self.xml_state, self.root_dir, self.target_dir, custom_args ) def create_image_format(self) -> None: """ Create ova disk format using ovftool from https://www.vmware.com/support/developer/ovf """ # Check for required ovftool ovftool = Path.which(filename='ovftool', access_mode=os.X_OK) if not ovftool: tool_not_found_message = dedent('''\n Required ovftool not found in PATH on the build host Building OVA images requires VMware's ovftool tool which can be installed from the following location https://developer.vmware.com/web/tool/ovf ''') raise KiwiCommandNotFound( tool_not_found_message ) # Create the vmdk disk image and vmx config self.vmdk.create_image_format() # Convert to ova using ovftool vmx = self.get_target_file_path_for_format('vmx') ova = self.get_target_file_path_for_format('ova') try: os.unlink(ova) except OSError: pass ovftool_options = [] if CommandCapabilities.has_option_in_help( ovftool, '--shaAlgorithm', raise_on_error=False ): ovftool_options.append('--shaAlgorithm=SHA1') if CommandCapabilities.has_option_in_help( ovftool, '--allowExtraConfig', raise_on_error=False ): ovftool_options.append('--allowExtraConfig') if CommandCapabilities.has_option_in_help( ovftool, '--exportFlags', raise_on_error=False ): ovftool_options.append('--exportFlags=extraconfig') Command.run( [ovftool] + ovftool_options + [vmx, ova] ) # ovftool ignores the umask and creates files with 0600 # apply file permission bits set in the vmx file to the # ova file st = os.stat(vmx) os.chmod(ova, stat.S_IMODE(st.st_mode)) def store_to_result(self, result: Result) -> None: """ Store the resulting ova file into the provided result instance. :param object result: Instance of Result """ result.add( key='disk_format_image', filename=self.get_target_file_path_for_format('ova'), use_for_bundle=True, compress=self.runtime_config.get_bundle_compression( default=False ), shasum=True ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/qcow2.py0000644000000000000000000000536115015277265015606 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.utils.temporary import Temporary from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command from kiwi.system.result import Result class DiskFormatQcow2(DiskFormatBase): """ **Create qcow2 disk format** """ def post_init(self, custom_args: dict) -> None: """ qcow2 disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ self.image_format: str = 'qcow2' self.options = self.get_qemu_option_list(custom_args) def create_image_format(self) -> None: """ Create qcow2 disk format """ intermediate = Temporary().new_file() Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', self.image_format ] + self.options + [ intermediate.name ] ) Command.run( [ 'qemu-img', 'convert', '-c', '-f', self.image_format, intermediate.name, '-O', self.image_format, self.get_target_file_path_for_format(self.image_format) ] ) def store_to_result(self, result: Result) -> None: """ Store result file of the format conversion into the provided result instance. In case of a qcow2 format we store the result uncompressed Since the format conversion only takes the real bytes into account such that the sparseness of the raw disk will not result in the output format and can be taken one by one :param object result: Instance of Result """ result.add( key='disk_format_image', filename=self.get_target_file_path_for_format( self.image_format ), use_for_bundle=True, compress=self.runtime_config.get_bundle_compression( default=False ), shasum=True ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/template/__init__.py0000644000000000000000000000000015015277265020106 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/template/vagrant_config.py0000644000000000000000000000646115015277265021357 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 from textwrap import dedent class VagrantConfigTemplate: """ **Generate a Vagrantfile configuration template** This class creates a simple template for the Vagrantfile that is included inside a vagrant box. The included Vagrantfile carries additional information for vagrant: by default that is nothing, but depending on the provider additional information need to be present. These can be passed via the parameter ``custom_settings`` to the method :meth:`get_template`. Example usage: The default without any additional settings will result in this Vagrantfile: .. code:: python >>> vagrant_config = VagrantConfigTemplate() >>> print( ... vagrant_config.get_template() ... ) Vagrant.configure("2") do |config| end If your provider/box requires additional settings, provide them as follows: .. code:: python >>> extra_settings = dedent(''' ... config.vm.hostname = "no-dead-beef" ... config.vm.provider :special do |special| ... special.secret_settings = "please_work" ... end ... ''').strip() >>> print( ... vagrant_config.get_template(extra_settings) ... ) Vagrant.configure("2") do |config| config.vm.hostname = "no-dead-beef" config.vm.provider :special do |special| special.secret_settings = "please_work" end end """ def __init__(self): self.indent = ' ' self.header = dedent(''' Vagrant.configure("2") do |config| ''').strip() + os.linesep self.end = dedent(''' end ''').strip() def get_template(self, custom_settings=None): """ Return a new template with ``custom_settings`` included and indented appropriately. :param str custom_settings: String of additional settings that get pasted into the Vagrantfile template. The string is put at the correct indentation level for you, but the internal indentation has to be provided by the caller. :return: A string with ``custom_settings`` inserted at the appropriate position. The template has one the variable ``mac_address`` that must be substituted. :rtype: str """ template = self.header if custom_settings: template += self.indent template += self.indent.join( custom_settings.splitlines(True) ) template += os.linesep template += self.end return template ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/template/virtualbox_ovf.py0000644000000000000000000001625515015277265021443 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 # from string import Template class VirtualboxOvfTemplate: """ **Generate a OVF file template for a vagrant virtualbox box** This class provides a template for virtualbox' ovf configuration file that is embedded inside the vagrant box. The template itself was extracted from a vagrant box that was build via packer and from a script provided by Neal Gompa. """ def __init__(self): # note: the OS ID is set to Other Linux 64 self.OVF_TEMPLATE = """ List of the virtual disks used in the package Logical networks used in the package Logical network used by this appliance. A virtual machine The kind of installed guest operating system ${vm_description} Linux_64 Virtual hardware requirements for a virtual machine Virtual Hardware Family 0 ${vm_name} virtualbox-2.2 1 virtual CPU Number of virtual CPUs 1 virtual CPU(s) 1 3 1 MegaBytes 1024 MB of memory Memory Size 1024 MB of memory 2 4 1024 0 ideController0 IDE Controller ideController0 3 PIIX4 5 1 ideController1 IDE Controller ideController1 4 PIIX4 5 0 sataController0 SATA Controller sataController0 5 AHCI 20 3 false sound Sound Card sound 6 ensoniq1371 35 0 disk1 Disk Image disk1 /disk/vmdisk1 7 5 17 true Ethernet adapter on 'NAT' NAT Ethernet adapter on 'NAT' 8 E1000 10 """ def get_template(self): """ Return the actual ovf template. The following values must be substituted: - ``vm_name``: the name of this VM - ``disk_image_capacity``: Size of the virtual disk image in GB - ``vm_description``: a description of this VM """ return Template(self.OVF_TEMPLATE) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/template/vmware_settings.py0000644000000000000000000001415715015277265021612 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 class VmwareSettingsTemplate: """ VMware machine settings template """ def __init__(self): self.cr = '\n' self.header = dedent(''' #!/usr/bin/env vmware # kiwi generated VMware settings file config.version = "8" tools.syncTime = "true" uuid.action = "create" virtualHW.version = "${virtual_hardware_version}" displayName = "${display_name}" guestOS = "${guest_os}" ''').strip() + self.cr self.ide_disk = dedent(''' ide${disk_id}:0.present = "true" ide${disk_id}:0.fileName= "${vmdk_file}" ide${disk_id}:0.redo = "" ''').strip() + self.cr self.scsi_disk = dedent(''' scsi${disk_id}.present = "true" scsi${disk_id}.sharedBus = "none" scsi${disk_id}.virtualDev = "${scsi_controller_name}" scsi${disk_id}:0.present = "true" scsi${disk_id}:0.fileName = "${vmdk_file}" scsi${disk_id}:0.deviceType = "scsi-hardDisk" ''').strip() + self.cr self.network = dedent(''' ethernet{nic_id}.present = "true" ethernet{nic_id}.allow64bitVmxnet = "true" ''').strip() + self.cr self.network_mac_static = dedent(''' ethernet{nic_id}.addressType = "static" ethernet{nic_id}.address = "{mac_address}" ''').strip() + self.cr self.network_mac_generated = dedent(''' ethernet{nic_id}.addressType = "generated" ''').strip() + self.cr self.network_driver = dedent(''' ethernet{nic_id}.virtualDev = "{network_driver}" ''').strip() + self.cr self.network_connection_type = dedent(''' ethernet{nic_id}.connectionType = "{mode}" ''').strip() + self.cr self.memory = dedent(''' memsize = "${memory_size}" ''').strip() + self.cr self.number_of_cpus = dedent(''' numvcpus = "${number_of_cpus}" ''').strip() + self.cr self.ide_iso = dedent(''' ide${iso_id}:0.present = "true" ide${iso_id}:0.deviceType = "cdrom-raw" ide${iso_id}:0.autodetect = "true" ide${iso_id}:0.startConnected = "true" ''').strip() + self.cr self.scsi_iso = dedent(''' scsi${iso_id}:0.present = "true" scsi${iso_id}:0.deviceType = "cdrom-raw" scsi${iso_id}:0.autodetect = "true" scsi${iso_id}:0.startConnected = "true" ''').strip() + self.cr self.usb = dedent(''' usb.present = "true" ''').strip() + self.cr self.defaults = dedent(''' priority.grabbed = "normal" priority.ungrabbed = "normal" powerType.powerOff = "soft" powerType.powerOn = "soft" powerType.suspend = "soft" powerType.reset = "soft" ''').strip() + self.cr def get_template( self, memory_setup=False, cpu_setup=False, network_setup=False, iso_setup=False, disk_controller='ide', iso_controller='ide' ): """ VMware machine configuration template :param bool memory_setup: with main memory setup true|false :param bool cpu_setup: with number of CPU's setup true|false :param bool network_setup: with network emulation true|false :param bool iso_setup: with CD/DVD drive emulation true|false :param string disk_controller: add disk controller setup to template :param string iso_controller: add CD/DVD controller setup to template :param string network_mac: add static MAC address setup to template :param string network_driver: add network driver setup to template :param string network_connection_type: add connection type to template :rtype: Template """ template_data = self.header template_data += self.defaults if memory_setup: template_data += self.memory if cpu_setup: template_data += self.number_of_cpus if disk_controller == 'ide': template_data += self.ide_disk else: template_data += self.scsi_disk if network_setup: for nic_id, nic_setup in list(network_setup.items()): template_data += self.network.format(nic_id=nic_id) if nic_setup['mac'] == 'generated': template_data += self.network_mac_generated.format( nic_id=nic_id ) else: template_data += self.network_mac_static.format( nic_id=nic_id, mac_address=nic_setup['mac'] ) if nic_setup['driver']: template_data += self.network_driver.format( nic_id=nic_id, network_driver=nic_setup['driver'] ) if nic_setup['connection_type']: template_data += self.network_connection_type.format( nic_id=nic_id, mode=nic_setup['connection_type'] ) if iso_setup: if iso_controller == 'ide': template_data += self.ide_iso else: template_data += self.scsi_iso template_data += self.usb return Template(template_data) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vagrant_base.py0000644000000000000000000002025015015277265017201 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 json import os.path from kiwi.utils.temporary import Temporary from typing import ( Dict, Optional, List ) from kiwi import xml_parse from kiwi.system.result import Result from kiwi.storage.subformat.base import DiskFormatBase from kiwi.storage.subformat.template.vagrant_config import VagrantConfigTemplate from kiwi.command import Command from kiwi.exceptions import ( KiwiFormatSetupError ) class DiskFormatVagrantBase(DiskFormatBase): """ Base class for creating vagrant boxes. The documentation of the vagrant box format can be found here: https://www.vagrantup.com/docs/boxes/format.html In a nutshell, a vagrant box is a tar, tar.gz or zip archive of the following: 1. ``metadata.json``: A json file that contains the name of the provider and arbitrary additional data (that vagrant doesn't care about). 2. ``Vagrantfile``: A Vagrantfile which defines the boxes' MAC address. It can be also used to define other settings of the box, e.g. the method via which the ``/vagrant/`` directory is shared. This file is either automatically generated by KIWI or we use a file that has been provided by the user (depends on the setting in `vagrantconfig.embebbed_vagrantfile`) 3. The actual virtual disk image: this is provider specific and vagrant simply forwards it to your virtual machine provider. Required methods/variables that child classes must implement: * :meth:`vagrant_post_init` post initializing method that has to specify the vagrant provider name in :attr:`provider` and the box name in :attr:`image_format`. Note: new providers also needs to be specified in the schema and the box name needs to be registered to :func:`kiwi.defaults.Defaults.get_disk_format_types` * :meth:`create_box_img` Optional methods: * :meth:`get_additional_metadata` * :meth:`get_additional_vagrant_config_settings` """ def post_init(self, custom_args: Dict['str', xml_parse.vagrantconfig]): """ vagrant disk format post initialization method store vagrantconfig information provided via custom_args :param dict custom_args: Contains instance of xml_parse::vagrantconfig .. code:: python {'vagrantconfig': object} """ if not custom_args or 'vagrantconfig' not in custom_args: raise KiwiFormatSetupError( 'object init requires custom_args hash with a vagrantconfig' ) if not custom_args['vagrantconfig']: raise KiwiFormatSetupError( 'no vagrantconfig provided' ) self.vagrantconfig = custom_args['vagrantconfig'] self.vagrant_post_init() def vagrant_post_init(self) -> None: """ Vagrant provider specific post initialization method Setup vagrant provider and box name. This information must be set by the specialized provider class implementation to make the this base class methods effective """ self.provider: Optional[str] = None def create_box_img(self, temp_image_dir: str) -> List[str]: """ Provider specific image creation step: this function creates the actual box image. It must be implemented by a child class. """ raise NotImplementedError def create_image_format(self) -> None: """ Create a vagrant box for any provider. This includes: * creation of box metadata.json * creation of box Vagrantfile (either from scratch or by using the user provided Vagrantfile) * creation of result format tarball from the files created above """ if not self.image_format or not self.provider: raise NotImplementedError( 'vagrant_post_init: Missing provider and/or box name setup' ) temp_image_dir = Temporary(prefix='kiwi_vagrant_box.').new_dir() box_img_files = self.create_box_img(temp_image_dir.name) metadata_json = os.path.join(temp_image_dir.name, 'metadata.json') with open(metadata_json, 'w') as meta: meta.write(self._create_box_metadata()) vagrantfile = os.path.join(temp_image_dir.name, 'Vagrantfile') with open(vagrantfile, 'w') as vagrant: # autogenerate a Vagrantfile: if not self.vagrantconfig.get_embedded_vagrantfile(): embedded_vagrantfile = self._create_box_vagrantconfig() # user provided a Vagrantfile else: with open( os.path.join( self.xml_state.xml_data.description_dir, self.vagrantconfig.get_embedded_vagrantfile() ), 'r') as users_vagrantfile: embedded_vagrantfile = users_vagrantfile.read(-1) vagrant.write(embedded_vagrantfile) Command.run( [ 'tar', '-C', temp_image_dir.name, '-czf', self.get_target_file_path_for_format( self.image_format ), os.path.basename(metadata_json), os.path.basename(vagrantfile) ] + [ os.path.basename(box_img_file) for box_img_file in box_img_files ] ) def store_to_result(self, result: Result) -> None: """ Store result file of the vagrant format conversion into the provided result instance. In this case compression is unwanted because the box is already created as a compressed tarball :param object result: Instance of Result """ if not self.image_format: raise NotImplementedError( 'vagrant_post_init: Missing box name setup' ) result.add( key='disk_format_image', filename=self.get_target_file_path_for_format( self.image_format ), use_for_bundle=True, compress=False, shasum=True ) def get_additional_metadata(self) -> Dict: """ Provide :meth:`create_image_format` with additional metadata that will be included in ``metadata.json``. The default implementation returns an empty dictionary. :return: A dictionary that is serializable to JSON :rtype: dict """ return {} def get_additional_vagrant_config_settings(self) -> str: """ Supply additional configuration settings for vagrant to be included in the resulting box. This function can be used by child classes to customize the behavior for different providers: the supplied configuration settings get forwarded to :func:`VagrantConfigTemplate.get_template` as the parameter ``custom_settings`` and included in the ``Vagrantfile``. The default implementation returns nothing. :return: additional vagrant settings :rtype: str """ return '' def _create_box_metadata(self): metadata = self.get_additional_metadata() or {} metadata['provider'] = self.provider return json.dumps( metadata, sort_keys=True, indent=2, separators=(',', ': ') ) def _create_box_vagrantconfig(self): template = VagrantConfigTemplate() return template.get_template( custom_settings=self.get_additional_vagrant_config_settings() ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vagrant_libvirt.py0000644000000000000000000000550515015277265017750 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 textwrap import dedent from typing import ( List, Dict ) # project from kiwi.storage.subformat.vagrant_base import DiskFormatVagrantBase from kiwi.storage.subformat.qcow2 import DiskFormatQcow2 from kiwi.command import Command class DiskFormatVagrantLibVirt(DiskFormatVagrantBase): """ **Create a vagrant box for the libvirt provider** """ def vagrant_post_init(self) -> None: self.image_format = 'vagrant.libvirt.box' self.provider = 'libvirt' def create_box_img(self, temp_image_dir: str) -> List[str]: """ Creates the qcow2 disk image box for libvirt vagrant provider :param str temp_image_dir: Path to the temporary directory used to build the box image :return: A list of files relevant for the libvirt box to be included in the vagrant box :rtype: list """ qcow = DiskFormatQcow2( self.xml_state, self.root_dir, self.target_dir ) qcow.create_image_format() box_img = os.sep.join([temp_image_dir, 'box.img']) Command.run( [ 'mv', self.get_target_file_path_for_format(qcow.image_format), box_img ] ) return [box_img] def get_additional_metadata(self) -> Dict: """ Provide box metadata needed to create the box in vagrant :return: Returns a dictionary containing the virtual image format and the size of the image. :rtype: dict """ return { 'format': 'qcow2', 'virtual_size': int(self.vagrantconfig.get_virtualsize() or 42) } def get_additional_vagrant_config_settings(self) -> str: """ Returns settings for the libvirt provider telling vagrant to use kvm. :return: ruby code to be evaluated as string :rtype: str """ return dedent(''' config.vm.synced_folder ".", "/vagrant", type: "rsync" config.vm.provider :libvirt do |libvirt| libvirt.driver = "kvm" end ''').strip() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vagrant_virtualbox.py0000644000000000000000000000755715015277265020505 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 random from textwrap import dedent from typing import List # project from kiwi.storage.subformat.template.virtualbox_ovf import ( VirtualboxOvfTemplate ) from kiwi.storage.subformat.vagrant_base import DiskFormatVagrantBase from kiwi.storage.subformat.vmdk import DiskFormatVmdk from kiwi.command import Command class DiskFormatVagrantVirtualBox(DiskFormatVagrantBase): """ **Create a vagrant box for the virtualbox provider** """ def vagrant_post_init(self) -> None: self.provider = 'virtualbox' self.image_format = 'vagrant.virtualbox.box' def get_additional_vagrant_config_settings(self) -> str: """ Configure the default shared folder to use rsync when guest additions are not present inside the box. :return: ruby code to be evaluated as string :rtype: str """ extra_settings = dedent(''' config.vm.base_mac = "{mac_address}" ''').strip().format(mac_address=self._random_mac()) if not self.xml_state.get_vagrant_config_virtualbox_guest_additions(): extra_settings += os.linesep + dedent(''' config.vm.synced_folder ".", "/vagrant", type: "rsync" ''').strip() return extra_settings def create_box_img(self, temp_image_dir: str) -> List[str]: """ Create the vmdk image for the Virtualbox vagrant provider. This function creates the vmdk disk image and the ovf file. The latter is created via the class :class:`VirtualboxOvfTemplate`. :param str temp_image_dir: Path to the temporary directory used to build the box image :return: A list of files relevant for the virtualbox box to be included in the vagrant box :rtype: list """ vmdk = DiskFormatVmdk(self.xml_state, self.root_dir, self.target_dir) vmdk.create_image_format() box_img = os.sep.join([temp_image_dir, 'box.vmdk']) Command.run( [ 'mv', self.get_target_file_path_for_format(vmdk.image_format), box_img ] ) box_ovf = os.sep.join([temp_image_dir, 'box.ovf']) ovf_template = VirtualboxOvfTemplate() disk_image_capacity = self.vagrantconfig.get_virtualsize() or 42 xml_description_specification = self.xml_state \ .get_description_section().specification with open(box_ovf, "w") as ovf_file: ovf_file.write( ovf_template.get_template().substitute( { 'root_uuid': self.xml_state.get_root_filesystem_uuid(), 'vm_name': self.xml_state.xml_data.name, 'disk_image_capacity': disk_image_capacity, 'vm_description': xml_description_specification } ) ) return [box_img, box_ovf] @staticmethod def _random_mac(): return '%02x%02x%02x%02x%02x%02x'.upper() % ( 0x00, 0x16, 0x3e, random.randrange(0, 0x7e), random.randrange(0, 0xff), random.randrange(0, 0xff) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vdi.py0000644000000000000000000000313515015277265015332 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 # project from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command class DiskFormatVdi(DiskFormatBase): """ **Create vdi disk format** """ def post_init(self, custom_args: Dict) -> None: """ vdi disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ self.image_format = 'vdi' self.options = self.get_qemu_option_list(custom_args) def create_image_format(self) -> None: """ Create vdi disk format """ Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', self.image_format ] + self.options + [ self.get_target_file_path_for_format(self.image_format) ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vhd.py0000644000000000000000000000312315015277265015326 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 # project from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command class DiskFormatVhd(DiskFormatBase): """ **Create vhd disk format** """ def post_init(self, custom_args: Dict) -> None: """ vhd disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ self.image_format = 'vhd' self.options = self.get_qemu_option_list(custom_args) def create_image_format(self) -> None: """ Create vhd disk format """ Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', 'vpc' ] + self.options + [ self.get_target_file_path_for_format(self.image_format) ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vhdfixed.py0000644000000000000000000001331715015277265016354 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 import struct from binascii import unhexlify import re # project from kiwi.system.result import Result from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command from kiwi.exceptions import KiwiVhdTagError class DiskFormatVhdFixed(DiskFormatBase): """ **Create vhd image format in fixed subformat** """ def post_init(self, custom_args: Dict) -> None: """ vhd disk format post initialization method Store qemu options as list from custom args dict Extract disk tag from custom args :param dict custom_args: custom vhdfixed and qemu argument dictionary .. code:: python {'--tag': 'billing_code', '--qemu-opt': 'value'} """ self.image_format = 'vhdfixed' self.tag = None if '--tag' in custom_args: self.tag = custom_args['--tag'] del custom_args['--tag'] self.options = self.get_qemu_option_list(custom_args) self.options.append('-o') self.options.append('subformat=fixed') def create_image_format(self) -> None: """ Create vhd fixed disk format """ Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', 'vpc' ] + self.options + [ self.get_target_file_path_for_format(self.image_format) ] ) if self.tag: self._write_vhd_tag(self.tag) def store_to_result(self, result: Result) -> None: """ Store result file of the vhdfixed format conversion into the provided result instance. In this case compressing the result is preferred as vhdfixed is not a compressed or dynamic format. :param object result: Instance of Result """ compression = self.runtime_config.get_bundle_compression(default=True) if self.xml_state.get_luks_credentials() is not None: compression = False result.add( key='disk_format_image', filename=self.get_target_file_path_for_format( self.image_format ), use_for_bundle=True, compress=compression, shasum=True ) def _pack_net_guid_tag(self, tag): """ Pack tag format into 16 byte binary representation. String format of the tag is: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX :param string tag: tagname """ tag_format = re.match( ''.join( [ '^', '([0-9a-f]{8})', '-', '([0-9a-f]{4})', '-', '([0-9a-f]{4})', '-', '([0-9a-f]{4})', '-', '([0-9a-f]{12})', '$' ] ), tag ) if not tag_format: raise KiwiVhdTagError( 'disk tag %s does not match format' % tag ) # pack first nibble into 4 byte unsigned long type binary_tag_part_1 = struct.pack( 'I', list(struct.unpack('>L', unhexlify(tag_format.group(1))))[0] ) # pack second nibble into 2 byte unsigned short type binary_tag_part_2 = struct.pack( 'H', list(struct.unpack('>H', unhexlify(tag_format.group(2))))[0] ) # pack third nibble into 2 byte unsigned short type binary_tag_part_3 = struct.pack( 'H', list(struct.unpack('>H', unhexlify(tag_format.group(3))))[0] ) # pack fourth nibble into hex binary_tag_part_4 = unhexlify(tag_format.group(4)) # pack fifth nibble into hex binary_tag_part_5 = unhexlify(tag_format.group(5)) return binary_tag_part_1 + binary_tag_part_2 + \ binary_tag_part_3 + binary_tag_part_4 + \ binary_tag_part_5 def _write_vhd_tag(self, tag): """ Azure service uses a tag injected into the disk image to identify the OS. The tag is 512B long, starting with a GUID, and is placed at a 64K offset from the start of the disk image. +------------------------------+ | jump | GUID(16B)000... | +------------------------------| | 64K offset | TAG (512B) | +------------+-----------------+ """ binary_tag = self._pack_net_guid_tag(tag) vhd_fixed_image = self.get_target_file_path_for_format('vhdfixed') # seek to 64k offset and zero out 512 byte with open(vhd_fixed_image, 'r+b') as vhd_fixed: with open('/dev/zero', 'rb') as null: vhd_fixed.seek(65536, 0) vhd_fixed.write(null.read(512)) vhd_fixed.seek(0, 2) # seek to 64k offset and write tag with open(vhd_fixed_image, 'r+b') as vhd_fixed: vhd_fixed.seek(65536, 0) vhd_fixed.write(binary_tag) vhd_fixed.seek(0, 2) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3581455 kiwi-10.2.24/kiwi/storage/subformat/vhdx.py0000644000000000000000000000331215015277265015516 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 # project from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command class DiskFormatVhdx(DiskFormatBase): """ **Create vhdx image format in dynamic subformat** """ def post_init(self, custom_args: Dict) -> None: """ vhdx disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ self.image_format = 'vhdx' self.options = self.get_qemu_option_list(custom_args) self.options.append('-o') self.options.append('subformat=dynamic') def create_image_format(self) -> None: """ Create vhdx dynamic disk format """ Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', 'vhdx' ] + self.options + [ self.get_target_file_path_for_format(self.image_format) ] ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/storage/subformat/vmdk.py0000644000000000000000000001422615015277265015514 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 import os # project from kiwi.system.result import Result from kiwi.storage.subformat.base import DiskFormatBase from kiwi.command import Command from kiwi.storage.subformat.template.vmware_settings import ( VmwareSettingsTemplate ) from kiwi.exceptions import ( KiwiTemplateError ) class DiskFormatVmdk(DiskFormatBase): """ Create vmdk disk format """ def post_init(self, custom_args: Dict) -> None: """ vmdk disk format post initialization method Store qemu options as list from custom args dict :param dict custom_args: custom qemu arguments dictionary """ self.image_format = 'vmdk' self.options = self.get_qemu_option_list(custom_args) def create_image_format(self) -> None: """ Create vmdk disk format and machine settings file """ Command.run( [ 'qemu-img', 'convert', '-f', 'raw', self.diskname, '-O', 'vmdk' ] + self.options + [ self.get_target_file_path_for_format('vmdk') ] ) self._create_vmware_settings_file() def store_to_result(self, result: Result) -> None: """ Store result files of the vmdk format conversion into the provided result instance. This includes the vmdk image file and the VMware settings file :param object result: Instance of Result """ compression = self.runtime_config.get_bundle_compression(default=False) if self.xml_state.get_luks_credentials() is not None: compression = False result.add( key='disk_format_image', filename=self.get_target_file_path_for_format('vmdk'), use_for_bundle=True, compress=compression, shasum=True ) result.add( key='disk_format_machine_settings', filename=self.get_target_file_path_for_format( 'vmx' ), use_for_bundle=True, compress=False, shasum=False ) def _create_vmware_settings_file(self): """ In order to run a vmdk image in VMware products a settings file is needed or the possibility to convert machine settings into an ovf via VMware's proprietary ovftool """ displayname = self.xml_state.xml_data.get_displayname() template_record = { 'display_name': displayname or self.xml_state.xml_data.get_name(), 'vmdk_file': os.path.basename(self.get_target_file_path_for_format('vmdk')), 'virtual_hardware_version': '9', 'guest_os': 'suse-64', 'disk_id': '0' } # Basic setup machine_setup = self.xml_state.get_build_type_machine_section() memory_setup = None cpu_setup = None if machine_setup: memory_setup = machine_setup.get_memory() hardware_version = machine_setup.get_HWversion() guest_os = machine_setup.get_guestOS() cpu_setup = machine_setup.get_ncpus() if hardware_version: template_record['virtual_hardware_version'] = hardware_version if guest_os: template_record['guest_os'] = guest_os if memory_setup: template_record['memory_size'] = memory_setup if cpu_setup: template_record['number_of_cpus'] = cpu_setup # CD/DVD setup iso_setup = self.xml_state.get_build_type_vmdvd_section() iso_controller = 'ide' if iso_setup: iso_controller = iso_setup.get_controller() or iso_controller template_record['iso_id'] = iso_setup.get_id() # Network setup network_entries = self.xml_state.get_build_type_vmnic_entries() network_setup = {} for network_entry in network_entries: network_setup[network_entry.get_interface() or '0'] = { 'driver': network_entry.get_driver(), 'connection_type': network_entry.get_mode(), 'mac': network_entry.get_mac() or 'generated', } # Disk setup disk_setup = self.xml_state.get_build_type_vmdisk_section() disk_controller = 'ide' if disk_setup: disk_controller = disk_setup.get_controller() or disk_controller disk_id = disk_setup.get_id() if not disk_controller == 'ide': template_record['scsi_controller_name'] = disk_controller if disk_id: template_record['disk_id'] = disk_id # Addition custom entries custom_entries = self.xml_state.get_build_type_vmconfig_entries() # Build settings template and write settings file settings_template = VmwareSettingsTemplate().get_template( memory_setup, cpu_setup, network_setup, iso_setup, disk_controller, iso_controller ) try: settings_file = self.get_target_file_path_for_format('vmx') with open(settings_file, 'w') as config: config.write(settings_template.substitute(template_record)) for custom_entry in custom_entries: config.write(custom_entry + os.linesep) except Exception as e: raise KiwiTemplateError( '%s: %s' % (type(e).__name__, format(e)) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/__init__.py0000644000000000000000000000000015015277265014151 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/identifier.py0000644000000000000000000000435315015277265014553 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 random import struct # project from kiwi.storage.device_provider import DeviceProvider class SystemIdentifier: """ **Create a random ID to identify the system** The information is used to create the mbrid file as an example :param str image_id: hex identifier string """ def __init__(self): self.image_id = None def get_id(self) -> str: """ Current hex identifier :return: hex id :rtype: str """ return self.image_id def calculate_id(self) -> None: """ Calculate random hex id Using 4 tuples of rand in range from 1..0xfe """ self.image_id = '0x%02x%02x%02x%02x' % ( self._rand(), self._rand(), self._rand(), self._rand() ) def write(self, filename: str) -> None: """ Write current hex identifier to file :param str filename: file path name """ with open(filename, 'w') as identifier: identifier.write('%s\n' % self.image_id) def write_to_disk(self, device_provider: DeviceProvider) -> None: """ Write current hex identifier to MBR at offset 0x1b8 on disk :param object device_provider: Instance based on DeviceProvider """ if self.image_id: packed_id = struct.pack(' # import re import os from typing import ( NamedTuple, Optional, List ) # project from kiwi.command import Command from kiwi.exceptions import KiwiKernelLookupError kernel_type = NamedTuple( 'kernel_type', [ ('name', str), ('filename', str), ('version', str) ] ) xen_hypervisor_type = NamedTuple( 'xen_hypervisor_type', [ ('filename', str), ('name', str) ] ) class Kernel: """ **Implementes kernel lookup and extraction from given root tree** :param str root_dir: root directory path name :param list kernel_names: list of kernel names to search for functions.sh::suseStripKernel() provides a normalized file so that we do not have to search for many different names in this code """ def __init__(self, root_dir: str): self.root_dir = root_dir self.kernel_names = self._setup_kernel_names_for_lookup() def get_kernel( self, raise_on_not_found: bool = False ) -> Optional[kernel_type]: """ Lookup kernel files and provide filename and version :param bool raise_on_not_found: sets the method to raise an exception if the kernel is not found :raises KiwiKernelLookupError: if raise_on_not_found flag is active and kernel is not found :return: tuple with filename, kernelname and version :rtype: tuple|None """ kernel_patterns = [ '.*/boot/.*?-(.*)', '.*/lib/modules/(.*)/.*' ] for kernel_name in self.kernel_names: kernel_file = os.sep.join( [self.root_dir, kernel_name] ) if os.path.exists(kernel_file): for kernel_pattern in kernel_patterns: version_match = re.match(kernel_pattern, kernel_file) if version_match: version = version_match.group(1) return kernel_type( name=os.path.basename( os.path.realpath(kernel_file) ), filename=kernel_file, version=version ) if raise_on_not_found: raise KiwiKernelLookupError( 'No kernel found in {0}, searched for {1}'.format( self.root_dir, self.kernel_names ) ) return None def get_xen_hypervisor(self) -> Optional[xen_hypervisor_type]: """ Lookup xen hypervisor and provide filename and hypervisor name :return: tuple with filename and hypervisor name :rtype: tuple|None """ xen_hypervisor = self.root_dir + '/boot/xen.gz' if os.path.exists(xen_hypervisor): return xen_hypervisor_type( filename=xen_hypervisor, name='xen.gz' ) return None def copy_kernel(self, target_dir: str, file_name: str = None) -> None: """ Copy kernel to specified target If no file_name is given the target filename is set as kernel-.kernel :param str target_dir: target path name :param str filename: base filename in target """ kernel = self.get_kernel() if kernel: if not file_name: file_name = 'kernel-' + kernel.version + '.kernel' target_file = ''.join( [target_dir, '/', file_name] ) Command.run(['cp', kernel.filename, target_file]) def copy_xen_hypervisor( self, target_dir: str, file_name: str = None ) -> None: """ Copy xen hypervisor to specified target If no file_name is given the target filename is set as hypervisor- :param str target_dir: target path name :param str filename: base filename in target """ xen = self.get_xen_hypervisor() if xen: if not file_name: file_name = 'hypervisor-' + xen.name target_file = ''.join( [target_dir, '/', file_name] ) Command.run(['cp', xen.filename, target_file]) def _setup_kernel_names_for_lookup(self) -> List[str]: """ The kernel image name is different per arch and distribution This method returns a list of possible kernel image names in order to search and find one of them :return: list of kernel image names :rtype: list """ kernel_names = [] kernel_dirs = [] kernel_module_dirs = [ os.sep.join([self.root_dir, 'lib/modules']), os.sep.join([self.root_dir, 'usr/lib/modules']) ] for kernel_module_dir in kernel_module_dirs: if os.path.isdir(kernel_module_dir): kernel_entries = sorted(os.listdir(kernel_module_dir)) if kernel_entries: kernel_dirs += kernel_entries if kernel_dirs: # append lookup for the real kernel image names # depending on the arch and os they are different # in their prefix kernel_prefixes = [ 'uImage', 'Image', 'zImage', 'vmlinuz', 'image', 'vmlinux', 'kernel' ] for kernel_prefix in kernel_prefixes: for kernel_dir in kernel_dirs: # add kernel names to be found in boot kernel_names.append( f'boot/{kernel_prefix}-{kernel_dir}' ) # add kernel names to be found in modules kernel_names.append( f'lib/modules/{kernel_dir}/{kernel_prefix}' ) kernel_names.append( f'usr/lib/modules/{kernel_dir}/{kernel_prefix}' ) return kernel_names ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/mount.py0000644000000000000000000001457715015277265013604 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 os import logging from typing import ( List, Dict, Optional, Union ) # project from kiwi.defaults import Defaults from kiwi.mount_manager import MountManager from kiwi.storage.disk import ptable_entry_type from kiwi.volume_manager.base import VolumeManagerBase from kiwi.filesystem.base import FileSystemBase log = logging.getLogger('kiwi') class ImageSystem: """ **Access the target image from the block layer** """ def __init__( self, device_map: Dict, root_dir: str, volume_manager: Optional[Union[FileSystemBase, VolumeManagerBase]] = None, partitions: Dict[str, ptable_entry_type] = {} ) -> None: """ Construct a new ImageSystem object :param dict device_map: Block device map :param str root_dir: Path to unpacked image root tree :param VolumeManagerBase volume_manager: Optional VolumeManager instance """ self.arch = Defaults.get_platform_name() self.device_map = device_map self.root_dir = root_dir self.volume_manager = volume_manager self.partitions = partitions self.mount_list: List[MountManager] = [] self.root_mount_mountpoint = '' def __enter__(self): return self def mountpoint(self) -> str: """ Return image root mountpoint :return: mountpoint path or empty string :rtype: str """ return self.root_mount_mountpoint def mount(self) -> None: """ Mount image system from current block layers """ # mount root boot and efi devices as they are present (root_device, boot_device, efi_device) = self._setup_device_names() if self.volume_manager: self.volume_manager.mount_volumes() self.root_mount_mountpoint = \ self.volume_manager.get_mountpoint() or '' else: root_mount = MountManager(device=root_device) self.mount_list.append(root_mount) root_mount.mount() self.root_mount_mountpoint = root_mount.mountpoint if 's390' in self.arch: boot_mount = MountManager( device=boot_device, mountpoint=os.path.join( self.root_mount_mountpoint, 'boot', 'zipl' ) ) else: boot_mount = MountManager( device=boot_device, mountpoint=os.path.join( self.root_mount_mountpoint, 'boot' ) ) if efi_device: efi_mount = MountManager( device=efi_device, mountpoint=os.path.join( self.root_mount_mountpoint, 'boot', 'efi' ) ) if root_device != boot_device: self.mount_list.append(boot_mount) boot_mount.mount() if efi_device: self.mount_list.append(efi_mount) efi_mount.mount() if self.partitions: 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) ) ) self.mount_list.append(partition_mount) partition_mount.mount() # bind mount /image from unpacked root to get access to e.g scripts image_mount = MountManager( device=os.path.join(self.root_dir, 'image'), mountpoint=os.path.join( self.root_mount_mountpoint, 'image' ) ) self.mount_list.append(image_mount) image_mount.bind_mount() # mount tmp as tmpfs tmp_mount = MountManager( device='tmpfs', mountpoint=os.path.join( self.root_mount_mountpoint, 'tmp' ) ) self.mount_list.append(tmp_mount) tmp_mount.tmpfs_mount() # mount var/tmp as tmpfs var_tmp_mount = MountManager( device='tmpfs', mountpoint=os.path.join( self.root_mount_mountpoint, 'var', 'tmp' ) ) self.mount_list.append(var_tmp_mount) var_tmp_mount.tmpfs_mount() # mount kernel interfaces as bind for location in ('proc', 'sys', 'dev'): shared_mount = MountManager( device=os.path.join('/', location), mountpoint=os.path.join( self.root_mount_mountpoint, location ) ) self.mount_list.append(shared_mount) shared_mount.bind_mount() def umount(self) -> None: """ Umount all elements of mount_list in reverse order """ for mount in reversed(self.mount_list): if mount.is_mounted(): mount.umount() if self.volume_manager: self.volume_manager.umount_volumes() def _setup_device_names(self) -> tuple: root_device = self.device_map['root'].get_device() boot_device = root_device efi_device = '' if 'readonly' in self.device_map: root_device = self.device_map['readonly'].get_device() boot_device = root_device if 'boot' in self.device_map: boot_device = self.device_map['boot'].get_device() if 'efi' in self.device_map: efi_device = self.device_map['efi'].get_device() return (root_device, boot_device, efi_device) def __exit__(self, exc_type, exc_value, traceback): self.umount() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/prepare.py0000644000000000000000000006121315015277265014065 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, Any, Optional ) from textwrap import dedent # project from kiwi.command import Command from kiwi.xml_parse import repository from kiwi.xml_state import XMLState from kiwi.system.root_init import RootInit from kiwi.system.root_import import RootImport from kiwi.system.root_bind import RootBind from kiwi.repository import Repository from kiwi.package_manager import PackageManager from kiwi.package_manager.base import PackageManagerBase from kiwi.command_process import CommandProcess from kiwi.system.uri import Uri from kiwi.archive.tar import ArchiveTar from kiwi.exceptions import ( KiwiBootStrapPhaseFailed, KiwiSystemUpdateFailed, KiwiSystemInstallPackagesFailed, KiwiSystemDeletePackagesFailed, KiwiInstallPhaseFailed, KiwiPackagesDeletePhaseFailed ) log: Any = logging.getLogger('kiwi') class SystemPrepare: """ Implements preparation and installation of a new root system :param object xml_state: instance of :class:`XMLState` :param str root_dir: Path to new root directory :param bool allow_existing: Allow using an existing root_dir """ def __init__( self, xml_state: XMLState, root_dir: str, allow_existing: bool = False ): """ Setup and host bind new root system at given root_dir directory """ self.root_import = None log.info('Setup root directory: %s', root_dir) if not log.getLogLevel() == logging.DEBUG and not log.get_logfile(): self.issue_message = dedent(''' {headline}: {reason} Further details to clarify the error might have been reported earlier in the package manager log information and did not get exposed to the caller. Thus if the above message is not clear on the error please call kiwi with the --debug or --logfile option. ''') else: self.issue_message = '{headline}: {reason}' root = RootInit( root_dir, allow_existing ) root.create() image_uris = xml_state.get_derived_from_image_uri() delta_root = xml_state.build_type.get_delta_root() if image_uris: self.root_import = RootImport.new( root_dir, image_uris, xml_state.build_type.get_image() ) if delta_root: self.root_import.overlay_data() else: self.root_import.sync_data() root_bind = RootBind( root ) root_bind.setup_intermediate_config() root_bind.mount_kernel_file_systems(delta_root) root_bind.mount_shared_directory() self.delta_root = delta_root self.root_dir = root_dir self.xml_state = xml_state self.profiles = xml_state.profiles self.root_bind = root_bind #: A list of Uri references self.uri_list: List[Uri] = [] def __enter__(self): return self def setup_repositories( self, clear_cache: bool = False, signing_keys: List[str] = None, target_arch: Optional[str] = None ) -> PackageManagerBase: """ Set up repositories for software installation and return a package manager for performing software installation tasks :param bool clear_cache: Flag the clear cache before configure anything :param list signing_keys: Keys imported to the package manager :param str target_arch: Target architecture name :return: instance of :class:`PackageManager` subclass :rtype: PackageManager """ repository_options = [] repository_sections = \ self.xml_state.get_repository_sections_used_for_build() package_manager = self.xml_state.get_package_manager() release_version = self.xml_state.get_release_version() rpm_locale_list = self.xml_state.get_rpm_locale() if self.xml_state.get_rpm_check_signatures(): repository_options.append('check_signatures') if self.xml_state.get_rpm_excludedocs(): repository_options.append('exclude_docs') if rpm_locale_list: repository_options.append( '_install_langs%{0}'.format(':'.join(rpm_locale_list)) ) if target_arch: repository_options.append( f'_target_arch%{target_arch}' ) with Repository.new( self.root_bind, package_manager, repository_options ) as repo: repo.setup_package_database_configuration() if signing_keys: repo.import_trusted_keys(signing_keys) for xml_repo in repository_sections: repo_type = xml_repo.get_type() repo_source = xml_repo.get_source().get_path() repo_architectures = xml_repo.get_architectures() repo_user = xml_repo.get_username() repo_secret = xml_repo.get_password() repo_alias = xml_repo.get_alias() repo_priority = xml_repo.get_priority() repo_dist = xml_repo.get_distribution() repo_components = xml_repo.get_components() repo_repository_gpgcheck = xml_repo.get_repository_gpgcheck() repo_package_gpgcheck = xml_repo.get_package_gpgcheck() repo_customization_script = self._get_repo_customization_script( xml_repo ) repo_sourcetype = xml_repo.get_sourcetype() log.info( 'Setting up repository %s', Uri.print_sensitive(repo_source) ) log.info('--> Type: {0}'.format(repo_type)) if repo_sourcetype: log.info('--> SourceType: {0}'.format(repo_sourcetype)) if repo_priority: log.info('--> Priority: {0}'.format(repo_priority)) uri = Uri(repo_source, repo_type) repo_source_translated = uri.translate() log.info( '--> Translated: {0}'.format( Uri.print_sensitive(repo_source_translated) ) ) if not repo_alias: repo_alias = uri.alias() log.info('--> Alias: {0}'.format(repo_alias)) if not uri.is_remote() and not os.path.exists( repo_source_translated ): log.warning( 'repository %s does not exist and will be skipped', repo_source ) continue if not uri.is_remote(): self.root_bind.mount_shared_directory( repo_source_translated ) repo.add_repo( repo_alias, repo_source_translated, repo_type, repo_priority, repo_dist, repo_components, repo_user, repo_secret, uri.credentials_file_name(), repo_repository_gpgcheck, repo_package_gpgcheck, repo_sourcetype, repo_customization_script, repo_architectures ) if clear_cache: repo.delete_repo_cache(repo_alias) self.uri_list.append(uri) repo.cleanup_unused_repos() return PackageManager.new( repository=repo, package_manager_name=package_manager, release_version=release_version ) def install_bootstrap( self, manager: PackageManagerBase, plus_packages: List = None ) -> None: """ Install system software using the package manager from the host, also known as bootstrapping :param object manager: instance of a :class:`PackageManager` subclass :param list plus_packages: list of additional packages :raises KiwiBootStrapPhaseFailed: if the bootstrapping process fails either installing packages or including bootstrap archives """ if not self.xml_state.get_bootstrap_packages_sections() \ and not plus_packages: log.warning('No sections marked as "bootstrap" found') log.info('Processing of bootstrap stage skipped') return log.info('Installing bootstrap packages') bootstrap_packages = self.xml_state.get_bootstrap_packages( plus_packages ) collection_type = self.xml_state.get_bootstrap_collection_type() log.info('--> collection type: %s', collection_type) bootstrap_collections = self.xml_state.get_bootstrap_collections() bootstrap_products = self.xml_state.get_bootstrap_products() bootstrap_archives = self.xml_state.get_bootstrap_archives() bootstrap_archives_target_dirs = self.xml_state.get_bootstrap_archives_target_dirs() bootstrap_packages_ignored = self.xml_state.get_bootstrap_ignore_packages() package_manager = self.xml_state.get_package_manager() # process package installations if collection_type == 'onlyRequired': manager.process_only_required() else: manager.process_plus_recommended() all_install_items = self._setup_requests( manager, bootstrap_packages, bootstrap_collections, bootstrap_products, bootstrap_packages_ignored ) manager.setup_repository_modules( self.xml_state.get_collection_modules() ) process = CommandProcess( command=manager.process_install_requests_bootstrap( self.root_bind, self.xml_state.get_bootstrap_package_name() ), log_topic='bootstrap' ) try: process.poll_show_progress( items_to_complete=all_install_items, match_method=process.create_match_method( manager.match_package_installed ), with_stderr=True if package_manager == 'dnf5' else False ) except Exception as issue: if manager.has_failed(process.returncode()): raise KiwiBootStrapPhaseFailed( self.issue_message.format( headline='Bootstrap package installation failed', reason=f'{issue}: {manager.get_error_details()}' ) ) manager.post_process_install_requests_bootstrap( self.root_bind, self.delta_root ) # process archive installations if bootstrap_archives: try: self._install_archives( bootstrap_archives, bootstrap_archives_target_dirs ) except Exception as issue: raise KiwiBootStrapPhaseFailed( self.issue_message.format( headline='Bootstrap archive installation failed', reason=issue ) ) def install_system(self, manager: PackageManagerBase) -> None: """ Install system software using the package manager inside of the new root directory. This is done via a chroot operation and requires the desired package manager to became installed via the bootstrap phase :param object manager: instance of a :class:`PackageManager` subclass :raises KiwiInstallPhaseFailed: if the install process fails either installing packages or including any archive """ log.info( 'Installing system (chroot) for build type: %s', self.xml_state.get_build_type_name() ) collection_type = self.xml_state.get_system_collection_type() log.info('--> collection type: %s', collection_type) system_packages = self.xml_state.get_system_packages() system_collections = self.xml_state.get_system_collections() system_products = self.xml_state.get_system_products() system_archives = self.xml_state.get_system_archives() system_archives_target_dirs = self.xml_state.get_system_archives_target_dirs() system_packages_ignored = self.xml_state.get_system_ignore_packages() package_manager = self.xml_state.get_package_manager() # process package installations if collection_type == 'onlyRequired': manager.process_only_required() else: manager.process_plus_recommended() all_install_items = self._setup_requests( manager, system_packages, system_collections, system_products, system_packages_ignored ) if all_install_items: process = CommandProcess( command=manager.process_install_requests(), log_topic='system' ) try: process.poll_show_progress( items_to_complete=all_install_items, match_method=process.create_match_method( manager.match_package_installed ), with_stderr=True if package_manager == 'dnf5' else False ) except Exception as issue: if manager.has_failed(process.returncode()): raise KiwiInstallPhaseFailed( self.issue_message.format( headline='System package installation failed', reason=issue ) ) # process archive installations if system_archives: try: self._install_archives( system_archives, system_archives_target_dirs ) except Exception as issue: raise KiwiInstallPhaseFailed( self.issue_message.format( headline='System archive installation failed', reason=issue ) ) def pinch_system( self, manager: Optional[PackageManagerBase] = None, force: bool = False ) -> None: """ Delete packages marked for deletion in the XML description. If force param is set to False uninstalls packages marked with `type="uninstall"` if any; if force is set to True deletes packages marked with `type="delete"` if any. :param object manager: instance of :class:`PackageManager` subclass :param bool force: Forced deletion True|False :raises KiwiPackagesDeletePhaseFailed: if the deletion packages process fails """ if self.delta_root: # In delta mode create a reference tree to allow # to diff on deleted data Command.run( [ 'rsync', '-a', f'{self.root_dir}_cow/', f'{self.root_dir}_cow_before_pinch' ] ) to_become_deleted_packages = \ self.xml_state.get_to_become_deleted_packages(force) if to_become_deleted_packages: log.info( '{0} system packages (chroot)'.format( 'Force deleting' if force else 'Uninstalling' ) ) try: with ExitStack() as stack: if manager is None: package_manager = self.xml_state.get_package_manager() release_version = self.xml_state.get_release_version() repo = Repository.new(self.root_bind, package_manager) stack.push(repo) manager = PackageManager.new( repository=repo, package_manager_name=package_manager, release_version=release_version ) stack.push(manager) self.delete_packages( manager, to_become_deleted_packages, force ) except Exception as issue: raise KiwiPackagesDeletePhaseFailed( self.issue_message.format( headline='System package deletion failed', reason=issue ) ) def install_packages( self, manager: PackageManagerBase, packages: List ) -> None: """ Install one or more packages using the package manager inside of the new root directory :param object manager: instance of a :class:`PackageManager` subclass :param list packages: package list :raises KiwiSystemInstallPackagesFailed: if installation process fails """ log.info('Installing system packages (chroot)') package_manager = self.xml_state.get_package_manager() all_install_items = self._setup_requests( manager, packages ) if all_install_items: process = CommandProcess( command=manager.process_install_requests(), log_topic='system' ) try: process.poll_show_progress( items_to_complete=all_install_items, match_method=process.create_match_method( manager.match_package_installed ), with_stderr=True if package_manager == 'dnf5' else False ) except Exception as issue: raise KiwiSystemInstallPackagesFailed( self.issue_message.format( headline='Package installation failed', reason=issue ) ) def delete_packages( self, manager: PackageManagerBase, packages: List, force: bool = False ) -> None: """ Delete one or more packages using the package manager inside of the new root directory. If the removal is set with `force` flag only listed packages are deleted and any dependency break or leftover is ignored. :param object manager: instance of a :class:`PackageManager` subclass :param list packages: package list :param bool force: force deletion true|false :raises KiwiSystemDeletePackagesFailed: if installation process fails """ package_manager = self.xml_state.get_package_manager() all_delete_items = self._setup_requests( manager, packages ) if all_delete_items: log.info( '{0} system packages (chroot)'.format( 'Force deleting' if force else 'Uninstall' ) ) process = CommandProcess( command=manager.process_delete_requests(force), log_topic='system' ) try: process.poll_show_progress( items_to_complete=all_delete_items, match_method=process.create_match_method( manager.match_package_deleted ), with_stderr=True if package_manager == 'dnf5' else False ) manager.post_process_delete_requests(self.root_bind) except Exception as issue: raise KiwiSystemDeletePackagesFailed( self.issue_message.format( headline='Package deletion failed', reason=issue ) ) def update_system(self, manager: PackageManagerBase) -> None: """ Install package updates from the used repositories. the process uses the package manager from inside of the new root directory :param object manager: instance of a :class:`PackageManager` subclass :raises KiwiSystemUpdateFailed: if packages update fails """ log.info('Update system (chroot)') process = CommandProcess( command=manager.update(), log_topic='update' ) try: process.poll() except Exception as issue: raise KiwiSystemUpdateFailed( self.issue_message.format( headline='System update failed', reason=issue ) ) def clean_package_manager_leftovers(self) -> None: """ This methods cleans some package manager artifacts created at run time such as macros """ package_manager = self.xml_state.get_package_manager() release_version = self.xml_state.get_release_version() with Repository.new(self.root_bind, package_manager) as repo: with PackageManager.new( repository=repo, package_manager_name=package_manager, release_version=release_version ) as manager: manager.clean_leftovers() def _install_archives(self, archive_list, archive_target_dir_dict): log.info("Installing archives") for archive in archive_list: log.info("--> archive: %s", archive) description_dir = \ self.xml_state.xml_data.description_dir derived_description_dir = \ self.xml_state.xml_data.derived_description_dir archive_is_absolute = archive.startswith('/') if archive_is_absolute: archive_file = archive else: archive_file = '/'.join( [description_dir, archive] ) archive_exists = os.path.exists(archive_file) if not archive_is_absolute \ and not archive_exists and derived_description_dir: archive_file = '/'.join( [derived_description_dir, archive] ) target_dir = self.root_bind.root_dir if archive_target_dir_dict.get(archive): target_dir = os.path.normpath( os.sep.join( [target_dir, archive_target_dir_dict.get(archive)] ) ) log.info('--> target dir: %s', target_dir) tar = ArchiveTar(archive_file) tar.extract(target_dir) def _setup_requests( self, manager, packages, collections=None, products=None, ignored=None ): if packages: for package in sorted(packages): log.info('--> package: {0}'.format(package)) manager.request_package(package) if collections: for collection in sorted(collections): log.info('--> collection: {0}'.format(collection)) manager.request_collection(collection) if products: for product in sorted(products): log.info('--> product: {0}'.format(product)) manager.request_product(product) if ignored: for package in sorted(ignored): log.info('--> package excluded: {0}'.format(package)) manager.request_package_exclusion(package) return \ manager.package_requests + \ manager.collection_requests + \ manager.product_requests + \ manager.exclude_requests def _get_repo_customization_script(self, xml_repo: repository) -> str: script_path = xml_repo.get_customize() if script_path and not os.path.isabs(script_path): script_path = os.path.join( self.xml_state.xml_data.description_dir, script_path ) return script_path def __exit__(self, exc_type, exc_value, traceback): if hasattr(self, 'root_bind'): self.root_bind.cleanup() if self.root_import: self.root_import.overlay_finalize(self.xml_state) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/profile.py0000644000000000000000000003761215015277265014075 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 from typing import Dict # project from kiwi.utils.temporary import Temporary from kiwi.xml_state import XMLState from kiwi.system.shell import Shell from kiwi.defaults import Defaults log = logging.getLogger('kiwi') class Profile: """ **Create bash readable .profile environment from the XML description** :param object xml_state: instance of :class`XMLState` """ def __init__(self, xml_state: XMLState): self.xml_state = xml_state self.dot_profile: Dict = {} self._image_names_to_profile() self._profile_names_to_profile() self._packages_marked_for_deletion_to_profile() self._type_to_profile() self._type_complex_to_profile() self._preferences_to_profile() self._systemdisk_to_profile() self._strip_to_profile() self._oemconfig_to_profile() self._drivers_to_profile() self._root_partition_uuid_to_profile() def get_settings(self) -> Dict: """ Return all profile elements that has a value """ profile = {} for key, value in list(self.dot_profile.items()): profile[key] = Shell.format_to_variable_value(value) return collections.OrderedDict( sorted(profile.items()) ) def add(self, key: str, value: str) -> None: """ Add key/value pair to profile dictionary :param str key: profile key :param str value: profile value """ self.dot_profile[key] = value def delete(self, key: str) -> None: if key in self.dot_profile: del self.dot_profile[key] def create(self, filename: str) -> None: """ Create bash quoted profile :param str filename: file path name """ sorted_profile = self.get_settings() temp_profile = Temporary().new_file() with open(temp_profile.name, 'w') as profile: for key, value in list(sorted_profile.items()): profile.write( '{0}={1}{2}'.format(key, value, os.linesep) ) profile_environment = Shell.quote_key_value_file(temp_profile.name) with open(filename, 'w') as profile: for line in profile_environment: profile.write(line + os.linesep) log.debug('--> {0}'.format(line)) def _oemconfig_to_profile(self): # kiwi_oemvmcp_parmfile # kiwi_oemmultipath_scan # kiwi_oemswapMB # kiwi_oemrootMB # kiwi_oemresizeonce # kiwi_oempartition_install # kiwi_oemdevicefilter # kiwi_oemtitle # kiwi_oemkboot # kiwi_oemnicfilter # kiwi_oemreboot # kiwi_oemrebootinteractive # kiwi_oemshutdown # kiwi_oemshutdowninteractive # kiwi_oemsilentboot # kiwi_oemsilentinstall # kiwi_oemsilentverify # kiwi_oemskipverify # kiwi_oembootwait # kiwi_oemunattended # kiwi_oemunattended_id # kiwi_oemrecovery # kiwi_oemrecoveryID # kiwi_oemrecoveryPartSize # kiwi_oemrecoveryInPlace oemconfig = self.xml_state.get_build_type_oemconfig_section() if oemconfig: self.dot_profile['kiwi_oemvmcp_parmfile'] = \ self._text(oemconfig.get_oem_vmcp_parmfile()) self.dot_profile['kiwi_oemmultipath_scan'] = \ self._text(oemconfig.get_oem_multipath_scan()) self.dot_profile['kiwi_oemswapMB'] = \ self._text(oemconfig.get_oem_swapsize()) self.dot_profile['kiwi_oemrootMB'] = \ self._text(oemconfig.get_oem_systemsize()) self.dot_profile['kiwi_oemresizeonce'] = \ self._text(oemconfig.get_oem_resize_once()) self.dot_profile['kiwi_oempartition_install'] = \ self._text(oemconfig.get_oem_partition_install()) self.dot_profile['kiwi_oemramdisksize'] = \ self._text(oemconfig.get_oem_ramdisk_size()) self.dot_profile['kiwi_oemdevicefilter'] = \ self._text(oemconfig.get_oem_device_filter()) self.dot_profile['kiwi_oemtitle'] = \ self._text(oemconfig.get_oem_boot_title()) self.dot_profile['kiwi_oemkboot'] = \ self._text(oemconfig.get_oem_kiwi_initrd()) self.dot_profile['kiwi_oemnicfilter'] = \ self._text(oemconfig.get_oem_nic_filter()) self.dot_profile['kiwi_oemreboot'] = \ self._text(oemconfig.get_oem_reboot()) self.dot_profile['kiwi_oemrebootinteractive'] = \ self._text(oemconfig.get_oem_reboot_interactive()) self.dot_profile['kiwi_oemshutdown'] = \ self._text(oemconfig.get_oem_shutdown()) self.dot_profile['kiwi_oemshutdowninteractive'] = \ self._text(oemconfig.get_oem_shutdown_interactive()) self.dot_profile['kiwi_oemsilentboot'] = \ self._text(oemconfig.get_oem_silent_boot()) self.dot_profile['kiwi_oemsilentinstall'] = \ self._text(oemconfig.get_oem_silent_install()) self.dot_profile['kiwi_oemsilentverify'] = \ self._text(oemconfig.get_oem_silent_verify()) self.dot_profile['kiwi_oemskipverify'] = \ self._text(oemconfig.get_oem_skip_verify()) self.dot_profile['kiwi_oembootwait'] = \ self._text(oemconfig.get_oem_bootwait()) self.dot_profile['kiwi_oemunattended'] = \ self._text(oemconfig.get_oem_unattended()) self.dot_profile['kiwi_oemunattended_id'] = \ self._text(oemconfig.get_oem_unattended_id()) self.dot_profile['kiwi_oemrecovery'] = \ self._text(oemconfig.get_oem_recovery()) self.dot_profile['kiwi_oemrecoveryID'] = \ self._text(oemconfig.get_oem_recoveryID()) self.dot_profile['kiwi_oemrecoveryPartSize'] = \ self._text(oemconfig.get_oem_recovery_part_size()) self.dot_profile['kiwi_oemrecoveryInPlace'] = \ self._text(oemconfig.get_oem_inplace_recovery()) def _drivers_to_profile(self): # kiwi_drivers self.dot_profile['kiwi_drivers'] = ','.join( self.xml_state.get_drivers_list() ) def _type_complex_to_profile(self): # kiwi_xendomain # kiwi_install_volid if self.xml_state.is_xen_server(): self.dot_profile['kiwi_xendomain'] = 'dom0' if self.xml_state.get_build_type_name() == 'oem': install_volid = self.xml_state.build_type.get_volid() or \ Defaults.get_install_volume_id() self.dot_profile['kiwi_install_volid'] = install_volid if self.xml_state.get_build_type_name() == 'iso': live_iso_volid = self.xml_state.build_type.get_volid() or \ Defaults.get_volume_id() self.dot_profile['kiwi_live_volid'] = live_iso_volid def _strip_to_profile(self): # kiwi_strip_delete # kiwi_strip_tools # kiwi_strip_libs self.dot_profile['kiwi_strip_delete'] = ' '.join( self.xml_state.get_strip_files_to_delete() ) self.dot_profile['kiwi_strip_tools'] = ' '.join( self.xml_state.get_strip_tools_to_keep() ) self.dot_profile['kiwi_strip_libs'] = ' '.join( self.xml_state.get_strip_libraries_to_keep() ) def _systemdisk_to_profile(self): # kiwi_lvmgroup # kiwi_lvm # kiwi_Volume_X systemdisk = self.xml_state.get_build_type_system_disk_section() if systemdisk: volume_manager_name = self.xml_state.get_volume_management() if volume_manager_name == 'lvm': self.dot_profile['kiwi_lvm'] = 'true' self.dot_profile['kiwi_lvmgroup'] = systemdisk.get_name() if not self.dot_profile['kiwi_lvmgroup']: self.dot_profile['kiwi_lvmgroup'] = \ Defaults.get_default_volume_group_name() volume_count = 1 for volume in self.xml_state.get_volumes(): if volume.is_root_volume: volume_id_name = 'kiwi_Volume_Root' else: volume_id_name = 'kiwi_Volume_{id}'.format(id=volume_count) volume_count += 1 self.dot_profile[volume_id_name] = '|'.join( [ volume.name, 'size:all' if volume.fullsize else volume.size, volume.mountpoint or '' ] ) def _preferences_to_profile(self): # kiwi_iversion # kiwi_showlicense # kiwi_keytable # kiwi_timezone # kiwi_language # kiwi_splash_theme # kiwi_loader_theme for preferences in reversed(self.xml_state.get_preferences_sections()): if 'kiwi_iversion' not in self.dot_profile and preferences.get_version(): self.dot_profile['kiwi_iversion'] = \ self._text(preferences.get_version()) if 'kiwi_showlicense' not in self.dot_profile: self.dot_profile['kiwi_showlicense'] = \ self._text(preferences.get_showlicense()) if 'kiwi_keytable' not in self.dot_profile and preferences.get_keytable(): self.dot_profile['kiwi_keytable'] = \ self._text(preferences.get_keytable()) if 'kiwi_timezone' not in self.dot_profile and preferences.get_timezone(): self.dot_profile['kiwi_timezone'] = \ self._text(preferences.get_timezone()) if 'kiwi_language' not in self.dot_profile and preferences.get_locale(): self.dot_profile['kiwi_language'] = \ self._text(preferences.get_locale()) if 'kiwi_splash_theme' not in self.dot_profile and preferences.get_bootsplash_theme(): self.dot_profile['kiwi_splash_theme'] = \ self._text(preferences.get_bootsplash_theme()) if 'kiwi_loader_theme' not in self.dot_profile and preferences.get_bootloader_theme(): self.dot_profile['kiwi_loader_theme'] = \ self._text(preferences.get_bootloader_theme()) def _type_to_profile(self): # kiwi_type # kiwi_compressed # kiwi_boot_timeout # kiwi_wwid_wait_timeout # kiwi_hybrid # kiwi_hybridpersistent # kiwi_hybridpersistent_filesystem # kiwi_initrd_system # kiwi_ramonly # kiwi_target_blocksize # kiwi_target_removable # kiwi_cmdline # kiwi_firmware # kiwi_bootloader # kiwi_bootloader_console # kiwi_devicepersistency # kiwi_installboot # kiwi_bootkernel # kiwi_fsmountoptions # kiwi_bootprofile # kiwi_vga # kiwi_btrfs_root_is_snapper_snapshot # kiwi_startsector type_section = self.xml_state.build_type self.dot_profile['kiwi_type'] = \ type_section.get_image() self.dot_profile['kiwi_compressed'] = \ type_section.get_compressed() self.dot_profile['kiwi_boot_timeout'] = \ self.xml_state.get_build_type_bootloader_timeout() self.dot_profile['kiwi_wwid_wait_timeout'] = \ type_section.get_wwid_wait_timeout() self.dot_profile['kiwi_hybridpersistent'] = \ type_section.get_hybridpersistent() self.dot_profile['kiwi_hybridpersistent_filesystem'] = \ type_section.get_hybridpersistent_filesystem() self.dot_profile['kiwi_initrd_system'] = \ self.xml_state.get_initrd_system() self.dot_profile['kiwi_ramonly'] = \ type_section.get_ramonly() self.dot_profile['kiwi_target_blocksize'] = \ type_section.get_target_blocksize() self.dot_profile['kiwi_target_removable'] = \ type_section.get_target_removable() self.dot_profile['kiwi_cmdline'] = \ type_section.get_kernelcmdline() self.dot_profile['kiwi_firmware'] = \ type_section.get_firmware() self.dot_profile['kiwi_bootloader'] = \ self.xml_state.get_build_type_bootloader_name() self.dot_profile['kiwi_bootloader_console'] = "{}:{}".format( self.xml_state.get_build_type_bootloader_console()[0] or 'default', self.xml_state.get_build_type_bootloader_console()[1] or 'default' ) self.dot_profile['kiwi_btrfs_root_is_snapshot'] = \ type_section.get_btrfs_root_is_snapper_snapshot() self.dot_profile['kiwi_btrfs_root_is_snapper_snapshot'] = \ type_section.get_btrfs_root_is_snapper_snapshot() self.dot_profile['kiwi_gpt_hybrid_mbr'] = \ type_section.get_gpt_hybrid_mbr() self.dot_profile['kiwi_devicepersistency'] = \ type_section.get_devicepersistency() self.dot_profile['kiwi_installboot'] = \ type_section.get_installboot() self.dot_profile['kiwi_bootkernel'] = \ type_section.get_bootkernel() self.dot_profile['kiwi_fsmountoptions'] = \ type_section.get_fsmountoptions() self.dot_profile['kiwi_bootprofile'] = \ type_section.get_bootprofile() self.dot_profile['kiwi_vga'] = \ type_section.get_vga() self.dot_profile['kiwi_startsector'] = \ self.xml_state.get_disk_start_sector() self.dot_profile['kiwi_luks_empty_passphrase'] = \ self.xml_state.get_luks_credentials() == '' def _profile_names_to_profile(self): # kiwi_profiles self.dot_profile['kiwi_profiles'] = ','.join( self.xml_state.profiles ) def _packages_marked_for_deletion_to_profile(self): # kiwi_delete self.dot_profile['kiwi_delete'] = ' '.join( self.xml_state.get_to_become_deleted_packages() ) def _image_names_to_profile(self): # kiwi_displayname # kiwi_cpio_name # kiwi_iname self.dot_profile['kiwi_iname'] = \ self.xml_state.xml_data.get_name() self.dot_profile['kiwi_displayname'] = \ self.xml_state.xml_data.get_displayname() if not self.dot_profile['kiwi_displayname']: self.dot_profile['kiwi_displayname'] = \ self.dot_profile['kiwi_iname'] if self.xml_state.get_build_type_name() == 'cpio': self.dot_profile['kiwi_cpio_name'] = self.dot_profile['kiwi_iname'] def _root_partition_uuid_to_profile(self): # kiwi_rootpartuuid self.dot_profile['kiwi_rootpartuuid'] = \ self.xml_state.get_root_partition_uuid() def _text(self, section_content): """ Helper method to return the text for XML elements of the following structure:
text
. The data structure builder will return the text as a list """ if section_content: content = section_content[0] if content is True: return 'true' else: return content ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/result.py0000644000000000000000000001556115015277265013752 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 simplejson import pickle import os from typing import ( Dict, NamedTuple, TypeVar, Any ) # project from kiwi.xml_state import XMLState from kiwi.exceptions import ( KiwiResultError ) log = logging.getLogger('kiwi') # must be global to allow pickle to find it result_file_type = NamedTuple( 'result_file_type', [ ('filename', str), ('use_for_bundle', bool), ('compress', bool), ('shasum', bool) ] ) result_name_tags = NamedTuple( 'result_name_tags', [ ('N', str), # image name ('P', str), # concatenated profile name (_) ('A', str), # architecture name ('I', str), # custom ID setting ('T', str), # image type name ('M', str), # Major version number ('m', str), # Minor version number ('p', str), # Patch version number ('v', str) # Version string ] ) result_type = TypeVar('result_type', bound='Result') class Result: """ **Collect image building results** :param dict result_files: dict of result files :param object class_version: :class:`Result` class version :param object xml_state: instance of :class:`XMLState` """ def __init__(self, xml_state: XMLState): self.result_files: Dict[str, Any] = {} # Instances of this class are stored as result reference. # In order to handle class format changes any instance # provides a version information self.class_version: int = 1 self.xml_state = xml_state def add_bundle_format(self, pattern: str): version_string = self.xml_state.get_image_version() if '.' in version_string: (major, minor, patch) = version_string.split(sep='.', maxsplit=2) else: (major, minor, patch) = (version_string, '', '') self.name_tags = result_name_tags( N=self.xml_state.xml_data.get_name(), P='_'.join( self.xml_state.profiles ) if self.xml_state.profiles else '', A=self.xml_state.host_architecture, I='', T=self.xml_state.get_build_type_name(), M=major, m=minor, p=patch, v=version_string ) self.result_files['bundle_format'] = { 'pattern': pattern, 'tags': self.name_tags } def add( self, key: str, filename: str, use_for_bundle: bool = True, compress: bool = False, shasum: bool = True ) -> None: """ Add result tuple to result_files list :param str key: name :param str filename: file path name :param bool use_for_bundle: use when bundling results true|false :param bool compress: compress when bundling true|false :param bool shasum: create shasum when bundling true|false """ if key and filename: self.result_files[key] = result_file_type( filename=filename, use_for_bundle=use_for_bundle, compress=compress, shasum=shasum ) def get_results(self) -> Dict[str, result_file_type]: """ Current list of result tuples """ return self.result_files def print_results(self) -> None: """ Print results human readable """ if self.result_files: log.info('Result files:') for key, value in sorted(list(self.result_files.items())): if key == 'bundle_format': log.info('--> {0}: {1}'.format(key, value.get('pattern'))) else: log.info('--> {0}: {1}'.format(key, value.filename)) def dump(self, filename: str, portable: bool = True) -> None: """ Picke dump this instance to a file :param str filename: file path name :param bool portable: If set to true also create a .json formatted variant of the dump file which contains the elements of this instance that could be expressed in a portable json document. Default is set to: True :raises KiwiResultError: if pickle fails to dump :class:`Result` instance """ try: with open(filename, 'wb') as result: pickle.dump(self, result) if portable: with open(filename + '.json', 'w') as result_portable: result_portable.write( simplejson.dumps( self.result_files, sort_keys=True, indent=4 ) + os.linesep ) except Exception as e: raise KiwiResultError( 'Failed to pickle dump results: %s' % format(e) ) @staticmethod def load(filename: str) -> result_type: # type: ignore """ Load pickle dumped filename into a Result instance :param str filename: file path name :raises KiwiResultError: if filename does not exist or pickle fails to load filename """ if not os.path.exists(filename): raise KiwiResultError( 'No result information %s found' % filename ) try: with open(filename, 'rb') as result: return pickle.load(result) except Exception as e: raise KiwiResultError( 'Failed to pickle load results: %s' % type(e).__name__ ) @staticmethod def verify_image_size(size_limit: int, filename: str) -> None: """ Verifies the given image file does not exceed the size limit. Throws an exception if the limit is exceeded. If the size limit is set to None no verification is done. :param int size_limit: The size limit for filename in bytes. :param str filename: File to verify. :raises KiwiResultError: if filename exceeds the size limit """ if size_limit is not None: if os.path.getsize(filename) > size_limit: raise KiwiResultError( 'Build constraint failed: {0} is bigger than {1}' .format(filename, size_limit) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/root_bind.py0000644000000000000000000003040215015277265014402 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 shutil import textwrap from typing import List # project from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.path import Path from kiwi.mount_manager import MountManager from kiwi.utils.checksum import Checksum from kiwi.system.root_init import RootInit from kiwi.exceptions import ( KiwiMountKernelFileSystemsError, KiwiMountSharedDirectoryError, KiwiSetupIntermediateConfigError ) log = logging.getLogger('kiwi') class RootBind: """ **Implements binding/copying of host system paths into the new root directory** :param str root_dir: root directory path name :param list cleanup_files: list of files to cleanup, delete :param list mount_stack: list of mounted directories for cleanup :param list dir_stack: list of directories for cleanup :param list config_files: list of initial config files :param list bind_locations: list of kernel filesystems to bind mount :param str shared_location: shared directory between image root and build system root """ def __init__(self, root_init: RootInit): self.root_dir = root_init.root_dir self.cleanup_files: List[str] = [] self.mount_stack: List[MountManager] = [] self.dir_stack: List[str] = [] # need resolv.conf/hosts for chroot name resolution # need /etc/sysconfig/proxy for chroot proxy usage self.config_files = [ '/etc/resolv.conf', '/etc/hosts', '/etc/sysconfig/proxy' ] # need kernel filesystems bind mounted self.bind_locations = [ '/proc', '/dev', '/var/run/dbus', '/sys' ] # share the following directory with the host self.shared_location = '/' + Defaults.get_shared_cache_location() def mount_kernel_file_systems(self, delta_root: bool = False) -> None: """ Bind mount kernel filesystems :raises KiwiMountKernelFileSystemsError: if some kernel filesystem fails to mount """ try: if not delta_root: # Yes this bind mount to yourself looks weird and should not # be needed at all. The reason why it exists is to overcome # an issue that appears if you run containers in kiwi scripts # via e.g podman or docker. There is some information about the # topic here: https://github.com/moby/moby/issues/34817 shared_mount = MountManager( device=self.root_dir, mountpoint=self.root_dir ) shared_mount.bind_mount() self.mount_stack.append(shared_mount) for location in self.bind_locations: location_mount_target = os.path.normpath(os.sep.join([ self.root_dir, location ])) if os.path.exists(location) and os.path.exists( location_mount_target ): shared_mount = MountManager( device=location, mountpoint=location_mount_target ) shared_mount.bind_mount() self.mount_stack.append(shared_mount) except Exception as e: self.cleanup() raise KiwiMountKernelFileSystemsError( '%s: %s' % (type(e).__name__, format(e)) ) def umount_kernel_file_systems(self) -> None: """ Umount kernel filesystems :raises KiwiMountKernelFileSystemsError: if some kernel filesystem fails to mount """ umounts = [ mnt for mnt in self.mount_stack if mnt.device in self.bind_locations ] self._cleanup_mounts(umounts) def mount_shared_directory(self, host_dir: str = None) -> None: """ Bind mount shared location The shared location is a directory which shares data from the image buildsystem host with the image root system. It is used for the repository setup and the package manager cache to allow chroot operations without being forced to duplicate this data :param str host_dir: directory to share between image root and build system root :raises KiwiMountSharedDirectoryError: if mount fails """ if host_dir is None: host_dir = self.shared_location try: Path.create(self.root_dir + host_dir) Path.create('/' + host_dir) shared_mount = MountManager( device=host_dir, mountpoint=self.root_dir + host_dir ) shared_mount.bind_mount() self.mount_stack.append(shared_mount) self.dir_stack.append(host_dir) except Exception as e: self.cleanup() raise KiwiMountSharedDirectoryError( '%s: %s' % (type(e).__name__, format(e)) ) def setup_intermediate_config(self) -> None: """ Create intermediate config files Some config files e.g etc/hosts needs to be temporarly copied from the buildsystem host to the image root system in order to allow e.g DNS resolution in the way as it is configured on the buildsystem host. These config files only exists during the image build process and are not part of the final image :raises KiwiSetupIntermediateConfigError: if the management of intermediate configuration files fails """ try: for config in self.config_files: if os.path.exists(config): self.cleanup_files.append(config + '.kiwi') Command.run( ['cp', config, self.root_dir + config + '.kiwi'] ) link_target = os.path.basename(config) + '.kiwi' Command.run( ['ln', '-s', '-f', link_target, self.root_dir + config] ) checksum = Checksum(config) with open(self.root_dir + config + '.sha', 'w') as shasum: shasum.write(checksum.sha256()) except Exception as e: self.cleanup() raise KiwiSetupIntermediateConfigError( '%s: %s' % (type(e).__name__, format(e)) ) def cleanup(self) -> None: """ Cleanup mounted locations, directories and intermediate config files """ self._cleanup_mount_stack() self._cleanup_dir_stack() self._cleanup_intermediate_config() def _cleanup_intermediate_config(self): # delete kiwi copied config files config_files_to_delete = [] for config in self.cleanup_files: config_file = self.root_dir + config shasum_file = config_file.replace('.kiwi', '.sha') config_files_to_delete.append(config_file) config_files_to_delete.append(shasum_file) checksum = Checksum(config_file) if not checksum.matches(checksum.sha256(), shasum_file): message = textwrap.dedent('''\n Modifications to intermediate config file detected The file: {0} is a copy from the host system and symlinked to its origin in the image root during build time. Modifications to this file by e.g script code will not have any effect because the file gets restored by one of the following actions: 1. A package during installation provides it 2. A custom version of the file is setup as overlay file 3. The file is not provided by install or overlay and will be deleted at the end of the build process If you need a custom version of that file please provide it as an overlay file in your image description ''') log.warning(message.format(config_file)) del self.cleanup_files[:] # delete stale symlinks if there are any. normally the package # installation process should have replaced the symlinks with # real files from the packages. On deletion check for the # presence of a config file template and restore it try: for config in self.config_files: config_file = self.root_dir + config if os.path.islink(config_file): Command.run(['rm', '-f', config_file]) self._restore_config_template(config_file) Command.run(['rm', '-f'] + config_files_to_delete) except Exception as issue: log.warning( 'Failed to cleanup intermediate config files: {0}'.format(issue) ) self._restore_intermediate_config_rpmnew_variants() def _restore_config_template(self, config_file): """ Systems that use configuration templates and a tool to manage them might have skipped their call due to the presence of intermediate host config files in the new root tree. In this case if no custom file was provided the template file is used to restore the system standard configuration file """ template_map = { self.root_dir + '/etc/sysconfig/proxy': 'sysconfig.proxy' } template_dirs = [ self.root_dir + '/var/adm/fillup-templates', self.root_dir + '/usr/share/fillup-templates' ] if template_map.get(config_file): for template_dir in template_dirs: template_file = os.sep.join( [template_dir, template_map.get(config_file)] ) if os.path.exists(template_file): Command.run( ['cp', template_file, config_file] ) def _restore_intermediate_config_rpmnew_variants(self): """ check for rpmnew variants of the config files. if the package installed an rpmnew variant of the config file it needs to be moved to the original file name """ for config in self.config_files: config_rpm_new = self.root_dir + config + '.rpmnew' if os.path.exists(config_rpm_new) and not \ os.path.exists(self.root_dir + config): shutil.move(config_rpm_new, self.root_dir + config) def _cleanup_mounts(self, umounts): for umount in reversed(umounts): if umount.is_mounted(): try: umount.umount_lazy() self.mount_stack.remove(umount) except Exception as e: log.warning( 'Image root directory %s not cleanly umounted: %s', self.root_dir, format(e) ) else: log.warning('Path %s not a mountpoint', umount.mountpoint) def _cleanup_mount_stack(self): self._cleanup_mounts(self.mount_stack) del self.mount_stack[:] def _cleanup_dir_stack(self): for location in reversed(self.dir_stack): try: Path.remove_hierarchy(root=self.root_dir, path=location) except Exception as e: log.warning( 'Failed to remove directory hierarchy {0}: {1}'.format( self.root_dir + location, e ) ) del self.dir_stack[:] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/root_import/__init__.py0000644000000000000000000000501115015277265016535 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 importlib from typing import List from abc import ( ABCMeta, abstractmethod ) # project from kiwi.system.uri import Uri from kiwi.exceptions import KiwiRootImportError log = logging.getLogger('kiwi') class RootImport(metaclass=ABCMeta): """ Root import factory Attibutes * :attr:`root_dir` root directory path name * :attr:`image_uri` an instance of :class:`Uri` to an image containing a the root system * :attr:`image_type` type of the image to import """ @abstractmethod def __init__(self) -> None: return None # pragma: no cover @staticmethod def new(root_dir: str, image_uri: List[Uri], image_type: str): name_map = { 'docker': 'OCI', 'oci': 'OCI' } log.info( 'Importing root from a {0} image type'.format(image_type) ) (custom_args, module_namespace) = \ RootImport._custom_args_for_root_import(image_type) try: rootimport = importlib.import_module( 'kiwi.system.root_import.{0}'.format(module_namespace) ) module_name = 'RootImport{0}'.format(name_map[module_namespace]) return rootimport.__dict__[module_name]( root_dir, image_uri, custom_args ) except Exception as issue: raise KiwiRootImportError( 'Support to import {0} images not implemented: {1}'.format( image_type, issue ) ) @staticmethod def _custom_args_for_root_import(image_type: str): if image_type == 'docker': custom_args = {'archive_transport': 'docker-archive'} else: custom_args = {'archive_transport': 'oci-archive'} return [custom_args, 'oci'] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/root_import/base.py0000644000000000000000000001523515015277265015721 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 pathlib from typing import ( Dict, List, Optional ) # project from kiwi.xml_state import XMLState from kiwi.defaults import Defaults from kiwi.system.setup import SystemSetup from kiwi.path import Path from kiwi.system.uri import Uri from kiwi.command import Command from kiwi.mount_manager import MountManager from kiwi.utils.checksum import Checksum from kiwi.exceptions import ( KiwiRootImportError, KiwiUriTypeUnknown ) log = logging.getLogger('kiwi') class RootImportBase: """ Imports the root system from an already packed image. * :attr:`root_dir` root directory path name * :attr:`image_uris` Uri object(s) to store source location(s) * :attr:`custom_args` Dictonary to set specialized class specific configuration values """ def __init__( self, root_dir: str, image_uris: List[Uri], custom_args: Dict[str, str] = {} ): self.raw_urls: List[str] = [] self.image_files: List[str] = [] self.overlay: Optional[MountManager] = None self.root_dir = root_dir for image_uri in image_uris: try: if image_uri.is_remote(): raise KiwiRootImportError( 'Only local imports are supported' ) image_file = image_uri.translate() self.image_files.append(image_file) if not os.path.exists(image_file): raise KiwiRootImportError( f'Could not stat base image file: {image_file}' ) except KiwiUriTypeUnknown: # Let specialized class handle unknown uri schemes raw_url = image_uri.uri log.warning( f'Unkown URI type for the base image: {raw_url}' ) self.raw_urls.append(raw_url) self.post_init(custom_args) def post_init(self, custom_args): """ Initialization of the specialized import class Implementation in specialized root import class """ pass def overlay_finalize(self, xml_state: XMLState) -> None: """ Umount the overlay root, delete lower and work directories and move the upper (delta) to represent the final root_dir. All files that got deleted will be reported in a metadata file named Defaults.get_removed_files_name(). This information can be used by other tools to know about actively deleted files and bring them back at a later point in time. :param XMLState xml_state: Instance of XML state """ if self.overlay: pinch_reference = f'{self.root_dir}_cow_before_pinch' removed = f'{self.root_dir}/{Defaults.get_removed_files_name()}' systemfiles = f'{self.root_dir}/{Defaults.get_system_files_name()}' system = SystemSetup(xml_state, self.root_dir) # Run config-overlay.sh system.call_config_overlay_script() # Run config-host-overlay.sh system.call_config_host_overlay_script( working_directory=self.root_dir ) # Include system files if requested if xml_state.build_type.get_require_system_files(): with open(systemfiles, 'a') as systemfiles_fd: # copy on write makes this file to become part of the delta systemfiles_fd.write(os.linesep) # Umount and rename upper to be the new root self.overlay.umount() Path.wipe(self.root_dir) log.debug("renaming %s -> %s", self.overlay.upper, self.root_dir) pathlib.Path(self.overlay.upper).replace(self.root_dir) # Create removed files metadata if not os.path.isdir(pinch_reference): Path.create(pinch_reference) exclude_options = [] removed_files = [] for item in Defaults.get_exclude_list_for_removed_files_detection(): exclude_options.append('--exclude') exclude_options.append(item) get_removed = [ 'rsync', '-av', '--dry-run', '--out-format=%n' ] + exclude_options + [ f'{pinch_reference}/', f'{self.root_dir}/' ] for item in Command.run(get_removed).output.split(os.linesep): entry = f'{pinch_reference}/{item}' if item and os.path.exists(entry) and not os.path.isdir(entry): removed_files.append(item) Path.wipe(pinch_reference) if removed_files: with open(removed, 'w') as removed_fd: for filename in removed_files: removed_fd.write(filename) removed_fd.write(os.linesep) # delete character device nodes from delta tree. # (c) device nodes are used to track deleted files # and directories from the lower path of the overlayfs # tree. Since we extract the upper path of the overlay # any changes to the lower tree cannot be tracked. Command.run( [ 'find', self.root_dir, '-type', 'c', '-delete' ] ) Path.wipe(self.overlay.lower) Path.wipe(self.overlay.work) def overlay_data(self): """ Synchronize data from the given base image to the target root directory as an overlayfs mounted target. Implementation in specialized root import class """ raise NotImplementedError def sync_data(self): """ Synchronizes the root system of `image_file` into the root_dir Implementation in specialized root import class """ raise NotImplementedError def _make_checksum(self, image): checksum = Checksum(image) checksum.sha256(''.join([image, '.sha256'])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/root_import/oci.py0000644000000000000000000001000315015277265015545 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 pathlib from typing import ( Dict, List ) # project from kiwi.system.root_import.base import RootImportBase from kiwi.mount_manager import MountManager from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.utils.compress import Compress from kiwi.oci_tools import OCI log = logging.getLogger('kiwi') class RootImportOCI(RootImportBase): """ Implements the base class for importing a root system from a oci image tarball file. """ def post_init(self, custom_args: Dict[str, str]): self.archive_transport = custom_args['archive_transport'] self.compressed_image_files: List[Compress] = [] def sync_data(self): """ Synchronize data from the given base image to the target root directory. """ for image_url in self._get_image_urls(): with OCI.new() as oci: oci.import_container_image(image_url) oci.unpack() oci.import_rootfs(self.root_dir) # A copy of the uncompressed image(all derived imports) and # its checksum are kept inside the root_dir in order to ensure # the later steps i.e. system create are atomic and don't need # any third party archive. image_copy = Defaults.get_imported_root_image(self.root_dir) Path.create(os.path.dirname(image_copy)) oci.export_container_image( image_copy, 'oci-archive', Defaults.get_container_base_image_tag() ) self._make_checksum(image_copy) def overlay_data(self) -> None: """ Synchronize data from the given base image to the target root directory as an overlayfs mounted target. """ root_dir_ro = f'{self.root_dir}_ro' for image_url in self._get_image_urls(): with OCI.new() as oci: oci.import_container_image(image_url) oci.unpack() oci.import_rootfs(self.root_dir) log.debug("renaming %s -> %s", self.root_dir, root_dir_ro) pathlib.Path(self.root_dir).replace(root_dir_ro) Path.create(self.root_dir) self.overlay = MountManager(device='', mountpoint=self.root_dir) self.overlay.overlay_mount(root_dir_ro) def _get_image_urls(self) -> List[str]: if not self.raw_urls: image_urls: List[str] = [] for image_file in self.image_files: compressor = Compress(image_file) # Increase livetime of the the compressor instances # to the livetime of RootImportOCI. They create # temporary files which are referenced later and # need to live longer than this loop block self.compressed_image_files.append(compressor) if compressor.get_format(): compressor.uncompress(True) uncompressed_image = compressor.uncompressed_filename else: uncompressed_image = image_file image_urls.append( '{0}:{1}'.format( self.archive_transport, uncompressed_image ) ) return image_urls else: log.warning('Bypassing base image URI to OCI tools') return self.raw_urls ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/root_init.py0000644000000000000000000001043015015277265014430 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 pwd import getpwnam from shutil import copy import os # project from kiwi.utils.temporary import Temporary from kiwi.utils.sync import DataSync from kiwi.path import Path from kiwi.defaults import Defaults from kiwi.exceptions import ( KiwiRootDirExists, KiwiRootInitCreationError ) class RootInit: """ **Implements creation of new root directory for a linux system** Host system independent static default files and device nodes are created to initialize a new base system :param str root_dir: root directory path name """ def __init__(self, root_dir: str, allow_existing: bool = False): if not allow_existing and os.path.exists(root_dir): raise KiwiRootDirExists( 'Root directory %s already exists' % root_dir ) self.root_dir = root_dir def delete(self) -> None: """ Force delete root directory and its contents """ Path.wipe(self.root_dir) def create(self) -> None: """ Create new system root directory The method creates a temporary directory and initializes it for the purpose of building a system image from it. This includes the following setup: * create core system paths * create static core device nodes On success the contents of the temporary location are synced to the specified root_dir and the temporary location will be deleted. That way we never work on an incomplete initial setup :raises KiwiRootInitCreationError: if the init creation fails at some point """ root = Temporary(prefix='kiwi_root.').new_dir() Path.create(self.root_dir) try: self._create_base_directories(root.name) self._create_base_links(root.name) data = DataSync(root.name + '/', self.root_dir) data.sync_data( options=['-a', '--ignore-existing'] ) if Defaults.is_buildservice_worker(): copy( os.sep + Defaults.get_buildservice_env_name(), self.root_dir ) except Exception as e: self.delete() raise KiwiRootInitCreationError( '%s: %s' % (type(e).__name__, format(e)) ) def _create_base_directories(self, root): """ Create minimum collection of directories in the new root system required for kiwi to operate :param str root: path to the new root """ base_system_paths = ( Defaults.get_shared_cache_location(), 'dev/pts', 'proc', 'etc/sysconfig', 'run', 'sys', 'var' ) root_uid = getpwnam('root').pw_uid root_gid = getpwnam('root').pw_gid for path in base_system_paths: root_path = os.sep.join([root, path]) if not os.path.exists(root_path): os.makedirs(root_path) os.chown(root_path, root_uid, root_gid) def _create_base_links(self, root): """ Create minimum collection of file handle and runtime links which needs to be present prior to any package installation :param str root: path to the new root """ base_system_links = ( ('/proc/self/fd', '%s/dev/fd'), ('fd/2', '%s/dev/stderr'), ('fd/0', '%s/dev/stdin'), ('fd/1', '%s/dev/stdout') ) for src, target in base_system_links: os.symlink(src, target % root, ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3591456 kiwi-10.2.24/kiwi/system/setup.py0000644000000000000000000016743015015277265013577 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 glob import os import logging import copy import pathlib from collections import OrderedDict from collections import namedtuple from typing import ( Any, List, Optional, Dict ) # project import kiwi.defaults as defaults from kiwi.xml_parse import repository from kiwi.utils.fstab import Fstab from kiwi.xml_state import ( XMLState, FileT ) from kiwi.runtime_config import RuntimeConfig from kiwi.mount_manager import MountManager from kiwi.system.uri import Uri from kiwi.repository import Repository from kiwi.system.root_bind import RootBind from kiwi.system.root_init import RootInit from kiwi.command import Command from kiwi.command_process import CommandProcess from kiwi.utils.sync import DataSync from kiwi.defaults import Defaults from kiwi.system.users import Users from kiwi.system.shell import Shell from kiwi.path import Path from kiwi.archive.tar import ArchiveTar from kiwi.utils.compress import Compress from kiwi.utils.command_capabilities import CommandCapabilities from kiwi.utils.rpm_database import RpmDataBase from kiwi.builder.template.container_import import BuilderTemplateSystemdUnit from kiwi.system.profile import Profile from kiwi.exceptions import ( KiwiImportDescriptionError, KiwiScriptFailed, KiwiFileNotFound ) log: Any = logging.getLogger('kiwi') class SystemSetup: """ **Implementation of system setup steps supported by kiwi** Kiwi is not responsible for the system configuration, however some setup steps needs to be performed in order to provide a minimal work environment inside of the image according to the desired image type. :param object xml_state: instance of :class:`XMLState` :param str root_dir: root directory path name """ def __init__(self, xml_state: XMLState, root_dir: str): self.runtime_config = RuntimeConfig() self.arch = Defaults.get_platform_name() self.xml_state = xml_state self.description_dir = \ xml_state.xml_data.description_dir self.derived_description_dir = \ xml_state.xml_data.derived_description_dir self.root_dir = root_dir self._preferences_lookup() self._oemconfig_lookup() def setup_registry_import(self) -> None: """ Fetch container(s) and activate systemd unit to load containers from local oci-archive file during boot """ container_files_to_load = [] container_execs_to_load = [] after_services = set() for container in self.xml_state.get_containers(): log.info(f'Fetching container: {container.name}') pathlib.Path(f'{self.root_dir}/{defaults.LOCAL_CONTAINERS}').mkdir( parents=True, exist_ok=True ) container.fetch_command(self.root_dir) if container.load_command: container_files_to_load.append(container.container_file) container_execs_to_load.append(container.load_command) if container.backend == 'docker': after_services.add('docker.service') if container_files_to_load: log.info('--> Setup kiwi_containers.service import unit') service = BuilderTemplateSystemdUnit() unit_template = service.get_container_import_template( container_files_to_load, container_execs_to_load, list(after_services) ) unit = unit_template.substitute() unit_file = '{0}/etc/systemd/system/{1}.service'.format( self.root_dir, 'kiwi_containers' ) with open(unit_file, 'w') as systemd: systemd.write(unit) Command.run( [ 'chroot', self.root_dir, 'systemctl', 'enable', 'kiwi_containers' ] ) def import_description(self) -> None: """ Import XML descriptions, custom scripts, archives and script helper methods """ log.info('Importing Image description to system tree') description = os.path.join( self.root_dir, defaults.IMAGE_METADATA_DIR, 'config.xml' ) log.info( '--> Importing state XML description to {0}'.format(description) ) Path.create( os.path.join(self.root_dir, defaults.IMAGE_METADATA_DIR) ) with open(description, 'w', encoding='utf-8') as config: config.write('') self.xml_state.xml_data.export(outfile=config, level=0) self._import_custom_scripts() self._import_custom_archives() self._import_custom_files() self._import_cdroot_archive() def script_exists(self, name: str) -> bool: """ Check if provided script base name exists in the image description :param str name: script base name """ return os.path.exists(os.path.join(self.description_dir, name)) def cleanup(self) -> None: """ Delete all traces of a kiwi description which are not required in the later image """ Command.run( [ 'chroot', self.root_dir, 'rm', '-f', '.kconfig', '.profile', 'config.bootoptions', 'config.partids' ] ) meta_dir = f'{self.root_dir}/{defaults.IMAGE_METADATA_DIR}' if os.path.isdir(meta_dir): image_meta = MountManager( device='none', mountpoint=meta_dir ) image_meta.umount() Path.wipe(meta_dir) def import_repositories_marked_as_imageinclude(self) -> None: """ Those sections which are marked with the imageinclude attribute should be permanently added to the image repository configuration """ repository_sections = \ self.xml_state.get_repository_sections_used_in_image() root = RootInit( root_dir=self.root_dir, allow_existing=True ) with Repository.new( RootBind(root), self.xml_state.get_package_manager() ) as repo: repo.use_default_location() for xml_repo in repository_sections: repo_type = xml_repo.get_type() repo_source = xml_repo.get_source().get_path() repo_architectures = xml_repo.get_architectures() repo_user = xml_repo.get_username() repo_secret = xml_repo.get_password() repo_alias = xml_repo.get_alias() repo_priority = xml_repo.get_priority() repo_dist = xml_repo.get_distribution() repo_components = xml_repo.get_components() repo_repository_gpgcheck = xml_repo.get_repository_gpgcheck() repo_package_gpgcheck = xml_repo.get_package_gpgcheck() repo_customization_script = self._get_repo_customization_script( xml_repo ) repo_sourcetype = xml_repo.get_sourcetype() uri = Uri(repo_source, repo_type) repo_source_translated = uri.translate( check_build_environment=False ) if not repo_alias: repo_alias = uri.alias() log.info( 'Setting up image repository {0}'.format( Uri.print_sensitive(repo_source) ) ) log.info('--> Type: {0}'.format(repo_type)) log.info( '--> Translated: {0}'.format( Uri.print_sensitive(repo_source_translated) ) ) log.info('--> Alias: {0}'.format(repo_alias)) repo.add_repo( repo_alias, repo_source_translated, repo_type, repo_priority, repo_dist, repo_components, repo_user, repo_secret, uri.credentials_file_name(), repo_repository_gpgcheck, repo_package_gpgcheck, repo_sourcetype, repo_customization_script, repo_architectures ) def import_cdroot_files(self, target_dir: str) -> None: """ Copy cdroot files from the image description to the specified target directory. Supported is a tar archive named config-cdroot.tar[.compression-postfix] :param str target_dir: directory to unpack archive to """ glob_match = self.description_dir + '/config-cdroot.tar*' for cdroot_archive in sorted(glob.iglob(glob_match)): log.info( 'Extracting ISO user config archive: {0} to: {1}'.format( cdroot_archive, target_dir ) ) archive = ArchiveTar(cdroot_archive) archive.extract(target_dir) break def import_files(self) -> None: system_files = self.xml_state.get_system_files() bootstrap_files = self.xml_state.get_bootstrap_files() if system_files: self._sync_files(system_files) if bootstrap_files: self._sync_files(bootstrap_files) def import_overlay_files( self, follow_links: bool = False, preserve_owner_group: bool = False ) -> None: """ Copy overlay files from the image description to the image root tree. Supported are a root/ directory or a root.tar.gz tarball. The root/ directory takes precedence over the tarball. In addition the method also supports profile specific overlay files which are searched in a directory of the same name as the profile name. The overall order for including overlay files is as follows: 1. root/ dir or root.tar.gz 2. PROFILE_NAME/ dir(s) in the order of the selected profiles :param bool follow_links: follow symlinks true|false :param bool preserve_owner_group: preserve permissions true|false """ overlay_directory = self.description_dir + '/root/' overlay_archive = self.description_dir + '/root.tar.gz' if os.path.exists(overlay_directory): self._sync_overlay_files( f'{os.path.normpath(overlay_directory)}/', follow_links, preserve_owner_group ) elif os.path.exists(overlay_archive): log.info('Extracting user defined files from archive to image tree') archive = ArchiveTar(overlay_archive) archive.extract(self.root_dir) for profile in self.xml_state.profiles: overlay_directory = os.path.join( self.description_dir, profile ) + os.sep if os.path.exists(overlay_directory): self._sync_overlay_files( overlay_directory, follow_links, preserve_owner_group, profile ) def setup_machine_id(self) -> None: """ Setup systemd machine id There are various states of /etc/machine-id: a) Does not exist: Triggers ConditionFirstBoot, but does not work if the filesystem is initially read-only (booted without "rw"). b) Exists, is empty: Does not trigger ConditionFirstBoot, but works with read-only mounts. c) Exists, contains the string "uninitialized": Same as b), but triggers ConditionFirstBoot. Supported by systemd v247+ only. d) Exists, contains a valid ID. See the machine-id(5) man page for details. In images, d) is not desirable, so truncate the file. This is the previous behaviour and what existing images expect. If the image has one of the other states, keep it as-is. """ machine_id = os.path.join( self.root_dir, 'etc', 'machine-id' ) if os.path.exists(machine_id): with open(machine_id, 'r+') as f: if 'uninitialized' not in f.read(): f.truncate(0) def setup_permissions(self) -> None: """ Check and Fix permissions using chkstat Call chkstat in system mode which reads /etc/sysconfig/security to determine the configured security level and applies the appropriate permission definitions from the /etc/permissions* files. It's possible to provide those files as overlay files in the image description to apply a certain permission setup when needed. Otherwise the default setup as provided on the package level applies. It's required that the image root system has chkstat installed. If not present KIWI skips this step and continuous with a warning. """ chkstat = Path.which( 'chkstat', root_dir=self.root_dir, access_mode=os.X_OK ) if chkstat: log.info('Check/Fix File Permissions') Command.run( ['chroot', self.root_dir, 'chkstat', '--system', '--set'] ) else: log.debug( 'chkstat not found in image. File Permissions Check skipped' ) def setup_keyboard_map(self) -> None: """ Setup console keyboard """ if 'keytable' in self.preferences: log.info( 'Setting up keytable: {0}'.format(self.preferences['keytable']) ) if CommandCapabilities.has_option_in_help( 'systemd-firstboot', '--keymap', root=self.root_dir, raise_on_error=False ): Path.wipe(self.root_dir + '/etc/vconsole.conf') Command.run([ 'chroot', self.root_dir, 'systemd-firstboot', '--keymap=' + self.preferences['keytable'] ]) elif os.path.exists(self.root_dir + '/etc/sysconfig/keyboard'): Shell.run_common_function( 'baseUpdateSysConfig', [ self.root_dir + '/etc/sysconfig/keyboard', 'KEYTABLE', '"' + self.preferences['keytable'] + '"' ] ) else: log.warning( 'keyboard setup skipped no capable ' 'systemd-firstboot or etc/sysconfig/keyboard found' ) def setup_locale(self) -> None: """ Setup UTF8 system wide locale """ if 'locale' in self.preferences: if 'POSIX' in self.preferences['locale'].split(','): locale = 'POSIX' else: locale = '{0}.UTF-8'.format( self.preferences['locale'].split(',')[0] ) log.info( 'Setting up locale: {0}'.format(self.preferences['locale']) ) if CommandCapabilities.has_option_in_help( 'systemd-firstboot', '--locale', root=self.root_dir, raise_on_error=False ): Path.wipe(self.root_dir + '/etc/locale.conf') Command.run([ 'chroot', self.root_dir, 'systemd-firstboot', '--locale=' + locale ]) def setup_timezone(self) -> None: """ Setup timezone symlink """ if 'timezone' in self.preferences: log.info( 'Setting up timezone: {0}'.format(self.preferences['timezone']) ) if CommandCapabilities.has_option_in_help( 'systemd-firstboot', '--timezone', root=self.root_dir, raise_on_error=False ): Path.wipe(self.root_dir + '/etc/localtime') Command.run([ 'chroot', self.root_dir, 'systemd-firstboot', '--timezone=' + self.preferences['timezone'] ]) else: zoneinfo = '/usr/share/zoneinfo/' + self.preferences['timezone'] Command.run([ 'chroot', self.root_dir, 'ln', '-s', '-f', zoneinfo, '/etc/localtime' ]) def setup_groups(self) -> None: """ Add groups for configured users """ system_users = Users(self.root_dir) for user in self.xml_state.get_users(): group_dict = self._get_group_names_and_ids_for_user(user.get_name()) for group, group_id in group_dict.items(): if not system_users.group_exists(group): log.info('Adding group {0}'.format(group)) options = self._process_user_options( group_id=group_id ) system_users.group_add(group, options) def setup_users(self) -> None: """ Add/Modify configured users """ system_users = Users(self.root_dir) for user in self.xml_state.get_users(): log.info('Setting up user {0}'.format(user.get_name())) password = user.get_password() password_format = user.get_pwdformat() home_path = user.get_home() user_name = user.get_name() user_id = user.get_id() user_realname = user.get_realname() user_shell = user.get_shell() user_groups = list( self._get_group_names_and_ids_for_user(user.get_name()).keys() ) user_exists = system_users.user_exists(user_name) options = self._process_user_options( password_format=password_format, password=password, user_shell=user_shell, user_groups=user_groups, user_id=user_id, user_realname=user_realname, user_exists=user_exists, home_path=home_path ) group_msg = '--> Primary group for user {0}: {1}'.format( user_name, user_groups[0] ) if len(user_groups) else '' if user_exists: log.info('--> Modifying user: {0}'.format(user_name)) if group_msg: log.info(group_msg) system_users.user_modify(user_name, options) else: log.info('--> Adding user: {0}'.format(user_name)) if group_msg: log.info(group_msg) system_users.user_add(user_name, options) if home_path: log.info( '--> Setting permissions for {0}'.format(home_path) ) # Emtpy group string assumes the login or default group system_users.setup_home_for_user( user_name, user_groups[0] if len(user_groups) else '', home_path ) def setup_plymouth_splash(self) -> None: """ Setup the KIWI configured splash theme as default The method uses the plymouth-set-default-theme tool to setup the theme for the plymouth splash system. Only in case the tool could be found in the image root, it is assumed plymouth splash is in use and the tool is called in a chroot operation """ theme_setup = 'plymouth-set-default-theme' if Path.which(filename=theme_setup, root_dir=self.root_dir): for preferences in self.xml_state.get_preferences_sections(): splash_section_content = preferences.get_bootsplash_theme() if splash_section_content: splash_theme = splash_section_content[0] Command.run( ['chroot', self.root_dir, theme_setup, splash_theme] ) def import_image_identifier(self) -> None: """ Create etc/ImageID identifier file """ image_id = self.xml_state.xml_data.get_id() if image_id and os.path.exists(self.root_dir + '/etc'): image_id_file = self.root_dir + '/etc/ImageID' log.info( 'Creating image identifier: {0} in {1}'.format( image_id, image_id_file ) ) with open(image_id_file, 'w') as identifier: identifier.write('{0}{1}'.format(image_id, os.linesep)) def set_selinux_file_contexts(self, security_context_file: str) -> None: """ Initialize the security context fields (extended attributes) on the files matching the security_context_file :param str security_context_file: path file name """ log.info('Processing SELinux file security contexts') if not os.access(self.root_dir, os.W_OK): log.info('System is read-only, security context unchanged') return exclude = [] for devname in Defaults.get_exclude_list_for_non_physical_devices(): exclude.append('-e') exclude.append(f'/{devname}') # setfiles doesn't come with a stable command API and older versions # needs a different invocation syntax. On older versions the usage # information explicitly lists "setfiles -c policyfile" which is not # present in newer versions. As setfiles doesn't come with a simple # --version option, checking for this extra element in the usage # was the only pointer I could come up with to differentiate the # call options. if CommandCapabilities.has_option_in_help( 'setfiles', 'setfiles -c policyfile', ['--help'], root=self.root_dir, raise_on_error=False, silent=True ): Command.run( [ 'chroot', self.root_dir, 'setfiles', '-c', self._find_selinux_policy_file( self.xml_state.build_type.get_selinux_policy() or 'targeted' ), security_context_file ] ) Command.run( [ 'chroot', self.root_dir, 'setfiles', '-F', '-p' ] + exclude + [ security_context_file, '/' ] ) else: Command.run( [ 'chroot', self.root_dir, 'setfiles', '-T0', '-F', '-p', '-c', self._find_selinux_policy_file( self.xml_state.build_type.get_selinux_policy() or 'targeted' ) ] + exclude + [ security_context_file, '/' ] ) def setup_selinux_file_contexts(self) -> None: """ Set SELinux file security contexts if the default context file is found """ security_context = '/etc/selinux/targeted/contexts/files/file_contexts' if os.path.exists(self.root_dir + security_context): if Path.which(filename='setfiles', access_mode=os.X_OK, root_dir=self.root_dir): self.set_selinux_file_contexts(security_context) else: log.warning( 'security_context found but setfiles tool not installed' ) def export_modprobe_setup(self, target_root_dir: str) -> None: """ Export etc/modprobe.d to given root_dir :param str target_root_dir: path name """ modprobe_config = self.root_dir + '/etc/modprobe.d' if os.path.exists(modprobe_config): log.info('Export modprobe configuration') Path.create(target_root_dir + '/etc') data = DataSync( modprobe_config, target_root_dir + '/etc/' ) data.sync_data( options=['-a'] ) def export_flake_pilot_system_file_list( self, target_dir: str, file_name: str ) -> str: """ Export image package file list to the target_dir and filename :param str target_dir: path name :param str file_name: file name """ packager = Defaults.get_default_packager_tool( self.xml_state.get_package_manager() ) result_file = os.path.normpath(f'{target_dir}/{file_name}') if packager == 'rpm': self._export_rpm_flake_pilot_system_file_list(result_file) return result_file return '' def export_package_list(self, target_dir: str) -> str: """ Export image package list as metadata reference used by the open buildservice :param str target_dir: path name """ image_version = self.xml_state.get_image_version() or 'unspecified' filename = ''.join( [ target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + image_version, '.packages' ] ) packager = Defaults.get_default_packager_tool( self.xml_state.get_package_manager() ) if packager == 'rpm': self._export_rpm_package_list(filename) return filename elif packager == 'dpkg': self._export_deb_package_list(filename) return filename elif packager == 'pacman': self._export_pacman_package_list(filename) return filename return '' def export_package_changes(self, target_dir: str) -> str: """ Export image package changelog for comparision of actual changes of the installed packages :param str target_dir: path name """ image_version = self.xml_state.get_image_version() or 'unspecified' if self.runtime_config.get_package_changes(): filename = ''.join( [ target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + image_version, '.changes' ] ) packager = Defaults.get_default_packager_tool( self.xml_state.get_package_manager() ) if packager == 'rpm': self._export_rpm_package_changes(filename) return filename elif packager == 'dpkg': self._export_deb_package_changes(filename) return filename return '' def export_package_verification(self, target_dir: str) -> str: """ Export package verification result as metadata reference used by the open buildservice :param str target_dir: path name """ image_version = self.xml_state.get_image_version() or 'unspecified' filename = ''.join( [ target_dir, '/', self.xml_state.xml_data.get_name(), '.' + self.arch, '-' + image_version, '.verified' ] ) packager = Defaults.get_default_packager_tool( self.xml_state.get_package_manager() ) if packager == 'rpm': self._export_rpm_package_verification(filename) return filename elif packager == 'dpkg': self._export_deb_package_verification(filename) return filename return '' def call_disk_script(self) -> None: """ Call disk.sh script chrooted """ self._call_script( defaults.POST_DISK_SYNC_SCRIPT ) def call_pre_disk_script(self) -> None: """ Call pre_disk_sync.sh script chrooted """ self._call_script( defaults.PRE_DISK_SYNC_SCRIPT ) def call_post_bootstrap_script(self) -> None: """ Call post_bootstrap.sh script chrooted """ self._call_script( defaults.POST_BOOTSTRAP_SCRIPT ) def call_config_script(self) -> None: """ Call config.sh script chrooted """ self._call_script( defaults.POST_PREPARE_SCRIPT ) def call_config_overlay_script(self) -> None: """ Call config-overlay.sh script chrooted """ self._call_script( defaults.POST_PREPARE_OVERLAY_SCRIPT ) def call_config_host_overlay_script(self, working_directory: str = None) -> None: """ Call config-host-overlay.sh script _NON_ chrooted """ self._call_script_no_chroot( name=defaults.POST_HOST_PREPARE_OVERLAY_SCRIPT, option_list=[], working_directory=working_directory ) def call_image_script(self) -> None: """ Call images.sh script chrooted """ self._call_script( defaults.PRE_CREATE_SCRIPT ) def call_edit_boot_config_script( self, filesystem: str, boot_part_id: int, working_directory: str = None ) -> None: """ Call configured editbootconfig script _NON_ chrooted Pass the boot filesystem name and the partition number of the boot partition as parameters to the call :param str filesystem: boot filesystem name :param int boot_part_id: boot partition number :param str working_directory: directory name """ self._call_script_no_chroot( name=defaults.EDIT_BOOT_CONFIG_SCRIPT, option_list=[filesystem, format(boot_part_id)], working_directory=working_directory ) def call_edit_boot_install_script( self, diskname: str, boot_device_node: str, working_directory: str = None ) -> None: """ Call configured editbootinstall script _NON_ chrooted Pass the disk file name and the device node of the boot partition as parameters to the call :param str diskname: file path name :param str boot_device_node: boot device node name :param str working_directory: directory name """ self._call_script_no_chroot( name=defaults.EDIT_BOOT_INSTALL_SCRIPT, option_list=[diskname, boot_device_node], working_directory=working_directory ) def create_system_files(self) -> None: """ Create file list of packages to be used by flake-pilot """ if self.xml_state.build_type.get_provide_system_files(): self.export_flake_pilot_system_file_list( self.root_dir, Defaults.get_system_files_name() ) def create_fstab(self, fstab: Fstab) -> None: """ Create etc/fstab from given Fstab object Custom fstab modifications are possible and handled in the following order: 1. Look for an optional fstab.append file which allows to append custom fstab entries to the final fstab. Once embedded the fstab.append file will be deleted 2. Look for an optional fstab.patch file which allows to patch the current contents of the fstab file with a given patch file. Once patched the fstab.patch file will be deleted 3. Look for an optional fstab.script file which is called chrooted for the purpose of updating the fstab file as appropriate. Note: There is no validation in place that checks if the script actually handles fstab or any other file in the image rootfs. Once called the fstab.script file will be deleted :param object fstab: instance of Fstab """ fstab_file = self.root_dir + '/etc/fstab' fstab_append_file = self.root_dir + '/etc/fstab.append' fstab_patch_file = self.root_dir + '/etc/fstab.patch' fstab_script_file = self.root_dir + '/etc/fstab.script' fstab.export(fstab_file) if os.path.exists(fstab_append_file): with open(fstab_file, 'a') as fstab_io: with open(fstab_append_file, 'r') as append: fstab_io.write(append.read()) Path.wipe(fstab_append_file) if os.path.exists(fstab_patch_file): Command.run( ['patch', fstab_file, fstab_patch_file] ) Path.wipe(fstab_patch_file) if os.path.exists(fstab_script_file): self._call_script( name='etc/fstab.script', path_prefix='' ) Path.wipe(fstab_script_file) def create_init_link_from_linuxrc(self) -> None: """ kiwi boot images provides the linuxrc script, however the kernel also expects an init executable to be present. This method creates a hard link to the linuxrc file """ Command.run( ['ln', self.root_dir + '/linuxrc', self.root_dir + '/init'] ) def create_recovery_archive(self) -> None: """ Create a compressed recovery archive from the root tree for use with kiwi's recvoery system. The method creates additional data into the image root filesystem which is deleted prior to the creation of a new recovery data set """ # cleanup bash_comand = [ 'rm', '-f', self.root_dir + '/recovery.*' ] Command.run(['bash', '-c', ' '.join(bash_comand)]) if not self.oemconfig['recovery']: return # recovery.tar log.info('Creating recovery tar archive') metadata = { 'archive_name': self.root_dir + '/recovery.tar', 'archive_filecount': self.root_dir + '/recovery.tar.files', 'archive_size': self.root_dir + '/recovery.tar.size', 'partition_size': self.root_dir + '/recovery.partition.size', 'partition_filesystem': self.root_dir + '/recovery.tar.filesystem' } pathlib.Path(metadata['archive_name']).touch() archive = ArchiveTar( filename=metadata['archive_name'], create_from_file_list=False ) archive.create( source_dir=self.root_dir, exclude=['dev', 'proc', 'sys'], options=[ '--numeric-owner', '--hard-dereference', '--preserve-permissions' ] ) # recovery.tar.filesystem recovery_filesystem = self.xml_state.build_type.get_filesystem() with open(metadata['partition_filesystem'], 'w') as partfs: partfs.write('{0}'.format(recovery_filesystem)) log.info( '--> Recovery partition filesystem: {0}'.format(recovery_filesystem) ) # recovery.tar.files bash_comand = [ 'tar', '-tf', metadata['archive_name'], '|', 'wc', '-l' ] tar_files_call = Command.run( ['bash', '-c', ' '.join(bash_comand)] ) tar_files_count = int(tar_files_call.output.rstrip('\n')) with open(metadata['archive_filecount'], 'w') as files: files.write('{0}{1}'.format(tar_files_count, os.linesep)) log.info( '--> Recovery file count: {0} files'.format(tar_files_count) ) # recovery.tar.size recovery_archive_size_bytes = os.path.getsize(metadata['archive_name']) with open(metadata['archive_size'], 'w') as size: size.write('{0}'.format(recovery_archive_size_bytes)) log.info( '--> Recovery uncompressed size: {0} mbytes'.format( int(recovery_archive_size_bytes / 1048576) ) ) # recovery.tar.gz log.info('--> Compressing recovery archive') compress = Compress(self.root_dir + '/recovery.tar') compress.gzip() # recovery.partition.size recovery_archive_gz_size_mbytes = int( os.path.getsize(metadata['archive_name'] + '.gz') / 1048576 ) recovery_partition_mbytes = recovery_archive_gz_size_mbytes \ + Defaults.get_recovery_spare_mbytes() with open(metadata['partition_size'], 'w') as gzsize: gzsize.write('{0}'.format(recovery_partition_mbytes)) log.info( '--> Recovery partition size: {0} mbytes'.format( recovery_partition_mbytes ) ) # delete recovery archive if inplace recovery is requested # In this mode the recovery archive is created at install time # and not at image creation time. However the recovery metadata # is preserved in order to be able to check if enough space # is available on the disk to create the recovery archive. if self.oemconfig['recovery_inplace']: log.info( '--> Inplace recovery requested, deleting archive' ) Path.wipe(metadata['archive_name'] + '.gz') def _process_user_options( self, password_format: str = '', password: str = '', user_shell: str = '', user_groups: List[str] = [], user_id: str = '', group_id: str = '', user_realname: str = '', user_exists: bool = True, home_path: str = '' ): options = [] if password_format == 'plain': password = self._create_passwd_hash(password) if password: options.append('-p') options.append(password) if user_shell: options.append('-s') options.append(user_shell) if len(user_groups): options.append('-g') options.append(user_groups[0]) if len(user_groups) > 1: options.append('-G') options.append(','.join(user_groups[1:])) if user_id: options.append('-u') options.append('{0}'.format(user_id)) if group_id: options.append('-g') options.append('{0}'.format(group_id)) if user_realname: options.append('-c') options.append(user_realname) if not user_exists: options.append('-m') if home_path: options.append('-d') options.append(home_path) return options def _import_cdroot_archive(self): glob_match = self.description_dir + '/config-cdroot.tar*' for cdroot_archive in sorted(glob.iglob(glob_match)): archive_file = os.path.join( self.root_dir, defaults.IMAGE_METADATA_DIR ) log.info( '--> Importing {0} archive to {1}'.format( cdroot_archive, archive_file ) ) Command.run( ['cp', cdroot_archive, archive_file + os.sep] ) break def _import_custom_files(self): """ Import custom file files """ file_list = [] system_files = self.xml_state.get_system_files() bootstrap_files = self.xml_state.get_bootstrap_files() if system_files: file_list += system_files.keys() if bootstrap_files: file_list += bootstrap_files.keys() file_target_dir = os.path.join( self.root_dir, defaults.IMAGE_METADATA_DIR ) + os.sep for file in file_list: file_is_absolute = file.startswith(os.sep) if file_is_absolute: file_file = file else: file_file = os.path.join(self.description_dir, file) file_exists = os.path.exists(file_file) if not file_exists: if self.derived_description_dir and not file_is_absolute: file_file = self.derived_description_dir + '/' + file file_exists = os.path.exists(file_file) if file_exists: log.info( '--> Importing {0} file to {1}'.format( file_file, file_target_dir ) ) Command.run( ['cp', file_file, file_target_dir] ) else: raise KiwiImportDescriptionError( f'Specified file {file_file} does not exist' ) def _import_custom_archives(self): """ Import custom tar archive files """ archive_list = [] system_archives = self.xml_state.get_system_archives() bootstrap_archives = self.xml_state.get_bootstrap_archives() if system_archives: archive_list += system_archives if bootstrap_archives: archive_list += bootstrap_archives archive_target_dir = os.path.join( self.root_dir, defaults.IMAGE_METADATA_DIR ) + os.sep for archive in archive_list: archive_is_absolute = archive.startswith(os.sep) if archive_is_absolute: archive_file = archive else: archive_file = os.path.join(self.description_dir, archive) archive_exists = os.path.exists(archive_file) if not archive_exists: if self.derived_description_dir and not archive_is_absolute: archive_file = self.derived_description_dir + '/' + archive archive_exists = os.path.exists(archive_file) if archive_exists: log.info( '--> Importing {0} archive to {1}'.format( archive_file, archive_target_dir ) ) Command.run( ['cp', archive_file, archive_target_dir] ) else: raise KiwiImportDescriptionError( 'Specified archive {0} does not exist'.format(archive_file) ) def _import_custom_scripts(self): """ Import custom scripts """ # custom_scripts defines a dictionary with all script hooks # for each script name a the filepath and additional flags # are defined. the filepath could be either a relative or # absolute information. If filepath is set to None this indicates # the script hook is not used. The raise_if_not_exists flag # causes kiwi to raise an exception if a specified script # filepath does not exist script_type = namedtuple( 'script_type', ['filepath', 'raise_if_not_exists'] ) custom_scripts = { defaults.POST_BOOTSTRAP_SCRIPT: script_type( filepath=defaults.POST_BOOTSTRAP_SCRIPT, raise_if_not_exists=False ), defaults.POST_PREPARE_SCRIPT: script_type( filepath=defaults.POST_PREPARE_SCRIPT, raise_if_not_exists=False ), defaults.POST_PREPARE_OVERLAY_SCRIPT: script_type( filepath=defaults.POST_PREPARE_OVERLAY_SCRIPT, raise_if_not_exists=False ), defaults.POST_HOST_PREPARE_OVERLAY_SCRIPT: script_type( filepath=defaults.POST_HOST_PREPARE_OVERLAY_SCRIPT, raise_if_not_exists=False ), defaults.PRE_CREATE_SCRIPT: script_type( filepath=defaults.PRE_CREATE_SCRIPT, raise_if_not_exists=False ), defaults.PRE_DISK_SYNC_SCRIPT: script_type( filepath=defaults.PRE_DISK_SYNC_SCRIPT, raise_if_not_exists=False ), defaults.POST_DISK_SYNC_SCRIPT: script_type( filepath=defaults.POST_DISK_SYNC_SCRIPT, raise_if_not_exists=False ), defaults.EDIT_BOOT_CONFIG_SCRIPT: script_type( filepath=self.xml_state.build_type.get_editbootconfig(), raise_if_not_exists=True ), defaults.EDIT_BOOT_INSTALL_SCRIPT: script_type( filepath=self.xml_state.build_type.get_editbootinstall(), raise_if_not_exists=True ) } sorted_custom_scripts = OrderedDict( sorted(custom_scripts.items()) ) script_target_dir = os.path.join( self.root_dir, defaults.IMAGE_METADATA_DIR ) need_script_helper_functions = False for name, script in list(sorted_custom_scripts.items()): if script.filepath: if script.filepath.startswith('/'): script_file = script.filepath else: script_file = os.path.join( self.description_dir, script.filepath ) if os.path.exists(script_file): script_target_file = os.path.join( script_target_dir, name ) log.info( '--> Importing {0} script to {1}'.format( script.filepath, script_target_file ) ) Command.run( ['cp', script_file, script_target_file] ) need_script_helper_functions = True elif script.raise_if_not_exists: raise KiwiImportDescriptionError( 'Specified script {0} does not exist'.format( script_file ) ) if need_script_helper_functions: log.info('--> Importing script helper functions') Command.run( [ 'cp', Defaults.get_common_functions_file(), self.root_dir + '/.kconfig' ] ) def _call_script( self, name, option_list=None, path_prefix=defaults.IMAGE_METADATA_DIR ): script_path = os.path.join(self.root_dir, path_prefix, name) if os.path.exists(script_path): options = option_list or [] if log.getLogFlags().get('run-scripts-in-screen'): # Run scripts in a screen session if requested command = ['screen', '-t', '-X', 'chroot', self.root_dir] else: # In standard mode run scripts without a terminal # associated to them command = ['chroot', self.root_dir] if not Path.access(script_path, os.X_OK): command.append('bash') command.append( os.path.join(os.sep, path_prefix, name) ) command.extend(options) profile = Profile(self.xml_state) caller_environment = copy.deepcopy(os.environ) caller_environment.update(profile.get_settings()) config_script = Command.call(command, caller_environment) process = CommandProcess( command=config_script, log_topic='Calling ' + name + ' script' ) result = process.poll_and_watch() if result.returncode != 0: raise KiwiScriptFailed( '{0} failed: {1}'.format(name, result.stderr) ) # if configured, assign SELinux labels self.setup_selinux_file_contexts() def _call_script_no_chroot( self, name: str, option_list: List[str], working_directory: Optional[str] ): if not working_directory: working_directory = self.root_dir script_path = os.path.abspath( os.sep.join([self.root_dir, 'image', name]) ) if os.path.exists(script_path): bash_command = [ 'cd', working_directory, '&&', 'bash', '--norc', script_path, ' '.join(option_list) ] if log.getLogFlags().get('run-scripts-in-screen'): # Run scripts in a screen session if requested config_script = Command.call( ['screen', '-t', '-X', 'bash', '-c', ' '.join(bash_command)] ) else: # In standard mode run script through bash config_script = Command.call( ['bash', '-c', ' '.join(bash_command)] ) process = CommandProcess( command=config_script, log_topic='Calling ' + name + ' script' ) result = process.poll_and_watch() if result.returncode != 0: raise KiwiScriptFailed( '{0} failed: {1}'.format(name, result.stderr) ) def _create_passwd_hash(self, password): openssl = Command.run( ['openssl', 'passwd', '-1', '-salt', 'xyz', password] ) return openssl.output[:-1] def _preferences_lookup(self): self.preferences = {} for preferences in self.xml_state.get_preferences_sections(): timezone_section = preferences.get_timezone() locale_section = preferences.get_locale() keytable_section = preferences.get_keytable() if timezone_section: self.preferences['timezone'] = timezone_section[0] if locale_section: self.preferences['locale'] = locale_section[0] if keytable_section: self.preferences['keytable'] = keytable_section[0] def _oemconfig_lookup(self): self.oemconfig = { 'recovery_inplace': False, 'recovery': False } oemconfig = self.xml_state.get_build_type_oemconfig_section() if oemconfig: self.oemconfig['recovery'] = \ self._text(oemconfig.get_oem_recovery()) self.oemconfig['recovery_inplace'] = \ self._text(oemconfig.get_oem_inplace_recovery()) def _text(self, section_content): """ Helper method to return the text for XML elements of the following structure:
text
. """ if section_content: return section_content[0] def _export_rpm_flake_pilot_system_file_list(self, filename): log.info('Export rpm system files script for flake-pilot') dbpath_option = [ '--dbpath', self._get_rpm_database_location() ] skip_list = self.xml_state.get_system_files_ignore_packages() query_call = Command.run( [ 'rpm', '--root', self.root_dir, '-qa', '--qf', '%{NAME}\n' ] + dbpath_option ) with open(filename, 'w', encoding='utf-8') as systemfiles: systemfiles.write('set -e\n') for package in query_call.output.splitlines(): if package not in skip_list: systemfiles.write(f'rpm --noghost -ql {package}\n') def _export_rpm_package_list(self, filename): log.info('Export rpm packages metadata') dbpath_option = [ '--dbpath', self._get_rpm_database_location() ] query_call = Command.run( [ 'rpm', '--root', self.root_dir, '-qa', '--qf', '|'.join( [ '%{NAME}', '%{EPOCH}', '%{VERSION}', '%{RELEASE}', '%{ARCH}', '%{DISTURL}', '%{LICENSE}' ] ) + '\\n' ] + dbpath_option ) with open(filename, 'w', encoding='utf-8') as packages: packages.write( os.linesep.join(sorted(query_call.output.splitlines())) ) packages.write(os.linesep) def _export_deb_package_list(self, filename): log.info('Export deb packages metadata') query_call = Command.run( [ 'dpkg-query', '--admindir', os.sep.join([self.root_dir, 'var/lib/dpkg']), '-W', '-f', '|'.join( [ '${Package}', 'None', '${Version}', 'None', '${Architecture}', 'None', 'None' ] ) + '\\n' ] ) with open(filename, 'w', encoding='utf-8') as packages: packages.write( os.linesep.join(sorted(query_call.output.splitlines())) ) packages.write(os.linesep) def _export_pacman_package_list(self, filename): log.info('Export pacman packages metadata') query_call = Command.run( [ 'pacman', '--query', '--dbpath', os.sep.join([self.root_dir, 'var/lib/pacman']) ] ) with open(filename, 'w') as packages: for line in query_call.output.splitlines(): package, _, version_release = line.partition(' ') version, _, release = version_release.partition('-') packages.writelines([ '{0}|None|{1}|{2}|None|None|None{3}'.format( package, version, release, os.linesep ) ]) def _export_rpm_package_changes(self, filename): log.info('Export rpm packages changelog metadata') dbpath_option = [ '--dbpath', self._get_rpm_database_location() ] query_call = Command.run( [ 'rpm', '--root', self.root_dir, '-qa', '--qf', '%{NAME}|\\n', '--changelog' ] + dbpath_option ) with open(filename, 'w', encoding='utf-8') as changelog: changelog.write(query_call.output) def _export_deb_package_changes(self, filename): log.info('Export deb packages changelog metadata') package_doc_dir = os.sep.join( [self.root_dir, '/usr/share/doc'] ) with open(filename, 'w', encoding='utf-8') as changelog: for package in sorted(os.listdir(package_doc_dir)): changelog_file = os.sep.join( [package_doc_dir, package, 'changelog.Debian.gz'] ) if os.path.exists(changelog_file): changelog.write( '{0}{1}{2}'.format(package, '|', os.linesep) ) changelog.write( '{0}{1}'.format( Command.run(['zcat', changelog_file]).output, os.linesep ) ) def _export_rpm_package_verification(self, filename): log.info('Export rpm verification metadata') dbpath_option = [ '--dbpath', self._get_rpm_database_location() ] query_call = Command.run( command=['rpm', '--root', self.root_dir, '-Va'] + dbpath_option, raise_on_error=False ) with open(filename, 'w', encoding='utf-8') as verified: verified.write(query_call.output) def _export_deb_package_verification(self, filename): log.info('Export deb verification metadata') query_call = Command.run( command=[ 'dpkg', '--root', self.root_dir, '-V', '--verify-format', 'rpm' ], raise_on_error=False ) with open(filename, 'w', encoding='utf-8') as verified: verified.write(query_call.output) def _get_rpm_database_location(self): shared_mount = MountManager( device='/dev', mountpoint=self.root_dir + '/dev' ) if not shared_mount.is_mounted(): shared_mount.bind_mount() rpmdb = RpmDataBase(self.root_dir) if rpmdb.has_rpm(): dbpath = rpmdb.rpmdb_image.expand_query('%_dbpath') else: dbpath = rpmdb.rpmdb_host.expand_query('%_dbpath') if shared_mount.is_mounted(): shared_mount.umount_lazy() return dbpath def _sync_files(self, file_list: Dict[str, FileT]) -> None: log.info("Installing files") ordered_files = OrderedDict(sorted(file_list.items())) for filename, file_t in list(ordered_files.items()): target = file_t.target target_owner = file_t.owner target_permissions = file_t.permissions file_file = '/'.join( [self.root_dir, 'image', filename] ) target_name = self.root_dir if target: target_name = os.path.normpath( os.sep.join([target_name, target]) ) log.info(f'--> file: {file_file} -> {target_name}') if target_owner: Command.run( [ 'chroot', self.root_dir, 'chown', target_owner, file_file.replace(self.root_dir, '') ] ) if target_permissions: Command.run( [ 'chroot', self.root_dir, 'chmod', target_permissions, file_file.replace(self.root_dir, '') ] ) if os.path.dirname(target_name): Path.create(os.path.dirname(target_name)) data = DataSync(file_file, target_name) data.sync_data( options=Defaults.get_sync_options() ) def _sync_overlay_files( self, overlay_directory, follow_links=False, preserve_owner_group=False, profile=None ): log.info( 'Copying user defined {0} to image tree'.format( 'files for profile: {0}'.format(profile) if profile else 'files' ) ) sync_options = [ '-r', '-p', '-t', '-D', '-H', '-X', '-A', '--one-file-system' ] if follow_links: sync_options.append('--copy-links') else: sync_options.append('--links') if preserve_owner_group: sync_options.append('-o') sync_options.append('-g') data = DataSync( overlay_directory, self.root_dir ) data.sync_data( options=sync_options ) def _get_repo_customization_script(self, xml_repo: repository) -> str: script_path = xml_repo.get_customize() if script_path and not os.path.isabs(script_path): script_path = os.path.join(self.description_dir, script_path) return script_path def _get_group_names_and_ids_for_user(self, user: str) -> OrderedDict: group: OrderedDict = OrderedDict() for group_match in self.xml_state.get_user_groups(user): group_and_id = group_match.split(':') group.update( [( group_and_id[0], group_and_id[1] if len(group_and_id) > 1 else '' )] ) return group def _find_selinux_policy_file(self, policy_name: str) -> str: policy_dir = f'/etc/selinux/{policy_name}/policy' policy_dir_in_root = self.root_dir + policy_dir scandir_error = '' try: with os.scandir(policy_dir_in_root) as policy_path: for entry in policy_path: if entry.is_file() and entry.name.startswith('policy.'): return os.path.join(policy_dir, entry.name) except Exception as issue: scandir_error = format(issue) raise KiwiFileNotFound( 'Unable to find SELinux policy in {0!r} {1}'.format( policy_dir_in_root, scandir_error ) ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/system/shell.py0000644000000000000000000000764315015277265013545 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, Any, List from collections.abc import Iterable # project from kiwi.utils.temporary import Temporary from kiwi.command import Command from kiwi.defaults import Defaults from kiwi.exceptions import KiwiShellVariableValueError class Shell: """ **Special character handling for shell evaluated code** """ @staticmethod def quote(message: str) -> str: """ Quote characters which have a special meaning for bash but should be used as normal characters. actually I had planned to use pipes.quote but it does not quote as I had expected it. e.g 'name_wit_a_$' does not quote the $ so we do it on our own for the scope of kiwi :param str message: message text :return: quoted text :rtype: str """ # \\ quoting must be first in the list quote_characters = ['\\', '$', '"', '`', '!'] for quote in quote_characters: message = message.replace(quote, '\\' + quote) return message @staticmethod def quote_key_value_file(filename: str) -> List[str]: """ Quote given input file which has to be of the form key=value to be able to become sourced by the shell :param str filename: file path name :return: list of quoted text :rtype: List[str] """ temp_copy = Temporary().new_file() Command.run(['cp', filename, temp_copy.name]) Shell.run_common_function('baseQuoteFile', [temp_copy.name]) with open(temp_copy.name) as quoted: return quoted.read().splitlines() @staticmethod def run_common_function(name: str, parameters: List[str]) -> None: """ Run a function implemented in config/functions.sh :param str name: function name :param list parameters: function arguments """ Command.run( [ 'bash', '-c', 'source ' + ''.join( [ Defaults.get_common_functions_file(), '; ', name, ' ', ' '.join(parameters) ] ) ] ) @staticmethod def format_to_variable_value(value: Optional[Any]) -> str: """ Format given variable value to return a string value representation that can be sourced by shell scripts. If the provided value is not representable as a string (list, dict, tuple etc) an exception is raised :param any value: a python variable :raises KiwiShellVariableValueError: if value is an iterable :return: string value representation :rtype: str """ if value is None: return '' if isinstance(value, bool): return format(value).lower() if isinstance(value, str): return value if isinstance(value, bytes): return format(value.decode()) if isinstance(value, Iterable): # we will have a hard time to turn an iterable (list, dict ...) # into a useful string raise KiwiShellVariableValueError( 'Value cannot be {0}'.format(type(value)) ) return format(value) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/system/size.py0000644000000000000000000000677015015277265013410 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 # project from kiwi.command import Command from kiwi.defaults import Defaults class SystemSize: """ **Provide source tree size information** :param str source_dir: source directory path name """ def __init__(self, source_dir: str): self.source_dir = source_dir def customize(self, size: float, requested_filesystem: str) -> int: """ Increase the sum of all file sizes by an empiric factor Each filesystem has some overhead it needs to manage itself. Thus the plain data size is always smaller as the size of the container which embeds it. This method increases the given size by a filesystem specific empiric factor to ensure the given data size can be stored in a filesystem of the customized size :param float size: mbsize to update :param str requested_filesystem: filesystem name :return: mbytes :rtype: int """ if requested_filesystem: if requested_filesystem.startswith('ext'): size *= 1.5 file_count = self.accumulate_files() inode_mbytes = \ file_count * Defaults.get_default_inode_size() / 1048576 size += 2 * inode_mbytes elif requested_filesystem == 'btrfs': size *= 1.5 elif requested_filesystem == 'xfs': size *= 1.5 return int(size) def accumulate_mbyte_file_sizes(self, exclude: List[str] = None) -> int: """ Calculate data size of all data in the source tree :param list exclude: list of paths to exclude :return: mbytes :rtype: int """ exclude_options: List[str] = [] for nodev in Defaults.get_exclude_list_for_non_physical_devices(): exclude_options.append('--exclude') exclude_options.append( os.sep.join([self.source_dir, nodev]) ) if exclude: for item in exclude: exclude_options.append('--exclude') exclude_options.append(item) du_call = Command.run( [ 'du', '-s', '--apparent-size', '--block-size', '1' ] + exclude_options + [ self.source_dir ] ) return int(int(du_call.output.split('\t')[0]) / 1048576) def accumulate_files(self) -> int: """ Calculate sum of all files in the source tree :return: number of files :rtype: int """ bash_comand = [ 'find', self.source_dir, '|', 'wc', '-l' ] wc_call = Command.run( [ 'bash', '-c', ' '.join(bash_comand) ] ) return int(wc_call.output.rstrip('\n')) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/system/uri.py0000644000000000000000000003125415015277265013230 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 lxml import etree from urllib.parse import ( urlparse, ParseResult, quote ) from urllib.request import urlopen from urllib.request import Request import requests from uuid import uuid4 from typing import Optional # project from kiwi.defaults import Defaults from kiwi.runtime_config import RuntimeConfig from kiwi.exceptions import ( KiwiUriStyleUnknown, KiwiUriTypeUnknown, KiwiUriOpenError ) log = logging.getLogger('kiwi') class Uri: """ **Normalize and manage URI types** """ def __init__( self, uri: str, repo_type: str = 'rpm-md', source_type: str = '' ): """ Manage kiwi source URIs and allow transformation into standard URLs :param str uri: URI, remote, local or metalink repository location The resource type as part of the URI can be set to one of: * http: * https: * ftp: * obs: * dir: * file: * obsrepositories: * this: The special this:// type resolve to the image description directory. The code to resolve this is not part of the Uri class because it has no state information about the image description directory. Therefore the resolving of the this:// path happens on construction of an XMLState object as part of the resolve_this_path() method. The method resolves the path into a native dir:// URI which can be properly handled here. :param str repo_type: repository type name, defaults to 'rpm-md' and is only effectively used when building inside of the open build service which maps local repositories to a specific environment :param str source_type: specify source type if the provided URI is a service. Currently only the metalink source type is handled """ self.runtime_config = RuntimeConfig() if source_type == 'metalink': uri = self._resolve_metalink_uri(uri) self.repo_type = repo_type self.uri = uri if not uri.startswith(os.sep) else ''.join( [Defaults.get_default_uri_type(), uri] ) self.remote_uri_types = { 'http': True, 'https': True, 'ftp': True, 'obs': True } self.local_uri_type = { 'dir': True, 'file': True, 'obsrepositories': True } def translate(self, check_build_environment: bool = True) -> str: """ Translate repository location according to their URI type Depending on the URI type the provided location needs to be adapted e.g updated by the service URL in case of an open buildservice project name :raises KiwiUriStyleUnknown: if the uri scheme can't be detected, is unknown or it is inconsistent with the build environment :param bool check_build_environment: specify if the uri translation should depend on the environment the build is called in. As of today this only effects the translation result if the image build happens inside of the Open Build Service :return: translated repository location :rtype: str """ uri = urlparse(self.uri) if not uri.scheme: raise KiwiUriStyleUnknown( 'URI scheme not detected {uri}'.format(uri=self.uri) ) elif uri.scheme == 'obs': if check_build_environment and Defaults.is_buildservice_worker(): return self._buildservice_path( name=''.join([uri.netloc, uri.path]).replace(':/', ':'), fragment=uri.fragment, urischeme=uri.scheme ) else: return self._obs_project_download_link( ''.join([uri.netloc, uri.path]).replace(':/', ':') ) elif uri.scheme == 'obsrepositories': if not Defaults.is_buildservice_worker(): raise KiwiUriStyleUnknown( 'Only the buildservice can use the {0} schema'.format( uri.scheme ) ) return self._buildservice_path( name=''.join([uri.netloc, uri.path]).replace(':/', ':'), fragment=uri.fragment, urischeme=uri.scheme ) elif uri.scheme == 'dir': return self._local_path(uri.path) elif uri.scheme == 'file': return self._local_path(uri.path) elif uri.scheme.startswith('http') or uri.scheme == 'ftp': netloc = uri.netloc uri_with_credentials_pattern = '^(.*):(.*)@(.*)$' sensitive_match = re.match(uri_with_credentials_pattern, netloc) if sensitive_match: netloc = "{0}:{1}@{2}".format( quote(sensitive_match.group(1)), quote(sensitive_match.group(2)), sensitive_match.group(3) ) if self._get_credentials_uri() or not uri.query: return ''.join( [uri.scheme, '://', netloc, uri.path] ) else: return ''.join( [uri.scheme, '://', netloc, uri.path, '?', uri.query] ) else: raise KiwiUriStyleUnknown( 'URI schema %s not supported' % self.uri ) @staticmethod def print_sensitive(location: str) -> str: uri_with_credentials_pattern = '^.*://(.*:.*)@.*' sensitive_match = re.match(uri_with_credentials_pattern, location) if sensitive_match: return location.replace(sensitive_match.group(1), '******') else: return location def credentials_file_name(self) -> str: """ Filename to store repository credentials :return: credentials file name :rtype: str """ uri = self._get_credentials_uri() # initialize query with default credentials file name. # The information will be overwritten if the uri contains # a parameter query with a credentials parameter query = {'credentials': 'kiwiRepoCredentials'} if uri: query = dict(params.split('=') for params in uri.query.split('&')) # type: ignore return query['credentials'] def alias(self) -> str: """ Create hex representation of uuid4 If the repository definition from the XML description does not provide an alias, kiwi creates one for you. However it's better to assign a human readable alias in the XML configuration :return: alias name as hex representation of uuid4 :rtype: str """ return uuid4().hex def is_remote(self) -> bool: """ Check if URI is a remote or local location :return: True|False :rtype: bool """ uri = urlparse(self.uri) if not uri.scheme: raise KiwiUriStyleUnknown( 'URI scheme not detected %s' % self.uri ) if uri.scheme == 'obs' and Defaults.is_buildservice_worker(): return False elif uri.scheme in self.remote_uri_types: return True elif uri.scheme in self.local_uri_type: return False else: raise KiwiUriTypeUnknown( 'URI type %s unknown' % uri.scheme ) def is_public(self) -> bool: """ Check if URI is considered to be publicly reachable :return: True|False :rtype: bool """ uri = urlparse(self.uri) if not uri.scheme: # unknown uri schema is considered not public return False elif uri.scheme == 'obs': # obs is public but only if the configured download_server is public return self.runtime_config.is_obs_public() elif uri.scheme in self.remote_uri_types: # listed in remote uri types, thus public return True else: # unknown uri type considered not public return False def get_fragment(self) -> str: """ Returns the fragment part of the URI. :return: fragment part of the URI if any, empty string otherwise :rtype: str """ uri = urlparse(self.uri) return uri.fragment def _get_credentials_uri(self) -> Optional[ParseResult]: uri = urlparse(self.uri) credentials_uri = None if uri.query and uri.query.startswith('credentials='): credentials_uri = uri return credentials_uri def _local_path(self, path: str) -> str: return os.path.abspath(os.path.normpath(path)) def _obs_project_download_link(self, name: str) -> str: name_parts = name.split(os.sep) repository = name_parts.pop() project = os.sep.join(name_parts) download_link = None try: download_link = os.sep.join( [ self.runtime_config.get_obs_download_server_url(), project.replace(':', ':/'), repository ] ) if not Defaults.is_buildservice_worker(): request = requests.get(download_link) request.raise_for_status() return request.url else: log.warning( 'Using {0} without location verification due to build ' 'in isolated environment'.format(download_link) ) return download_link except Exception as issue: raise KiwiUriOpenError( f'{download_link}: {issue}' ) def _buildservice_path( self, name: str, urischeme: str, fragment: str = '' ) -> str: """ Special to openSUSE buildservice. If the buildservice builds the image it arranges the repos for each build in a special environment, the so called build worker. """ bs_source_dir = '/usr/src/packages/SOURCES' if self.repo_type == 'container': if urischeme == 'obsrepositories': local_path = os.sep.join( [bs_source_dir, 'containers/_obsrepositories', name] ) else: local_path = os.sep.join( [bs_source_dir, 'containers', name] ) if fragment: local_path = ''.join([local_path, '#', fragment]) else: local_path = os.sep.join( [bs_source_dir, 'repos', name] ) return self._local_path(local_path) def _resolve_metalink_uri(self, uri: str) -> str: selected_repo_source = uri namespace_map = dict( metalink="http://www.metalinker.org/" ) expression = '//metalink:file[@name="repomd.xml"]/metalink:resources/*' try: metalink_location = urlopen(Request(uri)) xml = etree.parse(metalink_location) url_list = xml.getroot().xpath( expression, namespaces=namespace_map ) source_dict = {} for url in url_list: if url.get('protocol') == 'https': source_dict[url.text] = int(url.get('preference')) start_preference = 0 for url in sorted(source_dict.keys()): preference = source_dict[url] if preference > start_preference: selected_repo_source = url start_preference = preference except Exception as issue: raise KiwiUriOpenError( f'Failed to resolve metalink URI: {issue}' ) selected_repo_source = selected_repo_source.replace( 'repodata/repomd.xml', '' ) return selected_repo_source ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/system/users.py0000644000000000000000000000615315015277265013572 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 # project from kiwi.command import Command class Users: """ **Operations on users and groups in a root directory** :param str root_dir: root directory path name """ def __init__(self, root_dir: str): self.root_dir = root_dir def user_exists(self, user_name: str) -> bool: """ Check if user exists :param str user_name: user name :return: True|False :rtype: bool """ return self._search_for(user_name, '/etc/passwd') def group_exists(self, group_name: str) -> bool: """ Check if group exists :param str group_name: group name :return: True|False :rtype: bool """ return self._search_for(group_name, '/etc/group') def group_add(self, group_name: str, options: List[str]) -> None: """ Add group with options :param str group_name: group name :param list options: groupadd options """ Command.run( ['chroot', self.root_dir, 'groupadd'] + options + [group_name] ) def user_add(self, user_name: str, options: List[str]) -> None: """ Add user with options :param str user_name: user name :param list options: useradd options """ Command.run( ['chroot', self.root_dir, 'useradd'] + options + [user_name] ) def user_modify(self, user_name: str, options: List[str]) -> None: """ Modify user with options :param str user_name: user name :param list options: usermod options """ Command.run( ['chroot', self.root_dir, 'usermod'] + options + [user_name] ) def setup_home_for_user( self, user_name: str, group_name: str, home_path: str ) -> None: """ Setup user home directory :param str user_name: user name :param str group_name: group name :param str home_path: path name """ user_and_group = user_name + ':' + group_name Command.run( ['chroot', self.root_dir, 'chown', '-R', user_and_group, home_path] ) def _search_for(self, name, in_file): search = '^' + name + ':' try: Command.run( ['chroot', self.root_dir, 'grep', '-q', search, in_file] ) except Exception: return False return True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/__init__.py0000644000000000000000000000000015015277265013752 0ustar00././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/base.py0000644000000000000000000002533015015277265013142 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 glob from typing import ( List, Dict, Optional, Union, Any ) from operator import attrgetter # project from kiwi.cli import Cli from kiwi.xml_state import XMLState from kiwi.xml_description import XMLDescription from kiwi.runtime_checker import RuntimeChecker from kiwi.exceptions import ( KiwiConfigFileNotFound ) log: Any = logging.getLogger('kiwi') class CliTask: """ Base class for all task classes, loads the task and provides the interface to the command options and the XML description Attributes * :attr:`should_perform_task_setup` Indicates if the task should perform the setup steps which covers the following task configurations: * setup debug level * setup logfile * setup color output """ def __init__(self, should_perform_task_setup: bool = True) -> None: self.cli = Cli() # initialize runtime checker self.runtime_checker: Optional[RuntimeChecker] = None # help requested self.cli.show_and_exit_on_help_request() # load/import task module self.task = self.cli.load_command() # get command specific args self.command_args = self.cli.get_command_args() # get global args self.global_args = self.cli.get_global_args() # initialize generic runtime check dicts self.checks_before_command_args: Dict[str, List[str]] = { 'check_image_version_provided': [], 'check_initrd_selection_required': [], 'check_boot_description_exists': [], 'check_consistent_kernel_in_boot_and_system_image': [], 'check_container_tool_chain_installed': [], 'check_volume_setup_defines_reserved_labels': [], 'check_volume_setup_defines_multiple_fullsize_volumes': [], 'check_volume_setup_has_no_root_definition': [], 'check_volume_label_used_with_lvm': [], 'check_swap_name_used_with_lvm': [], 'check_xen_uniquely_setup_as_server_or_guest': [], 'check_mediacheck_installed': [], 'check_dracut_module_for_live_iso_in_package_list': [], 'check_dracut_module_for_disk_overlay_in_package_list': [], 'check_dracut_module_for_disk_oem_in_package_list': [], 'check_dracut_module_for_oem_install_in_package_list': [], 'check_appx_naming_conventions_valid': [], 'check_image_type_unique': [], 'check_include_references_unresolvable': [], 'check_luksformat_options_valid': [], 'check_partuuid_persistency_type_used_with_mbr': [], 'check_efi_fat_image_has_correct_size': [] } self.checks_after_command_args: Dict[str, List[str]] = { 'check_repositories_configured': [], 'check_image_include_repos_publicly_resolvable': [] } if should_perform_task_setup: # set log file if self.global_args['--logfile']: log.set_logfile( self.global_args['--logfile'] ) # set log socket if self.global_args['--logsocket']: log.set_log_socket( self.global_args['--logsocket'] ) # set log level if self.global_args['--loglevel']: try: log.setLogLevel(int(self.global_args['--loglevel'])) except ValueError: # Not a numeric log level, stick with the default # which is INFO log.setLogLevel(logging.INFO) elif self.global_args['--debug']: log.setLogLevel(logging.DEBUG) else: log.setLogLevel(logging.INFO) if self.global_args['--logfile'] == 'stdout': # deactivate standard console logger by setting # the highest possible log entry level log.setLogLevel(logging.CRITICAL, except_for=['file', 'socket']) elif self.global_args['--logfile']: # set debug level for the file and socket logger log.setLogLevel(logging.DEBUG, only_for=['file', 'socket']) # set log flags if self.global_args['--debug-run-scripts-in-screen']: log.setLogFlag('run-scripts-in-screen') if self.global_args['--color-output']: log.set_color_format() # initialize runtime configuration # import RuntimeConfig late to make sure the logging setup applies from kiwi.runtime_config import RuntimeConfig self.runtime_config = RuntimeConfig() def load_xml_description( self, description_directory: str, kiwi_file: str = '' ) -> None: """ Load, upgrade, validate XML description :param str description_directory: Path to the image description :param str kiwi_file: 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 """ if kiwi_file: config_file = os.sep.join([description_directory, kiwi_file]) else: config_file = os.sep.join([description_directory, '/config.xml']) if not os.path.exists(config_file): # alternative config file lookup location config_file = description_directory + '/image/config.xml' if not os.path.exists(config_file): # glob config file search, first match wins glob_match = description_directory + '/*.kiwi' for kiwi_file in sorted(glob.iglob(glob_match)): config_file = kiwi_file break if not os.path.exists(config_file): raise KiwiConfigFileNotFound( 'no XML description found in %s' % description_directory ) self.description = XMLDescription( config_file ) self.xml_data = self.description.load() self.config_file = config_file.replace('//', '/') self.xml_state = XMLState( self.xml_data, self.global_args['--profile'], self.global_args['--type'] ) log.info('--> loaded %s', self.config_file) if self.xml_state.build_type: log.info( '--> Selected build type: %s', self.xml_state.get_build_type_name() ) if self.xml_state.profiles: log.info( '--> Selected profiles: %s', ','.join(self.xml_state.profiles) ) self.runtime_checker = RuntimeChecker(self.xml_state) def quadruple_token( self, option: str ) -> List[Union[bool, str, List[str], None]]: """ Helper method for commandline options of the form --option a,b,c,d Make sure to provide a common result for option values which separates the information in a comma separated list of values :param str option: comma separated option string :return: common option value representation :rtype: list """ return self._ntuple_token(option, 4) def eleventuple_token( self, option: str ) -> List[Union[bool, str, List[str], None]]: """ Helper method for commandline options of the form --option a,b,c,d,e,f,g,h,i,j,k Make sure to provide a common result for option values which separates the information in a comma separated list of values :param str option: comma separated option string :return: common option value representation :rtype: list """ return self._ntuple_token(option, 11) def attr_token( self, option: str ) -> List[Union[bool, str, List[str], None]]: """ Helper method for commandline options of the form --option attribute=value :param str option: attribute=value string :return: common option value representation :rtype: list """ return self._ntuple_token(option, 2, '=') def run_checks(self, checks: Dict[str, List[str]]) -> None: """ This method runs the given runtime checks excluding the ones disabled in the runtime configuration file. :param dict checks: A dictionary with the runtime method names as keys and their arguments list as the values. """ exclude_list = self.runtime_config.get_disabled_runtime_checks() if self.runtime_checker is not None: for method, args in { key: value for key, value in checks.items() if key not in exclude_list }.items(): attrgetter(method)(self.runtime_checker)(*args) def _pop_token(self, tokens: List[str]) -> Union[bool, str, List[str]]: token = tokens.pop(0) if len(token) > 0 and token == 'true': return True elif len(token) > 0 and token == 'false': return False elif len(token) > 0 and token.startswith('{'): return token.replace('{', '').replace('}', '').split(';') else: return token def _ntuple_token( self, option: str, tuple_count: int, separator: str = ',' ) -> List[Union[bool, str, List[str], None]]: """ Helper method for commandline options of the form --option a,b,c,d,e,f Make sure to provide a common result for option values which separates the information in a comma separated list of values :param str option: comma separated option string :param int tuple_count: divide into tuple_count tuples :return: common option value representation :rtype: list """ tokens = option.split(separator, tuple_count - 1) if option else [] return [ self._pop_token(tokens) if len(tokens) else None for _ in range( 0, tuple_count ) ] ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/image_info.py0000644000000000000000000002064515015277265014331 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 # """ usage: kiwi-ng image info -h | --help kiwi-ng image info --description= [--resolve-package-list] [--list-profiles] [--print-kiwi-env] [--ignore-repos] [--add-repo=...] [--print-xml|--print-yaml|--print-toml] kiwi-ng image info help commands: info provide information about the specified image description options: --add-repo= add repository with given source, type, alias and priority --description= the description must be a directory containing a kiwi XML description and optional metadata files --ignore-repos ignore all repos from the XML configuration --resolve-package-list solve package dependencies and return a list of all packages including their attributes e.g size, shasum, etc... --list-profiles list profiles available for the selected/default type --print-kiwi-env print kiwi profile environment variables --print-xml|--print-yaml|--print-toml print image description in specified format """ import os # project from kiwi.tasks.base import CliTask from kiwi.help import Help from kiwi.utils.output import DataOutput from kiwi.solver.sat import Sat from kiwi.solver.repository import SolverRepository from kiwi.solver.repository.base import SolverRepositoryBase from kiwi.system.uri import Uri from kiwi.system.profile import Profile from kiwi.defaults import Defaults class ImageInfoTask(CliTask): """ Implements retrieval of in depth information for an image description Attributes * :attr:`manual` Instance of Help """ def process(self): """ Walks through the given info options and provide the requested data """ self.manual = Help() if self.command_args.get('help') is True: return self.manual.show('kiwi::image::info') self.load_xml_description( self.command_args['--description'], self.global_args['--kiwi-file'] ) if self.command_args['--ignore-repos']: self.xml_state.delete_repository_sections() if self.command_args['--add-repo']: for add_repo in self.command_args['--add-repo']: (repo_source, repo_type, repo_alias, repo_prio) = \ self.quadruple_token(add_repo) self.xml_state.add_repository( repo_source, repo_type, repo_alias, repo_prio ) self.runtime_checker.check_repositories_configured() result = { 'image': self.xml_state.xml_data.get_name() } if self.command_args['--print-kiwi-env']: profile = Profile(self.xml_state) defaults = Defaults() defaults.to_profile(profile) result['kiwi_env'] = profile.get_settings() if self.command_args['--list-profiles']: result['profile_names'] = [] for profiles_section in self.xml_state.xml_data.get_profiles(): for profile in profiles_section.get_profile(): result['profile_names'].append(profile.get_name()) if self.command_args['--resolve-package-list']: solver = self._setup_solver() boostrap_package_list = self.xml_state.get_bootstrap_packages() package_list = boostrap_package_list + \ self.xml_state.get_system_packages() bootstrap_collection_type = \ self.xml_state.get_bootstrap_collection_type() system_collection_type = \ self.xml_state.get_system_collection_type() bootstrap_packages = solver.solve( boostrap_package_list, False, True if bootstrap_collection_type == 'onlyRequired' else False ) solved_packages = solver.solve( self.xml_state.get_system_packages(), False, True if system_collection_type == 'onlyRequired' else False ) solved_packages.update(bootstrap_packages) package_info = {} for package, metadata in sorted(list(solved_packages.items())): if package in package_list: status = 'listed_in_kiwi_description' else: status = 'added_by_dependency_solver' package_info[package] = { 'source': metadata.uri, 'installsize_bytes': int(metadata.installsize_bytes), 'arch': metadata.arch, 'version': metadata.version, 'status': status } result['resolved-packages'] = package_info if self.global_args['--color-output']: DataOutput(result, style='color').display() else: DataOutput(result).display() if self.command_args['--print-xml']: DataOutput.display_file( self.description.markup.get_xml_description(), 'Description(XML):' ) elif self.command_args['--print-yaml']: DataOutput.display_file( self.description.markup.get_yaml_description(), 'Description(YAML):' ) elif self.command_args['--print-toml']: DataOutput.display_file( self.description.markup.get_toml_description(), 'Description(TOML):' ) def _setup_solver(self): solver = Sat() for xml_repo in self.xml_state.get_repository_sections_used_for_build(): repo_source = xml_repo.get_source().get_path() repo_sourcetype = xml_repo.get_sourcetype() or '' repo_user = xml_repo.get_username() repo_secret = xml_repo.get_password() repo_type = xml_repo.get_type() repo_dist = xml_repo.get_distribution() repo_components = xml_repo.get_components() if not repo_type: repo_type = SolverRepositoryBase( Uri(uri=repo_source, source_type=repo_sourcetype), repo_user, repo_secret ).get_repo_type() if repo_type == 'apt-deb': # Debian based repos can be setup for a specific # distribution including a list of individual components. # For each component of the selected distribution extra # repository metadata exists. In such a case we iterate # over the configured dist components and add them as # repository each. dist_type = solver.set_dist_type('deb') if repo_components and repo_dist: for component in repo_components.split(): repo_source_for_component = os.sep.join( [ repo_source.rstrip(os.sep), 'dists', repo_dist, component, f'binary-{dist_type.get("arch")}' ] ) solver.add_repository( SolverRepository.new( Uri( repo_source_for_component, repo_type, repo_sourcetype ), repo_user, repo_secret ) ) continue solver.add_repository( SolverRepository.new( Uri(repo_source, repo_type, repo_sourcetype), repo_user, repo_secret ) ) return solver ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/image_resize.py0000644000000000000000000001106715015277265014675 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 # """ usage: kiwi-ng image resize -h | --help kiwi-ng image resize --target-dir= --size= [--root=] kiwi-ng image resize help commands: resize 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 options: --root= the path to the root directory, if not specified kiwi searches the root directory in build/image-root below the specified target directory --size= 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 --target-dir= the target directory to expect image build results """ import os import logging # project from kiwi.firmware import FirmWare from kiwi.storage.loop_device import LoopDevice from kiwi.partitioner import Partitioner from kiwi.tasks.base import CliTask from kiwi.help import Help from kiwi.storage.subformat import DiskFormat from kiwi.utils.size import StringToSize from kiwi.exceptions import ( KiwiImageResizeError ) log = logging.getLogger('kiwi') class ImageResizeTask(CliTask): """ Implements resizing of disk images and their disk format Attributes * :attr:`manual` Instance of Help """ def process(self): """ reformats raw disk image and its format to a new disk geometry using the qemu tool chain """ self.manual = Help() if self.command_args.get('help') is True: return self.manual.show('kiwi::image::resize') abs_target_dir_path = os.path.abspath( self.command_args['--target-dir'] ) if self.command_args['--root']: image_root = os.path.abspath( os.path.normpath(self.command_args['--root']) ) else: image_root = os.sep.join( [abs_target_dir_path, 'build', 'image-root'] ) self.load_xml_description( image_root, self.global_args['--kiwi-file'] ) disk_format = self.xml_state.build_type.get_format() with DiskFormat.new( disk_format or 'raw', self.xml_state, image_root, abs_target_dir_path ) as image_format: if not image_format.has_raw_disk(): raise KiwiImageResizeError( 'no raw disk image {0} found in build results'.format( image_format.diskname ) ) new_disk_size = StringToSize.to_bytes(self.command_args['--size']) # resize raw disk log.info( f'Resizing raw disk to {new_disk_size} bytes' ) resize_result = image_format.resize_raw_disk(new_disk_size) # resize raw disk partition table firmware = FirmWare(self.xml_state) with LoopDevice(image_format.diskname) as loop_provider: loop_provider.create(overwrite=False) partitioner = Partitioner.new( firmware.get_partition_table_type(), loop_provider ) partitioner.resize_table() # resize disk format from resized raw disk if disk_format and resize_result is True: log.info( f'Creating {disk_format} disk format from resized raw disk' ) image_format.create_image_format() elif resize_result is False: log.info( f'Raw disk is already at {new_disk_size} bytes' ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/result_bundle.py0000644000000000000000000003616515015277265015107 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 # """ usage: kiwi-ng result bundle -h | --help kiwi-ng result bundle --target-dir= --id= --bundle-dir= [--bundle-format=] [--zsync-source=] [--package-as-rpm] [--no-compress] kiwi-ng result bundle help commands: bundle 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. options: --bundle-dir= directory to store the bundle results --id= the bundle id. A free form text appended to the version information of the result image filename --target-dir= the target directory to expect image build results --zsync-source= 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 --package-as-rpm Take all result files and create an rpm package out of it --bundle-format= specify the bundle format to create the bundle. If provided this setting will overwrite an eventually provided bundle_format attribute from the main image description --no-compress Do not compress the result image file(s) """ from collections import OrderedDict from textwrap import dedent from typing import List import logging import glob import os # project from kiwi.tasks.base import CliTask from kiwi.help import Help from kiwi.system.result import ( Result, result_file_type ) from kiwi.path import Path from kiwi.utils.compress import Compress from kiwi.utils.checksum import Checksum from kiwi.privileges import Privileges from kiwi.command import Command from kiwi.exceptions import ( KiwiBundleError ) log = logging.getLogger('kiwi') class ResultBundleTask(CliTask): """ Implements result bundler Attributes * :attr:`manual` Instance of Help """ def process(self): """ 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 """ self.manual = Help() if self._help(): return if self.command_args['--package-as-rpm']: Privileges.check_for_root_permissions() # load serialized result object from target directory result_directory = os.path.abspath(self.command_args['--target-dir']) bundle_directory = os.path.abspath(self.command_args['--bundle-dir']) if result_directory == bundle_directory: raise KiwiBundleError( 'Bundle directory must be different from target directory' ) log.info( 'Bundle build results from %s', result_directory ) result = Result.load( result_directory + '/kiwi.result' ) if self.command_args['--bundle-format']: result.add_bundle_format(self.command_args['--bundle-format']) image_version = result.xml_state.get_image_version() image_name = result.xml_state.xml_data.get_name() image_description = result.xml_state.get_description_section() ordered_results = OrderedDict(sorted(result.get_results().items())) # hard link bundle files, compress and build checksum if self.command_args['--package-as-rpm']: Path.wipe(bundle_directory) if not os.path.exists(bundle_directory): Path.create(bundle_directory) bundle_file_format_name = '' if 'bundle_format' in ordered_results: bundle_format = ordered_results['bundle_format'] tags = bundle_format['tags'] bundle_file_format_name = bundle_format['pattern'] # Insert image name bundle_file_format_name = bundle_file_format_name.replace( '%N', tags.N ) # Insert Concatenated profile name (_) bundle_file_format_name = bundle_file_format_name.replace( '%P', tags.P ) # Insert Architecture name bundle_file_format_name = bundle_file_format_name.replace( '%A', tags.A ) # Insert Image build type name bundle_file_format_name = bundle_file_format_name.replace( '%T', tags.T ) # Insert Image Major version number bundle_file_format_name = bundle_file_format_name.replace( '%M', format(tags.M) ) # Insert Image Minor version number bundle_file_format_name = bundle_file_format_name.replace( '%m', format(tags.m) ) # Insert Image Patch version number bundle_file_format_name = bundle_file_format_name.replace( '%p', format(tags.p) ) # Insert Version string bundle_file_format_name = bundle_file_format_name.replace( '%v', format(tags.v) ) # Insert Bundle ID bundle_file_format_name = bundle_file_format_name.replace( '%I', self.command_args['--id'] ) del ordered_results['bundle_format'] # copy result files origin_files = [] copied_files = [] for result_file in list(ordered_results.values()): if result_file.use_for_bundle: bundle_file_basename = self._get_bundle_file_basename( result, result_file, bundle_file_format_name ) log.info('Creating %s', bundle_file_basename) bundle_file = ''.join( [bundle_directory, '/', bundle_file_basename] ) Command.run( [ 'cp', result_file.filename, bundle_file ] ) copied_files.append(bundle_file) result_file_basename = os.path.basename(result_file.filename) if result_file_basename != bundle_file_basename: symlink_orignal_name = f'{bundle_directory}/{result_file_basename}' if os.path.islink(symlink_orignal_name): os.unlink(symlink_orignal_name) os.symlink(bundle_file, symlink_orignal_name) origin_files.append(symlink_orignal_name) # check and fix file references due to possible bundle_format renames for origin_file in origin_files: origin_file_basename = os.path.basename(origin_file) bundle_file_basename = os.path.basename(os.readlink(origin_file)) log.info(f'Checking file references for {origin_file_basename}') for result_file in copied_files: if 'text' in Command.run(['file', result_file]).output: log.info(f'--> {result_file}') Command.run( [ 'sed', '-ie', f's/{origin_file_basename}/{bundle_file_basename}/g', result_file ] ) os.unlink(f'{result_file}e') os.unlink(origin_file) # finalize result data, checksums, compressions for result_file in list(ordered_results.values()): if result_file.use_for_bundle: bundle_file_basename = self._get_bundle_file_basename( result, result_file, bundle_file_format_name ) log.info(f'Finalizing {bundle_file_basename}') bundle_file = ''.join( [bundle_directory, '/', bundle_file_basename] ) if result_file.compress and not self.command_args['--no-compress']: log.info('--> Compressing') compress = Compress(bundle_file) bundle_file = compress.xz(self.runtime_config.get_xz_options()) if self.command_args['--zsync-source'] and result_file.shasum: # Files with a checksum are considered to be image files # and are therefore eligible to be provided via the # requested Partial/differential file download based on # zsync zsyncmake = Path.which('zsyncmake', access_mode=os.X_OK) if zsyncmake: log.info('--> Creating zsync control file') Command.run( [ zsyncmake, '-e', '-u', os.sep.join( [ self.command_args['--zsync-source'], os.path.basename(bundle_file) ] ), '-o', bundle_file + '.zsync', bundle_file ] ) else: log.warning( '--> zsyncmake missing, zsync setup skipped' ) if result_file.shasum: log.info('--> Creating SHA 256 sum') checksum = Checksum(bundle_file) with open(bundle_file + '.sha256', 'w') as shasum: shasum.write( '{0} {1}{2}'.format( checksum.sha256(), os.path.basename(bundle_file), os.linesep ) ) if self.command_args['--package-as-rpm']: ResultBundleTask._build_rpm_package( bundle_directory, bundle_file_format_name or image_name, image_version, image_description.specification, list(glob.iglob(f'{bundle_directory}/*')) ) def _get_bundle_file_basename( self, result: Result, result_file: result_file_type, bundle_file_format_name: str ) -> str: image_version = result.xml_state.get_image_version() image_name = result.xml_state.xml_data.get_name() special_exts = ['.oci.tar.', '.docker.tar.', '.vagrant.'] if any(special_ext in result_file.filename for special_ext in special_exts): extension = f'{".".join(result_file.filename.split(".")[-3:])}' elif '.tar.' in result_file.filename: extension = f'tar.{result_file.filename.split(".").pop()}' else: extension = result_file.filename.split('.').pop() if bundle_file_format_name: bundle_file_basename = '.'.join( [bundle_file_format_name, extension] ) else: bundle_file_basename = os.path.basename( result_file.filename ) # The bundle id is only taken into account for image results # which contains the image version appended in its file name part_name = list(bundle_file_basename.partition(image_name)) bundle_file_basename = ''.join( [ part_name[0], part_name[1], part_name[2].replace( image_version, image_version + '-' + self.command_args['--id'] ) ] ) return bundle_file_basename @staticmethod def _build_rpm_package( bundle_directory: str, image_name: str, image_version: str, description_text, filenames: List[str] ) -> None: source_links = [] for source_file in filenames: source_links.append( ' '.join( [ 'ln', '%{_sourcedir}/' + os.path.basename(source_file), '%{buildroot}' + f'/var/tmp/{image_name}' ] ) ) spec_file_name = os.path.join(bundle_directory, f'{image_name}.spec') spec_data = dedent(''' %global _sourcedir %(pwd) %global _rpmdir . Url: https://osinside.github.io/kiwi Name: {name} Summary: {name} Version: {version} Release: 0 Group: %{{sysgroup}} License: GPL-3.0-or-later BuildRoot: %{{_tmppath}}/%{{name}}-%{{version}}-build BuildArch: noarch %description {description} %prep %build %install install -d -m 755 %{{buildroot}}/var/tmp/{name} {source_links} %clean rm -rf %{{buildroot}} %files %defattr(-, root, root) /var/tmp/{name} %changelog ''').format( name=image_name, version=image_version, description=description_text, source_links=os.linesep.join(source_links) ) with open(spec_file_name, 'w') as spec: spec.write(spec_data) os.chdir(bundle_directory) log.info('Creating rpm package...') Command.run( [ 'rpmbuild', '--nodeps', '--nocheck', '--rmspec', '-bb', spec_file_name ] ) for source_file in filenames: os.unlink(source_file) Command.run( [ 'bash', '-c', 'mv noarch/*.rpm . && rmdir noarch' ] ) def _help(self): if self.command_args['help']: self.manual.show('kiwi::result::bundle') else: return False return self.manual ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/result_list.py0000644000000000000000000000371315015277265014602 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 # """ usage: kiwi-ng result list -h | --help kiwi-ng result list --target-dir= kiwi-ng result list help commands: list list result information from a previous system command options: --target-dir= the target directory as it was used in a system command """ import os import logging # project from kiwi.tasks.base import CliTask from kiwi.help import Help from kiwi.system.result import Result log = logging.getLogger('kiwi') class ResultListTask(CliTask): """ Implements result listing Attributes * :attr:`manual` Instance of Help """ def process(self): """ List result information from a previous system command """ self.manual = Help() if self._help(): return result_directory = os.path.abspath( self.command_args['--target-dir'] ) log.info( 'Listing results from %s', result_directory ) result = Result.load( result_directory + '/kiwi.result' ) result.print_results() def _help(self): if self.command_args['help']: self.manual.show('kiwi::result::list') else: return False return self.manual ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1748336309.3601456 kiwi-10.2.24/kiwi/tasks/system_build.py0000644000000000000000000003710615015277265014737 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 # """ usage: kiwi-ng system build -h | --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=...] [--delete-package=...] [--set-container-derived-from=] [--set-container-tag=] [--add-container-label=