diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/COPYING @@ -0,0 +1,674 @@ + 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 +. diff --git a/Makefile b/Makefile index 952372e..0992fee 100644 --- a/Makefile +++ b/Makefile @@ -1,36 +1,44 @@ -SRC = compute/ -DIST = dist/ -DOCS_SRC = docs/source/ -DOCS_BUILD = docs/build/ +SRCDIR = compute +DISTDIR = dist +DOCS_SRCDIR = docs/source +DOCS_BUILDDIR = docs/build .PHONY: docs all: build +requirements.txt: + poetry export -f requirements.txt -o requirements.txt + build: format lint + awk '/^version/{print $$3}' pyproject.toml \ + | xargs -I {} sed "s/__version__ =.*/__version__ = '{}'/" -i $(SRCDIR)/__init__.py poetry build +build-deb: build + cd packaging && $(MAKE) + format: - poetry run isort $(SRC) - poetry run ruff format $(SRC) + poetry run isort $(SRCDIR) + poetry run ruff format $(SRCDIR) lint: - poetry run ruff check $(SRC) + poetry run ruff check $(SRCDIR) docs: - poetry run sphinx-build $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-build $(DOCS_SRCDIR) $(DOCS_BUILDDIR) docs-versions: - poetry run sphinx-multiversion $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-multiversion $(DOCS_SRCDIR) $(DOCS_BUILDDIR) serve-docs: - poetry run sphinx-autobuild $(DOCS_SRC) $(DOCS_BUILD) + poetry run sphinx-autobuild $(DOCS_SRCDIR) $(DOCS_BUILDDIR) clean: - [ -d $(DIST) ] && rm -rf $(DIST) || true - [ -d $(DOCS_BUILD) ] && rm -rf $(DOCS_BUILD) || true + [ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true + [ -d $(DOCS_BUILDDIR) ] && rm -rf $(DOCS_BUILDDIR) || true find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true + cd packaging && $(MAKE) clean -test-build: - poetry build - scp $(DIST)/*.tar.gz vm:~ +test-build: build-deb + scp packaging/build/compute*.deb vm:~ diff --git a/README.md b/README.md index 723b03c..d2236a4 100644 --- a/README.md +++ b/README.md @@ -49,3 +49,107 @@ Install [poetry](https://python-poetry.org/), clone this repository and run: ``` poetry install --with dev --with docs ``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. + +# Installation + +Packages can be installed via `dpkg` or `apt-get`: + +``` +# apt-get install ./compute*.deb +``` + +After installation prepare environment, run following command to start libvirtd and create required storage pools: + +``` +# systemctl enable --now libvirtd.service +# virsh net-start default +# virsh net-autostart default +# for pool in images volumes; do + virsh pool-define-as $pool dir - - - - "/$pool" + virsh pool-build $pool + virsh pool-start $pool +done +``` + +Then set environment variables in your `~/.profile`, `~/.bashrc` or global in `/etc/profile.d/compute` or `/etc/bash.bashrc`: + +``` +export CMP_IMAGES_POOL=images +export CMP_VOLUMES_POOL=volumes +``` + +Configuration file is yet not supported. + +Make sure the variables are exported to the environment: + +``` +printenv | grep CMP_ +``` + +If the command didn't show anything _source_ your rc files or relogin. + + +# Basic usage + +To get help run: + +``` +compute --help +``` + +Also you can use `compute` as generic Python library. For example: + +```python +from compute import Session + +with Session() as session: + instance = session.get_instance('myinstance') + if not instance.is_running(): + instance.start() + else: + print('instance is already running') +``` + +# Create compute instances + +Place your qcow2 image in `/volumes` directory. For example `debian_12.qcow2`. + +Create `instance.yaml` file with following content: + +```yaml +name: myinstance +memory: 2048 # memory in MiB +vcpus: 2 +image: debian_12.qcow2 +volumes: + - type: file + is_system: true + target: vda + capacity: + value: 10 + unit: GiB +``` + +Refer to `Instance` class docs for more info. Full `instance.yaml` example will be provided later. + +To initialise instance run: + +``` +compute -l debug init instance.yaml +``` + +Start instance: + +``` +compute start myinstance +``` diff --git a/compute/__init__.py b/compute/__init__.py index 07940b8..ffe06d7 100644 --- a/compute/__init__.py +++ b/compute/__init__.py @@ -1,6 +1,21 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Compute instances management library.""" -__version__ = '0.1.0' +__version__ = '0.1.0-dev1' from .instance import Instance, InstanceConfig, InstanceSchema from .session import Session diff --git a/compute/__main__.py b/compute/__main__.py index c6467ef..4995fbd 100644 --- a/compute/__main__.py +++ b/compute/__main__.py @@ -1,6 +1,21 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Command line interface for compute module.""" -from compute.cli import control +from compute.cli import main -control.cli() +main.cli() diff --git a/compute/cli/control.py b/compute/cli/control.py index 93ad959..f5a5b91 100644 --- a/compute/cli/control.py +++ b/compute/cli/control.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Command line interface.""" import argparse @@ -15,10 +30,7 @@ import yaml from pydantic import ValidationError from compute import __version__ -from compute.exceptions import ( - ComputeServiceError, - GuestAgentTimeoutExceededError, -) +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError from compute.instance import GuestAgent from compute.session import Session from compute.utils import ids @@ -198,10 +210,23 @@ def _create_instance(session: Session, file: io.TextIOWrapper) -> None: sys.exit() +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + def main(session: Session, args: argparse.Namespace) -> None: """Perform actions.""" match args.command: - case 'create': + case 'init': _create_instance(session, args.file) case 'exec': _exec_guest_agent_command(session, args) @@ -211,14 +236,16 @@ def main(session: Session, args: argparse.Namespace) -> None: instance = session.get_instance(args.instance) instance.start() case 'shutdown': - instance = session.get_instance(args.instance) - instance.shutdown(args.method) + _shutdown_instance(session, args) case 'reboot': instance = session.get_instance(args.instance) instance.reboot() case 'reset': instance = session.get_instance(args.instance) instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() case 'pause': instance = session.get_instance(args.instance) instance.pause() @@ -234,7 +261,7 @@ def main(session: Session, args: argparse.Namespace) -> None: case 'setmem': instance = session.get_instance(args.instance) instance.set_memory(args.memory, live=True) - case 'setpasswd': + case 'setpass': instance = session.get_instance(args.instance) instance.set_user_password( args.username, @@ -261,7 +288,6 @@ def cli() -> None: # noqa: PLR0915 '-c', '--connect', metavar='URI', - default='qemu:///system', help='libvirt connection URI', ) root.add_argument( @@ -270,7 +296,7 @@ def cli() -> None: # noqa: PLR0915 type=str.lower, metavar='LEVEL', choices=log_levels, - help='log level [envvar: CMP_LOG]', + help='log level', ) root.add_argument( '-V', @@ -280,13 +306,16 @@ def cli() -> None: # noqa: PLR0915 ) subparsers = root.add_subparsers(dest='command', metavar='COMMAND') - # create command - create = subparsers.add_parser( - 'create', help='create new instance from YAML config file' + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' ) - create.add_argument( + init.add_argument( 'file', type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', ) # exec subcommand @@ -307,14 +336,14 @@ def cli() -> None: # noqa: PLR0915 default=60, help=( 'waiting time in seconds for a command to be executed ' - 'in guest, 60 sec by default' + 'in guest [default: 60]' ), ) execute.add_argument( '-x', '--executable', default='/bin/sh', - help='path to executable in guest, /bin/sh by default', + help='path to executable in guest [default: /bin/sh]', ) execute.add_argument( '-e', @@ -352,12 +381,36 @@ def cli() -> None: # noqa: PLR0915 # shutdown subcommand shutdown = subparsers.add_parser('shutdown', help='shutdown instance') shutdown.add_argument('instance') - shutdown.add_argument( - '-m', - '--method', - choices=['soft', 'normal', 'hard', 'unsafe'], - default='normal', - help='use shutdown method', + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), ) # reboot subcommand @@ -368,6 +421,10 @@ def cli() -> None: # noqa: PLR0915 reset = subparsers.add_parser('reset', help='reset instance') reset.add_argument('instance') + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + # pause subcommand pause = subparsers.add_parser('pause', help='pause instance') pause.add_argument('instance') @@ -390,15 +447,15 @@ def cli() -> None: # noqa: PLR0915 setmem.add_argument('instance') setmem.add_argument('memory', type=int, help='memory in MiB') - # setpasswd subcommand - setpasswd = subparsers.add_parser( - 'setpasswd', + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', help='set user password in guest', ) - setpasswd.add_argument('instance') - setpasswd.add_argument('username') - setpasswd.add_argument('password') - setpasswd.add_argument( + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( '-e', '--encrypted', action='store_true', @@ -419,10 +476,18 @@ def cli() -> None: # noqa: PLR0915 ) log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + try: - with Session(args.connect) as session: + with Session(connect_uri) as session: main(session, args) - except ComputeServiceError as e: + except ComputeError as e: sys.exit(f'error: {e}') except KeyboardInterrupt: sys.exit() diff --git a/compute/common.py b/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/compute/exceptions.py b/compute/exceptions.py index 0528afd..1eef8de 100644 --- a/compute/exceptions.py +++ b/compute/exceptions.py @@ -1,19 +1,34 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Exceptions.""" -class ComputeServiceError(Exception): - """Basic exception class for Compute.""" +class ComputeError(Exception): + """Basic exception class.""" -class ConfigLoaderError(ComputeServiceError): +class ConfigLoaderError(ComputeError): """Something went wrong when loading configuration.""" -class SessionError(ComputeServiceError): +class SessionError(ComputeError): """Something went wrong while connecting to libvirtd.""" -class GuestAgentError(ComputeServiceError): +class GuestAgentError(ComputeError): """Something went wring when QEMU Guest Agent call.""" @@ -33,7 +48,7 @@ class GuestAgentCommandNotSupportedError(GuestAgentError): """Guest agent command is not supported or blacklisted on guest.""" -class StoragePoolError(ComputeServiceError): +class StoragePoolError(ComputeError): """Something went wrong when operating with storage pool.""" @@ -53,7 +68,7 @@ class VolumeNotFoundError(StoragePoolError): super().__init__(f"storage volume '{msg}' not found") -class InstanceError(ComputeServiceError): +class InstanceError(ComputeError): """Something went wrong while interacting with the domain.""" diff --git a/compute/instance/__init__.py b/compute/instance/__init__.py index 100c1c5..6e2b150 100644 --- a/compute/instance/__init__.py +++ b/compute/instance/__init__.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + from .guest_agent import GuestAgent from .instance import Instance, InstanceConfig from .schemas import InstanceSchema diff --git a/compute/instance/guest_agent.py b/compute/instance/guest_agent.py index 5355ae3..4381591 100644 --- a/compute/instance/guest_agent.py +++ b/compute/instance/guest_agent.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Interacting with the QEMU Guest Agent.""" import json diff --git a/compute/instance/instance.py b/compute/instance/instance.py index 143caed..5b806e6 100644 --- a/compute/instance/instance.py +++ b/compute/instance/instance.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Manage compute instances.""" __all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] @@ -9,6 +24,7 @@ import libvirt from lxml import etree from lxml.builder import E +from compute.common import DeviceConfig, EntityConfig from compute.exceptions import ( GuestAgentCommandNotSupportedError, InstanceError, @@ -28,8 +44,8 @@ from .schemas import ( log = logging.getLogger(__name__) -class InstanceConfig: - """Compute instance config builder.""" +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" def __init__(self, schema: InstanceSchema): """ @@ -181,10 +197,6 @@ class InstanceInfo(NamedTuple): cputime: int -class DeviceConfig: - """Abstract device config class.""" - - class Instance: """Manage compute instances.""" @@ -492,7 +504,7 @@ class Instance: return child[0].getparent() if child else None def attach_device( - self, device: 'DeviceConfig', *, live: bool = False + self, device: DeviceConfig, *, live: bool = False ) -> None: """ Attach device to compute instance. @@ -517,7 +529,7 @@ class Instance: self.domain.attachDeviceFlags(device.to_xml(), flags=flags) def detach_device( - self, device: 'DeviceConfig', *, live: bool = False + self, device: DeviceConfig, *, live: bool = False ) -> None: """ Dettach device from compute instance. @@ -545,8 +557,8 @@ class Instance: """ Detach disk device by target name. - There is no ``attach_disk()`` method. Use :method:`attach_device` - with :class:`DiskConfig` as parameter. + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. :param name: Disk name e.g. 'vda', 'sda', etc. This name may not match the name of the disk inside the guest OS. @@ -574,14 +586,14 @@ class Instance: raise InstanceError(msg) self.detach_device(DiskConfig(**disk_params), live=True) - def resize_volume( + def resize_disk( self, name: str, capacity: int, unit: units.DataUnit ) -> None: """ Resize attached block device. :param name: Disk device name e.g. `vda`, `sda`, etc. - :param capacity: New volume capacity. + :param capacity: New capacity. :param unit: Capacity unit. """ self.domain.blockResize( @@ -590,6 +602,10 @@ class Instance: flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, ) + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + def pause(self) -> None: """Pause instance.""" if not self.is_running(): @@ -600,9 +616,9 @@ class Instance: """Resume paused instance.""" self.domain.resume() - def list_ssh_keys(self, user: str) -> list[str]: + def get_ssh_keys(self, user: str) -> list[str]: """ - Get list of SSH keys on guest for specific user. + Return list of SSH keys on guest for specific user. :param user: Username. """ @@ -617,7 +633,7 @@ class Instance: """ raise NotImplementedError - def remove_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: """ Remove SSH keys from guest for specific user. @@ -632,7 +648,7 @@ class Instance: """ Set new user password in guest OS. - This action performs by guest agent inside guest. + This action performs by guest agent inside the guest. :param user: Username. :param password: Password. @@ -653,6 +669,7 @@ class Instance: return self.domain.XMLDesc(flags) def delete(self) -> None: - """Undefine instance and delete local volumes.""" + """Undefine instance.""" + # TODO @ge: delete local disks self.shutdown(method='HARD') self.domain.undefine() diff --git a/compute/instance/schemas.py b/compute/instance/schemas.py index 684a72d..f5a677c 100644 --- a/compute/instance/schemas.py +++ b/compute/instance/schemas.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Compute instance related objects schemas.""" import re diff --git a/compute/session.py b/compute/session.py index 7506fc5..de5f900 100644 --- a/compute/session.py +++ b/compute/session.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Hypervisor session manager.""" import logging diff --git a/compute/storage/__init__.py b/compute/storage/__init__.py index 5090edd..34aae30 100644 --- a/compute/storage/__init__.py +++ b/compute/storage/__init__.py @@ -1,2 +1,17 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + from .pool import StoragePool from .volume import DiskConfig, Volume, VolumeConfig diff --git a/compute/storage/pool.py b/compute/storage/pool.py index 0a11d3a..cb17494 100644 --- a/compute/storage/pool.py +++ b/compute/storage/pool.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Manage storage pools.""" import logging diff --git a/compute/storage/volume.py b/compute/storage/volume.py index b7dfaea..11a1dc4 100644 --- a/compute/storage/volume.py +++ b/compute/storage/volume.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Manage storage volumes.""" from dataclasses import dataclass @@ -8,13 +23,14 @@ import libvirt from lxml import etree from lxml.builder import E +from compute.common import DeviceConfig, EntityConfig from compute.utils import units @dataclass -class VolumeConfig: +class VolumeConfig(EntityConfig): """ - Storage volume config builder. + Storage volume XML config builder. Generate XML config for creating a volume in a libvirt storage pool. @@ -48,9 +64,9 @@ class VolumeConfig: @dataclass -class DiskConfig: +class DiskConfig(DeviceConfig): """ - Disk config builder. + Disk XML config builder. Generate XML config for attaching or detaching storage volumes to compute instances. diff --git a/compute/utils/config_loader.py b/compute/utils/config_loader.py index 5763d03..aaeb0fe 100644 --- a/compute/utils/config_loader.py +++ b/compute/utils/config_loader.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Configuration loader.""" import tomllib diff --git a/compute/utils/ids.py b/compute/utils/ids.py index 335017f..8a6454a 100644 --- a/compute/utils/ids.py +++ b/compute/utils/ids.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Random identificators.""" # ruff: noqa: S311, C417 diff --git a/compute/utils/units.py b/compute/utils/units.py index 7e4b632..57a4583 100644 --- a/compute/utils/units.py +++ b/compute/utils/units.py @@ -1,3 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + """Tools for data units convertion.""" from enum import StrEnum diff --git a/docs/source/conf.py b/docs/source/conf.py index c2c53c3..38e4a1f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ -# Add ../../compute to path for autodoc +# Add ../.. to path for autodoc Sphinx extension import os import sys -sys.path.insert(0, os.path.abspath('../../compute')) +sys.path.insert(0, os.path.abspath('../..')) # Project information project = 'Compute' diff --git a/packaging/Dockerfile b/packaging/Dockerfile new file mode 100644 index 0000000..4659618 --- /dev/null +++ b/packaging/Dockerfile @@ -0,0 +1,23 @@ +FROM debian:bookworm-slim +WORKDIR /mnt/build +RUN apt-get update; \ + env DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends -y \ + build-essential \ + bash-completion \ + debhelper \ + quilt \ + dh-make \ + dh-python \ + pybuild-plugin-pyproject \ + python3-poetry-core \ + python3-all \ + python3-setuptools \ + python3-sphinx \ + python3-sphinx-multiversion \ + python3-libvirt \ + python3-lxml \ + python3-yaml \ + python3-pydantic; \ + apt clean; \ + echo "alias ll='ls -alFh'" >> /etc/bash.bashrc +USER 1000:1000 diff --git a/packaging/Makefile b/packaging/Makefile new file mode 100644 index 0000000..d92de39 --- /dev/null +++ b/packaging/Makefile @@ -0,0 +1,24 @@ +DOCKER_CMD ?= docker +DOCKER_IMG = pybuilder:bookworm +DEBBUILDDIR = build + +all: docker-build build + +clean: + test -d $(DEBBUILDDIR) && rm -rf $(DEBBUILDDIR) || true + +docker-build: + $(DOCKER_CMD) build -f Dockerfile -t $(DOCKER_IMG) . + +build: clean + mkdir -p $(DEBBUILDDIR) + cp -v ../dist/compute-*[.tar.gz] $(DEBBUILDDIR)/ + cp -r ../docs $(DEBBUILDDIR)/ + if [ -f build.sh.bak ]; then mv build.sh{.bak,}; fi + cp build.sh{,.bak} + awk '/authors/{gsub(/[\[\]]/,"");print $$3" "$$4}' ../pyproject.toml \ + | sed "s/['<>]//g" \ + | tr ' ' '\n' \ + | xargs -I {} sed "0,/%placeholder%/s//{}/" -i build.sh + $(DOCKER_CMD) run --rm -i -v $$PWD:/mnt $(DOCKER_IMG) bash < build.sh + mv build.sh{.bak,} diff --git a/packaging/build.sh b/packaging/build.sh new file mode 100644 index 0000000..575c5d5 --- /dev/null +++ b/packaging/build.sh @@ -0,0 +1,15 @@ +set -o errexit +set -o xtrace +export USER=build +export HOME=/mnt +export DEBFULLNAME='%placeholder%' +export DEBEMAIL='%placeholder%' +mkdir -p /mnt/build && cd /mnt/build +tar xf compute-*[.tar.gz] +cd compute-*[^.tar.gz] +sed -e "s%\.\./\.\.%$PWD%" -i ../docs/source/conf.py +dh_make --copyright gpl3 --yes --python --file ../compute-*[.tar.gz] +rm debian/*.ex debian/README.{Debian,source} debian/*.docs +sed -e 's/\* Initial release.*/\* This is the development build, see commits in upstream repo for info./' -i debian/changelog +cp -v ../../files/{control,rules,copyright,docs,compute.bash-completion} debian/ +dpkg-buildpackage -us -uc diff --git a/packaging/build/compute-0.1.0.dev1.tar.gz b/packaging/build/compute-0.1.0.dev1.tar.gz new file mode 100644 index 0000000..fcb6882 Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1.tar.gz differ diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/METADATA @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD new file mode 100644 index 0000000..5f97163 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/RECORD @@ -0,0 +1,23 @@ +../scripts/compute,sha256=b-Gj6H6ssfbGalpouUMSX5pmsjqDnN9xMdTwnU-UfZY,216 +compute/__init__.py,sha256=x4zp_CoVPKgDT6AqhometspAyinGxJUXO48duJ5aHUM,873 +compute/__main__.py,sha256=zJyKJul6pCbguFPtVLZBoAuZl9RXibn4CCMn46jIgUQ,745 +compute/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/cli/control.py,sha256=83wnR21pHOPyyk1i1n_YBIDz6dCFB6hmuIFguIk68rs,14634 +compute/common.py,sha256=G1qwC1EybG5LEJtyoux9ymiqB2ZOsgKXlCpbuhHv55Y,948 +compute/exceptions.py,sha256=Ga59L55qSAPeyDfjANPuMh4yVSRWHDYi9xqq5o4_7-0,2452 +compute/instance/__init__.py,sha256=kHN8jVamyrBZYZgi62tPtJ7rS73gUPhfswLalmPA5Zs,772 +compute/instance/guest_agent.py,sha256=fq89kQbcV5X5eFCsMmujRuwTOSghWO4ZhAjvxyUu84M,7018 +compute/instance/instance.py,sha256=WP6oTJfdAf6QlefwVLqdC8J6XoKHum6nZhwwHOEtjNk,23297 +compute/instance/schemas.py,sha256=B51ytPlxhnx0MrkR2WYhd49RaRT7Is7NsIM9OrMUpvI,4288 +compute/session.py,sha256=znYOIzoiCbSG62k-ViaXti_lOnw88wD8Syp3nCXAJ28,10050 +compute/storage/__init__.py,sha256=zNaVjZ2925DxrVUFWwVRsGU6bSYbF46sb4L6NsaiKbw,736 +compute/storage/pool.py,sha256=9z99bBDbb4ATGpfMkEWpxAO4fEQHNVOxxf0iUln9cN0,4197 +compute/storage/volume.py,sha256=_TbK9Y4d3NAeknPUiuhldAT3ZaN1sZgjy4QzC-Sw4Io,4110 +compute/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/utils/config_loader.py,sha256=ul1J3sZg0D9R0HbOz5Pg9JmL4nFaMahAzQEdGaWFABU,1989 +compute/utils/ids.py,sha256=fg6Xsg4OMM-BIaU3DPu0L91ICwx-L3qNoELEwQZz2s0,1007 +compute/utils/units.py,sha256=UkwD0zQ-rlpSpkbfezCcvJx4D8iZlI9M-oXXvdVEvy0,1549 +compute-0.1.0.dev1.dist-info/METADATA,sha256=tbX8xp92Jwqf44sOwPB-HqKHLezab5dU9DrQDYFitDQ,1944 +compute-0.1.0.dev1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88 +compute-0.1.0.dev1.dist-info/entry_points.txt,sha256=xHhg-Fo9Z5gJnIahbG8pVIGNDqlH5Eordn8hnXUwscw,51 +compute-0.1.0.dev1.dist-info/RECORD,, diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL new file mode 100644 index 0000000..4ba7671 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.4.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt new file mode 100644 index 0000000..4130f9f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute-0.1.0.dev1.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +compute=compute.cli.control:cli + diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/build/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl new file mode 100644 index 0000000..acf2d64 Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/compute-0.1.0.dev1-py3-none-any.whl differ diff --git a/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute new file mode 100755 index 0000000..56e33f2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/.pybuild/cpython3_3.11/scripts/compute @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from compute.cli.control import cli +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(cli()) diff --git a/packaging/build/compute-0.1.0.dev1/PKG-INFO b/packaging/build/compute-0.1.0.dev1/PKG-INFO new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/PKG-INFO @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/README.md b/packaging/build/compute-0.1.0.dev1/README.md new file mode 100644 index 0000000..0131e8e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/README.md @@ -0,0 +1,65 @@ +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` diff --git a/packaging/build/compute-0.1.0.dev1/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/compute/common.py b/packaging/build/compute-0.1.0.dev1/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/compute/session.py b/packaging/build/compute-0.1.0.dev1/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/installed-by-dh_installdocs b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute-doc/installed-by-dh_installdocs new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/dh_installchangelogs.dch.trimmed @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs new file mode 100644 index 0000000..c2dd0c3 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/.debhelper/generated/compute/installed-by-dh_installdocs @@ -0,0 +1 @@ +./README.md diff --git a/packaging/build/compute-0.1.0.dev1/debian/changelog b/packaging/build/compute-0.1.0.dev1/debian/changelog new file mode 100644 index 0000000..bb9efc5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/changelog @@ -0,0 +1,5 @@ +compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + + * This is the development build, see commits in upstream repo for info. + + -- ge Wed, 22 Nov 2023 23:06:43 +0000 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log new file mode 100644 index 0000000..8dc2028 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.debhelper.log @@ -0,0 +1 @@ +dh_sphinxdoc diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars new file mode 100644 index 0000000..c41bfd3 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc.substvars @@ -0,0 +1,4 @@ +sphinxdoc:Depends=libjs-sphinxdoc (>= 1.0), libjs-sphinxdoc (>= 2.4.3-5~), libjs-sphinxdoc (>= 5.0), libjs-sphinxdoc (>= 5.2) +sphinxdoc:Built-Using=alabaster (= 0.7.12-1), sphinx (= 5.3.0-4) +misc:Depends= +misc:Pre-Depends= diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control new file mode 100644 index 0000000..72814c9 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/control @@ -0,0 +1,11 @@ +Package: compute-doc +Source: compute +Version: 0.1.0.dev1-1 +Architecture: all +Maintainer: ge +Installed-Size: 376 +Depends: libjs-sphinxdoc (>= 5.2) +Section: doc +Priority: optional +Homepage: https://git.lulzette.ru/hstack/compute +Description: Compute instances management library and tools (documentation) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums new file mode 100644 index 0000000..5ab5be6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/DEBIAN/md5sums @@ -0,0 +1,40 @@ +6845278a102bd147f30f770ed1134ce5 usr/share/doc/compute-doc/changelog.Debian.gz +fb1a6c11d7a8fa5f238617c20b13b6a1 usr/share/doc/compute-doc/copyright +705113edf19bbf7f9d406fccd98ebef9 usr/share/doc/compute-doc/html/_sources/index.rst.txt +91934f7b742b8395043e25cfa73682af usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt +de8bc1c2c00774ddee5363aef80c0775 usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt +2a0040e0a150de53ed929e963af635a8 usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt +dd6324cb85dc57ef37c4f8161aa2d233 usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt +c594567565cc48a247932409d9adcc4a usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt +e6a69ab447e455dba6e7b865a3d872d2 usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt +ba27654c086857e64d58468b13bc31c4 usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt +801ccc953fc57199b06ec122e10f784c usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt +324ae7c877f3cf7895b2a5d3af579345 usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt +db91c0d83c2c80e9f9323a8943eeeff4 usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt +572ed749dd8924c36f1afe9e8e14d4d3 usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt +4fc9d553e40384beedf38e21f205d2a7 usr/share/doc/compute-doc/html/_static/alabaster.css +23ffe661f835b08e157d492a86aae74d usr/share/doc/compute-doc/html/_static/basic.css +dad0c9b31e59069c83018ce87594ed65 usr/share/doc/compute-doc/html/_static/custom.css +5e103d51310d4e0c065325d795cc9def usr/share/doc/compute-doc/html/_static/documentation_options.js +ba0c95766a77a6c598a7ca542f1db738 usr/share/doc/compute-doc/html/_static/file.png +5b6b3233153feca50a94aa6c60873a5f usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png +36b1a4b05451c7acde7ced60b2f6bc21 usr/share/doc/compute-doc/html/_static/minus.png +0d7849fd4d4148b7f78cab60a087633a usr/share/doc/compute-doc/html/_static/plus.png +4f81be1c1dd97a6ec76af15b8f926189 usr/share/doc/compute-doc/html/_static/pygments.css +fd297228a19ece7e38824d0704f3635d usr/share/doc/compute-doc/html/genindex.html +3e038e6169c721ebacf889ea4ac5c1bf usr/share/doc/compute-doc/html/index.html +b8e4906e5136e907ab0d7ae826720603 usr/share/doc/compute-doc/html/objects.inv +2658558520c0c9f209dd4c69516facfd usr/share/doc/compute-doc/html/py-modindex.html +4254a2ecc3e154f52646febebd0ef6e6 usr/share/doc/compute-doc/html/pyapi/exceptions.html +bf4609f321d2c60399574c3e52dd6a44 usr/share/doc/compute-doc/html/pyapi/index.html +730aab71986cb938e9aff03ba203c9a9 usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html +fad8eba8a9cb9b1befd8e0ecdf1bbe5f usr/share/doc/compute-doc/html/pyapi/instance/index.html +781272676f0b35c52f43b99f2ca86647 usr/share/doc/compute-doc/html/pyapi/instance/instance.html +ede88501ec628083bb1ad1cb86cdec9f usr/share/doc/compute-doc/html/pyapi/instance/schemas.html +4c8d372d298068aba7272d11feb2cc52 usr/share/doc/compute-doc/html/pyapi/session.html +000f86f6184a455843017772ff2fec9d usr/share/doc/compute-doc/html/pyapi/storage/index.html +a2b63c0194a1e55be8d7036b46851986 usr/share/doc/compute-doc/html/pyapi/storage/pool.html +8d4e9081b213585aad36b4daadc37e26 usr/share/doc/compute-doc/html/pyapi/storage/volume.html +307d7a44f4343b0f34ee758e4ab20d88 usr/share/doc/compute-doc/html/pyapi/utils.html +5999199d4710213969f7fb1b50647f4a usr/share/doc/compute-doc/html/search.html +148b182d3691ae88c629783c3623007d usr/share/doc/compute-doc/html/searchindex.js diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz new file mode 100644 index 0000000..40eae6f Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/changelog.Debian.gz differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + 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 package 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 . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt new file mode 100644 index 0000000..81222c2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/index.rst.txt @@ -0,0 +1,16 @@ +Compute +======= + +Compute instances management library. + +.. toctree:: + :maxdepth: 1 + + pyapi/index + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt new file mode 100644 index 0000000..3912721 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/exceptions.rst.txt @@ -0,0 +1,5 @@ +``exceptions`` +============== + +.. automodule:: compute.exceptions + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt new file mode 100644 index 0000000..e0cebb8 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/index.rst.txt @@ -0,0 +1,49 @@ +Python API +========== + +The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the `myinstance` instance. + +.. code-block:: python + + import logging + + from compute import Session + + logging.basicConfig(level=logging.DEBUG) + + with Session() as session: + instance = session.get_instance('myinstance') + instance.set_vcpus(4) + instance.start() + instance.set_autostart(enabled=True) + + +:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect` +and returns objects of other classes of the present library. + +Entity representation +--------------------- + +Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +:class:`Volume`. + +The configuration files of various libvirt objects in `compute` are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the ``to_xml()`` method. For example :class:`VolumeConfig`. + +`Pydantic `_ models are used to validate input data. +For example :class:`VolumeSchema`. + +Modules documentation +--------------------- + +.. toctree:: + :maxdepth: 4 + + session + instance/index + storage/index + utils + exceptions diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt new file mode 100644 index 0000000..1305140 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/guest_agent.rst.txt @@ -0,0 +1,6 @@ +``guest_agent`` +=============== + +.. automodule:: compute.instance.guest_agent + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt new file mode 100644 index 0000000..659ffc2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/index.rst.txt @@ -0,0 +1,10 @@ +``instance`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + instance + guest_agent + schemas diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt new file mode 100644 index 0000000..3c58f1f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/instance.rst.txt @@ -0,0 +1,6 @@ +``instance`` +============ + +.. automodule:: compute.instance.instance + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt new file mode 100644 index 0000000..7dacabf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/instance/schemas.rst.txt @@ -0,0 +1,5 @@ +``schemas`` +=========== + +.. automodule:: compute.instance.schemas + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt new file mode 100644 index 0000000..2dec16e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/session.rst.txt @@ -0,0 +1,6 @@ +``session`` +=========== + +.. automodule:: compute.session + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt new file mode 100644 index 0000000..e9ea734 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/index.rst.txt @@ -0,0 +1,9 @@ +``storage`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + pool + volume diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt new file mode 100644 index 0000000..398124e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/pool.rst.txt @@ -0,0 +1,6 @@ +``pool`` +======== + +.. automodule:: compute.storage.pool + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt new file mode 100644 index 0000000..e1ba8d0 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/storage/volume.rst.txt @@ -0,0 +1,6 @@ +``volume`` +========== + +.. automodule:: compute.storage.volume + :members: + :special-members: __init__ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt new file mode 100644 index 0000000..b5ab60a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_sources/pyapi/utils.rst.txt @@ -0,0 +1,14 @@ +``utils`` +========= + +``utils.units`` +--------------- + +.. automodule:: compute.utils.units + :members: + +``utils.ids`` +------------- + +.. automodule:: compute.utils.ids + :members: diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js new file mode 120000 index 0000000..e04de6d --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/_sphinx_javascript_frameworks_compat.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/_sphinx_javascript_frameworks_compat.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css new file mode 100644 index 0000000..0eddaeb --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/alabaster.css @@ -0,0 +1,701 @@ +@import url("basic.css"); + +/* -- page layout ----------------------------------------------------------- */ + +body { + font-family: Georgia, serif; + font-size: 17px; + background-color: #fff; + color: #000; + margin: 0; + padding: 0; +} + + +div.document { + width: 940px; + margin: 30px auto 0 auto; +} + +div.documentwrapper { + float: left; + width: 100%; +} + +div.bodywrapper { + margin: 0 0 0 220px; +} + +div.sphinxsidebar { + width: 220px; + font-size: 14px; + line-height: 1.5; +} + +hr { + border: 1px solid #B1B4B6; +} + +div.body { + background-color: #fff; + color: #3E4349; + padding: 0 30px 0 30px; +} + +div.body > .section { + text-align: left; +} + +div.footer { + width: 940px; + margin: 20px auto 30px auto; + font-size: 14px; + color: #888; + text-align: right; +} + +div.footer a { + color: #888; +} + +p.caption { + font-family: inherit; + font-size: inherit; +} + + +div.relations { + display: none; +} + + +div.sphinxsidebar a { + color: #444; + text-decoration: none; + border-bottom: 1px dotted #999; +} + +div.sphinxsidebar a:hover { + border-bottom: 1px solid #999; +} + +div.sphinxsidebarwrapper { + padding: 18px 10px; +} + +div.sphinxsidebarwrapper p.logo { + padding: 0; + margin: -10px 0 0 0px; + text-align: center; +} + +div.sphinxsidebarwrapper h1.logo { + margin-top: -10px; + text-align: center; + margin-bottom: 5px; + text-align: left; +} + +div.sphinxsidebarwrapper h1.logo-name { + margin-top: 0px; +} + +div.sphinxsidebarwrapper p.blurb { + margin-top: 0; + font-style: normal; +} + +div.sphinxsidebar h3, +div.sphinxsidebar h4 { + font-family: Georgia, serif; + color: #444; + font-size: 24px; + font-weight: normal; + margin: 0 0 5px 0; + padding: 0; +} + +div.sphinxsidebar h4 { + font-size: 20px; +} + +div.sphinxsidebar h3 a { + color: #444; +} + +div.sphinxsidebar p.logo a, +div.sphinxsidebar h3 a, +div.sphinxsidebar p.logo a:hover, +div.sphinxsidebar h3 a:hover { + border: none; +} + +div.sphinxsidebar p { + color: #555; + margin: 10px 0; +} + +div.sphinxsidebar ul { + margin: 10px 0; + padding: 0; + color: #000; +} + +div.sphinxsidebar ul li.toctree-l1 > a { + font-size: 120%; +} + +div.sphinxsidebar ul li.toctree-l2 > a { + font-size: 110%; +} + +div.sphinxsidebar input { + border: 1px solid #CCC; + font-family: Georgia, serif; + font-size: 1em; +} + +div.sphinxsidebar hr { + border: none; + height: 1px; + color: #AAA; + background: #AAA; + + text-align: left; + margin-left: 0; + width: 50%; +} + +div.sphinxsidebar .badge { + border-bottom: none; +} + +div.sphinxsidebar .badge:hover { + border-bottom: none; +} + +/* To address an issue with donation coming after search */ +div.sphinxsidebar h3.donation { + margin-top: 10px; +} + +/* -- body styles ----------------------------------------------------------- */ + +a { + color: #004B6B; + text-decoration: underline; +} + +a:hover { + color: #6D4100; + text-decoration: underline; +} + +div.body h1, +div.body h2, +div.body h3, +div.body h4, +div.body h5, +div.body h6 { + font-family: Georgia, serif; + font-weight: normal; + margin: 30px 0px 10px 0px; + padding: 0; +} + +div.body h1 { margin-top: 0; padding-top: 0; font-size: 240%; } +div.body h2 { font-size: 180%; } +div.body h3 { font-size: 150%; } +div.body h4 { font-size: 130%; } +div.body h5 { font-size: 100%; } +div.body h6 { font-size: 100%; } + +a.headerlink { + color: #DDD; + padding: 0 4px; + text-decoration: none; +} + +a.headerlink:hover { + color: #444; + background: #EAEAEA; +} + +div.body p, div.body dd, div.body li { + line-height: 1.4em; +} + +div.admonition { + margin: 20px 0px; + padding: 10px 30px; + background-color: #EEE; + border: 1px solid #CCC; +} + +div.admonition tt.xref, div.admonition code.xref, div.admonition a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fafafa; +} + +div.admonition p.admonition-title { + font-family: Georgia, serif; + font-weight: normal; + font-size: 24px; + margin: 0 0 10px 0; + padding: 0; + line-height: 1; +} + +div.admonition p.last { + margin-bottom: 0; +} + +div.highlight { + background-color: #fff; +} + +dt:target, .highlight { + background: #FAF3E8; +} + +div.warning { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.danger { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.error { + background-color: #FCC; + border: 1px solid #FAA; + -moz-box-shadow: 2px 2px 4px #D52C2C; + -webkit-box-shadow: 2px 2px 4px #D52C2C; + box-shadow: 2px 2px 4px #D52C2C; +} + +div.caution { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.attention { + background-color: #FCC; + border: 1px solid #FAA; +} + +div.important { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.note { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.tip { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.hint { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.seealso { + background-color: #EEE; + border: 1px solid #CCC; +} + +div.topic { + background-color: #EEE; +} + +p.admonition-title { + display: inline; +} + +p.admonition-title:after { + content: ":"; +} + +pre, tt, code { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; + font-size: 0.9em; +} + +.hll { + background-color: #FFC; + margin: 0 -12px; + padding: 0 12px; + display: block; +} + +img.screenshot { +} + +tt.descname, tt.descclassname, code.descname, code.descclassname { + font-size: 0.95em; +} + +tt.descname, code.descname { + padding-right: 0.08em; +} + +img.screenshot { + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils { + border: 1px solid #888; + -moz-box-shadow: 2px 2px 4px #EEE; + -webkit-box-shadow: 2px 2px 4px #EEE; + box-shadow: 2px 2px 4px #EEE; +} + +table.docutils td, table.docutils th { + border: 1px solid #888; + padding: 0.25em 0.7em; +} + +table.field-list, table.footnote { + border: none; + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + +table.footnote { + margin: 15px 0; + width: 100%; + border: 1px solid #EEE; + background: #FDFDFD; + font-size: 0.9em; +} + +table.footnote + table.footnote { + margin-top: -15px; + border-top: none; +} + +table.field-list th { + padding: 0 0.8em 0 0; +} + +table.field-list td { + padding: 0; +} + +table.field-list p { + margin-bottom: 0.8em; +} + +/* Cloned from + * https://github.com/sphinx-doc/sphinx/commit/ef60dbfce09286b20b7385333d63a60321784e68 + */ +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +table.footnote td.label { + width: .1px; + padding: 0.3em 0 0.3em 0.5em; +} + +table.footnote td { + padding: 0.3em 0.5em; +} + +dl { + margin: 0; + padding: 0; +} + +dl dd { + margin-left: 30px; +} + +blockquote { + margin: 0 0 0 30px; + padding: 0; +} + +ul, ol { + /* Matches the 30px from the narrow-screen "li > ul" selector below */ + margin: 10px 0 10px 30px; + padding: 0; +} + +pre { + background: #EEE; + padding: 7px 30px; + margin: 15px 0px; + line-height: 1.3em; +} + +div.viewcode-block:target { + background: #ffd; +} + +dl pre, blockquote pre, li pre { + margin-left: 0; + padding-left: 30px; +} + +tt, code { + background-color: #ecf0f3; + color: #222; + /* padding: 1px 2px; */ +} + +tt.xref, code.xref, a tt { + background-color: #FBFBFB; + border-bottom: 1px solid #fff; +} + +a.reference { + text-decoration: none; + border-bottom: 1px dotted #004B6B; +} + +/* Don't put an underline on images */ +a.image-reference, a.image-reference:hover { + border-bottom: none; +} + +a.reference:hover { + border-bottom: 1px solid #6D4100; +} + +a.footnote-reference { + text-decoration: none; + font-size: 0.7em; + vertical-align: top; + border-bottom: 1px dotted #004B6B; +} + +a.footnote-reference:hover { + border-bottom: 1px solid #6D4100; +} + +a:hover tt, a:hover code { + background: #EEE; +} + + +@media screen and (max-width: 870px) { + + div.sphinxsidebar { + display: none; + } + + div.document { + width: 100%; + + } + + div.documentwrapper { + margin-left: 0; + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + } + + div.bodywrapper { + margin-top: 0; + margin-right: 0; + margin-bottom: 0; + margin-left: 0; + } + + ul { + margin-left: 0; + } + + li > ul { + /* Matches the 30px from the "ul, ol" selector above */ + margin-left: 30px; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .bodywrapper { + margin: 0; + } + + .footer { + width: auto; + } + + .github { + display: none; + } + + + +} + + + +@media screen and (max-width: 875px) { + + body { + margin: 0; + padding: 20px 30px; + } + + div.documentwrapper { + float: none; + background: #fff; + } + + div.sphinxsidebar { + display: block; + float: none; + width: 102.5%; + margin: 50px -30px -20px -30px; + padding: 10px 20px; + background: #333; + color: #FFF; + } + + div.sphinxsidebar h3, div.sphinxsidebar h4, div.sphinxsidebar p, + div.sphinxsidebar h3 a { + color: #fff; + } + + div.sphinxsidebar a { + color: #AAA; + } + + div.sphinxsidebar p.logo { + display: none; + } + + div.document { + width: 100%; + margin: 0; + } + + div.footer { + display: none; + } + + div.bodywrapper { + margin: 0; + } + + div.body { + min-height: 0; + padding: 0; + } + + .rtd_doc_footer { + display: none; + } + + .document { + width: auto; + } + + .footer { + width: auto; + } + + .footer { + width: auto; + } + + .github { + display: none; + } +} + + +/* misc. */ + +.revsys-inline { + display: none!important; +} + +/* Make nested-list/multi-paragraph items look better in Releases changelog + * pages. Without this, docutils' magical list fuckery causes inconsistent + * formatting between different release sub-lists. + */ +div#changelog > div.section > ul > li > p:only-child { + margin-bottom: 0; +} + +/* Hide fugly table cell borders in ..bibliography:: directive output */ +table.docutils.citation, table.docutils.citation td, table.docutils.citation th { + border: none; + /* Below needed in some edge cases; if not applied, bottom shadows appear */ + -moz-box-shadow: none; + -webkit-box-shadow: none; + box-shadow: none; +} + + +/* relbar */ + +.related { + line-height: 30px; + width: 100%; + font-size: 0.9rem; +} + +.related.top { + border-bottom: 1px solid #EEE; + margin-bottom: 20px; +} + +.related.bottom { + border-top: 1px solid #EEE; +} + +.related ul { + padding: 0; + margin: 0; + list-style: none; +} + +.related li { + display: inline; +} + +nav#rellinks { + float: right; +} + +nav#rellinks li+li:before { + content: "|"; +} + +nav#breadcrumbs li+li:before { + content: "\00BB"; +} + +/* Hide certain items when printing */ +@media print { + div.related { + display: none; + } +} \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css new file mode 100644 index 0000000..4e9a9f1 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/basic.css @@ -0,0 +1,900 @@ +/* + * basic.css + * ~~~~~~~~~ + * + * Sphinx stylesheet -- basic theme. + * + * :copyright: Copyright 2007-2022 by the Sphinx team, see AUTHORS. + * :license: BSD, see LICENSE for details. + * + */ + +/* -- main layout ----------------------------------------------------------- */ + +div.clearer { + clear: both; +} + +div.section::after { + display: block; + content: ''; + clear: left; +} + +/* -- relbar ---------------------------------------------------------------- */ + +div.related { + width: 100%; + font-size: 90%; +} + +div.related h3 { + display: none; +} + +div.related ul { + margin: 0; + padding: 0 0 0 10px; + list-style: none; +} + +div.related li { + display: inline; +} + +div.related li.right { + float: right; + margin-right: 5px; +} + +/* -- sidebar --------------------------------------------------------------- */ + +div.sphinxsidebarwrapper { + padding: 10px 5px 0 10px; +} + +div.sphinxsidebar { + float: left; + width: 230px; + margin-left: -100%; + font-size: 90%; + word-wrap: break-word; + overflow-wrap : break-word; +} + +div.sphinxsidebar ul { + list-style: none; +} + +div.sphinxsidebar ul ul, +div.sphinxsidebar ul.want-points { + margin-left: 20px; + list-style: square; +} + +div.sphinxsidebar ul ul { + margin-top: 0; + margin-bottom: 0; +} + +div.sphinxsidebar form { + margin-top: 10px; +} + +div.sphinxsidebar input { + border: 1px solid #98dbcc; + font-family: sans-serif; + font-size: 1em; +} + +div.sphinxsidebar #searchbox form.search { + overflow: hidden; +} + +div.sphinxsidebar #searchbox input[type="text"] { + float: left; + width: 80%; + padding: 0.25em; + box-sizing: border-box; +} + +div.sphinxsidebar #searchbox input[type="submit"] { + float: left; + width: 20%; + border-left: none; + padding: 0.25em; + box-sizing: border-box; +} + + +img { + border: 0; + max-width: 100%; +} + +/* -- search page ----------------------------------------------------------- */ + +ul.search { + margin: 10px 0 0 20px; + padding: 0; +} + +ul.search li { + padding: 5px 0 5px 20px; + background-image: url(file.png); + background-repeat: no-repeat; + background-position: 0 7px; +} + +ul.search li a { + font-weight: bold; +} + +ul.search li p.context { + color: #888; + margin: 2px 0 0 30px; + text-align: left; +} + +ul.keywordmatches li.goodmatch a { + font-weight: bold; +} + +/* -- index page ------------------------------------------------------------ */ + +table.contentstable { + width: 90%; + margin-left: auto; + margin-right: auto; +} + +table.contentstable p.biglink { + line-height: 150%; +} + +a.biglink { + font-size: 1.3em; +} + +span.linkdescr { + font-style: italic; + padding-top: 5px; + font-size: 90%; +} + +/* -- general index --------------------------------------------------------- */ + +table.indextable { + width: 100%; +} + +table.indextable td { + text-align: left; + vertical-align: top; +} + +table.indextable ul { + margin-top: 0; + margin-bottom: 0; + list-style-type: none; +} + +table.indextable > tbody > tr > td > ul { + padding-left: 0em; +} + +table.indextable tr.pcap { + height: 10px; +} + +table.indextable tr.cap { + margin-top: 10px; + background-color: #f2f2f2; +} + +img.toggler { + margin-right: 3px; + margin-top: 3px; + cursor: pointer; +} + +div.modindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +div.genindex-jumpbox { + border-top: 1px solid #ddd; + border-bottom: 1px solid #ddd; + margin: 1em 0 1em 0; + padding: 0.4em; +} + +/* -- domain module index --------------------------------------------------- */ + +table.modindextable td { + padding: 2px; + border-collapse: collapse; +} + +/* -- general body styles --------------------------------------------------- */ + +div.body { + min-width: 360px; + max-width: 800px; +} + +div.body p, div.body dd, div.body li, div.body blockquote { + -moz-hyphens: auto; + -ms-hyphens: auto; + -webkit-hyphens: auto; + hyphens: auto; +} + +a.headerlink { + visibility: hidden; +} + +h1:hover > a.headerlink, +h2:hover > a.headerlink, +h3:hover > a.headerlink, +h4:hover > a.headerlink, +h5:hover > a.headerlink, +h6:hover > a.headerlink, +dt:hover > a.headerlink, +caption:hover > a.headerlink, +p.caption:hover > a.headerlink, +div.code-block-caption:hover > a.headerlink { + visibility: visible; +} + +div.body p.caption { + text-align: inherit; +} + +div.body td { + text-align: left; +} + +.first { + margin-top: 0 !important; +} + +p.rubric { + margin-top: 30px; + font-weight: bold; +} + +img.align-left, figure.align-left, .figure.align-left, object.align-left { + clear: left; + float: left; + margin-right: 1em; +} + +img.align-right, figure.align-right, .figure.align-right, object.align-right { + clear: right; + float: right; + margin-left: 1em; +} + +img.align-center, figure.align-center, .figure.align-center, object.align-center { + display: block; + margin-left: auto; + margin-right: auto; +} + +img.align-default, figure.align-default, .figure.align-default { + display: block; + margin-left: auto; + margin-right: auto; +} + +.align-left { + text-align: left; +} + +.align-center { + text-align: center; +} + +.align-default { + text-align: center; +} + +.align-right { + text-align: right; +} + +/* -- sidebars -------------------------------------------------------------- */ + +div.sidebar, +aside.sidebar { + margin: 0 0 0.5em 1em; + border: 1px solid #ddb; + padding: 7px; + background-color: #ffe; + width: 40%; + float: right; + clear: right; + overflow-x: auto; +} + +p.sidebar-title { + font-weight: bold; +} +nav.contents, +aside.topic, +div.admonition, div.topic, blockquote { + clear: left; +} + +/* -- topics ---------------------------------------------------------------- */ +nav.contents, +aside.topic, +div.topic { + border: 1px solid #ccc; + padding: 7px; + margin: 10px 0 10px 0; +} + +p.topic-title { + font-size: 1.1em; + font-weight: bold; + margin-top: 10px; +} + +/* -- admonitions ----------------------------------------------------------- */ + +div.admonition { + margin-top: 10px; + margin-bottom: 10px; + padding: 7px; +} + +div.admonition dt { + font-weight: bold; +} + +p.admonition-title { + margin: 0px 10px 5px 0px; + font-weight: bold; +} + +div.body p.centered { + text-align: center; + margin-top: 25px; +} + +/* -- content of sidebars/topics/admonitions -------------------------------- */ + +div.sidebar > :last-child, +aside.sidebar > :last-child, +nav.contents > :last-child, +aside.topic > :last-child, +div.topic > :last-child, +div.admonition > :last-child { + margin-bottom: 0; +} + +div.sidebar::after, +aside.sidebar::after, +nav.contents::after, +aside.topic::after, +div.topic::after, +div.admonition::after, +blockquote::after { + display: block; + content: ''; + clear: both; +} + +/* -- tables ---------------------------------------------------------------- */ + +table.docutils { + margin-top: 10px; + margin-bottom: 10px; + border: 0; + border-collapse: collapse; +} + +table.align-center { + margin-left: auto; + margin-right: auto; +} + +table.align-default { + margin-left: auto; + margin-right: auto; +} + +table caption span.caption-number { + font-style: italic; +} + +table caption span.caption-text { +} + +table.docutils td, table.docutils th { + padding: 1px 8px 1px 5px; + border-top: 0; + border-left: 0; + border-right: 0; + border-bottom: 1px solid #aaa; +} + +th { + text-align: left; + padding-right: 5px; +} + +table.citation { + border-left: solid 1px gray; + margin-left: 1px; +} + +table.citation td { + border-bottom: none; +} + +th > :first-child, +td > :first-child { + margin-top: 0px; +} + +th > :last-child, +td > :last-child { + margin-bottom: 0px; +} + +/* -- figures --------------------------------------------------------------- */ + +div.figure, figure { + margin: 0.5em; + padding: 0.5em; +} + +div.figure p.caption, figcaption { + padding: 0.3em; +} + +div.figure p.caption span.caption-number, +figcaption span.caption-number { + font-style: italic; +} + +div.figure p.caption span.caption-text, +figcaption span.caption-text { +} + +/* -- field list styles ----------------------------------------------------- */ + +table.field-list td, table.field-list th { + border: 0 !important; +} + +.field-list ul { + margin: 0; + padding-left: 1em; +} + +.field-list p { + margin: 0; +} + +.field-name { + -moz-hyphens: manual; + -ms-hyphens: manual; + -webkit-hyphens: manual; + hyphens: manual; +} + +/* -- hlist styles ---------------------------------------------------------- */ + +table.hlist { + margin: 1em 0; +} + +table.hlist td { + vertical-align: top; +} + +/* -- object description styles --------------------------------------------- */ + +.sig { + font-family: 'Consolas', 'Menlo', 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', monospace; +} + +.sig-name, code.descname { + background-color: transparent; + font-weight: bold; +} + +.sig-name { + font-size: 1.1em; +} + +code.descname { + font-size: 1.2em; +} + +.sig-prename, code.descclassname { + background-color: transparent; +} + +.optional { + font-size: 1.3em; +} + +.sig-paren { + font-size: larger; +} + +.sig-param.n { + font-style: italic; +} + +/* C++ specific styling */ + +.sig-inline.c-texpr, +.sig-inline.cpp-texpr { + font-family: unset; +} + +.sig.c .k, .sig.c .kt, +.sig.cpp .k, .sig.cpp .kt { + color: #0033B3; +} + +.sig.c .m, +.sig.cpp .m { + color: #1750EB; +} + +.sig.c .s, .sig.c .sc, +.sig.cpp .s, .sig.cpp .sc { + color: #067D17; +} + + +/* -- other body styles ----------------------------------------------------- */ + +ol.arabic { + list-style: decimal; +} + +ol.loweralpha { + list-style: lower-alpha; +} + +ol.upperalpha { + list-style: upper-alpha; +} + +ol.lowerroman { + list-style: lower-roman; +} + +ol.upperroman { + list-style: upper-roman; +} + +:not(li) > ol > li:first-child > :first-child, +:not(li) > ul > li:first-child > :first-child { + margin-top: 0px; +} + +:not(li) > ol > li:last-child > :last-child, +:not(li) > ul > li:last-child > :last-child { + margin-bottom: 0px; +} + +ol.simple ol p, +ol.simple ul p, +ul.simple ol p, +ul.simple ul p { + margin-top: 0; +} + +ol.simple > li:not(:first-child) > p, +ul.simple > li:not(:first-child) > p { + margin-top: 0; +} + +ol.simple p, +ul.simple p { + margin-bottom: 0; +} +aside.footnote > span, +div.citation > span { + float: left; +} +aside.footnote > span:last-of-type, +div.citation > span:last-of-type { + padding-right: 0.5em; +} +aside.footnote > p { + margin-left: 2em; +} +div.citation > p { + margin-left: 4em; +} +aside.footnote > p:last-of-type, +div.citation > p:last-of-type { + margin-bottom: 0em; +} +aside.footnote > p:last-of-type:after, +div.citation > p:last-of-type:after { + content: ""; + clear: both; +} + +dl.field-list { + display: grid; + grid-template-columns: fit-content(30%) auto; +} + +dl.field-list > dt { + font-weight: bold; + word-break: break-word; + padding-left: 0.5em; + padding-right: 5px; +} + +dl.field-list > dd { + padding-left: 0.5em; + margin-top: 0em; + margin-left: 0em; + margin-bottom: 0em; +} + +dl { + margin-bottom: 15px; +} + +dd > :first-child { + margin-top: 0px; +} + +dd ul, dd table { + margin-bottom: 10px; +} + +dd { + margin-top: 3px; + margin-bottom: 10px; + margin-left: 30px; +} + +dl > dd:last-child, +dl > dd:last-child > :last-child { + margin-bottom: 0; +} + +dt:target, span.highlighted { + background-color: #fbe54e; +} + +rect.highlighted { + fill: #fbe54e; +} + +dl.glossary dt { + font-weight: bold; + font-size: 1.1em; +} + +.versionmodified { + font-style: italic; +} + +.system-message { + background-color: #fda; + padding: 5px; + border: 3px solid red; +} + +.footnote:target { + background-color: #ffa; +} + +.line-block { + display: block; + margin-top: 1em; + margin-bottom: 1em; +} + +.line-block .line-block { + margin-top: 0; + margin-bottom: 0; + margin-left: 1.5em; +} + +.guilabel, .menuselection { + font-family: sans-serif; +} + +.accelerator { + text-decoration: underline; +} + +.classifier { + font-style: oblique; +} + +.classifier:before { + font-style: normal; + margin: 0 0.5em; + content: ":"; + display: inline-block; +} + +abbr, acronym { + border-bottom: dotted 1px; + cursor: help; +} + +/* -- code displays --------------------------------------------------------- */ + +pre { + overflow: auto; + overflow-y: hidden; /* fixes display issues on Chrome browsers */ +} + +pre, div[class*="highlight-"] { + clear: both; +} + +span.pre { + -moz-hyphens: none; + -ms-hyphens: none; + -webkit-hyphens: none; + hyphens: none; + white-space: nowrap; +} + +div[class*="highlight-"] { + margin: 1em 0; +} + +td.linenos pre { + border: 0; + background-color: transparent; + color: #aaa; +} + +table.highlighttable { + display: block; +} + +table.highlighttable tbody { + display: block; +} + +table.highlighttable tr { + display: flex; +} + +table.highlighttable td { + margin: 0; + padding: 0; +} + +table.highlighttable td.linenos { + padding-right: 0.5em; +} + +table.highlighttable td.code { + flex: 1; + overflow: hidden; +} + +.highlight .hll { + display: block; +} + +div.highlight pre, +table.highlighttable pre { + margin: 0; +} + +div.code-block-caption + div { + margin-top: 0; +} + +div.code-block-caption { + margin-top: 1em; + padding: 2px 5px; + font-size: small; +} + +div.code-block-caption code { + background-color: transparent; +} + +table.highlighttable td.linenos, +span.linenos, +div.highlight span.gp { /* gp: Generic.Prompt */ + user-select: none; + -webkit-user-select: text; /* Safari fallback only */ + -webkit-user-select: none; /* Chrome/Safari */ + -moz-user-select: none; /* Firefox */ + -ms-user-select: none; /* IE10+ */ +} + +div.code-block-caption span.caption-number { + padding: 0.1em 0.3em; + font-style: italic; +} + +div.code-block-caption span.caption-text { +} + +div.literal-block-wrapper { + margin: 1em 0; +} + +code.xref, a code { + background-color: transparent; + font-weight: bold; +} + +h1 code, h2 code, h3 code, h4 code, h5 code, h6 code { + background-color: transparent; +} + +.viewcode-link { + float: right; +} + +.viewcode-back { + float: right; + font-family: sans-serif; +} + +div.viewcode-block:target { + margin: -1px -10px; + padding: 0 10px; +} + +/* -- math display ---------------------------------------------------------- */ + +img.math { + vertical-align: middle; +} + +div.body div.math p { + text-align: center; +} + +span.eqno { + float: right; +} + +span.eqno a.headerlink { + position: absolute; + z-index: 1; +} + +div.math:hover a.headerlink { + visibility: visible; +} + +/* -- printout stylesheet --------------------------------------------------- */ + +@media print { + div.document, + div.documentwrapper, + div.bodywrapper { + margin: 0 !important; + width: 100%; + } + + div.sphinxsidebar, + div.related, + div.footer, + #top-link { + display: none; + } +} \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css new file mode 100644 index 0000000..2a924f1 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/custom.css @@ -0,0 +1 @@ +/* This file intentionally left blank. */ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js new file mode 120000 index 0000000..e51872e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/doctools.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/doctools.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js new file mode 100644 index 0000000..e49ed18 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/documentation_options.js @@ -0,0 +1,14 @@ +var DOCUMENTATION_OPTIONS = { + URL_ROOT: document.getElementById("documentation_options").getAttribute('data-url_root'), + VERSION: '0.1.0', + LANGUAGE: 'en', + COLLAPSE_INDEX: false, + BUILDER: 'html', + FILE_SUFFIX: '.html', + LINK_SUFFIX: '.html', + HAS_SOURCE: true, + SOURCELINK_SUFFIX: '.txt', + NAVIGATION_WITH_KEYS: false, + SHOW_SEARCH_SUMMARY: true, + ENABLE_SEARCH_SHORTCUTS: true, +}; \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png new file mode 100644 index 0000000..a858a41 Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/file.png differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png new file mode 100644 index 0000000..146ef8a Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/forkme_right_darkblue_121621.png differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/jquery.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/jquery.js new file mode 120000 index 0000000..e82f704 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/jquery.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/jquery.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/language_data.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/language_data.js new file mode 120000 index 0000000..678e1c9 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/language_data.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/language_data.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/minus.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/minus.png new file mode 100644 index 0000000..d96755f Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/minus.png differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png new file mode 100644 index 0000000..7107cec Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/plus.png differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css new file mode 100644 index 0000000..9abe04b --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/pygments.css @@ -0,0 +1,83 @@ +pre { line-height: 125%; } +td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; } +td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; } +.highlight .hll { background-color: #ffffcc } +.highlight { background: #f8f8f8; } +.highlight .c { color: #8f5902; font-style: italic } /* Comment */ +.highlight .err { color: #a40000; border: 1px solid #ef2929 } /* Error */ +.highlight .g { color: #000000 } /* Generic */ +.highlight .k { color: #004461; font-weight: bold } /* Keyword */ +.highlight .l { color: #000000 } /* Literal */ +.highlight .n { color: #000000 } /* Name */ +.highlight .o { color: #582800 } /* Operator */ +.highlight .x { color: #000000 } /* Other */ +.highlight .p { color: #000000; font-weight: bold } /* Punctuation */ +.highlight .ch { color: #8f5902; font-style: italic } /* Comment.Hashbang */ +.highlight .cm { color: #8f5902; font-style: italic } /* Comment.Multiline */ +.highlight .cp { color: #8f5902 } /* Comment.Preproc */ +.highlight .cpf { color: #8f5902; font-style: italic } /* Comment.PreprocFile */ +.highlight .c1 { color: #8f5902; font-style: italic } /* Comment.Single */ +.highlight .cs { color: #8f5902; font-style: italic } /* Comment.Special */ +.highlight .gd { color: #a40000 } /* Generic.Deleted */ +.highlight .ge { color: #000000; font-style: italic } /* Generic.Emph */ +.highlight .gr { color: #ef2929 } /* Generic.Error */ +.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ +.highlight .gi { color: #00A000 } /* Generic.Inserted */ +.highlight .go { color: #888888 } /* Generic.Output */ +.highlight .gp { color: #745334 } /* Generic.Prompt */ +.highlight .gs { color: #000000; font-weight: bold } /* Generic.Strong */ +.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ +.highlight .gt { color: #a40000; font-weight: bold } /* Generic.Traceback */ +.highlight .kc { color: #004461; font-weight: bold } /* Keyword.Constant */ +.highlight .kd { color: #004461; font-weight: bold } /* Keyword.Declaration */ +.highlight .kn { color: #004461; font-weight: bold } /* Keyword.Namespace */ +.highlight .kp { color: #004461; font-weight: bold } /* Keyword.Pseudo */ +.highlight .kr { color: #004461; font-weight: bold } /* Keyword.Reserved */ +.highlight .kt { color: #004461; font-weight: bold } /* Keyword.Type */ +.highlight .ld { color: #000000 } /* Literal.Date */ +.highlight .m { color: #990000 } /* Literal.Number */ +.highlight .s { color: #4e9a06 } /* Literal.String */ +.highlight .na { color: #c4a000 } /* Name.Attribute */ +.highlight .nb { color: #004461 } /* Name.Builtin */ +.highlight .nc { color: #000000 } /* Name.Class */ +.highlight .no { color: #000000 } /* Name.Constant */ +.highlight .nd { color: #888888 } /* Name.Decorator */ +.highlight .ni { color: #ce5c00 } /* Name.Entity */ +.highlight .ne { color: #cc0000; font-weight: bold } /* Name.Exception */ +.highlight .nf { color: #000000 } /* Name.Function */ +.highlight .nl { color: #f57900 } /* Name.Label */ +.highlight .nn { color: #000000 } /* Name.Namespace */ +.highlight .nx { color: #000000 } /* Name.Other */ +.highlight .py { color: #000000 } /* Name.Property */ +.highlight .nt { color: #004461; font-weight: bold } /* Name.Tag */ +.highlight .nv { color: #000000 } /* Name.Variable */ +.highlight .ow { color: #004461; font-weight: bold } /* Operator.Word */ +.highlight .pm { color: #000000; font-weight: bold } /* Punctuation.Marker */ +.highlight .w { color: #f8f8f8; text-decoration: underline } /* Text.Whitespace */ +.highlight .mb { color: #990000 } /* Literal.Number.Bin */ +.highlight .mf { color: #990000 } /* Literal.Number.Float */ +.highlight .mh { color: #990000 } /* Literal.Number.Hex */ +.highlight .mi { color: #990000 } /* Literal.Number.Integer */ +.highlight .mo { color: #990000 } /* Literal.Number.Oct */ +.highlight .sa { color: #4e9a06 } /* Literal.String.Affix */ +.highlight .sb { color: #4e9a06 } /* Literal.String.Backtick */ +.highlight .sc { color: #4e9a06 } /* Literal.String.Char */ +.highlight .dl { color: #4e9a06 } /* Literal.String.Delimiter */ +.highlight .sd { color: #8f5902; font-style: italic } /* Literal.String.Doc */ +.highlight .s2 { color: #4e9a06 } /* Literal.String.Double */ +.highlight .se { color: #4e9a06 } /* Literal.String.Escape */ +.highlight .sh { color: #4e9a06 } /* Literal.String.Heredoc */ +.highlight .si { color: #4e9a06 } /* Literal.String.Interpol */ +.highlight .sx { color: #4e9a06 } /* Literal.String.Other */ +.highlight .sr { color: #4e9a06 } /* Literal.String.Regex */ +.highlight .s1 { color: #4e9a06 } /* Literal.String.Single */ +.highlight .ss { color: #4e9a06 } /* Literal.String.Symbol */ +.highlight .bp { color: #3465a4 } /* Name.Builtin.Pseudo */ +.highlight .fm { color: #000000 } /* Name.Function.Magic */ +.highlight .vc { color: #000000 } /* Name.Variable.Class */ +.highlight .vg { color: #000000 } /* Name.Variable.Global */ +.highlight .vi { color: #000000 } /* Name.Variable.Instance */ +.highlight .vm { color: #000000 } /* Name.Variable.Magic */ +.highlight .il { color: #990000 } /* Literal.Number.Integer.Long */ \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js new file mode 120000 index 0000000..2d33672 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/searchtools.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/searchtools.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js new file mode 120000 index 0000000..75db705 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/sphinx_highlight.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/sphinx_highlight.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js new file mode 120000 index 0000000..94df107 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/_static/underscore.js @@ -0,0 +1 @@ +../../../../javascript/sphinxdoc/1.0/underscore.js \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html new file mode 100644 index 0000000..9434347 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/genindex.html @@ -0,0 +1,614 @@ + + + + + + + + Index — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Index

+ +
+ _ + | A + | B + | C + | D + | E + | G + | I + | L + | M + | N + | P + | R + | S + | T + | U + | V + +
+

_

+ + +
+ +

A

+ + + +
+ +

B

+ + +
+ +

C

+ + + +
+ +

D

+ + + +
+ +

E

+ + + +
+ +

G

+ + + +
+ +

I

+ + + +
+ +

L

+ + + +
+ +

M

+ + +
+ +

N

+ + + +
+ +

P

+ + + +
+ +

R

+ + + +
+ +

S

+ + + +
+ +

T

+ + + +
+ +

U

+ + +
+ +

V

+ + + +
+ + + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html new file mode 100644 index 0000000..dfb6fab --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/index.html @@ -0,0 +1,122 @@ + + + + + + + + + Compute — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Compute

+

Compute instances management library.

+
+ +
+
+

Indices and tables

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv new file mode 100644 index 0000000..353c123 Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/objects.inv differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/py-modindex.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/py-modindex.html new file mode 100644 index 0000000..d9f35ef --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/py-modindex.html @@ -0,0 +1,165 @@ + + + + + + + + Python Module Index — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ + +

Python Module Index

+ +
+ c +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
 
+ c
+ compute +
    + compute.exceptions +
    + compute.instance.guest_agent +
    + compute.instance.instance +
    + compute.instance.schemas +
    + compute.session +
    + compute.storage.pool +
    + compute.storage.volume +
    + compute.utils.ids +
    + compute.utils.units +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html new file mode 100644 index 0000000..1868528 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/exceptions.html @@ -0,0 +1,183 @@ + + + + + + + + + exceptions — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

exceptions

+

Exceptions.

+
+
+exception compute.exceptions.ComputeError
+

Basic exception class.

+
+ +
+
+exception compute.exceptions.ConfigLoaderError
+

Something went wrong when loading configuration.

+
+ +
+
+exception compute.exceptions.GuestAgentCommandNotSupportedError
+

Guest agent command is not supported or blacklisted on guest.

+
+ +
+
+exception compute.exceptions.GuestAgentError
+

Something went wring when QEMU Guest Agent call.

+
+ +
+
+exception compute.exceptions.GuestAgentTimeoutExceededError(msg: int)
+

QEMU timeout exceeded.

+
+ +
+
+exception compute.exceptions.GuestAgentUnavailableError
+

Guest agent is not connected or is unavailable.

+
+ +
+
+exception compute.exceptions.InstanceError
+

Something went wrong while interacting with the domain.

+
+ +
+
+exception compute.exceptions.InstanceNotFoundError(msg: str)
+

Virtual machine or container not found on compute node.

+
+ +
+
+exception compute.exceptions.SessionError
+

Something went wrong while connecting to libvirtd.

+
+ +
+
+exception compute.exceptions.StoragePoolError
+

Something went wrong when operating with storage pool.

+
+ +
+
+exception compute.exceptions.StoragePoolNotFoundError(msg: str)
+

Storage pool not found.

+
+ +
+
+exception compute.exceptions.VolumeNotFoundError(msg: str)
+

Storage volume not found.

+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html new file mode 100644 index 0000000..7162de0 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/index.html @@ -0,0 +1,342 @@ + + + + + + + + + Python API — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

Python API

+

The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the myinstance instance.

+
import logging
+
+from compute import Session
+
+logging.basicConfig(level=logging.DEBUG)
+
+with Session() as session:
+    instance = session.get_instance('myinstance')
+    instance.set_vcpus(4)
+    instance.start()
+    instance.set_autostart(enabled=True)
+
+
+

Session context manager provides an abstraction over libvirt.virConnect +and returns objects of other classes of the present library.

+
+

Entity representation

+

Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +Volume.

+

The configuration files of various libvirt objects in compute are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the to_xml() method. For example VolumeConfig.

+

Pydantic models are used to validate input data. +For example VolumeSchema.

+
+
+

Modules documentation

+
+ +
+
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html new file mode 100644 index 0000000..a8cca32 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/guest_agent.html @@ -0,0 +1,266 @@ + + + + + + + + + guest_agent — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

guest_agent

+

Interacting with the QEMU Guest Agent.

+
+
+class compute.instance.guest_agent.GuestAgent(domain: virDomain, timeout: int = 60)
+

Class for interacting with QEMU guest agent.

+
+
+__init__(domain: virDomain, timeout: int = 60)
+

Initialise GuestAgent.

+
+
Parameters:
+
    +
  • domain – Libvirt domain object

  • +
  • timeout – QEMU timeout

  • +
+
+
+
+ +
+
+execute(command: dict) dict
+

Execute QEMU guest agent command.

+

See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html

+
+
Parameters:
+

command – QEMU guest agent command as dict

+
+
Returns:
+

Command output

+
+
Return type:
+

dict

+
+
+
+ +
+
+get_supported_commands() set[str]
+

Return set of supported guest agent commands.

+
+ +
+
+guest_exec(path: str, args: list[str] | None = None, env: list[str] | None = None, stdin: str | None = None, *, capture_output: bool = False, decode_output: bool = False, poll: bool = False) GuestExecOutput
+

Execute qemu-exec command and return output.

+
+
Parameters:
+
    +
  • path – Path ot executable on guest.

  • +
  • arg – List of arguments to pass to executable.

  • +
  • env – List of environment variables to pass to executable. +For example: ['LANG=C', 'TERM=xterm']

  • +
  • stdin – Data to pass to executable STDIN.

  • +
  • capture_output – Capture command output.

  • +
  • decode_output – Use base64_decode() to decode command output. +Affects only if capture_output is True.

  • +
  • poll – Poll command output. Uses self.timeout and +POLL_INTERVAL constant.

  • +
+
+
Returns:
+

Command output

+
+
Return type:
+

GuestExecOutput

+
+
+
+ +
+
+guest_exec_status(pid: int, *, poll: bool = False, poll_interval: float = 0.3) dict
+

Execute guest-exec-status and return output.

+
+
Parameters:
+
    +
  • pid – PID in guest.

  • +
  • poll – If True poll command status.

  • +
  • poll_interval – Time between attempts to obtain command status.

  • +
+
+
Returns:
+

Command output

+
+
Return type:
+

dict

+
+
+
+ +
+
+is_available() bool
+

Execute guest-ping.

+
+
Returns:
+

True or False if guest agent is unreachable.

+
+
Return type:
+

bool

+
+
+
+ +
+
+raise_for_commands(commands: list[str]) None
+

Raise exception if QEMU GA command is not available.

+
+
Parameters:
+

commands – List of required commands

+
+
Raise:
+

GuestAgentCommandNotSupportedError

+
+
+
+ +
+ +
+
+class compute.instance.guest_agent.GuestExecOutput(exited: bool | None = None, exitcode: int | None = None, stdout: str | None = None, stderr: str | None = None)
+

QEMU guest-exec command output.

+
+
+exitcode: int | None
+

Alias for field number 1

+
+ +
+
+exited: bool | None
+

Alias for field number 0

+
+ +
+
+stderr: str | None
+

Alias for field number 3

+
+ +
+
+stdout: str | None
+

Alias for field number 2

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html new file mode 100644 index 0000000..87e074f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/index.html @@ -0,0 +1,120 @@ + + + + + + + + + instance — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

instance

+
+

Contents:

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html new file mode 100644 index 0000000..9f34c04 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/instance.html @@ -0,0 +1,490 @@ + + + + + + + + + instance — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

instance

+

Manage compute instances.

+
+
+class compute.instance.instance.Instance(domain: virDomain)
+

Manage compute instances.

+
+
+__init__(domain: virDomain)
+

Initialise Instance.

+
+
Variables:
+
    +
  • domain (libvirt.virDomain) – domain object

  • +
  • connection (libvirt.virConnect) – connection object

  • +
  • name (str) – domain name

  • +
  • guest_agent (GuestAgent) – GuestAgent object

  • +
+
+
Parameters:
+

domain – libvirt domain object

+
+
+
+ +
+
+attach_device(device: EntityConfig, *, live: bool = False) None
+

Attach device to compute instance.

+
+
Parameters:
+
    +
  • device – Object with device description e.g. DiskConfig

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+delete() None
+

Undefine instance.

+
+ +
+
+delete_ssh_keys(user: str, ssh_keys: list[str]) None
+

Remove SSH keys from guest for specific user.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • ssh_keys – List of public SSH keys.

  • +
+
+
+
+ +
+
+detach_device(device: EntityConfig, *, live: bool = False) None
+

Dettach device from compute instance.

+
+
Parameters:
+
    +
  • device – Object with device description e.g. DiskConfig

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+detach_disk(name: str) None
+

Detach disk device by target name.

+

There is no attach_disk() method. Use attach_device() +with DiskConfig as argument.

+
+
Parameters:
+

name – Disk name e.g. ‘vda’, ‘sda’, etc. This name may +not match the name of the disk inside the guest OS.

+
+
+
+ +
+
+dump_xml(*, inactive: bool = False) str
+

Return instance XML description.

+
+ +
+
+get_disks() list[compute.storage.volume.DiskConfig]
+

Return list of attached disks.

+
+ +
+
+get_info() InstanceInfo
+

Return instance info.

+
+ +
+
+get_max_memory() int
+

Maximum memory value for domain in KiB.

+
+ +
+
+get_max_vcpus() int
+

Maximum vCPUs number for domain.

+
+ +
+
+get_ssh_keys(user: str) list[str]
+

Return list of SSH keys on guest for specific user.

+
+
Parameters:
+

user – Username.

+
+
+
+ +
+
+get_status() str
+

Return instance state: ‘running’, ‘shutoff’, etc.

+

Reference: +https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState

+
+ +
+
+is_autostart() bool
+

Return True if instance autostart is enabled, else return False.

+
+ +
+
+is_running() bool
+

Return True if instance is running, else return False.

+
+ +
+
+pause() None
+

Pause instance.

+
+ +
+
+power_reset() None
+

Shutdown instance and start.

+

By analogy with real hardware, this is a normal server shutdown, +and then turning off from the power supply and turning it on again.

+

This method is applicable in cases where there has been a +configuration change in libvirt and you need to restart the +instance to apply the new configuration.

+
+ +
+
+reboot() None
+

Send ACPI signal to guest OS to reboot. OS may ignore this.

+
+ +
+
+reset() None
+

Reset instance.

+

Copypaste from libvirt doc:

+

Reset a domain immediately without any guest OS shutdown. +Reset emulates the power reset button on a machine, where all +hardware sees the RST line set and reinitializes internal state.

+

Note that there is a risk of data loss caused by reset without any +guest OS shutdown.

+
+ +
+
+resize_disk(name: str, capacity: int, unit: DataUnit) None
+

Resize attached block device.

+
+
Parameters:
+
    +
  • name – Disk device name e.g. vda, sda, etc.

  • +
  • capacity – New capacity.

  • +
  • unit – Capacity unit.

  • +
+
+
+
+ +
+
+resume() None
+

Resume paused instance.

+
+ +
+
+set_autostart(*, enabled: bool) None
+

Set autostart flag for instance.

+
+
Parameters:
+

enabled – Bool argument to set or unset autostart flag.

+
+
+
+ +
+
+set_memory(memory: int, *, live: bool = False) None
+

Set memory.

+

If live is True and instance is not currently running set memory +in config and will applied when instance boot.

+
+
Parameters:
+
    +
  • memory – Memory value in mebibytes

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+set_ssh_keys(user: str, ssh_keys: list[str]) None
+

Add SSH keys to guest for specific user.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • ssh_keys – List of public SSH keys.

  • +
+
+
+
+ +
+
+set_user_password(user: str, password: str, *, encrypted: bool = False) None
+

Set new user password in guest OS.

+

This action performs by guest agent inside the guest.

+
+
Parameters:
+
    +
  • user – Username.

  • +
  • password – Password.

  • +
  • encrypted – Set it to True if password is already encrypted. +Right encryption method depends on guest OS.

  • +
+
+
+
+ +
+
+set_vcpus(nvcpus: int, *, live: bool = False) None
+

Set vCPU number.

+

If live is True and instance is not currently running vCPUs +will set in config and will applied when instance boot.

+

NB: Note that if this call is executed before the guest has +finished booting, the guest may fail to process the change.

+
+
Parameters:
+
    +
  • nvcpus – Number of vCPUs

  • +
  • live – Affect a running instance

  • +
+
+
+
+ +
+
+shutdown(method: str | None = None) None
+

Shutdown instance.

+

Shutdown methods:

+
+
SOFT

Use guest agent to shutdown. If guest agent is unavailable +NORMAL method will be used.

+
+
NORMAL

Use method choosen by hypervisor to shutdown. Usually send ACPI +signal to guest OS. OS may ignore ACPI e.g. if guest is hanged.

+
+
HARD

Shutdown instance without any guest OS shutdown. This is simular +to unplugging machine from power. Internally send SIGTERM to +instance process and destroy it gracefully.

+
+
UNSAFE

Force shutdown. Internally send SIGKILL to instance process. +There is high data corruption risk!

+
+
+

If method is None NORMAL method will used.

+
+
Parameters:
+

method – Method used to shutdown instance

+
+
+
+ +
+
+start() None
+

Start defined instance.

+
+ +
+ +
+
+class compute.instance.instance.InstanceConfig(schema: InstanceSchema)
+

Compute instance XML config builder.

+
+
+__init__(schema: InstanceSchema)
+

Initialise InstanceConfig.

+
+
Parameters:
+

schema – InstanceSchema object

+
+
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+
+class compute.instance.instance.InstanceInfo(state: str, max_memory: int, memory: int, nproc: int, cputime: int)
+

Store compute instance info.

+

Reference: +https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo

+
+
+cputime: int
+

Alias for field number 4

+
+ +
+
+max_memory: int
+

Alias for field number 1

+
+ +
+
+memory: int
+

Alias for field number 2

+
+ +
+
+nproc: int
+

Alias for field number 3

+
+ +
+
+state: str
+

Alias for field number 0

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html new file mode 100644 index 0000000..ef15eb2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/instance/schemas.html @@ -0,0 +1,187 @@ + + + + + + + + + schemas — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

schemas

+

Compute instance related objects schemas.

+
+
+class compute.instance.schemas.BootOptionsSchema(*, order: tuple)
+

Instance boot settings.

+
+ +
+
+class compute.instance.schemas.CPUEmulationMode(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

CPU emulation mode enumerated.

+
+ +
+
+class compute.instance.schemas.CPUFeaturesSchema(*, require: list[str], disable: list[str])
+

CPU features model.

+
+ +
+
+class compute.instance.schemas.CPUSchema(*, emulation_mode: CPUEmulationMode, model: str | None = None, vendor: str | None = None, topology: compute.instance.schemas.CPUTopologySchema | None = None, features: compute.instance.schemas.CPUFeaturesSchema | None = None)
+

CPU model.

+
+ +
+
+class compute.instance.schemas.CPUTopologySchema(*, sockets: int, cores: int, threads: int, dies: int = 1)
+

CPU topology model.

+
+ +
+
+class compute.instance.schemas.EntityModel
+

Basic entity model.

+
+
+class Config
+

Do not allow extra fields.

+
+ +
+ +
+
+class compute.instance.schemas.InstanceSchema(*, name: str, title: str | None = None, description: str | None = None, memory: int, max_memory: int, vcpus: int, max_vcpus: int, cpu: CPUSchema, machine: str, emulator: Path, arch: str, boot: BootOptionsSchema, volumes: list[compute.instance.schemas.VolumeSchema], network_interfaces: list[compute.instance.schemas.NetworkInterfaceSchema], image: str | None = None)
+

Compute instance model.

+
+ +
+
+class compute.instance.schemas.NetworkInterfaceSchema(*, source: str, mac: str)
+

Network inerface model.

+
+ +
+
+class compute.instance.schemas.VolumeCapacitySchema(*, value: int, unit: DataUnit)
+

Storage volume capacity field model.

+
+ +
+
+class compute.instance.schemas.VolumeSchema(*, type: VolumeType, target: str, capacity: VolumeCapacitySchema, source: str | None = None, is_readonly: bool = False, is_system: bool = False)
+

Storage volume model.

+
+ +
+
+class compute.instance.schemas.VolumeType(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

Storage volume types enumeration.

+
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html new file mode 100644 index 0000000..6babb3e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/session.html @@ -0,0 +1,331 @@ + + + + + + + + + session — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

session

+

Hypervisor session manager.

+
+
+class compute.session.Capabilities(arch: str, virt_type: str, emulator: str, machine: str, max_vcpus: int, cpu_vendor: str, cpu_model: str, cpu_features: dict, usable_cpus: list[dict])
+

Store domain capabilities info.

+
+
+arch: str
+

Alias for field number 0

+
+ +
+
+cpu_features: dict
+

Alias for field number 7

+
+ +
+
+cpu_model: str
+

Alias for field number 6

+
+ +
+
+cpu_vendor: str
+

Alias for field number 5

+
+ +
+
+emulator: str
+

Alias for field number 2

+
+ +
+
+machine: str
+

Alias for field number 3

+
+ +
+
+max_vcpus: int
+

Alias for field number 4

+
+ +
+
+usable_cpus: list[dict]
+

Alias for field number 8

+
+ +
+
+virt_type: str
+

Alias for field number 1

+
+ +
+ +
+
+class compute.session.NodeInfo(arch: str, memory: int, cpus: int, mhz: int, nodes: int, sockets: int, cores: int, threads: int)
+

Store compute node info.

+

See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo +NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB.

+
+
+arch: str
+

Alias for field number 0

+
+ +
+
+cores: int
+

Alias for field number 6

+
+ +
+
+cpus: int
+

Alias for field number 2

+
+ +
+
+memory: int
+

Alias for field number 1

+
+ +
+
+mhz: int
+

Alias for field number 3

+
+ +
+
+nodes: int
+

Alias for field number 4

+
+ +
+
+sockets: int
+

Alias for field number 5

+
+ +
+
+threads: int
+

Alias for field number 7

+
+ +
+ +
+
+class compute.session.Session(uri: str | None = None)
+

Hypervisor session context manager.

+
+
Variables:
+
    +
  • IMAGES_POOL – images storage pool name taken from env

  • +
  • VOLUMES_POOL – volumes storage pool name taken from env

  • +
+
+
+
+
+__init__(uri: str | None = None)
+

Initialise session with hypervisor.

+
+
Variables:
+
    +
  • uri (str) – libvirt connection URI.

  • +
  • connection (libvirt.virConnect) – libvirt connection object.

  • +
+
+
Parameters:
+

uri – libvirt connection URI.

+
+
+
+ +
+
+close() None
+

Close connection to libvirt daemon.

+
+ +
+
+create_instance(**kwargs: Any) Instance
+

Create and return new compute instance.

+
+
Parameters:
+
    +
  • name (str) – Instance name.

  • +
  • title (str) – Instance title for humans.

  • +
  • description (str) – Some information about instance.

  • +
  • memory (int) – Memory in MiB.

  • +
  • max_memory (int) – Maximum memory in MiB.

  • +
  • vcpus (int) – Number of vCPUs.

  • +
  • max_vcpus (int) – Maximum vCPUs.

  • +
  • cpu (dict) – CPU configuration. See CPUSchema for info.

  • +
  • machine (str) – QEMU emulated machine.

  • +
  • emulator (str) – Path to emulator.

  • +
  • arch (str) – CPU architecture to virtualization.

  • +
  • boot (dict) – Boot settings. See BootOptionsSchema.

  • +
  • image (str) – Source disk image name for system disk.

  • +
  • volumes (list[dict]) – List of storage volume configs. For more info +see VolumeSchema.

  • +
  • network_interfaces (list[dict]) – List of virtual network interfaces +configs. See NetworkInterfaceSchema for more info.

  • +
+
+
+
+ +
+
+get_capabilities() Capabilities
+

Return capabilities e.g. arch, virt, emulator, etc.

+
+ +
+
+get_instance(name: str) Instance
+

Get compute instance by name.

+
+ +
+
+get_node_info() NodeInfo
+

Return information about compute node.

+
+ +
+
+get_storage_pool(name: str) StoragePool
+

Get storage pool by name.

+
+ +
+
+list_instances() list[compute.instance.instance.Instance]
+

List all instances.

+
+ +
+
+list_storage_pools() list[compute.storage.pool.StoragePool]
+

List all strage pools.

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html new file mode 100644 index 0000000..0a6ca1b --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/index.html @@ -0,0 +1,119 @@ + + + + + + + + + storage — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

storage

+
+

Contents:

+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html new file mode 100644 index 0000000..43fa341 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/pool.html @@ -0,0 +1,201 @@ + + + + + + + + + pool — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

pool

+

Manage storage pools.

+
+
+class compute.storage.pool.StoragePool(pool: virStoragePool)
+

Storage pool manipulating class.

+
+
+__init__(pool: virStoragePool)
+

Initislise StoragePool.

+
+ +
+
+clone_volume(src: Volume, dst: VolumeConfig) Volume
+

Make storage volume copy.

+
+
Parameters:
+
    +
  • src – Input volume

  • +
  • dst – Output volume config

  • +
+
+
+
+ +
+
+create_volume(vol_conf: VolumeConfig) Volume
+

Create storage volume and return Volume instance.

+
+ +
+
+dump_xml() str
+

Return storage pool XML description as string.

+
+ +
+
+get_usage_info() StoragePoolUsageInfo
+

Return info about storage pool usage.

+
+ +
+
+get_volume(name: str) compute.storage.volume.Volume | None
+

Lookup and return Volume instance or None.

+
+ +
+
+list_volumes() list[compute.storage.volume.Volume]
+

Return list of volumes in storage pool.

+
+ +
+
+refresh() None
+

Refresh storage pool.

+
+ +
+ +
+
+class compute.storage.pool.StoragePoolUsageInfo(capacity: int, allocation: int, available: int)
+

Storage pool usage info.

+
+
+allocation: int
+

Alias for field number 1

+
+ +
+
+available: int
+

Alias for field number 2

+
+ +
+
+capacity: int
+

Alias for field number 0

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html new file mode 100644 index 0000000..9e35b8f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/storage/volume.html @@ -0,0 +1,210 @@ + + + + + + + + + volume — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

volume

+

Manage storage volumes.

+
+
+class compute.storage.volume.DiskConfig(disk_type: str, source: str | pathlib.Path, target: str, readonly: bool = False)
+

Disk XML config builder.

+

Generate XML config for attaching or detaching storage volumes +to compute instances.

+
+
+__init__(disk_type: str, source: str | pathlib.Path, target: str, readonly: bool = False) None
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+
+class compute.storage.volume.Volume(pool: virStoragePool, vol: virStorageVol)
+

Storage volume manipulating class.

+
+
+__init__(pool: virStoragePool, vol: virStorageVol)
+

Initialise Volume.

+
+
Parameters:
+
    +
  • pool – libvirt virStoragePool object

  • +
  • vol – libvirt virStorageVol object

  • +
+
+
+
+ +
+
+clone(vol_conf: VolumeConfig) None
+

Make a copy of volume to the same storage pool.

+
+
Parameters:
+

VolumeInfo (vol_info) – New storage volume dataclass object

+
+
+
+ +
+
+delete() None
+

Delete volume from storage pool.

+
+ +
+
+dump_xml() str
+

Return volume XML description as string.

+
+ +
+
+resize(capacity: int, unit: DataUnit) None
+

Resize volume.

+
+
Parameters:
+
    +
  • int (capacity) – Volume new capacity.

  • +
  • DataUnit (unit) – Data unit. Internally converts into bytes.

  • +
+
+
+
+ +
+ +
+
+class compute.storage.volume.VolumeConfig(name: str, path: str, capacity: int)
+

Storage volume XML config builder.

+

Generate XML config for creating a volume in a libvirt +storage pool.

+
+
+__init__(name: str, path: str, capacity: int) None
+
+ +
+
+to_xml() str
+

Return XML config for libvirt.

+
+ +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html new file mode 100644 index 0000000..b09e008 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/pyapi/utils.html @@ -0,0 +1,144 @@ + + + + + + + + + utils — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +
+

utils

+
+

utils.units

+

Tools for data units convertion.

+
+
+class compute.utils.units.DataUnit(value, names=None, *, module=None, qualname=None, type=None, start=1, boundary=None)
+

Data units enumerated.

+
+ +
+
+exception compute.utils.units.InvalidDataUnitError(msg: str)
+

Data unit is not valid.

+
+ +
+
+compute.utils.units.to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) int
+

Convert value to bytes. See DataUnit.

+
+ +
+
+

utils.ids

+

Random identificators.

+
+
+compute.utils.ids.random_mac() str
+

Retrun random MAC address.

+
+ +
+
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html new file mode 100644 index 0000000..60189cf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/search.html @@ -0,0 +1,124 @@ + + + + + + + + Search — Compute 0.1.0 documentation + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + +
+ +

Search

+ + + + +

+ Searching for multiple words only shows matches that contain + all words. +

+ + +
+ + + +
+ + + +
+ +
+ + +
+ +
+
+ +
+
+ + + + + + + \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js new file mode 100644 index 0000000..d20a6ca --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute-doc/usr/share/doc/compute-doc/html/searchindex.js @@ -0,0 +1 @@ +Search.setIndex({"docnames": ["index", "pyapi/exceptions", "pyapi/index", "pyapi/instance/guest_agent", "pyapi/instance/index", "pyapi/instance/instance", "pyapi/instance/schemas", "pyapi/session", "pyapi/storage/index", "pyapi/storage/pool", "pyapi/storage/volume", "pyapi/utils"], "filenames": ["index.rst", "pyapi/exceptions.rst", "pyapi/index.rst", "pyapi/instance/guest_agent.rst", "pyapi/instance/index.rst", "pyapi/instance/instance.rst", "pyapi/instance/schemas.rst", "pyapi/session.rst", "pyapi/storage/index.rst", "pyapi/storage/pool.rst", "pyapi/storage/volume.rst", "pyapi/utils.rst"], "titles": ["Compute", "exceptions", "Python API", "guest_agent", "instance", "instance", "schemas", "session", "storage", "pool", "volume", "utils"], "terms": {"instanc": [0, 2, 3, 6, 7, 9, 10], "manag": [0, 2, 5, 7, 9, 10], "librari": [0, 2], "python": 0, "api": 0, "index": 0, "modul": [0, 6, 11], "search": 0, "page": 0, "comput": [1, 2, 3, 5, 6, 7, 9, 10, 11], "computeerror": [1, 2], "basic": [1, 6], "class": [1, 2, 3, 5, 6, 7, 9, 10, 11], "configloadererror": [1, 2], "someth": 1, "went": 1, "wrong": [1, 7], "when": [1, 5], "load": 1, "configur": [1, 2, 5, 7], "guestagentcommandnotsupportederror": [1, 2, 3], "guest": [1, 3, 5], "agent": [1, 3, 5], "command": [1, 3], "i": [1, 2, 3, 5, 7, 11], "support": [1, 3], "blacklist": 1, "guestagenterror": [1, 2], "wring": 1, "qemu": [1, 3, 7], "call": [1, 2, 5], "guestagenttimeoutexceedederror": [1, 2], "msg": [1, 11], "int": [1, 3, 5, 6, 7, 9, 10, 11], "timeout": [1, 3], "exceed": 1, "guestagentunavailableerror": [1, 2], "connect": [1, 5, 7], "unavail": [1, 5], "instanceerror": [1, 2], "while": 1, "interact": [1, 3], "domain": [1, 3, 5, 7], "instancenotfounderror": [1, 2], "str": [1, 3, 5, 6, 7, 9, 10, 11], "virtual": [1, 7], "machin": [1, 2, 5, 6, 7], "contain": 1, "found": 1, "node": [1, 2, 7], "sessionerror": [1, 2], "libvirtd": 1, "storagepoolerror": [1, 2], "oper": [1, 2], "storag": [1, 2, 5, 6, 7, 9, 10], "pool": [1, 2, 7, 8, 10], "storagepoolnotfounderror": [1, 2], "volumenotfounderror": [1, 2], "volum": [1, 2, 5, 6, 7, 8, 9], "The": 2, "allow": [2, 6], "you": [2, 5], "perform": [2, 5], "action": [2, 5], "programmat": 2, "below": 2, "an": 2, "exampl": [2, 3], "chang": [2, 5], "paramet": [2, 3, 5, 7, 9, 10], "launch": 2, "myinstanc": 2, "import": 2, "log": 2, "from": [2, 5, 7, 10], "session": 2, "basicconfig": 2, "level": 2, "debug": 2, "get_inst": [2, 7], "set_vcpu": [2, 5], "4": [2, 5, 7], "start": [2, 5, 6, 11], "set_autostart": [2, 5], "enabl": [2, 5], "true": [2, 3, 5], "context": [2, 7], "provid": 2, "abstract": 2, "over": 2, "libvirt": [2, 3, 5, 7, 10], "virconnect": [2, 5, 7], "return": [2, 3, 5, 7, 9, 10], "object": [2, 3, 5, 6, 7, 10], "other": 2, "present": 2, "ar": 2, "repres": 2, "These": 2, "directli": 2, "method": [2, 5], "hypervisor": [2, 5, 7], "file": 2, "variou": 2, "describ": 2, "special": 2, "dataclass": [2, 10], "store": [2, 5, 7], "its": 2, "properti": 2, "can": 2, "xml": [2, 5, 9, 10], "config": [2, 5, 6, 7, 9, 10], "us": [2, 3, 5], "to_xml": [2, 5, 10], "For": [2, 3, 7], "volumeconfig": [2, 9, 10], "pydant": 2, "model": [2, 6], "valid": [2, 11], "input": [2, 9], "data": [2, 3, 5, 10, 11], "volumeschema": [2, 6, 7], "capabl": [2, 7], "arch": [2, 6, 7], "cpu_featur": [2, 7], "cpu_model": [2, 7], "cpu_vendor": [2, 7], "emul": [2, 5, 6, 7], "max_vcpu": [2, 6, 7], "usable_cpu": [2, 7], "virt_typ": [2, 7], "nodeinfo": [2, 7], "core": [2, 6, 7], "cpu": [2, 6, 7], "memori": [2, 5, 6, 7], "mhz": [2, 7], "socket": [2, 6, 7], "thread": [2, 6, 7], "__init__": [2, 3, 5, 7, 9, 10], "close": [2, 7], "create_inst": [2, 7], "get_cap": [2, 7], "get_node_info": [2, 7], "get_storage_pool": [2, 7], "list_inst": [2, 7], "list_storage_pool": [2, 7], "attach_devic": [2, 5], "delet": [2, 5, 10], "delete_ssh_kei": [2, 5], "detach_devic": [2, 5], "detach_disk": [2, 5], "dump_xml": [2, 5, 9, 10], "get_disk": [2, 5], "get_info": [2, 5], "get_max_memori": [2, 5], "get_max_vcpu": [2, 5], "get_ssh_kei": [2, 5], "get_statu": [2, 5], "is_autostart": [2, 5], "is_run": [2, 5], "paus": [2, 5], "power_reset": [2, 5], "reboot": [2, 5], "reset": [2, 5], "resize_disk": [2, 5], "resum": [2, 5], "set_memori": [2, 5], "set_ssh_kei": [2, 5], "set_user_password": [2, 5], "shutdown": [2, 5], "instanceconfig": [2, 5], "instanceinfo": [2, 5], "cputim": [2, 5], "max_memori": [2, 5, 6, 7], "nproc": [2, 5], "state": [2, 5], "guest_ag": [2, 4, 5], "guestag": [2, 3, 5], "execut": [2, 3, 5], "get_supported_command": [2, 3], "guest_exec": [2, 3], "guest_exec_statu": [2, 3], "is_avail": [2, 3], "raise_for_command": [2, 3], "guestexecoutput": [2, 3], "exitcod": [2, 3], "exit": [2, 3], "stderr": [2, 3], "stdout": [2, 3], "schema": [2, 4, 5], "bootoptionsschema": [2, 6, 7], "cpuemulationmod": [2, 6], "cpufeaturesschema": [2, 6], "cpuschema": [2, 6, 7], "cputopologyschema": [2, 6], "entitymodel": [2, 6], "instanceschema": [2, 5, 6], "networkinterfaceschema": [2, 6, 7], "volumecapacityschema": [2, 6], "volumetyp": [2, 6], "storagepool": [2, 7, 9], "clone_volum": [2, 9], "create_volum": [2, 9], "get_usage_info": [2, 9], "get_volum": [2, 9], "list_volum": [2, 9], "refresh": [2, 9], "storagepoolusageinfo": [2, 9], "alloc": [2, 9], "avail": [2, 3, 9], "capac": [2, 5, 6, 9, 10], "diskconfig": [2, 5, 10], "clone": [2, 10], "resiz": [2, 5, 10], "util": 2, "unit": [2, 5, 6, 7, 10], "dataunit": [2, 5, 6, 10, 11], "invaliddatauniterror": [2, 11], "to_byt": [2, 11], "id": 2, "random_mac": [2, 11], "except": [2, 3, 11], "virdomain": [3, 5], "60": 3, "initialis": [3, 5, 7, 10], "dict": [3, 7], "see": [3, 5, 7, 11], "http": [3, 5, 7], "project": 3, "gitlab": 3, "io": 3, "interop": 3, "ga": 3, "ref": 3, "html": [3, 5, 7], "output": [3, 9], "type": [3, 6, 11], "set": [3, 5, 6, 7], "path": [3, 6, 7, 10], "arg": 3, "list": [3, 5, 6, 7, 9], "none": [3, 5, 6, 7, 9, 10, 11], "env": [3, 7], "stdin": 3, "capture_output": 3, "bool": [3, 5, 6, 10], "fals": [3, 5, 6, 10], "decode_output": 3, "poll": 3, "exec": 3, "ot": 3, "argument": [3, 5], "pass": 3, "environ": 3, "variabl": [3, 5, 7], "lang": 3, "c": 3, "term": 3, "xterm": 3, "captur": 3, "base64_decod": 3, "decod": 3, "affect": [3, 5], "onli": 3, "self": 3, "poll_interv": 3, "constant": 3, "pid": 3, "float": 3, "0": [3, 5, 7, 9], "3": [3, 5, 7], "statu": 3, "If": [3, 5], "time": 3, "between": 3, "attempt": 3, "obtain": 3, "ping": 3, "unreach": 3, "rais": 3, "requir": [3, 6], "alia": [3, 5, 7, 9], "field": [3, 5, 6, 7, 9], "number": [3, 5, 7, 9], "1": [3, 5, 6, 7, 9, 11], "2": [3, 5, 7, 9], "name": [5, 6, 7, 9, 10, 11], "devic": 5, "entityconfig": 5, "live": 5, "attach": [5, 10], "descript": [5, 6, 7, 9, 10], "e": [5, 7], "g": [5, 7], "run": 5, "undefin": 5, "user": 5, "ssh_kei": 5, "remov": 5, "ssh": 5, "kei": 5, "specif": 5, "usernam": 5, "public": 5, "dettach": 5, "detach": [5, 10], "disk": [5, 7, 10], "target": [5, 6, 10], "There": 5, "attach_disk": 5, "vda": 5, "sda": 5, "etc": [5, 7], "thi": 5, "mai": 5, "match": 5, "insid": 5, "o": 5, "inact": 5, "info": [5, 7, 9], "maximum": [5, 7], "valu": [5, 6, 11], "kib": 5, "vcpu": [5, 6, 7], "shutoff": 5, "refer": 5, "org": [5, 7], "virdomainst": 5, "autostart": 5, "els": 5, "By": 5, "analogi": 5, "real": 5, "hardwar": 5, "normal": 5, "server": 5, "turn": 5, "off": 5, "power": 5, "suppli": 5, "again": 5, "applic": 5, "case": 5, "where": 5, "ha": 5, "been": 5, "need": 5, "restart": 5, "appli": 5, "new": [5, 7, 10], "send": 5, "acpi": 5, "signal": 5, "ignor": 5, "copypast": 5, "doc": [5, 7], "immedi": 5, "without": 5, "ani": [5, 7], "button": 5, "all": [5, 7], "rst": 5, "line": 5, "reiniti": 5, "intern": [5, 10], "note": [5, 7], "risk": 5, "loss": 5, "caus": 5, "block": 5, "flag": 5, "unset": 5, "current": 5, "boot": [5, 6, 7], "mebibyt": 5, "add": 5, "password": 5, "encrypt": 5, "alreadi": 5, "right": 5, "depend": 5, "nvcpu": 5, "nb": 5, "befor": 5, "finish": 5, "fail": 5, "process": 5, "soft": 5, "choosen": 5, "usual": 5, "hang": 5, "hard": 5, "simular": 5, "unplug": 5, "sigterm": 5, "destroi": 5, "gracefulli": 5, "unsaf": 5, "forc": 5, "sigkil": 5, "high": 5, "corrupt": 5, "defin": 5, "builder": [5, 10], "virdomaininfo": 5, "relat": 6, "order": 6, "tupl": 6, "qualnam": [6, 11], "boundari": [6, 11], "mode": 6, "enumer": [6, 11], "disabl": 6, "featur": 6, "emulation_mod": 6, "vendor": 6, "topologi": 6, "di": 6, "entiti": 6, "do": 6, "extra": 6, "titl": [6, 7], "network_interfac": [6, 7], "imag": [6, 7], "sourc": [6, 7, 10], "mac": [6, 11], "network": [6, 7], "inerfac": 6, "is_readonli": 6, "is_system": 6, "7": 7, "6": 7, "5": 7, "8": 7, "host": 7, "virnodeinfo": 7, "actual": 7, "mib": 7, "uri": 7, "images_pool": 7, "taken": 7, "volumes_pool": 7, "daemon": 7, "kwarg": 7, "creat": [7, 9, 10], "human": 7, "some": 7, "inform": 7, "about": [7, 9], "architectur": 7, "system": 7, "more": 7, "interfac": 7, "virt": 7, "get": 7, "strage": 7, "virstoragepool": [9, 10], "manipul": [9, 10], "initislis": 9, "src": 9, "dst": 9, "make": [9, 10], "copi": [9, 10], "vol_conf": [9, 10], "string": [9, 10], "usag": 9, "lookup": 9, "disk_typ": 10, "pathlib": 10, "readonli": 10, "gener": 10, "vol": 10, "virstoragevol": 10, "same": 10, "volumeinfo": 10, "vol_info": 10, "convert": [10, 11], "byte": [10, 11], "tool": 11, "random": 11, "identif": 11, "retrun": 11, "address": 11}, "objects": {"compute": [[1, 0, 0, "-", "exceptions"], [7, 0, 0, "-", "session"]], "compute.exceptions": [[1, 1, 1, "", "ComputeError"], [1, 1, 1, "", "ConfigLoaderError"], [1, 1, 1, "", "GuestAgentCommandNotSupportedError"], [1, 1, 1, "", "GuestAgentError"], [1, 1, 1, "", "GuestAgentTimeoutExceededError"], [1, 1, 1, "", "GuestAgentUnavailableError"], [1, 1, 1, "", "InstanceError"], [1, 1, 1, "", "InstanceNotFoundError"], [1, 1, 1, "", "SessionError"], [1, 1, 1, "", "StoragePoolError"], [1, 1, 1, "", "StoragePoolNotFoundError"], [1, 1, 1, "", "VolumeNotFoundError"]], "compute.instance": [[3, 0, 0, "-", "guest_agent"], [5, 0, 0, "-", "instance"], [6, 0, 0, "-", "schemas"]], "compute.instance.guest_agent": [[3, 2, 1, "", "GuestAgent"], [3, 2, 1, "", "GuestExecOutput"]], "compute.instance.guest_agent.GuestAgent": [[3, 3, 1, "", "__init__"], [3, 3, 1, "", "execute"], [3, 3, 1, "", "get_supported_commands"], [3, 3, 1, "", "guest_exec"], [3, 3, 1, "", "guest_exec_status"], [3, 3, 1, "", "is_available"], [3, 3, 1, "", "raise_for_commands"]], "compute.instance.guest_agent.GuestExecOutput": [[3, 4, 1, "", "exitcode"], [3, 4, 1, "", "exited"], [3, 4, 1, "", "stderr"], [3, 4, 1, "", "stdout"]], "compute.instance.instance": [[5, 2, 1, "", "Instance"], [5, 2, 1, "", "InstanceConfig"], [5, 2, 1, "", "InstanceInfo"]], "compute.instance.instance.Instance": [[5, 3, 1, "", "__init__"], [5, 3, 1, "", "attach_device"], [5, 3, 1, "", "delete"], [5, 3, 1, "", "delete_ssh_keys"], [5, 3, 1, "", "detach_device"], [5, 3, 1, "", "detach_disk"], [5, 3, 1, "", "dump_xml"], [5, 3, 1, "", "get_disks"], [5, 3, 1, "", "get_info"], [5, 3, 1, "", "get_max_memory"], [5, 3, 1, "", "get_max_vcpus"], [5, 3, 1, "", "get_ssh_keys"], [5, 3, 1, "", "get_status"], [5, 3, 1, "", "is_autostart"], [5, 3, 1, "", "is_running"], [5, 3, 1, "", "pause"], [5, 3, 1, "", "power_reset"], [5, 3, 1, "", "reboot"], [5, 3, 1, "", "reset"], [5, 3, 1, "", "resize_disk"], [5, 3, 1, "", "resume"], [5, 3, 1, "", "set_autostart"], [5, 3, 1, "", "set_memory"], [5, 3, 1, "", "set_ssh_keys"], [5, 3, 1, "", "set_user_password"], [5, 3, 1, "", "set_vcpus"], [5, 3, 1, "", "shutdown"], [5, 3, 1, "", "start"]], "compute.instance.instance.InstanceConfig": [[5, 3, 1, "", "__init__"], [5, 3, 1, "", "to_xml"]], "compute.instance.instance.InstanceInfo": [[5, 4, 1, "", "cputime"], [5, 4, 1, "", "max_memory"], [5, 4, 1, "", "memory"], [5, 4, 1, "", "nproc"], [5, 4, 1, "", "state"]], "compute.instance.schemas": [[6, 2, 1, "", "BootOptionsSchema"], [6, 2, 1, "", "CPUEmulationMode"], [6, 2, 1, "", "CPUFeaturesSchema"], [6, 2, 1, "", "CPUSchema"], [6, 2, 1, "", "CPUTopologySchema"], [6, 2, 1, "", "EntityModel"], [6, 2, 1, "", "InstanceSchema"], [6, 2, 1, "", "NetworkInterfaceSchema"], [6, 2, 1, "", "VolumeCapacitySchema"], [6, 2, 1, "", "VolumeSchema"], [6, 2, 1, "", "VolumeType"]], "compute.instance.schemas.EntityModel": [[6, 2, 1, "", "Config"]], "compute.session": [[7, 2, 1, "", "Capabilities"], [7, 2, 1, "", "NodeInfo"], [7, 2, 1, "", "Session"]], "compute.session.Capabilities": [[7, 4, 1, "", "arch"], [7, 4, 1, "", "cpu_features"], [7, 4, 1, "", "cpu_model"], [7, 4, 1, "", "cpu_vendor"], [7, 4, 1, "", "emulator"], [7, 4, 1, "", "machine"], [7, 4, 1, "", "max_vcpus"], [7, 4, 1, "", "usable_cpus"], [7, 4, 1, "", "virt_type"]], "compute.session.NodeInfo": [[7, 4, 1, "", "arch"], [7, 4, 1, "", "cores"], [7, 4, 1, "", "cpus"], [7, 4, 1, "", "memory"], [7, 4, 1, "", "mhz"], [7, 4, 1, "", "nodes"], [7, 4, 1, "", "sockets"], [7, 4, 1, "", "threads"]], "compute.session.Session": [[7, 3, 1, "", "__init__"], [7, 3, 1, "", "close"], [7, 3, 1, "", "create_instance"], [7, 3, 1, "", "get_capabilities"], [7, 3, 1, "", "get_instance"], [7, 3, 1, "", "get_node_info"], [7, 3, 1, "", "get_storage_pool"], [7, 3, 1, "", "list_instances"], [7, 3, 1, "", "list_storage_pools"]], "compute.storage": [[9, 0, 0, "-", "pool"], [10, 0, 0, "-", "volume"]], "compute.storage.pool": [[9, 2, 1, "", "StoragePool"], [9, 2, 1, "", "StoragePoolUsageInfo"]], "compute.storage.pool.StoragePool": [[9, 3, 1, "", "__init__"], [9, 3, 1, "", "clone_volume"], [9, 3, 1, "", "create_volume"], [9, 3, 1, "", "dump_xml"], [9, 3, 1, "", "get_usage_info"], [9, 3, 1, "", "get_volume"], [9, 3, 1, "", "list_volumes"], [9, 3, 1, "", "refresh"]], "compute.storage.pool.StoragePoolUsageInfo": [[9, 4, 1, "", "allocation"], [9, 4, 1, "", "available"], [9, 4, 1, "", "capacity"]], "compute.storage.volume": [[10, 2, 1, "", "DiskConfig"], [10, 2, 1, "", "Volume"], [10, 2, 1, "", "VolumeConfig"]], "compute.storage.volume.DiskConfig": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "to_xml"]], "compute.storage.volume.Volume": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "clone"], [10, 3, 1, "", "delete"], [10, 3, 1, "", "dump_xml"], [10, 3, 1, "", "resize"]], "compute.storage.volume.VolumeConfig": [[10, 3, 1, "", "__init__"], [10, 3, 1, "", "to_xml"]], "compute.utils": [[11, 0, 0, "-", "ids"], [11, 0, 0, "-", "units"]], "compute.utils.ids": [[11, 5, 1, "", "random_mac"]], "compute.utils.units": [[11, 2, 1, "", "DataUnit"], [11, 1, 1, "", "InvalidDataUnitError"], [11, 5, 1, "", "to_bytes"]]}, "objtypes": {"0": "py:module", "1": "py:exception", "2": "py:class", "3": "py:method", "4": "py:attribute", "5": "py:function"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "exception", "Python exception"], "2": ["py", "class", "Python class"], "3": ["py", "method", "Python method"], "4": ["py", "attribute", "Python attribute"], "5": ["py", "function", "Python function"]}, "titleterms": {"comput": 0, "indic": 0, "tabl": 0, "except": 1, "python": 2, "api": 2, "entiti": 2, "represent": 2, "modul": 2, "document": 2, "guest_ag": 3, "instanc": [4, 5], "content": [4, 8], "schema": 6, "session": 7, "storag": 8, "pool": 9, "volum": 10, "util": 11, "unit": 11, "id": 11}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 8, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 57}, "alltitles": {"Compute": [[0, "compute"]], "Indices and tables": [[0, "indices-and-tables"]], "exceptions": [[1, "module-compute.exceptions"]], "Python API": [[2, "python-api"]], "Entity representation": [[2, "entity-representation"]], "Modules documentation": [[2, "modules-documentation"]], "guest_agent": [[3, "module-compute.instance.guest_agent"]], "instance": [[4, "instance"], [5, "module-compute.instance.instance"]], "Contents:": [[4, null], [8, null]], "schemas": [[6, "module-compute.instance.schemas"]], "session": [[7, "module-compute.session"]], "storage": [[8, "storage"]], "pool": [[9, "module-compute.storage.pool"]], "volume": [[10, "module-compute.storage.volume"]], "utils": [[11, "utils"]], "utils.units": [[11, "module-compute.utils.units"]], "utils.ids": [[11, "module-compute.utils.ids"]]}, "indexentries": {"computeerror": [[1, "compute.exceptions.ComputeError"]], "configloadererror": [[1, "compute.exceptions.ConfigLoaderError"]], "guestagentcommandnotsupportederror": [[1, "compute.exceptions.GuestAgentCommandNotSupportedError"]], "guestagenterror": [[1, "compute.exceptions.GuestAgentError"]], "guestagenttimeoutexceedederror": [[1, "compute.exceptions.GuestAgentTimeoutExceededError"]], "guestagentunavailableerror": [[1, "compute.exceptions.GuestAgentUnavailableError"]], "instanceerror": [[1, "compute.exceptions.InstanceError"]], "instancenotfounderror": [[1, "compute.exceptions.InstanceNotFoundError"]], "sessionerror": [[1, "compute.exceptions.SessionError"]], "storagepoolerror": [[1, "compute.exceptions.StoragePoolError"]], "storagepoolnotfounderror": [[1, "compute.exceptions.StoragePoolNotFoundError"]], "volumenotfounderror": [[1, "compute.exceptions.VolumeNotFoundError"]], "compute.exceptions": [[1, "module-compute.exceptions"]], "module": [[1, "module-compute.exceptions"], [3, "module-compute.instance.guest_agent"], [5, "module-compute.instance.instance"], [6, "module-compute.instance.schemas"], [7, "module-compute.session"], [9, "module-compute.storage.pool"], [10, "module-compute.storage.volume"], [11, "module-compute.utils.ids"], [11, "module-compute.utils.units"]], "guestagent (class in compute.instance.guest_agent)": [[3, "compute.instance.guest_agent.GuestAgent"]], "guestexecoutput (class in compute.instance.guest_agent)": [[3, "compute.instance.guest_agent.GuestExecOutput"]], "__init__() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.__init__"]], "compute.instance.guest_agent": [[3, "module-compute.instance.guest_agent"]], "execute() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.execute"]], "exitcode (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.exitcode"]], "exited (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.exited"]], "get_supported_commands() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.get_supported_commands"]], "guest_exec() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.guest_exec"]], "guest_exec_status() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.guest_exec_status"]], "is_available() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.is_available"]], "raise_for_commands() (compute.instance.guest_agent.guestagent method)": [[3, "compute.instance.guest_agent.GuestAgent.raise_for_commands"]], "stderr (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.stderr"]], "stdout (compute.instance.guest_agent.guestexecoutput attribute)": [[3, "compute.instance.guest_agent.GuestExecOutput.stdout"]], "instance (class in compute.instance.instance)": [[5, "compute.instance.instance.Instance"]], "instanceconfig (class in compute.instance.instance)": [[5, "compute.instance.instance.InstanceConfig"]], "instanceinfo (class in compute.instance.instance)": [[5, "compute.instance.instance.InstanceInfo"]], "__init__() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.__init__"]], "__init__() (compute.instance.instance.instanceconfig method)": [[5, "compute.instance.instance.InstanceConfig.__init__"]], "attach_device() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.attach_device"]], "compute.instance.instance": [[5, "module-compute.instance.instance"]], "cputime (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.cputime"]], "delete() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.delete"]], "delete_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.delete_ssh_keys"]], "detach_device() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.detach_device"]], "detach_disk() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.detach_disk"]], "dump_xml() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.dump_xml"]], "get_disks() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_disks"]], "get_info() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_info"]], "get_max_memory() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_max_memory"]], "get_max_vcpus() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_max_vcpus"]], "get_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_ssh_keys"]], "get_status() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.get_status"]], "is_autostart() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.is_autostart"]], "is_running() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.is_running"]], "max_memory (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.max_memory"]], "memory (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.memory"]], "nproc (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.nproc"]], "pause() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.pause"]], "power_reset() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.power_reset"]], "reboot() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.reboot"]], "reset() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.reset"]], "resize_disk() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.resize_disk"]], "resume() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.resume"]], "set_autostart() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_autostart"]], "set_memory() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_memory"]], "set_ssh_keys() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_ssh_keys"]], "set_user_password() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_user_password"]], "set_vcpus() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.set_vcpus"]], "shutdown() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.shutdown"]], "start() (compute.instance.instance.instance method)": [[5, "compute.instance.instance.Instance.start"]], "state (compute.instance.instance.instanceinfo attribute)": [[5, "compute.instance.instance.InstanceInfo.state"]], "to_xml() (compute.instance.instance.instanceconfig method)": [[5, "compute.instance.instance.InstanceConfig.to_xml"]], "bootoptionsschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.BootOptionsSchema"]], "cpuemulationmode (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUEmulationMode"]], "cpufeaturesschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUFeaturesSchema"]], "cpuschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUSchema"]], "cputopologyschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.CPUTopologySchema"]], "entitymodel (class in compute.instance.schemas)": [[6, "compute.instance.schemas.EntityModel"]], "entitymodel.config (class in compute.instance.schemas)": [[6, "compute.instance.schemas.EntityModel.Config"]], "instanceschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.InstanceSchema"]], "networkinterfaceschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.NetworkInterfaceSchema"]], "volumecapacityschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeCapacitySchema"]], "volumeschema (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeSchema"]], "volumetype (class in compute.instance.schemas)": [[6, "compute.instance.schemas.VolumeType"]], "compute.instance.schemas": [[6, "module-compute.instance.schemas"]], "capabilities (class in compute.session)": [[7, "compute.session.Capabilities"]], "nodeinfo (class in compute.session)": [[7, "compute.session.NodeInfo"]], "session (class in compute.session)": [[7, "compute.session.Session"]], "__init__() (compute.session.session method)": [[7, "compute.session.Session.__init__"]], "arch (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.arch"]], "arch (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.arch"]], "close() (compute.session.session method)": [[7, "compute.session.Session.close"]], "compute.session": [[7, "module-compute.session"]], "cores (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.cores"]], "cpu_features (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_features"]], "cpu_model (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_model"]], "cpu_vendor (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.cpu_vendor"]], "cpus (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.cpus"]], "create_instance() (compute.session.session method)": [[7, "compute.session.Session.create_instance"]], "emulator (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.emulator"]], "get_capabilities() (compute.session.session method)": [[7, "compute.session.Session.get_capabilities"]], "get_instance() (compute.session.session method)": [[7, "compute.session.Session.get_instance"]], "get_node_info() (compute.session.session method)": [[7, "compute.session.Session.get_node_info"]], "get_storage_pool() (compute.session.session method)": [[7, "compute.session.Session.get_storage_pool"]], "list_instances() (compute.session.session method)": [[7, "compute.session.Session.list_instances"]], "list_storage_pools() (compute.session.session method)": [[7, "compute.session.Session.list_storage_pools"]], "machine (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.machine"]], "max_vcpus (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.max_vcpus"]], "memory (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.memory"]], "mhz (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.mhz"]], "nodes (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.nodes"]], "sockets (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.sockets"]], "threads (compute.session.nodeinfo attribute)": [[7, "compute.session.NodeInfo.threads"]], "usable_cpus (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.usable_cpus"]], "virt_type (compute.session.capabilities attribute)": [[7, "compute.session.Capabilities.virt_type"]], "storagepool (class in compute.storage.pool)": [[9, "compute.storage.pool.StoragePool"]], "storagepoolusageinfo (class in compute.storage.pool)": [[9, "compute.storage.pool.StoragePoolUsageInfo"]], "__init__() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.__init__"]], "allocation (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.allocation"]], "available (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.available"]], "capacity (compute.storage.pool.storagepoolusageinfo attribute)": [[9, "compute.storage.pool.StoragePoolUsageInfo.capacity"]], "clone_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.clone_volume"]], "compute.storage.pool": [[9, "module-compute.storage.pool"]], "create_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.create_volume"]], "dump_xml() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.dump_xml"]], "get_usage_info() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.get_usage_info"]], "get_volume() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.get_volume"]], "list_volumes() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.list_volumes"]], "refresh() (compute.storage.pool.storagepool method)": [[9, "compute.storage.pool.StoragePool.refresh"]], "diskconfig (class in compute.storage.volume)": [[10, "compute.storage.volume.DiskConfig"]], "volume (class in compute.storage.volume)": [[10, "compute.storage.volume.Volume"]], "volumeconfig (class in compute.storage.volume)": [[10, "compute.storage.volume.VolumeConfig"]], "__init__() (compute.storage.volume.diskconfig method)": [[10, "compute.storage.volume.DiskConfig.__init__"]], "__init__() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.__init__"]], "__init__() (compute.storage.volume.volumeconfig method)": [[10, "compute.storage.volume.VolumeConfig.__init__"]], "clone() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.clone"]], "compute.storage.volume": [[10, "module-compute.storage.volume"]], "delete() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.delete"]], "dump_xml() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.dump_xml"]], "resize() (compute.storage.volume.volume method)": [[10, "compute.storage.volume.Volume.resize"]], "to_xml() (compute.storage.volume.diskconfig method)": [[10, "compute.storage.volume.DiskConfig.to_xml"]], "to_xml() (compute.storage.volume.volumeconfig method)": [[10, "compute.storage.volume.VolumeConfig.to_xml"]], "dataunit (class in compute.utils.units)": [[11, "compute.utils.units.DataUnit"]], "invaliddatauniterror": [[11, "compute.utils.units.InvalidDataUnitError"]], "compute.utils.ids": [[11, "module-compute.utils.ids"]], "compute.utils.units": [[11, "module-compute.utils.units"]], "random_mac() (in module compute.utils.ids)": [[11, "compute.utils.ids.random_mac"]], "to_bytes() (in module compute.utils.units)": [[11, "compute.utils.units.to_bytes"]]}}) \ No newline at end of file diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion b/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.bash-completion @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log b/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log new file mode 100644 index 0000000..8dc2028 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.debhelper.log @@ -0,0 +1 @@ +dh_sphinxdoc diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper b/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper new file mode 100644 index 0000000..2545e7a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.postinst.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p compute +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p compute || true +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper b/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper new file mode 100644 index 0000000..062ac2f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.prerm.debhelper @@ -0,0 +1,10 @@ + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p compute +else + dpkg -L compute | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute.substvars b/packaging/build/compute-0.1.0.dev1/debian/compute.substvars new file mode 100644 index 0000000..6561153 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute.substvars @@ -0,0 +1,3 @@ +python3:Depends=python3-libvirt, python3-lxml, python3-pydantic, python3-yaml, python3:any +misc:Depends= +misc:Pre-Depends= diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control new file mode 100644 index 0000000..906243f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/control @@ -0,0 +1,12 @@ +Package: compute +Version: 0.1.0.dev1-1 +Architecture: all +Maintainer: ge +Installed-Size: 118 +Depends: python3-libvirt, python3-lxml, python3-pydantic, python3-yaml, python3:any, qemu-system, qemu-utils, libvirt-daemon-system, libvirt-clients +Recommends: dnsmasq +Suggests: compute-doc +Section: admin +Priority: optional +Homepage: https://git.lulzette.ru/hstack/compute +Description: Compute instances management library and tools (Python 3) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums new file mode 100644 index 0000000..28d1b77 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/md5sums @@ -0,0 +1,27 @@ +e21aa7b0b8fd557e047296cdf5ced826 usr/bin/compute +f9bc2efd4317ac0a92b8c7d283b947b8 usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA +db790365fd79ce4e960409f8cfc71dae usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD +b65598d0aa0cfe0f390246499e741adb usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL +d6561300b96471e4e471ea1615006527 usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt +9c54095f8462231dc4be8f87fadee594 usr/lib/python3/dist-packages/compute/__init__.py +a1b4018266bd8295c5e829c45948f642 usr/lib/python3/dist-packages/compute/__main__.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/compute/cli/__init__.py +8c13534e878816096e129b15462d0840 usr/lib/python3/dist-packages/compute/cli/control.py +1c4b0023246c9cd9d37e2addc255d7f9 usr/lib/python3/dist-packages/compute/common.py +665c006c01d16e64323037b0089cacef usr/lib/python3/dist-packages/compute/exceptions.py +1ff1400c5f71bd3a55ce2521258b5bd2 usr/lib/python3/dist-packages/compute/instance/__init__.py +82ec67ce83d65b991a8aba5e70f30e76 usr/lib/python3/dist-packages/compute/instance/guest_agent.py +135f6785552229c6fac04ab1d7c3113b usr/lib/python3/dist-packages/compute/instance/instance.py +00df3cb0195a2b97f1972f020bdbb243 usr/lib/python3/dist-packages/compute/instance/schemas.py +1d557cf313b52726a3591bd2e59c3c9b usr/lib/python3/dist-packages/compute/session.py +0a98a65c1a665afb4e4ed9cb3aef38f5 usr/lib/python3/dist-packages/compute/storage/__init__.py +ecf7a8e68c733d8e5b241ca33ae7cae0 usr/lib/python3/dist-packages/compute/storage/pool.py +c4a6cb9dbccfaa9217c2dbc4a833e8c9 usr/lib/python3/dist-packages/compute/storage/volume.py +d41d8cd98f00b204e9800998ecf8427e usr/lib/python3/dist-packages/compute/utils/__init__.py +e7797202c176137f38a6652cf45170a2 usr/lib/python3/dist-packages/compute/utils/config_loader.py +6c36830706d7d714d9b3c1d23dcccf14 usr/lib/python3/dist-packages/compute/utils/ids.py +964156c54ebe27ba2b14313f8f9f9754 usr/lib/python3/dist-packages/compute/utils/units.py +1fd80db613384b8d5782cf8c5843eb94 usr/share/bash-completion/completions/compute +672a4b3f13e2a14e4040c7a513ed60ba usr/share/doc/compute/README.md +6845278a102bd147f30f770ed1134ce5 usr/share/doc/compute/changelog.Debian.gz +fb1a6c11d7a8fa5f238617c20b13b6a1 usr/share/doc/compute/copyright diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst new file mode 100755 index 0000000..cebdb00 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/postinst @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3compile >/dev/null 2>&1; then + py3compile -p compute +fi +if command -v pypy3compile >/dev/null 2>&1; then + pypy3compile -p compute || true +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm new file mode 100755 index 0000000..d867122 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/DEBIAN/prerm @@ -0,0 +1,12 @@ +#!/bin/sh +set -e + +# Automatically added by dh_python3 +if command -v py3clean >/dev/null 2>&1; then + py3clean -p compute +else + dpkg -L compute | sed -En -e '/^(.*)\/(.+)\.py$/s,,rm "\1/__pycache__/\2".*,e' + find /usr/lib/python3/dist-packages/ -type d -name __pycache__ -empty -print0 | xargs --null --no-run-if-empty rmdir +fi + +# End automatically added section diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute new file mode 100755 index 0000000..56e33f2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/bin/compute @@ -0,0 +1,8 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from compute.cli.control import cli +if __name__ == "__main__": + sys.argv[0] = re.sub(r"(-script\.pyw|\.exe)?$", "", sys.argv[0]) + sys.exit(cli()) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA new file mode 100644 index 0000000..f4c22ad --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/METADATA @@ -0,0 +1,81 @@ +Metadata-Version: 2.1 +Name: compute +Version: 0.1.0.dev1 +Summary: Compute instances management library and tools +Author: ge +Author-email: ge@nixhacks.net +Requires-Python: >=3.11,<4.0 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.11 +Requires-Dist: libvirt-python (==9.0.0) +Requires-Dist: lxml (>=4.9.2,<5.0.0) +Requires-Dist: pydantic (==1.10.4) +Requires-Dist: pyyaml (>=6.0.1,<7.0.0) +Description-Content-Type: text/markdown + +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` + diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD new file mode 100644 index 0000000..5f97163 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/RECORD @@ -0,0 +1,23 @@ +../scripts/compute,sha256=b-Gj6H6ssfbGalpouUMSX5pmsjqDnN9xMdTwnU-UfZY,216 +compute/__init__.py,sha256=x4zp_CoVPKgDT6AqhometspAyinGxJUXO48duJ5aHUM,873 +compute/__main__.py,sha256=zJyKJul6pCbguFPtVLZBoAuZl9RXibn4CCMn46jIgUQ,745 +compute/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/cli/control.py,sha256=83wnR21pHOPyyk1i1n_YBIDz6dCFB6hmuIFguIk68rs,14634 +compute/common.py,sha256=G1qwC1EybG5LEJtyoux9ymiqB2ZOsgKXlCpbuhHv55Y,948 +compute/exceptions.py,sha256=Ga59L55qSAPeyDfjANPuMh4yVSRWHDYi9xqq5o4_7-0,2452 +compute/instance/__init__.py,sha256=kHN8jVamyrBZYZgi62tPtJ7rS73gUPhfswLalmPA5Zs,772 +compute/instance/guest_agent.py,sha256=fq89kQbcV5X5eFCsMmujRuwTOSghWO4ZhAjvxyUu84M,7018 +compute/instance/instance.py,sha256=WP6oTJfdAf6QlefwVLqdC8J6XoKHum6nZhwwHOEtjNk,23297 +compute/instance/schemas.py,sha256=B51ytPlxhnx0MrkR2WYhd49RaRT7Is7NsIM9OrMUpvI,4288 +compute/session.py,sha256=znYOIzoiCbSG62k-ViaXti_lOnw88wD8Syp3nCXAJ28,10050 +compute/storage/__init__.py,sha256=zNaVjZ2925DxrVUFWwVRsGU6bSYbF46sb4L6NsaiKbw,736 +compute/storage/pool.py,sha256=9z99bBDbb4ATGpfMkEWpxAO4fEQHNVOxxf0iUln9cN0,4197 +compute/storage/volume.py,sha256=_TbK9Y4d3NAeknPUiuhldAT3ZaN1sZgjy4QzC-Sw4Io,4110 +compute/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +compute/utils/config_loader.py,sha256=ul1J3sZg0D9R0HbOz5Pg9JmL4nFaMahAzQEdGaWFABU,1989 +compute/utils/ids.py,sha256=fg6Xsg4OMM-BIaU3DPu0L91ICwx-L3qNoELEwQZz2s0,1007 +compute/utils/units.py,sha256=UkwD0zQ-rlpSpkbfezCcvJx4D8iZlI9M-oXXvdVEvy0,1549 +compute-0.1.0.dev1.dist-info/METADATA,sha256=tbX8xp92Jwqf44sOwPB-HqKHLezab5dU9DrQDYFitDQ,1944 +compute-0.1.0.dev1.dist-info/WHEEL,sha256=vVCvjcmxuUltf8cYhJ0sJMRDLr1XsPuxEId8YDzbyCY,88 +compute-0.1.0.dev1.dist-info/entry_points.txt,sha256=xHhg-Fo9Z5gJnIahbG8pVIGNDqlH5Eordn8hnXUwscw,51 +compute-0.1.0.dev1.dist-info/RECORD,, diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL new file mode 100644 index 0000000..4ba7671 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 1.4.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt new file mode 100644 index 0000000..4130f9f --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute-0.1.0.dev1.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +compute=compute.cli.control:cli + diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py new file mode 100644 index 0000000..ffe06d7 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__init__.py @@ -0,0 +1,22 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instances management library.""" + +__version__ = '0.1.0-dev1' + +from .instance import Instance, InstanceConfig, InstanceSchema +from .session import Session +from .storage import StoragePool, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py new file mode 100644 index 0000000..4995fbd --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/__main__.py @@ -0,0 +1,21 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface for compute module.""" + +from compute.cli import main + + +main.cli() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py new file mode 100644 index 0000000..f5a5b91 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/cli/control.py @@ -0,0 +1,501 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Command line interface.""" + +import argparse +import io +import logging +import os +import shlex +import sys +from collections import UserDict +from typing import Any +from uuid import uuid4 + +import libvirt +import yaml +from pydantic import ValidationError + +from compute import __version__ +from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError +from compute.instance import GuestAgent +from compute.session import Session +from compute.utils import ids + + +log = logging.getLogger(__name__) +log_levels = [lv.lower() for lv in logging.getLevelNamesMapping()] + +libvirt.registerErrorHandler( + lambda userdata, err: None, # noqa: ARG005 + ctx=None, +) + + +class Table: + """Minimalistic text table constructor.""" + + def __init__(self, whitespace: str | None = None): + """Initialise Table.""" + self.whitespace = whitespace or '\t' + self.header = [] + self.rows = [] + self.table = '' + + def add_row(self, row: list) -> None: + """Add table row.""" + self.rows.append([str(col) for col in row]) + + def add_rows(self, rows: list[list]) -> None: + """Add multiple rows.""" + for row in rows: + self.add_row(row) + + def __str__(self) -> str: + """Build table and return.""" + widths = [max(map(len, col)) for col in zip(*self.rows, strict=True)] + self.rows.insert(0, [str(h).upper() for h in self.header]) + for row in self.rows: + self.table += self.whitespace.join( + ( + val.ljust(width) + for val, width in zip(row, widths, strict=True) + ) + ) + self.table += '\n' + return self.table.strip() + + +def _list_instances(session: Session) -> None: + table = Table() + table.header = ['NAME', 'STATE'] + for instance in session.list_instances(): + table.add_row( + [ + instance.name, + instance.get_status(), + ] + ) + print(table) + sys.exit() + + +def _exec_guest_agent_command( + session: Session, args: argparse.Namespace +) -> None: + instance = session.get_instance(args.instance) + ga = GuestAgent(instance.domain, timeout=args.timeout) + arguments = args.arguments.copy() + if len(arguments) > 1 and not args.no_join_args: + arguments = [shlex.join(arguments)] + if not args.no_join_args: + arguments.insert(0, '-c') + stdin = None + if not sys.stdin.isatty(): + stdin = sys.stdin.read() + try: + output = ga.guest_exec( + path=args.executable, + args=arguments, + env=args.env, + stdin=stdin, + capture_output=True, + decode_output=True, + poll=True, + ) + except GuestAgentTimeoutExceededError as e: + sys.exit( + f'{e}. NOTE: command may still running in guest, ' + f'PID={ga.last_pid}' + ) + if output.stderr: + print(output.stderr.strip(), file=sys.stderr) + if output.stdout: + print(output.stdout.strip(), file=sys.stdout) + sys.exit(output.exitcode) + + +class _NotPresent: + """ + Type for representing non-existent dictionary keys. + + See :class:`_FillableDict`. + """ + + +class _FillableDict(UserDict): + """Use :method:`fill` to add key if not present.""" + + def __init__(self, data: dict): + self.data = data + + def fill(self, key: str, value: Any) -> None: # noqa: ANN401 + if self.data.get(key, _NotPresent) is _NotPresent: + self.data[key] = value + + +def _merge_dicts(a: dict, b: dict, path: list[str] | None = None) -> dict: + """Merge `b` into `a`. Return modified `a`.""" + if path is None: + path = [] + for key in b: + if key in a: + if isinstance(a[key], dict) and isinstance(b[key], dict): + _merge_dicts(a[key], b[key], [path + str(key)]) + elif a[key] == b[key]: + pass # same leaf value + else: + a[key] = b[key] # replace existing key's values + else: + a[key] = b[key] + return a + + +def _create_instance(session: Session, file: io.TextIOWrapper) -> None: + try: + data = _FillableDict(yaml.load(file.read(), Loader=yaml.SafeLoader)) + log.debug('Read from file: %s', data) + except yaml.YAMLError as e: + sys.exit(f'error: cannot parse YAML: {e}') + + capabilities = session.get_capabilities() + node_info = session.get_node_info() + + data.fill('name', uuid4().hex) + data.fill('title', None) + data.fill('description', None) + data.fill('arch', capabilities.arch) + data.fill('machine', capabilities.machine) + data.fill('emulator', capabilities.emulator) + data.fill('max_vcpus', node_info.cpus) + data.fill('max_memory', node_info.memory) + data.fill('cpu', {}) + cpu = { + 'emulation_mode': 'host-passthrough', + 'model': None, + 'vendor': None, + 'topology': None, + 'features': None, + } + data['cpu'] = _merge_dicts(data['cpu'], cpu) + data.fill( + 'network_interfaces', + [{'source': 'default', 'mac': ids.random_mac()}], + ) + data.fill('boot', {'order': ['cdrom', 'hd']}) + + try: + log.debug('Input data: %s', data) + session.create_instance(**data) + except ValidationError as e: + for error in e.errors(): + fields = '.'.join([str(lc) for lc in error['loc']]) + print( + f"validation error: {fields}: {error['msg']}", + file=sys.stderr, + ) + sys.exit() + + +def _shutdown_instance(session: Session, args: argparse.Namespace) -> None: + instance = session.get_instance(args.instance) + if args.soft: + method = 'SOFT' + elif args.hard: + method = 'HARD' + elif args.unsafe: + method = 'UNSAFE' + else: + method = 'NORMAL' + instance.shutdown(method) + + +def main(session: Session, args: argparse.Namespace) -> None: + """Perform actions.""" + match args.command: + case 'init': + _create_instance(session, args.file) + case 'exec': + _exec_guest_agent_command(session, args) + case 'ls': + _list_instances(session) + case 'start': + instance = session.get_instance(args.instance) + instance.start() + case 'shutdown': + _shutdown_instance(session, args) + case 'reboot': + instance = session.get_instance(args.instance) + instance.reboot() + case 'reset': + instance = session.get_instance(args.instance) + instance.reset() + case 'powrst': + instance = session.get_instance(args.instance) + instance.power_reset() + case 'pause': + instance = session.get_instance(args.instance) + instance.pause() + case 'resume': + instance = session.get_instance(args.instance) + instance.resume() + case 'status': + instance = session.get_instance(args.instance) + print(instance.status) + case 'setvcpus': + instance = session.get_instance(args.instance) + instance.set_vcpus(args.nvcpus, live=True) + case 'setmem': + instance = session.get_instance(args.instance) + instance.set_memory(args.memory, live=True) + case 'setpass': + instance = session.get_instance(args.instance) + instance.set_user_password( + args.username, + args.password, + encrypted=args.encrypted, + ) + + +def cli() -> None: # noqa: PLR0915 + """Return command line arguments parser.""" + root = argparse.ArgumentParser( + prog='compute', + description='manage compute instances', + formatter_class=argparse.RawTextHelpFormatter, + ) + root.add_argument( + '-v', + '--verbose', + action='store_true', + default=False, + help='enable verbose mode', + ) + root.add_argument( + '-c', + '--connect', + metavar='URI', + help='libvirt connection URI', + ) + root.add_argument( + '-l', + '--log-level', + type=str.lower, + metavar='LEVEL', + choices=log_levels, + help='log level', + ) + root.add_argument( + '-V', + '--version', + action='version', + version=__version__, + ) + subparsers = root.add_subparsers(dest='command', metavar='COMMAND') + + # init command + init = subparsers.add_parser( + 'init', help='initialise instance using YAML config file' + ) + init.add_argument( + 'file', + type=argparse.FileType('r', encoding='UTF-8'), + nargs='?', + default='instance.yaml', + help='instance config [default: instance.yaml]', + ) + + # exec subcommand + execute = subparsers.add_parser( + 'exec', + help='execute command in guest via guest agent', + description=( + 'NOTE: any argument after instance name will be passed into ' + 'guest as shell command.' + ), + ) + execute.add_argument('instance') + execute.add_argument('arguments', nargs=argparse.REMAINDER) + execute.add_argument( + '-t', + '--timeout', + type=int, + default=60, + help=( + 'waiting time in seconds for a command to be executed ' + 'in guest [default: 60]' + ), + ) + execute.add_argument( + '-x', + '--executable', + default='/bin/sh', + help='path to executable in guest [default: /bin/sh]', + ) + execute.add_argument( + '-e', + '--env', + type=str, + nargs='?', + action='append', + help='environment variables to pass to executable in guest', + ) + execute.add_argument( + '-n', + '--no-join-args', + action='store_true', + default=False, + help=( + "do not join arguments list and add '-c' option, suitable " + 'for non-shell executables and other specific cases.' + ), + ) + + # ls subcommand + listall = subparsers.add_parser('ls', help='list instances') + listall.add_argument( + '-a', + '--all', + action='store_true', + default=False, + help='list all instances including inactive', + ) + + # start subcommand + start = subparsers.add_parser('start', help='start instance') + start.add_argument('instance') + + # shutdown subcommand + shutdown = subparsers.add_parser('shutdown', help='shutdown instance') + shutdown.add_argument('instance') + shutdown_opts = shutdown.add_mutually_exclusive_group() + shutdown_opts.add_argument( + '-s', + '--soft', + action='store_true', + help='normal guest OS shutdown, guest agent is used', + ) + shutdown_opts.add_argument( + '-n', + '--normal', + action='store_true', + help='shutdown with hypervisor selected method [default]', + ) + shutdown_opts.add_argument( + '-H', + '--hard', + action='store_true', + help=( + "gracefully destroy instance, it's like long " + 'pressing the power button' + ), + ) + shutdown_opts.add_argument( + '-u', + '--unsafe', + action='store_true', + help=( + 'destroy instance, this is similar to a power outage ' + 'and may result in data loss or corruption' + ), + ) + + # reboot subcommand + reboot = subparsers.add_parser('reboot', help='reboot instance') + reboot.add_argument('instance') + + # reset subcommand + reset = subparsers.add_parser('reset', help='reset instance') + reset.add_argument('instance') + + # powrst subcommand + powrst = subparsers.add_parser('powrst', help='power reset instance') + powrst.add_argument('instance') + + # pause subcommand + pause = subparsers.add_parser('pause', help='pause instance') + pause.add_argument('instance') + + # resume subcommand + resume = subparsers.add_parser('resume', help='resume paused instance') + resume.add_argument('instance') + + # status subcommand + status = subparsers.add_parser('status', help='display instance status') + status.add_argument('instance') + + # setvcpus subcommand + setvcpus = subparsers.add_parser('setvcpus', help='set vCPU number') + setvcpus.add_argument('instance') + setvcpus.add_argument('nvcpus', type=int) + + # setmem subcommand + setmem = subparsers.add_parser('setmem', help='set memory size') + setmem.add_argument('instance') + setmem.add_argument('memory', type=int, help='memory in MiB') + + # setpass subcommand + setpass = subparsers.add_parser( + 'setpass', + help='set user password in guest', + ) + setpass.add_argument('instance') + setpass.add_argument('username') + setpass.add_argument('password') + setpass.add_argument( + '-e', + '--encrypted', + action='store_true', + default=False, + help='set it if password is already encrypted', + ) + + args = root.parse_args() + if args.command is None: + root.print_help() + sys.exit() + + log_level = args.log_level or os.getenv('CMP_LOG') + + if isinstance(log_level, str) and log_level.lower() in log_levels: + logging.basicConfig( + level=logging.getLevelNamesMapping()[log_level.upper()] + ) + + log.debug('CLI started with args: %s', args) + + connect_uri = ( + args.connect + or os.getenv('CMP_LIBVIRT_URI') + or os.getenv('LIBVIRT_DEFAULT_URI') + or 'qemu:///system' + ) + + try: + with Session(connect_uri) as session: + main(session, args) + except ComputeError as e: + sys.exit(f'error: {e}') + except KeyboardInterrupt: + sys.exit() + except SystemExit as e: + sys.exit(e) + except Exception as e: # noqa: BLE001 + sys.exit(f'unexpected error {type(e)}: {e}') + + +if __name__ == '__main__': + cli() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py new file mode 100644 index 0000000..34a339a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/common.py @@ -0,0 +1,30 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Common symbols.""" + +from abc import ABC, abstractmethod + + +class EntityConfig(ABC): + """An abstract entity XML config builder class.""" + + @abstractmethod + def to_xml(self) -> str: + """Return device XML config.""" + raise NotImplementedError + + +DeviceConfig = EntityConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py new file mode 100644 index 0000000..1eef8de --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/exceptions.py @@ -0,0 +1,80 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Exceptions.""" + + +class ComputeError(Exception): + """Basic exception class.""" + + +class ConfigLoaderError(ComputeError): + """Something went wrong when loading configuration.""" + + +class SessionError(ComputeError): + """Something went wrong while connecting to libvirtd.""" + + +class GuestAgentError(ComputeError): + """Something went wring when QEMU Guest Agent call.""" + + +class GuestAgentUnavailableError(GuestAgentError): + """Guest agent is not connected or is unavailable.""" + + +class GuestAgentTimeoutExceededError(GuestAgentError): + """QEMU timeout exceeded.""" + + def __init__(self, msg: int): + """Initialise GuestAgentTimeoutExceededError.""" + super().__init__(f'QEMU timeout ({msg} sec) exceeded') + + +class GuestAgentCommandNotSupportedError(GuestAgentError): + """Guest agent command is not supported or blacklisted on guest.""" + + +class StoragePoolError(ComputeError): + """Something went wrong when operating with storage pool.""" + + +class StoragePoolNotFoundError(StoragePoolError): + """Storage pool not found.""" + + def __init__(self, msg: str): + """Initialise StoragePoolNotFoundError.""" + super().__init__(f"storage pool named '{msg}' not found") + + +class VolumeNotFoundError(StoragePoolError): + """Storage volume not found.""" + + def __init__(self, msg: str): + """Initialise VolumeNotFoundError.""" + super().__init__(f"storage volume '{msg}' not found") + + +class InstanceError(ComputeError): + """Something went wrong while interacting with the domain.""" + + +class InstanceNotFoundError(InstanceError): + """Virtual machine or container not found on compute node.""" + + def __init__(self, msg: str): + """Initialise InstanceNotFoundError.""" + super().__init__(f"compute instance '{msg}' not found") diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py new file mode 100644 index 0000000..6e2b150 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/__init__.py @@ -0,0 +1,18 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .guest_agent import GuestAgent +from .instance import Instance, InstanceConfig +from .schemas import InstanceSchema diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py new file mode 100644 index 0000000..4381591 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/guest_agent.py @@ -0,0 +1,208 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Interacting with the QEMU Guest Agent.""" + +import json +import logging +from base64 import b64decode, standard_b64encode +from time import sleep, time +from typing import NamedTuple + +import libvirt +import libvirt_qemu + +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + GuestAgentError, + GuestAgentTimeoutExceededError, + GuestAgentUnavailableError, +) + + +log = logging.getLogger(__name__) + + +class GuestExecOutput(NamedTuple): + """QEMU guest-exec command output.""" + + exited: bool | None = None + exitcode: int | None = None + stdout: str | None = None + stderr: str | None = None + + +class GuestAgent: + """Class for interacting with QEMU guest agent.""" + + def __init__(self, domain: libvirt.virDomain, timeout: int = 60): + """ + Initialise GuestAgent. + + :param domain: Libvirt domain object + :param timeout: QEMU timeout + """ + self.domain = domain + self.timeout = timeout + self.flags = libvirt_qemu.VIR_DOMAIN_QEMU_MONITOR_COMMAND_DEFAULT + self.last_pid = None + + def execute(self, command: dict) -> dict: + """ + Execute QEMU guest agent command. + + See: https://qemu-project.gitlab.io/qemu/interop/qemu-ga-ref.html + + :param command: QEMU guest agent command as dict + :return: Command output + :rtype: dict + """ + log.debug(command) + try: + output = libvirt_qemu.qemuAgentCommand( + self.domain, json.dumps(command), self.timeout, self.flags + ) + return json.loads(output) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_AGENT_UNRESPONSIVE: + raise GuestAgentUnavailableError(e) from e + raise GuestAgentError(e) from e + + def is_available(self) -> bool: + """ + Execute guest-ping. + + :return: True or False if guest agent is unreachable. + :rtype: bool + """ + try: + if self.execute({'execute': 'guest-ping', 'arguments': {}}): + return True + except GuestAgentError: + return False + + def get_supported_commands(self) -> set[str]: + """Return set of supported guest agent commands.""" + output = self.execute({'execute': 'guest-info', 'arguments': {}}) + return { + cmd['name'] + for cmd in output['return']['supported_commands'] + if cmd['enabled'] is True + } + + def raise_for_commands(self, commands: list[str]) -> None: + """ + Raise exception if QEMU GA command is not available. + + :param commands: List of required commands + :raise: GuestAgentCommandNotSupportedError + """ + supported = self.get_supported_commands() + for command in commands: + if command not in supported: + raise GuestAgentCommandNotSupportedError(command) + + def guest_exec( # noqa: PLR0913 + self, + path: str, + args: list[str] | None = None, + env: list[str] | None = None, + stdin: str | None = None, + *, + capture_output: bool = False, + decode_output: bool = False, + poll: bool = False, + ) -> GuestExecOutput: + """ + Execute qemu-exec command and return output. + + :param path: Path ot executable on guest. + :param arg: List of arguments to pass to executable. + :param env: List of environment variables to pass to executable. + For example: ``['LANG=C', 'TERM=xterm']`` + :param stdin: Data to pass to executable STDIN. + :param capture_output: Capture command output. + :param decode_output: Use base64_decode() to decode command output. + Affects only if `capture_output` is True. + :param poll: Poll command output. Uses `self.timeout` and + POLL_INTERVAL constant. + :return: Command output + :rtype: GuestExecOutput + """ + self.raise_for_commands(['guest-exec', 'guest-exec-status']) + command = { + 'execute': 'guest-exec', + 'arguments': { + 'path': path, + **({'arg': args} if args else {}), + **({'env': env} if env else {}), + **( + { + 'input-data': standard_b64encode( + stdin.encode('utf-8') + ).decode('utf-8') + } + if stdin + else {} + ), + 'capture-output': capture_output, + }, + } + output = self.execute(command) + self.last_pid = pid = output['return']['pid'] + command_status = self.guest_exec_status(pid, poll=poll)['return'] + exited = command_status['exited'] + exitcode = command_status['exitcode'] + stdout = command_status.get('out-data', None) + stderr = command_status.get('err-data', None) + if decode_output: + stdout = b64decode(stdout or '').decode('utf-8') + stderr = b64decode(stderr or '').decode('utf-8') + return GuestExecOutput(exited, exitcode, stdout, stderr) + + def guest_exec_status( + self, pid: int, *, poll: bool = False, poll_interval: float = 0.3 + ) -> dict: + """ + Execute guest-exec-status and return output. + + :param pid: PID in guest. + :param poll: If True poll command status. + :param poll_interval: Time between attempts to obtain command status. + :return: Command output + :rtype: dict + """ + self.raise_for_commands(['guest-exec-status']) + command = { + 'execute': 'guest-exec-status', + 'arguments': {'pid': pid}, + } + if not poll: + return self.execute(command) + start_time = time() + while True: + command_status = self.execute(command) + if command_status['return']['exited']: + break + sleep(poll_interval) + now = time() + if now - start_time > self.timeout: + raise GuestAgentTimeoutExceededError(self.timeout) + log.debug( + 'Polling command pid=%s finished, time taken: %s seconds', + pid, + int(time() - start_time), + ) + return command_status diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py new file mode 100644 index 0000000..5b806e6 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/instance.py @@ -0,0 +1,675 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage compute instances.""" + +__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo'] + +import logging +from typing import NamedTuple + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.exceptions import ( + GuestAgentCommandNotSupportedError, + InstanceError, +) +from compute.storage import DiskConfig +from compute.utils import units + +from .guest_agent import GuestAgent +from .schemas import ( + CPUEmulationMode, + CPUSchema, + InstanceSchema, + NetworkInterfaceSchema, +) + + +log = logging.getLogger(__name__) + + +class InstanceConfig(EntityConfig): + """Compute instance XML config builder.""" + + def __init__(self, schema: InstanceSchema): + """ + Initialise InstanceConfig. + + :param schema: InstanceSchema object + """ + self.name = schema.name + self.title = schema.title + self.description = schema.description + self.memory = schema.memory + self.max_memory = schema.max_memory + self.vcpus = schema.vcpus + self.max_vcpus = schema.max_vcpus + self.cpu = schema.cpu + self.machine = schema.machine + self.emulator = schema.emulator + self.arch = schema.arch + self.boot = schema.boot + self.network_interfaces = schema.network_interfaces + + def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element: + options = { + 'mode': cpu.emulation_mode, + 'match': 'exact', + 'check': 'partial', + } + if cpu.emulation_mode == CPUEmulationMode.HOST_PASSTHROUGH: + options['check'] = 'none' + options['migratable'] = 'on' + xml = E.cpu(**options) + if cpu.model: + xml.append(E.model(cpu.model, fallback='forbid')) + if cpu.vendor: + xml.append(E.vendor(cpu.vendor)) + if cpu.topology: + xml.append( + E.topology( + sockets=str(cpu.topology.sockets), + dies=str(cpu.topology.dies), + cores=str(cpu.topology.cores), + threads=str(cpu.topology.threads), + ) + ) + if cpu.features: + for feature in cpu.features.require: + xml.append(E.feature(policy='require', name=feature)) + for feature in cpu.features.disable: + xml.append(E.feature(policy='disable', name=feature)) + return xml + + def _gen_vcpus_xml(self, vcpus: int, max_vcpus: int) -> etree.Element: + xml = E.vcpus() + xml.append(E.vcpu(id='0', enabled='yes', hotpluggable='no', order='1')) + for i in range(max_vcpus - 1): + enabled = 'yes' if (i + 2) <= vcpus else 'no' + xml.append( + E.vcpu( + id=str(i + 1), + enabled=enabled, + hotpluggable='yes', + order=str(i + 2), + ) + ) + return xml + + def _gen_network_interface_xml( + self, interface: NetworkInterfaceSchema + ) -> etree.Element: + return E.interface( + E.source(network=interface.source), + E.mac(address=interface.mac), + type='network', + ) + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.domain(type='kvm') + xml.append(E.name(self.name)) + if self.title: + xml.append(E.title(self.title)) + if self.description: + xml.append(E.description(self.description)) + xml.append(E.metadata()) + xml.append(E.memory(str(self.max_memory * 1024), unit='KiB')) + xml.append(E.currentMemory(str(self.memory * 1024), unit='KiB')) + xml.append( + E.vcpu( + str(self.max_vcpus), + placement='static', + current=str(self.vcpus), + ) + ) + xml.append(self._gen_cpu_xml(self.cpu)) + os = E.os(E.type('hvm', machine=self.machine, arch=self.arch)) + for dev in self.boot.order: + os.append(E.boot(dev=dev)) + xml.append(os) + xml.append(E.features(E.acpi(), E.apic())) + xml.append(E.on_poweroff('destroy')) + xml.append(E.on_reboot('restart')) + xml.append(E.on_crash('restart')) + xml.append( + E.pm( + E('suspend-to-mem', enabled='no'), + E('suspend-to-disk', enabled='no'), + ) + ) + devices = E.devices() + devices.append(E.emulator(str(self.emulator))) + for interface in self.network_interfaces: + devices.append(self._gen_network_interface_xml(interface)) + devices.append(E.graphics(type='vnc', port='-1', autoport='yes')) + devices.append(E.input(type='tablet', bus='usb')) + devices.append( + E.channel( + E.source(mode='bind'), + E.target(type='virtio', name='org.qemu.guest_agent.0'), + E.address( + type='virtio-serial', controller='0', bus='0', port='1' + ), + type='unix', + ) + ) + devices.append( + E.console(E.target(type='serial', port='0'), type='pty') + ) + devices.append( + E.video( + E.model(type='vga', vram='16384', heads='1', primary='yes') + ) + ) + xml.append(devices) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class InstanceInfo(NamedTuple): + """ + Store compute instance info. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainInfo + """ + + state: str + max_memory: int + memory: int + nproc: int + cputime: int + + +class Instance: + """Manage compute instances.""" + + def __init__(self, domain: libvirt.virDomain): + """ + Initialise Instance. + + :ivar libvirt.virDomain domain: domain object + :ivar libvirt.virConnect connection: connection object + :ivar str name: domain name + :ivar GuestAgent guest_agent: :class:`GuestAgent` object + + :param domain: libvirt domain object + """ + self.domain = domain + self.connection = domain.connect() + self.name = domain.name() + self.guest_agent = GuestAgent(domain) + + def _expand_instance_state(self, state: int) -> str: + states = { + libvirt.VIR_DOMAIN_NOSTATE: 'nostate', + libvirt.VIR_DOMAIN_RUNNING: 'running', + libvirt.VIR_DOMAIN_BLOCKED: 'blocked', + libvirt.VIR_DOMAIN_PAUSED: 'paused', + libvirt.VIR_DOMAIN_SHUTDOWN: 'shutdown', + libvirt.VIR_DOMAIN_SHUTOFF: 'shutoff', + libvirt.VIR_DOMAIN_CRASHED: 'crashed', + libvirt.VIR_DOMAIN_PMSUSPENDED: 'pmsuspended', + } + return states[state] + + def get_info(self) -> InstanceInfo: + """Return instance info.""" + info = self.domain.info() + return InstanceInfo( + state=self._expand_instance_state(info[0]), + max_memory=info[1], + memory=info[2], + nproc=info[3], + cputime=info[4], + ) + + def get_status(self) -> str: + """ + Return instance state: 'running', 'shutoff', etc. + + Reference: + https://libvirt.org/html/libvirt-libvirt-domain.html#virDomainState + """ + try: + state, _ = self.domain.state() + except libvirt.libvirtError as e: + raise InstanceError( + 'Cannot fetch status of ' f'instance={self.name}: {e}' + ) from e + return self._expand_instance_state(state) + + def is_running(self) -> bool: + """Return True if instance is running, else return False.""" + if self.domain.isActive() != 1: + # 0 - is inactive, -1 - is error + return False + return True + + def is_autostart(self) -> bool: + """Return True if instance autostart is enabled, else return False.""" + try: + return bool(self.domain.autostart()) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot get autostart status for ' + f'instance={self.name}: {e}' + ) from e + + def get_max_memory(self) -> int: + """Maximum memory value for domain in KiB.""" + return self.domain.maxMemory() + + def get_max_vcpus(self) -> int: + """Maximum vCPUs number for domain.""" + return self.domain.maxVcpus() + + def start(self) -> None: + """Start defined instance.""" + log.info('Starting instnce=%s', self.name) + if self.is_running(): + log.warning( + 'Already started, nothing to do instance=%s', self.name + ) + return + try: + self.domain.create() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot start instance={self.name}: {e}' + ) from e + + def shutdown(self, method: str | None = None) -> None: + """ + Shutdown instance. + + Shutdown methods: + + SOFT + Use guest agent to shutdown. If guest agent is unavailable + NORMAL method will be used. + + NORMAL + Use method choosen by hypervisor to shutdown. Usually send ACPI + signal to guest OS. OS may ignore ACPI e.g. if guest is hanged. + + HARD + Shutdown instance without any guest OS shutdown. This is simular + to unplugging machine from power. Internally send SIGTERM to + instance process and destroy it gracefully. + + UNSAFE + Force shutdown. Internally send SIGKILL to instance process. + There is high data corruption risk! + + If method is None NORMAL method will used. + + :param method: Method used to shutdown instance + """ + methods = { + 'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT, + 'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT, + 'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL, + 'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT, + } + if method is None: + method = 'NORMAL' + if not isinstance(method, str): + raise TypeError( + f"Shutdown method must be a 'str', not {type(method)}" + ) + method = method.upper() + if method not in methods: + raise ValueError(f"Unsupported shutdown method: '{method}'") + try: + if method in ['SOFT', 'NORMAL']: + self.domain.shutdownFlags(flags=methods[method]) + elif method in ['HARD', 'UNSAFE']: + self.domain.destroyFlags(flags=methods[method]) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot shutdown instance={self.name} ' f'{method=}: {e}' + ) from e + + def reboot(self) -> None: + """Send ACPI signal to guest OS to reboot. OS may ignore this.""" + try: + self.domain.reboot() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reboot instance={self.name}: {e}' + ) from e + + def reset(self) -> None: + """ + Reset instance. + + Copypaste from libvirt doc: + + Reset a domain immediately without any guest OS shutdown. + Reset emulates the power reset button on a machine, where all + hardware sees the RST line set and reinitializes internal state. + + Note that there is a risk of data loss caused by reset without any + guest OS shutdown. + """ + try: + self.domain.reset() + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot reset instance={self.name}: {e}' + ) from e + + def power_reset(self) -> None: + """ + Shutdown instance and start. + + By analogy with real hardware, this is a normal server shutdown, + and then turning off from the power supply and turning it on again. + + This method is applicable in cases where there has been a + configuration change in libvirt and you need to restart the + instance to apply the new configuration. + """ + self.shutdown(method='NORMAL') + self.start() + + def set_autostart(self, *, enabled: bool) -> None: + """ + Set autostart flag for instance. + + :param enabled: Bool argument to set or unset autostart flag. + """ + autostart = 1 if enabled else 0 + try: + self.domain.setAutostart(autostart) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set autostart flag for instance={self.name} ' + f'{autostart=}: {e}' + ) from e + + def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None: + """ + Set vCPU number. + + If `live` is True and instance is not currently running vCPUs + will set in config and will applied when instance boot. + + NB: Note that if this call is executed before the guest has + finished booting, the guest may fail to process the change. + + :param nvcpus: Number of vCPUs + :param live: Affect a running instance + """ + if nvcpus <= 0: + raise InstanceError('Cannot set zero vCPUs') + if nvcpus > self.get_max_vcpus(): + raise InstanceError('vCPUs count is greather than max_vcpus') + if nvcpus == self.get_info().nproc: + log.warning( + 'Instance instance=%s already have %s vCPUs, nothing to do', + self.name, + nvcpus, + ) + return + try: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + self.domain.setVcpusFlags(nvcpus, flags=flags) + if live is True: + if not self.is_running(): + log.warning( + 'Instance is not running, changes applied in ' + 'instance config.' + ) + return + flags = libvirt.VIR_DOMAIN_AFFECT_LIVE + self.domain.setVcpusFlags(nvcpus, flags=flags) + if self.guest_agent.is_available(): + try: + self.guest_agent.raise_for_commands( + ['guest-set-vcpus'] + ) + flags = libvirt.VIR_DOMAIN_VCPU_GUEST + self.domain.setVcpusFlags(nvcpus, flags=flags) + except GuestAgentCommandNotSupportedError: + log.warning( + 'Cannot set vCPUs in guest via agent, you may ' + 'need to apply changes in guest manually.' + ) + else: + log.warning( + 'Cannot set vCPUs in guest OS on instance=%s. ' + 'You may need to apply CPUs in guest manually.', + self.name, + ) + except libvirt.libvirtError as e: + raise InstanceError( + f'Cannot set vCPUs for instance={self.name}: {e}' + ) from e + + def set_memory(self, memory: int, *, live: bool = False) -> None: + """ + Set memory. + + If `live` is True and instance is not currently running set memory + in config and will applied when instance boot. + + :param memory: Memory value in mebibytes + :param live: Affect a running instance + """ + if memory <= 0: + raise InstanceError('Cannot set zero memory') + if (memory * 1024) > self.get_max_memory(): + raise InstanceError('Memory is greather than max_memory') + if (memory * 1024) == self.get_info().memory: + log.warning( + "Instance '%s' already have %s memory, nothing to do", + self.name, + memory, + ) + return + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + try: + self.domain.setMemoryFlags(memory * 1024, flags=flags) + except libvirt.libvirtError as e: + msg = f'Cannot set memory for instance={self.name} {memory=}: {e}' + raise InstanceError(msg) from e + + def _get_disk_by_target(self, target: str) -> etree.Element: + xml = etree.fromstring(self.dump_xml()) # noqa: S320 + child = xml.xpath(f'/domain/devices/disk/target[@dev="{target}"]') + return child[0].getparent() if child else None + + def attach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Attach device to compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target): + log.warning( + "Volume with target '%s' is already attached", + device.target, + ) + return + self.domain.attachDeviceFlags(device.to_xml(), flags=flags) + + def detach_device( + self, device: DeviceConfig, *, live: bool = False + ) -> None: + """ + Dettach device from compute instance. + + :param device: Object with device description e.g. DiskConfig + :param live: Affect a running instance + """ + if live and self.is_running(): + flags = ( + libvirt.VIR_DOMAIN_AFFECT_LIVE + | libvirt.VIR_DOMAIN_AFFECT_CONFIG + ) + else: + flags = libvirt.VIR_DOMAIN_AFFECT_CONFIG + if isinstance(device, DiskConfig): # noqa: SIM102 + if self._get_disk_by_target(device.target) is None: + log.warning( + "Volume with target '%s' is already detached", + device.target, + ) + return + self.domain.detachDeviceFlags(device.to_xml(), flags=flags) + + def detach_disk(self, name: str) -> None: + """ + Detach disk device by target name. + + There is no ``attach_disk()`` method. Use :func:`attach_device` + with :class:`DiskConfig` as argument. + + :param name: Disk name e.g. 'vda', 'sda', etc. This name may + not match the name of the disk inside the guest OS. + """ + xml = self._get_disk_by_target(name) + if xml is None: + log.warning( + "Volume with target '%s' is already detached", + name, + ) + return + disk_params = { + 'disk_type': xml.get('type'), + 'source': xml.find('source').get('file'), + 'target': xml.find('target').get('dev'), + 'readonly': False if xml.find('readonly') is None else True, # noqa: SIM211 + } + for param in disk_params: + if disk_params[param] is None: + msg = ( + f"Cannot detach volume with target '{name}': " + f"parameter '{param}' is not defined in libvirt XML " + 'config on host.' + ) + raise InstanceError(msg) + self.detach_device(DiskConfig(**disk_params), live=True) + + def resize_disk( + self, name: str, capacity: int, unit: units.DataUnit + ) -> None: + """ + Resize attached block device. + + :param name: Disk device name e.g. `vda`, `sda`, etc. + :param capacity: New capacity. + :param unit: Capacity unit. + """ + self.domain.blockResize( + name, + units.to_bytes(capacity, unit=unit), + flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES, + ) + + def get_disks(self) -> list[DiskConfig]: + """Return list of attached disks.""" + raise NotImplementedError + + def pause(self) -> None: + """Pause instance.""" + if not self.is_running(): + raise InstanceError('Cannot pause inactive instance') + self.domain.suspend() + + def resume(self) -> None: + """Resume paused instance.""" + self.domain.resume() + + def get_ssh_keys(self, user: str) -> list[str]: + """ + Return list of SSH keys on guest for specific user. + + :param user: Username. + """ + raise NotImplementedError + + def set_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Add SSH keys to guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def delete_ssh_keys(self, user: str, ssh_keys: list[str]) -> None: + """ + Remove SSH keys from guest for specific user. + + :param user: Username. + :param ssh_keys: List of public SSH keys. + """ + raise NotImplementedError + + def set_user_password( + self, user: str, password: str, *, encrypted: bool = False + ) -> None: + """ + Set new user password in guest OS. + + This action performs by guest agent inside the guest. + + :param user: Username. + :param password: Password. + :param encrypted: Set it to True if password is already encrypted. + Right encryption method depends on guest OS. + """ + if not self.guest_agent.is_available(): + raise InstanceError( + 'Cannot change password: guest agent is unavailable' + ) + self.guest_agent.raise_for_commands(['guest-set-user-password']) + flags = libvirt.VIR_DOMAIN_PASSWORD_ENCRYPTED if encrypted else 0 + self.domain.setUserPassword(user, password, flags=flags) + + def dump_xml(self, *, inactive: bool = False) -> str: + """Return instance XML description.""" + flags = libvirt.VIR_DOMAIN_XML_INACTIVE if inactive else 0 + return self.domain.XMLDesc(flags) + + def delete(self) -> None: + """Undefine instance.""" + # TODO @ge: delete local disks + self.shutdown(method='HARD') + self.domain.undefine() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py new file mode 100644 index 0000000..f5a677c --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/instance/schemas.py @@ -0,0 +1,165 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Compute instance related objects schemas.""" + +import re +from enum import StrEnum +from pathlib import Path + +from pydantic import BaseModel, Extra, validator + +from compute.utils.units import DataUnit + + +class EntityModel(BaseModel): + """Basic entity model.""" + + class Config: + """Do not allow extra fields.""" + + extra = Extra.forbid + + +class CPUEmulationMode(StrEnum): + """CPU emulation mode enumerated.""" + + HOST_PASSTHROUGH = 'host-passthrough' + HOST_MODEL = 'host-model' + CUSTOM = 'custom' + MAXIMUM = 'maximum' + + +class CPUTopologySchema(EntityModel): + """CPU topology model.""" + + sockets: int + cores: int + threads: int + dies: int = 1 + + +class CPUFeaturesSchema(EntityModel): + """CPU features model.""" + + require: list[str] + disable: list[str] + + +class CPUSchema(EntityModel): + """CPU model.""" + + emulation_mode: CPUEmulationMode + model: str | None + vendor: str | None + topology: CPUTopologySchema | None + features: CPUFeaturesSchema | None + + +class VolumeType(StrEnum): + """Storage volume types enumeration.""" + + FILE = 'file' + + +class VolumeCapacitySchema(EntityModel): + """Storage volume capacity field model.""" + + value: int + unit: DataUnit + + +class VolumeSchema(EntityModel): + """Storage volume model.""" + + type: VolumeType # noqa: A003 + target: str + capacity: VolumeCapacitySchema + source: str | None = None + is_readonly: bool = False + is_system: bool = False + + +class NetworkInterfaceSchema(EntityModel): + """Network inerface model.""" + + source: str + mac: str + + +class BootOptionsSchema(EntityModel): + """Instance boot settings.""" + + order: tuple + + +class InstanceSchema(EntityModel): + """Compute instance model.""" + + name: str + title: str | None + description: str | None + memory: int + max_memory: int + vcpus: int + max_vcpus: int + cpu: CPUSchema + machine: str + emulator: Path + arch: str + boot: BootOptionsSchema + volumes: list[VolumeSchema] + network_interfaces: list[NetworkInterfaceSchema] + image: str | None = None + + @validator('name') + def _check_name(cls, value: str) -> str: # noqa: N805 + if not re.match(r'^[a-z0-9_]+$', value): + msg = ( + 'Name can contain only lowercase letters, numbers ' + 'and underscore.' + ) + raise ValueError(msg) + return value + + @validator('cpu') + def _check_topology(cls, cpu: int, values: dict) -> CPUSchema: # noqa: N805 + topo = cpu.topology + max_vcpus = values['max_vcpus'] + if topo and topo.sockets * topo.cores * topo.threads != max_vcpus: + msg = f'CPU topology does not match with {max_vcpus=}' + raise ValueError(msg) + return cpu + + @validator('volumes') + def _check_volumes(cls, volumes: list) -> list: # noqa: N805 + if len([v for v in volumes if v.is_system is True]) != 1: + msg = 'volumes list must contain one system volume' + raise ValueError(msg) + vol_with_source = 0 + for vol in volumes: + if vol.is_system is True and vol.is_readonly is True: + msg = 'volume marked as system cannot be readonly' + raise ValueError(msg) + if vol.source is not None: + vol_with_source += 1 + return volumes + + @validator('network_interfaces') + def _check_network_interfaces(cls, value: list) -> list: # noqa: N805 + if not value: + msg = 'Network interfaces list must contain at least one element' + raise ValueError(msg) + return value diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py new file mode 100644 index 0000000..de5f900 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/session.py @@ -0,0 +1,286 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Hypervisor session manager.""" + +import logging +import os +from contextlib import AbstractContextManager +from types import TracebackType +from typing import Any, NamedTuple +from uuid import uuid4 + +import libvirt +from lxml import etree + +from .exceptions import ( + InstanceNotFoundError, + SessionError, + StoragePoolNotFoundError, +) +from .instance import Instance, InstanceConfig, InstanceSchema +from .storage import DiskConfig, StoragePool, VolumeConfig +from .utils import units + + +log = logging.getLogger(__name__) + + +class Capabilities(NamedTuple): + """Store domain capabilities info.""" + + arch: str + virt_type: str + emulator: str + machine: str + max_vcpus: int + cpu_vendor: str + cpu_model: str + cpu_features: dict + usable_cpus: list[dict] + + +class NodeInfo(NamedTuple): + """ + Store compute node info. + + See https://libvirt.org/html/libvirt-libvirt-host.html#virNodeInfo + NOTE: memory unit in libvirt docs is wrong! Actual unit is MiB. + """ + + arch: str + memory: int + cpus: int + mhz: int + nodes: int + sockets: int + cores: int + threads: int + + +class Session(AbstractContextManager): + """ + Hypervisor session context manager. + + :cvar IMAGES_POOL: images storage pool name taken from env + :cvar VOLUMES_POOL: volumes storage pool name taken from env + """ + + IMAGES_POOL = os.getenv('CMP_IMAGES_POOL') + VOLUMES_POOL = os.getenv('CMP_VOLUMES_POOL') + + def __init__(self, uri: str | None = None): + """ + Initialise session with hypervisor. + + :ivar str uri: libvirt connection URI. + :ivar libvirt.virConnect connection: libvirt connection object. + + :param uri: libvirt connection URI. + """ + self.uri = uri or 'qemu:///system' + self.connection = libvirt.open(self.uri) + + def __enter__(self): + """Return Session object.""" + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + exc_traceback: TracebackType | None, + ): + """Close the connection when leaving the context.""" + self.close() + + def close(self) -> None: + """Close connection to libvirt daemon.""" + self.connection.close() + + def get_node_info(self) -> NodeInfo: + """Return information about compute node.""" + info = self.connection.getInfo() + return NodeInfo( + arch=info[0], + memory=info[1], + cpus=info[2], + mhz=info[3], + nodes=info[4], + sockets=info[5], + cores=info[6], + threads=info[7], + ) + + def _cap_get_usable_cpus(self, xml: etree.Element) -> list[dict]: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="custom"]')[0] + cpus = [] + for cpu in x.findall('model'): + if cpu.get('usable') == 'yes': + cpus.append( # noqa: PERF401 + { + 'vendor': cpu.get('vendor'), + 'model': cpu.text, + } + ) + return cpus + + def _cap_get_cpu_features(self, xml: etree.Element) -> dict: + x = xml.xpath('/domainCapabilities/cpu/mode[@name="host-model"]')[0] + require = [] + disable = [] + for feature in x.findall('feature'): + policy = feature.get('policy') + name = feature.get('name') + if policy == 'require': + require.append(name) + if policy == 'disable': + disable.append(name) + return {'require': require, 'disable': disable} + + def get_capabilities(self) -> Capabilities: + """Return capabilities e.g. arch, virt, emulator, etc.""" + prefix = '/domainCapabilities' + hprefix = f'{prefix}/cpu/mode[@name="host-model"]' + caps = etree.fromstring(self.connection.getDomainCapabilities()) # noqa: S320 + return Capabilities( + arch=caps.xpath(f'{prefix}/arch/text()')[0], + virt_type=caps.xpath(f'{prefix}/domain/text()')[0], + emulator=caps.xpath(f'{prefix}/path/text()')[0], + machine=caps.xpath(f'{prefix}/machine/text()')[0], + max_vcpus=int(caps.xpath(f'{prefix}/vcpu/@max')[0]), + cpu_vendor=caps.xpath(f'{hprefix}/vendor/text()')[0], + cpu_model=caps.xpath(f'{hprefix}/model/text()')[0], + cpu_features=self._cap_get_cpu_features(caps), + usable_cpus=self._cap_get_cpus(caps), + ) + + def create_instance(self, **kwargs: Any) -> Instance: + """ + Create and return new compute instance. + + :param name: Instance name. + :type name: str + :param title: Instance title for humans. + :type title: str + :param description: Some information about instance. + :type description: str + :param memory: Memory in MiB. + :type memory: int + :param max_memory: Maximum memory in MiB. + :type max_memory: int + :param vcpus: Number of vCPUs. + :type vcpus: int + :param max_vcpus: Maximum vCPUs. + :type max_vcpus: int + :param cpu: CPU configuration. See :class:`CPUSchema` for info. + :type cpu: dict + :param machine: QEMU emulated machine. + :type machine: str + :param emulator: Path to emulator. + :type emulator: str + :param arch: CPU architecture to virtualization. + :type arch: str + :param boot: Boot settings. See :class:`BootOptionsSchema`. + :type boot: dict + :param image: Source disk image name for system disk. + :type image: str + :param volumes: List of storage volume configs. For more info + see :class:`VolumeSchema`. + :type volumes: list[dict] + :param network_interfaces: List of virtual network interfaces + configs. See :class:`NetworkInterfaceSchema` for more info. + :type network_interfaces: list[dict] + """ + data = InstanceSchema(**kwargs) + config = InstanceConfig(data) + log.info('Define XML...') + log.info(config.to_xml()) + self.connection.defineXML(config.to_xml()) + log.info('Getting instance...') + instance = self.get_instance(config.name) + log.info('Creating volumes...') + for volume in data.volumes: + log.info('Creating volume=%s', volume) + capacity = units.to_bytes( + volume.capacity.value, volume.capacity.unit + ) + log.info('Connecting to images pool...') + images_pool = self.get_storage_pool(self.IMAGES_POOL) + log.info('Connecting to volumes pool...') + volumes_pool = self.get_storage_pool(self.VOLUMES_POOL) + log.info('Building volume configuration...') + if not volume.source: + vol_name = f'{uuid4()}.qcow2' + else: + vol_name = volume.source + vol_conf = VolumeConfig( + name=vol_name, + path=str(volumes_pool.path.joinpath(vol_name)), + capacity=capacity, + ) + log.info('Volume configuration is:\n %s', vol_conf.to_xml()) + if volume.is_system is True and data.image: + log.info( + "Volume is marked as 'system', start cloning image..." + ) + log.info('Get image %s', data.image) + image = images_pool.get_volume(data.image) + log.info('Cloning image into volumes pool...') + vol = volumes_pool.clone_volume(image, vol_conf) + log.info( + 'Resize cloned volume to specified size: %s', + capacity, + ) + vol.resize(capacity, unit=units.DataUnit.BYTES) + else: + log.info('Create volume...') + volumes_pool.create_volume(vol_conf) + log.info('Attaching volume to instance...') + instance.attach_device( + DiskConfig( + disk_type=volume.type, + source=vol_conf.path, + target=volume.target, + readonly=volume.is_readonly, + ) + ) + return instance + + def get_instance(self, name: str) -> Instance: + """Get compute instance by name.""" + try: + return Instance(self.connection.lookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: + raise InstanceNotFoundError(name) from e + raise SessionError(e) from e + + def list_instances(self) -> list[Instance]: + """List all instances.""" + return [Instance(dom) for dom in self.connection.listAllDomains()] + + def get_storage_pool(self, name: str) -> StoragePool: + """Get storage pool by name.""" + try: + return StoragePool(self.connection.storagePoolLookupByName(name)) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_POOL: + raise StoragePoolNotFoundError(name) from e + raise SessionError(e) from e + + def list_storage_pools(self) -> list[StoragePool]: + """List all strage pools.""" + return [StoragePool(p) for p in self.connection.listStoragePools()] diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py new file mode 100644 index 0000000..34aae30 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/__init__.py @@ -0,0 +1,17 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +from .pool import StoragePool +from .volume import DiskConfig, Volume, VolumeConfig diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py new file mode 100644 index 0000000..cb17494 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/pool.py @@ -0,0 +1,124 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage pools.""" + +import logging +from pathlib import Path +from typing import NamedTuple + +import libvirt +from lxml import etree + +from compute.exceptions import StoragePoolError, VolumeNotFoundError + +from .volume import Volume, VolumeConfig + + +log = logging.getLogger(__name__) + + +class StoragePoolUsageInfo(NamedTuple): + """Storage pool usage info.""" + + capacity: int + allocation: int + available: int + + +class StoragePool: + """Storage pool manipulating class.""" + + def __init__(self, pool: libvirt.virStoragePool): + """Initislise StoragePool.""" + self.pool = pool + self.name = pool.name() + self.path = self._get_path() + + def _get_path(self) -> Path: + """Return storage pool path.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return Path(xml.xpath('/pool/target/path/text()')[0]) + + def get_usage_info(self) -> StoragePoolUsageInfo: + """Return info about storage pool usage.""" + xml = etree.fromstring(self.pool.XMLDesc()) # noqa: S320 + return StoragePoolUsageInfo( + capacity=int(xml.xpath('/pool/capacity/text()')[0]), + allocation=int(xml.xpath('/pool/allocation/text()')[0]), + available=int(xml.xpath('/pool/available/text()')[0]), + ) + + def dump_xml(self) -> str: + """Return storage pool XML description as string.""" + return self.pool.XMLDesc() + + def refresh(self) -> None: + """Refresh storage pool.""" + # TODO @ge: handle libvirt asynchronous job related exceptions + self.pool.refresh() + + def create_volume(self, vol_conf: VolumeConfig) -> Volume: + """Create storage volume and return Volume instance.""" + log.info( + 'Create storage volume vol=%s in pool=%s', vol_conf.name, self.name + ) + vol = self.pool.createXML( + vol_conf.to_xml(), + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + return Volume(self.pool, vol) + + def clone_volume(self, src: Volume, dst: VolumeConfig) -> Volume: + """ + Make storage volume copy. + + :param src: Input volume + :param dst: Output volume config + """ + log.info( + 'Start volume cloning ' + 'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s', + src.pool_name, + src.name, + self.pool.name, + dst.name, + ) + vol = self.pool.createXMLFrom( + dst.to_xml(), # new volume XML description + src.vol, # source volume virStorageVol object + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + if vol is None: + raise StoragePoolError + return Volume(self.pool, vol) + + def get_volume(self, name: str) -> Volume | None: + """Lookup and return Volume instance or None.""" + log.info( + 'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name + ) + try: + vol = self.pool.storageVolLookupByName(name) + return Volume(self.pool, vol) + except libvirt.libvirtError as e: + if e.get_error_code() == libvirt.VIR_ERR_NO_STORAGE_VOL: + raise VolumeNotFoundError(name) from e + log.exception('unexpected error from libvirt') + raise StoragePoolError(e) from e + + def list_volumes(self) -> list[Volume]: + """Return list of volumes in storage pool.""" + return [Volume(self.pool, vol) for vol in self.pool.listAllVolumes()] diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py new file mode 100644 index 0000000..11a1dc4 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/storage/volume.py @@ -0,0 +1,138 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Manage storage volumes.""" + +from dataclasses import dataclass +from pathlib import Path +from time import time + +import libvirt +from lxml import etree +from lxml.builder import E + +from compute.common import DeviceConfig, EntityConfig +from compute.utils import units + + +@dataclass +class VolumeConfig(EntityConfig): + """ + Storage volume XML config builder. + + Generate XML config for creating a volume in a libvirt + storage pool. + """ + + name: str + path: str + capacity: int + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + unixtime = str(int(time())) + xml = E.volume(type='file') + xml.append(E.name(self.name)) + xml.append(E.key(self.path)) + xml.append(E.source()) + xml.append(E.capacity(str(self.capacity), unit='bytes')) + xml.append(E.allocation('0')) + xml.append( + E.target( + E.path(self.path), + E.format(type='qcow2'), + E.timestamps( + E.atime(unixtime), E.mtime(unixtime), E.ctime(unixtime) + ), + E.compat('1.1'), + E.features(E.lazy_refcounts()), + ) + ) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +@dataclass +class DiskConfig(DeviceConfig): + """ + Disk XML config builder. + + Generate XML config for attaching or detaching storage volumes + to compute instances. + """ + + disk_type: str + source: str | Path + target: str + readonly: bool = False + + def to_xml(self) -> str: + """Return XML config for libvirt.""" + xml = E.disk(type=self.disk_type, device='disk') + xml.append(E.driver(name='qemu', type='qcow2', cache='writethrough')) + if self.disk_type == 'file': + xml.append(E.source(file=str(self.source))) + xml.append(E.target(dev=self.target, bus='virtio')) + if self.readonly: + xml.append(E.readonly()) + return etree.tostring(xml, encoding='unicode', pretty_print=True) + + +class Volume: + """Storage volume manipulating class.""" + + def __init__( + self, pool: libvirt.virStoragePool, vol: libvirt.virStorageVol + ): + """ + Initialise Volume. + + :param pool: libvirt virStoragePool object + :param vol: libvirt virStorageVol object + """ + self.pool = pool + self.pool_name = pool.name() + self.vol = vol + self.name = vol.name() + self.path = Path(vol.path()) + + def dump_xml(self) -> str: + """Return volume XML description as string.""" + return self.vol.XMLDesc() + + def clone(self, vol_conf: VolumeConfig) -> None: + """ + Make a copy of volume to the same storage pool. + + :param vol_info VolumeInfo: New storage volume dataclass object + """ + self.pool.createXMLFrom( + vol_conf.to_xml(), + self.vol, + flags=libvirt.VIR_STORAGE_VOL_CREATE_PREALLOC_METADATA, + ) + + def resize(self, capacity: int, unit: units.DataUnit) -> None: + """ + Resize volume. + + :param capacity int: Volume new capacity. + :param unit DataUnit: Data unit. Internally converts into bytes. + """ + # TODO @ge: Check actual volume size before resize + self.vol.resize(units.to_bytes(capacity, unit=unit)) + + def delete(self) -> None: + """Delete volume from storage pool.""" + self.vol.delete() diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/__init__.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py new file mode 100644 index 0000000..aaeb0fe --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/config_loader.py @@ -0,0 +1,56 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Configuration loader.""" + +import tomllib +from collections import UserDict +from pathlib import Path + +from compute.exceptions import ConfigLoaderError + + +DEFAULT_CONFIGURATION = {} +DEFAULT_CONFIG_FILE = '/etc/computed/computed.toml' + + +class ConfigLoader(UserDict): + """UserDict for storing configuration.""" + + def __init__(self, file: Path | None = None): + """ + Initialise ConfigLoader. + + :param file: Path to configuration file. If `file` is None + use default path from DEFAULT_CONFIG_FILE constant. + """ + # TODO @ge: load deafult configuration + self.file = Path(file) if file else Path(DEFAULT_CONFIG_FILE) + super().__init__(self.load()) + + def load(self) -> dict: + """Load confguration object from TOML file.""" + try: + with Path(self.file).open('rb') as configfile: + return tomllib.load(configfile) + # TODO @ge: add config schema validation + except tomllib.TOMLDecodeError as tomlerr: + raise ConfigLoaderError( + f'Bad TOML syntax in config file: {self.file}: {tomlerr}' + ) from tomlerr + except (OSError, ValueError) as readerr: + raise ConfigLoaderError( + f'Cannot read config file: {self.file}: {readerr}' + ) from readerr diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py new file mode 100644 index 0000000..8a6454a --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/ids.py @@ -0,0 +1,33 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Random identificators.""" + +# ruff: noqa: S311, C417 + +import random + + +def random_mac() -> str: + """Retrun random MAC address.""" + mac = [ + 0x00, + 0x16, + 0x3E, + random.randint(0x00, 0x7F), + random.randint(0x00, 0xFF), + random.randint(0x00, 0xFF), + ] + return ':'.join(map(lambda x: '%02x' % x, mac)) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py new file mode 100644 index 0000000..57a4583 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/lib/python3/dist-packages/compute/utils/units.py @@ -0,0 +1,54 @@ +# This file is part of Compute +# +# Compute 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. +# +# Ansible 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 Ansible. If not, see . + +"""Tools for data units convertion.""" + +from enum import StrEnum + + +class DataUnit(StrEnum): + """Data units enumerated.""" + + BYTES = 'bytes' + KIB = 'KiB' + MIB = 'MiB' + GIB = 'GiB' + TIB = 'TiB' + + +class InvalidDataUnitError(ValueError): + """Data unit is not valid.""" + + def __init__(self, msg: str): + """Initialise InvalidDataUnitError.""" + super().__init__( + f'{msg}, valid units are: {", ".join(list(DataUnit))}' + ) + + +def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int: + """Convert value to bytes. See :class:`DataUnit`.""" + try: + _ = DataUnit(unit) + except ValueError as e: + raise InvalidDataUnitError(e) from e + powers = { + DataUnit.BYTES: 0, + DataUnit.KIB: 1, + DataUnit.MIB: 2, + DataUnit.GIB: 3, + DataUnit.TIB: 4, + } + return value * pow(1024, powers[unit]) diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/bash-completion/completions/compute @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md new file mode 100644 index 0000000..0131e8e --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/README.md @@ -0,0 +1,65 @@ +# Compute + +Compute instances management library and tools. + +## Docs + +Run `make serve-docs`. See [Development](#development) below. + +## Roadmap + +- [x] Create instances +- [ ] CDROM +- [ ] cloud-init for provisioning instances +- [x] Instance power management +- [x] Instance pause and resume +- [x] vCPU hotplug +- [x] Memory hotplug +- [x] Hot disk resize [not tested] +- [ ] CPU topology customization +- [x] CPU customization (emulation mode, model, vendor, features) +- [ ] BIOS/UEFI settings +- [x] Device attaching +- [x] Device detaching +- [ ] GPU passthrough +- [ ] CPU guarantied resource percent support +- [x] QEMU Guest Agent management +- [ ] Instance resources usage stats +- [ ] SSH-keys management +- [x] Setting user passwords in guest +- [x] QCOW2 disks support +- [ ] ZVOL support +- [ ] Network disks support +- [ ] Images service integration (Images service is not implemented yet) +- [ ] Manage storage pools +- [ ] Idempotency +- [ ] CLI [in progress] +- [ ] HTTP API +- [ ] Instance migrations +- [ ] Instance snapshots +- [ ] Instance backups +- [ ] LXC + +## Development + +Python 3.11+ is required. + +Install [poetry](https://python-poetry.org/), clone this repository and run: + +``` +poetry install --with dev --with docs +``` + +# Build Debian package + +Install Docker first, then run: + +``` +make build-deb +``` + +`compute` and `compute-doc` packages will built. See packaging/build directory. Packages can be installed via `dpkg` or `apt-get`: + +``` +apt-get install ./compute*.deb +``` diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz new file mode 100644 index 0000000..40eae6f Binary files /dev/null and b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/changelog.Debian.gz differ diff --git a/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/compute/usr/share/doc/compute/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + 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 package 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 . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/control b/packaging/build/compute-0.1.0.dev1/debian/control new file mode 100644 index 0000000..6b99835 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/control @@ -0,0 +1,48 @@ +Source: compute +Section: admin +Priority: optional +Maintainer: ge +Rules-Requires-Root: no +Build-Depends: + debhelper-compat (= 13), + dh-sequence-python3, + bash-completion, + pybuild-plugin-pyproject, + python3-poetry-core, + python3-setuptools, + python3-all, + python3-sphinx, + python3-sphinx-multiversion, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Standards-Version: 4.6.2 +Homepage: https://git.lulzette.ru/hstack/compute + +Package: compute +Architecture: all +Depends: + ${python3:Depends}, + ${misc:Depends}, + qemu-system, + qemu-utils, + libvirt-daemon-system, + libvirt-clients, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Recommends: + dnsmasq +Suggests: + compute-doc +Description: Compute instances management library and tools (Python 3) + +Package: compute-doc +Section: doc +Architecture: all +Depends: + ${sphinxdoc:Depends}, + ${misc:Depends}, +Description: Compute instances management library and tools (documentation) diff --git a/packaging/build/compute-0.1.0.dev1/debian/copyright b/packaging/build/compute-0.1.0.dev1/debian/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + 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 package 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 . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp b/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp new file mode 100644 index 0000000..3445b01 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/debhelper-build-stamp @@ -0,0 +1,2 @@ +compute +compute-doc diff --git a/packaging/build/compute-0.1.0.dev1/debian/docs b/packaging/build/compute-0.1.0.dev1/debian/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/docs @@ -0,0 +1 @@ +README.md diff --git a/packaging/build/compute-0.1.0.dev1/debian/files b/packaging/build/compute-0.1.0.dev1/debian/files new file mode 100644 index 0000000..e63edf9 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/files @@ -0,0 +1,3 @@ +compute-doc_0.1.0.dev1-1_all.deb doc optional +compute_0.1.0.dev1-1_all.deb admin optional +compute_0.1.0.dev1-1_amd64.buildinfo admin optional diff --git a/packaging/build/compute-0.1.0.dev1/debian/rules b/packaging/build/compute-0.1.0.dev1/debian/rules new file mode 100755 index 0000000..f99ef32 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/rules @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 +export PYBUILD_DESTDIR_python3=debian/compute + +%: + dh $@ --with python3,sphinxdoc,bash-completion --buildsystem=pybuild + +override_dh_auto_test: + @echo No tests there + +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) + http_proxy=127.0.0.1:9 https_proxy=127.0.0.1:9 \ + HTTP_PROXY=127.0.0.1:9 HTTPS_PROXY=127.0.0.1:9 \ + PYTHONPATH=. PYTHON=python3 python3 -m sphinx $(SPHINXOPTS) -b html \ + ../docs/source \ + $(CURDIR)/debian/compute-doc/usr/share/doc/compute-doc/html + dh_sphinxdoc +endif diff --git a/packaging/build/compute-0.1.0.dev1/debian/source/format b/packaging/build/compute-0.1.0.dev1/debian/source/format new file mode 100644 index 0000000..163aaf8 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/source/format @@ -0,0 +1 @@ +3.0 (quilt) diff --git a/packaging/build/compute-0.1.0.dev1/debian/source/options b/packaging/build/compute-0.1.0.dev1/debian/source/options new file mode 100644 index 0000000..cb61fa5 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/source/options @@ -0,0 +1 @@ +extend-diff-ignore = "^[^/]*[.]egg-info/" diff --git a/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex b/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex new file mode 100644 index 0000000..3fc47cc --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/debian/upstream/metadata.ex @@ -0,0 +1,10 @@ +# Example file for upstream/metadata. +# See https://wiki.debian.org/UpstreamMetadata for more info/fields. +# Below an example based on a github project. + +# Bug-Database: https://github.com//compute/issues +# Bug-Submit: https://github.com//compute/issues/new +# Changelog: https://github.com//compute/blob/master/CHANGES +# Documentation: https://github.com//compute/wiki +# Repository-Browse: https://github.com//compute +# Repository: https://github.com//compute.git diff --git a/packaging/build/compute-0.1.0.dev1/pyproject.toml b/packaging/build/compute-0.1.0.dev1/pyproject.toml new file mode 100644 index 0000000..f7aab25 --- /dev/null +++ b/packaging/build/compute-0.1.0.dev1/pyproject.toml @@ -0,0 +1,61 @@ +[tool.poetry] +name = 'compute' +version = '0.1.0-dev1' +description = 'Compute instances management library and tools' +authors = ['ge '] +readme = 'README.md' + +[tool.poetry.dependencies] +python = '^3.11' +libvirt-python = '9.0.0' +lxml = '^4.9.2' +pydantic = '1.10.4' +pyyaml = "^6.0.1" + +[tool.poetry.scripts] +compute = 'compute.cli.control:cli' + +[tool.poetry.group.dev.dependencies] +ruff = '^0.1.3' +isort = '^5.12.0' + +[tool.poetry.group.docs.dependencies] +sphinx = '^7.2.6' +sphinx-autobuild = '^2021.3.14' +sphinx-multiversion = '^0.2.4' + +[build-system] +requires = ['poetry-core'] +build-backend = 'poetry.core.masonry.api' + +[tool.isort] +skip = ['.gitignore'] +lines_after_imports = 2 +include_trailing_comma = true +split_on_trailing_comma = true + +[tool.ruff] +line-length = 79 +indent-width = 4 +target-version = 'py311' + +[tool.ruff.lint] +select = ['ALL'] +ignore = [ + 'Q000', 'Q003', 'D211', 'D212', + 'ANN101', 'ISC001', 'COM812', + 'D203', 'ANN204', 'T201', + 'EM102', 'TRY003', 'EM101', + 'TD003', 'TD006', 'FIX002', # 'todo' strings linting +] +exclude = ['__init__.py'] + +[tool.ruff.lint.flake8-annotations] +mypy-init-return = true +allow-star-arg-any = true + +[tool.ruff.format] +quote-style = 'single' + +[tool.ruff.isort] +lines-after-imports = 2 diff --git a/packaging/build/compute-doc_0.1.0.dev1-1_all.deb b/packaging/build/compute-doc_0.1.0.dev1-1_all.deb new file mode 100644 index 0000000..c855890 Binary files /dev/null and b/packaging/build/compute-doc_0.1.0.dev1-1_all.deb differ diff --git a/packaging/build/compute_0.1.0.dev1-1.debian.tar.xz b/packaging/build/compute_0.1.0.dev1-1.debian.tar.xz new file mode 100644 index 0000000..ce59eb3 Binary files /dev/null and b/packaging/build/compute_0.1.0.dev1-1.debian.tar.xz differ diff --git a/packaging/build/compute_0.1.0.dev1-1.dsc b/packaging/build/compute_0.1.0.dev1-1.dsc new file mode 100644 index 0000000..91f1fdb --- /dev/null +++ b/packaging/build/compute_0.1.0.dev1-1.dsc @@ -0,0 +1,21 @@ +Format: 3.0 (quilt) +Source: compute +Binary: compute, compute-doc +Architecture: all +Version: 0.1.0.dev1-1 +Maintainer: ge +Homepage: https://git.lulzette.ru/hstack/compute +Standards-Version: 4.6.2 +Build-Depends: debhelper-compat (= 13), dh-sequence-python3, bash-completion, pybuild-plugin-pyproject, python3-poetry-core, python3-setuptools, python3-all, python3-sphinx, python3-sphinx-multiversion, python3-libvirt, python3-lxml, python3-yaml, python3-pydantic +Package-List: + compute deb admin optional arch=all + compute-doc deb doc optional arch=all +Checksums-Sha1: + 94be605a5a0ca8b0ea93a46dd5029a2513486190 20824 compute_0.1.0.dev1.orig.tar.gz + 57790e9df9659f913fa1da65b77c84a3aba4976c 2660 compute_0.1.0.dev1-1.debian.tar.xz +Checksums-Sha256: + e310d2ddbdb334737efc7adcc98eac2db2f158e5e989ddfade2ddfae07a6174d 20824 compute_0.1.0.dev1.orig.tar.gz + c9e267c79fa5a9e06625ac0502af528f4b526c74035fa23e6933c2c8e6429ab2 2660 compute_0.1.0.dev1-1.debian.tar.xz +Files: + de78bd5eecc56034a990dd9395a089c4 20824 compute_0.1.0.dev1.orig.tar.gz + cb9d6978a83d7f1842063315333d6278 2660 compute_0.1.0.dev1-1.debian.tar.xz diff --git a/packaging/build/compute_0.1.0.dev1-1_all.deb b/packaging/build/compute_0.1.0.dev1-1_all.deb new file mode 100644 index 0000000..d448f22 Binary files /dev/null and b/packaging/build/compute_0.1.0.dev1-1_all.deb differ diff --git a/packaging/build/compute_0.1.0.dev1-1_amd64.buildinfo b/packaging/build/compute_0.1.0.dev1-1_amd64.buildinfo new file mode 100644 index 0000000..4110b27 --- /dev/null +++ b/packaging/build/compute_0.1.0.dev1-1_amd64.buildinfo @@ -0,0 +1,270 @@ +Format: 1.0 +Source: compute +Binary: compute compute-doc +Architecture: all source +Version: 0.1.0.dev1-1 +Checksums-Md5: + 635eae482cdff5bbe99a3911ed9e915c 1123 compute_0.1.0.dev1-1.dsc + 8a8c6490cb363870735ec2572cf65cdf 40424 compute-doc_0.1.0.dev1-1_all.deb + bf5fb2ffd00e5373a54461f34b2d7033 21644 compute_0.1.0.dev1-1_all.deb +Checksums-Sha1: + 455d0b203d96d97d4272d30be72c27cdde50fdc5 1123 compute_0.1.0.dev1-1.dsc + ef2a0d6dd481adc0cf4bed1a2bc98536f1795adf 40424 compute-doc_0.1.0.dev1-1_all.deb + 20ecb0342e494a426634a5124462e49c6f7fd2c9 21644 compute_0.1.0.dev1-1_all.deb +Checksums-Sha256: + c92ba4e3db43b496e01aa912f6e59240f7cd647b8b4950005182d91d071e31ac 1123 compute_0.1.0.dev1-1.dsc + 1c0d14fc87885f5dafe8bcbe1b6a07d9a57ce2dc0943a9837f751e3142ee8a42 40424 compute-doc_0.1.0.dev1-1_all.deb + a1f5a032f653276be3e4dc43818d663850463167a2b4b39138e184be1dabb44f 21644 compute_0.1.0.dev1-1_all.deb +Build-Origin: Debian +Build-Architecture: amd64 +Build-Date: Wed, 22 Nov 2023 23:06:48 +0000 +Build-Tainted-By: + merged-usr-via-aliased-dirs +Installed-Build-Depends: + autoconf (= 2.71-3), + automake (= 1:1.16.5-1.3), + autopoint (= 0.21-12), + autotools-dev (= 20220109.1), + base-files (= 12.4+deb12u2), + base-passwd (= 3.6.1), + bash (= 5.2.15-2+b2), + bash-completion (= 1:2.11-6), + binutils (= 2.40-2), + binutils-common (= 2.40-2), + binutils-x86-64-linux-gnu (= 2.40-2), + bsdextrautils (= 2.38.1-5+b1), + bsdutils (= 1:2.38.1-5+b1), + build-essential (= 12.9), + bzip2 (= 1.0.8-5+b1), + ca-certificates (= 20230311), + coreutils (= 9.1-1), + cpp (= 4:12.2.0-3), + cpp-12 (= 12.2.0-14), + dash (= 0.5.12-2), + debconf (= 1.5.82), + debhelper (= 13.11.4), + debianutils (= 5.7-0.5~deb12u1), + dh-autoreconf (= 20), + dh-python (= 5.20230130+deb12u1), + dh-strip-nondeterminism (= 1.13.1-1), + diffutils (= 1:3.8-4), + docutils-common (= 0.19+dfsg-6), + dpkg (= 1.21.22), + dpkg-dev (= 1.21.22), + dwz (= 0.15-1), + file (= 1:5.44-3), + findutils (= 4.9.0-4), + g++ (= 4:12.2.0-3), + g++-12 (= 12.2.0-14), + gcc (= 4:12.2.0-3), + gcc-12 (= 12.2.0-14), + gcc-12-base (= 12.2.0-14), + gettext (= 0.21-12), + gettext-base (= 0.21-12), + grep (= 3.8-5), + groff-base (= 1.22.4-10), + gzip (= 1.12-1), + hostname (= 3.23+nmu1), + init-system-helpers (= 1.65.2), + intltool-debian (= 0.35.0+20060710.6), + libacl1 (= 2.3.1-3), + libapparmor1 (= 3.0.8-3), + libarchive-zip-perl (= 1.68-1), + libasan8 (= 12.2.0-14), + libatomic1 (= 12.2.0-14), + libattr1 (= 1:2.5.1-4), + libaudit-common (= 1:3.0.9-1), + libaudit1 (= 1:3.0.9-1), + libbinutils (= 2.40-2), + libblkid1 (= 2.38.1-5+b1), + libbrotli1 (= 1.0.9-2+b6), + libbz2-1.0 (= 1.0.8-5+b1), + libc-bin (= 2.36-9+deb12u3), + libc-dev-bin (= 2.36-9+deb12u3), + libc6 (= 2.36-9+deb12u3), + libc6-dev (= 2.36-9+deb12u3), + libcap-ng0 (= 0.8.3-1+b3), + libcap2 (= 1:2.66-4), + libcc1-0 (= 12.2.0-14), + libcom-err2 (= 1.47.0-2), + libcrypt-dev (= 1:4.4.33-2), + libcrypt1 (= 1:4.4.33-2), + libctf-nobfd0 (= 2.40-2), + libctf0 (= 2.40-2), + libcurl3-gnutls (= 7.88.1-10+deb12u4), + libdb5.3 (= 5.3.28+dfsg2-1), + libdebconfclient0 (= 0.270), + libdebhelper-perl (= 13.11.4), + libdpkg-perl (= 1.21.22), + libelf1 (= 0.188-2.1), + libexpat1 (= 2.5.0-1), + libffi8 (= 3.4.4-1), + libfile-stripnondeterminism-perl (= 1.13.1-1), + libgcc-12-dev (= 12.2.0-14), + libgcc-s1 (= 12.2.0-14), + libgcrypt20 (= 1.10.1-3), + libgdbm-compat4 (= 1.23-3), + libgdbm6 (= 1.23-3), + libglib2.0-0 (= 2.74.6-2), + libgmp10 (= 2:6.2.1+dfsg1-1.1), + libgnutls30 (= 3.7.9-2), + libgomp1 (= 12.2.0-14), + libgpg-error0 (= 1.46-1), + libgprofng0 (= 2.40-2), + libgssapi-krb5-2 (= 1.20.1-2+deb12u1), + libhogweed6 (= 3.8.1-2), + libicu72 (= 72.1-3), + libidn2-0 (= 2.3.3-1+b1), + libisl23 (= 0.25-1), + libitm1 (= 12.2.0-14), + libjansson4 (= 2.14-2), + libjs-jquery (= 3.6.1+dfsg+~3.5.14-1), + libjs-sphinxdoc (= 5.3.0-4), + libjs-underscore (= 1.13.4~dfsg+~1.11.4-3), + libjson-perl (= 4.10000-1), + libk5crypto3 (= 1.20.1-2+deb12u1), + libkeyutils1 (= 1.6.3-2), + libkrb5-3 (= 1.20.1-2+deb12u1), + libkrb5support0 (= 1.20.1-2+deb12u1), + libldap-2.5-0 (= 2.5.13+dfsg-5), + liblsan0 (= 12.2.0-14), + liblz4-1 (= 1.9.4-1), + liblzma5 (= 5.4.1-0.2), + libmagic-mgc (= 1:5.44-3), + libmagic1 (= 1:5.44-3), + libmd0 (= 1.0.4-2), + libmount1 (= 2.38.1-5+b1), + libmpc3 (= 1.3.1-1), + libmpfr6 (= 4.2.0-1), + libncursesw6 (= 6.4-4), + libnettle8 (= 3.8.1-2), + libnghttp2-14 (= 1.52.0-1), + libnl-3-200 (= 3.7.0-0.2+b1), + libnsl-dev (= 1.3.0-2), + libnsl2 (= 1.3.0-2), + libnuma1 (= 2.0.16-1), + libp11-kit0 (= 0.24.1-2), + libpam-modules (= 1.5.2-6+deb12u1), + libpam-modules-bin (= 1.5.2-6+deb12u1), + libpam-runtime (= 1.5.2-6+deb12u1), + libpam0g (= 1.5.2-6+deb12u1), + libpcre2-8-0 (= 10.42-1), + libperl5.36 (= 5.36.0-7), + libpipeline1 (= 1.5.7-1), + libpsl5 (= 0.21.2-1), + libpython3-stdlib (= 3.11.2-1+b1), + libpython3.11-minimal (= 3.11.2-6), + libpython3.11-stdlib (= 3.11.2-6), + libquadmath0 (= 12.2.0-14), + libreadline8 (= 8.2-1.3), + librtmp1 (= 2.4+20151223.gitfa8646d.1-2+b2), + libsasl2-2 (= 2.1.28+dfsg-10), + libsasl2-modules-db (= 2.1.28+dfsg-10), + libseccomp2 (= 2.5.4-1+b3), + libselinux1 (= 3.4-1+b6), + libsmartcols1 (= 2.38.1-5+b1), + libsqlite3-0 (= 3.40.1-2), + libssh-4 (= 0.10.5-2), + libssh2-1 (= 1.10.0-3+b1), + libssl3 (= 3.0.11-1~deb12u2), + libstdc++-12-dev (= 12.2.0-14), + libstdc++6 (= 12.2.0-14), + libsub-override-perl (= 0.09-4), + libsystemd0 (= 252.17-1~deb12u1), + libtasn1-6 (= 4.19.0-2), + libtinfo6 (= 6.4-4), + libtirpc-common (= 1.3.3+ds-1), + libtirpc-dev (= 1.3.3+ds-1), + libtirpc3 (= 1.3.3+ds-1), + libtool (= 2.4.7-5), + libtsan2 (= 12.2.0-14), + libubsan1 (= 12.2.0-14), + libuchardet0 (= 0.0.7-1), + libudev1 (= 252.17-1~deb12u1), + libunistring2 (= 1.0-2), + libuuid1 (= 2.38.1-5+b1), + libvirt0 (= 9.0.0-4), + libxml2 (= 2.9.14+dfsg-1.3~deb12u1), + libxslt1.1 (= 1.1.35-1), + libyajl2 (= 2.1.0-3+deb12u2), + libyaml-0-2 (= 0.2.5-1), + libzstd1 (= 1.5.4+dfsg2-5), + linux-libc-dev (= 6.1.55-1), + login (= 1:4.13+dfsg1-1+b1), + m4 (= 1.4.19-3), + make (= 4.3-4.1), + man-db (= 2.11.2-2), + mawk (= 1.3.4.20200120-3.1), + media-types (= 10.0.0), + ncurses-base (= 6.4-4), + ncurses-bin (= 6.4-4), + openssl (= 3.0.11-1~deb12u2), + patch (= 2.7.6-7), + perl (= 5.36.0-7), + perl-base (= 5.36.0-7), + perl-modules-5.36 (= 5.36.0-7), + po-debconf (= 1.0.21+nmu1), + pybuild-plugin-pyproject (= 5.20230130+deb12u1), + python-babel-localedata (= 2.10.3-1), + python3 (= 3.11.2-1+b1), + python3-alabaster (= 0.7.12-1), + python3-all (= 3.11.2-1+b1), + python3-babel (= 2.10.3-1), + python3-build (= 0.9.0-1), + python3-certifi (= 2022.9.24-1), + python3-chardet (= 5.1.0+dfsg-2), + python3-charset-normalizer (= 3.0.1-2), + python3-distutils (= 3.11.2-3), + python3-docutils (= 0.19+dfsg-6), + python3-idna (= 3.3-1), + python3-imagesize (= 1.4.1-1), + python3-importlib-metadata (= 4.12.0-1), + python3-installer (= 0.6.0+dfsg1-1), + python3-jinja2 (= 3.1.2-1), + python3-lib2to3 (= 3.11.2-3), + python3-libvirt (= 9.0.0-1), + python3-lxml (= 4.9.2-1+b1), + python3-markupsafe (= 2.1.2-1+b1), + python3-minimal (= 3.11.2-1+b1), + python3-more-itertools (= 8.10.0-2), + python3-packaging (= 23.0-1), + python3-pep517 (= 0.13.0-2), + python3-pkg-resources (= 66.1.1-1), + python3-poetry-core (= 1.4.0-4), + python3-pydantic (= 1.10.4-1), + python3-pygments (= 2.14.0+dfsg-1), + python3-requests (= 2.28.1+dfsg-1), + python3-roman (= 3.3-3), + python3-setuptools (= 66.1.1-1), + python3-six (= 1.16.0-4), + python3-snowballstemmer (= 2.2.0-2), + python3-sphinx (= 5.3.0-4), + python3-sphinx-multiversion (= 0.2.4-2), + python3-toml (= 0.10.2-1), + python3-tomli (= 2.0.1-2), + python3-typing-extensions (= 4.4.0-1), + python3-tz (= 2022.7.1-4), + python3-urllib3 (= 1.26.12-1), + python3-wheel (= 0.38.4-2), + python3-yaml (= 6.0-3+b2), + python3-zipp (= 1.0.0-6), + python3.11 (= 3.11.2-6), + python3.11-minimal (= 3.11.2-6), + readline-common (= 8.2-1.3), + rpcsvc-proto (= 1.4.3-1), + sed (= 4.9-1), + sensible-utils (= 0.0.17+nmu1), + sgml-base (= 1.31), + sphinx-common (= 5.3.0-4), + sysvinit-utils (= 3.06-4), + tar (= 1.34+dfsg-1.2), + tzdata (= 2023c-5), + usr-is-merged (= 35), + util-linux (= 2.38.1-5+b1), + util-linux-extra (= 2.38.1-5+b1), + xml-core (= 0.18+nmu1), + xz-utils (= 5.4.1-0.2), + zlib1g (= 1:1.2.13.dfsg-1) +Environment: + DEB_BUILD_OPTIONS="parallel=16" + SOURCE_DATE_EPOCH="1700694403" diff --git a/packaging/build/compute_0.1.0.dev1-1_amd64.changes b/packaging/build/compute_0.1.0.dev1-1_amd64.changes new file mode 100644 index 0000000..c300293 --- /dev/null +++ b/packaging/build/compute_0.1.0.dev1-1_amd64.changes @@ -0,0 +1,38 @@ +Format: 1.8 +Date: Wed, 22 Nov 2023 23:06:43 +0000 +Source: compute +Binary: compute compute-doc +Architecture: source all +Version: 0.1.0.dev1-1 +Distribution: UNRELEASED +Urgency: medium +Maintainer: ge +Changed-By: ge +Description: + compute - Compute instances management library and tools (Python 3) + compute-doc - Compute instances management library and tools (documentation) +Changes: + compute (0.1.0.dev1-1) UNRELEASED; urgency=medium + . + * This is the development build, see commits in upstream repo for info. +Checksums-Sha1: + 455d0b203d96d97d4272d30be72c27cdde50fdc5 1123 compute_0.1.0.dev1-1.dsc + 94be605a5a0ca8b0ea93a46dd5029a2513486190 20824 compute_0.1.0.dev1.orig.tar.gz + 57790e9df9659f913fa1da65b77c84a3aba4976c 2660 compute_0.1.0.dev1-1.debian.tar.xz + ef2a0d6dd481adc0cf4bed1a2bc98536f1795adf 40424 compute-doc_0.1.0.dev1-1_all.deb + 20ecb0342e494a426634a5124462e49c6f7fd2c9 21644 compute_0.1.0.dev1-1_all.deb + 78f050568f3f50c415f23caf851482663c78513e 8126 compute_0.1.0.dev1-1_amd64.buildinfo +Checksums-Sha256: + c92ba4e3db43b496e01aa912f6e59240f7cd647b8b4950005182d91d071e31ac 1123 compute_0.1.0.dev1-1.dsc + e310d2ddbdb334737efc7adcc98eac2db2f158e5e989ddfade2ddfae07a6174d 20824 compute_0.1.0.dev1.orig.tar.gz + c9e267c79fa5a9e06625ac0502af528f4b526c74035fa23e6933c2c8e6429ab2 2660 compute_0.1.0.dev1-1.debian.tar.xz + 1c0d14fc87885f5dafe8bcbe1b6a07d9a57ce2dc0943a9837f751e3142ee8a42 40424 compute-doc_0.1.0.dev1-1_all.deb + a1f5a032f653276be3e4dc43818d663850463167a2b4b39138e184be1dabb44f 21644 compute_0.1.0.dev1-1_all.deb + d4ae62c2a518e36ba1acd6d56a94d1b218ffd766f5f558d7c73ac7a2ffe8102d 8126 compute_0.1.0.dev1-1_amd64.buildinfo +Files: + 635eae482cdff5bbe99a3911ed9e915c 1123 admin optional compute_0.1.0.dev1-1.dsc + de78bd5eecc56034a990dd9395a089c4 20824 admin optional compute_0.1.0.dev1.orig.tar.gz + cb9d6978a83d7f1842063315333d6278 2660 admin optional compute_0.1.0.dev1-1.debian.tar.xz + 8a8c6490cb363870735ec2572cf65cdf 40424 doc optional compute-doc_0.1.0.dev1-1_all.deb + bf5fb2ffd00e5373a54461f34b2d7033 21644 admin optional compute_0.1.0.dev1-1_all.deb + c319ab6fc548baccadb65dee3b0869af 8126 admin optional compute_0.1.0.dev1-1_amd64.buildinfo diff --git a/packaging/build/compute_0.1.0.dev1.orig.tar.gz b/packaging/build/compute_0.1.0.dev1.orig.tar.gz new file mode 100644 index 0000000..fcb6882 Binary files /dev/null and b/packaging/build/compute_0.1.0.dev1.orig.tar.gz differ diff --git a/packaging/build/docs/Makefile b/packaging/build/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/packaging/build/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/packaging/build/docs/make.bat b/packaging/build/docs/make.bat new file mode 100644 index 0000000..747ffb7 --- /dev/null +++ b/packaging/build/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/packaging/build/docs/source/_templates/versioning.html b/packaging/build/docs/source/_templates/versioning.html new file mode 100644 index 0000000..318bd87 --- /dev/null +++ b/packaging/build/docs/source/_templates/versioning.html @@ -0,0 +1,8 @@ +{% if versions %} +

{{ _('Версии') }}

+ +{% endif %} diff --git a/packaging/build/docs/source/conf.py b/packaging/build/docs/source/conf.py new file mode 100644 index 0000000..d8738f3 --- /dev/null +++ b/packaging/build/docs/source/conf.py @@ -0,0 +1,33 @@ +# Add /mnt/build/compute-0.1.0.dev1 to path for autodoc Sphinx extension +import os +import sys +sys.path.insert(0, os.path.abspath('/mnt/build/compute-0.1.0.dev1')) + +# Project information +project = 'Compute' +copyright = '2023, Compute Authors' +author = 'Compute Authors' +release = '0.1.0' + +# Sphinx general settings +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx_multiversion', +] +templates_path = ['_templates'] +exclude_patterns = [] +language = 'en' + +# HTML output settings +html_theme = 'alabaster' +html_static_path = ['_static'] +html_sidebars = { + '**': [ + 'about.html', + 'navigation.html', + 'relations.html', + 'searchbox.html', + 'donate.html', + 'versioning.html', + ] +} diff --git a/packaging/build/docs/source/index.rst b/packaging/build/docs/source/index.rst new file mode 100644 index 0000000..81222c2 --- /dev/null +++ b/packaging/build/docs/source/index.rst @@ -0,0 +1,16 @@ +Compute +======= + +Compute instances management library. + +.. toctree:: + :maxdepth: 1 + + pyapi/index + +Indices and tables +------------------ + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/packaging/build/docs/source/pyapi/exceptions.rst b/packaging/build/docs/source/pyapi/exceptions.rst new file mode 100644 index 0000000..3912721 --- /dev/null +++ b/packaging/build/docs/source/pyapi/exceptions.rst @@ -0,0 +1,5 @@ +``exceptions`` +============== + +.. automodule:: compute.exceptions + :members: diff --git a/packaging/build/docs/source/pyapi/index.rst b/packaging/build/docs/source/pyapi/index.rst new file mode 100644 index 0000000..e0cebb8 --- /dev/null +++ b/packaging/build/docs/source/pyapi/index.rst @@ -0,0 +1,49 @@ +Python API +========== + +The API allows you to perform actions on instances programmatically. Below is +an example of changing parameters and launching the `myinstance` instance. + +.. code-block:: python + + import logging + + from compute import Session + + logging.basicConfig(level=logging.DEBUG) + + with Session() as session: + instance = session.get_instance('myinstance') + instance.set_vcpus(4) + instance.start() + instance.set_autostart(enabled=True) + + +:class:`Session` context manager provides an abstraction over :class:`libvirt.virConnect` +and returns objects of other classes of the present library. + +Entity representation +--------------------- + +Entities such as a compute-instance are represented as classes. These classes directly +call libvirt methods to perform operations on the hypervisor. An example class is +:class:`Volume`. + +The configuration files of various libvirt objects in `compute` are described by special +dataclasses. The dataclass stores object parameters in its properties and can return an +XML config for libvirt using the ``to_xml()`` method. For example :class:`VolumeConfig`. + +`Pydantic `_ models are used to validate input data. +For example :class:`VolumeSchema`. + +Modules documentation +--------------------- + +.. toctree:: + :maxdepth: 4 + + session + instance/index + storage/index + utils + exceptions diff --git a/packaging/build/docs/source/pyapi/instance/guest_agent.rst b/packaging/build/docs/source/pyapi/instance/guest_agent.rst new file mode 100644 index 0000000..1305140 --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/guest_agent.rst @@ -0,0 +1,6 @@ +``guest_agent`` +=============== + +.. automodule:: compute.instance.guest_agent + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/instance/index.rst b/packaging/build/docs/source/pyapi/instance/index.rst new file mode 100644 index 0000000..659ffc2 --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/index.rst @@ -0,0 +1,10 @@ +``instance`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + instance + guest_agent + schemas diff --git a/packaging/build/docs/source/pyapi/instance/instance.rst b/packaging/build/docs/source/pyapi/instance/instance.rst new file mode 100644 index 0000000..3c58f1f --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/instance.rst @@ -0,0 +1,6 @@ +``instance`` +============ + +.. automodule:: compute.instance.instance + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/instance/schemas.rst b/packaging/build/docs/source/pyapi/instance/schemas.rst new file mode 100644 index 0000000..7dacabf --- /dev/null +++ b/packaging/build/docs/source/pyapi/instance/schemas.rst @@ -0,0 +1,5 @@ +``schemas`` +=========== + +.. automodule:: compute.instance.schemas + :members: diff --git a/packaging/build/docs/source/pyapi/session.rst b/packaging/build/docs/source/pyapi/session.rst new file mode 100644 index 0000000..2dec16e --- /dev/null +++ b/packaging/build/docs/source/pyapi/session.rst @@ -0,0 +1,6 @@ +``session`` +=========== + +.. automodule:: compute.session + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/storage/index.rst b/packaging/build/docs/source/pyapi/storage/index.rst new file mode 100644 index 0000000..e9ea734 --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/index.rst @@ -0,0 +1,9 @@ +``storage`` +============ + +.. toctree:: + :maxdepth: 1 + :caption: Contents: + + pool + volume diff --git a/packaging/build/docs/source/pyapi/storage/pool.rst b/packaging/build/docs/source/pyapi/storage/pool.rst new file mode 100644 index 0000000..398124e --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/pool.rst @@ -0,0 +1,6 @@ +``pool`` +======== + +.. automodule:: compute.storage.pool + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/storage/volume.rst b/packaging/build/docs/source/pyapi/storage/volume.rst new file mode 100644 index 0000000..e1ba8d0 --- /dev/null +++ b/packaging/build/docs/source/pyapi/storage/volume.rst @@ -0,0 +1,6 @@ +``volume`` +========== + +.. automodule:: compute.storage.volume + :members: + :special-members: __init__ diff --git a/packaging/build/docs/source/pyapi/utils.rst b/packaging/build/docs/source/pyapi/utils.rst new file mode 100644 index 0000000..b5ab60a --- /dev/null +++ b/packaging/build/docs/source/pyapi/utils.rst @@ -0,0 +1,14 @@ +``utils`` +========= + +``utils.units`` +--------------- + +.. automodule:: compute.utils.units + :members: + +``utils.ids`` +------------- + +.. automodule:: compute.utils.ids + :members: diff --git a/packaging/files/compute.bash-completion b/packaging/files/compute.bash-completion new file mode 100644 index 0000000..a0dcdf2 --- /dev/null +++ b/packaging/files/compute.bash-completion @@ -0,0 +1,93 @@ +# compute bash completion script + +_compute_root_cmd=" + --version + --verbose + --connect + --log-level + init + exec + ls + start + shutdown + reboot + reset + powrst + pause + resume + status + setvcpus + setmem + setpasswd" +_compute_init_opts="" +_compute_exec_opts=" + --timeout + --executable + --env + --no-join-args" +_compute_ls_opts="" +_compute_start_opts="" +_compute_shutdown_opts="--method" +_compute_reboot_opts="" +_compute_reset_opts="" +_compute_powrst_opts="" +_compute_pause_opts="" +_compute_resume_opts="" +_compute_status_opts="" +_compute_setvcpus_opts="" +_compute_setmem_opts="" +_compute_setpasswd_opts="--encrypted" + +_compute_complete_instances() +{ + for file in /etc/libvirt/qemu/*.xml; do + nodir="${file##*/}" + printf '%s ' "${nodir//\.xml}" + done +} + +_compute_compreply() +{ + if [[ "$current" = [a-z]* ]]; then + _compute_compwords="$(_compute_complete_instances)" + else + _compute_compwords="$*" + fi + COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) +} + +_compute_complete() +{ + local current previous nshift + current="${COMP_WORDS[COMP_CWORD]}" + case "$COMP_CWORD" in + 1) COMPREPLY=($(compgen -W "$_compute_root_cmd" -- "$current")) + ;; + 2|3|4|5) + nshift=$((COMP_CWORD-1)) + previous="${COMP_WORDS[COMP_CWORD-nshift]}" + case "$previous" in + init) COMPREPLY=($(compgen -f -- "$current"));; + exec) _compute_compreply "$_compute_exec_opts";; + ls) COMPREPLY=($(compgen -W "$_compute_ls_opts" -- "$current"));; + start) _compute_compreply "$_compute_start_opts";; + shutdown) _compute_compreply "$_compute_shutdown_opts";; + reboot) _compute_compreply "$_compute_reboot_opts";; + reset) _compute_compreply "$_compute_reset_opts";; + powrst) _compute_compreply "$_compute_powrst_opts";; + pause) _compute_compreply "$_compute_pause_opts";; + resume) _compute_compreply "$_compute_resume_opts";; + status) _compute_compreply "$_compute_status_opts";; + setvcpus) _compute_compreply "$_compute_setvcpus_opts";; + setmem) _compute_compreply "$_compute_setmem_opts";; + setpasswd) _compute_compreply "$_compute_setpasswd_opts";; + *) COMPREPLY=() + esac + ;; + *) COMPREPLY=($(compgen -W "$_compute_compwords" -- "$current")) + esac +} + +complete -F _compute_complete compute + +# vim: ft=bash diff --git a/packaging/files/control b/packaging/files/control new file mode 100644 index 0000000..6b99835 --- /dev/null +++ b/packaging/files/control @@ -0,0 +1,48 @@ +Source: compute +Section: admin +Priority: optional +Maintainer: ge +Rules-Requires-Root: no +Build-Depends: + debhelper-compat (= 13), + dh-sequence-python3, + bash-completion, + pybuild-plugin-pyproject, + python3-poetry-core, + python3-setuptools, + python3-all, + python3-sphinx, + python3-sphinx-multiversion, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Standards-Version: 4.6.2 +Homepage: https://git.lulzette.ru/hstack/compute + +Package: compute +Architecture: all +Depends: + ${python3:Depends}, + ${misc:Depends}, + qemu-system, + qemu-utils, + libvirt-daemon-system, + libvirt-clients, + python3-libvirt, + python3-lxml, + python3-yaml, + python3-pydantic +Recommends: + dnsmasq +Suggests: + compute-doc +Description: Compute instances management library and tools (Python 3) + +Package: compute-doc +Section: doc +Architecture: all +Depends: + ${sphinxdoc:Depends}, + ${misc:Depends}, +Description: Compute instances management library and tools (documentation) diff --git a/packaging/files/copyright b/packaging/files/copyright new file mode 100644 index 0000000..185dcbf --- /dev/null +++ b/packaging/files/copyright @@ -0,0 +1,32 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Source: https://git.lulzette.ru/hstack/compute +Upstream-Name: compute + +Files: + * +Copyright: + 2023 ge +License: GPL-3.0+ + +Files: + debian/* +Copyright: + 2023 ge +License: GPL-3.0+ + +License: GPL-3.0+ + 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 package 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 . +Comment: + On Debian systems, the complete text of the GNU General + Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". diff --git a/packaging/files/docs b/packaging/files/docs new file mode 100644 index 0000000..b43bf86 --- /dev/null +++ b/packaging/files/docs @@ -0,0 +1 @@ +README.md diff --git a/packaging/files/rules b/packaging/files/rules new file mode 100755 index 0000000..f99ef32 --- /dev/null +++ b/packaging/files/rules @@ -0,0 +1,20 @@ +#!/usr/bin/make -f + +export DH_VERBOSE = 1 +export PYBUILD_DESTDIR_python3=debian/compute + +%: + dh $@ --with python3,sphinxdoc,bash-completion --buildsystem=pybuild + +override_dh_auto_test: + @echo No tests there + +override_dh_sphinxdoc: +ifeq (,$(findstring nodoc, $(DEB_BUILD_OPTIONS))) + http_proxy=127.0.0.1:9 https_proxy=127.0.0.1:9 \ + HTTP_PROXY=127.0.0.1:9 HTTPS_PROXY=127.0.0.1:9 \ + PYTHONPATH=. PYTHON=python3 python3 -m sphinx $(SPHINXOPTS) -b html \ + ../docs/source \ + $(CURDIR)/debian/compute-doc/usr/share/doc/compute-doc/html + dh_sphinxdoc +endif diff --git a/pyproject.toml b/pyproject.toml index aa5e8e4..f7aab25 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [tool.poetry] name = 'compute' -version = '0.1.0' -description = 'Compute instances management library' +version = '0.1.0-dev1' +description = 'Compute instances management library and tools' authors = ['ge '] readme = 'README.md' @@ -42,10 +42,11 @@ target-version = 'py311' [tool.ruff.lint] select = ['ALL'] ignore = [ - 'Q000', 'Q003', 'D211', 'D212', 'ANN101', 'ISC001', 'COM812', + 'Q000', 'Q003', 'D211', 'D212', + 'ANN101', 'ISC001', 'COM812', 'D203', 'ANN204', 'T201', - 'EM102', 'TRY003', 'EM101', # maybe not ignore? - 'TD003', 'TD006', 'FIX002', # todo strings linting + 'EM102', 'TRY003', 'EM101', + 'TD003', 'TD006', 'FIX002', # 'todo' strings linting ] exclude = ['__init__.py'] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d31aae1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,186 @@ +libvirt-python==9.0.0 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:49702d33fa8cbcae19fa727467a69f7ae2241b3091324085ca1cc752b2b414ce +lxml==4.9.3 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:05186a0f1346ae12553d66df1cfce6f251589fea3ad3da4f3ef4e34b2d58c6a3 \ + --hash=sha256:075b731ddd9e7f68ad24c635374211376aa05a281673ede86cbe1d1b3455279d \ + --hash=sha256:081d32421db5df44c41b7f08a334a090a545c54ba977e47fd7cc2deece78809a \ + --hash=sha256:0a3d3487f07c1d7f150894c238299934a2a074ef590b583103a45002035be120 \ + --hash=sha256:0bfd0767c5c1de2551a120673b72e5d4b628737cb05414f03c3277bf9bed3305 \ + --hash=sha256:0c0850c8b02c298d3c7006b23e98249515ac57430e16a166873fc47a5d549287 \ + --hash=sha256:0e2cb47860da1f7e9a5256254b74ae331687b9672dfa780eed355c4c9c3dbd23 \ + --hash=sha256:120fa9349a24c7043854c53cae8cec227e1f79195a7493e09e0c12e29f918e52 \ + --hash=sha256:1247694b26342a7bf47c02e513d32225ededd18045264d40758abeb3c838a51f \ + --hash=sha256:141f1d1a9b663c679dc524af3ea1773e618907e96075262726c7612c02b149a4 \ + --hash=sha256:14e019fd83b831b2e61baed40cab76222139926b1fb5ed0e79225bc0cae14584 \ + --hash=sha256:1509dd12b773c02acd154582088820893109f6ca27ef7291b003d0e81666109f \ + --hash=sha256:17a753023436a18e27dd7769e798ce302963c236bc4114ceee5b25c18c52c693 \ + --hash=sha256:1e224d5755dba2f4a9498e150c43792392ac9b5380aa1b845f98a1618c94eeef \ + --hash=sha256:1f447ea5429b54f9582d4b955f5f1985f278ce5cf169f72eea8afd9502973dd5 \ + --hash=sha256:23eed6d7b1a3336ad92d8e39d4bfe09073c31bfe502f20ca5116b2a334f8ec02 \ + --hash=sha256:25f32acefac14ef7bd53e4218fe93b804ef6f6b92ffdb4322bb6d49d94cad2bc \ + --hash=sha256:2c74524e179f2ad6d2a4f7caf70e2d96639c0954c943ad601a9e146c76408ed7 \ + --hash=sha256:303bf1edce6ced16bf67a18a1cf8339d0db79577eec5d9a6d4a80f0fb10aa2da \ + --hash=sha256:3331bece23c9ee066e0fb3f96c61322b9e0f54d775fccefff4c38ca488de283a \ + --hash=sha256:3e9bdd30efde2b9ccfa9cb5768ba04fe71b018a25ea093379c857c9dad262c40 \ + --hash=sha256:411007c0d88188d9f621b11d252cce90c4a2d1a49db6c068e3c16422f306eab8 \ + --hash=sha256:42871176e7896d5d45138f6d28751053c711ed4d48d8e30b498da155af39aebd \ + --hash=sha256:46f409a2d60f634fe550f7133ed30ad5321ae2e6630f13657fb9479506b00601 \ + --hash=sha256:48628bd53a426c9eb9bc066a923acaa0878d1e86129fd5359aee99285f4eed9c \ + --hash=sha256:48d6ed886b343d11493129e019da91d4039826794a3e3027321c56d9e71505be \ + --hash=sha256:4930be26af26ac545c3dffb662521d4e6268352866956672231887d18f0eaab2 \ + --hash=sha256:4aec80cde9197340bc353d2768e2a75f5f60bacda2bab72ab1dc499589b3878c \ + --hash=sha256:4c28a9144688aef80d6ea666c809b4b0e50010a2aca784c97f5e6bf143d9f129 \ + --hash=sha256:4d2d1edbca80b510443f51afd8496be95529db04a509bc8faee49c7b0fb6d2cc \ + --hash=sha256:4dd9a263e845a72eacb60d12401e37c616438ea2e5442885f65082c276dfb2b2 \ + --hash=sha256:4f1026bc732b6a7f96369f7bfe1a4f2290fb34dce00d8644bc3036fb351a4ca1 \ + --hash=sha256:4fb960a632a49f2f089d522f70496640fdf1218f1243889da3822e0a9f5f3ba7 \ + --hash=sha256:50670615eaf97227d5dc60de2dc99fb134a7130d310d783314e7724bf163f75d \ + --hash=sha256:50baa9c1c47efcaef189f31e3d00d697c6d4afda5c3cde0302d063492ff9b477 \ + --hash=sha256:53ace1c1fd5a74ef662f844a0413446c0629d151055340e9893da958a374f70d \ + --hash=sha256:5515edd2a6d1a5a70bfcdee23b42ec33425e405c5b351478ab7dc9347228f96e \ + --hash=sha256:56dc1f1ebccc656d1b3ed288f11e27172a01503fc016bcabdcbc0978b19352b7 \ + --hash=sha256:578695735c5a3f51569810dfebd05dd6f888147a34f0f98d4bb27e92b76e05c2 \ + --hash=sha256:57aba1bbdf450b726d58b2aea5fe47c7875f5afb2c4a23784ed78f19a0462574 \ + --hash=sha256:57d6ba0ca2b0c462f339640d22882acc711de224d769edf29962b09f77129cbf \ + --hash=sha256:5c245b783db29c4e4fbbbfc9c5a78be496c9fea25517f90606aa1f6b2b3d5f7b \ + --hash=sha256:5c31c7462abdf8f2ac0577d9f05279727e698f97ecbb02f17939ea99ae8daa98 \ + --hash=sha256:64f479d719dc9f4c813ad9bb6b28f8390360660b73b2e4beb4cb0ae7104f1c12 \ + --hash=sha256:65299ea57d82fb91c7f019300d24050c4ddeb7c5a190e076b5f48a2b43d19c42 \ + --hash=sha256:6689a3d7fd13dc687e9102a27e98ef33730ac4fe37795d5036d18b4d527abd35 \ + --hash=sha256:690dafd0b187ed38583a648076865d8c229661ed20e48f2335d68e2cf7dc829d \ + --hash=sha256:6fc3c450eaa0b56f815c7b62f2b7fba7266c4779adcf1cece9e6deb1de7305ce \ + --hash=sha256:704f61ba8c1283c71b16135caf697557f5ecf3e74d9e453233e4771d68a1f42d \ + --hash=sha256:71c52db65e4b56b8ddc5bb89fb2e66c558ed9d1a74a45ceb7dcb20c191c3df2f \ + --hash=sha256:71d66ee82e7417828af6ecd7db817913cb0cf9d4e61aa0ac1fde0583d84358db \ + --hash=sha256:7d298a1bd60c067ea75d9f684f5f3992c9d6766fadbc0bcedd39750bf344c2f4 \ + --hash=sha256:8b77946fd508cbf0fccd8e400a7f71d4ac0e1595812e66025bac475a8e811694 \ + --hash=sha256:8d7e43bd40f65f7d97ad8ef5c9b1778943d02f04febef12def25f7583d19baac \ + --hash=sha256:8df133a2ea5e74eef5e8fc6f19b9e085f758768a16e9877a60aec455ed2609b2 \ + --hash=sha256:8ed74706b26ad100433da4b9d807eae371efaa266ffc3e9191ea436087a9d6a7 \ + --hash=sha256:92af161ecbdb2883c4593d5ed4815ea71b31fafd7fd05789b23100d081ecac96 \ + --hash=sha256:97047f0d25cd4bcae81f9ec9dc290ca3e15927c192df17331b53bebe0e3ff96d \ + --hash=sha256:9719fe17307a9e814580af1f5c6e05ca593b12fb7e44fe62450a5384dbf61b4b \ + --hash=sha256:9767e79108424fb6c3edf8f81e6730666a50feb01a328f4a016464a5893f835a \ + --hash=sha256:9a92d3faef50658dd2c5470af249985782bf754c4e18e15afb67d3ab06233f13 \ + --hash=sha256:9bb6ad405121241e99a86efff22d3ef469024ce22875a7ae045896ad23ba2340 \ + --hash=sha256:9e28c51fa0ce5674be9f560c6761c1b441631901993f76700b1b30ca6c8378d6 \ + --hash=sha256:aca086dc5f9ef98c512bac8efea4483eb84abbf926eaeedf7b91479feb092458 \ + --hash=sha256:ae8b9c6deb1e634ba4f1930eb67ef6e6bf6a44b6eb5ad605642b2d6d5ed9ce3c \ + --hash=sha256:b0a545b46b526d418eb91754565ba5b63b1c0b12f9bd2f808c852d9b4b2f9b5c \ + --hash=sha256:b4e4bc18382088514ebde9328da057775055940a1f2e18f6ad2d78aa0f3ec5b9 \ + --hash=sha256:b6420a005548ad52154c8ceab4a1290ff78d757f9e5cbc68f8c77089acd3c432 \ + --hash=sha256:b86164d2cff4d3aaa1f04a14685cbc072efd0b4f99ca5708b2ad1b9b5988a991 \ + --hash=sha256:bb3bb49c7a6ad9d981d734ef7c7193bc349ac338776a0360cc671eaee89bcf69 \ + --hash=sha256:bef4e656f7d98aaa3486d2627e7d2df1157d7e88e7efd43a65aa5dd4714916cf \ + --hash=sha256:c0781a98ff5e6586926293e59480b64ddd46282953203c76ae15dbbbf302e8bb \ + --hash=sha256:c2006f5c8d28dee289f7020f721354362fa304acbaaf9745751ac4006650254b \ + --hash=sha256:c41bfca0bd3532d53d16fd34d20806d5c2b1ace22a2f2e4c0008570bf2c58833 \ + --hash=sha256:cd47b4a0d41d2afa3e58e5bf1f62069255aa2fd6ff5ee41604418ca925911d76 \ + --hash=sha256:cdb650fc86227eba20de1a29d4b2c1bfe139dc75a0669270033cb2ea3d391b85 \ + --hash=sha256:cef2502e7e8a96fe5ad686d60b49e1ab03e438bd9123987994528febd569868e \ + --hash=sha256:d27be7405547d1f958b60837dc4c1007da90b8b23f54ba1f8b728c78fdb19d50 \ + --hash=sha256:d37017287a7adb6ab77e1c5bee9bcf9660f90ff445042b790402a654d2ad81d8 \ + --hash=sha256:d3ff32724f98fbbbfa9f49d82852b159e9784d6094983d9a8b7f2ddaebb063d4 \ + --hash=sha256:d73d8ecf8ecf10a3bd007f2192725a34bd62898e8da27eb9d32a58084f93962b \ + --hash=sha256:dd708cf4ee4408cf46a48b108fb9427bfa00b9b85812a9262b5c668af2533ea5 \ + --hash=sha256:e3cd95e10c2610c360154afdc2f1480aea394f4a4f1ea0a5eacce49640c9b190 \ + --hash=sha256:e4da8ca0c0c0aea88fd46be8e44bd49716772358d648cce45fe387f7b92374a7 \ + --hash=sha256:eadfbbbfb41b44034a4c757fd5d70baccd43296fb894dba0295606a7cf3124aa \ + --hash=sha256:ed667f49b11360951e201453fc3967344d0d0263aa415e1619e85ae7fd17b4e0 \ + --hash=sha256:f3df3db1d336b9356dd3112eae5f5c2b8b377f3bc826848567f10bfddfee77e9 \ + --hash=sha256:f6bdac493b949141b733c5345b6ba8f87a226029cbabc7e9e121a413e49441e0 \ + --hash=sha256:fbf521479bcac1e25a663df882c46a641a9bff6b56dc8b0fafaebd2f66fb231b \ + --hash=sha256:fc9b106a1bf918db68619fdcd6d5ad4f972fdd19c01d19bdb6bf63f3589a9ec5 \ + --hash=sha256:fcdd00edfd0a3001e0181eab3e63bd5c74ad3e67152c84f93f13769a40e073a7 \ + --hash=sha256:fe4bda6bd4340caa6e5cf95e73f8fea5c4bfc55763dd42f1b50a94c1b4a2fbd4 +pydantic==1.10.4 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:05a81b006be15655b2a1bae5faa4280cf7c81d0e09fcb49b342ebf826abe5a72 \ + --hash=sha256:0b53e1d41e97063d51a02821b80538053ee4608b9a181c1005441f1673c55423 \ + --hash=sha256:2b3ce5f16deb45c472dde1a0ee05619298c864a20cded09c4edd820e1454129f \ + --hash=sha256:2e82a6d37a95e0b1b42b82ab340ada3963aea1317fd7f888bb6b9dfbf4fff57c \ + --hash=sha256:301d626a59edbe5dfb48fcae245896379a450d04baeed50ef40d8199f2733b06 \ + --hash=sha256:39f4a73e5342b25c2959529f07f026ef58147249f9b7431e1ba8414a36761f53 \ + --hash=sha256:4948f264678c703f3877d1c8877c4e3b2e12e549c57795107f08cf70c6ec7774 \ + --hash=sha256:4b05697738e7d2040696b0a66d9f0a10bec0efa1883ca75ee9e55baf511909d6 \ + --hash=sha256:51bdeb10d2db0f288e71d49c9cefa609bca271720ecd0c58009bd7504a0c464c \ + --hash=sha256:55b1625899acd33229c4352ce0ae54038529b412bd51c4915349b49ca575258f \ + --hash=sha256:572066051eeac73d23f95ba9a71349c42a3e05999d0ee1572b7860235b850cc6 \ + --hash=sha256:6a05a9db1ef5be0fe63e988f9617ca2551013f55000289c671f71ec16f4985e3 \ + --hash=sha256:6dc1cc241440ed7ca9ab59d9929075445da6b7c94ced281b3dd4cfe6c8cff817 \ + --hash=sha256:6e7124d6855b2780611d9f5e1e145e86667eaa3bd9459192c8dc1a097f5e9903 \ + --hash=sha256:75d52162fe6b2b55964fbb0af2ee58e99791a3138588c482572bb6087953113a \ + --hash=sha256:78cec42b95dbb500a1f7120bdf95c401f6abb616bbe8785ef09887306792e66e \ + --hash=sha256:7feb6a2d401f4d6863050f58325b8d99c1e56f4512d98b11ac64ad1751dc647d \ + --hash=sha256:8775d4ef5e7299a2f4699501077a0defdaac5b6c4321173bcb0f3c496fbadf85 \ + --hash=sha256:887ca463c3bc47103c123bc06919c86720e80e1214aab79e9b779cda0ff92a00 \ + --hash=sha256:9193d4f4ee8feca58bc56c8306bcb820f5c7905fd919e0750acdeeeef0615b28 \ + --hash=sha256:983e720704431a6573d626b00662eb78a07148c9115129f9b4351091ec95ecc3 \ + --hash=sha256:990406d226dea0e8f25f643b370224771878142155b879784ce89f633541a024 \ + --hash=sha256:9cbdc268a62d9a98c56e2452d6c41c0263d64a2009aac69246486f01b4f594c4 \ + --hash=sha256:a48f1953c4a1d9bd0b5167ac50da9a79f6072c63c4cef4cf2a3736994903583e \ + --hash=sha256:a9a6747cac06c2beb466064dda999a13176b23535e4c496c9d48e6406f92d42d \ + --hash=sha256:a9f2de23bec87ff306aef658384b02aa7c32389766af3c5dee9ce33e80222dfa \ + --hash=sha256:b5635de53e6686fe7a44b5cf25fcc419a0d5e5c1a1efe73d49d48fe7586db854 \ + --hash=sha256:b6f9d649892a6f54a39ed56b8dfd5e08b5f3be5f893da430bed76975f3735d15 \ + --hash=sha256:b9a3859f24eb4e097502a3be1fb4b2abb79b6103dd9e2e0edb70613a4459a648 \ + --hash=sha256:cd8702c5142afda03dc2b1ee6bc358b62b3735b2cce53fc77b31ca9f728e4bc8 \ + --hash=sha256:d7b5a3821225f5c43496c324b0d6875fde910a1c2933d726a743ce328fbb2a8c \ + --hash=sha256:d88c4c0e5c5dfd05092a4b271282ef0588e5f4aaf345778056fc5259ba098857 \ + --hash=sha256:eb992a1ef739cc7b543576337bebfc62c0e6567434e522e97291b251a41dad7f \ + --hash=sha256:f2f7eb6273dd12472d7f218e1fef6f7c7c2f00ac2e1ecde4db8824c457300416 \ + --hash=sha256:fdf88ab63c3ee282c76d652fc86518aacb737ff35796023fae56a65ced1a5978 \ + --hash=sha256:fdf8d759ef326962b4678d89e275ffc55b7ce59d917d9f72233762061fd04a2d +pyyaml==6.0.1 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \ + --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \ + --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \ + --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \ + --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \ + --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \ + --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \ + --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \ + --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \ + --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \ + --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \ + --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \ + --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \ + --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \ + --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \ + --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \ + --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \ + --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \ + --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \ + --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \ + --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \ + --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \ + --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \ + --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \ + --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \ + --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \ + --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \ + --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \ + --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \ + --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \ + --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \ + --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \ + --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \ + --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \ + --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \ + --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \ + --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \ + --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \ + --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \ + --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \ + --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \ + --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \ + --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \ + --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \ + --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \ + --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \ + --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \ + --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \ + --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \ + --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f +typing-extensions==4.8.0 ; python_version >= "3.11" and python_version < "4.0" \ + --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \ + --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef