various improvements
This commit is contained in:
		
							
								
								
									
										674
									
								
								COPYING
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										674
									
								
								COPYING
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,674 @@
 | 
				
			|||||||
 | 
					                    GNU GENERAL PUBLIC LICENSE
 | 
				
			||||||
 | 
					                       Version 3, 29 June 2007
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
 | 
				
			||||||
 | 
					 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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <one line to give the program's name and a brief idea of what it does.>
 | 
				
			||||||
 | 
					    Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <program>  Copyright (C) <year>  <name of author>
 | 
				
			||||||
 | 
					    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
 | 
				
			||||||
 | 
					<https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  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
 | 
				
			||||||
 | 
					<https://www.gnu.org/licenses/why-not-lgpl.html>.
 | 
				
			||||||
							
								
								
									
										38
									
								
								Makefile
									
									
									
									
									
								
							
							
						
						
									
										38
									
								
								Makefile
									
									
									
									
									
								
							@@ -1,36 +1,44 @@
 | 
				
			|||||||
SRC = compute/
 | 
					SRCDIR = compute
 | 
				
			||||||
DIST = dist/
 | 
					DISTDIR = dist
 | 
				
			||||||
DOCS_SRC = docs/source/
 | 
					DOCS_SRCDIR = docs/source
 | 
				
			||||||
DOCS_BUILD = docs/build/
 | 
					DOCS_BUILDDIR = docs/build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.PHONY: docs
 | 
					.PHONY: docs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
all: build
 | 
					all: build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					requirements.txt:
 | 
				
			||||||
 | 
						poetry export -f requirements.txt -o requirements.txt
 | 
				
			||||||
 | 
					
 | 
				
			||||||
build: format lint
 | 
					build: format lint
 | 
				
			||||||
 | 
						awk '/^version/{print $$3}' pyproject.toml \
 | 
				
			||||||
 | 
							| xargs -I {} sed "s/__version__ =.*/__version__ = '{}'/" -i $(SRCDIR)/__init__.py
 | 
				
			||||||
	poetry build
 | 
						poetry build
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					build-deb: build
 | 
				
			||||||
 | 
						cd packaging && $(MAKE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
format:
 | 
					format:
 | 
				
			||||||
	poetry run isort $(SRC)
 | 
						poetry run isort $(SRCDIR)
 | 
				
			||||||
	poetry run ruff format $(SRC)
 | 
						poetry run ruff format $(SRCDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
lint:
 | 
					lint:
 | 
				
			||||||
	poetry run ruff check $(SRC)
 | 
						poetry run ruff check $(SRCDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
docs:
 | 
					docs:
 | 
				
			||||||
	poetry run sphinx-build $(DOCS_SRC) $(DOCS_BUILD)
 | 
						poetry run sphinx-build $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
docs-versions:
 | 
					docs-versions:
 | 
				
			||||||
	poetry run sphinx-multiversion $(DOCS_SRC) $(DOCS_BUILD)
 | 
						poetry run sphinx-multiversion $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
serve-docs:
 | 
					serve-docs:
 | 
				
			||||||
	poetry run sphinx-autobuild $(DOCS_SRC) $(DOCS_BUILD)
 | 
						poetry run sphinx-autobuild $(DOCS_SRCDIR) $(DOCS_BUILDDIR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
clean:
 | 
					clean:
 | 
				
			||||||
	[ -d $(DIST) ] && rm -rf $(DIST) || true
 | 
						[ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true
 | 
				
			||||||
	[ -d $(DOCS_BUILD) ] && rm -rf $(DOCS_BUILD) || true
 | 
						[ -d $(DOCS_BUILDDIR) ] && rm -rf $(DOCS_BUILDDIR) || true
 | 
				
			||||||
	find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
 | 
						find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
 | 
				
			||||||
 | 
						cd packaging && $(MAKE) clean
 | 
				
			||||||
 | 
					
 | 
				
			||||||
test-build:
 | 
					test-build: build-deb
 | 
				
			||||||
	poetry build
 | 
						scp packaging/build/compute*.deb vm:~
 | 
				
			||||||
	scp $(DIST)/*.tar.gz vm:~
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										104
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										104
									
								
								README.md
									
									
									
									
									
								
							@@ -49,3 +49,107 @@ Install [poetry](https://python-poetry.org/), clone this repository and run:
 | 
				
			|||||||
```
 | 
					```
 | 
				
			||||||
poetry install --with dev --with docs
 | 
					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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Compute instances management library."""
 | 
					"""Compute instances management library."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__version__ = '0.1.0'
 | 
					__version__ = '0.1.0-dev1'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .instance import Instance, InstanceConfig, InstanceSchema
 | 
					from .instance import Instance, InstanceConfig, InstanceSchema
 | 
				
			||||||
from .session import Session
 | 
					from .session import Session
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Command line interface for compute module."""
 | 
					"""Command line interface for compute module."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from compute.cli import control
 | 
					from compute.cli import main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
control.cli()
 | 
					main.cli()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Command line interface."""
 | 
					"""Command line interface."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import argparse
 | 
					import argparse
 | 
				
			||||||
@@ -15,10 +30,7 @@ import yaml
 | 
				
			|||||||
from pydantic import ValidationError
 | 
					from pydantic import ValidationError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from compute import __version__
 | 
					from compute import __version__
 | 
				
			||||||
from compute.exceptions import (
 | 
					from compute.exceptions import ComputeError, GuestAgentTimeoutExceededError
 | 
				
			||||||
    ComputeServiceError,
 | 
					 | 
				
			||||||
    GuestAgentTimeoutExceededError,
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
from compute.instance import GuestAgent
 | 
					from compute.instance import GuestAgent
 | 
				
			||||||
from compute.session import Session
 | 
					from compute.session import Session
 | 
				
			||||||
from compute.utils import ids
 | 
					from compute.utils import ids
 | 
				
			||||||
@@ -198,10 +210,23 @@ def _create_instance(session: Session, file: io.TextIOWrapper) -> None:
 | 
				
			|||||||
        sys.exit()
 | 
					        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:
 | 
					def main(session: Session, args: argparse.Namespace) -> None:
 | 
				
			||||||
    """Perform actions."""
 | 
					    """Perform actions."""
 | 
				
			||||||
    match args.command:
 | 
					    match args.command:
 | 
				
			||||||
        case 'create':
 | 
					        case 'init':
 | 
				
			||||||
            _create_instance(session, args.file)
 | 
					            _create_instance(session, args.file)
 | 
				
			||||||
        case 'exec':
 | 
					        case 'exec':
 | 
				
			||||||
            _exec_guest_agent_command(session, args)
 | 
					            _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 = session.get_instance(args.instance)
 | 
				
			||||||
            instance.start()
 | 
					            instance.start()
 | 
				
			||||||
        case 'shutdown':
 | 
					        case 'shutdown':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            _shutdown_instance(session, args)
 | 
				
			||||||
            instance.shutdown(args.method)
 | 
					 | 
				
			||||||
        case 'reboot':
 | 
					        case 'reboot':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
            instance.reboot()
 | 
					            instance.reboot()
 | 
				
			||||||
        case 'reset':
 | 
					        case 'reset':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
            instance.reset()
 | 
					            instance.reset()
 | 
				
			||||||
 | 
					        case 'powrst':
 | 
				
			||||||
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
 | 
					            instance.power_reset()
 | 
				
			||||||
        case 'pause':
 | 
					        case 'pause':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
            instance.pause()
 | 
					            instance.pause()
 | 
				
			||||||
@@ -234,7 +261,7 @@ def main(session: Session, args: argparse.Namespace) -> None:
 | 
				
			|||||||
        case 'setmem':
 | 
					        case 'setmem':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
            instance.set_memory(args.memory, live=True)
 | 
					            instance.set_memory(args.memory, live=True)
 | 
				
			||||||
        case 'setpasswd':
 | 
					        case 'setpass':
 | 
				
			||||||
            instance = session.get_instance(args.instance)
 | 
					            instance = session.get_instance(args.instance)
 | 
				
			||||||
            instance.set_user_password(
 | 
					            instance.set_user_password(
 | 
				
			||||||
                args.username,
 | 
					                args.username,
 | 
				
			||||||
@@ -261,7 +288,6 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        '-c',
 | 
					        '-c',
 | 
				
			||||||
        '--connect',
 | 
					        '--connect',
 | 
				
			||||||
        metavar='URI',
 | 
					        metavar='URI',
 | 
				
			||||||
        default='qemu:///system',
 | 
					 | 
				
			||||||
        help='libvirt connection URI',
 | 
					        help='libvirt connection URI',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    root.add_argument(
 | 
					    root.add_argument(
 | 
				
			||||||
@@ -270,7 +296,7 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        type=str.lower,
 | 
					        type=str.lower,
 | 
				
			||||||
        metavar='LEVEL',
 | 
					        metavar='LEVEL',
 | 
				
			||||||
        choices=log_levels,
 | 
					        choices=log_levels,
 | 
				
			||||||
        help='log level [envvar: CMP_LOG]',
 | 
					        help='log level',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    root.add_argument(
 | 
					    root.add_argument(
 | 
				
			||||||
        '-V',
 | 
					        '-V',
 | 
				
			||||||
@@ -280,13 +306,16 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
    subparsers = root.add_subparsers(dest='command', metavar='COMMAND')
 | 
					    subparsers = root.add_subparsers(dest='command', metavar='COMMAND')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # create command
 | 
					    # init command
 | 
				
			||||||
    create = subparsers.add_parser(
 | 
					    init = subparsers.add_parser(
 | 
				
			||||||
        'create', help='create new instance from YAML config file'
 | 
					        'init', help='initialise instance using YAML config file'
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    create.add_argument(
 | 
					    init.add_argument(
 | 
				
			||||||
        'file',
 | 
					        'file',
 | 
				
			||||||
        type=argparse.FileType('r', encoding='UTF-8'),
 | 
					        type=argparse.FileType('r', encoding='UTF-8'),
 | 
				
			||||||
 | 
					        nargs='?',
 | 
				
			||||||
 | 
					        default='instance.yaml',
 | 
				
			||||||
 | 
					        help='instance config [default: instance.yaml]',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # exec subcommand
 | 
					    # exec subcommand
 | 
				
			||||||
@@ -307,14 +336,14 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        default=60,
 | 
					        default=60,
 | 
				
			||||||
        help=(
 | 
					        help=(
 | 
				
			||||||
            'waiting time in seconds for a command to be executed '
 | 
					            'waiting time in seconds for a command to be executed '
 | 
				
			||||||
            'in guest, 60 sec by default'
 | 
					            'in guest [default: 60]'
 | 
				
			||||||
        ),
 | 
					        ),
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    execute.add_argument(
 | 
					    execute.add_argument(
 | 
				
			||||||
        '-x',
 | 
					        '-x',
 | 
				
			||||||
        '--executable',
 | 
					        '--executable',
 | 
				
			||||||
        default='/bin/sh',
 | 
					        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(
 | 
					    execute.add_argument(
 | 
				
			||||||
        '-e',
 | 
					        '-e',
 | 
				
			||||||
@@ -352,12 +381,36 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
    # shutdown subcommand
 | 
					    # shutdown subcommand
 | 
				
			||||||
    shutdown = subparsers.add_parser('shutdown', help='shutdown instance')
 | 
					    shutdown = subparsers.add_parser('shutdown', help='shutdown instance')
 | 
				
			||||||
    shutdown.add_argument('instance')
 | 
					    shutdown.add_argument('instance')
 | 
				
			||||||
    shutdown.add_argument(
 | 
					    shutdown_opts = shutdown.add_mutually_exclusive_group()
 | 
				
			||||||
        '-m',
 | 
					    shutdown_opts.add_argument(
 | 
				
			||||||
        '--method',
 | 
					        '-s',
 | 
				
			||||||
        choices=['soft', 'normal', 'hard', 'unsafe'],
 | 
					        '--soft',
 | 
				
			||||||
        default='normal',
 | 
					        action='store_true',
 | 
				
			||||||
        help='use shutdown method',
 | 
					        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 subcommand
 | 
				
			||||||
@@ -368,6 +421,10 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
    reset = subparsers.add_parser('reset', help='reset instance')
 | 
					    reset = subparsers.add_parser('reset', help='reset instance')
 | 
				
			||||||
    reset.add_argument('instance')
 | 
					    reset.add_argument('instance')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # powrst subcommand
 | 
				
			||||||
 | 
					    powrst = subparsers.add_parser('powrst', help='power reset instance')
 | 
				
			||||||
 | 
					    powrst.add_argument('instance')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # pause subcommand
 | 
					    # pause subcommand
 | 
				
			||||||
    pause = subparsers.add_parser('pause', help='pause instance')
 | 
					    pause = subparsers.add_parser('pause', help='pause instance')
 | 
				
			||||||
    pause.add_argument('instance')
 | 
					    pause.add_argument('instance')
 | 
				
			||||||
@@ -390,15 +447,15 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
    setmem.add_argument('instance')
 | 
					    setmem.add_argument('instance')
 | 
				
			||||||
    setmem.add_argument('memory', type=int, help='memory in MiB')
 | 
					    setmem.add_argument('memory', type=int, help='memory in MiB')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # setpasswd subcommand
 | 
					    # setpass subcommand
 | 
				
			||||||
    setpasswd = subparsers.add_parser(
 | 
					    setpass = subparsers.add_parser(
 | 
				
			||||||
        'setpasswd',
 | 
					        'setpass',
 | 
				
			||||||
        help='set user password in guest',
 | 
					        help='set user password in guest',
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    setpasswd.add_argument('instance')
 | 
					    setpass.add_argument('instance')
 | 
				
			||||||
    setpasswd.add_argument('username')
 | 
					    setpass.add_argument('username')
 | 
				
			||||||
    setpasswd.add_argument('password')
 | 
					    setpass.add_argument('password')
 | 
				
			||||||
    setpasswd.add_argument(
 | 
					    setpass.add_argument(
 | 
				
			||||||
        '-e',
 | 
					        '-e',
 | 
				
			||||||
        '--encrypted',
 | 
					        '--encrypted',
 | 
				
			||||||
        action='store_true',
 | 
					        action='store_true',
 | 
				
			||||||
@@ -419,10 +476,18 @@ def cli() -> None:  # noqa: PLR0915
 | 
				
			|||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    log.debug('CLI started with args: %s', args)
 | 
					    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:
 | 
					    try:
 | 
				
			||||||
        with Session(args.connect) as session:
 | 
					        with Session(connect_uri) as session:
 | 
				
			||||||
            main(session, args)
 | 
					            main(session, args)
 | 
				
			||||||
    except ComputeServiceError as e:
 | 
					    except ComputeError as e:
 | 
				
			||||||
        sys.exit(f'error: {e}')
 | 
					        sys.exit(f'error: {e}')
 | 
				
			||||||
    except KeyboardInterrupt:
 | 
					    except KeyboardInterrupt:
 | 
				
			||||||
        sys.exit()
 | 
					        sys.exit()
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										30
									
								
								compute/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								compute/common.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Exceptions."""
 | 
					"""Exceptions."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ComputeServiceError(Exception):
 | 
					class ComputeError(Exception):
 | 
				
			||||||
    """Basic exception class for Compute."""
 | 
					    """Basic exception class."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class ConfigLoaderError(ComputeServiceError):
 | 
					class ConfigLoaderError(ComputeError):
 | 
				
			||||||
    """Something went wrong when loading configuration."""
 | 
					    """Something went wrong when loading configuration."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class SessionError(ComputeServiceError):
 | 
					class SessionError(ComputeError):
 | 
				
			||||||
    """Something went wrong while connecting to libvirtd."""
 | 
					    """Something went wrong while connecting to libvirtd."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class GuestAgentError(ComputeServiceError):
 | 
					class GuestAgentError(ComputeError):
 | 
				
			||||||
    """Something went wring when QEMU Guest Agent call."""
 | 
					    """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."""
 | 
					    """Guest agent command is not supported or blacklisted on guest."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class StoragePoolError(ComputeServiceError):
 | 
					class StoragePoolError(ComputeError):
 | 
				
			||||||
    """Something went wrong when operating with storage pool."""
 | 
					    """Something went wrong when operating with storage pool."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -53,7 +68,7 @@ class VolumeNotFoundError(StoragePoolError):
 | 
				
			|||||||
        super().__init__(f"storage volume '{msg}' not found")
 | 
					        super().__init__(f"storage volume '{msg}' not found")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InstanceError(ComputeServiceError):
 | 
					class InstanceError(ComputeError):
 | 
				
			||||||
    """Something went wrong while interacting with the domain."""
 | 
					    """Something went wrong while interacting with the domain."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .guest_agent import GuestAgent
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
from .instance import Instance, InstanceConfig
 | 
					from .instance import Instance, InstanceConfig
 | 
				
			||||||
from .schemas import InstanceSchema
 | 
					from .schemas import InstanceSchema
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Interacting with the QEMU Guest Agent."""
 | 
					"""Interacting with the QEMU Guest Agent."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import json
 | 
					import json
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage compute instances."""
 | 
					"""Manage compute instances."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
					__all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
 | 
				
			||||||
@@ -9,6 +24,7 @@ import libvirt
 | 
				
			|||||||
from lxml import etree
 | 
					from lxml import etree
 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.common import DeviceConfig, EntityConfig
 | 
				
			||||||
from compute.exceptions import (
 | 
					from compute.exceptions import (
 | 
				
			||||||
    GuestAgentCommandNotSupportedError,
 | 
					    GuestAgentCommandNotSupportedError,
 | 
				
			||||||
    InstanceError,
 | 
					    InstanceError,
 | 
				
			||||||
@@ -28,8 +44,8 @@ from .schemas import (
 | 
				
			|||||||
log = logging.getLogger(__name__)
 | 
					log = logging.getLogger(__name__)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InstanceConfig:
 | 
					class InstanceConfig(EntityConfig):
 | 
				
			||||||
    """Compute instance config builder."""
 | 
					    """Compute instance XML config builder."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __init__(self, schema: InstanceSchema):
 | 
					    def __init__(self, schema: InstanceSchema):
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -181,10 +197,6 @@ class InstanceInfo(NamedTuple):
 | 
				
			|||||||
    cputime: int
 | 
					    cputime: int
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DeviceConfig:
 | 
					 | 
				
			||||||
    """Abstract device config class."""
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
class Instance:
 | 
					class Instance:
 | 
				
			||||||
    """Manage compute instances."""
 | 
					    """Manage compute instances."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -492,7 +504,7 @@ class Instance:
 | 
				
			|||||||
        return child[0].getparent() if child else None
 | 
					        return child[0].getparent() if child else None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def attach_device(
 | 
					    def attach_device(
 | 
				
			||||||
        self, device: 'DeviceConfig', *, live: bool = False
 | 
					        self, device: DeviceConfig, *, live: bool = False
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Attach device to compute instance.
 | 
					        Attach device to compute instance.
 | 
				
			||||||
@@ -517,7 +529,7 @@ class Instance:
 | 
				
			|||||||
        self.domain.attachDeviceFlags(device.to_xml(), flags=flags)
 | 
					        self.domain.attachDeviceFlags(device.to_xml(), flags=flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def detach_device(
 | 
					    def detach_device(
 | 
				
			||||||
        self, device: 'DeviceConfig', *, live: bool = False
 | 
					        self, device: DeviceConfig, *, live: bool = False
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Dettach device from compute instance.
 | 
					        Dettach device from compute instance.
 | 
				
			||||||
@@ -545,8 +557,8 @@ class Instance:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Detach disk device by target name.
 | 
					        Detach disk device by target name.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        There is no ``attach_disk()`` method. Use :method:`attach_device`
 | 
					        There is no ``attach_disk()`` method. Use :func:`attach_device`
 | 
				
			||||||
        with :class:`DiskConfig` as parameter.
 | 
					        with :class:`DiskConfig` as argument.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param name: Disk name e.g. 'vda', 'sda', etc. This name may
 | 
					        :param name: Disk name e.g. 'vda', 'sda', etc. This name may
 | 
				
			||||||
            not match the name of the disk inside the guest OS.
 | 
					            not match the name of the disk inside the guest OS.
 | 
				
			||||||
@@ -574,14 +586,14 @@ class Instance:
 | 
				
			|||||||
                raise InstanceError(msg)
 | 
					                raise InstanceError(msg)
 | 
				
			||||||
        self.detach_device(DiskConfig(**disk_params), live=True)
 | 
					        self.detach_device(DiskConfig(**disk_params), live=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def resize_volume(
 | 
					    def resize_disk(
 | 
				
			||||||
        self, name: str, capacity: int, unit: units.DataUnit
 | 
					        self, name: str, capacity: int, unit: units.DataUnit
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        Resize attached block device.
 | 
					        Resize attached block device.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        :param name: Disk device name e.g. `vda`, `sda`, etc.
 | 
					        :param name: Disk device name e.g. `vda`, `sda`, etc.
 | 
				
			||||||
        :param capacity: New volume capacity.
 | 
					        :param capacity: New capacity.
 | 
				
			||||||
        :param unit: Capacity unit.
 | 
					        :param unit: Capacity unit.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
        self.domain.blockResize(
 | 
					        self.domain.blockResize(
 | 
				
			||||||
@@ -590,6 +602,10 @@ class Instance:
 | 
				
			|||||||
            flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
 | 
					            flags=libvirt.VIR_DOMAIN_BLOCK_RESIZE_BYTES,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def get_disks(self) -> list[DiskConfig]:
 | 
				
			||||||
 | 
					        """Return list of attached disks."""
 | 
				
			||||||
 | 
					        raise NotImplementedError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def pause(self) -> None:
 | 
					    def pause(self) -> None:
 | 
				
			||||||
        """Pause instance."""
 | 
					        """Pause instance."""
 | 
				
			||||||
        if not self.is_running():
 | 
					        if not self.is_running():
 | 
				
			||||||
@@ -600,9 +616,9 @@ class Instance:
 | 
				
			|||||||
        """Resume paused instance."""
 | 
					        """Resume paused instance."""
 | 
				
			||||||
        self.domain.resume()
 | 
					        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.
 | 
					        :param user: Username.
 | 
				
			||||||
        """
 | 
					        """
 | 
				
			||||||
@@ -617,7 +633,7 @@ class Instance:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        raise NotImplementedError
 | 
					        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.
 | 
					        Remove SSH keys from guest for specific user.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -632,7 +648,7 @@ class Instance:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        Set new user password in guest OS.
 | 
					        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 user: Username.
 | 
				
			||||||
        :param password: Password.
 | 
					        :param password: Password.
 | 
				
			||||||
@@ -653,6 +669,7 @@ class Instance:
 | 
				
			|||||||
        return self.domain.XMLDesc(flags)
 | 
					        return self.domain.XMLDesc(flags)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def delete(self) -> None:
 | 
					    def delete(self) -> None:
 | 
				
			||||||
        """Undefine instance and delete local volumes."""
 | 
					        """Undefine instance."""
 | 
				
			||||||
 | 
					        # TODO @ge: delete local disks
 | 
				
			||||||
        self.shutdown(method='HARD')
 | 
					        self.shutdown(method='HARD')
 | 
				
			||||||
        self.domain.undefine()
 | 
					        self.domain.undefine()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Compute instance related objects schemas."""
 | 
					"""Compute instance related objects schemas."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import re
 | 
					import re
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Hypervisor session manager."""
 | 
					"""Hypervisor session manager."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from .pool import StoragePool
 | 
					from .pool import StoragePool
 | 
				
			||||||
from .volume import DiskConfig, Volume, VolumeConfig
 | 
					from .volume import DiskConfig, Volume, VolumeConfig
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage storage pools."""
 | 
					"""Manage storage pools."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import logging
 | 
					import logging
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Manage storage volumes."""
 | 
					"""Manage storage volumes."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from dataclasses import dataclass
 | 
					from dataclasses import dataclass
 | 
				
			||||||
@@ -8,13 +23,14 @@ import libvirt
 | 
				
			|||||||
from lxml import etree
 | 
					from lxml import etree
 | 
				
			||||||
from lxml.builder import E
 | 
					from lxml.builder import E
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.common import DeviceConfig, EntityConfig
 | 
				
			||||||
from compute.utils import units
 | 
					from compute.utils import units
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@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
 | 
					    Generate XML config for creating a volume in a libvirt
 | 
				
			||||||
    storage pool.
 | 
					    storage pool.
 | 
				
			||||||
@@ -48,9 +64,9 @@ class VolumeConfig:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@dataclass
 | 
					@dataclass
 | 
				
			||||||
class DiskConfig:
 | 
					class DiskConfig(DeviceConfig):
 | 
				
			||||||
    """
 | 
					    """
 | 
				
			||||||
    Disk config builder.
 | 
					    Disk XML config builder.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Generate XML config for attaching or detaching storage volumes
 | 
					    Generate XML config for attaching or detaching storage volumes
 | 
				
			||||||
    to compute instances.
 | 
					    to compute instances.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Configuration loader."""
 | 
					"""Configuration loader."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import tomllib
 | 
					import tomllib
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Random identificators."""
 | 
					"""Random identificators."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# ruff: noqa: S311, C417
 | 
					# ruff: noqa: S311, C417
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""Tools for data units convertion."""
 | 
					"""Tools for data units convertion."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from enum import StrEnum
 | 
					from enum import StrEnum
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
# Add ../../compute to path for autodoc
 | 
					# Add ../.. to path for autodoc Sphinx extension
 | 
				
			||||||
import os
 | 
					import os
 | 
				
			||||||
import sys
 | 
					import sys
 | 
				
			||||||
sys.path.insert(0, os.path.abspath('../../compute'))
 | 
					sys.path.insert(0, os.path.abspath('../..'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Project information
 | 
					# Project information
 | 
				
			||||||
project = 'Compute'
 | 
					project = 'Compute'
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										23
									
								
								packaging/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								packaging/Dockerfile
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										24
									
								
								packaging/Makefile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								packaging/Makefile
									
									
									
									
									
										Normal file
									
								
							@@ -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,}
 | 
				
			||||||
							
								
								
									
										15
									
								
								packaging/build.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								packaging/build.sh
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								packaging/build/compute-0.1.0.dev1.tar.gz
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packaging/build/compute-0.1.0.dev1.tar.gz
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							@@ -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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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,,
 | 
				
			||||||
@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					Wheel-Version: 1.0
 | 
				
			||||||
 | 
					Generator: poetry-core 1.4.0
 | 
				
			||||||
 | 
					Root-Is-Purelib: true
 | 
				
			||||||
 | 
					Tag: py3-none-any
 | 
				
			||||||
@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					[console_scripts]
 | 
				
			||||||
 | 
					compute=compute.cli.control:cli
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""Command line interface for compute module."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.cli import main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main.cli()
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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")
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
 | 
					from .instance import Instance, InstanceConfig
 | 
				
			||||||
 | 
					from .schemas import InstanceSchema
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()]
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .pool import StoragePool
 | 
				
			||||||
 | 
					from .volume import DiskConfig, Volume, VolumeConfig
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()]
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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))
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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])
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							@@ -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())
 | 
				
			||||||
							
								
								
									
										81
									
								
								packaging/build/compute-0.1.0.dev1/PKG-INFO
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								packaging/build/compute-0.1.0.dev1/PKG-INFO
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										65
									
								
								packaging/build/compute-0.1.0.dev1/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								packaging/build/compute-0.1.0.dev1/README.md
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										22
									
								
								packaging/build/compute-0.1.0.dev1/compute/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								packaging/build/compute-0.1.0.dev1/compute/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
							
								
								
									
										21
									
								
								packaging/build/compute-0.1.0.dev1/compute/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								packaging/build/compute-0.1.0.dev1/compute/__main__.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""Command line interface for compute module."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from compute.cli import main
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main.cli()
 | 
				
			||||||
							
								
								
									
										501
									
								
								packaging/build/compute-0.1.0.dev1/compute/cli/control.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										501
									
								
								packaging/build/compute-0.1.0.dev1/compute/cli/control.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
							
								
								
									
										30
									
								
								packaging/build/compute-0.1.0.dev1/compute/common.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								packaging/build/compute-0.1.0.dev1/compute/common.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
							
								
								
									
										80
									
								
								packaging/build/compute-0.1.0.dev1/compute/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								packaging/build/compute-0.1.0.dev1/compute/exceptions.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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")
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .guest_agent import GuestAgent
 | 
				
			||||||
 | 
					from .instance import Instance, InstanceConfig
 | 
				
			||||||
 | 
					from .schemas import InstanceSchema
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
							
								
								
									
										675
									
								
								packaging/build/compute-0.1.0.dev1/compute/instance/instance.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										675
									
								
								packaging/build/compute-0.1.0.dev1/compute/instance/instance.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
							
								
								
									
										165
									
								
								packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								packaging/build/compute-0.1.0.dev1/compute/instance/schemas.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
							
								
								
									
										286
									
								
								packaging/build/compute-0.1.0.dev1/compute/session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										286
									
								
								packaging/build/compute-0.1.0.dev1/compute/session.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()]
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from .pool import StoragePool
 | 
				
			||||||
 | 
					from .volume import DiskConfig, Volume, VolumeConfig
 | 
				
			||||||
							
								
								
									
										124
									
								
								packaging/build/compute-0.1.0.dev1/compute/storage/pool.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								packaging/build/compute-0.1.0.dev1/compute/storage/pool.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()]
 | 
				
			||||||
							
								
								
									
										138
									
								
								packaging/build/compute-0.1.0.dev1/compute/storage/volume.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								packaging/build/compute-0.1.0.dev1/compute/storage/volume.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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()
 | 
				
			||||||
@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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
 | 
				
			||||||
							
								
								
									
										33
									
								
								packaging/build/compute-0.1.0.dev1/compute/utils/ids.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								packaging/build/compute-0.1.0.dev1/compute/utils/ids.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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))
 | 
				
			||||||
							
								
								
									
										54
									
								
								packaging/build/compute-0.1.0.dev1/compute/utils/units.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								packaging/build/compute-0.1.0.dev1/compute/utils/units.py
									
									
									
									
									
										Normal file
									
								
							@@ -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 <http://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					"""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])
 | 
				
			||||||
@@ -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 <ge@nixhacks.net>  Wed, 22 Nov 2023 23:06:43 +0000
 | 
				
			||||||
@@ -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 <ge@nixhacks.net>  Wed, 22 Nov 2023 23:06:43 +0000
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					./README.md
 | 
				
			||||||
							
								
								
									
										5
									
								
								packaging/build/compute-0.1.0.dev1/debian/changelog
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								packaging/build/compute-0.1.0.dev1/debian/changelog
									
									
									
									
									
										Normal file
									
								
							@@ -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 <ge@nixhacks.net>  Wed, 22 Nov 2023 23:06:43 +0000
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					dh_sphinxdoc
 | 
				
			||||||
@@ -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=
 | 
				
			||||||
@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					Package: compute-doc
 | 
				
			||||||
 | 
					Source: compute
 | 
				
			||||||
 | 
					Version: 0.1.0.dev1-1
 | 
				
			||||||
 | 
					Architecture: all
 | 
				
			||||||
 | 
					Maintainer: ge <ge@nixhacks.net>
 | 
				
			||||||
 | 
					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)
 | 
				
			||||||
@@ -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
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							@@ -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 <ge@nixhacks.net>
 | 
				
			||||||
 | 
					License: GPL-3.0+
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Files:
 | 
				
			||||||
 | 
					 debian/*
 | 
				
			||||||
 | 
					Copyright:
 | 
				
			||||||
 | 
					 2023 ge <ge@nixhacks.net>
 | 
				
			||||||
 | 
					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 <https://www.gnu.org/licenses/>.
 | 
				
			||||||
 | 
					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".
 | 
				
			||||||
@@ -0,0 +1,16 @@
 | 
				
			|||||||
 | 
					Compute
 | 
				
			||||||
 | 
					=======
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Compute instances management library.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pyapi/index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Indices and tables
 | 
				
			||||||
 | 
					------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					* :ref:`genindex`
 | 
				
			||||||
 | 
					* :ref:`modindex`
 | 
				
			||||||
 | 
					* :ref:`search`
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					``exceptions``
 | 
				
			||||||
 | 
					==============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.exceptions
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
@@ -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 <https://docs.pydantic.dev/>`_ models are used to validate input data.
 | 
				
			||||||
 | 
					For example :class:`VolumeSchema`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Modules documentation
 | 
				
			||||||
 | 
					---------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 4
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    session
 | 
				
			||||||
 | 
					    instance/index
 | 
				
			||||||
 | 
					    storage/index
 | 
				
			||||||
 | 
					    utils
 | 
				
			||||||
 | 
					    exceptions
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					``guest_agent``
 | 
				
			||||||
 | 
					===============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.instance.guest_agent
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :special-members: __init__
 | 
				
			||||||
@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					``instance``
 | 
				
			||||||
 | 
					============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 1
 | 
				
			||||||
 | 
					    :caption: Contents:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    instance
 | 
				
			||||||
 | 
					    guest_agent
 | 
				
			||||||
 | 
					    schemas
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					``instance``
 | 
				
			||||||
 | 
					============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.instance.instance
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :special-members: __init__
 | 
				
			||||||
@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					``schemas``
 | 
				
			||||||
 | 
					===========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.instance.schemas
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					``session``
 | 
				
			||||||
 | 
					===========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.session
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :special-members: __init__
 | 
				
			||||||
@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					``storage``
 | 
				
			||||||
 | 
					============
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. toctree::
 | 
				
			||||||
 | 
					    :maxdepth: 1
 | 
				
			||||||
 | 
					    :caption: Contents:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pool
 | 
				
			||||||
 | 
					    volume
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					``pool``
 | 
				
			||||||
 | 
					========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.storage.pool
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :special-members: __init__
 | 
				
			||||||
@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					``volume``
 | 
				
			||||||
 | 
					==========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.storage.volume
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					   :special-members: __init__
 | 
				
			||||||
@@ -0,0 +1,14 @@
 | 
				
			|||||||
 | 
					``utils``
 | 
				
			||||||
 | 
					=========
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``utils.units``
 | 
				
			||||||
 | 
					---------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.utils.units
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``utils.ids``
 | 
				
			||||||
 | 
					-------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. automodule:: compute.utils.ids
 | 
				
			||||||
 | 
					   :members:
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					../../../../javascript/sphinxdoc/1.0/_sphinx_javascript_frameworks_compat.js
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -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;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					/* This file intentionally left blank. */
 | 
				
			||||||
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					../../../../javascript/sphinxdoc/1.0/doctools.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,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 286 B  | 
										
											Binary file not shown.
										
									
								
							| 
		 After Width: | Height: | Size: 7.6 KiB  | 
@@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					../../../../javascript/sphinxdoc/1.0/jquery.js
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user