├── .gitignore ├── .tito ├── packages │ └── virt-deploy └── tito.props ├── .travis.yml ├── COPYING ├── MANIFEST.in ├── README.rst ├── setup.py ├── tox.ini ├── virt-deploy.spec └── virtdeploy ├── __init__.py ├── cli.py ├── driverbase.py ├── drivers ├── __init__.py ├── libvirt.py └── test_libvirt.py ├── errors.py ├── test_cli.py ├── test_driverbase.py ├── test_errors.py ├── test_utils.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .tox 3 | AUTHORS 4 | ChangeLog 5 | virtdeploy.egg-info 6 | -------------------------------------------------------------------------------- /.tito/packages/virt-deploy: -------------------------------------------------------------------------------- 1 | 0.1.9-1 ./ 2 | -------------------------------------------------------------------------------- /.tito/tito.props: -------------------------------------------------------------------------------- 1 | [buildconfig] 2 | builder = tito.builder.Builder 3 | tagger = tito.tagger.VersionTagger 4 | changelog_do_not_remove_cherrypick = 0 5 | changelog_format = %s (%ae) 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 2.7 3 | sudo: false 4 | env: 5 | - TOX_ENV=py27 6 | - TOX_ENV=py34 7 | - TOX_ENV=pep8 8 | - TOX_ENV=coveralls 9 | install: 10 | - pip install tox 11 | script: 12 | - tox -e $TOX_ENV 13 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include tox.ini 2 | include virt-deploy.spec 3 | include COPYING 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Easily Deploy Virtual Machines 2 | ============================== 3 | 4 | .. image:: https://travis-ci.org/simon3z/virt-deploy.svg 5 | :target: https://travis-ci.org/simon3z/virt-deploy 6 | 7 | .. image:: https://coveralls.io/repos/simon3z/virt-deploy/badge.svg 8 | :target: https://coveralls.io/r/simon3z/virt-deploy 9 | 10 | Virt-deploy is a python library to standardize the deployment of virtual 11 | machines. It currently supports libvirt_ and takes advantage of virt-builder_ 12 | and virt-install_ to automate the creation of templates and instances. 13 | 14 | .. _libvirt: http://libvirt.org 15 | .. _virt-builder: http://libguestfs.org/virt-builder.1.html 16 | .. _virt-install: http://virt-manager.org 17 | 18 | :: 19 | 20 | usage: virt-deploy [-h] [-v] 21 | {create,start,stop,delete,templates,address,ssh} ... 22 | 23 | positional arguments: 24 | {create,start,stop,delete,templates,address,ssh} 25 | create create a new instance 26 | start start an instance 27 | stop stop an instance 28 | delete delete an instance 29 | templates list all the templates 30 | address instance ip address 31 | ssh connects to the instance 32 | 33 | optional arguments: 34 | -h, --help show this help message and exit 35 | -v, --version show program's version number and exit 36 | 37 | 38 | Creation of an Instance 39 | ======================= 40 | To create a new vm instance based on a fedora-21 template: 41 | 42 | :: 43 | 44 | # virt-deploy create instance01 fedora-21 45 | 46 | The fedora-21 template image will be downloaded (virt-builder), and prepared 47 | to be used (virt-sysprep). This is done only once when the template is used 48 | for the first time. 49 | 50 | The instance is then created with some customization such as random root 51 | password and the hostname. All the information are then summarized when 52 | the creation is completed: 53 | 54 | :: 55 | 56 | name: vm-test01-fedora-21-x86_64 57 | root password: xxxxxxxxxx 58 | mac address: 52:54:00:xx:xx:xx 59 | hostname: vm-test01 60 | ip address: 192.168.122.xxx 61 | 62 | 63 | Storage and Network Management 64 | ============================== 65 | 66 | Virt-deploy uses the 'default' libvirt storage pool and network. Images are 67 | created in the pool path and hostnames and ip addresses are assigned and 68 | registered in the network definition. 69 | 70 | 71 | Building from Sources 72 | ===================== 73 | 74 | At the moment the suggested procedure to build from sources is to produce 75 | rpms with the proper packages requirements (virt-builder and virt-install): 76 | 77 | :: 78 | 79 | $ python setup.py sdist 80 | $ sudo dnf builddep virt-deploy.spec 81 | $ rpmbuild -ta dist/virt-deploy-.tar.gz 82 | 83 | If you're a yum user (centos and fedora < 21) then you should use yum 84 | instead of dnf: 85 | 86 | :: 87 | 88 | $ sudo yum-builddep virt-deploy.spec 89 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from setuptools import find_packages 22 | from setuptools import setup 23 | 24 | 25 | setup( 26 | name='virt-deploy', 27 | description='Standardized deployment of virtual machines', 28 | author='Federico Simoncelli', 29 | author_email='fsimonce@redhat.com', 30 | url='https://github.com/simon3z/virt-deploy', 31 | version='0.1.9', 32 | packages=find_packages(), 33 | entry_points={ 34 | 'console_scripts': [ 35 | 'virt-deploy = virtdeploy.cli:main', 36 | ] 37 | }, 38 | ) 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | minversion = 1.6 3 | envlist = py27,py34,pep8,coverage 4 | 5 | [testenv] 6 | deps = 7 | lxml 8 | mock 9 | netaddr 10 | commands = 11 | python -m unittest discover 12 | 13 | [testenv:pep8] 14 | deps = 15 | flake8 16 | commands = 17 | flake8 virtdeploy 18 | 19 | [testenv:coverage] 20 | deps = 21 | {[testenv]deps} 22 | coverage==3.6 23 | commands = 24 | coverage run --source virtdeploy -m unittest discover 25 | 26 | [testenv:coveralls] 27 | passenv = 28 | TRAVIS 29 | TRAVIS_JOB_ID 30 | TRAVIS_BRANCH 31 | deps = 32 | {[testenv:coverage]deps} 33 | coveralls 34 | commands = 35 | {[testenv:coverage]commands} 36 | coveralls 37 | -------------------------------------------------------------------------------- /virt-deploy.spec: -------------------------------------------------------------------------------- 1 | %{!?python_sitelib: %global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")} 2 | 3 | Name: virt-deploy 4 | Version: 0.1.9 5 | Release: 1%{?dist} 6 | Summary: Virtual machines deployment tool 7 | 8 | License: GPLv2 9 | URL: https://github.com/simon3z/%{name} 10 | Source0: https://github.com/simon3z/%{name}/archive/%{version}/%{name}-%{version}.tar.gz 11 | 12 | BuildArch: noarch 13 | BuildRequires: python-devel 14 | BuildRequires: python-setuptools 15 | 16 | Requires: python-setuptools 17 | Requires: python-netaddr 18 | Requires: python-lxml 19 | Requires: libxml2-python 20 | Requires: qemu-img 21 | Requires: libguestfs-tools-c >= 1.23.24 22 | Requires: libguestfs-xfs 23 | Requires: virt-install 24 | Requires: libvirt-daemon-config-network 25 | Requires: libvirt-daemon-config-nwfilter 26 | 27 | %description 28 | Virtual machines deployment tool. 29 | 30 | 31 | %prep 32 | %setup -q 33 | 34 | 35 | %build 36 | %{__python} setup.py build 37 | 38 | 39 | %install 40 | rm -rf $RPM_BUILD_ROOT 41 | %{__python} setup.py install -O1 --skip-build --root $RPM_BUILD_ROOT 42 | 43 | 44 | %files 45 | %doc README.rst COPYING 46 | %{_bindir}/virt-deploy 47 | %{python_sitelib}/* 48 | 49 | 50 | %changelog 51 | * Tue Jan 31 2017 Federico Simoncelli 0.1.9-1 52 | - libvirt: automate support for centos 7.x (fsimonce@redhat.com) 53 | 54 | * Wed Jun 15 2016 Federico Simoncelli 0.1.8-1 55 | - new package built with tito 56 | 57 | * Sat Nov 14 2015 Federico Simoncelli - 0.1.7-1 58 | - update to 0.1.7 59 | 60 | * Mon Feb 2 2015 Federico Simoncelli - 0.1.6-1 61 | - update to 0.1.6 62 | 63 | * Tue Jan 20 2015 Federico Simoncelli - 0.1.5-1 64 | - update to 0.1.5 65 | 66 | * Sat Jan 17 2015 Federico Simoncelli - 0.1.4-1 67 | - update to 0.1.4 68 | 69 | * Thu Jan 15 2015 Federico Simoncelli - 0.1.3-1 70 | - update to 0.1.3 71 | 72 | * Tue Jan 13 2015 Federico Simoncelli - 0.1.2-1 73 | - update to 0.1.2 74 | 75 | * Sat Jan 3 2015 Federico Simoncelli - 0.1.1-1 76 | - initial build 77 | -------------------------------------------------------------------------------- /virtdeploy/__init__.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | DRIVERS = { 24 | 'libvirt': ('drivers.libvirt', 'VirtDeployLibvirtDriver'), 25 | } 26 | 27 | 28 | def get_driver_names(): 29 | return DRIVERS.keys() 30 | 31 | 32 | def get_driver_class(name): 33 | module = __import__(DRIVERS[name][0], globals(), locals(), name, 1) 34 | return getattr(module, DRIVERS[name][1]) 35 | 36 | 37 | def get_driver(name, args=(), kwargs={}): 38 | return get_driver_class(name)(*args, **kwargs) 39 | -------------------------------------------------------------------------------- /virtdeploy/cli.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | from __future__ import print_function 23 | 24 | import argparse 25 | import pkg_resources 26 | import subprocess 27 | import sys 28 | 29 | import virtdeploy 30 | from virtdeploy import errors 31 | from virtdeploy import utils 32 | 33 | DRIVER = 'libvirt' 34 | 35 | EXITCODE_SUCCESS = 0 36 | EXITCODE_FAILURE = 1 37 | EXITCODE_TIMEOUT = 124 38 | EXITCODE_KEYBINT = 130 39 | 40 | 41 | def instance_create(args): 42 | driver = virtdeploy.get_driver(DRIVER) 43 | instance = driver.instance_create(args.id, args.template) 44 | 45 | print('name: {0}'.format(instance['name'])) 46 | print('root password: {0}'.format(instance['password'])) 47 | print('mac address: {0}'.format(instance['mac'])) 48 | print('hostname: {0}'.format(instance['hostname'])) 49 | print('ip address: {0}'.format(instance['ipaddress'])) 50 | 51 | 52 | def instance_start(args): 53 | driver = virtdeploy.get_driver(DRIVER) 54 | driver.instance_start(args.name) 55 | 56 | if args.wait: 57 | address_found = utils.wait_tcp_access(driver, args.name) 58 | if address_found is None: 59 | return EXITCODE_TIMEOUT 60 | 61 | return EXITCODE_SUCCESS 62 | 63 | 64 | def instance_stop(args): 65 | driver = virtdeploy.get_driver(DRIVER) 66 | return driver.instance_stop(args.name) 67 | 68 | 69 | def instance_delete(args): 70 | driver = virtdeploy.get_driver(DRIVER) 71 | return driver.instance_delete(args.name) 72 | 73 | 74 | def template_list(args): 75 | driver = virtdeploy.get_driver(DRIVER) 76 | for template in driver.template_list(): 77 | print(u'{0:24}{1:24}'.format(template['id'], template['name'])) 78 | 79 | 80 | def instance_address(args): 81 | driver = virtdeploy.get_driver(DRIVER) 82 | print('\n'.join(driver.instance_address(args.name))) 83 | 84 | 85 | def command_ssh(args): 86 | driver = virtdeploy.get_driver(DRIVER) 87 | 88 | command = ['ssh', '-A', 89 | '-o', 'StrictHostKeychecking=no', 90 | '-o', 'UserKnownHostsFile=/dev/null', 91 | '-o', 'LogLevel=QUIET'] 92 | 93 | user, _, name = args.name.rpartition('@') 94 | 95 | if user: 96 | command.extend(('-l', user)) 97 | 98 | command.append(driver.instance_address(name)[0]) 99 | command.extend(args.arguments) 100 | 101 | return subprocess.call(command) 102 | 103 | 104 | COMMAND_TABLE = { 105 | 'create': instance_create, 106 | 'start': instance_start, 107 | 'stop': instance_stop, 108 | 'delete': instance_delete, 109 | 'templates': template_list, 110 | 'address': instance_address, 111 | 'ssh': command_ssh, 112 | } 113 | 114 | 115 | def parse_command_line(cmdline): 116 | parser = argparse.ArgumentParser() 117 | 118 | version = pkg_resources.get_distribution('virt-deploy').version 119 | parser.add_argument('-v', '--version', action='version', 120 | version='%(prog)s {0}'.format(version)) 121 | 122 | cmd = parser.add_subparsers(dest='command') 123 | 124 | cmd_create = cmd.add_parser('create', help='create a new instance') 125 | cmd_create.add_argument('id', help='new instance id') 126 | cmd_create.add_argument('template', help='template id') 127 | 128 | cmd_start = cmd.add_parser('start', help='start an instance') 129 | cmd_start.add_argument('--wait', action='store_true', 130 | help='wait for ssh access availability') 131 | cmd_start.add_argument('name', help='name of instance to start') 132 | 133 | cmd_stop = cmd.add_parser('stop', help='stop an instance') 134 | cmd_stop.add_argument('name', help='name of instance to stop') 135 | 136 | cmd_delete = cmd.add_parser('delete', help='delete an instance') 137 | cmd_delete.add_argument('name', help='name of instance to delete') 138 | 139 | cmd.add_parser('templates', help='list all the templates') 140 | 141 | cmd_address = cmd.add_parser('address', help='instance ip address') 142 | cmd_address.add_argument('name', help='instance name') 143 | 144 | cmd_ssh = cmd.add_parser('ssh', help='connects to the instance') 145 | cmd_ssh.add_argument('name', help='instance name') 146 | cmd_ssh.add_argument('arguments', nargs='*', help='ssh arguments') 147 | 148 | args = parser.parse_args(args=cmdline) 149 | return COMMAND_TABLE[args.command](args) 150 | 151 | 152 | def main(): 153 | try: 154 | return parse_command_line(sys.argv[1:]) 155 | except errors.VirtDeployException as e: 156 | print('error: {0}'.format(e), file=sys.stderr) 157 | raise SystemExit(EXITCODE_FAILURE) 158 | except KeyboardInterrupt: 159 | raise SystemExit(EXITCODE_KEYBINT) 160 | -------------------------------------------------------------------------------- /virtdeploy/driverbase.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | 24 | class VirtDeployDriverBase(object): 25 | def template_list(self): 26 | raise NotImplementedError('template_list') 27 | 28 | def instance_create(self, vmid, template, **kwargs): 29 | raise NotImplementedError('instance_create') 30 | 31 | def instance_address(self, vmid, network=None): 32 | raise NotImplementedError('instance_address') 33 | 34 | def instance_start(self, vmid): 35 | raise NotImplementedError('instance_start') 36 | 37 | def instance_stop(self, vmid): 38 | raise NotImplementedError('instance_stop') 39 | 40 | def instance_delete(self, vmid): 41 | raise NotImplementedError('instance_delete') 42 | -------------------------------------------------------------------------------- /virtdeploy/drivers/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/simon3z/virt-deploy/25aeb94b21575340447da3fcf0461e250c162e16/virtdeploy/drivers/__init__.py -------------------------------------------------------------------------------- /virtdeploy/drivers/libvirt.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import json 24 | import libvirt 25 | import netaddr 26 | import os 27 | import os.path 28 | import subprocess 29 | 30 | from lxml import etree 31 | 32 | from ..driverbase import VirtDeployDriverBase 33 | from ..errors import InstanceNotFound 34 | from ..errors import VirtDeployException 35 | from ..utils import execute 36 | from ..utils import random_password 37 | 38 | DEFAULT_NET = 'default' 39 | DEFAULT_POOL = 'default' 40 | 41 | BASE_FORMAT = 'qcow2' 42 | BASE_SIZE = '20G' 43 | 44 | INSTANCE_DEFAULTS = { 45 | 'cpus': 2, 46 | 'memory': 1024, 47 | 'arch': 'x86_64', 48 | 'network': DEFAULT_NET, 49 | 'pool': DEFAULT_POOL, 50 | 'password': None, 51 | } 52 | 53 | _NET_ADD_LAST = libvirt.VIR_NETWORK_UPDATE_COMMAND_ADD_LAST 54 | _NET_MODIFY = libvirt.VIR_NETWORK_UPDATE_COMMAND_MODIFY 55 | _NET_DELETE = libvirt.VIR_NETWORK_UPDATE_COMMAND_DELETE 56 | _NET_DNS_HOST = libvirt.VIR_NETWORK_SECTION_DNS_HOST 57 | _NET_DHCP_HOST = libvirt.VIR_NETWORK_SECTION_IP_DHCP_HOST 58 | _NET_UPDATE_FLAGS = ( 59 | libvirt.VIR_NETWORK_UPDATE_AFFECT_CONFIG | 60 | libvirt.VIR_NETWORK_UPDATE_AFFECT_LIVE 61 | ) 62 | 63 | _IMAGE_OS_TABLE = { 64 | 'centos-6': 'centos6.6', # TODO: fix versions 65 | } 66 | 67 | 68 | class VirtDeployLibvirtDriver(VirtDeployDriverBase): 69 | def __init__(self, uri='qemu:///system'): 70 | self._uri = uri 71 | 72 | def _libvirt_open(self): 73 | def libvirt_callback(ctx, err): 74 | pass # add logging only when required 75 | 76 | libvirt.registerErrorHandler(libvirt_callback, ctx=None) 77 | return libvirt.open(self._uri) 78 | 79 | def template_list(self): 80 | templates = _get_virt_templates() 81 | 82 | if templates['version'] != 1: 83 | raise VirtDeployException('Unsupported template list version') 84 | 85 | return [{'id': x['os-version'], 'name': x['full-name']} 86 | for x in templates['templates']] 87 | 88 | def instance_create(self, vmid, template, **kwargs): 89 | kwargs = dict(INSTANCE_DEFAULTS.items() + kwargs.items()) 90 | 91 | name = '{0}-{1}-{2}'.format(vmid, template, kwargs['arch']) 92 | image = '{0}.qcow2'.format(name) 93 | 94 | conn = self._libvirt_open() 95 | pool = conn.storagePoolLookupByName(kwargs['pool']) 96 | net = conn.networkLookupByName(kwargs['network']) 97 | 98 | repository = _get_pool_path(pool) 99 | path = os.path.join(repository, image) 100 | 101 | if os.path.exists(path): 102 | raise OSError(os.errno.EEXIST, "Image already exists") 103 | 104 | base = _create_base(template, kwargs['arch'], repository) 105 | 106 | execute(('qemu-img', 'create', '-f', 'qcow2', '-b', base, image), 107 | cwd=repository) 108 | 109 | hostname = 'vm-{0}'.format(vmid) 110 | 111 | domainname = _get_network_domainname(net) 112 | 113 | if domainname is None: 114 | fqdn = hostname 115 | else: 116 | fqdn = '{0}.{1}'.format(hostname, domainname) 117 | 118 | if kwargs['password'] is None: 119 | kwargs['password'] = random_password() 120 | 121 | password_string = 'password:{0}'.format(kwargs['password']) 122 | 123 | execute(('virt-customize', 124 | '-a', path, 125 | '--hostname', fqdn, 126 | '--root-password', password_string)) 127 | 128 | network = 'network={0}'.format(kwargs['network']) 129 | 130 | try: 131 | conn.nwfilterLookupByName('clean-traffic') 132 | except libvirt.libvirtError as e: 133 | if e.get_error_code() != libvirt.VIR_ERR_NO_NWFILTER: 134 | raise 135 | else: 136 | network += ',filterref=clean-traffic' 137 | 138 | disk = 'path={0},format=qcow2,bus=scsi,discard=unmap'.format(path) 139 | channel = 'unix,name=org.qemu.guest_agent.0' 140 | 141 | execute(('virt-install', 142 | '--quiet', 143 | '--connect={0}'.format(self._uri), 144 | '--name', name, 145 | '--cpu', 'host-model-only,+vmx', 146 | '--vcpus', str(kwargs['cpus']), 147 | '--memory', str(kwargs['memory']), 148 | '--controller', 'scsi,model=virtio-scsi', 149 | '--disk', disk, 150 | '--network', network, 151 | '--graphics', 'spice', 152 | '--channel', channel, 153 | '--os-variant', _get_image_os(template), 154 | '--import', 155 | '--noautoconsole', 156 | '--noreboot')) 157 | 158 | netmac = _get_domain_mac_addresses(_get_domain(conn, name)).next() 159 | ipaddress = _new_network_ipaddress(net) 160 | 161 | # TODO: fix race between _new_network_ipaddress and ip reservation 162 | _add_network_host(net, hostname, ipaddress) 163 | _add_network_dhcp_host(net, hostname, netmac['mac'], ipaddress) 164 | 165 | return { 166 | 'name': name, 167 | 'password': kwargs['password'], 168 | 'mac': netmac['mac'], 169 | 'hostname': fqdn, 170 | 'ipaddress': ipaddress, 171 | } 172 | 173 | def instance_address(self, vmid, network=None): 174 | conn = self._libvirt_open() 175 | dom = _get_domain(conn, vmid) 176 | 177 | netmacs = _get_domain_macs_by_network(dom) 178 | 179 | if network: 180 | netmacs = {k: v for k, v in netmacs.iteritems()} 181 | 182 | addresses = set() 183 | 184 | for name, macs in netmacs.iteritems(): 185 | net = conn.networkLookupByName(name) 186 | 187 | for lease in _get_network_dhcp_leases(net): 188 | if lease['mac'] in macs: 189 | addresses.add(lease['ip']) 190 | 191 | return list(addresses) 192 | 193 | def instance_start(self, vmid): 194 | dom = _get_domain(self._libvirt_open(), vmid) 195 | 196 | try: 197 | dom.create() 198 | except libvirt.libvirtError as e: 199 | if e.get_error_code() != libvirt.VIR_ERR_OPERATION_INVALID: 200 | raise 201 | 202 | def instance_stop(self, vmid): 203 | dom = _get_domain(self._libvirt_open(), vmid) 204 | 205 | try: 206 | dom.shutdownFlags( 207 | libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT | 208 | libvirt.VIR_DOMAIN_SHUTDOWN_ACPI_POWER_BTN 209 | ) 210 | except libvirt.libvirtError as e: 211 | if e.get_error_code() != libvirt.VIR_ERR_OPERATION_INVALID: 212 | raise 213 | 214 | def instance_delete(self, vmid): 215 | conn = self._libvirt_open() 216 | dom = _get_domain(conn, vmid) 217 | 218 | try: 219 | dom.destroy() 220 | except libvirt.libvirtError as e: 221 | if e.get_error_code() != libvirt.VIR_ERR_OPERATION_INVALID: 222 | raise 223 | 224 | xmldesc = etree.fromstring(dom.XMLDesc()) 225 | 226 | for disk in xmldesc.iterfind('./devices/disk/source'): 227 | try: 228 | os.remove(disk.get('file')) 229 | except OSError as e: 230 | if e.errno != os.errno.ENOENT: 231 | raise 232 | 233 | netmacs = _get_domain_macs_by_network(dom) 234 | 235 | for network, macs in netmacs.iteritems(): 236 | net = conn.networkLookupByName(network) 237 | 238 | for x in _get_network_dhcp_hosts(net): 239 | if x['mac'] in macs: 240 | _del_network_host(net, x['name']) 241 | _del_network_dhcp_host(net, x['name']) 242 | 243 | dom.undefineFlags(libvirt.VIR_DOMAIN_UNDEFINE_SNAPSHOTS_METADATA) 244 | 245 | 246 | def _get_image_os(image): 247 | if image.startswith('centos-7'): 248 | return 'centos7.0' 249 | try: 250 | return _IMAGE_OS_TABLE[image] 251 | except KeyError: 252 | return image.replace('-', '') 253 | 254 | 255 | def _create_base(template, arch, repository): 256 | name = '_{0}-{1}.{2}'.format(template, arch, BASE_FORMAT) 257 | path = os.path.join(repository, name) 258 | 259 | if not os.path.exists(path): 260 | execute(('virt-builder', template, 261 | '-o', path, 262 | '--size', BASE_SIZE, 263 | '--format', BASE_FORMAT, 264 | '--arch', arch, 265 | '--root-password', 'locked:disabled')) 266 | 267 | # As mentioned in the virt-builder man in section "CLONES" the 268 | # resulting image should be cleaned before bsing used as template. 269 | # TODO: handle half-backed templates 270 | execute(('virt-sysprep', '-a', path)) 271 | 272 | return name 273 | 274 | 275 | def _get_virt_templates(): 276 | stdout, _ = execute(('virt-builder', '-l', '--list-format', 'json'), 277 | stdout=subprocess.PIPE) 278 | return json.loads(stdout) 279 | 280 | 281 | def _get_domain(conn, name): 282 | try: 283 | return conn.lookupByName(name) 284 | except libvirt.libvirtError as e: 285 | if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN: 286 | raise InstanceNotFound(name) 287 | raise 288 | 289 | 290 | def _get_domain_mac_addresses(dom): 291 | xmldesc = etree.fromstring(dom.XMLDesc()) 292 | netxpath = './devices/interface[@type="network"]' 293 | 294 | for iface in xmldesc.iterfind(netxpath): 295 | network = iface.find('./source').get('network') 296 | mac = iface.find('./mac').get('address') 297 | 298 | yield {'mac': mac, 'network': network} 299 | 300 | 301 | def _get_domain_macs_by_network(dom): 302 | netmacs = {} 303 | 304 | for x in _get_domain_mac_addresses(dom): 305 | netmacs.setdefault(x['network'], []).append(x['mac']) 306 | 307 | return netmacs 308 | 309 | 310 | def _get_pool_path(pool): 311 | xmldesc = etree.fromstring(pool.XMLDesc()) 312 | 313 | for x in xmldesc.iterfind('.[@type="dir"]/target/path'): 314 | return x.text 315 | 316 | raise OSError(os.errno.ENOENT, 'Path not found for pool') 317 | 318 | 319 | def _get_network_domainname(net): 320 | xmldesc = etree.fromstring(net.XMLDesc()) 321 | 322 | for domain in xmldesc.iterfind('./domain'): 323 | return domain.get('name') 324 | 325 | 326 | def _add_network_host(net, hostname, ipaddress): 327 | xmlhost = etree.Element('host') 328 | xmlhost.set('ip', ipaddress) 329 | etree.SubElement(xmlhost, 'hostname').text = hostname 330 | 331 | # Attempt to delete if present 332 | _del_network_host(net, hostname) 333 | net.update(_NET_ADD_LAST, _NET_DNS_HOST, 0, etree.tostring(xmlhost), 334 | _NET_UPDATE_FLAGS) 335 | 336 | 337 | def _del_network_host(net, hostname): 338 | xmlhost = etree.Element('host') 339 | etree.SubElement(xmlhost, 'hostname').text = hostname 340 | 341 | try: 342 | net.update(_NET_DELETE, _NET_DNS_HOST, 0, etree.tostring(xmlhost), 343 | _NET_UPDATE_FLAGS) 344 | except libvirt.libvirtError as e: 345 | if e.get_error_code() != libvirt.VIR_ERR_OPERATION_INVALID: 346 | raise 347 | 348 | 349 | def _add_network_dhcp_host(net, hostname, mac, ipaddress): 350 | xmlhost = etree.Element('host') 351 | xmlhost.set('mac', mac) 352 | xmlhost.set('name', hostname) 353 | xmlhost.set('ip', ipaddress) 354 | 355 | # Attempt to delete if present 356 | _del_network_dhcp_host(net, hostname) 357 | net.update(_NET_ADD_LAST, _NET_DHCP_HOST, 0, etree.tostring(xmlhost), 358 | _NET_UPDATE_FLAGS) 359 | 360 | 361 | def _del_network_dhcp_host(net, hostname): 362 | xmlhost = etree.Element('host') 363 | xmlhost.set('name', hostname) 364 | 365 | try: 366 | net.update(_NET_DELETE, _NET_DHCP_HOST, 0, etree.tostring(xmlhost), 367 | _NET_UPDATE_FLAGS) 368 | except libvirt.libvirtError as e: 369 | if e.get_error_code() != libvirt.VIR_ERR_OPERATION_INVALID: 370 | raise 371 | 372 | 373 | def _get_network_dhcp_hosts(net): 374 | xmldesc = etree.fromstring(net.XMLDesc()) 375 | 376 | for x in xmldesc.iterfind('./ip/dhcp/host'): 377 | yield {'name': x.get('name'), 'mac': x.get('mac'), 378 | 'ip': x.get('ip')} 379 | 380 | 381 | def _get_network_dhcp_leases(net): 382 | for x in _get_network_dhcp_hosts(net): 383 | yield x 384 | 385 | for x in net.DHCPLeases(): 386 | yield {'name': x['hostname'], 'mac': x['mac'], 387 | 'ip': x['ipaddr']} 388 | 389 | 390 | def _new_network_ipaddress(net): 391 | xmldesc = etree.fromstring(net.XMLDesc()) 392 | 393 | hosts = _get_network_dhcp_leases(net) 394 | addresses = set(netaddr.IPAddress(x['ip']) for x in hosts) 395 | 396 | localip = xmldesc.find('./ip').get('address') 397 | netmask = xmldesc.find('./ip').get('netmask') 398 | 399 | addresses.add(netaddr.IPAddress(localip)) 400 | 401 | for ip in netaddr.IPNetwork(localip, netmask)[1:-1]: 402 | if ip not in addresses: 403 | return str(ip) 404 | -------------------------------------------------------------------------------- /virtdeploy/drivers/test_libvirt.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import errno 24 | import types 25 | import unittest 26 | 27 | from mock import MagicMock 28 | from mock import patch 29 | 30 | from ..errors import VirtDeployException 31 | 32 | 33 | class libvirtErrorMock(Exception): 34 | def __init__(self, code): 35 | self.code = code 36 | 37 | def get_error_code(self): 38 | return self.code 39 | 40 | 41 | class LibvirtMock(types.ModuleType, object): 42 | VIR_NETWORK_UPDATE_COMMAND_ADD_LAST = 3 43 | VIR_NETWORK_UPDATE_COMMAND_DELETE = 2 44 | VIR_NETWORK_UPDATE_COMMAND_MODIFY = 1 45 | VIR_NETWORK_SECTION_DNS_HOST = 10 46 | VIR_NETWORK_SECTION_IP_DHCP_HOST = 4 47 | VIR_NETWORK_UPDATE_AFFECT_CONFIG = 2 48 | VIR_NETWORK_UPDATE_AFFECT_LIVE = 1 49 | VIR_ERR_OPERATION_INVALID = 55 50 | 51 | libvirtError = libvirtErrorMock 52 | 53 | 54 | libvirt_mock = LibvirtMock('libvirt') 55 | _driver = None 56 | 57 | 58 | def module_mock(): 59 | global _driver 60 | 61 | if _driver is None: 62 | with patch.dict('sys.modules', {'libvirt': libvirt_mock}): 63 | _driver = __import__('libvirt', globals(), locals(), 64 | ['libvirt'], 1) 65 | 66 | return _driver 67 | 68 | 69 | def XMLDescMock(xmldesc=None): 70 | return MagicMock(**{'XMLDesc.return_value': xmldesc}) 71 | 72 | 73 | class TestImageOS(unittest.TestCase): 74 | def test_get_image_os(self): 75 | image_oses = ( 76 | ('centos-6', 'centos6.5'), 77 | ('centos-7.0', 'centos7.0'), 78 | ('centos-7.1', 'centos7.0'), 79 | ('centos-7.2', 'centos7.0'), 80 | ('fedora-20', 'fedora20'), 81 | ('fedora-21', 'fedora21'), 82 | ('fedora-22', 'fedora22'), 83 | ('fedora-23', 'fedora23'), 84 | ) 85 | 86 | for image, os in image_oses: 87 | self.assertEqual(os, module_mock()._get_image_os(image)) 88 | 89 | 90 | class TestNetwork(unittest.TestCase): 91 | NETXML_DOMAIN = """\ 92 | 93 | 94 | 95 | """ 96 | 97 | NETXML_DOMAIN_EMPTY = """\ 98 | 99 | 100 | 101 | """ 102 | 103 | NETXML_DOMAIN_MISSING = """\ 104 | 105 | 106 | 107 | """ 108 | 109 | def test_network_name(self): 110 | net = XMLDescMock(self.NETXML_DOMAIN) 111 | 112 | name = module_mock()._get_network_domainname(net) 113 | 114 | net.XMLDesc.assert_called_with() 115 | self.assertEqual(name, 'mydomain.example.com') 116 | 117 | def test_network_name_empty(self): 118 | net = XMLDescMock(self.NETXML_DOMAIN_EMPTY) 119 | 120 | name = module_mock()._get_network_domainname(net) 121 | 122 | net.XMLDesc.assert_called_with() 123 | self.assertIs(name, None) 124 | 125 | def test_network_name_missing(self): 126 | net = XMLDescMock(self.NETXML_DOMAIN_MISSING) 127 | 128 | name = module_mock()._get_network_domainname(net) 129 | 130 | net.XMLDesc.assert_called_with() 131 | self.assertIs(name, None) 132 | 133 | 134 | class TestStorage(unittest.TestCase): 135 | POOLXML_PATH_DIR = """\ 136 | 137 | 138 | /var/lib/libvirt/images 139 | 140 | 141 | """ 142 | 143 | POOLXML_PATH_ISCSI = """\ 144 | 145 | 146 | /var/lib/libvirt/images 147 | 148 | 149 | """ 150 | 151 | def test_pool_path_dir(self): 152 | pool = XMLDescMock(self.POOLXML_PATH_DIR) 153 | 154 | path = module_mock()._get_pool_path(pool) 155 | 156 | pool.XMLDesc.assert_called_with() 157 | self.assertEqual(path, '/var/lib/libvirt/images') 158 | 159 | def test_pool_path_iscsi(self): 160 | pool = XMLDescMock(self.POOLXML_PATH_ISCSI) 161 | 162 | with self.assertRaises(OSError) as cm: 163 | module_mock()._get_pool_path(pool) 164 | 165 | self.assertEqual(cm.exception.errno, errno.ENOENT) 166 | 167 | 168 | class TestDomain(unittest.TestCase): 169 | DOMXML_ONE_MACADDR = """\ 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | """ 179 | 180 | DOMXML_MULTI_MACADDR = """\ 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | """ 198 | 199 | def test_get_domain_one_mac_addresses(self): 200 | domain = XMLDescMock(self.DOMXML_ONE_MACADDR) 201 | 202 | macs = list(module_mock()._get_domain_mac_addresses(domain)) 203 | 204 | domain.XMLDesc.assert_called_with() 205 | self.assertEqual(macs, [ 206 | {'mac': '52:54:00:a0:b0:01', 'network': 'default'}, 207 | ]) 208 | 209 | def test_get_domain_multi_mac_addresses(self): 210 | domain = XMLDescMock(self.DOMXML_MULTI_MACADDR) 211 | 212 | macs = list(module_mock()._get_domain_mac_addresses(domain)) 213 | 214 | domain.XMLDesc.assert_called_with() 215 | self.assertEqual(macs, [ 216 | {'mac': '52:54:00:a0:b0:01', 'network': 'default'}, 217 | {'mac': '52:54:00:a0:b0:02', 'network': 'default'}, 218 | {'mac': '52:54:00:a0:b0:03', 'network': 'othernet1'}, 219 | ]) 220 | 221 | def test_get_domain_macs_by_network(self): 222 | domain = XMLDescMock(self.DOMXML_MULTI_MACADDR) 223 | 224 | netmacs = module_mock()._get_domain_macs_by_network(domain) 225 | 226 | domain.XMLDesc.assert_called_with() 227 | self.assertEqual(netmacs, { 228 | 'default': ['52:54:00:a0:b0:01', '52:54:00:a0:b0:02'], 229 | 'othernet1': ['52:54:00:a0:b0:03'], 230 | }) 231 | 232 | 233 | class TestNetworkDhcpHosts(unittest.TestCase): 234 | NETXML_DHCP = """\ 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | """ 245 | 246 | NETXML_DHCP_EXPECTED = [ 247 | {'mac': '52:54:00:a1:b2:01', 'name': 'test01', 248 | 'ip': '192.168.122.2'}, 249 | {'mac': '52:54:00:a1:b2:02', 'name': 'test02', 250 | 'ip': '192.168.122.3'}, 251 | {'mac': '52:54:00:a1:b2:03', 'name': None, 252 | 'ip': '192.168.122.4'}, 253 | ] 254 | 255 | NETXML_LEASES = [ 256 | {'hostname': 'lease04', 'mac': '52:54:00:a1:b2:01', 257 | 'ipaddr': '192.168.122.5'}, 258 | {'hostname': 'lease05', 'mac': '52:54:00:a1:b2:02', 259 | 'ipaddr': '192.168.122.6'}, 260 | {'hostname': None, 'mac': '52:54:00:a1:b2:03', 261 | 'ipaddr': '192.168.122.7'}, 262 | ] 263 | 264 | NETXML_LEASES_EXPECTED = [ 265 | {'mac': '52:54:00:a1:b2:01', 'name': 'lease04', 266 | 'ip': '192.168.122.5'}, 267 | {'mac': '52:54:00:a1:b2:02', 'name': 'lease05', 268 | 'ip': '192.168.122.6'}, 269 | {'mac': '52:54:00:a1:b2:03', 'name': None, 270 | 'ip': '192.168.122.7'}, 271 | ] 272 | 273 | NETXML_DHCP_EMPTY = """\ 274 | 275 | 276 | 277 | 278 | 279 | """ 280 | 281 | NETXML_DHCP_MISSING = """\ 282 | 283 | 284 | 285 | """ 286 | 287 | def test_dhcp_hosts(self): 288 | net = XMLDescMock(self.NETXML_DHCP) 289 | 290 | hosts = list(module_mock()._get_network_dhcp_hosts(net)) 291 | 292 | net.XMLDesc.assert_called_with() 293 | self.assertEqual(hosts, self.NETXML_DHCP_EXPECTED) 294 | 295 | def test_dhcp_hosts_empty(self): 296 | net = XMLDescMock(self.NETXML_DHCP_EMPTY) 297 | 298 | hosts = list(module_mock()._get_network_dhcp_hosts(net)) 299 | 300 | net.XMLDesc.assert_called_with() 301 | self.assertEqual(hosts, list()) 302 | 303 | def test_dhcp_hosts_missing(self): 304 | net = XMLDescMock(self.NETXML_DHCP_MISSING) 305 | 306 | hosts = list(module_mock()._get_network_dhcp_hosts(net)) 307 | 308 | net.XMLDesc.assert_called_with() 309 | self.assertEqual(hosts, list()) 310 | 311 | def test_add_dhcp_host(self): 312 | net = MagicMock() 313 | 314 | module_mock()._add_network_dhcp_host( 315 | net, 'test01', '52:54:00:a1:b2:01', '192.168.122.2') 316 | 317 | expected_xml = ('') 319 | net.update.assert_called_with(3, 4, 0, expected_xml.encode(), 3) 320 | 321 | def test_del_dhcp_host(self): 322 | net = MagicMock() 323 | 324 | module_mock()._del_network_dhcp_host(net, 'test01') 325 | 326 | expected_xml = '' 327 | net.update.assert_called_with(2, 4, 0, expected_xml.encode(), 3) 328 | 329 | def test_del_dhcp_host_failure_raised(self): 330 | net = MagicMock() 331 | net.update.side_effect = libvirtErrorMock(1) 332 | 333 | with self.assertRaises(libvirtErrorMock): 334 | module_mock()._del_network_dhcp_host(net, '192.168.122.2') 335 | 336 | def test_del_dhcp_host_failure_caught(self): 337 | net = MagicMock() 338 | net.update.side_effect = libvirtErrorMock(55) 339 | module_mock()._del_network_dhcp_host(net, '192.168.122.2') 340 | 341 | def test_get_dhcp_leases(self): 342 | net = XMLDescMock(self.NETXML_DHCP) 343 | net.DHCPLeases.return_value = self.NETXML_LEASES 344 | 345 | hosts = list(module_mock()._get_network_dhcp_leases(net)) 346 | 347 | net.DHCPLeases.assert_called_with() 348 | self.assertEqual(hosts, (self.NETXML_DHCP_EXPECTED + 349 | self.NETXML_LEASES_EXPECTED)) 350 | 351 | def test_new_network_ipaddress(self): 352 | net = XMLDescMock(self.NETXML_DHCP) 353 | net.DHCPLeases.return_value = self.NETXML_LEASES 354 | 355 | ipaddress = module_mock()._new_network_ipaddress(net) 356 | 357 | self.assertEqual(ipaddress, '192.168.122.8') 358 | 359 | 360 | class TestNetworkDnsHosts(unittest.TestCase): 361 | def test_add_dns_host(self): 362 | net = MagicMock() 363 | 364 | module_mock()._add_network_host(net, 'test01', '192.168.122.2') 365 | 366 | expected_xml = ('' 367 | 'test01') 368 | net.update.assert_called_with(3, 10, 0, expected_xml.encode(), 3) 369 | 370 | def test_del_dns_host(self): 371 | net = MagicMock() 372 | 373 | module_mock()._del_network_host(net, 'test01') 374 | 375 | expected_xml = 'test01' 376 | net.update.assert_called_with(2, 10, 0, expected_xml.encode(), 3) 377 | 378 | def test_del_dns_failure_raised(self): 379 | net = MagicMock() 380 | net.update.side_effect = libvirtErrorMock(1) 381 | 382 | with self.assertRaises(libvirtErrorMock): 383 | module_mock()._del_network_host(net, '192.168.122.2') 384 | 385 | def test_del_dns_failure_caught(self): 386 | net = MagicMock() 387 | net.update.side_effect = libvirtErrorMock(55) 388 | 389 | module_mock()._del_network_host(net, '192.168.122.2') 390 | 391 | 392 | class TestVirtBuilderTemplates(unittest.TestCase): 393 | VIRTBUILD_JSON = """\ 394 | { 395 | "version": 1, 396 | "templates": [ 397 | { "os-version": "centos-6", "full-name": "CentOS 6.6" }, 398 | { "os-version": "centos-7.0", "full-name": "CentOS 7.0" } 399 | ] 400 | } 401 | """ 402 | 403 | VIRTBUILD_JSON_FUTURE = """\ 404 | { 405 | "version": 2 406 | } 407 | """ 408 | 409 | def test_template_list(self): 410 | driver = module_mock().VirtDeployLibvirtDriver() 411 | 412 | with patch.object(module_mock(), 'execute') as execute_mock: 413 | execute_mock.return_value = (self.VIRTBUILD_JSON, '') 414 | templates = driver.template_list() 415 | 416 | self.assertEqual(templates, [ 417 | {'id': 'centos-6', 'name': 'CentOS 6.6'}, 418 | {'id': 'centos-7.0', 'name': 'CentOS 7.0'}, 419 | ]) 420 | 421 | def test_template_list_unsupported(self): 422 | driver = module_mock().VirtDeployLibvirtDriver() 423 | 424 | with patch.object(module_mock(), 'execute') as execute_mock: 425 | execute_mock.return_value = (self.VIRTBUILD_JSON_FUTURE, '') 426 | 427 | with self.assertRaises(VirtDeployException): 428 | driver.template_list() 429 | -------------------------------------------------------------------------------- /virtdeploy/errors.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | 24 | class VirtDeployException(Exception): 25 | def __init__(self, message="Unknown error"): 26 | self.message = message 27 | 28 | def __str__(self): 29 | return self.message 30 | 31 | 32 | class InstanceNotFound(VirtDeployException): 33 | def __init__(self, name): 34 | super(InstanceNotFound, self).__init__( 35 | 'No such instance: {0}'.format(name)) 36 | -------------------------------------------------------------------------------- /virtdeploy/test_cli.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import sys 24 | import unittest 25 | 26 | from mock import patch 27 | 28 | from . import cli 29 | from . import errors 30 | 31 | 32 | if sys.version_info[0] == 3: # pragma: no cover 33 | from io import StringIO 34 | else: # pragma: no cover 35 | from StringIO import StringIO 36 | 37 | 38 | class TestCommandLine(unittest.TestCase): 39 | HELP_OUTPUT = """\ 40 | usage: python -m unittest [-h] [-v] 41 | {create,start,stop,delete,templates,address,ssh} ... 42 | 43 | positional arguments: 44 | {create,start,stop,delete,templates,address,ssh} 45 | create create a new instance 46 | start start an instance 47 | stop stop an instance 48 | delete delete an instance 49 | templates list all the templates 50 | address instance ip address 51 | ssh connects to the instance 52 | 53 | optional arguments: 54 | -h, --help show this help message and exit 55 | -v, --version show program's version number and exit 56 | """ 57 | 58 | def test_help(self): 59 | with patch('sys.stdout', new=StringIO()) as stdout_mock: 60 | with self.assertRaises(SystemExit) as cm: 61 | cli.parse_command_line(['--help']) 62 | 63 | self.assertEqual(cm.exception.code, 0) 64 | self.assertEqual(stdout_mock.getvalue(), self.HELP_OUTPUT) 65 | 66 | def test_main_success(self): 67 | with patch.object(sys, 'argv', []): 68 | with patch('virtdeploy.cli.parse_command_line') as func_mock: 69 | cli.main() 70 | func_mock.assert_called_once_with([]) 71 | 72 | @patch('sys.stderr') 73 | def test_main_failure(self, stderr_mock): 74 | with patch('virtdeploy.cli.parse_command_line') as func_mock: 75 | func_mock.side_effect = errors.VirtDeployException 76 | 77 | with self.assertRaises(SystemExit) as cm: 78 | cli.main() 79 | 80 | self.assertEqual(cm.exception.code, 1) 81 | 82 | def test_main_interrupt(self): 83 | with patch('virtdeploy.cli.parse_command_line') as func_mock: 84 | func_mock.side_effect = KeyboardInterrupt 85 | 86 | with self.assertRaises(SystemExit) as cm: 87 | cli.main() 88 | 89 | self.assertEqual(cm.exception.code, cli.EXITCODE_KEYBINT) 90 | 91 | @patch('sys.stdout') 92 | @patch('virtdeploy.get_driver') 93 | def test_instance_create(self, driver_mock, stdout_mock): 94 | instance_create = driver_mock.return_value.instance_create 95 | instance_create.return_value = { 96 | 'name': 'test01', 97 | 'password': 'password', 98 | 'mac': '52:54:00:a0:b0:01', 99 | 'hostname': 'vm-test01.example.com', 100 | 'ipaddress': '192.168.122.2', 101 | } 102 | 103 | cli.parse_command_line(['create', 'test01', 'base01']) 104 | 105 | driver_mock.assert_called_with('libvirt') 106 | instance_create.assert_called_with('test01', 'base01') 107 | 108 | @patch('sys.stderr') 109 | @patch('virtdeploy.get_driver') 110 | def test_instance_create_fail1(self, driver_mock, stderr_mock): 111 | with self.assertRaises(SystemExit) as cm: 112 | cli.parse_command_line(['create', 'test01']) 113 | 114 | self.assertEqual(cm.exception.code, 2) 115 | 116 | @patch('sys.stderr') 117 | @patch('virtdeploy.get_driver') 118 | def test_instance_create_fail2(self, driver_mock, stderr_mock): 119 | with self.assertRaises(SystemExit) as cm: 120 | cli.parse_command_line(['create']) 121 | 122 | self.assertEqual(cm.exception.code, 2) 123 | 124 | @patch('virtdeploy.get_driver') 125 | def test_instance_delete(self, driver_mock): 126 | instance_delete = driver_mock.return_value.instance_delete 127 | 128 | cli.parse_command_line(['delete', 'test01']) 129 | 130 | driver_mock.assert_called_with('libvirt') 131 | instance_delete.assert_called_with('test01') 132 | 133 | @patch('sys.stdout') 134 | @patch('virtdeploy.get_driver') 135 | def test_instance_address(self, driver_mock, stdout_mock): 136 | instance_address = driver_mock.return_value.instance_address 137 | 138 | cli.parse_command_line(['address', 'test01']) 139 | 140 | driver_mock.assert_called_with('libvirt') 141 | instance_address.assert_called_with('test01') 142 | 143 | @patch('virtdeploy.get_driver') 144 | def test_instance_start(self, driver_mock): 145 | instance_start = driver_mock.return_value.instance_start 146 | 147 | cli.parse_command_line(['start', 'test01']) 148 | 149 | driver_mock.assert_called_with('libvirt') 150 | instance_start.assert_called_with('test01') 151 | 152 | @patch('virtdeploy.get_driver') 153 | @patch('virtdeploy.utils.wait_tcp_access') 154 | def test_instance_start_wait_success(self, wait_mock, driver_mock): 155 | instance_start = driver_mock.return_value.instance_start 156 | wait_mock.return_value = '192.168.122.2' 157 | 158 | cli.parse_command_line(['start', '--wait', 'test01']) 159 | 160 | driver_mock.assert_called_with('libvirt') 161 | instance_start.assert_called_with('test01') 162 | wait_mock.assert_called_with(driver_mock.return_value, 'test01') 163 | 164 | @patch('virtdeploy.get_driver') 165 | @patch('virtdeploy.utils.wait_tcp_access') 166 | def test_instance_start_wait_fail(self, wait_mock, driver_mock): 167 | instance_start = driver_mock.return_value.instance_start 168 | wait_mock.return_value = None 169 | 170 | cli.parse_command_line(['start', '--wait', 'test01']) 171 | 172 | driver_mock.assert_called_with('libvirt') 173 | instance_start.assert_called_with('test01') 174 | wait_mock.assert_called_with(driver_mock.return_value, 'test01') 175 | 176 | @patch('virtdeploy.get_driver') 177 | def test_instance_stop(self, driver_mock): 178 | instance_stop = driver_mock.return_value.instance_stop 179 | 180 | cli.parse_command_line(['stop', 'test01']) 181 | 182 | driver_mock.assert_called_with('libvirt') 183 | instance_stop.assert_called_with('test01') 184 | 185 | @patch('sys.stdout') 186 | @patch('virtdeploy.get_driver') 187 | def test_template_list(self, driver_mock, stdout_mock): 188 | template_list = driver_mock.return_value.template_list 189 | template_list.return_value = [ 190 | {'id': 'centos-6', 'name': 'CentOS 6'}, 191 | {'id': 'centos-7', 'name': 'CentOS 7'}, 192 | ] 193 | 194 | cli.parse_command_line(['templates']) 195 | 196 | driver_mock.assert_called_with('libvirt') 197 | template_list.assert_called_with() 198 | 199 | @patch('virtdeploy.get_driver') 200 | def test_instance_ssh(self, driver_mock): 201 | instance_address = driver_mock.return_value.instance_address 202 | instance_address.return_value = ['192.168.122.2'] 203 | 204 | with patch('subprocess.call') as call_mock: 205 | cli.parse_command_line(['ssh', 'test01']) 206 | 207 | call_mock.assert_called_with(['ssh', '-A', 208 | '-o', 'StrictHostKeychecking=no', 209 | '-o', 'UserKnownHostsFile=/dev/null', 210 | '-o', 'LogLevel=QUIET', 211 | '192.168.122.2']) 212 | 213 | @patch('virtdeploy.get_driver') 214 | def test_instance_ssh_user(self, driver_mock): 215 | instance_address = driver_mock.return_value.instance_address 216 | instance_address.return_value = ['192.168.122.3'] 217 | 218 | with patch('subprocess.call') as call_mock: 219 | cli.parse_command_line(['ssh', 'root@test02']) 220 | 221 | call_mock.assert_called_with(['ssh', '-A', 222 | '-o', 'StrictHostKeychecking=no', 223 | '-o', 'UserKnownHostsFile=/dev/null', 224 | '-o', 'LogLevel=QUIET', 225 | '-l', 'root', 226 | '192.168.122.3']) 227 | -------------------------------------------------------------------------------- /virtdeploy/test_driverbase.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import sys 24 | import inspect 25 | import unittest 26 | 27 | from mock import patch 28 | 29 | from . import get_driver 30 | from . import get_driver_class 31 | from . import get_driver_names 32 | 33 | from .driverbase import VirtDeployDriverBase 34 | from .drivers import test_libvirt 35 | 36 | 37 | class TestVirtDeployDriverBase(unittest.TestCase): 38 | def _get_driver_methods(self): 39 | return inspect.getmembers(VirtDeployDriverBase, inspect.ismethod) 40 | 41 | def _get_driver_class(self, name): 42 | with patch.dict(sys.modules, {'libvirt': test_libvirt.libvirt_mock}): 43 | return get_driver_class(name) 44 | 45 | def _get_driver(self, name): 46 | with patch.dict(sys.modules, {'libvirt': test_libvirt.libvirt_mock}): 47 | return get_driver(name) 48 | 49 | def test_base_not_implemented(self): 50 | driver = VirtDeployDriverBase() 51 | 52 | for name, method in self._get_driver_methods(): 53 | spec = inspect.getargspec(method) 54 | 55 | with self.assertRaises(NotImplementedError) as cm: 56 | getattr(driver, name)(*(None,) * (len(spec.args) - 1)) 57 | 58 | self.assertEqual(cm.exception.args[0], name) 59 | 60 | def test_drivers_interface(self): 61 | for driver_name in get_driver_names(): 62 | driver = self._get_driver_class(driver_name) 63 | 64 | for name, method in self._get_driver_methods(): 65 | driver_method = getattr(driver, name) 66 | self.assertNotEqual(driver_method, method) 67 | self.assertEqual(inspect.getargspec(method), 68 | inspect.getargspec(driver_method)) 69 | 70 | def test_get_drivers(self): 71 | for driver_name in get_driver_names(): 72 | driver = self._get_driver(driver_name) 73 | self.assertTrue(isinstance(driver, VirtDeployDriverBase)) 74 | -------------------------------------------------------------------------------- /virtdeploy/test_errors.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import unittest 24 | 25 | from . import errors 26 | 27 | 28 | class TestCommandLine(unittest.TestCase): 29 | def test_instance_not_found(self): 30 | self.assertEqual(str(errors.InstanceNotFound('test01')), 31 | 'No such instance: test01') 32 | -------------------------------------------------------------------------------- /virtdeploy/test_utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import unittest 24 | 25 | from mock import MagicMock 26 | from mock import call 27 | from mock import patch 28 | from socket import SOL_SOCKET 29 | from socket import SO_ERROR 30 | from subprocess import CalledProcessError 31 | 32 | from . import utils 33 | 34 | 35 | class TestRandomPassword(unittest.TestCase): 36 | def test_random_password(self): 37 | for size in range(6, 24): 38 | self.assertEqual(len(utils.random_password(size=size)), size) 39 | 40 | 41 | class TestExecute(unittest.TestCase): 42 | def test_execute_success(self): 43 | command = ('command', 'arg1', 'arg2') 44 | optargs = {'stdout': 1, 'stderr': 2, 'cwd': '/path'} 45 | outputs = ('hello stdout', 'hello stderr') 46 | 47 | with patch('subprocess.Popen') as popen_mock: 48 | popen_mock.return_value.communicate.return_value = outputs 49 | popen_mock.return_value.returncode = 0 50 | 51 | self.assertEqual(utils.execute(command, **optargs), outputs) 52 | 53 | popen_mock.assert_called_once_with(command, **optargs) 54 | 55 | def test_execute_failure(self): 56 | command = ('command', 'arg1', 'arg2') 57 | optargs = {'stdout': 1, 'stderr': 2, 'cwd': '/path'} 58 | outputs = ('', 'command error output') 59 | 60 | with patch('subprocess.Popen') as popen_mock: 61 | popen_mock.return_value.communicate.return_value = outputs 62 | popen_mock.return_value.returncode = 1 63 | 64 | with self.assertRaises(CalledProcessError) as cm: 65 | utils.execute(command, **optargs) 66 | 67 | self.assertEqual(cm.exception.returncode, 1) 68 | 69 | 70 | class TestMonotonicTime(unittest.TestCase): 71 | def test_monotonic_time(self): 72 | self.assertEqual(type(utils.monotonic_time()), float) 73 | 74 | 75 | class TestWaitTcpAccess(unittest.TestCase): 76 | TIMEOUT = 180.0 77 | MININT = 5.0 78 | HALFMININT = MININT / 2.0 79 | MAXINT = 10.0 80 | 81 | EXERCISES = ( 82 | { 83 | 'monotime': [0, 0, 0, TIMEOUT], 84 | 'probearg': [MAXINT], 85 | 'proberet': [None], 86 | 'retvalue': None, 87 | 'sleepexp': [call(MININT)], 88 | }, 89 | { 90 | 'monotime': [0, 0, (MININT - HALFMININT), TIMEOUT], 91 | 'probearg': [MAXINT], 92 | 'proberet': [None], 93 | 'retvalue': None, 94 | 'sleepexp': [call(HALFMININT)], 95 | }, 96 | { 97 | 'monotime': [0, 0, MININT, TIMEOUT], 98 | 'probearg': [MAXINT], 99 | 'proberet': [None], 100 | 'retvalue': None, 101 | 'sleepexp': [call(0)], 102 | }, 103 | { 104 | 'monotime': [0, 0, (MININT + 1.0), TIMEOUT], 105 | 'probearg': [MAXINT], 106 | 'proberet': [None], 107 | 'retvalue': None, 108 | 'sleepexp': [call(0)], 109 | }, 110 | { 111 | 'monotime': [0, 0, (MININT - HALFMININT), (TIMEOUT - MININT), 112 | (TIMEOUT - MININT + HALFMININT), TIMEOUT], 113 | 'probearg': [MAXINT, MININT], 114 | 'proberet': [None, None], 115 | 'retvalue': None, 116 | 'sleepexp': [call(HALFMININT)], 117 | }, 118 | { 119 | 'monotime': [0, 0, MININT, MAXINT], 120 | 'probearg': [MAXINT], 121 | 'proberet': [None, '192.168.122.2'], 122 | 'retvalue': '192.168.122.2', 123 | 'sleepexp': [call(0)], 124 | }, 125 | ) 126 | 127 | @patch('time.sleep') 128 | @patch('virtdeploy.utils.monotonic_time') 129 | @patch('virtdeploy.utils.probe_tcp_access') 130 | def test_wait_tcp_access(self, probe_mock, monotime_mock, sleep_mock): 131 | for exercise in self.EXERCISES: 132 | probe_mock.side_effect = exercise['proberet'] 133 | monotime_mock.side_effect = exercise['monotime'] 134 | 135 | retvalue = utils.wait_tcp_access(None, None, 136 | timeout=self.TIMEOUT, 137 | mininterval=self.MININT, 138 | maxinterval=self.MAXINT) 139 | 140 | probe_mock.called_with(timeout=exercise['probearg']) 141 | self.assertEqual(retvalue, exercise['retvalue']) 142 | self.assertEqual(sleep_mock.mock_calls, exercise['sleepexp']) 143 | 144 | sleep_mock.reset_mock() 145 | 146 | 147 | class TestProbeTcpAccess(unittest.TestCase): 148 | TIMEOUT = 10 149 | 150 | def setUp(self): 151 | self.addresses = ['192.168.122.2', '192.168.122.3'] 152 | self.sockets = [MagicMock() for _ in self.addresses] 153 | self.driver_mock = MagicMock() 154 | self.driver_mock.instance_address.return_value = self.addresses 155 | 156 | @patch('select.select') 157 | @patch('socket.socket') 158 | @patch('virtdeploy.utils.monotonic_time') 159 | def test_probe_timeout(self, time_mock, socket_mock, select_mock): 160 | socket_mock.side_effect = self.sockets 161 | time_mock.side_effect = [0, 0, self.TIMEOUT / 2, self.TIMEOUT] 162 | select_mock.return_value = (), (), () 163 | 164 | retvalue = utils.probe_tcp_access(self.driver_mock, None, 165 | timeout=self.TIMEOUT) 166 | 167 | self.assertEqual(retvalue, None) 168 | 169 | self._assert_sockets_calls() 170 | 171 | # FIXME: mock should record the collection snapshot, not the 172 | # collection object, code should be: 173 | # call((), self.sockets, (), self.TIMEOUT) 174 | select_mock.assert_has_calls([ 175 | call((), [], (), self.TIMEOUT), 176 | call((), [], (), self.TIMEOUT / 2), 177 | ]) 178 | 179 | @patch('select.select') 180 | @patch('socket.socket') 181 | @patch('virtdeploy.utils.monotonic_time') 182 | def test_probe_success(self, time_mock, socket_mock, select_mock): 183 | sock = self.sockets[0] 184 | 185 | sock.getsockopt.return_value = 0 186 | sock.getpeername.return_value = self.addresses[0] 187 | 188 | socket_mock.side_effect = self.sockets 189 | time_mock.side_effect = [0, 0] 190 | select_mock.return_value = (), (sock,), () 191 | 192 | retvalue = utils.probe_tcp_access(self.driver_mock, None, 193 | timeout=self.TIMEOUT) 194 | 195 | self.assertEqual(retvalue, self.addresses[0]) 196 | 197 | self._assert_sockets_calls() 198 | 199 | sock.getsockopt.assert_called_with(SOL_SOCKET, SO_ERROR) 200 | 201 | def _assert_sockets_calls(self): 202 | for sock, address in zip(self.sockets, self.addresses): 203 | sock.setblocking.assert_called_once_with(0) 204 | sock.connect_ex.assert_called_once_with((address, 22)) 205 | sock.close.assert_called_once_with() 206 | -------------------------------------------------------------------------------- /virtdeploy/utils.py: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2015 Red Hat, Inc. 3 | # 4 | # This program is free software; you can redistribute it and/or modify 5 | # it under the terms of the GNU General Public License as published by 6 | # the Free Software Foundation; either version 2 of the License, or 7 | # (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 17 | # 18 | # Refer to the README and COPYING files for full details of the license 19 | # 20 | 21 | from __future__ import absolute_import 22 | 23 | import os 24 | import random 25 | import select 26 | import socket 27 | import string 28 | import subprocess 29 | import time 30 | 31 | _PASSWORD_CHARS = string.ascii_letters + string.digits + '!#$%&' 32 | 33 | 34 | def execute(args, stdout=None, stderr=None, cwd=None): 35 | p = subprocess.Popen(args, stdout=stdout, stderr=stderr, cwd=cwd) 36 | 37 | out, err = p.communicate() 38 | 39 | if p.returncode != 0: 40 | raise subprocess.CalledProcessError(p.returncode, args) 41 | 42 | return out, err 43 | 44 | 45 | def random_password(size=12): 46 | chars = (random.choice(_PASSWORD_CHARS) for _ in range(size)) 47 | return ''.join(chars) 48 | 49 | 50 | def monotonic_time(): 51 | return os.times()[4] 52 | 53 | 54 | def probe_tcp_access(driver, vmid, port=22, timeout=10): 55 | sockets = list() 56 | endtime = monotonic_time() + timeout 57 | 58 | for address in driver.instance_address(vmid): 59 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 60 | sock.setblocking(0) 61 | sock.connect_ex((address, port)) 62 | sockets.append(sock) 63 | 64 | address_found = None 65 | 66 | while sockets and address_found is None: 67 | remaining = endtime - monotonic_time() 68 | 69 | if remaining <= 0: 70 | break 71 | 72 | _, response, _ = select.select((), sockets, (), remaining) 73 | 74 | for sock in response: 75 | e = sock.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) 76 | 77 | if e == 0: 78 | address_found = sock.getpeername() 79 | 80 | sock.close() 81 | sockets.remove(sock) 82 | 83 | for sock in list(sockets): 84 | sock.close() 85 | sockets.remove(sock) 86 | 87 | return address_found 88 | 89 | 90 | def wait_tcp_access(driver, vmid, port=22, timeout=180, 91 | mininterval=5.0, maxinterval=10.0): 92 | endtime = monotonic_time() + timeout 93 | 94 | while True: 95 | probetime = monotonic_time() 96 | remaining = min(maxinterval, endtime - probetime) 97 | 98 | if remaining <= 0: 99 | return None 100 | 101 | address_found = probe_tcp_access(driver, vmid, port, remaining) 102 | 103 | if address_found is not None: 104 | return address_found 105 | 106 | nexttry = probetime + mininterval 107 | 108 | if nexttry >= endtime: 109 | return None 110 | 111 | time.sleep(max(0, nexttry - monotonic_time())) 112 | --------------------------------------------------------------------------------