commit 7e033f008900c6c4545a80e332b8135af2e01978 Author: Jesse Gardner Date: Wed Mar 17 08:55:34 2021 -0700 qq2clone V 0.1.0 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7d6e2c2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,118 @@ +GNU GENERAL PUBLIC LICENSE +Version 2, June 1991 + +Copyright (C) 1989, 1991 Free Software Foundation, Inc. +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA + +Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. + +Preamble + +The licenses for most software are designed to take away your freedom to share and change it. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change free software--to make sure the software is free for all its users. This General Public License applies to most of the Free Software Foundation's software and to any other program whose authors commit to using it. (Some other Free Software Foundation software is covered by the GNU Lesser General Public License instead.) You can apply it to your programs, too. + +When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for this service if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs; and that you know you can do these things. + +To protect your rights, we need to make restrictions that forbid anyone to deny you these rights or to ask you to surrender the rights. These restrictions translate to certain responsibilities for you if you distribute copies of the software, or if you modify it. + +For example, if you distribute copies of such a program, whether gratis or for a fee, you must give the recipients all the rights that you have. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. + +We protect your rights with two steps: (1) copyright the software, and (2) offer you this license which gives you legal permission to copy, distribute and/or modify the software. + +Also, for each author's protection and ours, we want to make certain that everyone understands that there is no warranty for this free software. If the software is modified by someone else and passed on, we want its recipients to know that what they have is not the original, so that any problems introduced by others will not reflect on the original authors' reputations. + +Finally, any free program is threatened constantly by software patents. We wish to avoid the danger that redistributors of a free program will individually obtain patent licenses, in effect making the program proprietary. To prevent this, we have made it clear that any patent must be licensed for everyone's free use or not licensed at all. + +The precise terms and conditions for copying, distribution and modification follow. + +TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + +0. This License applies to any program or other work which contains a notice placed by the copyright holder saying it may be distributed under the terms of this General Public License. The "Program", below, refers to any such program or work, and a "work based on the Program" means either the Program or any derivative work under copyright law: that is to say, a work containing the Program or a portion of it, either verbatim or with modifications and/or translated into another language. (Hereinafter, translation is included without limitation in the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not covered by this License; they are outside its scope. The act of running the Program is not restricted, and the output from the Program is covered only if its contents constitute a work based on the Program (independent of having been made by running the Program). Whether that is true depends on what the Program does. + +1. You may copy and distribute verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice and disclaimer of warranty; keep intact all the notices that refer to this License and to the absence of any warranty; and give any other recipients of the Program a copy of this License along with the Program. + +You may charge a fee for the physical act of transferring a copy, and you may at your option offer warranty protection in exchange for a fee. + +2. You may modify your copy or copies of the Program or any portion of it, thus forming a work based on the Program, and copy and distribute such modifications or work under the terms of Section 1 above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in whole or in part contains or is derived from the Program or any part thereof, to be licensed as a whole at no charge to all third parties under the terms of this License. + + c) If the modified program normally reads commands interactively when run, you must cause it, when started running for such interactive use in the most ordinary way, to print or display an announcement including an appropriate copyright notice and a notice that there is no warranty (or else, saying that you provide a warranty) and that users may redistribute the program under these conditions, and telling the user how to view a copy of this License. (Exception: if the Program itself is interactive but does not normally print such an announcement, your work based on the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If identifiable sections of that work are not derived from the Program, and can be reasonably considered independent and separate works in themselves, then this License, and its terms, do not apply to those sections when you distribute them as separate works. But when you distribute the same sections as part of a whole which is a work based on the Program, the distribution of the whole must be on the terms of this License, whose permissions for other licensees extend to the entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest your rights to work written entirely by you; rather, the intent is to exercise the right to control the distribution of derivative or collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program with the Program (or with a work based on the Program) on a volume of a storage or distribution medium does not bring the other work under the scope of this License. + +3. You may copy and distribute the Program (or a work based on it, under Section 2) in object code or executable form under the terms of Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable source code, which must be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three years, to give any third party, for a charge no more than your cost of physically performing source distribution, a complete machine-readable copy of the corresponding source code, to be distributed under the terms of Sections 1 and 2 above on a medium customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer to distribute corresponding source code. (This alternative is allowed only for noncommercial distribution and only if you received the program in object code or executable form with such an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for making modifications to it. For an executable work, complete source code means all the source code for all modules it contains, plus any associated interface definition files, plus the scripts used to control compilation and installation of the executable. However, as a special exception, the source code distributed need not include anything that is normally distributed (in either source or binary form) with the major components (compiler, kernel, and so on) of the operating system on which the executable runs, unless that component itself accompanies the executable. + +If distribution of executable or object code is made by offering access to copy from a designated place, then offering equivalent access to copy the source code from the same place counts as distribution of the source code, even though third parties are not compelled to copy the source along with the object code. + +4. You may not copy, modify, sublicense, or distribute the Program except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense or distribute the Program is void, and will automatically terminate your rights under this License. However, parties who have received copies, or rights, from you under this License will not have their licenses terminated so long as such parties remain in full compliance. + +5. You are not required to accept this License, since you have not signed it. However, nothing else grants you permission to modify or distribute the Program or its derivative works. These actions are prohibited by law if you do not accept this License. Therefore, by modifying or distributing the Program (or any work based on the Program), you indicate your acceptance of this License to do so, and all its terms and conditions for copying, distributing or modifying the Program or works based on it. + +6. Each time you redistribute the Program (or any work based on the Program), the recipient automatically receives a license from the original licensor to copy, distribute or modify the Program subject to these terms and conditions. You may not impose any further restrictions on the recipients' exercise of the rights granted herein. You are not responsible for enforcing compliance by third parties to this License. + +7. If, as a consequence of a court judgment or allegation of patent infringement or for any other reason (not limited to patent issues), conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot distribute so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not distribute the Program at all. For example, if a patent license would not permit royalty-free redistribution of the Program by all those who receive copies directly or indirectly through you, then the only way you could satisfy both it and this License would be to refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under any particular circumstance, the balance of the section is intended to apply and the section as a whole is intended to apply in other circumstances. + +It is not the purpose of this section to induce you to infringe any patents or other property right claims or to contest validity of any such claims; this section has the sole purpose of protecting the integrity of the free software distribution system, which is implemented by public license practices. Many people have made generous contributions to the wide range of software distributed through that system in reliance on consistent application of that system; it is up to the author/donor to decide if he or she is willing to distribute software through any other system and a licensee cannot impose that choice. + +This section is intended to make thoroughly clear what is believed to be a consequence of the rest of this License. + +8. If the distribution and/or use of the Program is restricted in certain countries either by patents or by copyrighted interfaces, the original copyright holder who places the Program under this License may add an explicit geographical distribution limitation excluding those countries, so that distribution is permitted only in or among countries not thus excluded. In such case, this License incorporates the limitation as if written in the body of this License. + +9. The Free Software Foundation may publish revised and/or new versions of the General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. + +Each version is given a distinguishing version number. If the Program specifies a version number of this License which applies to it and "any later version", you have the option of following the terms and conditions either of that version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of this License, you may choose any version ever published by the Free Software Foundation. + +10. If you wish to incorporate parts of the Program into other free programs whose distribution conditions are different, write to the author to ask for permission. For software which is copyrighted by the Free Software Foundation, write to the Free Software Foundation; we sometimes make exceptions for this. Our decision will be guided by the two goals of preserving the free status of all derivatives of our free software and of promoting the sharing and reuse of software generally. + +NO WARRANTY + +11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +END OF TERMS AND CONDITIONS + +How to Apply These Terms to Your New Programs + +If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively convey the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. + + one line to give the program's name and an idea of what it does. Copyright (C) yyyy 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 2 of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, the commands you use may be called something other than `show w' and `show c'; they could even be mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your school, if any, to sign a "copyright disclaimer" for the program, if necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program `Gnomovision' (which makes passes at compilers) written by James Hacker. + +signature of Ty Coon, 1 April 1989 Ty Coon, President of Vice + diff --git a/man.pandoc b/man.pandoc new file mode 100644 index 0000000..a6b9cc4 --- /dev/null +++ b/man.pandoc @@ -0,0 +1,527 @@ +% QQ2CLONE(1) qq2clone 0.1 +% Jesse Gardner +% February 2021 + +# NAME +qq2clone - Create and manage QEMU/KVM VMs using template machines and qcow2 + images with backing files + +# SYNOPSIS +**qq2clone** [*OPTION*]... *COMMAND* [*ARG*]... + +# DESCRIPTION +**qq2clone** is a tool working on top of virsh that makes creating clones +of template QEMU/KVM machines simple. By using the copy on write feature +for which qcow2 is named, clones of an existing virtual machine can be made +without inadvertently altering the original image (with caveats - read the +**LIMITATIONS** section if you aren't already familiar with how copy on +write works). **qq2clone** makes creating and managing these clones simple +and efficient. + +**qq2clone** supports creating numerous clones of a template and +performing batch operations on them - including the execution of arbitrary +commands with exec. This simplifies workflows involving large numbers of +virtual machines, or the frequent creation/destruction of virtual machines. + +In addition to virsh, basic linux utilities and QEMU/KVM, qq2clone +requires: + + Bash 4.0+ + qemu-img + libvirt tools: + virt-clone + virt-xml + virt-xml-validate + xmllint (from libxml2) + +If you want to easily establish graphical connections to your virtual +machines, you should have virt-viewer and/or spicy installed and configure +your templates to use Spice graphics. This is not strictly necessary, and +with the use of **qq2clone** **exec** and a small script of your own +you can automate connecting to Spice/VNC clients of your choice without +too much hassle + +# OPTIONS + +Not every option has an effect in the context of every command. Specifying +an option that has no effect in the context of the command being invoked +will not produce an error, it simply will not do anything + +Options are parsed left to right, and right-hand options override +left-hand options. The only exception is for \-Q/\-\-quieter, which *must* +be the first option listed to work properly. + +\-c, \-\-connection [*URI*] +: Specify a non-default connection URI: sets the value of +LIBVIRT_DEFAULT_URI + +\-f, \-\-no\-spice +: Do not attempt to connect to a virtual machine's Spice graphics. +Overrides USE_SPICE setting in configuration + +\-g, \-\-use\-spice +: Attempt to connect to a virtual machine's spice graphics. Overrides +SPICE setting in configuration + +\-h, \-\-help +: Print basic help information and exit + +\-n, \-\-no-run +: After making a clone of a template, do not run it. Overrides NORUN +setting in configuration + +\-q, \-\-quiet +: Suppress most non-error output. Overrides QUIET setting in +configuration. Also suppresses various prompts for user choices, either +exiting with an error or making a safe default choice depending on the +command. Recommended only once familiar with the behavior of **qq2clone** + +\-Q, \-\-quieter +: This option is (currently) required to appear immediately following the +invocation of **qq2clone**. Suppresses all output, error message or +otherwise, except when running interactive commands or +commands that require output to be useful. The commands for which output +is not entirely supressed are: config list, config info, list, +list-templates, exec, edit, modify-template edit, and check. Other +commands will receive only an exit code as output. This option is intended +for calling qq2clone from a script. + +\-r, \-\-run +: Run a clone when creating it. Overrides NORUN setting in configuration + +\-s, \-\-storage [*LOCATION*] +: When creating a clone, place new disk image file(s) at location +specified by [*LOCATION*]. [*LOCATION*] may be one of an absolute +filepath, or the name of a libvirt directory type storage pool. Also +defines where state files will be saved when using **save** command. +Overrides STORAGE option in configuration + +\-S, \-\-spicy +: Use spicy rather than virt-viewer when connecting to the spice graphics +of a clone. Overrides SPICY setting in configuration + +\-t, \-\-template [*NAME*] +: Use template of given name as context when executing a clone command +(see TYPES OF COMMAND section above). Overrides TEMPLATE option in +configuration + +\-v, \-\-verbose +: Enable all output. Overrides QUIET setting in configuration + +\-V, \-\-virt\-viewer +: Use virt-viewer rather than spicy when connecting to the spice graphics +of a clone. Overrides SPICY setting in configuration + +# TYPES OF COMMAND + +There are two main classes of commands: commands that operate directly on +templates, and commands that create or operate on clones of templates. In +order to make it less likely that the user may unintentionally invoke a +command of one class when they intended to invoke one of the other, they +use a different syntax. Commands that operate on templates use the syntax: + + +**qq2clone** **command** [*template-name*] [*ARG*] ... + +while commands that operate on clones use the syntax: + +**qq2clone** \-\-template [*template-name*] **command** [*ARG*] ... + +Notice that commands operating on clones work within the context of a +template defined by the option \-\-template/\-t. Conversely, commands +operating on templates specify the template as an argument to the +command. There can also be a default template defined by the +TEMPLATE option in the configuration file, allowing the \-\-template +option to be omitted for commands that operate on clones. Commands +operating on templates do not respect this default - the template must +always be explicitly defined, further reducing the likelihood of +accidentally modifying or deleting a template. + +# TEMPLATE COMMMANDS + +**copy-template** [*CURRENT-NAME*] [*NEW-NAME*] +: Copy the XML of template *CURRENT-NAME* to a new template with +*NEW-NAME*. The new template will not receive a copy of the old template's +storage devices - it will point to the same locations + +**delete-template** [*NAME*] +: Delete the template *NAME*. This operation will succeed only if there +are currently no clones of the template + +**import-template** [*LIBVIRT-DOMAIN*] [*NAME*], **import-template** +[*XML-LOCATION*] [*NAME*] +: Import a new template from either an existing libvirt domain, or a fully +qualified filepath to a libvirt domain XML file on disk. If argument +*NAME* is ommited, qq2clone will assume you want to use the machine's name +as described in the XML file as the template name + +**list-templates** +: List the names of all existing templates + +**modify-template** [*NAME*] **sub-command** [*ARG*] ... +: Templates can be modified in various ways by invoking +**modify-template**. Each subcommand is described below + +**modify-template** [*NAME*] **commit-image** +: After an image has been created and modified as desired using +**modify-template** [*NAME*] **prepare-image**, **commit-image** is used +to alter a template's underlying storage device by commiting any changes +made using prepare-image. See the commit command described in **man** +**qemu-img** for more information on how this works + +**modify-template** [*NAME*] **destroy-image** +: Invoke virsh destroy on a running image created/run through +**modify-template** [*NAME*] **prepare-image**. This is generally not +wise, as it is equivalent to unplugging a physical machine and could cause +corruption to the image that will later be commited as a permanent change +to the template's image + +**modify-template** [*NAME*] **discard-image** +: Delete an image produced by **modify-template** [*NAME*] +**prepare-image** without commiting any changes + +**modify-template** [*NAME*] **edit** +: Edit the XML document defining a template + +**modify-template** [*NAME*] **rename** [*NEW-NAME*] +: Change the name of a template, and all of its clones + +**modify-template** [*NAME*] **prepare-image** +: Create and/or run a clone that acts as a staging area for changes to +the `template's` actual image. For instance, you could update the +`template's` software by running **modify-template** [*NAME*] +**prepare-image**, updating the clone produced by this command, +shutting it down, and then running **modify-template** [*NAME*] +**commit-image**. This serves a twofold purpose - to prevent incidental +damage to an underlying image by providing a safe buffer to work in, and +to allow modifications to be safely prepared for an underlying image even +while that image has existing clones. + +# CLONE COMMANDS + +A description of the argument *SET* is described in the **SETS** section +below + +**clone** [*NUMBER*] +: Invoke without any argument to produce a single clone. Supply a number +as an argument to specify the number of clones to create + +**connect** [*SET*] +: Start any machine in *SET* that `isn't` already running. If any machine +in *SET* has spice graphics and spicy or virt-viewer is installed, use one +or the other (chosen by command-line option or configuration) to connect +to the graphical console + +**destroy** [*SET*] +: Invoke virsh destroy on any running machine in *SET* (in other words, if +the domain is running forcibly turn it off) + +**edit** [*NUMBER*] +: Edit the XML file of the clone with given number + +**exec** [*SET*] [*command-string*] +: For every machine in *SET*, sequentially, execute the contents of the +command string in an environment where the following variables are defined +per clone: `"$uuid"`, `"$name"`, `"$disks"` (a newline delimited string +containing the machine's qcow2 disk device filepaths). This is done using +bash's eval command, so be sure to put any instances of these variables in +single quotes (double quotes inside the single quotes is best practice) or +they will not be set properly. If any instance of exec has a non-zero +return value, execution stops. + +**list** [*ARG*] +: Without arguments, list all clones of the current template and their +state. With argument "all", provide list including all clones of every +template. With argument "xml", produce an XML document with information +about every template, their clones, and their state. The XML option +is not complete - its format is at this point defined only implicitly, by +the output of this command. + +**resume** [*SET*] +: Resume any suspended machines in *SET* + +**rm** [*SET*] +: Destroy every domain in *SET* (if running), undefine them and delete +their storage volumes + +**rm-wipe** [*SET*] +: Destroy every domain in *SET* (if running), undefine them and wipe their +storage volumes using virsh + +**rm-shred** [*SET*] +: Destroy every domain in *SET* (if running), undefine them and shred +their storage volumes + +**save** [*SET*] +: Save execution state of every running domain in *SET* to file + +**save-rm** [*SET*] +: Delete the state file associated with every machine in *SET* + +**start** [*SET*] +: Start every machine in *SET* that is currently not running. For saved +domains, their state will be restored + +**suspend** [*SET*] +: Suspend execution of every machine in *SET* + +# OTHER COMMANDS + +**check** [*TEMPLATE-NAME*] +: As described in the limitations section, there are ways that qq2clone +can lose track of a clone. If this happens, it will remain in qq2clone's +database, its ID number will remain reserved, and its image files may not +be deleted and take up space doing nothing. the **check** command tries to +find and fix this and other problems. The *TEMPLATE-NAME* argument is +optional, and restricts the check to that template and its clones. +Otherwise, all templates are checked + +**config** list, **config** info [*OPTION*], **config** edit [*OPTION*] +: List all configuration options and their current value, get info about a +particular option, or edit one + +# SETS + +*SET* is listed as an argument to many commands. *SET* simply describes a +set of virtual machines - clones of a given template. *SET* is a comma +delimited list with no whitespace. *SET* can be an individual machine or +several individual machines designated by number: + + 1 (Machine 1) + 3,7 (Machines 3 and 7) + +Machine numbers can be shown with **qq2clone** **list**. Ranges and +omitted values are supported as well: + + 1,2-5,^3 (Machines 1 and 2-5 excluding 3) + 1-10,^3-7 (Machines 1-10 excluding 3-7) + +Lastly, groups of machines can be addressed by their state: + + all (All machines) + all,^running (All machines that aren't running) + ^running,1-10 (Machines 1-10 except those that are running) + +The possible states of a virtual machine are based on the states listed in +**man virsh**, with some modifications. States in qq2clone are: + + all + crashed + idle + in-shutdown + off + paused + pmsuspended + running + saved + +Specifying machines that do not exist will not cause an error: i.e., +1-10 is a valid set even if only machines 3-7 exist. A set will only cause +an error if it is malformed, includes zero existing machines, contains no +machines that the command being invoked may act upon, or includes numbers +less than 1. + +# CONFIG + +There is no need to refer to the manual to understand configuration +options. Use "**qq2clone** config list" to see all options and their +current values, and "**qq2clone** config info [*OPTION*]" to get +information about a particular option. However, here is the same +information provided by **qq2clone** info for each option + +TEMPLATE + +> This template will be used for commands like clone, rm, destroy when +option \-\-template/\-t is not specified +> +> Default value: Default value: `'0'` + +TEMPLATE_DIR + +> This is the where template XML files will be kept +> +> Default value: `'${HOME}/storage-qq2clone/templates'` + +QUIET + +> If set to 1, most non-error output will be suppressed +> +> Default value: `'0'` + +USE_SPICE + +> If set to 1, attempt to connect +> to the spice graphics of a virtual machine by default when cloning it, +if it is configured to use spice graphics. qq2clone can do this using the +programs spicy and virt-viewer. If either is installed on your system +during the first run, the default value is `'1'` (enabled). Otherwise, the +default value is `'0'` + +S_TIMEOUT + +> Wait this many seconds before timing out when trying to connect to a +virtual `machine's` spice graphics. +> +> Default value: `'10'` + +STORAGE + +> The default location to store clone images when creating them. Changing +this location is fine, but it is a good idea to ensure that whatever +location you do choose is only used by qq2clone +> +> Default value: `'${HOME}/storage-qq2clone/qq2clone-pool'` + +# EXAMPLES + +**qq2clone** \-\-template Debian \-\-run \-\-virt-viewer clone +: Make a clone of Debian, run it, and connect to its spice graphics using +virt\-viewer. All of these options could have instead been defined in the +configuration, so that the entire command would be: **qq2clone** clone + +**qq2clone** \-\-template Debian exec 3 'virsh console "$uuid"' +: Use virsh to connect to the serial console of template Debian's clone +with number 3 (as shown in **qq2clone** list) + +**qq2clone** **modify-template** Debian *prepare-image* +: Create a clone of Debian that can be used as a staging area for +permanent changes to the backing template storage device + +**qq2clone** **modify-template** Debian **commit-image** +: Commit changes to the image Debian staged with the previous command + +**qq2clone** **copy-template** Debian Debian_2 +: Copy the XML of template Debian, creating a new template with the same +backing storage device that you can edit as you please + +# LIMITATIONS + +The largest limitation of **qq2clone** is that it cannot protect your +template images from the actions of other software. If nothing else +touches a template's storage volumes, qq2clone can safely handle them +(barring unknown bugs or bad luck during a commit-image). However, +if something else alters the image upon which a template is based, its +existing clones may be corrupted and future clones may behave differently +than expected. It is the user's responsibility to understand this aspect +of copy on write and carefully manage template images. Future updates to +qq2clone may add features that give some additional protections, but this +risk is inherent to copy on write. + +Libvirt has permissions errors when a storage pool is in a "hidden" +directory with a name beginning with "." and qcow2 files with backing files +are involved. This may be due to apparmor, or it may be an issue with +libvirt. It is unknown how widespread this issue is, but it is the reason +that the default directory storage-qq2clone does not start with '.' + +If the UUID of a clone is changed, qq2clone will no longer be able to +track it and will not be able to perform commands on it anymore. +If virsh undefine is run on a clone, qq2clone will not be able to see +it once it is turned off. This limitation will be eliminated or reduced in +the future, when qq2clone moves away from relying on virsh and implements +direct usage of the libvirt API. It could be addressed now by using +transient domains, but that would require qq2clone to do more things +manually instead of just invoking virsh. Since the plan is to +transition to a different approach later, that would be wasted effort. For +now, if you find yourself in this position just use **qq2clone** check. + +qq2clone can only produce clones by making qcow2 image files. The backing +file need not be qcow2, but the images produced by qq2clone always will +be. This is unlikely to ever change - levaraging the features of qcow2 is +the entire purpose of qq2clone. If it does change, qq2clone will need a +new name. + +qq2clone does not support creating images in pool types other than +directories, and attempting to use a machine as a template when it has +storage volumes in a non-directory pool is likely to fail or have +unexpected results. Support for some other pool types may be added in the +future. + +qq2clone currently cannot copy storage volumes when importing a template +(it just references the originals), or when copying a template. This will +change in the future, and qq2clone will also be able to handle more complex +relationships between templates, clones and their images + +# FILES + +~/.config/qq2clone + +: This document simply contains a string defining the location at which +qq2clone will store files, including the database containing the rest of +it configuration options. Currently, qq2clone cannot run without ${HOME} +being defined unless a few lines are altered to refer to a new location + +~/storage-qq2clone + +: Directory where qq2clone stores all files and binary executables. Can be +changed by modifying ~/.config/qq2clone. This directory is not +named "qq2clone" because it can then slightly interfere with bash +completion when in the home directory, and it does not start with a '.' +for the reasons described in the **LIMITATIONS** section above + +~/storage-qq2clone/qq2clone.db +: sqlite3 database containing the configuration information for qq2clone, +as well as data about templates and clones + +~/storage-qq2clone/qq2clone-pool + +: Storage pool used for clone images and saved state files, if the +\-\-storage option is not used when creating or saving a clone and the +option STORAGE is not changed in the configuration file + +~/qq2clone/templates + +: Directory in which template XML files are stored. These can be edited +manually, but it is more advisable to use **qq2clone** **modify-template** +[*template-name*] edit + +# BUGS + +As described in the options section, the implementation of the +\-\-quieter/\-Q option needs some work. Its current behavior is the +easiest functional approach without complicating the options parser, but it +will eventually be modified and become better behaved. In addition to the +previously described problem, very early error messages will not be +suppressed. Most likely, the solution is to implement a better options +parser and make it the first thing to run when executing qq2clone. However, +the impact of this bug is minimal and other improvements are likely to +come before this bug fix. + +If you find any worse bugs, and I'm sure I missed some, please let me know +and I will fix them as time allows. + +# EXIT VALUES + +**10** +: No permission to access file or file doesn't exist + +**11** +: Required software dependencies are not met (see description for a list), +or are cannot be found in PATH + +**12** +: Invalid command line argument specified, or command specifies an invalid +action + +**13** +: Problem with a template - i.e., specified template does not exist, or +import-template failed because template of specified name already exists + +**14** +: Invocation of an external command failed + +**15** +: Problem with a libvirt XML file + +**16** +: Attempted action with a libvirt tool resulted in failure + +**17** +: Could not establish graphical spice connection to machine before timeout +expired + +**18** +: A file is of the wrong type or does not exist + +**19** +: Unexpected error - a bug in qq2clone, or a highly unexpected failure of +some command diff --git a/qq2clone b/qq2clone new file mode 100755 index 0000000..caad614 --- /dev/null +++ b/qq2clone @@ -0,0 +1,3218 @@ +#!/bin/bash +#shellcheck disable=1090 disable=2012 + + #-----------------# + #@@@@@@@@@@@@@@@@@# + #---ERROR CODES---# + #@@@@@@@@@@@@@@@@@# + #-----------------# + +E_permission=10 # No permission for access or file does not exist +E_depends=11 # Lacking required software +E_args=12 # Bad command line arguments or arguments specify illegal + # action +E_template=13 # Problem with a template +E_extcom=14 # An external command failed +E_xml=15 # Malformed XML or issue processing XML +E_libvirt=16 # Invokation of a libvirt tool was unsuccesful +E_timeout=17 # Timeout was exceeded before spice connection to clone + # was established +E_file=18 # Expected file does not exist or is of wrong type/format +E_unexpected=19 # Probably a bug in qq2clone + + #---------------------------------------------------# + #@@@#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---NAMED PIPE FOR PASSING DATA BETWEEN FUNCTIONS---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---------------------------------------------------# + +#=========================================================================# +check_pipe () +# DESCRIPTION: See if pipe is open +# INPUT: None +# OUTPUT: Return value +# PARAMETERS: None +#=========================================================================# +{ +local type +type="$(file -bL /proc/self/fd/3)" +if [[ ! "$type" =~ fifo ]]; then + return 1 +fi +return 0 +} +#=========================================================================# +open_pipe () +# DESCRIPTION: Open a named pipe in read/write mode on fd3 +# INPUT: None +# OUTPUT: None +# PARAMETERS: None +#=========================================================================# +{ +check_pipe && return +local TEMPDIR +TEMPDIR=$(mktemp -d) || temp_error +#shellcheck disable=2064 +trap "exec 3>&-; exec 3<&-;rm -rf $TEMPDIR" EXIT +fifo_path="${TEMPDIR}/qq2clone_fifo" +mkfifo "$fifo_path" || + { echo "Cannot make fifo" >&2; exit "$E_extcom" ;} +exec 3<>"$fifo_path" +return 0 +} +#=========================================================================# +read_pipe () +# DESCRIPTION: Flushes the contents of the named pipe to stdout, +# nonblocking +# INPUT: None +# OUTPUT: Contents of named pipe on fd3 +# PARAMETERS: $1: (Optional) If 1, read data out but also write it back +# into the pipe +#=========================================================================# +{ +# Note: This implementation allows for things like this to work: +# tr "a" "b" < <(read_pipe) | write_pipe 1 +echo "EOF" >&3 +local line match +while IFS= read -r line <&3; do + # write_pipe puts a + at the start of every line for read_pipe + if [[ "$line" =~ ^\+(.*)$ ]]; then + match="${BASH_REMATCH[1]}" + echo "$match" + (($#)) && (($1 == 1)) && write_pipe 0 "$match" + else + break + fi +done +return 0 +} +#=========================================================================# +write_pipe () +# DESCRIPTION: Write information to the named pipe, nonblocking unless it +# is told to look for input on stdin and nothing is sent there. Works in +# conjunction with read_pipe to make read_pipe non-blocking +# INPUT: Tell write_pipe whether information is coming on stdin or from +# a parameter, then pass information +# OUTPUT: None +# PARAMETERS: $1: '0' if passing another parameter(s), '1' if writing to +# stdin instead. +# $2 and on: If $1 is 0, this is the information write_pipe will +# write as "$*" +#=========================================================================# +{ +# We put a + at the beginning of every line to let read_pipe work in a +# non-blocking manner +if (($1)); then + local line + while IFS= read -r line; do + echo "+$line" >&3 + done +else + shift + echo "+$*" >&3 +fi +return 0 +} + + #-------------------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---GET/ALTER CONFIGURATION, CHECK SYSTEM---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-------------------------------------------# + +#=========================================================================# +check_config () +#= DESCRIPTION: Given a name or name/value pair, check if it is a +#= valid configuration option +#= INPUT: Either a name or name value pair +#= OUTPUT: Return 1 if the name is not a valid config option or if the +#= given value does is not valid for the given name. Return 0 else +#= PARAMETERS: $1: Name of config option, $2: (optional) value of option +#=========================================================================# +{ +(($#)) || unexpected_error check_config +declare -A def_opt +def_opt[TEMPLATE]=".*" +def_opt[TEMPLATE_DIR]="^/.*" +def_opt[QUIET]="^[01]$" +def_opt[USE_SPICE]="^[01]$" +def_opt[SPICY]="^[01]$" +def_opt[S_TIMEOUT]="^[0-9]+$" +def_opt[NORUN]="^[01]$" +def_opt[STORAGE]="^/.*" + +(( $# == 1 )) && + { [[ " ${!def_opt[*]} " =~ [[:space:]]${1}[[:space:]] ]]; + return $?; } + +local patt="${def_opt["${1}"]}" +[[ -n "$patt" ]] || return 1 +[[ "$2" =~ $patt ]] || return 1 + +return 0 +} +#=========================================================================# +check_depends () +# DESCRIPTION: Check that required software is present during initial auto- +# configuration +# INPUT: None +# OUTPUT: Return 0 on success or exits with descriptive message on failure +# PARAMETERS: None +#=========================================================================# +{ +local elem missing=0 + +# These we need to check for +local -a depends=( virsh virt-clone virt-xml virt-xml-validate qemu-img \ + xmllint ) + +# These are from gnu-coreutils, util-linux, or are simply extremely common +# and almost certainly present. However, checking for any external +#command invoked is harmless and may even help 1/10000 times +depends=( "${depends[@]}" basename chmod date dirname file grep less ls \ + md5sum mkfifo mkdir mktemp mv rm sed sort touch uniq uuidgen uuidparse \ + vi dirname ) + +(( BASH_VERSINFO[0] >= 4 )) || + { echo "This script must be run with Bash version 4.0+" + exit "$E_depends"; } >&2 + +for elem in "${depends[@]}"; do + if ! { command -v "$elem"; } &>/dev/null; then + ((missing++)) + echo "Missing required software: $elem" >&2 + fi +done + +((missing)) && + { echo "This script won't run until you install the listed software" >&2; + exit "$E_depends"; } + + return 0 +} +#=========================================================================# +disp_conf_names () +# DESCRIPTION: Display the name and value of all configuration options +# INPUT: None +# OUTPUT: Echoes config name="value" pairs +# PARAMETERS: None +#=========================================================================# +{ +local name value +while read -r name; do + read -r value + echo "'$name'='$value'" +done < <(sqlite3 "select name,value from CONFIG") +return 0 +} +#=========================================================================# +disp_conf_desc () +# DESCRIPTION: Display the description of a config option to the user +# INPUT: The name of the option +# OUTPUT: Echoes relevant lines of information +# PARAMETERS: $1: The config option name +#=========================================================================# +{ +if [[ "$1" == "TEMPLATE_DIR" ]]; then + echo "This is the where template XML files will be kept" + echo + echo "Default value: '${QQ2_DIR}/templates'" +elif [[ "$1" == "TEMPLATE" ]]; then + echo "This template will be used for commands like clone, rm, destroy" + echo "when option --template/-t is not specified" + echo + echo "Default value: '' (empty, disabled)" +elif [[ "$1" == "QUIET" ]]; then + echo "If set to 1, most non-error output will be suppressed" + echo + echo "Default value: '0'" +elif [[ "$1" == "SPICY" ]]; then + echo "If set to 1, use spicy as the default spice client instead of" + echo "virt-viewer. If virt-viewer is installed during the initial setup," + echo "the default value is '1' (enabled). Otherwise, the default value" + echo "is '0'" +elif [[ "$1" == "USE_SPICE" ]]; then + echo "If set to 1, attempt to connect to the spice graphics of a virtual" + echo "machine by default when cloning it, if it is configured to use" + echo "spice graphics. qq2clone can do this using the programs spicy and" + echo "virt-viewer. If either is installed on your system during the" + echo "first run, the default value is '1' (enabled). Otherwise, the" + echo "default value is '0'" +elif [[ "$1" == "S_TIMEOUT" ]]; then + echo "Wait this many seconds before timing out when trying to connect to" + echo "a virtual machine's spice graphics." + echo + echo "Default value: '10'" +elif [[ "$1" == "NORUN" ]]; then + echo "If set to 1, do not automatically run a machine after cloning it." + echo + echo "Default value: '0'" +elif [[ "$1" == "STORAGE" ]]; then + echo "The default location to store clone images when creating them." + echo "Changing this location is fine, but it is a good idea to ensure" + echo "that whatever location you do choose is only used by qq2clone" + echo + echo "Default value: '${QQ2_DIR}/qq2clone-pool'" +else + echo "No such configuration option '$1'" + return 1 +fi +return 0 +} +first_run_setup () +# DESCRIPTION: Generate a new database with default config values, +# create subdirectories of QQ2_DIR +# INPUT: None +# OUTPUT: None +# PARAMETERS: None +#=========================================================================# +{ +local QQ2_DIR="${HOME}/storage-qq2clone" +echo "$QQ2_DIR" > "${HOME}/.config/qq2clone" + +# Default locations of key directories +local TEMPLATE_DIR="${QQ2_DIR}/templates" +local POOL_DIR="${QQ2_DIR}/qq2clone-pool" + +check_rw "$QQ2_DIR" +check_depends + +[[ -e "${QQ2_DIR}/qq2clone.db" ]] && return 0 + +make_dir "$TEMPLATE_DIR" +make_dir "$POOL_DIR" +check_rw -r "$TEMPLATE_DIR" "$POOL_DIR" + +chmod +rx "${QQ2_DIR}/sqlite3" &>/dev/null +{ [[ -e "${QQ2_DIR}/sqlite3" ]] && [[ -x "${QQ2_DIR}/sqlite3" ]] && + [[ -r "${QQ2_DIR}/sqlite3" ]] ; } || + { echo "sqlite3 binary must be present at" + echo "${QQ2_DIR}/sqlite3" + echo "and must be readable/executable" + exit "$E_file"; } >&2 + +local use_spice spicy +if command -v virt-viewer &>/dev/null; then + use_spice=1 + spicy=0 +elif command -v spicy &>/dev/null; then + use_spice=1 + spicy=1 +else + use_spice=0 + spicy=0 +fi + +sqlite3 </dev/null; + unexpected_error get_config; } + +declare -gA OPT +declare -a opts +local elem +opts=(TEMPLATE_DIR TEMPLATE QUIET USE_SPICE SPICY S_TIMEOUT NORUN \ + STORAGE) +for elem in "${opts[@]}"; do + OPT["$elem"]="$(sqlite3 \ + "select value from CONFIG where name=\"$elem\"")" +done +} +#=========================================================================# +write_config () +# DESCRIPTION: Write an option name and value pair to config table. +# Checks that the option name and value are good. +# INPUT: Name and value of configuration option +# OUTPUT: Return 0 on success, 1 on bad option, 2 on bad value +# PARAMETERS: $1: Name of variable, $2: Value of variable +#=========================================================================# +{ +check_config "$1" || return 1 +check_config "$1" "$2" || return 2 + +sqlite3 "update CONFIG set value='$2' where name='$1';" + +return 0 +} + + #-----------------------# + #@@@@@@@@@@@@@@@@@@@@@@@# + #---USAGE INFORMATION---# + #@@@@@@@@@@@@@@@@@@@@@@@# + #-----------------------# +#=========================================================================# +usage () +# DESCRIPTION: Output basic usage information +# INPUT: None +# OUTPUT: Echo information to stdout to be read by the user +# PARAMETERS: None +#=========================================================================# +{ +(( OPT[QUIET] )) && return 0 +echo "qq2clone: quick qcow2 clone" +echo +echo "Description:" +echo " Create and manage QEMU/KVM VMs using template machines and qcow2" +echo " images with backing files" +echo +echo "Usage:" +echo " qq2clone [OPTION]... [COMMAND] [ARGUMENT]..." +echo +echo ' options: --connection/-c (URI) --no-spice/-f --help/-h' +echo ' --no-run/-n --quiet/-q --quieter/-Q --run/-r --spicy/-S' +echo ' --storage/-s (filepath/pool-name) --template/-t (name)' +echo ' --use-spice/-g --verbose/-v --virt-viewer/-V' +echo +echo " commands: check clone config connect copy-template delete-template" +echo " destroy edit exec import-template list list-templates" +echo " modify-template restore resume rm rm-wipe rm-shred save" +echo " save-rm start suspend" +echo +echo " For more information, see: man qq2clone" +return 0 +} + + #-----------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---INPUT/OUTPUT HELPER FUNCTIONS---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-----------------------------------# + + +#=========================================================================# +prompt_num () +# DESCRIPTION: Prompt user for a number between $1 and $2 +# INPUT: Inclusive endpoints of accepted interval, where the right hand +# endpoint is less than 10 +# OUTPUT: Echo choice when proper input is received +# PARAMETERS: $1: LH of interval, $2: RH of interval +#=========================================================================# +{ +{ (( $1 > -1 )) && (( $1 < $2 )) && (( $2 < 10 )) ; } || \ + unexpected_error prompt_num +local n +read -rsn 1 n +{ [[ "$n" =~ ^[0-9]$ ]] && (($1 <= n)) && ((n <= $2)); } || + { echo "Enter a number from $1 to $2" >&2; + prompt_num "$1" "$2"; + return 0; } +echo "$n" +return 0 +} +#=========================================================================# +prompt_yes_abort () +# DESCRIPTION: Prompt user to enter y, or any other key to abort +# INPUT: A keystroke +# OUTPUT: Prompts for input, returns 0 for Y/y and 1 for else +# PARAMETERS: $1, $2: (Optional) override disp with $1 and patt with $2 +#=========================================================================# +{ +local disp="Press (y) to accept, anthing else to abort" patt="^[Yy]$" +[[ -n "$1" ]] && disp="$1" +[[ -n "$2" ]] && patt="$2" +local char +echo "$disp" +read -rn 1 char +echo +[[ "$char" =~ $patt ]] && return 0 + return 1 +} +#=========================================================================# +prompt_yes_no () +# DESCRIPTION: Prompt user to enter y or n, repeatedly until they do +# INPUT: Keystrokes +# OUTPUT: Prompts for input, returns 1 for N/n or 0 for Y/y +# PARAMETERS: None +#========================================================================= +# +{ +local char +until [[ "$char" =~ ^[YyNn]$ ]]; do + echo "Press (y)/(n) for yes/no" + read -rn 1 char + echo +done +[[ "$char" =~ ^[Nn]$ ]] && return 1 +return 0 +} + + #-----------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---PARSE/SEARCH/MODIFY XML---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-----------------------------# + +#=========================================================================# +do_virt_xml () +# DESCRIPTION: Run a given virt-xml command, reading from and writing to +# the named pipe +# INPUT: Parameters to send to virt-xml +# OUTPUT: Nothing except on error +# PARAMETERS: $@: Passed to virt-xml +#=========================================================================# +{ +local xml +xml="$(virt-xml "$@" <<<"$(read_pipe)" 2>/dev/null)" || + { echo "Attempt to generate xml with virt-xml failed." + echo "This is probably a bug in qq2clone" + exit "$E_unexpected" ; } >&2 + +write_pipe 1 <<<"$xml" +return 0 +} + +#=========================================================================# +find_tag () +# DESCRIPTION: Use xmllint to do an xpath search of xml and write_pipe +# all matches +# INPUT: Xpath and XML +# OUTPUT: Write one match per line +# PARAMETERS: $1: Xpath, $2: XML location, or leave blank to read from +# stdin +#=========================================================================# +{ +if [[ -n "$2" ]]; then + write_pipe 1 <"$2" +else + local line + while read -r line; do + write_pipe 0 "$line" + done +fi + +xmllint --xpath "$1" --auto |& grep -qi 'xpath error' && + unexpected_error find_tag + +xmllint --noblanks --dropdtd --nowarning --xpath "$1" \ + 2>/dev/null <(read_pipe) | write_pipe 1 + +return 0 +} +#=========================================================================# +get_attr_value () +# DESCRIPTION: Given an attribute="value" pair, echo the value +# INPUT: Attribute value pair +# OUTPUT: Value, or unexpected_error if input doesn't match pattern (do not +# rely on this function for checking that a string is a name=value pair +# PARAMETERS: $1: attribute="value" +#=========================================================================# +{ +p="$(strip_ws "$1")" +[[ "$p" =~ ^[^\=]+\=[[:space:]]*[^[:space:]](.*).$ ]] && + { echo "${BASH_REMATCH[1]}"; return 0; } +unexpected_error get_attr_value +return 0 +} +#=========================================================================# +get_disk_devices () +# DESCRIPTION: Find all disk device file locations from an XML file +# INPUT: libvirt domain XML file on stdin +# OUTPUT: writepipe each file location +# PARAMETERS: None +#=========================================================================# +{ +find_tag '//devices/disk[@type="file"][@device="disk"]/source/@file' +local line val +while read -r line; do + val="$(get_attr_value "$line")" || unexpected_error get_disk_devices + write_pipe 0 "$val" +done < <(read_pipe) +return 0 +} +get_disk_devices_db () +# DESCRIPTION: Like get_disk_devices, but get info from the database +# INPUT: Machine number, or omit to get template info +# OUTPUT: writepipe each file location +# PARAMETERS: $1: (Optional) machine number +#=========================================================================# +{ +local query disk +if (($#)); then + query="select disks from CLONES where id='$1' \ + and template='${OPT[TEMPLATE]}';" +else + query="select disks from TEMPLATES where name='${OPT[TEMPLATE]}';" +fi +while read -r disk; do + write_pipe 0 "$disk" +done < <(sqlite3 "$query") +return 0 +} +#=========================================================================# +has_spice () +# DESCRIPTION: Check whether a machine supports spice graphics +# INPUT: A machine number +# OUTPUT: Returns 0 if yes and 1 if no +# PARAMETERS: $1: A machine number +#=========================================================================# +{ +local uuid +uuid="${CL_MAP["$1"]}" +find_tag '//devices/graphics[@type="spice"]' < <(virsh dumpxml "$uuid") +local match=0 line +while read -r line; do + match=1 +done < <(read_pipe) +((match)) && return 0 +return 1 +} + + #--------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---FILESYSTEM/DB INTERACTIONS---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #--------------------------------# + +#=========================================================================# +copy_file () +# DESCRIPTION: Copy file $1 to $2 and give error messages, exit on error +# INPUT: File to copy +# OUTPUT: Error messages and exit codes as needed +# PARAMETERS: $1: File to copy, $2: Location to copy to +#=========================================================================# +{ +(($# == 2)) || unexpected_error copy_file +check_rw "$1" "$(dirname "$2")" +[[ -e "$2" ]] && check_rw "$2" +cp -fR "$1" "$2" &>/dev/null || unexpected_error copy_file +return 0 +} +#=========================================================================# +get_md5 () +# DESCRIPTION: Get md5sum of a file without the trailing filename +# INPUT: A filepath +# OUTPUT: The md5sum +# PARAMETERS: $1: Filepath +#=========================================================================# +{ +local md5 +check_rw "$1" || unexpected_error get_md5 +md5="$(md5sum "$1")" +[[ "$md5" =~ ^[[:space:]]*([^[:space:]]+)([[:space:]]|$) ]] +echo "${BASH_REMATCH[1]}" +return 0 +} +#=========================================================================# +make_dir () +# DESCRIPTION: Make a directory at given location or exit with error +# message +# INPUT: Filepath +# OUTPUT: Error messages and exit code as needed +# PARAMETERS: $1: Filepath of directory to make +#=========================================================================# +{ +(($# == 1)) || unexpected_error make_dir +if [[ -e "$1" ]]; then + [[ -d "$1" ]] || + { echo "Tried to create directory:" + echo "$1" + echo "but it already exists and is not a directory" + exit "$E_file"; } >&2 + check_rw "$1" + return 0 +fi +mkdir -p "$1" &>/dev/null +check_rw "$1" +return 0 +} +#=========================================================================# +move_file () +# DESCRIPTION: Move file $1 to $2 or give error messages, exit on error +# INPUT: File to move, new location +# OUTPUT: Error messages and exit codes as needed +# PARAMETERS: $1: File to move, $2: Location to move to +#=========================================================================# +{ +(($# == 2)) || unexpected_error move_file +check_rw "$1" "$(dirname "$2")" +if [[ -e "$2" ]]; then + chmod +rw "$2" || + { echo "No permission to write $2" >&2; exit "$E_permission"; } +fi +mv -f "$1" "$2" &>/dev/null || unexpected_error move_file +return 0 +} +#=========================================================================# +write_file () +# DESCRIPTION: Write contents of named pipe to file or error and exit +# INPUT: Filepath as parameter, content via write_pipe +# OUTPUT: Error messages and exit codes as needed +# PARAMETERS: $1: Filepath +#=========================================================================# +{ +(($# == 1)) || unexpected_error write_file +touch "$1" +check_rw "$1" +[[ -d "$1" ]] && unexpected_error write_file +local temp1 temp2 +temp1="$(mktemp)" || temp_error +temp2="$(mktemp)" || { rm -f "$temp1" &>/dev/null; temp_error; } +cp "$1" "$temp1" || + { rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } +read_pipe > "$temp2" || + { rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } +mv -f "$temp2" "$1" &> /dev/null || + { rm -f "$1" &>/dev/null; mv -f "$temp1" "$1" &>/dev/null; + rm -f "$temp1" "$temp2" &>/dev/null; unexpected_error write_file; } +return 0 +} +#=========================================================================# +sqlite3 () +# DESCRIPTION: Pass arguments to sqlite3 binary, prepending basic +# parameters that are always used +# INPUT: Arguments to sqlite3 +# OUTPUT: Dependent on sqlite3 +# PARAMETERS: Arbitrary +#=========================================================================# +{ +"${QQ2_DIR}/sqlite3" --batch --separator $'\n' "${QQ2_DIR}/qq2clone.db"\ + "$@" || unexpected_error sqlite3 +} + + #-----------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---IMPORT/MODIFY TEMPLATES---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-----------------------------# + +# This section includes only helper functions for functions that import or +# modify templates + +#=========================================================================# +commit_image () +# DESCRIPTION: Commit changes from staging image(s) to template image(s) +# INPUT: Parameters to calling function +# OUTPUT: Status updates, prompts, error messages +# PARAMETERS: $@ from calling function +#=========================================================================# +{ +if (( ${#CL_MAP[@]} + ${#BAD_CL[@]} > 1)) && (($# < 3)); then + echo "This template still has clones. If changes are committed," + echo "these clones may become corrupted. To avoid this," + echo "retrieve any information you need from these clones" + echo "and then delete them. Aborting" + echo + echo "To continue anyway, append argument \"force\"" + exit "$E_args" +fi + +local disk check t d +if (($# < 3)); then + get_disk_devices_db + while read -r disk; do + while read -r t; do + check="$(sqlite3 "select exists \ + (select * from CLONES where template='$t');")" + ((check)) || continue + while read -r d; do + if [[ "$d" == "$disk" ]]; then + echo "Although this template has no clones, template" + echo "$t does. These templates share disk:" + echo + echo " $disk" + echo + echo "If changes are committed, these clones may become" + echo "corrupted. To avoid this, retrieve any information you" + echo "need from these clones and then delete them. Aborting." + echo + echo "To continue anyway, append argument \"force\"" + exit "$E_args" + fi >&2 + done < <(sqlite3 "select disks from TEMPLATES where name='$t';") + done < <(sqlite3 "select name from TEMPLATES where \ + not name='${OPT[TEMPLATE]}';") + done < <(read_pipe) +fi + +if (($# < 3)); then + echo "This operation has the potential to corrupt your master template" + echo "image if it is interrupted." + if ((OPT[QUIET] )); then + echo "Append argument force to continue" >&2 + exit "$E_args" + fi + prompt_yes_abort || exit 0 +fi + +while read -r disk; do + ((OPT[QUIET])) || echo "Committing $disk..." + output="$(qemu-img commit -d "$disk" 2>&1)" || + { echo "$output" >&2; echo "Operation failed" >&2; + exit "$E_unexpected"; } + rm -f "$disk" &>/dev/null || + { echo "Failed to delete old image. Permission issue? " >&2; + echo "Process may not have completed succesfully" >&2; + exit "$E_permission"; } +done < <(sqlite3 "select disks from CLONES where id='0' and\ + template='${OPT[TEMPLATE]}';") +delete_machine 0 0 + +return 0 +} +#=========================================================================# +get_template_name () +# DESCRIPTION: Helper for exec_com_import_template. write_pipes the +# original name from the xml, then the new one +# INPUT: XML is write_piped, and argument to exec_com_import_template +# giving the name to import template with is optionally provided +# OUTPUT: See description. Error messages if name is bad +# PARAMETERS: $1: (optional) Template name, overrides value from XML +#=========================================================================# +{ +local name char +name="$(strip_ws "$1")" + +if [[ -n "$name" ]]; then + valid_xml_name_check "$name" + template_name_available "$name" +fi + +local xmlname +find_tag '//name/text()' <<<"$(read_pipe)" +while read -r line; do + line="$(strip_ws "$line")" + if [[ -n "$xmlname" ]]; then + xmlname="${xmlname}${line}" + else + xmlname="$line" + fi +done < <(read_pipe) +write_pipe 0 "$xmlname" + +if [[ -z "$name" ]]; then + name="$xmlname" +fi + +write_pipe 0 "$name" +return 0 +} +#=========================================================================# +import_get_xml () +# DESCRIPTION: Determine if argument to exec_com_import_template is a +# libvirt domain on the current connection or a filepath, check that it +# is valid, and write_pipe the xml +# INPUT: argument designating template XML or domain name/uuid +# OUTPUT: Error messages as needed, write_pipes XML on success +# PARAMETERS: $1: $1 from calling funcion +#=========================================================================# +{ +if [[ "$1" =~ ^/ ]]; then + { [[ -e "$1" ]] && [[ -r "$1" ]] ; } || + { echo "No read permission for $1 or file does not exist" + exit "$E_permission" ; } >&2 + virt-xml-validate "$1" &> /dev/null || + { virt-xml-validate "$1"; + echo "File $1 is not a valid libvirt domain XML document" + exit "$E_xml" ; } >&2 + write_pipe 1 <"$1" +else + virsh dominfo "$1" &>/dev/null || + { echo "Cannot access libvirt domain with name/uuid $1. " + echo "Wrong connection URI? Currently $LIBVIRT_DEFAULT_URI" + exit "$E_libvirt"; } >&2 + local line uuid + while read -r line; do + if [[ "$line" =~ \ + ^[[:space:]]*[Uu][Uu][Ii][Dd]\:[[:space:]]*([^[:space:]]+) ]]; then + uuid="${BASH_REMATCH[1]}" + fi + done < <(virsh dominfo "$1") + local check=0 + check="$(sqlite3 "select exists (select * from CLONES \ + where uuid='$uuid');")" + if ((check)); then + echo "Cannot import a clone as a template" >&2 + exit "$E_template" + fi + virsh dumpxml --inactive "$1" 2>/dev/null | write_pipe 1 +fi +return 0 +} +#=========================================================================# +rename_template () +# DESCRIPTION: Change template name, and all of its clone names +# INPUT: A current template name, and a new one +# OUTPUT: Status updates, error messages +# PARAMETERS: $@ from calling function exec_com_modify_template +#=========================================================================# +{ +local old_name="$1" new_name="$3" +local tdir="${OPT[TEMPLATE_DIR]}" +local xml="${tdir}/${1}.xml" +check_rw "$xml" + +OPT[TEMPLATE]="$old_name" + + +write_pipe 1 <"$xml" +template_name_available "$new_name" +valid_xml_name_check "$new_name" +do_virt_xml --edit --metadata name="$new_name" + +xml="${tdir}/${new_name}.xml" +write_file "$xml" +rm -f "${tdir}/${old_name}.xml" &>/dev/null +sqlite3 "update TEMPLATES set name='${new_name}' where name='${old_name}';" +OPT[TEMPLATE]="$new_name" +check_template &>/dev/null # Just to update md5sum + +((OPT[QUIET])) || echo "Template name changed"; + + +if (( ${#CL_MAP[@]} + ${#BAD_CL[@]} )); then + if ! ((OPT[QUIET] == 2)); then + echo "Now renaming clones" + local machines_on + machines_on="$(get_target_set destroy)" + [[ -n "$machines_on" ]] && + { echo "All clones that are not turned off will not be renamed." + echo "qq2clone will still know they are clones of $new_name," + echo "but in virsh and virt-viewer their old name will remain." + echo + echo "Shut down any running clones of $new_name you wish renamed" + echo "and press enter when ready to proceed." + read -rs + echo ; } >&2 + fi + + local id uuid cl_name + while read -r id; do + read -r uuid + cl_name="$(unique_name_uuid 0 "${new_name}#$id")" + virsh domrename "$uuid" "$cl_name" &>/dev/null + sqlite3 "update CLONES set template='$new_name' where\ + template='$old_name';" + done < <( sqlite3 "select id,uuid from CLONES where \ + template='$old_name'" ) +fi + +(( OPT[QUIET] )) || echo "Template rename complete" +exit 0 +} +#=========================================================================# +user_undefine_domain () +# DESCRIPTION: Prompt the user to undefine libvirt domain (or not) +# INPUT: Domain name +# OUTPUT: Gives info to and prompts user +# PARAMETERS: $1: Domain name +#=========================================================================# +{ +((OPT[QUIET] == 2)) && return 0 +echo +echo "Would you like to undefine the libvirt domain this template" +echo "was made from? This prevents the original domain from being" +echo "accidentally run, altering the template disk and potentially" +echo "corrupting any clones of that template." +echo +echo "This will apply every flag listed in 'man virsh' for command" +echo "'undefine' that may be required to succeed. I.e., snapshot" +echo "and checkpoint metadata, managed save images, etc. will be" +echo "discarded." +echo +if prompt_yes_no; then + virsh domstate "$1" 2>/dev/null | grep -q "shut off" || + virsh domstate "$1" 2>/dev/null | grep -q "crashed" || + { echo "This domain is still running, so make sure you turn it off"; + echo "before making clones from this template. Otherwise, it will" + echo "continue modifying the template storage device even" + echo "though it is undefined."; } >&2 + if virsh undefine --managed-save --checkpoints-metadata\ + --snapshots-metadata --nvram "$1" &> /dev/null; then + echo "Domain undefined." + else + echo "Could not undefine domain. Import still completed." + fi +fi +} + + #-------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---ERROR MESSAGES AND CHECKS---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-------------------------------# + +#=========================================================================# +arg_error () +# DESCRIPTION: If args are too few, too many, or simply wrong, this +# function provides a concise way to exit with proper message and code +# INPUT: Type of problem, name of command, name of bad arg +# OUTPUT: Echo error message, exit with E_args +# PARAMETERS: $1: 0) too few args 1) too many args 2) incorrect arg +# $2: The command, $3: in the case of incorrect arguments, the bad +# argument in question (omit otherwise) +#=========================================================================# +{ +local line +if (( $1 == 0 )); then + echo "Too few arguments to qq2clone $2" +elif (( $1 == 1 )); then + echo "Too many arguments to qq2clone $2" +else + echo "Bad argument to qq2clone $2: $3" +fi +exit "$E_args" +} >&2 +#=========================================================================# +check_dir () +# DESCRIPTION: Checks that a directory can be written to +# INPUT: A filepath +# OUTPUT: Error messages and exit codes as needed +# PARAMETERS: $1: Filepath +#=========================================================================# +{ +[[ "$1" =~ ^/ ]] || + { echo "Invalid filepath $1 specified. Use an absolute filepath"; + exit "$E_args"; } +mkdir -p "$1" &>/dev/null +{ [[ -d "$1" ]] && [[ -w "$1" ]]; } || + { echo "Cannot create $1 or cannot write to directory, check "; + echo "filepath and permissions"; + exit "$E_permission"; } +return 0 +} >&2 +#=========================================================================# +check_rw () +# DESCRIPTION: Provide an error message and exit if specified file cannot +# be read and written to. If file is a directory and a preceding +# argument is '-r', check recursively +# INPUT: A filepath (preferably fully qualified as a file could technically +# be named '-r' +# OUTPUT: Error messages and exit codes as needed +# PARAMETERS: $@: Filepaths to check +#=========================================================================# +{ +local redir +if [[ "$1" == "-r" ]]; then + redir=1; shift +else + redir=0 +fi + +while (($#)); do + if { chmod +rw "$1" || { [[ -w "$1" ]] && [[ -r "$1" ]]; } || + readlink "$1" ; } &>/dev/null; +then + shift + elif [[ -e "$1" ]]; then + echo "No read/write permissions for $1" >&2 + exit "$E_permission" + else + echo "The filepath $1 either does not exist or cannot be seen " >&2 + echo "with current permissions" + exit "$E_permission" + fi + local type line + type="$(file -b "$1")" + if [[ "$type" =~ directory ]] && ((redir)); then + while read -r line; do + check_rw -r "$line" + done < <(find "$1" 2>/dev/null) + fi +done +return 0 +} >&2 +#=========================================================================# +check_template () +# DESCRIPTION: Check if OPT[TEMPLATE] is defined. If it is, see if its +# md5sum is in agreement with the database. If it isn't, update the +# database and see if it is valid. Make sure that aspect of the db is +# updated too. Return 1 if template is not defined, or 2 if it is not +# valid +# INPUT: None +# OUTPUT: Error message and exit +# PARAMETERS: None +#=========================================================================# +{ +local md5 md5_curr valid + +check_template_exists || return 1 + +md5="$(sqlite3 "select md5sum from TEMPLATES where \ + name='${OPT[TEMPLATE]}';")" + +[[ -e "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" ]] || + { sqlite3 "update TEMPLATES set md5sum='0',valid='0' \ + where name='${OPT[TEMPLATE]}';"; return 2; } + +valid="$(sqlite3 "select valid from TEMPLATES where \ + name='${OPT[TEMPLATE]}';")" + +check_rw "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" +local md5_curr +md5_curr="$(get_md5 "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml")" + +[[ "$md5" == "$md5_curr" ]] && [[ "$valid" == "1" ]] && return 0 +[[ "$md5" == "$md5_curr" ]] && [[ "$valid" == "0" ]] && return 2 +[[ "$md5" == "$md5_curr" ]] && unexpected_error check_template + +valid=0 +virt-xml-validate "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" \ + &>/dev/null && valid=1 +sqlite3 "update TEMPLATES set md5sum='$md5_curr',valid='$valid' \ + where name='${OPT[TEMPLATE]}';" + +local disks +if ((valid)); then + get_disk_devices < "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" + disks="$(read_pipe)" + sqlite3 "update TEMPLATES set disks='$disks',valid='$valid' \ + where name='${OPT[TEMPLATE]}';" +fi + +((valid)) || return 2 +return 0 +} +#=========================================================================# +check_template_disks () +# DESCRIPTION: Verify that the disks named by a template exist, can be read +# and are not locked. This check is not needed for most commands, but +# when it is needed, check_template should be succesfully run first to +# verify that the disks column in the database is correct +# INPUT: None +# OUTPUT: Error messages and exit if needed +# PARAMETERS: None +#=========================================================================# +{ +local disk qemu_out +while read -r disk; do + [[ -e "$disk" ]] || + { echo "Template ${OPT[TEMPLATE]} refers to $disk, which either"; + echo "does not exist or cannot be seen with current permissions"; + exit "$E_template"; } >&2 + [[ -r "$disk" ]] || + { echo "Template ${OPT[TEMPLATE]} refers to $disk, but the file" ; + echo "cannot be read"; + exit "$E_permission"; } + qemu_out="$(qemu-img info "$disk" 2>&1)" || + { echo "When checking the disk file $disk with qemu-img, the"; + echo "following problem was encountered:"; + echo "$qemu_out"; + exit "$E_libvirt"; } >&2 +done < <( sqlite3 "select disks from TEMPLATES where \ + name='${OPT[TEMPLATE]}';") + +return 0 +} +#=========================================================================# +check_template_exists () +# DESCRIPTION: There are a few places where it is necessary to check that +# a template exists, but not the rest of check_template, so this is its +# own function +# INPUT: None +# OUTPUT: Return 0 if OPT[TEMPLATE] exists and 1 if it does not +# PARAMETERS: None +#=========================================================================# +{ +local check +check="$(sqlite3 "select exists ( select * from TEMPLATES where\ + name='${OPT[TEMPLATE]}');")" +((check)) && return 0 +return 1 +} +#=========================================================================# +set_error () +# DESCRIPTION: Used when convert_to_seq fails +# INPUT: None +# OUTPUT: Error message and exit +# PARAMETERS: None +#=========================================================================# +{ +echo "Improper or badly formatted argument specifying machine or set of " +echo "machines" +exit "$E_args" +} >&2 +#=========================================================================# +target_error () +# DESCRIPTION: Used when intersection of user-specified set and set of +# existing machines that are valid targets for current command results in +# empty set +# INPUT: Name of command +# OUTPUT: Error message and exit +# PARAMETERS: $1: Name of command invoked with set +#=========================================================================# +{ +echo "Specified set of machines does not contain any valid targets" +echo "for $1 to operate on" +exit "$E_args" +} >&2 +#=========================================================================# +stage_error () +# DESCRIPTION: When an action is attempted on a 'staging' clone, (one +# created by modify-template prepare-image) but that clone is listed in +# BAD_CL, this message is displayed +# INPUT: None +# OUTPUT: Error message and exit +# PARAMETERS: None +#=========================================================================# +{ +echo "A clone staging changes to the template iamge was previously made," +echo "but it is now missing. Restore it manually, connect to the" +echo "appropriate URI if it is on another connection, or delete it from" +echo "qq2clone's database using the command:" +echo +echo " qq2clone check ${OPT[TEMPLATE]}" +echo +echo "(This clone will have ID: 0)" +exit "$E_permission" +} >&2 +#=========================================================================# +temp_error () +# DESCRIPTION: If mktemp fails, this function should be invoked +# INPUT: None +# OUTPUT: Error message and exit with E_extcom +# PARAMETERS: None +#=========================================================================# +{ +echo "Attempt to create a temp file with mktemp failed" +exit "$E_extcom" +} >&2 +#=========================================================================# +template_error () +# DESCRIPTION: Takes a return code from check_template, gives appropriate +# error message and exits if it is nonzero +# INPUT: Check_template return status +# OUTPUT: Error message and exit code or nothing +# PARAMETERS: $1: Return code from check_template +#=========================================================================# +{ +(($1 == 1)) && + { echo "The template '${OPT[TEMPLATE]}' does not exist"; + exit "$E_template"; } +(($1 == 2)) && + { echo "The template '${OPT[TEMPLATE]}' is not valid due to bad"; + echo -n "or missing XML at location:"; + echo + echo " ${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml"; + echo + exit "$E_template"; } +return 0 +} >&2 +#=========================================================================# +template_name_available () +# DESCRIPTION: Check that the template name is available, and give an +# appropriate error message if not +# INPUT: A name +# OUTPUT: An error message if needed +# PARAMETERS: $1: Template name to check +#=========================================================================# +{ +local name +while IFS= read -r name; do + if [[ "$name" == "$1" ]]; then + echo "The name $1 belongs to an existing template" + exit "$E_template" + fi +done < <(sqlite3 "select name from TEMPLATES;") +if [[ -e "${OPT[TEMPLATE_DIR]}/${1}.xml" ]]; then + echo "Although template name $1 is not currently in use," + echo "a file where this template's XML document belongs already" + echo "exists. Move or delete this file:" + echo + echo " ${OPT[TEMPLATE_DIR]}/${1}.xml" + exit "$E_template" +fi +return 0 +} >&2 +#=========================================================================# +unexpected_error () +# DESCRIPTION: Error on unexpected event, which is likely a bug in qq2clone +# INPUT: None +# OUTPUT: Error message and exit code +# PARAMETERS: $1: function name where error occurred +#=========================================================================# +{ +echo "qq2clone has encountered an unexpected problem." +echo "The problem occurred in function: $1" +exit "$E_unexpected" +} >&2 +#=========================================================================# +valid_xml_name_check () +# DESCRIPTION: Check that XML is valid after modifying name and return 0 +# if so, exit with error message else +# INPUT: write_piped XML file and new name as parameter. Leaves XML in +# pipe after execution +# OUTPUT: Error message and exit code if needed +# PARAMETERS: $1: The new name +#=========================================================================# +{ +virt-xml --edit --metadata name="$1"<<<"$(read_pipe 1)" &>/dev/null || + { echo "When trying to use name $1 to generate an xml" + echo "file, there was an error - this name is not acceptable" + echo "for a libvirt domain. Try another." + exit "$E_libvirt" ; } >&2 +} >&2 + + #--------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---HELPERS FOR EXEC_COM_CHECK---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #--------------------------------# + +#=========================================================================# +delete_template_and_clones () +# DESCRIPTION: Delete a template and all of its clones +# INPUT: None +# OUTPUT: Status updates +# PARAMETERS: None +#=========================================================================# +{ +echo +hr +echo +echo "DELETING TEMPLATE: ${OPT[TEMPLATE]}" +local id disk +echo +local fail=0 +echo " Deleting all defined clone domains" +for id in "${!CL_MAP[@]}"; do + echo " Attempting to delete ${OPT[TEMPLATE]}#${id} ${CL_MAP["$id"]}..." + if ( delete_machine "$id" ) &> /dev/null; then + echo " Success." + else + echo " Failed." + fail=1 + fi +done +if ((fail)) || (( ${#BAD_CL[@]} )); then + echo + echo " Manually deleting files and/or undefining any remaining domains" + while read -r id; do + while read -r disk; do + [[ -z "$disk" ]] && continue + rm -f "$disk" &>/dev/null || + [[ -e "$disk" ]] && + { echo "Failed to delete $disk, check permissions. Aborting" >&2; + exit "$E_permission"; } + done < <(sqlite3 "select disks from CLONES where id='$id' and \ + template='${OPT[TEMPLATE]}'") + sqlite3 "delete from CLONES where id='$id' and \ + template='${OPT[TEMPLATE]}';" + echo " Deleted ${OPT[TEMPLATE]}#${id} ${CL_MAP["$id"]}" + done < <(sqlite3 "select id from CLONES where \ + template='${OPT[TEMPLATE]}';") +fi +echo +echo " All clones deleted." +echo +rm -f "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" &>/dev/null +if [[ -e "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" ]]; then + echo "Failed to delete template XML at" >&2 + echo "${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" >&2 + echo "Aborting" + exit "$E_permission" +fi +sqlite3 "delete from TEMPLATES where name='${OPT[TEMPLATE]}';" +echo "TEMPLATE DELETED: Template ${OPT[TEMPLATE]} deleted." +return 0 +} +#=========================================================================# +prompt_delete_bad_clones () +# DESCRIPTION: Iterate through missing clones, prompting user before +# taking action +# INPUT: None +# OUTPUT: Prompts and status updates to user +# PARAMETERS: None +#=========================================================================# +{ +local id i=0 total="${#BAD_CL[@]}" disk prompt=1 select +local t="${OPT[TEMPLATE]}" +for id in "${!BAD_CL[@]}"; do + ((i++)) + echo + hr + echo + echo "MISSING CLONE ${i} / $total" + echo " ID: $id" + echo "UUID: ${BAD_CL["$id"]}" + while read -r disk; do + echo "DISK: $disk" + done < <(sqlite3 "select disks from CLONES where id='$id' and\ + template='${t}';") + echo + if ((prompt)); then + echo " (1) Delete clone from database, DO NOT delete disk files" + echo " (2) Delete clone from database, DO delete disk files" + echo " (3) Skip this clone" + echo " (4) Do option (1) for all missing clones of this template" + echo " (5) Do option (2) for all missing clones of this template" + echo " (6) Abort: leave the clones as they are" + select="$(prompt_num 1 6)" + (( select == 6 )) && { echo "Abort"; return 0; } + (( select == 5 )) && { select=2; prompt=0; } + (( select == 4 )) && { select=1; prompt=0; } + (( select == 3 )) && { echo "Skipping"; echo; continue; } + fi + echo + if ((select==2)); then + while read -r disk; do + [[ -e "$disk" ]] || + { echo " $disk :" + echo " Already deleted or has been moved" + continue; } + if rm -f "$disk" &>/dev/null; then + echo " Deleted $disk" + else + echo " Failed to delete $disk" + fi + done < <(sqlite3 "select disks from CLONES where id='$id' and\ + template='${t}';") + fi + sqlite3 "delete from CLONES where id='$id' and template='$t';" + echo " Clone deleted from database" + echo +done +return 0 +} +#=========================================================================# +prompt_delete_orphans () +# DESCRIPTION: Find any image files in qq2clone's default storage directory +# that don't belong to qqclone2 or any machine on the current libvirt +# connetion +# INPUT: None +# OUTPUT: Prompts user and gives status updates +# PARAMETERS: None +#=========================================================================# +{ +# This function is very long, partially due to lots of echo statements +# and requests for user input. However, all of the text describing things +# to the user also makes it easy to understand the code, so for now +# I am leaving it as-is. +hr +echo +echo "qq2clone will look in its default storage pool:" +echo +echo " ${OPT[STORAGE]}" +echo +echo "Any files found there that are not storage devices in use by" +echo "a virtual machine on the current libvirt connection or qq2clone" +echo "templates will be considered orphans. This shouldn't take too long," +echo "but it could if you have a lot of files in that location, a large" +echo "number of domains defined, or a slow machine" +echo +echo "qq2clone will take no action on these files unless directed to" +echo +echo "Ctrl+C to abort search" +echo +hr +echo +local patt +patt="^[[:space:]]*[^[:space:]]+[[:space:]]+([^[:space:]].*[^[:space:]])" +patt="${patt}[[:space:]]*$" + +local check +check="$(virsh list --all)" +check="$(strip_ws "$check")" +if [[ -n "$check" ]]; then check=1; else check=0; fi + +local uuid device path c=8 n match + +echo "Generating list of domain storage devices..." +while (( check )) && read -r device; do + while (( c )); do (( c-- )); continue 2; done; + + if [[ -z "$device" ]]; then + for ((c=3;c>0;)); do + read -r n; [[ -z "$n" ]] && continue + (( c-- )) + done + continue + fi + [[ "$device" =~ $patt ]] && write_pipe 0 "${BASH_REMATCH[1]}" + +done < <( while read -r uuid; do \ + echo "domblklist $uuid; echo --err null;\ + domblklist $uuid --inactive;"; done \ + < <( virsh list --all --uuid ) | virsh 2>&1 ) + +# We have all the disk files associated with domains, but we still need +# the ones referenced by template XML +declare -a templates +while read -r line; do + write_pipe 0 "$line" +done < <(sqlite3 "select disks from TEMPLATES;") + +echo "Generating list of filepaths in default storage pool..." +declare -a f_paths +local path +while read -r path; do + f_paths[${#f_paths[@]}]="$path" +done < <(find "${OPT[STORAGE]}" -depth -mindepth 1 2>/dev/null) + +echo "Comparing..." +declare -a orphans +match=0 +for path in "${f_paths[@]}"; do + while read -r device; do + (( match )) && continue + if [[ "$device" == "$path" ]]; then + match=1 + fi + done < <( read_pipe 1 ) + [[ "$match" == "1" ]] && { match=0; continue; } + orphans[${#orphans[@]}]="$path" +done +read_pipe >/dev/null + +local j=${#orphans[@]} +if ((j)); then + echo + echo "Total potentially orphaned files: $j" + echo + echo "Remember that false positives are very possible. qq2clone" + echo "considers any file in its default storage pool that is not" + echo "a storage device for a virtual machine listed by virsh or" + echo "a template known to qq2clone to be an orphan. It is unwise to" + echo "delete any detected files without looking at them first" + echo + prompt_yes_no && less \ + < <( for path in "${orphans[@]}"; do echo "$path"; done ) + echo + echo "Would you like to store a copy of this list to disk?" + local temp + if prompt_yes_no; then + temp="$(mktemp)" + for path in "${orphans[@]}"; do echo "$path"; done > "$temp" + echo "File printed to $temp" + fi + echo + echo "1) Delete all files found" + echo "2) Answer a prompt to delete or leave alone each file" + echo "3) Abort and handle the situation manually" + local select prompt=1 file fail=0 + select="$(prompt_num 1 3)" + ((select==1)) && prompt=0 + ((select==3)) && { echo "Abort"; return 0; } + + local i + for ((i=0;i/dev/null + rmdir "$file" &>/dev/null + rm -f "$file" &>/dev/null + if [[ -e "$file" ]]; then + echo "Unable to delete" + write_pipe "$file" + fail=1 + else + echo "Deleted" + fi + echo + done + if ((fail)); then + echo "Check complete, but failed to delete some files." + echo + echo "View a list of fails qq2clone failed to delete?" + if prompt_yes_no; then + echo + read_pipe 1 | less + else + echo + fi + echo "Save to disk?" + if prompt_yes_no; then + echo + temp="$(mktemp)" + read_pipe > "$temp" + echo "File printed to $temp" + else + echo + fi + else + echo "Orphaned file check completed" + fi +else + echo + echo "No orphaned files were found" +fi +return 0 +} + + #-----------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---INTERACT WITH VIRSH/VMs---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #-----------------------------# + +#=========================================================================# +clone () +# DESCRIPTION: Clone a virtual machine from OPT[TEMPLATE] +# INPUT: If desired, designate that clone should have special ID 0 +# OUTPUT: Echo message when complete or on error +# PARAMETERS: $1: (Optional) If '0', create clone intended for staging +# changes to a base template image +#=========================================================================# +{ +local base_mach_name line check i +local txml="${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" + +if [[ "$1" == "0" ]]; then + # ID reserved for clone where changes to template image are staged + i="0" + check="$(sqlite3 "select exists ( select * from CLONES where id='0' and \ + template='${OPT[TEMPLATE]}');")" + ((check)) && return 1 + base_mach_name="${OPT[TEMPLATE]}#STAGING" +else + for((i=1;;i++)); do + check="$(sqlite3 "select exists ( select * from CLONES where id='$i' \ + and template='${OPT[TEMPLATE]}');")" + (("$check")) || break + done + base_mach_name="${OPT[TEMPLATE]}#${i}" +fi + +local name uuid +{ + read -r name + read -r uuid +} < <(unique_name_uuid 2 "$base_mach_name") + + +local storage="${OPT[STORAGE]}" +declare -a f_arr +local img new_img j=-1 type level + +local disks +disks="$(sqlite3 "select disks from TEMPLATES where\ + name='${OPT[TEMPLATE]}';")" + +trap 'rm -f "${storage}/${name}.${uuid:?}.*"' INT +while read -r img; do + ((j++)) + new_img="${storage}/${name}.${uuid}.${j}.qcow2" + level=0 + while read -r line; do + [[ "$line" =~ \{[[:space:]]*$ ]] && { ((level++)); continue; } + [[ "$line" =~ \},?[[:space:]]*$ ]] && { ((level--)); continue; } + if ((level == 1)); then + [[ "$line" =~ \"format\":[[:space:]]*\"(.*)\" ]] && + type="${BASH_REMATCH[1]}" + fi + done < <(qemu-img info --output=json "$img") + qemu-img create -f qcow2 -F "$type" -b "$img" "$new_img"\ + &>/dev/null || + { rm -f "${storage}/${name}.${uuid:?}.*"; + unexpected_error clone; } + f_arr[${#f_arr[@]}]="-f" + f_arr[${#f_arr[@]}]="$new_img" +done < <(echo "$disks") + + +virt-clone --original-xml "$txml" --name "$name" --uuid "$uuid"\ + --preserve-data "${f_arr[@]}" &>/dev/null || + { rm -f "${storage}/${name}.${uuid:?}.*"; + unexpected_error clone; } + +disks="" +local before=0 +for ((j=1;j<${#f_arr[@]};j+=2)); do + if ((before)); then + disks="$(echo "$disks";echo "${f_arr["$j"]}")" + else + before=1 + disks="${f_arr["$j"]}" + fi +done + +sqlite3 "insert into CLONES values \ + ('$uuid','$i','${OPT[TEMPLATE]}','$disks');" + +((OPT[QUIET])) || echo "Cloned: $name $uuid" + +CL_MAP["$i"]="$uuid" +CL_STATE["$i"]="off" + +trap 'exit' INT +if (( OPT[NORUN] )); then + return 0 +elif ((OPT[USE_SPICE])); then + connect "$i" +else + start_domain "$i" +fi + +return 0 +} +#=========================================================================# +connect () +# DESCRIPTION: Run machine. If it has spice graphics, connect to graphical +# console with virt-viewer/spicy +# INPUT: Machine number +# OUTPUT: None except on error +# PARAMETERS: $1: Machine number +#=========================================================================# +{ +if (( OPT[SPICY] )); then + command -v spicy &> /dev/null || + { echo "Cannot find command spicy" >&2; exit "$E_extcom"; } +else + command -v virt-viewer &> /dev/null || + { echo "Cannot find command virt-viewer" >&2; exit "$E_extcom"; } +fi + +start_domain "$1" + +local uuid +uuid="${CL_MAP["$1"]}" +has_spice "$1" || return 0 + +local spice +read -ra spice < <(get_spice "$1") || + { echo "Machine did not become available before timeout" >&2; + exit "$E_timeout"; } +(( $1 == 0 )) && set -- "STAGING" +if (( OPT[SPICY] )); then + command -v spicy &> /dev/null || + { echo "Cannot find command spicy" >&2; exit "$E_extcom"; } + nohup spicy --title "${OPT[TEMPLATE]}#$1" -h "${spice[0]}" \ + -p "${spice[1]}" &>/dev/null & +else + command -v virt-viewer &> /dev/null || + { echo "Cannot find command virt-viewer" >&2; exit "$E_extcom"; } + nohup virt-viewer --uuid "$uuid" &>/dev/null & +fi + +return 0 +} +#=========================================================================# +delete_machine () +# DESCRIPTION: Delete a clone +# INPUT: Machine number +# OUTPUT: None +# PARAMETERS: $1: machine number, $2: 0 to just delete disks, 1 to wipe, 2 +# to shred +#=========================================================================# +{ +local uuid +uuid="${CL_MAP["$1"]}" + +local line disks before=0 +while read -r line; do + ((before)) && disks="${disks},"; before=1 + disks="${disks}$line" +done < <(sqlite3 "select disks from CLONES where id='$1' and\ + template='${OPT[TEMPLATE]}';") + +declare -a undef_args +undef_args=( '--managed-save' '--snapshots-metadata' '--nvram' \ + '--checkpoints-metadata' ) +if (( $2 < 2 )); then + undef_args=( "${undef_args[@]}" '--storage' "$disks") +fi +if (($2==1)); then + undef_args=( "${undef_args[@]}" "--wipe-storage" ) +fi + +destroy_domain "$1" &>/dev/null + +if (( $2 == 2 )); then + local disk + echo "Shredding ${OPT[TEMPLATE]}#$1 disks..." + while read -r disk; do + { + cd "$(dirname "$disk")" && + { shred -vf "$(basename "$disk")" 2>&1 | sed "s/^./ &/"; } && + rm -f "$disk" &>/dev/null + } || unexpected_error delete_machine + done < <(sqlite3 "select disks from CLONES where id='$1' and\ + template='${OPT[TEMPLATE]}';") +fi + +local virsh_out +virsh_out="$(virsh undefine "$uuid" "${undef_args[@]}" 2>&1)" || + { echo "$virsh_out"; unexpected_error delete_machine; } + +sqlite3 "delete from CLONES where id='$1' and \ + template='${OPT[TEMPLATE]}';" + +return 0 +} +#=========================================================================# +destroy_domain () +# DESCRIPTION: Invoke virsh destroy on given machine +# INPUT: A machine number +# OUTPUT: None +# PARAMETERS: A machine number +#=========================================================================# +{ +local uuid +uuid="${CL_MAP["$1"]}" +virsh destroy "$uuid" &>/dev/null +return 0 +} +#=========================================================================# +discard_save () +# DESCRIPTION: Delete a saved state file associated with a clone +# INPUT: A saved machine number +# OUTPUT: Error message, exit code if needed +# PARAMETERS: $1: A saved machine number +#=========================================================================# +{ +local virsh_out +virsh_out="$(virsh managedsave-remove "${CL_MAP["$1"]}" 2>&1)" || + { echo "$virsh_out"; exit "${E_libvirt}"; } +return 0 +} +#=========================================================================# +edit_xml () +# DESCRIPTION: Edit and verify OPT[TEMPLATE]'s XML file +# INPUT: None +# OUTPUT: Status updates to user, error messages as needed +# PARAMETERS: None +#=========================================================================# +{ +local xml="${OPT[TEMPLATE_DIR]}/${OPT[TEMPLATE]}.xml" +[[ -e "$xml" ]] || + { echo "The template XML is missing from $xml" + echo "There is nothing to edit." + exit "$E_template"; } >&2 + +check_template_exists || template_error $? + +local temp +temp="$(mktemp)" || temp_error +trap 'rm -f "$temp" &>/dev/null;' INT + +local ed_com="vi" +[[ -n "$EDITOR" ]] && ed_com="$EDITOR" +[[ -n "$VISUAL" ]] && ed_com="$VISUAL" + +local md5_curr md5_old +md5_old="$(get_md5 "$xml")" + +copy_file "$xml" "$temp" + +local loop=1 +while ((loop)); do + "$ed_com" "$temp" || + { echo "Editing XML with $ed_com failed" >&2; + rm -f "$temp" &>/dev/null; + exit "$E_extcom"; } + md5_curr="$(get_md5 "$temp")" + [[ "$md5_old" == "$md5_curr" ]] && + { ((OPT[QUIET])) || echo "No changes made to original XML"; + rm "$temp"; + exit 0; } + + local virt_message="" + if virt_message="$(virt-xml-validate "$temp" 2>&1)"; then + loop=0 + else + echo >&2 + echo "$virt_message" >&2 + echo "Changes resulted in malformed XML file. " >&2 + prompt_yes_abort \ + "Press (e) to edit again, anything else to abort and revert" \ + "^[Ee]$" >&2 || { rm "$temp" &>/dev/null; exit "$E_xml"; } + fi +done +move_file "$temp" "$xml" +check_template + +((OPT[QUIET])) || echo "XML modified" +trap 'exit' INT + +return 0 +} +#=========================================================================# +get_template_list () +# DESCRIPTION: List existing templates, alphabetically ordered, one per +# line +# INPUT: None +# OUTPUT: List of templates +# PARAMETERS: None +#=========================================================================# +{ +sqlite3 "select name from TEMPLATES;" | sort +return 0 +} +#=========================================================================# +get_spice () +# DESCRIPTION: Get the spice host and port of a running machine +# INPUT: The machine number +# OUTPUT: Echoes '$hostname $port' pertaining to spice connection +# PARAMETERS: $1: The machine number +#=========================================================================# +{ +local uuid time line spice state +time="$(date +%s)" +uuid="${CL_MAP["$1"]}" +until (( OPT[S_TIMEOUT] <= $(date +%s)-time )) ; do + spice="$(virsh domdisplay --type spice "$uuid" 2>&1)" + [[ "$spice" =~ ^'spice://'(.+):(.+)$ ]] || continue + echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]}"; + return 0; +done +return 1 +} +#=========================================================================# +get_state () +# DESCRIPTION: Get the state of a machine by number +# INPUT: The number of a clone managed by qq2clone +# OUTPUT: Echoes the state of the machine +# PARAMETERS: $1: Machine number +#=========================================================================# +{ +echo "${CL_STATE["$1"]}" +return 0 +} +#=========================================================================# +get_state_set () +# DESCRIPTION: List all machines in a given state +# INPUT: Name of a state +# OUTPUT: Echoes a space delimited list of machine numbers (can be empty) +# PARAMETERS: $1: state name +#=========================================================================# +{ +local id before=0 state +while read -r id; do + state="$(get_state "$id" )" + if [[ "$state" == "$1" ]] || [[ "$1" == "all" ]]; then + [[ "$id" == "0" ]] && continue + ((before)) && echo -n " "; before=1 + echo -n "$id " + fi +done < <(echo "${!CL_MAP[@]}" | tr " " "\n") +echo +return 0 +} +#=========================================================================# +get_target_set () +# DESCRIPTION: Get the set of all machines that a command may legally +# operate on +# INPUT: A command name +# OUTPUT: Echoes a space delimited set of machine numbers (can be empty) +# PARAMETERS: $1: The command name +#=========================================================================# +{ +local id before=0 statelist state +statelist="$(list_states "$1")" +while read -r id; do + state="$(get_state "$id" )" + if [[ "$statelist" =~ all ]] || [[ "$statelist" =~ $state ]] ; then + [[ "$id" == "0" ]] && continue + ((before)) && echo -n " "; before=1 + echo -n "$id" + fi +done < <(echo "${!CL_MAP[@]}" | tr " " "\n") +echo +return 0 +} +#=========================================================================# +list_display () +# DESCRIPTION: Display list of clones and their state to user +# INPUT: Optionally, pass argument "1" to print detailed xml list +# OUTPUT: Clone list +# PARAMETERS: $1: Set to one to print xml summary instead of a regular +# list +#=========================================================================# +{ +declare -A statelist +local line state + +if [[ -n "${CL_MAP[0]}" ]]; then + statelist["${CL_STATE[0]}"]="0" +fi + +while read -r line; do + [[ -z "$line" ]] && continue + state="$(get_state "$line")" + if [[ -n "${statelist["$state"]}" ]]; then + statelist["$state"]="${statelist["$state"]} $line" + else + statelist["$state"]="$line" + fi +done < <(get_state_set all | tr " " "\n") + +declare -a states machines +read -ra states \ + < <(echo "${!statelist[@]}" |tr " " "\n" | sort | tr "\n" " ") +local state m +if (($1)); then +#shellcheck disable=2119 +# This is only a (functioning) mock implementation meant as a proof of +# concept for what XML describing qq2clone's current state may be like. +# For this feature to be complete, it would: use a defined format, be +# implemented with proper, modular code, and contain all information to +# fully define qq2clone's state except for machine images and domain xml. + echo " " +else + echo "[${OPT[TEMPLATE]}]"; + if [[ -z "${statelist[*]}" ]]; then + echo " No clones" + else + for state in "${states[@]}"; do + read -ra machines <<<"$(strip_ws "${statelist["$state"]}")" + echo -n " ${state}: "; + for elem_mach in "${machines[@]}"; do + (( elem_mach )) || { echo -n "[#0: STAGING] "; continue; } + echo -n "#$elem_mach " + done + echo + done + fi +fi +return 0 +} +#=========================================================================# +list_states () +# DESCRIPTION: Helper for get_target_set. Lists machine states that a +# given command can act on. States are the same as listed in man virsh, +# but "shut off" is "off", "in shutdown" is "in-shutdown", "saved" is +# added, and "all" addresses machines in any state +# INPUT: The command name +# OUTPUT: Echoes a space delimited list of states +# PARAMETERS: $1: The command name, or if no arguments list all states +# as a space delimited list +#=========================================================================# +{ +if (($#)); then + if [[ "$1" == connect ]]; then + echo "all" + elif [[ "$1" == destroy ]]; then + echo "running idle paused in-shutdown pmsuspended" + elif [[ "$1" == exec ]]; then + echo "all" + elif [[ "$1" == restore ]]; then + echo "saved" + elif [[ "$1" == resume ]]; then + echo "paused" + elif [[ "$1" == rm ]]; then + echo "all" + elif [[ "$1" == rm-wipe ]]; then + echo "all" + elif [[ "$1" == rm-shred ]]; then + echo "all" + elif [[ "$1" == save ]]; then + echo "running pmsuspended idle paused paused" + elif [[ "$1" == save-rm ]]; then + echo "saved" + elif [[ "$1" == start ]]; then + echo "off crashed saved" + elif [[ "$1" == suspend ]]; then + echo "running pmsuspended idle" + fi +else + echo -n "all crashed idle in-shutdown off paused pmsuspended running" + echo " saved" +fi +} +#=========================================================================# +load_template () +# DESCRIPTION: Run check_template, build global arrays CL_MAP[ID]=UUID, +# CL_STATE[ID]=STATE, and BAD_CL[ID]=UUID +# INPUT: None +# OUTPUT: None +# PARAMETERS: None +#=========================================================================# +{ +# This is a hacky way of getting the information we need in a reasonably +# performant manner. It is a bit fragile, but not overly so assuming that +# virsh's output is fairly consistent across versions. This method is +# temporary, as later on qq2clone will include portions in (probably) C +# that use the libvirt API instead of virsh + +check_template +unset BAD_CL CL_MAP CL_STATE NAME_MAP +declare -ga BAD_CL CL_MAP CL_STATE NAME_MAP + +local t="${OPT[TEMPLATE]}" + +local check +check="$(sqlite3 "select exists ( select id,uuid from CLONES where \ + template='${OPT[TEMPLATE]}' );")" +((check)) || return 0 + +# Build array of all UUIDs -> IDs for template's clones +declare -A uuid_map +local id uuid +while read -r id; do + read -r uuid + uuid_map["$uuid"]="$id" +done < <(sqlite3 "select id,uuid from CLONES where template='$t';") + +# To use virsh in shell mode without having to repeatedly invoke it in +# different subshells for a large performance penalty, we will run it in +# the background and write to it with one fifo while reading it from +# another +local temp +temp="$(mktemp -d)" || temp_error +mkfifo "$temp/fifo" &>/dev/null || unexpected_error load_template +exec 4<>"$temp/fifo" +virsh <&3 >&4 2>&4 & +# virsh prepends 5 lines of useless output +local c; for ((c=5;c>0;c--)); do read -r <&4; done + +local prompt="virsh #" # In the virsh shell, input lines start with this + + echo "list --all --uuid" >&3 + echo "echo EOF" >&3 + while read -r uuid <&4; do + { [[ "$uuid" =~ ^$prompt ]] || [[ -z "$uuid" ]] ; } && continue + [[ "$uuid" == "EOF" ]] && break + + [[ -n "${uuid_map["$uuid"]}" ]] && + CL_MAP["${uuid_map["$uuid"]}"]="$uuid" + done + +local match _uuid +for uuid in "${!uuid_map[@]}"; do + match=0 + for _uuid in "${CL_MAP[@]}"; do + [[ "$uuid" == "$_uuid" ]] && { match=1; break; } + done + ((match)) && continue + BAD_CL["${uuid_map["$uuid"]}"]="$uuid" +done + +local line +for id in "${!CL_MAP[@]}"; do + uuid="${CL_MAP["$id"]}" + echo "domstate $uuid" >&3 + echo "echo EOF" >&3 + while read -r line <&4; do + { [[ "$line" =~ ^$prompt ]] || [[ -z "$line" ]] ; } && continue + [[ "$line" == "EOF" ]] && break + [[ "$line" == "in shutdown" ]] && line="in-shutdown" + [[ "$line" == "shut off" ]] && line="off" + CL_STATE["$id"]="$line" + done + + echo "domname $uuid" >&3 + echo "echo EOF" >&3 + while read -r line <&4; do + { [[ "$line" =~ ^$prompt ]] || [[ -z "$line" ]] ; } && continue + [[ "$line" == "EOF" ]] && break + NAME_MAP["$id"]="$line" + done +done + +echo "list --all --uuid --with-managed-save" >&3 +echo "echo EOF" >&3 + +while read -r uuid <&4; do + { [[ "$uuid" =~ ^$prompt ]] || [[ -z "$uuid" ]] ; } && continue + [[ "$uuid" == "EOF" ]] && break + id="${uuid_map["$uuid"]}" + [[ -z "$id" ]] && continue; [[ -z "${CL_MAP["$id"]}" ]] && continue; + CL_STATE["$id"]="saved" +done + +echo "quit" >&3 +exec 4>&- +exec 4<&- +rm -rf "$temp" &>/dev/null + +return 0 +} +#=========================================================================# +save_domain () +# DESCRIPTION: Save the state of a machine to disk +# INPUT: Machine number +# OUTPUT: None +# PARAMETERS: $1: Machine number +#=========================================================================# +{ +local uuid +uuid="${CL_MAP["$1"]}" +virsh managedsave "$uuid" &>/dev/null +return 0 +} +#=========================================================================# +start_domain () +# DESCRIPTION: If a domain is not running, start it +# INPUT: VM number or "staging" +# OUTPUT: None, except on error. Returns 0 if machine is running +# PARAMETERS: $1: Machine number +#=========================================================================# +{ + +local state uuid +uuid="${CL_MAP["$1"]}" + +state="$(get_state "$1")" + +if [[ "$state" =~ ^(off|crashed|saved)$ ]]; then + virsh start "$uuid" >/dev/null || + { echo "Virsh failed to start domain" >&2; + exit "$E_libvirt"; } +elif [[ "$state" == "paused" ]]; then + virsh resume "$uuid" >/dev/null || + { echo "Virsh failed to resume domain" >&2; + exit "$E_libvirt"; } +fi + +return 0 +} +#=========================================================================# +suspend_domain () +# DESCRIPTION: Suspend execution state of machine +# INPUT: A machine number +# OUTPUT: None +# PARAMETERS: $1: A machine number +#=========================================================================# +{ +local uuid +uuid="${CL_MAP["$1"]}" +virsh suspend "$uuid" &>/dev/null +} +#=========================================================================# +unique_name_uuid () +# DESCRIPTION: Generate a name and/or uuid unique within libvirt connection +# INPUT: Choice of uuid, name or both, and base machine name if requesting +# name +# OUTPUT: Echo name and uuid in that order, on separate lines +# PARAMETERS: $1: 0 for name only, 1 for uuid only, or 2 for both +# $2: Base machine name (required unless $1 is 1) +#=========================================================================# +{ # TODO there is no reason for this to be one function +local name="$2" uuid loop line unique list + +if (($1 != 1)); then + RANDOM=$$ + list="$(virsh list --all --name)" + unique=0 + until ((unique)); do + while read -r line; do + [[ -z "$line" ]] && continue + if [[ "$line" == "$name" ]]; then + name="${2}___${RANDOM}" + continue 2 + fi + done < <(echo "$list") + unique=1 + done + echo "$name" +fi + +if (($1 != 0)); then + list="$(virsh list --all --uuid)" + list="${list}$(echo;ls -1 "${OPT[STORAGE]}")" + unique=0 + uuid="$(uuidgen -r)" + until ((unique)); do + while read -r line; do + [[ -z "$line" ]] && continue + if [[ "$line" =~ .*"$uuid".* ]]; then + uuid="$(uuidgen -r)" + continue 2 + fi + done < <(echo "$list") + unique=1 + done + echo "$uuid" +fi + +return 0 +} + + #---------------------------------------------# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---PARSE COMMAND STRING AND INVOKE COMMAND---# + #@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@# + #---------------------------------------------# + +# Generally, user input should be checked in this section. When functions +# from other sections omit testing that user-defined values are valid, +# this is why + +#=========================================================================# +convert_to_seq () +# DESCRIPTION: Take a set of the format described in the man page and echo +# it as an ordered ascending, space delimited sequence of valid machine +# numbers +# INPUT: A set +# OUTPUT: A sequence of numbers. Returns 1 if bad set +# PARAMETERS: $1: A set +#=========================================================================# +{ +local s="$1" + +{ [[ "$1" =~ ^[,-] ]] || [[ "$1" =~ [,-'^']$ ]] || + [[ "$1" =~ [,-][,-] ]] || [[ "$1" =~ [[:space:]] ]] || + [[ "$1" =~ ^.(.*[^',']\^) ]]; } && return 1 + +declare -a parts +IFS="," read -ra parts <<<"$s" +local states i p not LH RH minus plus +states="$(list_states| tr " " "|")" + +for ((i=0;i<${#parts[@]};i++)); do + p="${parts["$i"]}" + not=0 + if [[ "${p:0:1}" == "^" ]]; then + p="${p:1}" + not=1 + fi + + if [[ "$p" =~ ^${states}$ ]]; then + p="$(get_state_set "$p")" + elif [[ "$p" =~ ^([0-9]+)'-'([0-9]+)$ ]]; then + LH="${BASH_REMATCH[1]}"; RH="${BASH_REMATCH[2]}"; + ((RH>LH)) || return 1 + ((LH>0)) || return 1 + for ((p=LH;LH0)) || return 1 + else + return 1 + fi + + if ((not)); then + minus=" $minus $p " + else + plus="$plus $p" + fi +done +[[ "$plus $minus" =~ [^0-9' '] ]] && return 1 + +local n before=0 +while read -r n; do + [[ -z "$n" ]] && continue + [[ "$minus" =~ [[:space:]]${n}[[:space:]] ]] && continue + ((before)) && echo -n " "; before=1 + echo -n "$n" +done < <( tr " " "\n" <<<"$plus" | sort -n | uniq ) +echo + +return 0 +} +#=========================================================================# +exec_com () +# DESCRIPTION: Apply flags to modify script behavior, determine which +# command the user has invoked, and perform some parsing and +# validating of input until it's time to hand off to a more specific +# function +# INPUT: All positional parameters sent to qq2clone +# OUTPUT: Error messages/exit values as needed +# PARAMETERS: Pass "$@" directly here from the global scope +#=========================================================================# +{ +parse_flags "$@" +local offset +offset="$(read_pipe)" +shift "$offset" +local com +com="$1" +shift + +local verbose_coms +verbose_coms="config|check|list|list-templates|exec|edit|modify-template" +if (( OPT[QUIET] == 2)) && + [[ ! "$com" =~ ^($verbose_coms)$ +]]; +then + exec &>/dev/null +fi + +virsh uri |& grep -qi ^QEMU || + { echo "Current libvirt URI is not QEMU:///" >&2; exit "$E_libvirt"; } + +[[ -n "$com" ]] || { usage >&2; exit "$E_args"; } + +# Commands which don't want check_template run (in advance of calling them +# anyway) +if [[ "$com" == "check" ]]; then + exec_com_check "$@" + exit 0 +elif [[ "$com" == "config" ]]; then + exec_com_config "$@" + exit 0 +elif [[ "$com" == "copy-template" ]]; then + exec_com_copy_template "$@" + exit 0 +elif [[ "$com" == "delete-template" ]]; then + exec_com_delete_template "$@" + exit 0 +elif [[ "$com" == "import-template" ]]; then + exec_com_import_template "$@" + exit 0 +elif [[ "$com" == "list-templates" ]]; then + exec_com_list_templates "$@" + exit 0 +elif [[ "$com" == "modify-template" ]]; then + exec_com_modify_template "$@" + exit 0 +elif [[ "$com" == "edit" ]]; then + exec_com_edit "$@" + exit 0 +elif [[ "$com" == "list" ]]; then + exec_com_list "$@" + exit 0 +fi + +# Clone and the set commands below get check_template run +if [[ "$com" == "clone" ]]; then + check_template; template_error "$?" + load_template + exec_com_clone "$@" + exit 0 +fi + +# All remaining commands require similiar logic to invoke, and act on sets +# of machines. set_coms[$com] provides a list of arguments to exec_com_set +declare -A set_coms +set_coms["connect"]="connect Connected" +set_coms["destroy"]="destroy_domain Destroyed" +set_coms["exec"]="This one is a special case, this text isn't used" +set_coms["restore"]="start_domain Restored" +set_coms["resume"]="start_domain Resumed" +set_coms["rm"]="delete_machine Deleted 0" +set_coms["rm-wipe"]="delete_machine Deleted_and_wiped 1" +set_coms["rm-shred"]="delete_machine Deleted_and_shredded 2" +set_coms["save"]="save_domain Saved" +set_coms["save-rm"]="discard_save Discarded_saved_state 0" +set_coms["start"]="start_domain Started" +set_coms["suspend"]="suspend_domain Suspended" + +local match=0 elem +for elem in "${!set_coms[@]}"; do + [[ "$elem" == "$com" ]] && { match=1; break; } +done +((match)) || { echo "Unknown command $com" >&2; exit "$E_args"; } +(($#)) || arg_error 0 "$com" + +load_template + +local seq +seq="$(convert_to_seq "$1")" || set_error +shift + +local set +set="$(seq_intersection "$seq" "$(get_target_set "$com")")" +[[ -n "$set" ]] || target_error "$com" + +declare -a command_array +read -ra command_array <<<"${set_coms["$com"]}" +if [[ "$com" == exec ]]; then + exec_com_exec "$set" "$@" + exit 0 +else + (($#)) && arg_error 1 "$com" +fi + +exec_com_set "$set" "${command_array[@]}" +exit 0 +} +#=========================================================================# +exec_com_check () +# DESCRIPTION: Attempt to find and solve problems regarding +# inconsistency between the database and UUIDs present on the libvirt +# connection, files in qq2clone's pool that aren't associated with a +# machine on the connection, or invalid template XML +# INPUT: Specify a template name, or nothing to act on all templates +# OUTPUT: Prompts and status updates +# PARAMETERS: $1: (optional) template name +#=========================================================================# +{ +(($# > 1)) && arg_error 1 check +local t + +declare -a templates +if (($#)); then + check_template_exists "$1" || template_error 1 + templates[0]="$1" +else + while read -r t; do + templates=( "${templates[@]}" "$t" ) + done < <(get_template_list) +fi + +for t in "${templates[@]}"; do + echo "BEGIN CHECK TEMPLATE: $t" + echo + OPT[TEMPLATE]="$t" + load_template + if check_template; then + echo "XML: Valid" + else + echo "XML: Invalid or missing" + echo + echo "Fix manually by repairing or replacing XML file at:" + echo + echo " ${OPT[TEMPLATE_DIR]}/${t}.xml" + echo + echo "qq2clone cannot repair bad XML. Fix this problem manually, or" + echo "allow qq2clone to delete the template and all of its clones." + echo + echo "Delete $t and all of its clones?" + echo "(Backing template disk files will not be touched)" + echo + if prompt_yes_no; then + delete_template_and_clones + echo + hr + continue + fi + fi + echo + + local n sum + + sum="$(( ${#BAD_CL[@]} + ${#CL_MAP[@]} ))" + echo "TOTAL CLONES: $sum" + if (( ${#BAD_CL[@]} )); then + echo + echo "Of these clones, there are some that have either been deleted" + echo "or undefined by a tool other than qq2clone, or exist on" + echo "another libvirt URI and cannot presently be seen." + echo + echo "TOTAL MISSING CLONES: ${#BAD_CL[@]}" + prompt_delete_bad_clones + fi + echo + echo "END CHECK TEMPLATE: $t" + echo + hr + echo +done + +echo "Do a complete check for potentially orphaned images files now?" +prompt_yes_no && { echo; prompt_delete_orphans; } + +exit 0 +} +#=========================================================================# +exec_com_clone () +# DESCRIPTION: Create clones of OPT[TEMPLATE] +# INPUT: A positive number, or nothing to create 1 clone +# OUTPUT: Outputs a total at the end, error messages/exit codes as needed +# PARAMETERS: $1: (Optional) a number +#=========================================================================# +{ +(( $# < 2 )) || arg_error 1 "clone" +if (($#)); then + [[ "$1" =~ ^[0-9]+$ ]] || arg_error 2 "clone" "$1" +fi + +check_template_disks + +(( $# == 0 )) && { clone; exit 0; } + +local i +for ((i=0;i<$1;i++)); do + clone +done + +exit 0 +} +#=========================================================================# +exec_com_config () +# DESCRIPTION: View or change configuration options +# INPUT: subcommand and further arguments +# OUTPUT: Varies +# PARAMETERS: $1: Subcommand list, info, or edit, $2: option name (for +# info and edit), $3: new option value (for edit) +#=========================================================================# +{ + +if ((OPT[QUIET] == 2)); then + [[ "$1" =~ ^(list|info)$ ]] || exec &>/dev/null +fi + +(( $# )) || arg_error 0 config + +if [[ "$1" == "list" ]]; then + [[ -n "$2" ]] && arg_error 1 "config list" + disp_conf_names + exit 0 +fi + +local option +read -r option < <(echo "$2" | tr "[:lower:]" "[:upper:]") + +if [[ "$1" == "info" ]]; then + [[ -n "$2" ]] || arg_error 0 "config info" + [[ -n "$3" ]] && arg_error 1 "config info" + disp_conf_desc "$option" || exit "$E_args" + exit 0 +fi + +if [[ "$1" == "edit" ]]; then + [[ -n "$2" ]] || arg_error 0 "config edit" + [[ -n "$4" ]] && arg_error 1 "config edit" + check_config "$option" || { echo "Unknown option: $option"; + exit "$E_args"; } + local line + if (($#==3));then + line="$3" + else + ((OPT[QUIET]==2)) && exit "$E_args" + echo "Enter new value for $2 without quotes:" + read -r line + line="$(strip_ws "$line")" + fi + local retval + write_config "$option" "$line" || retval="$?" + (( retval == 1 )) && arg_error 2 "config edit" "$2" + (( retval == 2 )) && + { echo "Bad value for option $option: $line"; exit "$E_args"; } + echo "Configuration value has been saved" + exit 0 +fi + +arg_error 2 config "$1" +} +#=========================================================================# +exec_com_copy_template () +# DESCRIPTION: Copy an existing template with a new name +# INPUT: The old and new template name +# OUTPUT: Status update/error messages +# PARAMETERS: $1: Existing template, $2: Copy's name +#=========================================================================# +{ +(($# < 2)) && arg_error 0 copy-template +(($# > 2)) && arg_error 1 copy-template +local old new +old="${OPT[TEMPLATE_DIR]}/$1.xml" +new="${OPT[TEMPLATE_DIR]}/$2.xml" + +OPT[TEMPLATE]="$1" +check_template || template_error "$?" + +template_name_available "$2" + +write_pipe 1 <"${old}" +valid_xml_name_check "$2" +do_virt_xml --edit --metadata name="$2" + +write_file "$new" + +local md5 +md5="$(get_md5 "$new")" + +local disks +get_disk_devices <"$new" +disks="$(read_pipe)" + +sqlite3 "insert into TEMPLATES values ('$2','$md5','$disks','1');" + +(( OPT[QUIET] )) || + echo "Copy \"$2\" of template \"$1\" made successfully" +exit 0 +} +#=========================================================================# +exec_com_delete_template () +# DESCRIPTION: Delete an existing template +# INPUT: A template name +# OUTPUT: Status updates/error messages +# PARAMETERS: $1: Template name +#=========================================================================# +{ +(( $# )) || arg_error 0 "delete-template" +(( $# > 1 )) && arg_error 1 "delete-template" +local txml="${OPT[TEMPLATE_DIR]}/${1}.xml" +OPT[TEMPLATE]="$1" +load_template +check_template; template_error "$?" + +if [[ ! -e "$txml" ]]; then + echo "No template named $1 exists" >&2 + exit "$E_template" +fi + +local char +if ! ((OPT[QUIET])); then + echo "Are you sure you want to delete template $1? Storage volumes" + echo "will not be touched." + prompt_yes_abort || exit 0 +fi + +local check +check="$(sqlite3 "select exists \ + (select * from CLONES where template='$1');")" +if ((check));then + echo "Clones must be deleted before their parent template" + echo "This can be done with:" + echo + echo " qq2clone -t ${OPT[TEMPLATE]} rm all" + echo " qq2clone modify-template ${OPT[TEMPLATE]} discard-image" + exit "$E_args" +fi >&2; + +check_rw "$txml" +rm -f "$txml" &>/dev/null || unexpected_error exec_com_delete_template +sqlite3 "delete from TEMPLATES where name='$1';" + +((OPT[QUIET])) || echo "Template $1 deleted" +exit +} +#=========================================================================# +exec_com_edit () +# DESCRIPTION: Edit the XML of a clone +# INPUT: A machine number +# OUTPUT: Status messages/errors +# PARAMETERS: $1: A machine number +#=========================================================================# +{ + virsh edit "${CL_MAP["$1"]}" + exit 0 +} +#=========================================================================# +exec_com_exec () +# DESCRIPTION: Execute arbitrary commands in a context where $uuid, $name, +# and $disks are defined +# INPUT: A set of machines and a command string +# OUTPUT: Dependent on input +# PARAMETERS: $1: A set of machine numbers, $2 and on: arbitrary +#=========================================================================# +{ +declare -a machines +read -ra machines <<<"$1" +shift +local elem uuid disks +for elem in "${machines[@]}"; do + uuid="${CL_MAP["$elem"]}" + name="${NAME_MAP["$elem"]}" + disks="$(sqlite3 "select disks from CLONES where id='$elem' and \ + template='${OPT[TEMPLATE]}';")" + export uuid name disks + bash -ic "eval '$*'" || + { ((OPT[QUIET]==2)) || echo "Iteration of exec failed, abort" >&2; + exit "$E_args"; } +done +exit 0 +} +#=========================================================================# +exec_com_import_template () +# DESCRIPTION: Import xml from a libvirt domain, or from a file +# INPUT: A filepath, or a libvirt domain name/uuid +# OUTPUT: Status updates if not OPT["QUIET"], error messages if named +# domain/xml does not exist +# PARAMETERS: +# $1: Absolute filepath to a libvirt domain XML file, or the name/uuid of +# a domain defined on the current libvirt connection +# $2: Optional. If set, use this value for template name rather than +# the one in domain XML +#=========================================================================# +{ +(( $# < 1 )) && arg_error 0 import-template +(( $# > 2 )) && arg_error 1 import-template +local xml +import_get_xml "$1" +xml="$(read_pipe)" + +find_tag '//domain[@type="kvm"]/@type'<<<"$xml" +local match +match="$(read_pipe)" +if [[ -z "$match" ]]; then + find_tag '//domain[@type="qemu"]/@type'<<<"$xml" + match="$(read_pipe)" + if [[ -z "$match" ]]; then + echo "Domain must be of type QEMU or KVM" >&2 + exit "$E_template" + fi +fi + +write_pipe 1 <<<"$xml" +get_template_name "$2" +local xmlname name +{ +read -r xmlname +read -r name +} < <(read_pipe) + +write_pipe 1 <<<"$xml" +do_virt_xml --edit --metadata name="$name" +write_file "${OPT[TEMPLATE_DIR]}/${name}.xml" + +get_disk_devices <"${OPT[TEMPLATE_DIR]}/${name}.xml" +local disks +disks="$(read_pipe)" + +local md5 +md5="$(get_md5 "${OPT[TEMPLATE_DIR]}/${name}.xml")" + +sqlite3 "insert into TEMPLATES values ('$name','$md5','$disks','1');" + +((OPT[QUIET])) || echo "Machine imported as: \"$name\"" + +if [[ "$1" =~ ^[^/] ]] && ! (( OPT[QUIET] )) ; then + user_undefine_domain "$xmlname" +fi + +exit 0 +} +#=========================================================================# +exec_com_list () +# DESCRIPTION: List clones +# INPUT: Nothing, "all", or "xml" +# OUTPUT: A list of clones and their state +# PARAMETERS: $1: (optional) "all" to run this command for all templates, +# "xml" to produce an xml document with detailed information about +# qq2clone's overall state +#=========================================================================# +{ +(( $# > 1)) && arg_error 1 "list" +if (($#)); then + local line + if [[ "$1" == "all" ]]; then + local before=0 + while read -r line; do + ((before)) && echo; before=1 + OPT[TEMPLATE]="$line" + load_template + list_display 0 + done < <(get_template_list) + elif [[ "$1" == "xml" ]]; then + echo "" + local name value + while read -r name; do + read -r value + echo " " + done < <(sqlite3 "select name,value from CONFIG;") + echo " ${LIBVIRT_DEFAULT_URI:-missing}" + while read -r line; do + OPT[TEMPLATE]="$line" + load_template + list_display 1 + done < <(get_template_list) + echo "" + else + arg_error 2 "list" "$1" + fi +else + [[ -z "${OPT[TEMPLATE]}" ]] && + { ((OPT[QUIET])) || echo "Specify the template to list" >&2 + exit "$E_template"; } + check_template_exists || template_error 1 + load_template + list_display +fi +exit 0 +} +#=========================================================================# +exec_com_list_templates () +# DESCRIPTION: List all template names +# INPUT: None +# OUTPUT: A list of template names +# PARAMETERS: None +#=========================================================================# +{ +(($#)) && arg_error 1 list-templates +get_template_list +exit 0 +} +#=========================================================================# +exec_com_modify_template () +# DESCRIPTION: Modify image(s) of an existing template +# INPUT: Template name, subcommand, further arguments +# OUTPUT: Status updates/error messages +# PARAMETERS: $1: Template name, $2 and on: subcommand and further +# arguments, see man page or read the if/elifs below +#=========================================================================# +{ +if ((OPT[QUIET] == 2)); then + [[ "$2" == "edit" ]] || exec &>/dev/null +fi + +(($#<2)) && arg_error 0 modify-template +(($#>3)) && arg_error 1 modify-template +if (($#==3)); then + { [[ "$2" == "commit-image" ]] || [[ "$2" == "rename" ]] ; } || + arg_error 2 modify-template "$1" "$2" "$3" +fi +OPT[TEMPLATE]="$1" + +if [[ "$2" == "edit" ]]; then + edit_xml + exit 0 +fi +check_template; template_error "$?" +load_template + +local is_staging=0 +[[ -n "${CL_STATE[0]}" ]] && is_staging=1 +[[ -n "${BAD_CL[0]}" ]] && is_staging=2 + +if [[ "$2" == "prepare-image" ]]; then + OPT[NORUN]=1 + ((is_staging == 2)) && stage_error + ((is_staging)) || { clone 0; load_template; } + connect 0 + +elif [[ "$2" == "commit-image" ]]; then + ((is_staging == 2)) && stage_error + if (($#==3)); then + [[ "$3" == "force" ]] || arg_error 2 modify-template "$1" "$2" "$3" + fi + ((is_staging)) || + { echo "No changes are staged" >&2; exit "$E_args"; } + commit_image "$@" + +elif [[ "$2" == "destroy-image" ]]; then + ((is_staging == 2)) && stage_error + local state uuid + state="$(get_state 0)" + [[ "$state" == "running" ]] || + { echo "Domain is not running" >&2; exit "$E_args"; } + uuid="${CL_MAP[0]}" + virsh destroy "$uuid" &>/dev/null + +elif [[ "$2" == "discard-image" ]]; then + ((is_staging == 2)) && stage_error + ((is_staging)) || + { echo "No image to discard" >&2; exit "$E_args"; } + delete_machine 0 0 + ((OPT[QUIET])) || echo "Image discarded" + +elif [[ "$2" == rename ]]; then + (( $#==3)) || arg_error 0 "modify-template $1 $2" + rename_template "$@" +else + arg_error 2 "modify-template" "$2" +fi +exit 0 +} +#=========================================================================# +exec_com_set () +# DESCRIPTION: Any qq2clone command that acts on a set of machines, with +# the exception of exec, uses this function to iterate over all machines +# in the set +# INPUT: Set of machines to act on, command to invoke, text to display to +# user, and any additional parameters to pass on +# OUTPUT: Status updates after each iteration +# PARAMETERS: $1: the set to act on, $2: Function name, $3: Text to add +# onto status update at end of function, with _ replaced by space, $4 +# and on: passed on to function called +#=========================================================================# +{ +local set com text +set="$1"; shift +com="$1"; shift +text="$1"; shift + +declare -a machines +read -ra machines <<<"$set" + +while [[ "$text" =~ (.+)_+(.+) ]]; do + text="${BASH_REMATCH[1]} ${BASH_REMATCH[2]}" +done + +local elem +for elem in "${machines[@]}"; do + "$com" "$elem" "$@" + ((OPT[QUIET])) || + echo "${text}: ${OPT[TEMPLATE]}#${elem} ${CL_MAP["$elem"]}" +done + +exit 0 +} +#=========================================================================# +hr () +# DESCRIPTION: Horizontal rule, constructed with - +# INPUT: None +# OUTPUT: Lots of - +# PARAMETERS: None +#=========================================================================# +{ +echo ---------------------------------------------------------------------- +} +#=========================================================================# +parse_flags () +# DESCRIPTION: Check all flags and change global variables accordingly +# INPUT: exec_com passes $@ here, containing all cmd line parameters +# OUTPUT: write_pipe the number by which exec_com should shift, +# or if unknown option is encountered exit with error message/code +# PARAMETERS: $@: Parameters to exec_com, originally from command line +#=========================================================================# +{ +# Check for --quiter/-Q first +declare -a args +local d=0 +args=( "$@" ) +exec 8>&1 +exec 9>&2 + +[[ "${args[0]}" == "--quieter" ]] && args[0]="-Q" +if [[ "${args[0]}" =~ ^\-Q ]]; then + OPT[QUIET]=2 + exec &>/dev/null +fi + +local short=":c:fghnqrs:St:vV" +local long="connection=c,no-spice=f,use-spice=g,help=h," +long="${long}no-run=n,quiet=q,run=r,storage=s,spicy=S," +long="${long}template=t,verbose=v,virt-viewer=V" + +short_flags "$short" "$long" "${args[@]}" +read -ra args < <(read_pipe) +set -- "${args[@]}" + +local opt optstring +while getopts "${short}Q" opt; do + case "$opt" in + c) LIBVIRT_DEFAULT_URI="$OPTARG" + export LIBVIRT_DEFAULT_URI + virsh list &>/dev/null || + { echo "Virsh cannot connect to URI \"$OPTARG\", exiting" >&2; + exit "$E_args"; } + ;; + f) OPT[USE_SPICE]=0 + ;; + g) OPT[USE_SPICE]=1 + ;; + h) ((OPT[QUIET]==2)) && exit "$E_args" + ((OPT[QUIET]=0)); usage; exit 0 + ;; + n) OPT[NORUN]=1 + ;; + q) (( OPT[QUIET] )) || OPT[QUIET]=1 + ;; + Q) : # Handled above + ;; + r) OPT[NORUN]=0 + ;; + s) storage_opt "$OPTARG" + ;; + S) OPT[USE_SPICE]=1; OPT[SPICY]=1 + ;; + t) OPT[TEMPLATE]="$OPTARG" + check_template_exists; template_error $? + ;; + v) OPT[QUIET]=0 + ;; + V) OPT[USE_SPICE]=1; OPT[SPICY]=0 + ;; + :) [[ "$long" =~ (,|^)([^,]+)=$OPTARG ]] + optstring="--${BASH_REMATCH[2]}/-${OPTARG}" + echo "Required argument to $optstring not found" >&2 + exit "$E_args" + ;; + *) echo "Option \"$OPTARG\" is not recognized" >&2 + exit "$E_args" + esac +done + +((OPTIND+=d)) +{ [[ -z "$OPTIND" ]] || (( OPTIND < 1 )) ; } && OPTIND=1 + +if [[ "${OPT[STORAGE]}/" =~ ^("${OPT[TEMPLATE_DIR]}/") ]]; then + echo "Invalid storage location ${OPT[STORAGE]}" + exit "$E_args" +fi + +write_pipe 0 $(( OPTIND-1 )) +exec 1>&8 8>&- +exec 2>&9 9>&- + +return 0 +} +#=========================================================================# +seq_intersection () +# DESCRIPTION: Echo the intersection of two integer sets +# INPUT: Two space delimited integer sets +# OUTPUT: The intersection of the two sets, space delimited, ordered +# ascending +# PARAMETERS: $1, $2: Integer sets +#=========================================================================# +{ +local n before=0 +while read -r n; do + [[ -z "$n" ]] && continue + [[ ! " $2 " =~ [[:space:]]${n}[[:space:]] ]] && continue + ((before)) && echo -n " "; before=1 + echo -n "$n" +done < <(tr " " "\n" <<<"$1" | sort -n | uniq) +echo + +return 0 +} +#=========================================================================# +short_flags () +# DESCRIPTION: translate long option names into short ones +# INPUT: A string describing all short options, another mapping long +# names to their short equivalents, and the arguments to check +# OUTPUT: write_pipe the parameters $3 and on passed to this function, +# but with long names translated to short ones +# PARAMETERS: $1: a string of the type used by builtin getopts. Must +# include ALL options, even those without a long name counterpart +# $2: a string of the form a-long=a,b-long=b +# $3 and on: all arguments to be checked +#=========================================================================# +{ +local s_string="$1" l_string="$2" +shift 2 +declare -a short_args=( "$@" ) +local elem i + +for ((i=0;i<${#short_args[@]};i++)); do + elem="${short_args["$i"]}" + if [[ "$elem" == "--" ]]; then + break + elif [[ "$elem" =~ ^-- ]]; then + elem="${elem:2}" + if [[ "$l_string" =~ (^|,)($elem)=(.) ]]; then + elem="${BASH_REMATCH[3]}" + short_args["$i"]="-$elem" + ((i--)) + else + echo "Unknown option: $elem" >&2 + exit "$E_args" + fi + elif [[ "$elem" =~ ^- ]]; then + local len=$(( ${#elem} - 1 )) + elem="${elem:${len}}" + if [[ "$s_string" =~ $elem: ]]; then + ((i++)) + fi + else + break + fi +done + +write_pipe 0 "${short_args[*]}" +return 0 +} +#=========================================================================# +storage_opt () +# DESCRIPTION: Helper for parse_flags. Handles checking and setting user +# option defining OPT[STORAGE] +# INPUT: $OPTARG to -s option +# OUTPUT: None, except on error +# PARAMETERS: $1: argument to --storage/-s +#=========================================================================# +{ +if [[ "$1" =~ ^/ ]]; then + OPT["STORAGE"]="$1" + check_dir "$1" +else + virsh pool-info "$1" &>/dev/null || + { echo "No such pool \"$1\" exists on current libvirt connection" >&2; + exit "$E_args"; } + local line match=0 + virsh pool-dumpxml "$1" 2>/dev/null | + find_tag "//pool[@type='dir']/target/path" + while read -r line; do + match=1 + [[ "$line" =~ ^[\<\>] ]] || OPT[STORAGE]="$line" + done < <(read_pipe) + ((match)) || + { echo "Specified pool is not of type dir, so it is not supported by" + echo "qq2clone at this time" + exit "$E_args"; } >&2 +fi +return 0 +} +#=========================================================================# +strip_ws () +# DESCRIPTION: Strip any leading and trailing whitespace +# INPUT: A string +# OUTPUT: Echo the result +# PARAMETERS: $1: String to strip +#=========================================================================# +{ +local str="$1" +if [[ "$str" =~ ^[[:space:]]+([^[:space:]].*)$ ]]; then + str="${BASH_REMATCH[1]}" +fi +if [[ "$str" =~ ^(.*[^[:space:]])[[:space:]]+$ ]]; then + str="${BASH_REMATCH[1]}" +fi +echo "$str" +return 0 +} + + #-----------------# + #@@@@@@@@@@@@@@@@@# + #---ENTRY POINT---# + #@@@@@@@@@@@@@@@@@# + #-----------------# + +if ! ((QQ2_NOEXECUTE)); then + open_pipe + [[ -e "${HOME:?}/.config/qq2clone" ]] || first_run_setup + get_config + exec_com "$@" + exit 0 +fi diff --git a/qq2clone.completion b/qq2clone.completion new file mode 100755 index 0000000..a1a41f9 --- /dev/null +++ b/qq2clone.completion @@ -0,0 +1,105 @@ +#!/bin/bash +#shellcheck disable=1090 disable=2012 + +[[ -e "${HOME:?}/.config/qq2clone" ]] || return 1 +QQ2_DIR="$(<"${HOME}/.config/qq2clone")" +[[ "$QQ2_DIR" =~ ^[[:space:]]*([^[:space:]].*)$ ]] && + QQ2_DIR="${BASH_REMATCH[1]}" +[[ "$QQ2_DIR" =~ ^(.*[^[:space:]])[[:space:]]*$ ]] && + QQ2_DIR="${BASH_REMATCH[1]}" + +[[ -e "${QQ2_DIR}/sqlite3" ]] || return 1 +chmod +rw "${QQ2_DIR}/sqlite3" &>/dev/null || return 1 + +declare -a templates +declare line +while read -r line; do + templates=( "${templates[@]}" "$line" ) +done < <("${QQ2_DIR}/sqlite3" --batch "${QQ2_DIR}/qq2clone.db" \ + "select name from TEMPLATES") + +_qq2clone () +{ +declare -a COMS FLAGS +COMS=( check clone config connect copy-template delete-template destroy \ + edit exec import-template list list-templates modify-template restore \ + resume rm rm-save save shred shred-save start suspend ) +FLAGS=( connection no-spice use-spice help no-run quiet quieter run spicy \ + storage template verbose virt-viewer ) + +local LAST_ARG THIS_ARG B4_LAST_ARG P set_coms +(( COMP_CWORD > 0 )) && + LAST_ARG="${COMP_WORDS[$((COMP_CWORD - 1))]}" +(( COMP_CWORD > 1 )) && + B4_LAST_ARG="${COMP_WORDS[$((COMP_CWORD - 2))]}" +THIS_ARG="${COMP_WORDS[$COMP_CWORD]}" +set_coms="connect|destroy|exec|resume|rm|rm\-save|save|shred|shred\-save|" +set_coms="${set_coms}start|suspend|restore" +declare -a suggestions + +if [[ "$THIS_ARG" =~ ^\-\- ]]; then + suggestions=("${FLAGS[@]}") + P="--" +elif [[ "$LAST_ARG" =~ ^(\-|modify|delete|copy)\-template$ ]] || + [[ "$LAST_ARG" == "template-snapshot" ]] || + [[ "$LAST_ARG" == "-t" ]]; then + suggestions=( "${templates[@]}" ) +elif [[ "$LAST_ARG" =~ ^(\-\-storage|\-s)$ ]]; then + read -ra suggestions < <(virsh pool-list --name | tr -d '\n') +elif [[ "$LAST_ARG" == "config" ]]; then + suggestions=( list edit info ) +elif [[ "$LAST_ARG" =~ ^(${set_coms})$ ]]; then + suggestions=( all running saved off in-shutdown idle paused crashed ) + suggestions=( "${suggestions[@]}" pmsuspended ) +elif [[ "$LAST_ARG" == "list" ]] && + [[ ! "$B4_LAST_ARG" == "config" ]]; then + suggestions=( all xml ) +else + local curr_com word elem + for word in "${COMP_WORDS[@]}"; do + for elem in "${COMS[@]}"; do + [[ "$elem" == "$word" ]] && { curr_com="$word"; break 2; } + done + done + + if [[ -n "$curr_com" ]]; then + if [[ "$curr_com" == "modify-template" ]] && + [[ "${COMP_WORDS[$((COMP_CWORD - 2))]}" == "$curr_com" ]]; then + suggestions=( edit prepare-image commit-image discard-image \ + destroy-image rename ) + elif [[ "$curr_com" == config ]] && + [[ "${COMP_WORDS[$((COMP_CWORD - 2))]}" == "$curr_com" ]] && + [[ "$LAST_ARG" =~ ^(info|edit)$ ]]; then + local line + while read -r line; do + [[ "$line" =~ \ + ^[[:space:]]*([^[:space:]].*[^[:space:]])[[:space:]]*$ ]] && + line="${BASH_REMATCH[1]}" + [[ "$line" =~ ^# ]] && continue + [[ "$line" =~ ^([^=]+).* ]] && + suggestions=( "${suggestions[@]}" "${BASH_REMATCH[1]}" ) + done <"${QQ2_DIR}/config" + suggestions=(NORUN QUIET USE_SPICE SPICY STORAGE S_TIMEOUT TEMPLATE \ + TEMPLATE_DIR) + else + suggestions=( ) + fi + else + suggestions=("${COMS[@]}") + fi +fi + +local i +declare -a comp +for ((i=0;i<${#suggestions[@]};i++)); do + if [[ "${P}${suggestions["$i"]}" =~ ^"${THIS_ARG}" ]]; then + comp=( "${comp[@]}" "${suggestions["$i"]}" ) + fi +done + +(( ${#comp} > 0 )) && + +read -ra COMPREPLY < <(compgen -P "${P}" -W "${comp[*]}" | tr "\n" " ") +} + +complete -F _qq2clone qq2clone