Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Javier Collado | ||||
Approved revision: | 816 | ||||
Merged at revision: | 790 | ||||
Proposed branch: | lp:~nuclearbob/utah/reorg | ||||
Merge into: | lp:utah | ||||
Diff against target: |
2747 lines (+1107/-1358) 20 files modified
conf/config (+1/-1) conf/utah/uqt-vm-tools.conf (+0/-60) debian/control (+25/-1) debian/rules (+35/-8) debian/utah.postinst (+0/-1) docs/source/reference.rst (+9/-3) examples/run_install_test.py (+1/-2) examples/run_test_bamboo_feeder.py (+2/-1) examples/run_test_cobbler.py (+5/-2) examples/run_test_vm.py (+1/-1) examples/utah-user-setup.sh (+0/-7) examples/vmtools-user-setup.sh (+0/-59) utah/provisioning/baremetal/bamboofeeder.py (+3/-3) utah/provisioning/baremetal/cobbler.py (+3/-3) utah/provisioning/baremetal/inventory.py (+153/-0) utah/provisioning/inventory/sqlite.py (+0/-183) utah/provisioning/provisioning.py (+3/-243) utah/provisioning/ssh.py (+261/-0) utah/provisioning/vm/libvirtvm.py (+0/-777) utah/provisioning/vm/vm.py (+605/-3) |
||||
To merge this branch: | bzr merge lp:~nuclearbob/utah/reorg | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Javier Collado (community) | Approve | ||
Review via email:
|
Commit message
Description of the change
This branch reorganizes the provisioning code.
SSHMixin now lives in ssh.py
TinySQLiteInventory was moved into vm.py since it's only useful for VMs.
vmtools support was removed. We can add uvt support later if we need it.
Everything from libvirtvm.py was moved into vm.py since we don't support any other VM types.
ManualBaremetal
The baremetal directory is now several packages: utah-baremetal, utah-cobbler, and utah-bamboofeeder. utah-cobbler and utah-bamboofeeder depend on utah-baremetal, and utah-all installs every utah package (including the parser.)
I've built and installed the packages and tested that VM provisioning still works as expected. I'm still working out a way to test a new package on physical installs without disrupting the ongoing tests in magners, but we can test those packages separately later if we need to. The code in them hasn't changed for the most part, it's just moved around a bit.
Preview Diff
1 | === modified file 'conf/config' | |||
2 | --- conf/config 2012-07-11 13:56:31 +0000 | |||
3 | +++ conf/config 2012-12-13 00:38:21 +0000 | |||
4 | @@ -1,1 +1,1 @@ | |||
6 | 1 | IdentityFile %d/.ssh/%u | 1 | IdentityFile %d/.ssh/utah |
7 | 2 | 2 | ||
8 | === removed file 'conf/utah/uqt-vm-tools.conf' | |||
9 | --- conf/utah/uqt-vm-tools.conf 2012-11-07 19:52:52 +0000 | |||
10 | +++ conf/utah/uqt-vm-tools.conf 1970-01-01 00:00:00 +0000 | |||
11 | @@ -1,60 +0,0 @@ | |||
12 | 1 | # list of all active releases (included devel) | ||
13 | 2 | vm_release_list="hardy lucid oneiric precise quantal raring" | ||
14 | 3 | |||
15 | 4 | # used by vm-repo (ie 'umt repo' puts stuff in /var/www/debs/testing/..., so | ||
16 | 5 | # vm_repo_url should be the URL to those files. The IP of the host is by | ||
17 | 6 | # default 192.168.122.1, and guests are 192.168.122.2-254. | ||
18 | 7 | vm_repo_url="http://192.168.122.1/debs/testing" | ||
19 | 8 | |||
20 | 9 | # vm-tools specific settings | ||
21 | 10 | vm_path="/var/lib/utah/vm" # where to store the VM images | ||
22 | 11 | vm_aptproxy="" # set if you want to use a local proxy (like, say, apt-cacher-ng) | ||
23 | 12 | vm_mirror="http://us.archive.ubuntu.com/ubuntu" | ||
24 | 13 | vm_security_mirror="" # set if want to use a local mirror for security | ||
25 | 14 | vm_mirror_host="us.archive.ubuntu.com" # Used with the mini iso | ||
26 | 15 | vm_mirror_dir="/ubuntu" # Used with the mini iso | ||
27 | 16 | vm_dir_iso="/var/cache/utah/iso" # set to directory containing .iso images | ||
28 | 17 | vm_dir_iso_cache="/var/cache/utah/iso/cache" # set to directory for preseeded iso cache | ||
29 | 18 | vm_image_size="8" # size in GB of vm-new disk images | ||
30 | 19 | vm_memory="512" # 384 is needed for desktops, 256 for servers | ||
31 | 20 | vm_ssh_key=~/.ssh/utah.pub # defaults to $HOME/.ssh/id_rsa.pub | ||
32 | 21 | vm_connect="qemu:///system" | ||
33 | 22 | vm_flavor="" # blank for default, set to override (eg 'rt') | ||
34 | 23 | vm_archs="i386 amd64" # architectures to use when using '-p PREFIX' | ||
35 | 24 | # with some commands | ||
36 | 25 | vm_extra_packages="python-yaml bzr git" # list of packages to also | ||
37 | 26 | # install via postinstall.sh | ||
38 | 27 | vm_username="utah" # defaults to your username (`whoami`) | ||
39 | 28 | vm_password="ubuntu" # defaults to "ubuntu" | ||
40 | 29 | vm_latecmd="" # allows specifying an additional late command | ||
41 | 30 | |||
42 | 31 | vm_root_size="4096" # Used by deprecated vm-new-vmbuilder tool | ||
43 | 32 | vm_swap_size="1024" # Used by deprecated vm-new-vmbuilder tool | ||
44 | 33 | |||
45 | 34 | # vm-iso specific settings (also uses vm-tools settings) | ||
46 | 35 | vm_iso_ndisks="1" # number of disks | ||
47 | 36 | vm_iso_vcpus="1" # number of virtual CPUs | ||
48 | 37 | vm_iso_ostype="linux" | ||
49 | 38 | vm_iso_osvariant="ubuntuLucid" | ||
50 | 39 | # see 'man virt-install' for details on 'sparse', 'format' and 'cache' | ||
51 | 40 | vm_iso_fully_allocate="yes" # fully allocate the disk image (usu. faster) | ||
52 | 41 | vm_iso_disk_format="qcow2" # raw, qcow2, vmdk, etc | ||
53 | 42 | vm_iso_disk_cache="none" # none, writethrough, writeback | ||
54 | 43 | |||
55 | 44 | # vm-new locale | ||
56 | 45 | vm_locale="en_US.UTF-8" | ||
57 | 46 | |||
58 | 47 | # vm-new keyboard layout | ||
59 | 48 | vm_setkeyboard="false" # set to "true" to enable the custom settings below | ||
60 | 49 | vm_xkbmodel="pc105" | ||
61 | 50 | vm_xkblayout="ca" | ||
62 | 51 | vm_xkbvariant="" | ||
63 | 52 | vm_xkboptions="lv3:ralt_switch" | ||
64 | 53 | |||
65 | 54 | # Use an alternate viewer such as gvncviewer or xtightvncviewer. Defaults to | ||
66 | 55 | # virt-viewer if unset. You can specify arguments to the viewer here. | ||
67 | 56 | #vm_viewer="xvnc4viewer" | ||
68 | 57 | #vm_viewer_args="" | ||
69 | 58 | |||
70 | 59 | # Set to 'no' to disable '.local' mDNS (avahi) lookups for VMs | ||
71 | 60 | vm_host_use_avahi="yes" | ||
72 | 61 | 0 | ||
73 | === modified file 'debian/control' | |||
74 | --- debian/control 2012-12-11 14:28:43 +0000 | |||
75 | +++ debian/control 2012-12-13 00:38:21 +0000 | |||
76 | @@ -18,10 +18,34 @@ | |||
77 | 18 | python-netifaces, python-paramiko, python-psutil, | 18 | python-netifaces, python-paramiko, python-psutil, |
78 | 19 | utah-client (=${binary:Version}) | 19 | utah-client (=${binary:Version}) |
79 | 20 | Recommends: dl-ubuntu-test-iso, kvm | 20 | Recommends: dl-ubuntu-test-iso, kvm |
80 | 21 | Suggests: cobbler, u-boot-tools, vm-tools | ||
81 | 22 | Description: Ubuntu Test Automation Harness | 21 | Description: Ubuntu Test Automation Harness |
82 | 23 | Automation framework for testing in Ubuntu | 22 | Automation framework for testing in Ubuntu |
83 | 24 | 23 | ||
84 | 24 | Package: utah-all | ||
85 | 25 | Architecture: all | ||
86 | 26 | Depends: ${misc:Depends}, ${python:Depends}, utah-bamboofeeder, utah-cobbler, | ||
87 | 27 | utah-parser | ||
88 | 28 | Description: Ubuntu Test Automation Harness Complete Package | ||
89 | 29 | Automation framework for testing in Ubuntu, all sections | ||
90 | 30 | |||
91 | 31 | Package: utah-bamboofeeder | ||
92 | 32 | Architecture: all | ||
93 | 33 | Depends: ${misc:Depends}, ${python:Depends}, u-boot-tools, utah-baremetal | ||
94 | 34 | Description: Ubuntu Test Automation Harness Bamboo Feeder Support | ||
95 | 35 | Automation framework for testing in Ubuntu, bamboo feeder portion | ||
96 | 36 | |||
97 | 37 | Package: utah-baremetal | ||
98 | 38 | Architecture: all | ||
99 | 39 | Depends: ${misc:Depends}, ${python:Depends}, utah | ||
100 | 40 | Description: Ubuntu Test Automation Harness Bare Metal Support | ||
101 | 41 | Automation framework for testing in Ubuntu, bare metal portion | ||
102 | 42 | |||
103 | 43 | Package: utah-cobbler | ||
104 | 44 | Architecture: all | ||
105 | 45 | Depends: ${misc:Depends}, ${python:Depends}, cobbler, utah-baremetal | ||
106 | 46 | Description: Ubuntu Test Automation Harness Cobbler Support | ||
107 | 47 | Automation framework for testing in Ubuntu, cobbler portion | ||
108 | 48 | |||
109 | 25 | Package: utah-client | 49 | Package: utah-client |
110 | 26 | Architecture: all | 50 | Architecture: all |
111 | 27 | Depends: ${misc:Depends}, ${python:Depends}, | 51 | Depends: ${misc:Depends}, ${python:Depends}, |
112 | 28 | 52 | ||
113 | === modified file 'debian/rules' | |||
114 | --- debian/rules 2012-11-30 14:03:47 +0000 | |||
115 | +++ debian/rules 2012-12-13 00:38:21 +0000 | |||
116 | @@ -22,6 +22,7 @@ | |||
117 | 22 | # utah should only install the provisioning subdirectory, and utah-client should install everything else | 22 | # utah should only install the provisioning subdirectory, and utah-client should install everything else |
118 | 23 | # except parser.py, which is now its own package | 23 | # except parser.py, which is now its own package |
119 | 24 | # and __init__.py, which is now in the common package | 24 | # and __init__.py, which is now in the common package |
120 | 25 | # The baremetal subdirectory in the provisioning directory is now 3 packages | ||
121 | 25 | # If we just use dh_auto_install, all packages will get everything | 26 | # If we just use dh_auto_install, all packages will get everything |
122 | 26 | # We start by building the whole thing | 27 | # We start by building the whole thing |
123 | 27 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --install-layout=deb --root=$(CURDIR); done | 28 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --install-layout=deb --root=$(CURDIR); done |
124 | @@ -37,6 +38,8 @@ | |||
125 | 37 | rm -r build/*/utah/* | 38 | rm -r build/*/utah/* |
126 | 38 | # And we put provisioning back | 39 | # And we put provisioning back |
127 | 39 | mv provisioning build/*/utah | 40 | mv provisioning build/*/utah |
128 | 41 | # But remove baremetal | ||
129 | 42 | mv build/*/utah/provisioning/baremetal . | ||
130 | 40 | # Now we install just the provisioning directory, again using --skip-build into the utah package tree | 43 | # Now we install just the provisioning directory, again using --skip-build into the utah package tree |
131 | 41 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah; done | 44 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah; done |
132 | 42 | # And now we put back the parser and install that | 45 | # And now we put back the parser and install that |
133 | @@ -47,24 +50,48 @@ | |||
134 | 47 | rm -r build/*/utah/* | 50 | rm -r build/*/utah/* |
135 | 48 | mv __init__.py build/*/utah | 51 | mv __init__.py build/*/utah |
136 | 49 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-common; done | 52 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-common; done |
137 | 53 | # Now we install just baremetal | ||
138 | 54 | rm -r build/*/utah/* | ||
139 | 55 | mkdir provisioning | ||
140 | 56 | mv baremetal provisioning | ||
141 | 57 | mv provisioning build/*/utah | ||
142 | 58 | # But without cobbler | ||
143 | 59 | mv build/*/utah/provisioning/baremetal/cobbler.py . | ||
144 | 60 | # And without bamboofeeder | ||
145 | 61 | mv build/*/utah/provisioning/baremetal/bamboofeeder.py . | ||
146 | 62 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-baremetal; done | ||
147 | 63 | # Now just cobbler | ||
148 | 64 | rm -r build/*/utah/provisioning/baremetal/* | ||
149 | 65 | mv cobbler.py build/*/utah/provisioning/baremetal | ||
150 | 66 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-cobbler; done | ||
151 | 67 | # Now just bamboofeeder | ||
152 | 68 | rm -r build/*/utah/provisioning/baremetal/* | ||
153 | 69 | mv bamboofeeder.py build/*/utah/provisioning/baremetal | ||
154 | 70 | set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-bamboofeeder; done | ||
155 | 50 | # Since the client changes names from client.py to utah, we can't use utah-client.install for that | 71 | # Since the client changes names from client.py to utah, we can't use utah-client.install for that |
156 | 51 | # We also need to make our directory since we do this before dh_auto_install | 72 | # We also need to make our directory since we do this before dh_auto_install |
157 | 52 | mkdir -p $(CURDIR)/debian/utah-client/usr/bin | 73 | mkdir -p $(CURDIR)/debian/utah-client/usr/bin |
158 | 53 | cp -aL client.py $(CURDIR)/debian/utah-client/usr/bin/utah | 74 | cp -aL client.py $(CURDIR)/debian/utah-client/usr/bin/utah |
159 | 54 | # phoenix.py needs to be in here to lose the .py | 75 | # phoenix.py needs to be in here to lose the .py |
160 | 55 | cp -aL utah/client/phoenix.py $(CURDIR)/debian/utah-client/usr/bin/phoenix | 76 | cp -aL utah/client/phoenix.py $(CURDIR)/debian/utah-client/usr/bin/phoenix |
168 | 56 | # Since utah and utah-client both got installed using python distutils, they both get an egg info directory | 77 | # Since all packages get installed using python distutils, they all get an egg info directory |
169 | 57 | # This will conflict if we leave it in both of them, so we remove it from the client | 78 | # This will conflict if we leave it in all of them, so we remove it from everything but common |
170 | 58 | rm -r $(CURDIR)/debian/utah-client/usr/lib/python*/dist-packages/utah-*.egg-info | 79 | for egg in $$(ls -d $(CURDIR)/debian/utah*/usr/lib/python*/dist-packages/utah-*.egg-info | grep -v "utah-common");\ |
171 | 59 | # Let's remove it from the parser as well | 80 | do rm -r $$egg;\ |
172 | 60 | rm -r $(CURDIR)/debian/utah-parser/usr/lib/python*/dist-packages/utah-*.egg-info | 81 | done |
166 | 61 | # We'll remove it from the server and leave it in the client package | ||
167 | 62 | rm -r $(CURDIR)/debian/utah/usr/lib/python*/dist-packages/utah-*.egg-info | ||
173 | 63 | # We want to symlink the utah example scripts into /usr/bin, and we need a directory for that | 82 | # We want to symlink the utah example scripts into /usr/bin, and we need a directory for that |
174 | 64 | mkdir -p $(CURDIR)/debian/utah/usr/bin | 83 | mkdir -p $(CURDIR)/debian/utah/usr/bin |
175 | 84 | mkdir -p $(CURDIR)/debian/utah-cobbler/usr/bin | ||
176 | 85 | mkdir -p $(CURDIR)/debian/utah-bamboofeeder/usr/bin | ||
177 | 65 | for script in $$(find examples -type f -name "*.py" -printf "%f\n"); do\ | 86 | for script in $$(find examples -type f -name "*.py" -printf "%f\n"); do\ |
178 | 66 | if [ ! -h "$(CURDIR)/debian/utah/usr/bin/$$script" ]; then\ | 87 | if [ ! -h "$(CURDIR)/debian/utah/usr/bin/$$script" ]; then\ |
180 | 67 | ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah/usr/bin/$$script;\ | 88 | if [ "$$script" = "run_test_cobbler.py" ]; then\ |
181 | 89 | ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah-cobbler/usr/bin/$$script;\ | ||
182 | 90 | elif [ "$$script" = "run_test_bamboo_feeder.py" ]; then\ | ||
183 | 91 | ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah-bamboofeeder/usr/bin/$$script;\ | ||
184 | 92 | else\ | ||
185 | 93 | ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah/usr/bin/$$script;\ | ||
186 | 94 | fi;\ | ||
187 | 68 | fi;\ | 95 | fi;\ |
188 | 69 | done | 96 | done |
189 | 70 | dh_auto_install | 97 | dh_auto_install |
190 | 71 | 98 | ||
191 | === modified file 'debian/utah.postinst' | |||
192 | --- debian/utah.postinst 2012-10-31 15:57:11 +0000 | |||
193 | +++ debian/utah.postinst 2012-12-13 00:38:21 +0000 | |||
194 | @@ -77,7 +77,6 @@ | |||
195 | 77 | 77 | ||
196 | 78 | usersetup | 78 | usersetup |
197 | 79 | 79 | ||
198 | 80 | ln -sf /etc/utah/uqt-vm-tools.conf ~utah/.uqt-vm-tools.conf | ||
199 | 81 | ln -sf /etc/utah/shell-profile ~utah/.profile | 80 | ln -sf /etc/utah/shell-profile ~utah/.profile |
200 | 82 | 81 | ||
201 | 83 | if ! ([ -f ~utah/.ssh/utah ] && [ -f ~utah/.ssh/utah.pub ]) | 82 | if ! ([ -f ~utah/.ssh/utah ] && [ -f ~utah/.ssh/utah.pub ]) |
202 | 84 | 83 | ||
203 | === modified file 'docs/source/reference.rst' | |||
204 | --- docs/source/reference.rst 2012-11-30 09:36:56 +0000 | |||
205 | +++ docs/source/reference.rst 2012-12-13 00:38:21 +0000 | |||
206 | @@ -88,6 +88,9 @@ | |||
207 | 88 | .. automodule:: utah.provisioning.provisioning | 88 | .. automodule:: utah.provisioning.provisioning |
208 | 89 | :members: | 89 | :members: |
209 | 90 | 90 | ||
210 | 91 | .. automodule:: utah.provisioning.ssh | ||
211 | 92 | :members: | ||
212 | 93 | |||
213 | 91 | .. automodule:: utah.provisioning.exceptions | 94 | .. automodule:: utah.provisioning.exceptions |
214 | 92 | :members: | 95 | :members: |
215 | 93 | 96 | ||
216 | @@ -96,9 +99,15 @@ | |||
217 | 96 | 99 | ||
218 | 97 | .. automodule:: utah.provisioning.baremetal | 100 | .. automodule:: utah.provisioning.baremetal |
219 | 98 | 101 | ||
220 | 102 | .. automodule:: utah.provisioning.baremetal.inventory | ||
221 | 103 | :members: | ||
222 | 104 | |||
223 | 99 | .. automodule:: utah.provisioning.baremetal.cobbler | 105 | .. automodule:: utah.provisioning.baremetal.cobbler |
224 | 100 | :members: | 106 | :members: |
225 | 101 | 107 | ||
226 | 108 | .. automodule:: utah.provisioning.baremetal.bamboofeeder | ||
227 | 109 | :members: | ||
228 | 110 | |||
229 | 102 | .. automodule:: utah.provisioning.baremetal.exceptions | 111 | .. automodule:: utah.provisioning.baremetal.exceptions |
230 | 103 | :members: | 112 | :members: |
231 | 104 | 113 | ||
232 | @@ -124,9 +133,6 @@ | |||
233 | 124 | .. automodule:: utah.provisioning.vm.exceptions | 133 | .. automodule:: utah.provisioning.vm.exceptions |
234 | 125 | :members: | 134 | :members: |
235 | 126 | 135 | ||
236 | 127 | .. automodule:: utah.provisioning.vm.libvirtvm | ||
237 | 128 | :members: | ||
238 | 129 | |||
239 | 130 | .. automodule:: utah.provisioning.vm.vm | 136 | .. automodule:: utah.provisioning.vm.vm |
240 | 131 | :members: | 137 | :members: |
241 | 132 | 138 | ||
242 | 133 | 139 | ||
243 | === modified file 'examples/run_install_test.py' | |||
244 | --- examples/run_install_test.py 2012-12-08 02:10:12 +0000 | |||
245 | +++ examples/run_install_test.py 2012-12-13 00:38:21 +0000 | |||
246 | @@ -22,8 +22,7 @@ | |||
247 | 22 | from utah.exceptions import UTAHException | 22 | from utah.exceptions import UTAHException |
248 | 23 | from utah.url import url_argument | 23 | from utah.url import url_argument |
249 | 24 | from utah.group import check_user_group, print_group_error_message | 24 | from utah.group import check_user_group, print_group_error_message |
252 | 25 | from utah.provisioning.inventory.sqlite import TinySQLiteInventory | 25 | from utah.provisioning.vm.vm import CustomVM, TinySQLiteInventory |
251 | 26 | from utah.provisioning.vm.libvirtvm import CustomVM | ||
253 | 27 | from utah.run import run_tests | 26 | from utah.run import run_tests |
254 | 28 | 27 | ||
255 | 29 | 28 | ||
256 | 30 | 29 | ||
257 | === modified file 'examples/run_test_bamboo_feeder.py' | |||
258 | --- examples/run_test_bamboo_feeder.py 2012-12-11 08:41:24 +0000 | |||
259 | +++ examples/run_test_bamboo_feeder.py 2012-12-13 00:38:21 +0000 | |||
260 | @@ -23,7 +23,8 @@ | |||
261 | 23 | from utah.exceptions import UTAHException | 23 | from utah.exceptions import UTAHException |
262 | 24 | from utah.group import check_user_group, print_group_error_message | 24 | from utah.group import check_user_group, print_group_error_message |
263 | 25 | from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine | 25 | from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine |
265 | 26 | from utah.provisioning.inventory.sqlite import ManualBaremetalSQLiteInventory | 26 | from utah.provisioning.baremetal.inventory import \ |
266 | 27 | ManualBaremetalSQLiteInventory | ||
267 | 27 | from utah.run import run_tests | 28 | from utah.run import run_tests |
268 | 28 | from utah.url import url_argument | 29 | from utah.url import url_argument |
269 | 29 | 30 | ||
270 | 30 | 31 | ||
271 | === modified file 'examples/run_test_cobbler.py' | |||
272 | --- examples/run_test_cobbler.py 2012-12-11 18:56:17 +0000 | |||
273 | +++ examples/run_test_cobbler.py 2012-12-13 00:38:21 +0000 | |||
274 | @@ -21,7 +21,9 @@ | |||
275 | 21 | from utah import config | 21 | from utah import config |
276 | 22 | from utah.exceptions import UTAHException | 22 | from utah.exceptions import UTAHException |
277 | 23 | from utah.group import check_user_group, print_group_error_message | 23 | from utah.group import check_user_group, print_group_error_message |
279 | 24 | from utah.provisioning.inventory.sqlite import ManualBaremetalSQLiteInventory | 24 | from utah.provisioning.baremetal.cobbler import CobblerMachine |
280 | 25 | from utah.provisioning.baremetal.inventory import \ | ||
281 | 26 | ManualBaremetalSQLiteInventory | ||
282 | 25 | from utah.run import run_tests | 27 | from utah.run import run_tests |
283 | 26 | from utah.url import url_argument | 28 | from utah.url import url_argument |
284 | 27 | 29 | ||
285 | @@ -105,7 +107,8 @@ | |||
286 | 105 | kw[arg] = value | 107 | kw[arg] = value |
287 | 106 | if getattr(args, 'type') is not None: | 108 | if getattr(args, 'type') is not None: |
288 | 107 | kw['installtype'] = args.type | 109 | kw['installtype'] = args.type |
290 | 108 | machine = inventory.request(clean=(not args.no_destroy), | 110 | machine = inventory.request(CobblerMachine, |
291 | 111 | clean=(not args.no_destroy), | ||
292 | 109 | debug=args.debug, dlpercentincrement=10, | 112 | debug=args.debug, dlpercentincrement=10, |
293 | 110 | name=args.name, new=True, **kw) | 113 | name=args.name, new=True, **kw) |
294 | 111 | exitstatus, locallogs = run_tests(args, machine) | 114 | exitstatus, locallogs = run_tests(args, machine) |
295 | 112 | 115 | ||
296 | === modified file 'examples/run_test_vm.py' | |||
297 | --- examples/run_test_vm.py 2012-12-10 19:18:18 +0000 | |||
298 | +++ examples/run_test_vm.py 2012-12-13 00:38:21 +0000 | |||
299 | @@ -21,7 +21,7 @@ | |||
300 | 21 | from utah import config | 21 | from utah import config |
301 | 22 | from utah.exceptions import UTAHException | 22 | from utah.exceptions import UTAHException |
302 | 23 | from utah.group import check_user_group, print_group_error_message | 23 | from utah.group import check_user_group, print_group_error_message |
304 | 24 | from utah.provisioning.inventory.sqlite import TinySQLiteInventory | 24 | from utah.provisioning.vm.vm import TinySQLiteInventory |
305 | 25 | from utah.run import run_tests | 25 | from utah.run import run_tests |
306 | 26 | 26 | ||
307 | 27 | 27 | ||
308 | 28 | 28 | ||
309 | === modified file 'examples/utah-user-setup.sh' | |||
310 | --- examples/utah-user-setup.sh 2012-07-19 20:00:02 +0000 | |||
311 | +++ examples/utah-user-setup.sh 2012-12-13 00:38:21 +0000 | |||
312 | @@ -63,13 +63,6 @@ | |||
313 | 63 | 63 | ||
314 | 64 | set +e | 64 | set +e |
315 | 65 | 65 | ||
316 | 66 | CMD=/usr/share/utah/examples/vmtools-user-setup.sh | ||
317 | 67 | if [ -f "$CMD" ] | ||
318 | 68 | then | ||
319 | 69 | echo "Setting up vm-tools config" | ||
320 | 70 | keepgoing $CMD $AUTO | ||
321 | 71 | fi | ||
322 | 72 | |||
323 | 73 | if [ "$LOGOUT" ] | 66 | if [ "$LOGOUT" ] |
324 | 74 | then | 67 | then |
325 | 75 | echo "User groups were updated" | 68 | echo "User groups were updated" |
326 | 76 | 69 | ||
327 | === removed file 'examples/vmtools-user-setup.sh' | |||
328 | --- examples/vmtools-user-setup.sh 2012-07-19 20:00:02 +0000 | |||
329 | +++ examples/vmtools-user-setup.sh 1970-01-01 00:00:00 +0000 | |||
330 | @@ -1,59 +0,0 @@ | |||
331 | 1 | #!/bin/bash | ||
332 | 2 | |||
333 | 3 | set -e | ||
334 | 4 | |||
335 | 5 | function keepgoing | ||
336 | 6 | { | ||
337 | 7 | echo "Going to run:" | ||
338 | 8 | echo "$@" | ||
339 | 9 | if [ "$AUTO" ] | ||
340 | 10 | then | ||
341 | 11 | $@ | ||
342 | 12 | else | ||
343 | 13 | echo "Hit enter to continue, enter any text to stop" | ||
344 | 14 | read ENTRY | ||
345 | 15 | if [ -n "$ENTRY" ] | ||
346 | 16 | then | ||
347 | 17 | exit 1 | ||
348 | 18 | else | ||
349 | 19 | echo $@ | bash | ||
350 | 20 | # echo "$@" | ||
351 | 21 | # $@ | ||
352 | 22 | fi | ||
353 | 23 | fi | ||
354 | 24 | } | ||
355 | 25 | |||
356 | 26 | if (echo "$@" | grep -iq "a") | ||
357 | 27 | then | ||
358 | 28 | export AUTO="auto" | ||
359 | 29 | else | ||
360 | 30 | export AUTO="" | ||
361 | 31 | fi | ||
362 | 32 | |||
363 | 33 | if ! [ -f ~/.uqt-vm-tools.conf ] | ||
364 | 34 | then | ||
365 | 35 | echo "Running vm-new to create ~/.uqv-vm-tools.conf" | ||
366 | 36 | if [ "$AUTO" ] | ||
367 | 37 | then | ||
368 | 38 | CMD="echo \"y\" | vm-new" | ||
369 | 39 | else | ||
370 | 40 | CMD=vm-new | ||
371 | 41 | fi | ||
372 | 42 | keepgoing $CMD | ||
373 | 43 | fi | ||
374 | 44 | |||
375 | 45 | SSHKEY=$(echo -e "import utah.config\nprint(utah.config.sshpublickey)" | python) | ||
376 | 46 | if ! (grep -q "^vm_ssh_key=$SSHKEY" ~/.uqt-vm-tools.conf) | ||
377 | 47 | then | ||
378 | 48 | CMD="sed 's@^vm_ssh_key=[^#]*#@vm_ssh_key=$SSHKEY #@' -i ~/.uqt-vm-tools.conf" | ||
379 | 49 | echo "Adding SSH key to config file" | ||
380 | 50 | keepgoing $CMD | ||
381 | 51 | fi | ||
382 | 52 | |||
383 | 53 | if ! (grep "^vm_extra_packages=" ~/.uqt-vm-tools.conf | grep "python-yaml" | grep "bzr" | grep -q "git") | ||
384 | 54 | then | ||
385 | 55 | CMD="sed 's/^vm_extra_packages=\"/vm_extra_packages=\"python-yaml bzr git /' -i ~/.uqt-vm-tools.conf" | ||
386 | 56 | echo "Adding client dependencies to config file" | ||
387 | 57 | keepgoing $CMD | ||
388 | 58 | fi | ||
389 | 59 | |||
390 | 60 | 0 | ||
391 | === modified file 'utah/provisioning/baremetal/bamboofeeder.py' | |||
392 | --- utah/provisioning/baremetal/bamboofeeder.py 2012-12-11 09:37:12 +0000 | |||
393 | +++ utah/provisioning/baremetal/bamboofeeder.py 2012-12-13 00:38:21 +0000 | |||
394 | @@ -25,13 +25,13 @@ | |||
395 | 25 | import tempfile | 25 | import tempfile |
396 | 26 | 26 | ||
397 | 27 | from utah import config | 27 | from utah import config |
398 | 28 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException | ||
399 | 29 | from utah.provisioning.baremetal.power import PowerMixin | ||
400 | 28 | from utah.provisioning.provisioning import ( | 30 | from utah.provisioning.provisioning import ( |
401 | 29 | CustomInstallMixin, | 31 | CustomInstallMixin, |
402 | 30 | Machine, | 32 | Machine, |
403 | 31 | SSHMixin, | ||
404 | 32 | ) | 33 | ) |
407 | 33 | from utah.provisioning.baremetal.power import PowerMixin | 34 | from utah.provisioning.ssh import SSHMixin |
406 | 34 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException | ||
408 | 35 | from utah.retry import retry | 35 | from utah.retry import retry |
409 | 36 | 36 | ||
410 | 37 | 37 | ||
411 | 38 | 38 | ||
412 | === modified file 'utah/provisioning/baremetal/cobbler.py' | |||
413 | --- utah/provisioning/baremetal/cobbler.py 2012-12-11 18:38:12 +0000 | |||
414 | +++ utah/provisioning/baremetal/cobbler.py 2012-12-13 00:38:21 +0000 | |||
415 | @@ -26,13 +26,13 @@ | |||
416 | 26 | import time | 26 | import time |
417 | 27 | 27 | ||
418 | 28 | from utah import config | 28 | from utah import config |
419 | 29 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException | ||
420 | 30 | from utah.provisioning.baremetal.power import PowerMixin | ||
421 | 29 | from utah.provisioning.provisioning import ( | 31 | from utah.provisioning.provisioning import ( |
422 | 30 | CustomInstallMixin, | 32 | CustomInstallMixin, |
423 | 31 | Machine, | 33 | Machine, |
424 | 32 | SSHMixin, | ||
425 | 33 | ) | 34 | ) |
428 | 34 | from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException | 35 | from utah.provisioning.ssh import SSHMixin |
427 | 35 | from utah.provisioning.baremetal.power import PowerMixin | ||
429 | 36 | from utah.retry import retry | 36 | from utah.retry import retry |
430 | 37 | 37 | ||
431 | 38 | 38 | ||
432 | 39 | 39 | ||
433 | === added file 'utah/provisioning/baremetal/inventory.py' | |||
434 | --- utah/provisioning/baremetal/inventory.py 1970-01-01 00:00:00 +0000 | |||
435 | +++ utah/provisioning/baremetal/inventory.py 2012-12-13 00:38:21 +0000 | |||
436 | @@ -0,0 +1,153 @@ | |||
437 | 1 | # Ubuntu Testing Automation Harness | ||
438 | 2 | # Copyright 2012 Canonical Ltd. | ||
439 | 3 | |||
440 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
441 | 5 | # under the terms of the GNU General Public License version 3, as published | ||
442 | 6 | # by the Free Software Foundation. | ||
443 | 7 | |||
444 | 8 | # This program is distributed in the hope that it will be useful, but | ||
445 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
446 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
447 | 11 | # PURPOSE. See the GNU General Public License for more details. | ||
448 | 12 | |||
449 | 13 | # You should have received a copy of the GNU General Public License along | ||
450 | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
451 | 15 | |||
452 | 16 | """ | ||
453 | 17 | Provide inventory functions specific to bare metal deployments. | ||
454 | 18 | """ | ||
455 | 19 | |||
456 | 20 | import os | ||
457 | 21 | import psutil | ||
458 | 22 | from utah.provisioning.inventory.exceptions import \ | ||
459 | 23 | UTAHProvisioningInventoryException | ||
460 | 24 | from utah.provisioning.inventory.sqlite import SQLiteInventory | ||
461 | 25 | |||
462 | 26 | |||
463 | 27 | class ManualBaremetalSQLiteInventory(SQLiteInventory): | ||
464 | 28 | """ | ||
465 | 29 | Keep an inventory of manually entered machines. | ||
466 | 30 | All columns other than machineid, name, and state are assumed to be | ||
467 | 31 | arguments for system creation (i.e., with cobbler). | ||
468 | 32 | """ | ||
469 | 33 | # TODO: rename this | ||
470 | 34 | def __init__(self, db='~/.utah-baremetal-inventory', | ||
471 | 35 | lockfile='~/.utah-baremetal-lock', *args, **kw): | ||
472 | 36 | db = os.path.expanduser(db) | ||
473 | 37 | lockfile = os.path.expanduser(lockfile) | ||
474 | 38 | if not os.path.isfile(db): | ||
475 | 39 | raise UTAHProvisioningInventoryException( | ||
476 | 40 | 'No machine database found at ' + db) | ||
477 | 41 | super(ManualBaremetalSQLiteInventory, self).__init__(*args, db=db, | ||
478 | 42 | lockfile=lockfile, | ||
479 | 43 | **kw) | ||
480 | 44 | machines_count = (self.connection | ||
481 | 45 | .execute('SELECT COUNT(*) FROM machines') | ||
482 | 46 | .fetchall()[0][0]) | ||
483 | 47 | if machines_count == 0: | ||
484 | 48 | raise UTAHProvisioningInventoryException('No machines in database') | ||
485 | 49 | self.machines = [] | ||
486 | 50 | |||
487 | 51 | def request(self, machinetype, name=None, *args, **kw): | ||
488 | 52 | query = 'SELECT * FROM machines' | ||
489 | 53 | queryvars = [] | ||
490 | 54 | if name is not None: | ||
491 | 55 | query += ' WHERE name=?' | ||
492 | 56 | queryvars.append(name) | ||
493 | 57 | result = self.connection.execute(query, queryvars).fetchall() | ||
494 | 58 | if result is None: | ||
495 | 59 | raise UTAHProvisioningInventoryException( | ||
496 | 60 | 'No machines meet criteria') | ||
497 | 61 | else: | ||
498 | 62 | for minfo in result: | ||
499 | 63 | machineinfo = dict(minfo) | ||
500 | 64 | if machineinfo['state'] == 'available': | ||
501 | 65 | return self._take(machineinfo, machinetype, *args, **kw) | ||
502 | 66 | for minfo in result: | ||
503 | 67 | machineinfo = dict(minfo) | ||
504 | 68 | pid = machineinfo['pid'] | ||
505 | 69 | try: | ||
506 | 70 | if not (psutil.pid_exists(pid) and ('utah' in | ||
507 | 71 | ' '.join(psutil.Process(pid).cmdline) | ||
508 | 72 | or 'run_test' in | ||
509 | 73 | ' '.join(psutil.Process(pid).cmdline))): | ||
510 | 74 | return self._take(machineinfo, machinetype, | ||
511 | 75 | *args, **kw) | ||
512 | 76 | except ValueError: | ||
513 | 77 | continue | ||
514 | 78 | |||
515 | 79 | raise UTAHProvisioningInventoryException( | ||
516 | 80 | 'All machines meeting criteria are currently unavailable') | ||
517 | 81 | |||
518 | 82 | def _take(self, machineinfo, machinetype, *args, **kw): | ||
519 | 83 | machineid = machineinfo.pop('machineid') | ||
520 | 84 | name = machineinfo.pop('name') | ||
521 | 85 | state = machineinfo.pop('state') | ||
522 | 86 | machineinfo.pop('pid') | ||
523 | 87 | update = self.connection.execute( | ||
524 | 88 | "UPDATE machines SET pid=?, state='provisioned' WHERE machineid=?" | ||
525 | 89 | "AND state=?", | ||
526 | 90 | [os.getpid(), machineid, state]).rowcount | ||
527 | 91 | if update == 1: | ||
528 | 92 | machine = machinetype(*args, inventory=self, | ||
529 | 93 | machineinfo=machineinfo, name=name, **kw) | ||
530 | 94 | self.machines.append(machine) | ||
531 | 95 | return machine | ||
532 | 96 | elif update == 0: | ||
533 | 97 | raise UTAHProvisioningInventoryException( | ||
534 | 98 | 'Machine was requested by another process ' | ||
535 | 99 | 'before we could request it') | ||
536 | 100 | elif update > 1: | ||
537 | 101 | raise UTAHProvisioningInventoryException( | ||
538 | 102 | 'Multiple machines exist ' | ||
539 | 103 | 'matching those criteria; ' | ||
540 | 104 | 'database ' + self.db + ' may be corrupt') | ||
541 | 105 | else: | ||
542 | 106 | raise UTAHProvisioningInventoryException( | ||
543 | 107 | 'Negative rowcount returned ' | ||
544 | 108 | 'when attempting to request machine') | ||
545 | 109 | |||
546 | 110 | def release(self, machine=None, name=None): | ||
547 | 111 | if machine is not None: | ||
548 | 112 | name = machine.name | ||
549 | 113 | if name is None: | ||
550 | 114 | raise UTAHProvisioningInventoryException( | ||
551 | 115 | 'name required to release a machine') | ||
552 | 116 | query = "UPDATE machines SET state='available' WHERE name=?" | ||
553 | 117 | queryvars = [name] | ||
554 | 118 | update = self.connection.execute(query, queryvars).rowcount | ||
555 | 119 | if update == 1: | ||
556 | 120 | if machine is not None: | ||
557 | 121 | if machine in self.machines: | ||
558 | 122 | self.machines.remove(machine) | ||
559 | 123 | elif update == 0: | ||
560 | 124 | raise UTAHProvisioningInventoryException( | ||
561 | 125 | 'SERIOUS ERROR: Another process released this machine ' | ||
562 | 126 | 'before we could, which means two processes provisioned ' | ||
563 | 127 | 'the same machine simultaneously') | ||
564 | 128 | elif update > 1: | ||
565 | 129 | raise UTAHProvisioningInventoryException( | ||
566 | 130 | 'Multiple machines exist matching those criteria; ' | ||
567 | 131 | 'database ' + self.db + ' may be corrupt') | ||
568 | 132 | else: | ||
569 | 133 | raise UTAHProvisioningInventoryException( | ||
570 | 134 | 'Negative rowcount returned ' | ||
571 | 135 | 'when attempting to release machine') | ||
572 | 136 | |||
573 | 137 | # Here is how I currently create the database: | ||
574 | 138 | # CREATE TABLE machines (machineid INTEGER PRIMARY KEY, | ||
575 | 139 | # name TEXT NOT NULL UNIQUE, | ||
576 | 140 | # state TEXT default 'available', | ||
577 | 141 | # pid INT, | ||
578 | 142 | # [mac-address] TEXT NOT NULL UNIQUE, | ||
579 | 143 | # [power-address] TEXT DEFAULT '10.97.0.13', | ||
580 | 144 | # [power-id] TEXT, | ||
581 | 145 | # [power-user] TEXT DEFAULT 'ubuntu', | ||
582 | 146 | # [power-pass] TEXT DEFAULT 'ubuntu', | ||
583 | 147 | # [power-type] TEXT DEFAULT 'sentryswitch_cdu'); | ||
584 | 148 | |||
585 | 149 | # Here is how I currently populate the database: | ||
586 | 150 | # INSERT INTO machines (name, [mac-address], [power-id]) | ||
587 | 151 | # VALUES ('acer-veriton-01-Pete', 'd0:27:88:9f:73:ce', 'Veriton_1'); | ||
588 | 152 | # INSERT INTO machines (name, [mac-address], [power-id]) | ||
589 | 153 | # VALUES ('acer-veriton-02-Pete', 'd0:27:88:9b:84:5b', 'Veriton_2'); | ||
590 | 0 | 154 | ||
591 | === modified file 'utah/provisioning/inventory/sqlite.py' | |||
592 | --- utah/provisioning/inventory/sqlite.py 2012-12-11 18:45:40 +0000 | |||
593 | +++ utah/provisioning/inventory/sqlite.py 2012-12-13 00:38:21 +0000 | |||
594 | @@ -17,12 +17,7 @@ | |||
595 | 17 | 17 | ||
596 | 18 | import sqlite3 | 18 | import sqlite3 |
597 | 19 | import os | 19 | import os |
598 | 20 | import psutil | ||
599 | 21 | from utah.provisioning.inventory.exceptions import \ | ||
600 | 22 | UTAHProvisioningInventoryException | ||
601 | 23 | from utah.provisioning.inventory.inventory import Inventory | 20 | from utah.provisioning.inventory.inventory import Inventory |
602 | 24 | from utah.provisioning.vm.libvirtvm import CustomVM | ||
603 | 25 | from utah.provisioning.baremetal.cobbler import CobblerMachine | ||
604 | 26 | 21 | ||
605 | 27 | 22 | ||
606 | 28 | class SQLiteInventory(Inventory): | 23 | class SQLiteInventory(Inventory): |
607 | @@ -41,181 +36,3 @@ | |||
608 | 41 | def delete(self): | 36 | def delete(self): |
609 | 42 | os.unlink(self.db) | 37 | os.unlink(self.db) |
610 | 43 | super(SQLiteInventory, self).delete() | 38 | super(SQLiteInventory, self).delete() |
611 | 44 | |||
612 | 45 | |||
613 | 46 | class TinySQLiteInventory(SQLiteInventory): | ||
614 | 47 | """ | ||
615 | 48 | Tiny SQLite inventory that implements request, release, and destroy. | ||
616 | 49 | |||
617 | 50 | No authentication or conflict checking currently exists. | ||
618 | 51 | """ | ||
619 | 52 | def __init__(self, *args, **kw): | ||
620 | 53 | """ | ||
621 | 54 | Initialize simple database. | ||
622 | 55 | """ | ||
623 | 56 | super(TinySQLiteInventory, self).__init__(*args, **kw) | ||
624 | 57 | self.connection.execute( | ||
625 | 58 | 'CREATE TABLE IF NOT EXISTS ' | ||
626 | 59 | 'machines(machineid INTEGER PRIMARY KEY, state TEXT)') | ||
627 | 60 | |||
628 | 61 | def request(self, machinetype=CustomVM, *args, **kw): | ||
629 | 62 | """ | ||
630 | 63 | Takes a Machine class as machinetype, and passes the newly generated | ||
631 | 64 | machineid along with all other arguments to that class's constructor, | ||
632 | 65 | returning the resulting object. | ||
633 | 66 | """ | ||
634 | 67 | cursor = self.connection.cursor() | ||
635 | 68 | cursor.execute("INSERT INTO machines (state) VALUES ('provisioned')") | ||
636 | 69 | machineid = cursor.lastrowid | ||
637 | 70 | return machinetype(machineid=machineid, *args, **kw) | ||
638 | 71 | |||
639 | 72 | def release(self, machineid): | ||
640 | 73 | """ | ||
641 | 74 | Updates the database to indicate the machine is available. | ||
642 | 75 | """ | ||
643 | 76 | if self.connection.execute( | ||
644 | 77 | "UPDATE machines SET state='available' WHERE machineid=?", | ||
645 | 78 | [machineid]): | ||
646 | 79 | return True | ||
647 | 80 | else: | ||
648 | 81 | return False | ||
649 | 82 | |||
650 | 83 | def destroy(self, machineid): | ||
651 | 84 | """ | ||
652 | 85 | Updates the database to indicate the machine is destroyed, but does not | ||
653 | 86 | destroy the machine. | ||
654 | 87 | """ | ||
655 | 88 | if self.connection.execute( | ||
656 | 89 | "UPDATE machines SET state='destroyed' ""WHERE machineid=?", | ||
657 | 90 | [machineid]): | ||
658 | 91 | return True | ||
659 | 92 | else: | ||
660 | 93 | return False | ||
661 | 94 | |||
662 | 95 | |||
663 | 96 | class ManualBaremetalSQLiteInventory(SQLiteInventory): | ||
664 | 97 | """ | ||
665 | 98 | Keep an inventory of manually entered machines. | ||
666 | 99 | All columns other than machineid, name, and state are assumed to be | ||
667 | 100 | arguments for system creation (i.e., with cobbler). | ||
668 | 101 | """ | ||
669 | 102 | # TODO: rename this | ||
670 | 103 | def __init__(self, db='~/.utah-baremetal-inventory', | ||
671 | 104 | lockfile='~/.utah-baremetal-lock', *args, **kw): | ||
672 | 105 | db = os.path.expanduser(db) | ||
673 | 106 | lockfile = os.path.expanduser(lockfile) | ||
674 | 107 | if not os.path.isfile(db): | ||
675 | 108 | raise UTAHProvisioningInventoryException( | ||
676 | 109 | 'No machine database found at ' + db) | ||
677 | 110 | super(ManualBaremetalSQLiteInventory, self).__init__(*args, db=db, | ||
678 | 111 | lockfile=lockfile, | ||
679 | 112 | **kw) | ||
680 | 113 | machines_count = (self.connection | ||
681 | 114 | .execute('SELECT COUNT(*) FROM machines') | ||
682 | 115 | .fetchall()[0][0]) | ||
683 | 116 | if machines_count == 0: | ||
684 | 117 | raise UTAHProvisioningInventoryException('No machines in database') | ||
685 | 118 | self.machines = [] | ||
686 | 119 | |||
687 | 120 | def request(self, machinetype=CobblerMachine, name=None, *args, **kw): | ||
688 | 121 | query = 'SELECT * FROM machines' | ||
689 | 122 | queryvars = [] | ||
690 | 123 | if name is not None: | ||
691 | 124 | query += ' WHERE name=?' | ||
692 | 125 | queryvars.append(name) | ||
693 | 126 | result = self.connection.execute(query, queryvars).fetchall() | ||
694 | 127 | if result is None: | ||
695 | 128 | raise UTAHProvisioningInventoryException( | ||
696 | 129 | 'No machines meet criteria') | ||
697 | 130 | else: | ||
698 | 131 | for minfo in result: | ||
699 | 132 | machineinfo = dict(minfo) | ||
700 | 133 | if machineinfo['state'] == 'available': | ||
701 | 134 | return self._take(machineinfo, *args, **kw) | ||
702 | 135 | for minfo in result: | ||
703 | 136 | machineinfo = dict(minfo) | ||
704 | 137 | pid = machineinfo['pid'] | ||
705 | 138 | try: | ||
706 | 139 | if not (psutil.pid_exists(pid) and ('utah' in | ||
707 | 140 | ' '.join(psutil.Process(pid).cmdline) | ||
708 | 141 | or 'run_test' in | ||
709 | 142 | ' '.join(psutil.Process(pid).cmdline))): | ||
710 | 143 | return self._take(machineinfo, *args, **kw) | ||
711 | 144 | except ValueError: | ||
712 | 145 | continue | ||
713 | 146 | |||
714 | 147 | raise UTAHProvisioningInventoryException( | ||
715 | 148 | 'All machines meeting criteria are currently unavailable') | ||
716 | 149 | |||
717 | 150 | def _take(self, machineinfo, *args, **kw): | ||
718 | 151 | machineid = machineinfo.pop('machineid') | ||
719 | 152 | name = machineinfo.pop('name') | ||
720 | 153 | state = machineinfo.pop('state') | ||
721 | 154 | machineinfo.pop('pid') | ||
722 | 155 | update = self.connection.execute( | ||
723 | 156 | "UPDATE machines SET pid=?, state='provisioned' WHERE machineid=?" | ||
724 | 157 | "AND state=?", | ||
725 | 158 | [os.getpid(), machineid, state]).rowcount | ||
726 | 159 | if update == 1: | ||
727 | 160 | machine = CobblerMachine(*args, inventory=self, | ||
728 | 161 | machineinfo=machineinfo, name=name, **kw) | ||
729 | 162 | self.machines.append(machine) | ||
730 | 163 | return machine | ||
731 | 164 | elif update == 0: | ||
732 | 165 | raise UTAHProvisioningInventoryException( | ||
733 | 166 | 'Machine was requested by another process ' | ||
734 | 167 | 'before we could request it') | ||
735 | 168 | elif update > 1: | ||
736 | 169 | raise UTAHProvisioningInventoryException( | ||
737 | 170 | 'Multiple machines exist ' | ||
738 | 171 | 'matching those criteria; ' | ||
739 | 172 | 'database ' + self.db + ' may be corrupt') | ||
740 | 173 | else: | ||
741 | 174 | raise UTAHProvisioningInventoryException( | ||
742 | 175 | 'Negative rowcount returned ' | ||
743 | 176 | 'when attempting to request machine') | ||
744 | 177 | |||
745 | 178 | def release(self, machine=None, name=None): | ||
746 | 179 | if machine is not None: | ||
747 | 180 | name = machine.name | ||
748 | 181 | if name is None: | ||
749 | 182 | raise UTAHProvisioningInventoryException( | ||
750 | 183 | 'name required to release a machine') | ||
751 | 184 | query = "UPDATE machines SET state='available' WHERE name=?" | ||
752 | 185 | queryvars = [name] | ||
753 | 186 | update = self.connection.execute(query, queryvars).rowcount | ||
754 | 187 | if update == 1: | ||
755 | 188 | if machine is not None: | ||
756 | 189 | if machine in self.machines: | ||
757 | 190 | self.machines.remove(machine) | ||
758 | 191 | elif update == 0: | ||
759 | 192 | raise UTAHProvisioningInventoryException( | ||
760 | 193 | 'SERIOUS ERROR: Another process released this machine ' | ||
761 | 194 | 'before we could, which means two processes provisioned ' | ||
762 | 195 | 'the same machine simultaneously') | ||
763 | 196 | elif update > 1: | ||
764 | 197 | raise UTAHProvisioningInventoryException( | ||
765 | 198 | 'Multiple machines exist matching those criteria; ' | ||
766 | 199 | 'database ' + self.db + ' may be corrupt') | ||
767 | 200 | else: | ||
768 | 201 | raise UTAHProvisioningInventoryException( | ||
769 | 202 | 'Negative rowcount returned ' | ||
770 | 203 | 'when attempting to release machine') | ||
771 | 204 | |||
772 | 205 | # Here is how I currently create the database: | ||
773 | 206 | # CREATE TABLE machines (machineid INTEGER PRIMARY KEY, | ||
774 | 207 | # name TEXT NOT NULL UNIQUE, | ||
775 | 208 | # state TEXT default 'available', | ||
776 | 209 | # pid TEXT, | ||
777 | 210 | # [mac-address] TEXT NOT NULL UNIQUE, | ||
778 | 211 | # [power-address] TEXT DEFAULT '10.97.0.13', | ||
779 | 212 | # [power-id] TEXT, | ||
780 | 213 | # [power-user] TEXT DEFAULT 'ubuntu', | ||
781 | 214 | # [power-pass] TEXT DEFAULT 'ubuntu', | ||
782 | 215 | # [power-type] TEXT DEFAULT 'sentryswitch_cdu'); | ||
783 | 216 | |||
784 | 217 | # Here is how I currently populate the database: | ||
785 | 218 | # INSERT INTO machines (name, [mac-address], [power-id]) | ||
786 | 219 | # VALUES ('acer-veriton-01-Pete', 'd0:27:88:9f:73:ce', 'Veriton_1'); | ||
787 | 220 | # INSERT INTO machines (name, [mac-address], [power-id]) | ||
788 | 221 | # VALUES ('acer-veriton-02-Pete', 'd0:27:88:9b:84:5b', 'Veriton_2'); | ||
789 | 222 | 39 | ||
790 | === modified file 'utah/provisioning/provisioning.py' | |||
791 | --- utah/provisioning/provisioning.py 2012-12-11 09:50:08 +0000 | |||
792 | +++ utah/provisioning/provisioning.py 2012-12-13 00:38:21 +0000 | |||
793 | @@ -18,26 +18,24 @@ | |||
794 | 18 | Functions here should apply to multiple machine types (VM, bare metal, etc.) | 18 | Functions here should apply to multiple machine types (VM, bare metal, etc.) |
795 | 19 | """ | 19 | """ |
796 | 20 | 20 | ||
797 | 21 | import apt.cache | ||
798 | 21 | import logging | 22 | import logging |
799 | 22 | import logging.handlers | 23 | import logging.handlers |
800 | 23 | import os | 24 | import os |
801 | 24 | import pipes | 25 | import pipes |
802 | 26 | import re | ||
803 | 25 | import shutil | 27 | import shutil |
804 | 26 | import socket | ||
805 | 27 | import subprocess | 28 | import subprocess |
806 | 28 | import sys | 29 | import sys |
807 | 29 | import time | 30 | import time |
808 | 30 | import urllib | 31 | import urllib |
809 | 31 | import uuid | 32 | import uuid |
810 | 32 | import paramiko | ||
811 | 33 | import re | ||
812 | 34 | import apt.cache | ||
813 | 35 | 33 | ||
814 | 36 | from glob import glob | 34 | from glob import glob |
815 | 37 | from stat import S_ISDIR | ||
816 | 38 | 35 | ||
817 | 39 | import utah.timeout | 36 | import utah.timeout |
818 | 40 | 37 | ||
819 | 38 | from utah import config | ||
820 | 41 | from utah.commandstr import commandstr | 39 | from utah.commandstr import commandstr |
821 | 42 | from utah.iso import ISO | 40 | from utah.iso import ISO |
822 | 43 | from utah.orderedcollections import ( | 41 | from utah.orderedcollections import ( |
823 | @@ -47,7 +45,6 @@ | |||
824 | 47 | from utah.preseed import Preseed | 45 | from utah.preseed import Preseed |
825 | 48 | from utah.provisioning.exceptions import UTAHProvisioningException | 46 | from utah.provisioning.exceptions import UTAHProvisioningException |
826 | 49 | from utah.retry import retry | 47 | from utah.retry import retry |
827 | 50 | from utah import config | ||
828 | 51 | 48 | ||
829 | 52 | 49 | ||
830 | 53 | class Machine(object): | 50 | class Machine(object): |
831 | @@ -679,243 +676,6 @@ | |||
832 | 679 | 'Try rerunning install.') | 676 | 'Try rerunning install.') |
833 | 680 | 677 | ||
834 | 681 | 678 | ||
835 | 682 | class SSHMixin(object): | ||
836 | 683 | """ | ||
837 | 684 | Provide common commands for machines accessed via ssh. | ||
838 | 685 | """ | ||
839 | 686 | def __init__(self, *args, **kwargs): | ||
840 | 687 | # Note: Since this is a mixin it doesn't expect any argument | ||
841 | 688 | # However, it calls super to initialize any other mixins in the mro | ||
842 | 689 | super(SSHMixin, self).__init__(*args, **kwargs) | ||
843 | 690 | ssh_client = paramiko.SSHClient() | ||
844 | 691 | ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | ||
845 | 692 | self.ssh_client = ssh_client | ||
846 | 693 | |||
847 | 694 | def run(self, command, _quiet=None, root=False, timeout=None): | ||
848 | 695 | """ | ||
849 | 696 | Run a command using ssh. | ||
850 | 697 | """ | ||
851 | 698 | if isinstance(command, basestring): | ||
852 | 699 | commandstring = command | ||
853 | 700 | else: | ||
854 | 701 | commandstring = ' '.join(command) | ||
855 | 702 | if root: | ||
856 | 703 | user = 'root' | ||
857 | 704 | else: | ||
858 | 705 | user = config.user | ||
859 | 706 | |||
860 | 707 | self.activecheck() | ||
861 | 708 | # Some commands expect run to return the output status of the command | ||
862 | 709 | # We're going to try the method described here: | ||
863 | 710 | # http://stackoverflow.com/questions/3562403/ | ||
864 | 711 | # With additions from here: | ||
865 | 712 | # http://od-eon.com/blogs/ | ||
866 | 713 | # stefan/automating-remote-commands-over-ssh-paramiko/ | ||
867 | 714 | self.logger.debug('Connecting SSH') | ||
868 | 715 | self.ssh_client.connect(self.name, | ||
869 | 716 | username=user, | ||
870 | 717 | key_filename=config.sshprivatekey) | ||
871 | 718 | |||
872 | 719 | self.logger.debug('Opening SSH session') | ||
873 | 720 | channel = self.ssh_client.get_transport().open_session() | ||
874 | 721 | |||
875 | 722 | self.logger.info('Running command through SSH: ' + commandstring) | ||
876 | 723 | stdout = channel.makefile('rb') | ||
877 | 724 | stderr = channel.makefile_stderr('rb') | ||
878 | 725 | if timeout is None: | ||
879 | 726 | channel.exec_command(commandstring) | ||
880 | 727 | else: | ||
881 | 728 | utah.timeout.timeout(timeout, channel.exec_command, commandstring) | ||
882 | 729 | retval = channel.recv_exit_status() | ||
883 | 730 | |||
884 | 731 | self.logger.debug('Closing SSH connection') | ||
885 | 732 | self.ssh_client.close() | ||
886 | 733 | |||
887 | 734 | log_level = logging.DEBUG if retval == 0 else logging.WARNING | ||
888 | 735 | log_message = 'Return code: {}'.format(retval) | ||
889 | 736 | self.logger.log(log_level, log_message) | ||
890 | 737 | |||
891 | 738 | self.logger.debug('Standard output follows:') | ||
892 | 739 | stdout_lines = stdout.readlines() | ||
893 | 740 | for line in stdout_lines: | ||
894 | 741 | self.logger.debug(line.strip()) | ||
895 | 742 | |||
896 | 743 | self.logger.debug('Standard error follows:') | ||
897 | 744 | stderr_lines = stderr.readlines() | ||
898 | 745 | for line in stderr_lines: | ||
899 | 746 | self.logger.debug(line.strip()) | ||
900 | 747 | |||
901 | 748 | return retval, ''.join(stdout_lines), ''.join(stderr_lines) | ||
902 | 749 | |||
903 | 750 | def uploadfiles(self, files, target=os.path.normpath('/tmp/')): | ||
904 | 751 | """ | ||
905 | 752 | Copy a file or list of files to a target directory on the machine. | ||
906 | 753 | """ | ||
907 | 754 | if isinstance(files, basestring): | ||
908 | 755 | files = [files] | ||
909 | 756 | |||
910 | 757 | self.activecheck() | ||
911 | 758 | self.ssh_client.connect(self.name, | ||
912 | 759 | username=config.user, | ||
913 | 760 | key_filename=config.sshprivatekey) | ||
914 | 761 | sftp_client = self.ssh_client.open_sftp() | ||
915 | 762 | failed = [] | ||
916 | 763 | try: | ||
917 | 764 | for localpath in files: | ||
918 | 765 | if os.path.isfile(localpath): | ||
919 | 766 | self.logger.info('Uploading ' + localpath | ||
920 | 767 | + ' from the host to ' + target | ||
921 | 768 | + ' on the machine') | ||
922 | 769 | remotepath = os.path.join(target, | ||
923 | 770 | os.path.basename(localpath)) | ||
924 | 771 | sftp_client.put(localpath, remotepath) | ||
925 | 772 | else: | ||
926 | 773 | failed.append(localpath) | ||
927 | 774 | finally: | ||
928 | 775 | sftp_client.close() | ||
929 | 776 | if len(failed) > 0: | ||
930 | 777 | err = UTAHProvisioningException('Files do not exist: ' | ||
931 | 778 | + ' '.join(failed)) | ||
932 | 779 | err.files = failed | ||
933 | 780 | raise err | ||
934 | 781 | |||
935 | 782 | def downloadfiles(self, files, target=os.path.normpath('/tmp/')): | ||
936 | 783 | """ | ||
937 | 784 | Copy a file or list of files from the machine to a target directory on | ||
938 | 785 | the local system. | ||
939 | 786 | """ | ||
940 | 787 | # TODO: check for directories and recurse into them | ||
941 | 788 | if isinstance(files, basestring): | ||
942 | 789 | files = [files] | ||
943 | 790 | |||
944 | 791 | self.activecheck() | ||
945 | 792 | self.ssh_client.connect(self.name, | ||
946 | 793 | username=config.user, | ||
947 | 794 | key_filename=config.sshprivatekey) | ||
948 | 795 | sftp_client = self.ssh_client.open_sftp() | ||
949 | 796 | if os.path.isdir(target): | ||
950 | 797 | get_localpath = lambda remotepath: \ | ||
951 | 798 | os.path.join(target, os.path.basename(remotepath)) | ||
952 | 799 | else: | ||
953 | 800 | get_localpath = lambda remotepath: target | ||
954 | 801 | |||
955 | 802 | try: | ||
956 | 803 | for remotepath in files: | ||
957 | 804 | localpath = get_localpath(remotepath) | ||
958 | 805 | self.logger.info('Downloading ' + remotepath | ||
959 | 806 | + ' from the machine to ' + target | ||
960 | 807 | + ' on the host') | ||
961 | 808 | sftp_client.get(remotepath, localpath) | ||
962 | 809 | finally: | ||
963 | 810 | sftp_client.close() | ||
964 | 811 | |||
965 | 812 | def downloadfilesrecursive(self, files, target=os.path.normpath('/tmp/')): | ||
966 | 813 | """ | ||
967 | 814 | Recursively copy all files in files to the target directory target. | ||
968 | 815 | """ | ||
969 | 816 | self.activecheck() | ||
970 | 817 | self.ssh_client.connect(self.name, | ||
971 | 818 | username=config.user, | ||
972 | 819 | key_filename=config.sshprivatekey) | ||
973 | 820 | sftp_client = self.ssh_client.open_sftp() | ||
974 | 821 | myfiles = [] | ||
975 | 822 | |||
976 | 823 | if isinstance(files, basestring): | ||
977 | 824 | files = [files] | ||
978 | 825 | |||
979 | 826 | for myfile in files: | ||
980 | 827 | newtarget = os.path.join(target, os.path.basename(myfile)) | ||
981 | 828 | if S_ISDIR(sftp_client.stat(myfile).st_mode): | ||
982 | 829 | self.logger.debug(myfile + ' is a directory, recursing') | ||
983 | 830 | if not os.path.isdir(newtarget): | ||
984 | 831 | self.logger.debug('Attempting to create ' + newtarget) | ||
985 | 832 | os.makedirs(newtarget) | ||
986 | 833 | myfiles = [os.path.join(myfile, x) | ||
987 | 834 | for x in sftp_client.listdir(myfile)] | ||
988 | 835 | # for basename in sftp_client.listdir(dirname): | ||
989 | 836 | # myfile = os.path.join(dirname, basename) | ||
990 | 837 | # if S_ISDIR(sftp_client.stat(myfile).st_mode): | ||
991 | 838 | # if not os.path.isdir(newtarget): | ||
992 | 839 | # os.makedirs(newtarget) | ||
993 | 840 | # self.downloadfilesrecursive(myfile, newtarget) | ||
994 | 841 | # else: | ||
995 | 842 | # myfiles.append(myfile) | ||
996 | 843 | self.downloadfilesrecursive(myfiles, newtarget) | ||
997 | 844 | else: | ||
998 | 845 | self.downloadfiles(myfile, newtarget) | ||
999 | 846 | |||
1000 | 847 | def destroy(self, *args, **kw): | ||
1001 | 848 | """ | ||
1002 | 849 | Clean up known hosts for machine. | ||
1003 | 850 | """ | ||
1004 | 851 | # TODO: evaluate value of known_hosts with paramiko | ||
1005 | 852 | self.logger.info('Removing machine addresses ' | ||
1006 | 853 | 'from ssh known_hosts file') | ||
1007 | 854 | addresses = [self.name] | ||
1008 | 855 | try: | ||
1009 | 856 | addresses.append(socket.gethostbyname(self.name)) | ||
1010 | 857 | except socket.gaierror as err: | ||
1011 | 858 | if err.errno in [-2, -5]: | ||
1012 | 859 | self.logger.debug(self.name | ||
1013 | 860 | + ' is not resolvable, ' | ||
1014 | 861 | + 'so not removing from known_hosts') | ||
1015 | 862 | else: | ||
1016 | 863 | raise err | ||
1017 | 864 | |||
1018 | 865 | old_host_keys = self.ssh_client.get_host_keys() | ||
1019 | 866 | new_host_keys = paramiko.HostKeys() | ||
1020 | 867 | addresses = set(addresses) | ||
1021 | 868 | for address, key in old_host_keys.iteritems(): | ||
1022 | 869 | # Skip keys so that they don't get added | ||
1023 | 870 | # into the new keys (i.e. they're removed) | ||
1024 | 871 | if address in addresses: | ||
1025 | 872 | continue | ||
1026 | 873 | new_host_keys[address] = key | ||
1027 | 874 | new_host_keys.save(config.sshknownhosts) | ||
1028 | 875 | self.ssh_client.close() | ||
1029 | 876 | |||
1030 | 877 | super(SSHMixin, self).destroy(*args, **kw) | ||
1031 | 878 | |||
1032 | 879 | def sshcheck(self, timeout=config.checktimeout): | ||
1033 | 880 | """ | ||
1034 | 881 | Sleep for a while and check if the machine is available via ssh. | ||
1035 | 882 | Return a retryable exception if it is not. | ||
1036 | 883 | Intended for use with retry. | ||
1037 | 884 | """ | ||
1038 | 885 | self.logger.info('Sleeping {timeout} seconds' | ||
1039 | 886 | .format(timeout=timeout)) | ||
1040 | 887 | time.sleep(timeout) | ||
1041 | 888 | self.logger.info('Checking for ssh availability') | ||
1042 | 889 | try: | ||
1043 | 890 | self.ssh_client.connect(self.name, | ||
1044 | 891 | username=config.user, | ||
1045 | 892 | key_filename=config.sshprivatekey) | ||
1046 | 893 | except socket.error as err: | ||
1047 | 894 | raise UTAHProvisioningException(str(err), retry=True) | ||
1048 | 895 | |||
1049 | 896 | def sshpoll(self, timeout=None, | ||
1050 | 897 | checktimeout=config.checktimeout, logmethod=None): | ||
1051 | 898 | """ | ||
1052 | 899 | Run sshcheck over and over until timeout expires. | ||
1053 | 900 | """ | ||
1054 | 901 | if timeout is None: | ||
1055 | 902 | timeout = self.boottimeout | ||
1056 | 903 | if logmethod is None: | ||
1057 | 904 | logmethod = self.logger.debug | ||
1058 | 905 | utah.timeout.timeout(timeout, retry, self.sshcheck, checktimeout, | ||
1059 | 906 | logmethod=logmethod) | ||
1060 | 907 | |||
1061 | 908 | def activecheck(self): | ||
1062 | 909 | """ | ||
1063 | 910 | Start the machine if needed, and check for SSH login. | ||
1064 | 911 | """ | ||
1065 | 912 | self.logger.debug('Checking if machine is active') | ||
1066 | 913 | self.provisioncheck() | ||
1067 | 914 | if not self.active: | ||
1068 | 915 | self._start() | ||
1069 | 916 | self.sshcheck() | ||
1070 | 917 | |||
1071 | 918 | |||
1072 | 919 | class CustomInstallMixin(object): | 679 | class CustomInstallMixin(object): |
1073 | 920 | """ | 680 | """ |
1074 | 921 | Provide routines for unpacking necessary boot files from images, | 681 | Provide routines for unpacking necessary boot files from images, |
1075 | 922 | 682 | ||
1076 | === added file 'utah/provisioning/ssh.py' | |||
1077 | --- utah/provisioning/ssh.py 1970-01-01 00:00:00 +0000 | |||
1078 | +++ utah/provisioning/ssh.py 2012-12-13 00:38:21 +0000 | |||
1079 | @@ -0,0 +1,261 @@ | |||
1080 | 1 | # Ubuntu Testing Automation Harness | ||
1081 | 2 | # Copyright 2012 Canonical Ltd. | ||
1082 | 3 | |||
1083 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
1084 | 5 | # under the terms of the GNU General Public License version 3, as published | ||
1085 | 6 | # by the Free Software Foundation. | ||
1086 | 7 | |||
1087 | 8 | # This program is distributed in the hope that it will be useful, but | ||
1088 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1089 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1090 | 11 | # PURPOSE. See the GNU General Public License for more details. | ||
1091 | 12 | |||
1092 | 13 | # You should have received a copy of the GNU General Public License along | ||
1093 | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1094 | 15 | |||
1095 | 16 | """ | ||
1096 | 17 | Provide a mixin class for machines with SSH support. | ||
1097 | 18 | """ | ||
1098 | 19 | |||
1099 | 20 | import logging | ||
1100 | 21 | import os | ||
1101 | 22 | import paramiko | ||
1102 | 23 | import socket | ||
1103 | 24 | import time | ||
1104 | 25 | |||
1105 | 26 | from stat import S_ISDIR | ||
1106 | 27 | |||
1107 | 28 | import utah.timeout | ||
1108 | 29 | |||
1109 | 30 | from utah import config | ||
1110 | 31 | from utah.provisioning.exceptions import UTAHProvisioningException | ||
1111 | 32 | from utah.retry import retry | ||
1112 | 33 | |||
1113 | 34 | |||
1114 | 35 | class SSHMixin(object): | ||
1115 | 36 | """ | ||
1116 | 37 | Provide common commands for machines accessed via ssh. | ||
1117 | 38 | """ | ||
1118 | 39 | def __init__(self, *args, **kwargs): | ||
1119 | 40 | # Note: Since this is a mixin it doesn't expect any argument | ||
1120 | 41 | # However, it calls super to initialize any other mixins in the mro | ||
1121 | 42 | super(SSHMixin, self).__init__(*args, **kwargs) | ||
1122 | 43 | ssh_client = paramiko.SSHClient() | ||
1123 | 44 | ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) | ||
1124 | 45 | self.ssh_client = ssh_client | ||
1125 | 46 | |||
1126 | 47 | def run(self, command, _quiet=None, root=False, timeout=None): | ||
1127 | 48 | """ | ||
1128 | 49 | Run a command using ssh. | ||
1129 | 50 | """ | ||
1130 | 51 | if isinstance(command, basestring): | ||
1131 | 52 | commandstring = command | ||
1132 | 53 | else: | ||
1133 | 54 | commandstring = ' '.join(command) | ||
1134 | 55 | if root: | ||
1135 | 56 | user = 'root' | ||
1136 | 57 | else: | ||
1137 | 58 | user = config.user | ||
1138 | 59 | |||
1139 | 60 | self.activecheck() | ||
1140 | 61 | # Some commands expect run to return the output status of the command | ||
1141 | 62 | # We're going to try the method described here: | ||
1142 | 63 | # http://stackoverflow.com/questions/3562403/ | ||
1143 | 64 | # With additions from here: | ||
1144 | 65 | # http://od-eon.com/blogs/ | ||
1145 | 66 | # stefan/automating-remote-commands-over-ssh-paramiko/ | ||
1146 | 67 | self.logger.debug('Connecting SSH') | ||
1147 | 68 | self.ssh_client.connect(self.name, | ||
1148 | 69 | username=user, | ||
1149 | 70 | key_filename=config.sshprivatekey) | ||
1150 | 71 | |||
1151 | 72 | self.logger.debug('Opening SSH session') | ||
1152 | 73 | channel = self.ssh_client.get_transport().open_session() | ||
1153 | 74 | |||
1154 | 75 | self.logger.info('Running command through SSH: ' + commandstring) | ||
1155 | 76 | stdout = channel.makefile('rb') | ||
1156 | 77 | stderr = channel.makefile_stderr('rb') | ||
1157 | 78 | if timeout is None: | ||
1158 | 79 | channel.exec_command(commandstring) | ||
1159 | 80 | else: | ||
1160 | 81 | utah.timeout.timeout(timeout, channel.exec_command, commandstring) | ||
1161 | 82 | retval = channel.recv_exit_status() | ||
1162 | 83 | |||
1163 | 84 | self.logger.debug('Closing SSH connection') | ||
1164 | 85 | self.ssh_client.close() | ||
1165 | 86 | |||
1166 | 87 | log_level = logging.DEBUG if retval == 0 else logging.WARNING | ||
1167 | 88 | log_message = 'Return code: {}'.format(retval) | ||
1168 | 89 | self.logger.log(log_level, log_message) | ||
1169 | 90 | |||
1170 | 91 | self.logger.debug('Standard output follows:') | ||
1171 | 92 | stdout_lines = stdout.readlines() | ||
1172 | 93 | for line in stdout_lines: | ||
1173 | 94 | self.logger.debug(line.strip()) | ||
1174 | 95 | |||
1175 | 96 | self.logger.debug('Standard error follows:') | ||
1176 | 97 | stderr_lines = stderr.readlines() | ||
1177 | 98 | for line in stderr_lines: | ||
1178 | 99 | self.logger.debug(line.strip()) | ||
1179 | 100 | |||
1180 | 101 | return retval, ''.join(stdout_lines), ''.join(stderr_lines) | ||
1181 | 102 | |||
1182 | 103 | def uploadfiles(self, files, target=os.path.normpath('/tmp/')): | ||
1183 | 104 | """ | ||
1184 | 105 | Copy a file or list of files to a target directory on the machine. | ||
1185 | 106 | """ | ||
1186 | 107 | if isinstance(files, basestring): | ||
1187 | 108 | files = [files] | ||
1188 | 109 | |||
1189 | 110 | self.activecheck() | ||
1190 | 111 | self.ssh_client.connect(self.name, | ||
1191 | 112 | username=config.user, | ||
1192 | 113 | key_filename=config.sshprivatekey) | ||
1193 | 114 | sftp_client = self.ssh_client.open_sftp() | ||
1194 | 115 | failed = [] | ||
1195 | 116 | try: | ||
1196 | 117 | for localpath in files: | ||
1197 | 118 | if os.path.isfile(localpath): | ||
1198 | 119 | self.logger.info('Uploading ' + localpath | ||
1199 | 120 | + ' from the host to ' + target | ||
1200 | 121 | + ' on the machine') | ||
1201 | 122 | remotepath = os.path.join(target, | ||
1202 | 123 | os.path.basename(localpath)) | ||
1203 | 124 | sftp_client.put(localpath, remotepath) | ||
1204 | 125 | else: | ||
1205 | 126 | failed.append(localpath) | ||
1206 | 127 | finally: | ||
1207 | 128 | sftp_client.close() | ||
1208 | 129 | if len(failed) > 0: | ||
1209 | 130 | err = UTAHProvisioningException('Files do not exist: ' | ||
1210 | 131 | + ' '.join(failed)) | ||
1211 | 132 | err.files = failed | ||
1212 | 133 | raise err | ||
1213 | 134 | |||
1214 | 135 | def downloadfiles(self, files, target=os.path.normpath('/tmp/')): | ||
1215 | 136 | """ | ||
1216 | 137 | Copy a file or list of files from the machine to a target directory on | ||
1217 | 138 | the local system. | ||
1218 | 139 | """ | ||
1219 | 140 | # TODO: check for directories and recurse into them | ||
1220 | 141 | if isinstance(files, basestring): | ||
1221 | 142 | files = [files] | ||
1222 | 143 | |||
1223 | 144 | self.activecheck() | ||
1224 | 145 | self.ssh_client.connect(self.name, | ||
1225 | 146 | username=config.user, | ||
1226 | 147 | key_filename=config.sshprivatekey) | ||
1227 | 148 | sftp_client = self.ssh_client.open_sftp() | ||
1228 | 149 | if os.path.isdir(target): | ||
1229 | 150 | get_localpath = lambda remotepath: \ | ||
1230 | 151 | os.path.join(target, os.path.basename(remotepath)) | ||
1231 | 152 | else: | ||
1232 | 153 | get_localpath = lambda remotepath: target | ||
1233 | 154 | |||
1234 | 155 | try: | ||
1235 | 156 | for remotepath in files: | ||
1236 | 157 | localpath = get_localpath(remotepath) | ||
1237 | 158 | self.logger.info('Downloading ' + remotepath | ||
1238 | 159 | + ' from the machine to ' + target | ||
1239 | 160 | + ' on the host') | ||
1240 | 161 | sftp_client.get(remotepath, localpath) | ||
1241 | 162 | finally: | ||
1242 | 163 | sftp_client.close() | ||
1243 | 164 | |||
1244 | 165 | def downloadfilesrecursive(self, files, target=os.path.normpath('/tmp/')): | ||
1245 | 166 | """ | ||
1246 | 167 | Recursively copy all files in files to the target directory target. | ||
1247 | 168 | """ | ||
1248 | 169 | self.activecheck() | ||
1249 | 170 | self.ssh_client.connect(self.name, | ||
1250 | 171 | username=config.user, | ||
1251 | 172 | key_filename=config.sshprivatekey) | ||
1252 | 173 | sftp_client = self.ssh_client.open_sftp() | ||
1253 | 174 | myfiles = [] | ||
1254 | 175 | |||
1255 | 176 | if isinstance(files, basestring): | ||
1256 | 177 | files = [files] | ||
1257 | 178 | |||
1258 | 179 | for myfile in files: | ||
1259 | 180 | newtarget = os.path.join(target, os.path.basename(myfile)) | ||
1260 | 181 | if S_ISDIR(sftp_client.stat(myfile).st_mode): | ||
1261 | 182 | self.logger.debug(myfile + ' is a directory, recursing') | ||
1262 | 183 | if not os.path.isdir(newtarget): | ||
1263 | 184 | self.logger.debug('Attempting to create ' + newtarget) | ||
1264 | 185 | os.makedirs(newtarget) | ||
1265 | 186 | myfiles = [os.path.join(myfile, x) | ||
1266 | 187 | for x in sftp_client.listdir(myfile)] | ||
1267 | 188 | self.downloadfilesrecursive(myfiles, newtarget) | ||
1268 | 189 | else: | ||
1269 | 190 | self.downloadfiles(myfile, newtarget) | ||
1270 | 191 | |||
1271 | 192 | def destroy(self, *args, **kw): | ||
1272 | 193 | """ | ||
1273 | 194 | Clean up known hosts for machine. | ||
1274 | 195 | """ | ||
1275 | 196 | # TODO: evaluate value of known_hosts with paramiko | ||
1276 | 197 | self.logger.info('Removing machine addresses ' | ||
1277 | 198 | 'from ssh known_hosts file') | ||
1278 | 199 | addresses = [self.name] | ||
1279 | 200 | try: | ||
1280 | 201 | addresses.append(socket.gethostbyname(self.name)) | ||
1281 | 202 | except socket.gaierror as err: | ||
1282 | 203 | if err.errno in [-2, -5]: | ||
1283 | 204 | self.logger.debug(self.name | ||
1284 | 205 | + ' is not resolvable, ' | ||
1285 | 206 | + 'so not removing from known_hosts') | ||
1286 | 207 | else: | ||
1287 | 208 | raise err | ||
1288 | 209 | |||
1289 | 210 | old_host_keys = self.ssh_client.get_host_keys() | ||
1290 | 211 | new_host_keys = paramiko.HostKeys() | ||
1291 | 212 | addresses = set(addresses) | ||
1292 | 213 | for address, key in old_host_keys.iteritems(): | ||
1293 | 214 | # Skip keys so that they don't get added | ||
1294 | 215 | # into the new keys (i.e. they're removed) | ||
1295 | 216 | if address in addresses: | ||
1296 | 217 | continue | ||
1297 | 218 | new_host_keys[address] = key | ||
1298 | 219 | new_host_keys.save(config.sshknownhosts) | ||
1299 | 220 | self.ssh_client.close() | ||
1300 | 221 | |||
1301 | 222 | super(SSHMixin, self).destroy(*args, **kw) | ||
1302 | 223 | |||
1303 | 224 | def sshcheck(self, timeout=config.checktimeout): | ||
1304 | 225 | """ | ||
1305 | 226 | Sleep for a while and check if the machine is available via ssh. | ||
1306 | 227 | Return a retryable exception if it is not. | ||
1307 | 228 | Intended for use with retry. | ||
1308 | 229 | """ | ||
1309 | 230 | self.logger.info('Sleeping {timeout} seconds' | ||
1310 | 231 | .format(timeout=timeout)) | ||
1311 | 232 | time.sleep(timeout) | ||
1312 | 233 | self.logger.info('Checking for ssh availability') | ||
1313 | 234 | try: | ||
1314 | 235 | self.ssh_client.connect(self.name, | ||
1315 | 236 | username=config.user, | ||
1316 | 237 | key_filename=config.sshprivatekey) | ||
1317 | 238 | except socket.error as err: | ||
1318 | 239 | raise UTAHProvisioningException(str(err), retry=True) | ||
1319 | 240 | |||
1320 | 241 | def sshpoll(self, timeout=None, | ||
1321 | 242 | checktimeout=config.checktimeout, logmethod=None): | ||
1322 | 243 | """ | ||
1323 | 244 | Run sshcheck over and over until timeout expires. | ||
1324 | 245 | """ | ||
1325 | 246 | if timeout is None: | ||
1326 | 247 | timeout = self.boottimeout | ||
1327 | 248 | if logmethod is None: | ||
1328 | 249 | logmethod = self.logger.debug | ||
1329 | 250 | utah.timeout.timeout(timeout, retry, self.sshcheck, checktimeout, | ||
1330 | 251 | logmethod=logmethod) | ||
1331 | 252 | |||
1332 | 253 | def activecheck(self): | ||
1333 | 254 | """ | ||
1334 | 255 | Start the machine if needed, and check for SSH login. | ||
1335 | 256 | """ | ||
1336 | 257 | self.logger.debug('Checking if machine is active') | ||
1337 | 258 | self.provisioncheck() | ||
1338 | 259 | if not self.active: | ||
1339 | 260 | self._start() | ||
1340 | 261 | self.sshcheck() | ||
1341 | 0 | 262 | ||
1342 | === removed file 'utah/provisioning/vm/libvirtvm.py' | |||
1343 | --- utah/provisioning/vm/libvirtvm.py 2012-12-03 14:02:18 +0000 | |||
1344 | +++ utah/provisioning/vm/libvirtvm.py 1970-01-01 00:00:00 +0000 | |||
1345 | @@ -1,777 +0,0 @@ | |||
1346 | 1 | # Ubuntu Testing Automation Harness | ||
1347 | 2 | # Copyright 2012 Canonical Ltd. | ||
1348 | 3 | |||
1349 | 4 | # This program is free software: you can redistribute it and/or modify it | ||
1350 | 5 | # under the terms of the GNU General Public License version 3, as published | ||
1351 | 6 | # by the Free Software Foundation. | ||
1352 | 7 | |||
1353 | 8 | # This program is distributed in the hope that it will be useful, but | ||
1354 | 9 | # WITHOUT ANY WARRANTY; without even the implied warranties of | ||
1355 | 10 | # MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR | ||
1356 | 11 | # PURPOSE. See the GNU General Public License for more details. | ||
1357 | 12 | |||
1358 | 13 | # You should have received a copy of the GNU General Public License along | ||
1359 | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. | ||
1360 | 15 | |||
1361 | 16 | """ | ||
1362 | 17 | Provide classes to provision libvirt-based virtual machines. | ||
1363 | 18 | """ | ||
1364 | 19 | |||
1365 | 20 | import os | ||
1366 | 21 | import random | ||
1367 | 22 | import shutil | ||
1368 | 23 | import string | ||
1369 | 24 | import subprocess | ||
1370 | 25 | import tempfile | ||
1371 | 26 | |||
1372 | 27 | from xml.etree import ElementTree | ||
1373 | 28 | |||
1374 | 29 | import apt.cache | ||
1375 | 30 | import libvirt | ||
1376 | 31 | |||
1377 | 32 | from utah.provisioning.provisioning import ( | ||
1378 | 33 | SSHMixin, | ||
1379 | 34 | CustomInstallMixin, | ||
1380 | 35 | ) | ||
1381 | 36 | from utah.provisioning.vm.vm import VM | ||
1382 | 37 | from utah.provisioning.vm.exceptions import UTAHVMProvisioningException | ||
1383 | 38 | from utah import config | ||
1384 | 39 | from utah.process import ProcessChecker | ||
1385 | 40 | from utah.timeout import UTAHTimeout | ||
1386 | 41 | |||
1387 | 42 | |||
1388 | 43 | class LibvirtVM(VM): | ||
1389 | 44 | """ | ||
1390 | 45 | Provide a class to utilize VMs using libvirt. | ||
1391 | 46 | |||
1392 | 47 | Capable of utilizing existing VMs. | ||
1393 | 48 | Creation currently handled by sublcasses. | ||
1394 | 49 | """ | ||
1395 | 50 | def __init__(self, *args, **kw): | ||
1396 | 51 | super(LibvirtVM, self).__init__(*args, **kw) | ||
1397 | 52 | libvirt.registerErrorHandler(self.libvirterrorhandler, None) | ||
1398 | 53 | self.lv = libvirt.open(config.qemupath) | ||
1399 | 54 | if self.lv is None: | ||
1400 | 55 | raise UTAHVMProvisioningException('Cannot connect to libvirt') | ||
1401 | 56 | self.logger.debug('LibvirtVM init finished') | ||
1402 | 57 | |||
1403 | 58 | def _load(self): | ||
1404 | 59 | """ | ||
1405 | 60 | Load an existing VM. | ||
1406 | 61 | """ | ||
1407 | 62 | self.logger.info('Loading VM') | ||
1408 | 63 | self.vm = self.lv.lookupByName(self.name) | ||
1409 | 64 | self.logger.info('VM loaded') | ||
1410 | 65 | return True | ||
1411 | 66 | |||
1412 | 67 | def _provision(self): | ||
1413 | 68 | """ | ||
1414 | 69 | Make an existing VM available using libvirt to look up the VM by name. | ||
1415 | 70 | """ | ||
1416 | 71 | self.logger.info('Provisioning VM') | ||
1417 | 72 | if self.new: | ||
1418 | 73 | self.logger.debug('New VM requested') | ||
1419 | 74 | try: | ||
1420 | 75 | self._load() | ||
1421 | 76 | self.logger.error('VM already exists') | ||
1422 | 77 | raise UTAHVMProvisioningException('Request new VM, but ' | ||
1423 | 78 | + self.name | ||
1424 | 79 | + ' already exists') | ||
1425 | 80 | except libvirt.libvirtError as err: | ||
1426 | 81 | if err.get_error_code() == 42: | ||
1427 | 82 | self._create() | ||
1428 | 83 | else: | ||
1429 | 84 | raise err | ||
1430 | 85 | |||
1431 | 86 | try: | ||
1432 | 87 | self._load() | ||
1433 | 88 | except libvirt.libvirtError as err: | ||
1434 | 89 | if err.get_error_code() == 42: | ||
1435 | 90 | self.logger.debug('Lookup failed') | ||
1436 | 91 | try: | ||
1437 | 92 | self._create() | ||
1438 | 93 | self._load() | ||
1439 | 94 | except UTAHVMProvisioningException as error: | ||
1440 | 95 | self.logger.error('VM lookup failed') | ||
1441 | 96 | raise UTAHVMProvisioningException('Cannot find VM named ' | ||
1442 | 97 | + self.name + | ||
1443 | 98 | ' and ' + str(error)) | ||
1444 | 99 | else: | ||
1445 | 100 | raise err | ||
1446 | 101 | self.provisioned = True | ||
1447 | 102 | self.logger.info('VM provisioned') | ||
1448 | 103 | |||
1449 | 104 | def activecheck(self): | ||
1450 | 105 | """ | ||
1451 | 106 | Verify the machine is provisioned, then start it if it is not started. | ||
1452 | 107 | """ | ||
1453 | 108 | self.logger.debug('Checking if VM is active') | ||
1454 | 109 | self.provisioncheck() | ||
1455 | 110 | if self.vm is not None: | ||
1456 | 111 | if self.vm.isActive() == 0: | ||
1457 | 112 | self._start() | ||
1458 | 113 | else: | ||
1459 | 114 | self.active = True | ||
1460 | 115 | else: | ||
1461 | 116 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
1462 | 117 | |||
1463 | 118 | def _start(self): | ||
1464 | 119 | """ | ||
1465 | 120 | Start the VM. | ||
1466 | 121 | """ | ||
1467 | 122 | self.logger.info('Starting VM') | ||
1468 | 123 | if self.vm is not None: | ||
1469 | 124 | if self.vm.isActive() == 0: | ||
1470 | 125 | self.vm.create() | ||
1471 | 126 | else: | ||
1472 | 127 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
1473 | 128 | self.active = True | ||
1474 | 129 | |||
1475 | 130 | def stop(self, force=False): | ||
1476 | 131 | """ | ||
1477 | 132 | Stop the machine. | ||
1478 | 133 | Setting force to true will do a hard shutdown instead of a graceful | ||
1479 | 134 | one. | ||
1480 | 135 | """ | ||
1481 | 136 | self.logger.info('Stopping VM') | ||
1482 | 137 | if self.vm is not None: | ||
1483 | 138 | if self.vm.isActive() == 0: | ||
1484 | 139 | self.logger.info('VM is already stopped') | ||
1485 | 140 | else: | ||
1486 | 141 | if force: | ||
1487 | 142 | self.logger.info('Forced shutdown requested') | ||
1488 | 143 | self.vm.destroy() | ||
1489 | 144 | else: | ||
1490 | 145 | self.vm.shutdown() | ||
1491 | 146 | else: | ||
1492 | 147 | self.logger.info('VM not yet created') | ||
1493 | 148 | self.active = False | ||
1494 | 149 | |||
1495 | 150 | def libvirterrorhandler(self, _context, err): | ||
1496 | 151 | """ | ||
1497 | 152 | Log libvirt errors instead of sending them directly to the console. | ||
1498 | 153 | """ | ||
1499 | 154 | errorcode = err.get_error_code() | ||
1500 | 155 | if errorcode in [9, 42]: | ||
1501 | 156 | # We see these as part of normal operations, | ||
1502 | 157 | # so we send them to debug | ||
1503 | 158 | # 9 is trying to create a VM that already exists | ||
1504 | 159 | # 42 is trying to load a VM that doesn't exist | ||
1505 | 160 | logmethod = self.logger.debug | ||
1506 | 161 | else: | ||
1507 | 162 | logmethod = self.logger.error | ||
1508 | 163 | logmethod('libvirt error: ' + err['message']) | ||
1509 | 164 | logmethod('libvirt error number is: ' + str(errorcode)) | ||
1510 | 165 | |||
1511 | 166 | |||
1512 | 167 | class VMToolsVM(SSHMixin, LibvirtVM): | ||
1513 | 168 | """ | ||
1514 | 169 | Provide a class to provision a VM using the ubuntu-qa-tools vm-tools. | ||
1515 | 170 | """ | ||
1516 | 171 | def __init__(self, machineid=None, prefix='utah', *args, **kw): | ||
1517 | 172 | if not apt.cache.Cache()['vm-tools'].is_installed: | ||
1518 | 173 | raise UTAHVMProvisioningException( | ||
1519 | 174 | 'vm-tools is not installed. ' | ||
1520 | 175 | 'Try: sudo apt-get install vm-tools') | ||
1521 | 176 | super(VMToolsVM, self).__init__(*args, machineid=machineid, name=None, | ||
1522 | 177 | **kw) | ||
1523 | 178 | self.logger.debug('VMToolsVM init finished') | ||
1524 | 179 | |||
1525 | 180 | def _start(self): | ||
1526 | 181 | """ | ||
1527 | 182 | Start the VM using vm-start, which will wait until SSH is up. | ||
1528 | 183 | """ | ||
1529 | 184 | self.logger.info('Starting the vm using vm-start') | ||
1530 | 185 | args = ['vm-start', '-v', '-w', self.name] | ||
1531 | 186 | self.active = (self._runargs(args) == 0) | ||
1532 | 187 | return self.active | ||
1533 | 188 | |||
1534 | 189 | def activecheck(self): | ||
1535 | 190 | """ | ||
1536 | 191 | Verify the machine is provisioned, then start it if it is not started. | ||
1537 | 192 | Use vm-wait to make sure it's up. | ||
1538 | 193 | """ | ||
1539 | 194 | self.provisioncheck() | ||
1540 | 195 | self.logger.debug('Checking if VMToolsVM is active') | ||
1541 | 196 | self._start() | ||
1542 | 197 | self.logger.info('Using vm-wait to ensure VM is active') | ||
1543 | 198 | if (self._runargs(['vm-wait', self.name, '300']) != 0): | ||
1544 | 199 | self.active = False | ||
1545 | 200 | raise UTAHVMProvisioningException('Timed out waiting for VM ' | ||
1546 | 201 | 'to be reachable') | ||
1547 | 202 | else: | ||
1548 | 203 | self.active = True | ||
1549 | 204 | |||
1550 | 205 | def _getcreationargs(self): | ||
1551 | 206 | """ | ||
1552 | 207 | Return the vm-new syntax used to create the VM. | ||
1553 | 208 | Can be used for debugging or instructional purposes. | ||
1554 | 209 | """ | ||
1555 | 210 | args = ['vm-new', '-f', '-r', '-v', '-b', config.bridge, | ||
1556 | 211 | '-t', self.installtype, self.series, self.arch, self.prefix] | ||
1557 | 212 | self.logger.debug('VM Creation args: ' + str(args)) | ||
1558 | 213 | return args | ||
1559 | 214 | |||
1560 | 215 | def _create(self): | ||
1561 | 216 | """ | ||
1562 | 217 | Run the command generated by getcreationargs | ||
1563 | 218 | and either return a good status or raise an exception. | ||
1564 | 219 | """ | ||
1565 | 220 | self.logger.info('Creating vm using vm-new') | ||
1566 | 221 | self.logger.info('This may take up to two hours, ' | ||
1567 | 222 | 'excluding download times, and may go over an hour ' | ||
1568 | 223 | 'without output during virt-install') | ||
1569 | 224 | args = self._getcreationargs() | ||
1570 | 225 | percent = 0 | ||
1571 | 226 | p = subprocess.Popen(args, | ||
1572 | 227 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT) | ||
1573 | 228 | while p.poll() is None: | ||
1574 | 229 | line = p.stdout.readline().strip() | ||
1575 | 230 | self.logger.debug(line) | ||
1576 | 231 | if 'Fetching release iso' in line: | ||
1577 | 232 | self.logger.info('Downloading ISO') | ||
1578 | 233 | if len(line.split()) >= 3 and '%' in line.split()[-3]: | ||
1579 | 234 | dlpercent = int(line.split()[-3].strip('%')) | ||
1580 | 235 | if dlpercent >= percent: | ||
1581 | 236 | self.logger.info('ISO ' + str(dlpercent) + '% downloaded, ' | ||
1582 | 237 | + line.split(' ')[-1] + ' remaining') | ||
1583 | 238 | percent += self.dlpercentincrement | ||
1584 | 239 | if ' saved ' in line: | ||
1585 | 240 | self.logger.info('ISO download complete') | ||
1586 | 241 | if 'Creating preseeded iso' in line: | ||
1587 | 242 | self.logger.info('Preseeding ISO') | ||
1588 | 243 | if 'virt-install' in line: | ||
1589 | 244 | self.logger.info('Installing system on VM ' | ||
1590 | 245 | '(may take over an hour)') | ||
1591 | 246 | self.logger.info('You can watch the progress with:') | ||
1592 | 247 | self.logger.info("\tvm-view " + self.name) | ||
1593 | 248 | self.logger.info('Take care not to interrupt the install') | ||
1594 | 249 | if 'Verifying lsb_release' in line: | ||
1595 | 250 | self.logger.info('Waiting for post-install ' | ||
1596 | 251 | 'and verifying system ' | ||
1597 | 252 | '(may take over a half hour)') | ||
1598 | 253 | if "No domains available for virt type 'hvm'" in line: | ||
1599 | 254 | self.logger.error('No hardware virtual machine ' | ||
1600 | 255 | 'support available') | ||
1601 | 256 | self.logger.info('Please ensure the following:') | ||
1602 | 257 | self.logger.info("\tYour processor supports hardware " | ||
1603 | 258 | "virtualization extensions") | ||
1604 | 259 | self.logger.info("\tHardware virtualization " | ||
1605 | 260 | "is not disabled in the BIOS") | ||
1606 | 261 | self.logger.info("\tkvm is installed") | ||
1607 | 262 | self.logger.info("\tThe kvm and processor-specific " | ||
1608 | 263 | "kvm kernel modules are installed and loaded") | ||
1609 | 264 | self.logger.error('Software virtual machine support ' | ||
1610 | 265 | 'is implemented in the CustomVM class') | ||
1611 | 266 | raise UTAHVMProvisioningException( | ||
1612 | 267 | 'No hardware virtual machine support available') | ||
1613 | 268 | if ('Could not find' in line | ||
1614 | 269 | and ".uqt-vm-tools.conf' configuration file!" in line): | ||
1615 | 270 | self.logger.error('No .uqt-vm-tools.conf configuration file ' | ||
1616 | 271 | 'found in home directory') | ||
1617 | 272 | self.logger.info('If you are running as the utah user, ' | ||
1618 | 273 | 'you can use the packaged config file:') | ||
1619 | 274 | self.logger.info("\tln -s /etc/utah/uqt-vm-tools.conf " | ||
1620 | 275 | "~utah/.uqt-vm-tools.conf") | ||
1621 | 276 | self.logger.info('As a different user, ' | ||
1622 | 277 | 'run vm-new with no arguments, ' | ||
1623 | 278 | 'and answer y when prompted ' | ||
1624 | 279 | 'to create an initial config file') | ||
1625 | 280 | self.logger.info('We recommend adding python-yaml ' | ||
1626 | 281 | 'to the vm_extra_packages line ' | ||
1627 | 282 | 'of this file, i.e.:') | ||
1628 | 283 | self.logger.info("\tsed " | ||
1629 | 284 | "'s/^vm_extra_packages=\"" | ||
1630 | 285 | "/vm_extra_packages=\"python-yaml /' " | ||
1631 | 286 | "-i ~/.uqt_vm_tools.conf") | ||
1632 | 287 | raise UTAHVMProvisioningException( | ||
1633 | 288 | 'No vm-tools config file available; ' | ||
1634 | 289 | 'more info available in ' | ||
1635 | 290 | + self.filehandler.baseFilename) | ||
1636 | 291 | if p.returncode == 0: | ||
1637 | 292 | return True | ||
1638 | 293 | else: | ||
1639 | 294 | raise UTAHVMProvisioningException( | ||
1640 | 295 | 'Failed to create VM: vm-new exit status: ' | ||
1641 | 296 | + str(p.returncode)) | ||
1642 | 297 | |||
1643 | 298 | def destroy(self): | ||
1644 | 299 | """ | ||
1645 | 300 | Use vm-remove to destroy the vm. | ||
1646 | 301 | """ | ||
1647 | 302 | self.logger.info('Destroying vm using vm-remove') | ||
1648 | 303 | self.provisioned = False | ||
1649 | 304 | args = ['vm-remove', '-f', self.name] | ||
1650 | 305 | return (self._runargs(args) == 0) | ||
1651 | 306 | |||
1652 | 307 | def _getcommandargs(self, command, quiet=None, root=False, timeout=300): | ||
1653 | 308 | """ | ||
1654 | 309 | Get the arguments to send to vm-cmd. | ||
1655 | 310 | Used by multiple other functions. | ||
1656 | 311 | """ | ||
1657 | 312 | if quiet is None: | ||
1658 | 313 | quiet = not self.debug | ||
1659 | 314 | args = ['vm-cmd', '-f'] | ||
1660 | 315 | if timeout is not None: | ||
1661 | 316 | args.extend(['-s', '-t', str(timeout)]) | ||
1662 | 317 | if quiet: | ||
1663 | 318 | args.append('-q') | ||
1664 | 319 | if root: | ||
1665 | 320 | args.append('-r') | ||
1666 | 321 | args.append(self.name) | ||
1667 | 322 | if isinstance(command, basestring): | ||
1668 | 323 | args.append(command) | ||
1669 | 324 | else: | ||
1670 | 325 | args.extend(command) | ||
1671 | 326 | self.logger.debug('vm-cmd command args: ' + str(args)) | ||
1672 | 327 | return args | ||
1673 | 328 | |||
1674 | 329 | def run(self, command, quiet=None, root=False, timeout=300): | ||
1675 | 330 | """ | ||
1676 | 331 | Run a command using vm-cmd. | ||
1677 | 332 | Timeout defaults to 300 seconds, but can be set to None to not send a | ||
1678 | 333 | timeout argument. | ||
1679 | 334 | """ | ||
1680 | 335 | if quiet is None: | ||
1681 | 336 | quiet = not self.debug | ||
1682 | 337 | self.activecheck() | ||
1683 | 338 | args = self._getcommandargs(command=command, quiet=quiet, | ||
1684 | 339 | root=root, timeout=timeout) | ||
1685 | 340 | self.logger.info('Running command on VM: ' + ' '.join(args)) | ||
1686 | 341 | p = subprocess.Popen(args, | ||
1687 | 342 | stdout=subprocess.PIPE, stderr=subprocess.PIPE) | ||
1688 | 343 | while p.poll() is None: | ||
1689 | 344 | pass | ||
1690 | 345 | return p.returncode, p.communicate()[0], p.communicate()[1] | ||
1691 | 346 | |||
1692 | 347 | def uploadfiles(self, files, target=os.path.normpath('/tmp/')): | ||
1693 | 348 | """ | ||
1694 | 349 | Copy a file or list of files to a target directory on the machine. | ||
1695 | 350 | """ | ||
1696 | 351 | if isinstance(files, basestring): | ||
1697 | 352 | files = [files] | ||
1698 | 353 | if not target.endswith('/'): | ||
1699 | 354 | target += '/' | ||
1700 | 355 | self.activecheck() | ||
1701 | 356 | success = True | ||
1702 | 357 | for myfile in files: | ||
1703 | 358 | self.logger.info('Uploading ' + myfile | ||
1704 | 359 | + ' from the host to ' + target + ' on the VM') | ||
1705 | 360 | returncode = self._runargs(['vm-scp', '-f', '-p', | ||
1706 | 361 | self.name, myfile, target]) | ||
1707 | 362 | if returncode != 0: | ||
1708 | 363 | success = False | ||
1709 | 364 | return success | ||
1710 | 365 | |||
1711 | 366 | |||
1712 | 367 | class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM): | ||
1713 | 368 | """ | ||
1714 | 369 | Install a VM from an image using libvirt direct kernel booting. | ||
1715 | 370 | """ | ||
1716 | 371 | def __init__(self, diskbus=None, disksizes=None, emulator=None, | ||
1717 | 372 | machineid=None, macs=None, name=None, prefix='utah', *args, | ||
1718 | 373 | **kw): | ||
1719 | 374 | # Make sure that no other virtualization solutions are running | ||
1720 | 375 | # TODO: see if this is needed for qemu or just kvm | ||
1721 | 376 | process_checker = ProcessChecker() | ||
1722 | 377 | for cmdline, app in [('/usr/lib/virtualbox/VirtualBox', 'VirtualBox'), | ||
1723 | 378 | ('/usr/lib/vmware/bin', 'VMware')]: | ||
1724 | 379 | if process_checker.check_cmdline(cmdline): | ||
1725 | 380 | message = process_checker.get_error_message(app) | ||
1726 | 381 | raise UTAHVMProvisioningException(message) | ||
1727 | 382 | |||
1728 | 383 | if diskbus is None: | ||
1729 | 384 | self.diskbus = config.diskbus | ||
1730 | 385 | else: | ||
1731 | 386 | self.diskbus = diskbus | ||
1732 | 387 | if disksizes is None: | ||
1733 | 388 | disksizes = config.disksizes | ||
1734 | 389 | if disksizes is None: | ||
1735 | 390 | self.disksizes = [8] | ||
1736 | 391 | else: | ||
1737 | 392 | self.disksizes = disksizes | ||
1738 | 393 | self.disks = [] | ||
1739 | 394 | if name is None: | ||
1740 | 395 | autoname = True | ||
1741 | 396 | name = '-'.join([str(prefix), str(machineid)]) | ||
1742 | 397 | else: | ||
1743 | 398 | autoname = False | ||
1744 | 399 | super(CustomVM, self).__init__(machineid=machineid, name=name, *args, | ||
1745 | 400 | **kw) | ||
1746 | 401 | # TODO: do a better job of separating installation | ||
1747 | 402 | # into _create rather than __init__ | ||
1748 | 403 | if self.image is None: | ||
1749 | 404 | raise UTAHVMProvisioningException('Image file required ' | ||
1750 | 405 | 'for custom VM installation') | ||
1751 | 406 | self._custominit() | ||
1752 | 407 | if autoname: | ||
1753 | 408 | self._namesetup() | ||
1754 | 409 | self._loggerunsetup() | ||
1755 | 410 | self._loggersetup() | ||
1756 | 411 | self._dirsetup() | ||
1757 | 412 | if emulator is None: | ||
1758 | 413 | emulator = config.emulator | ||
1759 | 414 | if emulator is None: | ||
1760 | 415 | if self._supportsdomaintype('kvm'): | ||
1761 | 416 | self.logger.info('Setting type to kvm ' | ||
1762 | 417 | 'since it is present in libvirt capabilities') | ||
1763 | 418 | self.domaintype = 'kvm' | ||
1764 | 419 | elif self._supportsdomaintype('qemu'): | ||
1765 | 420 | self.logger.info('Setting type to qemu ' | ||
1766 | 421 | 'since it is present in libvirt capabilities') | ||
1767 | 422 | self.domaintype = 'qemu' | ||
1768 | 423 | else: | ||
1769 | 424 | raise UTAHVMProvisioningException( | ||
1770 | 425 | 'kvm and qemu not supported in libvirt capabilities; ' | ||
1771 | 426 | 'please make sure qemu and/or kvm are installed ' | ||
1772 | 427 | 'and libvirt is configured correctly') | ||
1773 | 428 | else: | ||
1774 | 429 | self.domaintype = emulator | ||
1775 | 430 | if self.domaintype == 'qemu': | ||
1776 | 431 | self.logger.debug('Raising boot timeout for qemu domain') | ||
1777 | 432 | self.boottimeout *= 4 | ||
1778 | 433 | if macs is None: | ||
1779 | 434 | macs = [] | ||
1780 | 435 | self.macs = macs | ||
1781 | 436 | self.dircheck() | ||
1782 | 437 | self.logger.debug('CustomVM init finished') | ||
1783 | 438 | |||
1784 | 439 | def _createdisks(self, disksizes=None): | ||
1785 | 440 | """ | ||
1786 | 441 | Create disk files if needed and build a list of them. | ||
1787 | 442 | """ | ||
1788 | 443 | self.logger.info('Creating disks') | ||
1789 | 444 | if disksizes is None: | ||
1790 | 445 | disksizes = self.disksizes | ||
1791 | 446 | for index, size in enumerate(disksizes): | ||
1792 | 447 | disksize = '{}G'.format(size) | ||
1793 | 448 | basename = 'disk{}.qcow2'.format(index) | ||
1794 | 449 | diskfile = os.path.join(self.directory, basename) | ||
1795 | 450 | if not os.path.isfile(diskfile): | ||
1796 | 451 | cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize] | ||
1797 | 452 | self.logger.debug('Creating ' + disksize + ' disk using:') | ||
1798 | 453 | self.logger.debug(' '.join(cmd)) | ||
1799 | 454 | if self._runargs(cmd) != 0: | ||
1800 | 455 | raise UTAHVMProvisioningException( | ||
1801 | 456 | 'Could not create disk image at ' + diskfile) | ||
1802 | 457 | disk = {'bus': self.diskbus, | ||
1803 | 458 | 'file': diskfile, | ||
1804 | 459 | 'size': disksize, | ||
1805 | 460 | 'type': 'qcow2'} | ||
1806 | 461 | self.disks.append(disk) | ||
1807 | 462 | self.logger.debug('Adding disk to list') | ||
1808 | 463 | |||
1809 | 464 | def _supportsdomaintype(self, domaintype): | ||
1810 | 465 | """ | ||
1811 | 466 | Check emulator support in libvirt capabilities. | ||
1812 | 467 | """ | ||
1813 | 468 | capabilities = ElementTree.fromstring(self.lv.getCapabilities()) | ||
1814 | 469 | for guest in capabilities.iterfind('guest'): | ||
1815 | 470 | for arch in guest.iterfind('arch'): | ||
1816 | 471 | for domain in arch.iterfind('domain'): | ||
1817 | 472 | if domaintype in domain.get('type'): | ||
1818 | 473 | return True | ||
1819 | 474 | return False | ||
1820 | 475 | |||
1821 | 476 | def _installxml(self, cmdline=None, image=None, initrd=None, | ||
1822 | 477 | kernel=None, tmpdir=None, xml=None): | ||
1823 | 478 | """ | ||
1824 | 479 | Return the XML tree to be passed to libvirt for VM installation. | ||
1825 | 480 | """ | ||
1826 | 481 | self.logger.info('Creating installation XML') | ||
1827 | 482 | if cmdline is None: | ||
1828 | 483 | cmdline = self.cmdline | ||
1829 | 484 | if image is None: | ||
1830 | 485 | image = self.image.image | ||
1831 | 486 | if initrd is None: | ||
1832 | 487 | initrd = self.initrd | ||
1833 | 488 | if kernel is None: | ||
1834 | 489 | kernel = self.kernel | ||
1835 | 490 | if xml is None: | ||
1836 | 491 | xml = self.xml | ||
1837 | 492 | if tmpdir is None: | ||
1838 | 493 | tmpdir = self.tmpdir | ||
1839 | 494 | xmlt = ElementTree.ElementTree(file=xml) | ||
1840 | 495 | if self.rewrite in ['all', 'minimal']: | ||
1841 | 496 | self.logger.debug('Setting VM to shutdown on reboot') | ||
1842 | 497 | xmlt.find('on_reboot').text = 'destroy' | ||
1843 | 498 | if self.rewrite == 'all': | ||
1844 | 499 | self._installxml_rewrite_all(cmdline, image, initrd, kernel, | ||
1845 | 500 | xmlt) | ||
1846 | 501 | else: | ||
1847 | 502 | self.logger.info('Not rewriting XML because rewrite is ' + | ||
1848 | 503 | self.rewrite) | ||
1849 | 504 | if self.debug: | ||
1850 | 505 | xmlt.write(os.path.join(tmpdir, 'install.xml')) | ||
1851 | 506 | self.logger.info('Installation XML ready') | ||
1852 | 507 | return xmlt | ||
1853 | 508 | |||
1854 | 509 | def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt, | ||
1855 | 510 | kernel_txt, xmlt): | ||
1856 | 511 | """ | ||
1857 | 512 | Rewrite the whole configuration file for the VM | ||
1858 | 513 | """ | ||
1859 | 514 | self.logger.debug('Rewriting basic info') | ||
1860 | 515 | xmlt.find('name').text = self.name | ||
1861 | 516 | xmlt.find('uuid').text = self.uuid | ||
1862 | 517 | self.logger.debug('Setting type to qemu in case no ' | ||
1863 | 518 | 'hardware virtualization present') | ||
1864 | 519 | xmlt.getroot().set('type', self.domaintype) | ||
1865 | 520 | ose = xmlt.find('os') | ||
1866 | 521 | if self.arch == ('i386'): | ||
1867 | 522 | ose.find('type').set('arch', 'i686') | ||
1868 | 523 | elif self.arch == ('amd64'): | ||
1869 | 524 | ose.find('type').set('arch', 'x86_64') | ||
1870 | 525 | else: | ||
1871 | 526 | ose.find('type').set('arch', self.arch) | ||
1872 | 527 | self.logger.debug('Setting up boot info') | ||
1873 | 528 | for kernele in list(ose.iterfind('kernel')): | ||
1874 | 529 | ose.remove(kernele) | ||
1875 | 530 | kernele = ElementTree.Element('kernel') | ||
1876 | 531 | kernele.text = kernel_txt | ||
1877 | 532 | ose.append(kernele) | ||
1878 | 533 | for initrde in list(ose.iterfind('initrd')): | ||
1879 | 534 | ose.remove(initrde) | ||
1880 | 535 | initrde = ElementTree.Element('initrd') | ||
1881 | 536 | initrde.text = initrd_txt | ||
1882 | 537 | ose.append(initrde) | ||
1883 | 538 | for cmdlinee in list(ose.iterfind('cmdline')): | ||
1884 | 539 | ose.remove(cmdlinee) | ||
1885 | 540 | cmdlinee = ElementTree.Element('cmdline') | ||
1886 | 541 | cmdlinee.text = cmdline_txt | ||
1887 | 542 | ose.append(cmdlinee) | ||
1888 | 543 | self.logger.debug('Setting up devices') | ||
1889 | 544 | devices = xmlt.find('devices') | ||
1890 | 545 | self.logger.debug('Setting up disks') | ||
1891 | 546 | for disk in list(devices.iterfind('disk')): | ||
1892 | 547 | if disk.get('device') == 'disk': | ||
1893 | 548 | devices.remove(disk) | ||
1894 | 549 | self.logger.debug('Removed existing disk') | ||
1895 | 550 | #TODO: Add a cdrom if none exists | ||
1896 | 551 | if disk.get('device') == 'cdrom': | ||
1897 | 552 | if disk.find('source') is not None: | ||
1898 | 553 | disk.find('source').set('file', image) | ||
1899 | 554 | self.logger.debug('Rewrote existing CD-ROM') | ||
1900 | 555 | else: | ||
1901 | 556 | source = ElementTree.Element('source') | ||
1902 | 557 | source.set('file', image) | ||
1903 | 558 | disk.append(source) | ||
1904 | 559 | self.logger.debug('Added source to existing ' | ||
1905 | 560 | 'CD-ROM') | ||
1906 | 561 | for disk in self.disks: | ||
1907 | 562 | diske = ElementTree.Element('disk') | ||
1908 | 563 | diske.set('type', 'file') | ||
1909 | 564 | diske.set('device', 'disk') | ||
1910 | 565 | driver = ElementTree.Element('driver') | ||
1911 | 566 | driver.set('name', 'qemu') | ||
1912 | 567 | driver.set('type', disk['type']) | ||
1913 | 568 | diske.append(driver) | ||
1914 | 569 | source = ElementTree.Element('source') | ||
1915 | 570 | source.set('file', disk['file']) | ||
1916 | 571 | diske.append(source) | ||
1917 | 572 | target = ElementTree.Element('target') | ||
1918 | 573 | dev = "vd%s" % (string.ascii_lowercase[self.disks.index(disk)]) | ||
1919 | 574 | target.set('dev', dev) | ||
1920 | 575 | target.set('bus', disk['bus']) | ||
1921 | 576 | diske.append(target) | ||
1922 | 577 | devices.append(diske) | ||
1923 | 578 | self.logger.debug('Added ' + str(disk['size']) + ' disk') | ||
1924 | 579 | macs = list(self.macs) | ||
1925 | 580 | for interface in devices.iterfind('interface'): | ||
1926 | 581 | if interface.get('type') in ['network', 'bridge']: | ||
1927 | 582 | if len(macs) > 0: | ||
1928 | 583 | mac = macs.pop(0) | ||
1929 | 584 | interface.find('mac').set('address', mac) | ||
1930 | 585 | self.logger.debug('Rewrote interface ' | ||
1931 | 586 | 'to use specified mac address ' + mac) | ||
1932 | 587 | else: | ||
1933 | 588 | mac = random_mac_address() | ||
1934 | 589 | interface.find('mac').set('address', mac) | ||
1935 | 590 | self.macs.append(mac) | ||
1936 | 591 | self.logger.debug('Rewrote interface ' | ||
1937 | 592 | 'to use random mac address ' + mac) | ||
1938 | 593 | if interface.get('type') == 'bridge': | ||
1939 | 594 | interface.find('source').set('bridge', config.bridge) | ||
1940 | 595 | serial = ElementTree.Element('serial') | ||
1941 | 596 | serial.set('type', 'file') | ||
1942 | 597 | source = ElementTree.Element('source') | ||
1943 | 598 | log_filename = os.path.join(config.logpath, self.name + '.syslog.log') | ||
1944 | 599 | source.set('path', log_filename) | ||
1945 | 600 | serial.append(source) | ||
1946 | 601 | target = ElementTree.Element('target') | ||
1947 | 602 | target.set('port', '0') | ||
1948 | 603 | serial.append(target) | ||
1949 | 604 | devices.append(serial) | ||
1950 | 605 | |||
1951 | 606 | def _installvm(self, lv=None, tmpdir=None, xml=None): | ||
1952 | 607 | """ | ||
1953 | 608 | Install a VM, then undefine it in libvirt. | ||
1954 | 609 | The final installation will recreate the VM using the existing disks. | ||
1955 | 610 | """ | ||
1956 | 611 | self.logger.info('Creating VM') | ||
1957 | 612 | if lv is None: | ||
1958 | 613 | lv = self.lv | ||
1959 | 614 | if xml is None: | ||
1960 | 615 | xml = self.xml | ||
1961 | 616 | if tmpdir is None: | ||
1962 | 617 | tmpdir = self.tmpdir | ||
1963 | 618 | vm = lv.defineXML(ElementTree.tostring(xml.getroot())) | ||
1964 | 619 | os.chmod(tmpdir, 0755) | ||
1965 | 620 | vm.create() | ||
1966 | 621 | self.logger.info('Installing system on VM (may take over an hour)') | ||
1967 | 622 | self.logger.info('You can watch the progress with virt-viewer') | ||
1968 | 623 | log_filename = os.path.join(config.logpath, self.name + '.syslog.log') | ||
1969 | 624 | self.logger.info('Logs will be written to ' + log_filename) | ||
1970 | 625 | |||
1971 | 626 | while vm.isActive() is not 0: | ||
1972 | 627 | pass | ||
1973 | 628 | |||
1974 | 629 | vm.undefine() | ||
1975 | 630 | self.logger.info('Installation complete') | ||
1976 | 631 | |||
1977 | 632 | def _finalxml(self, tmpdir=None, xml=None): | ||
1978 | 633 | """ | ||
1979 | 634 | Create the XML to be used for the post-installation VM. | ||
1980 | 635 | This may be a transformation of the installation XML. | ||
1981 | 636 | """ | ||
1982 | 637 | self.logger.info('Creating final VM XML') | ||
1983 | 638 | if xml is None: | ||
1984 | 639 | xml = ElementTree.ElementTree(file=self.xml) | ||
1985 | 640 | if tmpdir is None: | ||
1986 | 641 | tmpdir = self.tmpdir | ||
1987 | 642 | if self.rewrite in ['all', 'minimal']: | ||
1988 | 643 | self.logger.debug('Setting VM to reboot normally on reboot') | ||
1989 | 644 | xml.find('on_reboot').text = 'restart' | ||
1990 | 645 | if self.rewrite == 'all': | ||
1991 | 646 | self.logger.debug('Removing VM install parameters') | ||
1992 | 647 | ose = xml.find('os') | ||
1993 | 648 | for kernel in ose.iterfind('kernel'): | ||
1994 | 649 | ose.remove(kernel) | ||
1995 | 650 | for initrd in ose.iterfind('initrd'): | ||
1996 | 651 | ose.remove(initrd) | ||
1997 | 652 | for cmdline in ose.iterfind('cmdline'): | ||
1998 | 653 | ose.remove(cmdline) | ||
1999 | 654 | devices = xml.find('devices') | ||
2000 | 655 | devices.remove(devices.find('serial')) | ||
2001 | 656 | for disk in list(devices.iterfind('disk')): | ||
2002 | 657 | if disk.get('device') == 'cdrom': | ||
2003 | 658 | disk.remove(disk.find('source')) | ||
2004 | 659 | else: | ||
2005 | 660 | self.logger.info('Not rewriting XML because rewrite is ' + | ||
2006 | 661 | self.rewrite) | ||
2007 | 662 | if self.debug: | ||
2008 | 663 | xml.write(os.path.join(tmpdir, 'final.xml')) | ||
2009 | 664 | return xml | ||
2010 | 665 | |||
2011 | 666 | def _tmpimage(self, image=None, tmpdir=None): | ||
2012 | 667 | """ | ||
2013 | 668 | Create a temporary copy of the image so libvirt will lock that copy. | ||
2014 | 669 | This allows other simultaneous processes to update the cached image. | ||
2015 | 670 | """ | ||
2016 | 671 | if image is None: | ||
2017 | 672 | image = self.image.image | ||
2018 | 673 | if tmpdir is None: | ||
2019 | 674 | tmpdir = self.tmpdir | ||
2020 | 675 | self.logger.info('Making temp copy of install image') | ||
2021 | 676 | tmpimage = os.path.join(tmpdir, os.path.basename(image)) | ||
2022 | 677 | self.logger.debug('Copying ' + image + ' to ' + tmpimage) | ||
2023 | 678 | shutil.copyfile(image, tmpimage) | ||
2024 | 679 | return tmpimage | ||
2025 | 680 | |||
2026 | 681 | def _create(self): | ||
2027 | 682 | """ | ||
2028 | 683 | Create the VM, install the system, and prepare it to boot. | ||
2029 | 684 | This primarily calls functions from CustomInstallMixin and CustomVM. | ||
2030 | 685 | """ | ||
2031 | 686 | self.logger.info('Creating custom virtual machine') | ||
2032 | 687 | |||
2033 | 688 | tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_') | ||
2034 | 689 | self.logger.debug('Working dir: ' + tmpdir) | ||
2035 | 690 | os.chdir(tmpdir) | ||
2036 | 691 | |||
2037 | 692 | kernel = self._preparekernel(kernel=self.kernel, tmpdir=tmpdir) | ||
2038 | 693 | |||
2039 | 694 | initrd = self._prepareinitrd(initrd=self.initrd, tmpdir=tmpdir) | ||
2040 | 695 | |||
2041 | 696 | self._unpackinitrd(initrd=initrd, tmpdir=tmpdir) | ||
2042 | 697 | |||
2043 | 698 | self._setuplatecommand(tmpdir=tmpdir) | ||
2044 | 699 | |||
2045 | 700 | self._setuppreseed(tmpdir=tmpdir) | ||
2046 | 701 | |||
2047 | 702 | if self.rewrite == 'all': | ||
2048 | 703 | self._setuplogging(tmpdir=tmpdir) | ||
2049 | 704 | else: | ||
2050 | 705 | self.logger.debug('Skipping logging setup because rewrite is' + | ||
2051 | 706 | self.rewrite) | ||
2052 | 707 | |||
2053 | 708 | initrd = self._repackinitrd(tmpdir=tmpdir) | ||
2054 | 709 | |||
2055 | 710 | self._createdisks() | ||
2056 | 711 | |||
2057 | 712 | image = self._tmpimage(image=self.image.image, tmpdir=tmpdir) | ||
2058 | 713 | |||
2059 | 714 | xml = self._installxml(cmdline=self.cmdline, image=image, | ||
2060 | 715 | initrd=initrd, kernel=kernel, | ||
2061 | 716 | tmpdir=tmpdir, xml=self.xml) | ||
2062 | 717 | |||
2063 | 718 | self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml) | ||
2064 | 719 | |||
2065 | 720 | xml = self._finalxml(tmpdir=tmpdir, xml=xml) | ||
2066 | 721 | |||
2067 | 722 | self.logger.info('Setting up final VM') | ||
2068 | 723 | self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot())) | ||
2069 | 724 | |||
2070 | 725 | if self.debug: | ||
2071 | 726 | self.logger.info('Leaving temp directory ' | ||
2072 | 727 | 'because debug is enabled: ' + tmpdir) | ||
2073 | 728 | else: | ||
2074 | 729 | self.logger.info('Cleaning up temp directory') | ||
2075 | 730 | shutil.rmtree(tmpdir) | ||
2076 | 731 | return True | ||
2077 | 732 | |||
2078 | 733 | def _start(self): | ||
2079 | 734 | """ | ||
2080 | 735 | Start the VM. | ||
2081 | 736 | """ | ||
2082 | 737 | self.logger.info('Starting CustomVM') | ||
2083 | 738 | if self.vm is not None: | ||
2084 | 739 | if self.vm.isActive() == 0: | ||
2085 | 740 | self.vm.create() | ||
2086 | 741 | else: | ||
2087 | 742 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
2088 | 743 | self.logger.info('Waiting ' + str(self.boottimeout) + | ||
2089 | 744 | ' seconds to allow machine to boot') | ||
2090 | 745 | try: | ||
2091 | 746 | self.pingpoll(timeout=self.boottimeout) | ||
2092 | 747 | except UTAHTimeout: | ||
2093 | 748 | # Ignore timeout for ping, since depending on the network | ||
2094 | 749 | # configuration ssh might still work despite of the ping failure. | ||
2095 | 750 | self.logger.warning('Network connectivity (ping) failure') | ||
2096 | 751 | self.sshpoll(timeout=self.boottimeout) | ||
2097 | 752 | self.active = True | ||
2098 | 753 | |||
2099 | 754 | def destroy(self, *args, **kw): | ||
2100 | 755 | """ | ||
2101 | 756 | Remove the machine from libvirt and remove all the disk files. | ||
2102 | 757 | """ | ||
2103 | 758 | # TODO: make this use standard cleanup | ||
2104 | 759 | super(CustomVM, self).destroy(*args, **kw) | ||
2105 | 760 | self.stop(force=True) | ||
2106 | 761 | if self.vm is not None: | ||
2107 | 762 | self.vm.undefine() | ||
2108 | 763 | else: | ||
2109 | 764 | self.logger.info('VM not created') | ||
2110 | 765 | for disk in self.disks: | ||
2111 | 766 | os.unlink(disk['file']) | ||
2112 | 767 | shutil.rmtree(self.directory) | ||
2113 | 768 | |||
2114 | 769 | |||
2115 | 770 | # See http://kennethreitz.com/blog/generate-a-random-mac-address-in-python/ | ||
2116 | 771 | def random_mac_address(): | ||
2117 | 772 | """Returns a completely random Mac Address""" | ||
2118 | 773 | mac = [0x52, 0x54, 0x00, | ||
2119 | 774 | random.randint(0x00, 0xff), | ||
2120 | 775 | random.randint(0x00, 0xff), | ||
2121 | 776 | random.randint(0x00, 0xff)] | ||
2122 | 777 | return ':'.join(map(lambda x: "%02x" % x, mac)) | ||
2123 | 778 | 0 | ||
2124 | === modified file 'utah/provisioning/vm/vm.py' | |||
2125 | --- utah/provisioning/vm/vm.py 2012-12-03 14:02:18 +0000 | |||
2126 | +++ utah/provisioning/vm/vm.py 2012-12-13 00:38:21 +0000 | |||
2127 | @@ -14,11 +14,26 @@ | |||
2128 | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. | 14 | # with this program. If not, see <http://www.gnu.org/licenses/>. |
2129 | 15 | 15 | ||
2130 | 16 | """ | 16 | """ |
2133 | 17 | Consolidate functions specific to virtual machine provisioning, | 17 | Consolidate functions for virtual machine provisioning. |
2134 | 18 | but not specific to any type of virtual machine. | 18 | Non-libvirt VMs can be supported elsewhere if support for them is needed. |
2135 | 19 | """ | 19 | """ |
2136 | 20 | 20 | ||
2138 | 21 | from utah.provisioning.provisioning import Machine | 21 | import libvirt |
2139 | 22 | import os | ||
2140 | 23 | import random | ||
2141 | 24 | import shutil | ||
2142 | 25 | import string | ||
2143 | 26 | import tempfile | ||
2144 | 27 | |||
2145 | 28 | from xml.etree import ElementTree | ||
2146 | 29 | |||
2147 | 30 | from utah import config | ||
2148 | 31 | from utah.process import ProcessChecker | ||
2149 | 32 | from utah.provisioning.inventory.sqlite import SQLiteInventory | ||
2150 | 33 | from utah.provisioning.provisioning import CustomInstallMixin, Machine | ||
2151 | 34 | from utah.provisioning.ssh import SSHMixin | ||
2152 | 35 | from utah.provisioning.vm.exceptions import UTAHVMProvisioningException | ||
2153 | 36 | from utah.timeout import UTAHTimeout | ||
2154 | 22 | 37 | ||
2155 | 23 | 38 | ||
2156 | 24 | class VM(Machine): | 39 | class VM(Machine): |
2157 | @@ -29,3 +44,590 @@ | |||
2158 | 29 | super(VM, self).__init__(*args, **kw) | 44 | super(VM, self).__init__(*args, **kw) |
2159 | 30 | self.vm = None | 45 | self.vm = None |
2160 | 31 | self.logger.debug('VM init finished') | 46 | self.logger.debug('VM init finished') |
2161 | 47 | |||
2162 | 48 | |||
2163 | 49 | class LibvirtVM(VM): | ||
2164 | 50 | """ | ||
2165 | 51 | Provide a class to utilize VMs using libvirt. | ||
2166 | 52 | |||
2167 | 53 | Capable of utilizing existing VMs. | ||
2168 | 54 | Creation currently handled by sublcasses. | ||
2169 | 55 | """ | ||
2170 | 56 | def __init__(self, *args, **kw): | ||
2171 | 57 | super(LibvirtVM, self).__init__(*args, **kw) | ||
2172 | 58 | libvirt.registerErrorHandler(self.libvirterrorhandler, None) | ||
2173 | 59 | self.lv = libvirt.open(config.qemupath) | ||
2174 | 60 | if self.lv is None: | ||
2175 | 61 | raise UTAHVMProvisioningException('Cannot connect to libvirt') | ||
2176 | 62 | self.logger.debug('LibvirtVM init finished') | ||
2177 | 63 | |||
2178 | 64 | def _load(self): | ||
2179 | 65 | """ | ||
2180 | 66 | Load an existing VM. | ||
2181 | 67 | """ | ||
2182 | 68 | self.logger.info('Loading VM') | ||
2183 | 69 | self.vm = self.lv.lookupByName(self.name) | ||
2184 | 70 | self.logger.info('VM loaded') | ||
2185 | 71 | return True | ||
2186 | 72 | |||
2187 | 73 | def _provision(self): | ||
2188 | 74 | """ | ||
2189 | 75 | Make an existing VM available using libvirt to look up the VM by name. | ||
2190 | 76 | """ | ||
2191 | 77 | self.logger.info('Provisioning VM') | ||
2192 | 78 | if self.new: | ||
2193 | 79 | self.logger.debug('New VM requested') | ||
2194 | 80 | try: | ||
2195 | 81 | self._load() | ||
2196 | 82 | self.logger.error('VM already exists') | ||
2197 | 83 | raise UTAHVMProvisioningException('Request new VM, but ' | ||
2198 | 84 | + self.name | ||
2199 | 85 | + ' already exists') | ||
2200 | 86 | except libvirt.libvirtError as err: | ||
2201 | 87 | if err.get_error_code() == 42: | ||
2202 | 88 | self._create() | ||
2203 | 89 | else: | ||
2204 | 90 | raise err | ||
2205 | 91 | |||
2206 | 92 | try: | ||
2207 | 93 | self._load() | ||
2208 | 94 | except libvirt.libvirtError as err: | ||
2209 | 95 | if err.get_error_code() == 42: | ||
2210 | 96 | self.logger.debug('Lookup failed') | ||
2211 | 97 | try: | ||
2212 | 98 | self._create() | ||
2213 | 99 | self._load() | ||
2214 | 100 | except UTAHVMProvisioningException as error: | ||
2215 | 101 | self.logger.error('VM lookup failed') | ||
2216 | 102 | raise UTAHVMProvisioningException('Cannot find VM named ' | ||
2217 | 103 | + self.name + | ||
2218 | 104 | ' and ' + str(error)) | ||
2219 | 105 | else: | ||
2220 | 106 | raise err | ||
2221 | 107 | self.provisioned = True | ||
2222 | 108 | self.logger.info('VM provisioned') | ||
2223 | 109 | |||
2224 | 110 | def activecheck(self): | ||
2225 | 111 | """ | ||
2226 | 112 | Verify the machine is provisioned, then start it if it is not started. | ||
2227 | 113 | """ | ||
2228 | 114 | self.logger.debug('Checking if VM is active') | ||
2229 | 115 | self.provisioncheck() | ||
2230 | 116 | if self.vm is not None: | ||
2231 | 117 | if self.vm.isActive() == 0: | ||
2232 | 118 | self._start() | ||
2233 | 119 | else: | ||
2234 | 120 | self.active = True | ||
2235 | 121 | else: | ||
2236 | 122 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
2237 | 123 | |||
2238 | 124 | def _start(self): | ||
2239 | 125 | """ | ||
2240 | 126 | Start the VM. | ||
2241 | 127 | """ | ||
2242 | 128 | self.logger.info('Starting VM') | ||
2243 | 129 | if self.vm is not None: | ||
2244 | 130 | if self.vm.isActive() == 0: | ||
2245 | 131 | self.vm.create() | ||
2246 | 132 | else: | ||
2247 | 133 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
2248 | 134 | self.active = True | ||
2249 | 135 | |||
2250 | 136 | def stop(self, force=False): | ||
2251 | 137 | """ | ||
2252 | 138 | Stop the machine. | ||
2253 | 139 | Setting force to true will do a hard shutdown instead of a graceful | ||
2254 | 140 | one. | ||
2255 | 141 | """ | ||
2256 | 142 | self.logger.info('Stopping VM') | ||
2257 | 143 | if self.vm is not None: | ||
2258 | 144 | if self.vm.isActive() == 0: | ||
2259 | 145 | self.logger.info('VM is already stopped') | ||
2260 | 146 | else: | ||
2261 | 147 | if force: | ||
2262 | 148 | self.logger.info('Forced shutdown requested') | ||
2263 | 149 | self.vm.destroy() | ||
2264 | 150 | else: | ||
2265 | 151 | self.vm.shutdown() | ||
2266 | 152 | else: | ||
2267 | 153 | self.logger.info('VM not yet created') | ||
2268 | 154 | self.active = False | ||
2269 | 155 | |||
2270 | 156 | def libvirterrorhandler(self, _context, err): | ||
2271 | 157 | """ | ||
2272 | 158 | Log libvirt errors instead of sending them directly to the console. | ||
2273 | 159 | """ | ||
2274 | 160 | errorcode = err.get_error_code() | ||
2275 | 161 | if errorcode in [9, 42]: | ||
2276 | 162 | # We see these as part of normal operations, | ||
2277 | 163 | # so we send them to debug | ||
2278 | 164 | # 9 is trying to create a VM that already exists | ||
2279 | 165 | # 42 is trying to load a VM that doesn't exist | ||
2280 | 166 | logmethod = self.logger.debug | ||
2281 | 167 | else: | ||
2282 | 168 | logmethod = self.logger.error | ||
2283 | 169 | logmethod('libvirt error: ' + err['message']) | ||
2284 | 170 | logmethod('libvirt error number is: ' + str(errorcode)) | ||
2285 | 171 | |||
2286 | 172 | |||
2287 | 173 | class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM): | ||
2288 | 174 | """ | ||
2289 | 175 | Install a VM from an image using libvirt direct kernel booting. | ||
2290 | 176 | """ | ||
2291 | 177 | def __init__(self, diskbus=None, disksizes=None, emulator=None, | ||
2292 | 178 | machineid=None, macs=None, name=None, prefix='utah', *args, | ||
2293 | 179 | **kw): | ||
2294 | 180 | # Make sure that no other virtualization solutions are running | ||
2295 | 181 | # TODO: see if this is needed for qemu or just kvm | ||
2296 | 182 | process_checker = ProcessChecker() | ||
2297 | 183 | for cmdline, app in [('/usr/lib/virtualbox/VirtualBox', 'VirtualBox'), | ||
2298 | 184 | ('/usr/lib/vmware/bin', 'VMware')]: | ||
2299 | 185 | if process_checker.check_cmdline(cmdline): | ||
2300 | 186 | message = process_checker.get_error_message(app) | ||
2301 | 187 | raise UTAHVMProvisioningException(message) | ||
2302 | 188 | |||
2303 | 189 | if diskbus is None: | ||
2304 | 190 | self.diskbus = config.diskbus | ||
2305 | 191 | else: | ||
2306 | 192 | self.diskbus = diskbus | ||
2307 | 193 | if disksizes is None: | ||
2308 | 194 | disksizes = config.disksizes | ||
2309 | 195 | if disksizes is None: | ||
2310 | 196 | self.disksizes = [8] | ||
2311 | 197 | else: | ||
2312 | 198 | self.disksizes = disksizes | ||
2313 | 199 | self.disks = [] | ||
2314 | 200 | if name is None: | ||
2315 | 201 | autoname = True | ||
2316 | 202 | name = '-'.join([str(prefix), str(machineid)]) | ||
2317 | 203 | else: | ||
2318 | 204 | autoname = False | ||
2319 | 205 | super(CustomVM, self).__init__(machineid=machineid, name=name, *args, | ||
2320 | 206 | **kw) | ||
2321 | 207 | # TODO: do a better job of separating installation | ||
2322 | 208 | # into _create rather than __init__ | ||
2323 | 209 | if self.image is None: | ||
2324 | 210 | raise UTAHVMProvisioningException('Image file required ' | ||
2325 | 211 | 'for custom VM installation') | ||
2326 | 212 | self._custominit() | ||
2327 | 213 | if autoname: | ||
2328 | 214 | self._namesetup() | ||
2329 | 215 | self._loggerunsetup() | ||
2330 | 216 | self._loggersetup() | ||
2331 | 217 | self._dirsetup() | ||
2332 | 218 | if emulator is None: | ||
2333 | 219 | emulator = config.emulator | ||
2334 | 220 | if emulator is None: | ||
2335 | 221 | if self._supportsdomaintype('kvm'): | ||
2336 | 222 | self.logger.info('Setting type to kvm ' | ||
2337 | 223 | 'since it is present in libvirt capabilities') | ||
2338 | 224 | self.domaintype = 'kvm' | ||
2339 | 225 | elif self._supportsdomaintype('qemu'): | ||
2340 | 226 | self.logger.info('Setting type to qemu ' | ||
2341 | 227 | 'since it is present in libvirt capabilities') | ||
2342 | 228 | self.domaintype = 'qemu' | ||
2343 | 229 | else: | ||
2344 | 230 | raise UTAHVMProvisioningException( | ||
2345 | 231 | 'kvm and qemu not supported in libvirt capabilities; ' | ||
2346 | 232 | 'please make sure qemu and/or kvm are installed ' | ||
2347 | 233 | 'and libvirt is configured correctly') | ||
2348 | 234 | else: | ||
2349 | 235 | self.domaintype = emulator | ||
2350 | 236 | if self.domaintype == 'qemu': | ||
2351 | 237 | self.logger.debug('Raising boot timeout for qemu domain') | ||
2352 | 238 | self.boottimeout *= 4 | ||
2353 | 239 | if macs is None: | ||
2354 | 240 | macs = [] | ||
2355 | 241 | self.macs = macs | ||
2356 | 242 | self.dircheck() | ||
2357 | 243 | self.logger.debug('CustomVM init finished') | ||
2358 | 244 | |||
2359 | 245 | def _createdisks(self, disksizes=None): | ||
2360 | 246 | """ | ||
2361 | 247 | Create disk files if needed and build a list of them. | ||
2362 | 248 | """ | ||
2363 | 249 | self.logger.info('Creating disks') | ||
2364 | 250 | if disksizes is None: | ||
2365 | 251 | disksizes = self.disksizes | ||
2366 | 252 | for index, size in enumerate(disksizes): | ||
2367 | 253 | disksize = '{}G'.format(size) | ||
2368 | 254 | basename = 'disk{}.qcow2'.format(index) | ||
2369 | 255 | diskfile = os.path.join(self.directory, basename) | ||
2370 | 256 | if not os.path.isfile(diskfile): | ||
2371 | 257 | cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize] | ||
2372 | 258 | self.logger.debug('Creating ' + disksize + ' disk using:') | ||
2373 | 259 | self.logger.debug(' '.join(cmd)) | ||
2374 | 260 | if self._runargs(cmd) != 0: | ||
2375 | 261 | raise UTAHVMProvisioningException( | ||
2376 | 262 | 'Could not create disk image at ' + diskfile) | ||
2377 | 263 | disk = {'bus': self.diskbus, | ||
2378 | 264 | 'file': diskfile, | ||
2379 | 265 | 'size': disksize, | ||
2380 | 266 | 'type': 'qcow2'} | ||
2381 | 267 | self.disks.append(disk) | ||
2382 | 268 | self.logger.debug('Adding disk to list') | ||
2383 | 269 | |||
2384 | 270 | def _supportsdomaintype(self, domaintype): | ||
2385 | 271 | """ | ||
2386 | 272 | Check emulator support in libvirt capabilities. | ||
2387 | 273 | """ | ||
2388 | 274 | capabilities = ElementTree.fromstring(self.lv.getCapabilities()) | ||
2389 | 275 | for guest in capabilities.iterfind('guest'): | ||
2390 | 276 | for arch in guest.iterfind('arch'): | ||
2391 | 277 | for domain in arch.iterfind('domain'): | ||
2392 | 278 | if domaintype in domain.get('type'): | ||
2393 | 279 | return True | ||
2394 | 280 | return False | ||
2395 | 281 | |||
2396 | 282 | def _installxml(self, cmdline=None, image=None, initrd=None, | ||
2397 | 283 | kernel=None, tmpdir=None, xml=None): | ||
2398 | 284 | """ | ||
2399 | 285 | Return the XML tree to be passed to libvirt for VM installation. | ||
2400 | 286 | """ | ||
2401 | 287 | self.logger.info('Creating installation XML') | ||
2402 | 288 | if cmdline is None: | ||
2403 | 289 | cmdline = self.cmdline | ||
2404 | 290 | if image is None: | ||
2405 | 291 | image = self.image.image | ||
2406 | 292 | if initrd is None: | ||
2407 | 293 | initrd = self.initrd | ||
2408 | 294 | if kernel is None: | ||
2409 | 295 | kernel = self.kernel | ||
2410 | 296 | if xml is None: | ||
2411 | 297 | xml = self.xml | ||
2412 | 298 | if tmpdir is None: | ||
2413 | 299 | tmpdir = self.tmpdir | ||
2414 | 300 | xmlt = ElementTree.ElementTree(file=xml) | ||
2415 | 301 | if self.rewrite in ['all', 'minimal']: | ||
2416 | 302 | self.logger.debug('Setting VM to shutdown on reboot') | ||
2417 | 303 | xmlt.find('on_reboot').text = 'destroy' | ||
2418 | 304 | if self.rewrite == 'all': | ||
2419 | 305 | self._installxml_rewrite_all(cmdline, image, initrd, kernel, | ||
2420 | 306 | xmlt) | ||
2421 | 307 | else: | ||
2422 | 308 | self.logger.info('Not rewriting XML because rewrite is ' + | ||
2423 | 309 | self.rewrite) | ||
2424 | 310 | if self.debug: | ||
2425 | 311 | xmlt.write(os.path.join(tmpdir, 'install.xml')) | ||
2426 | 312 | self.logger.info('Installation XML ready') | ||
2427 | 313 | return xmlt | ||
2428 | 314 | |||
2429 | 315 | def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt, | ||
2430 | 316 | kernel_txt, xmlt): | ||
2431 | 317 | """ | ||
2432 | 318 | Rewrite the whole configuration file for the VM | ||
2433 | 319 | """ | ||
2434 | 320 | self.logger.debug('Rewriting basic info') | ||
2435 | 321 | xmlt.find('name').text = self.name | ||
2436 | 322 | xmlt.find('uuid').text = self.uuid | ||
2437 | 323 | self.logger.debug('Setting type to qemu in case no ' | ||
2438 | 324 | 'hardware virtualization present') | ||
2439 | 325 | xmlt.getroot().set('type', self.domaintype) | ||
2440 | 326 | ose = xmlt.find('os') | ||
2441 | 327 | if self.arch == ('i386'): | ||
2442 | 328 | ose.find('type').set('arch', 'i686') | ||
2443 | 329 | elif self.arch == ('amd64'): | ||
2444 | 330 | ose.find('type').set('arch', 'x86_64') | ||
2445 | 331 | else: | ||
2446 | 332 | ose.find('type').set('arch', self.arch) | ||
2447 | 333 | self.logger.debug('Setting up boot info') | ||
2448 | 334 | for kernele in list(ose.iterfind('kernel')): | ||
2449 | 335 | ose.remove(kernele) | ||
2450 | 336 | kernele = ElementTree.Element('kernel') | ||
2451 | 337 | kernele.text = kernel_txt | ||
2452 | 338 | ose.append(kernele) | ||
2453 | 339 | for initrde in list(ose.iterfind('initrd')): | ||
2454 | 340 | ose.remove(initrde) | ||
2455 | 341 | initrde = ElementTree.Element('initrd') | ||
2456 | 342 | initrde.text = initrd_txt | ||
2457 | 343 | ose.append(initrde) | ||
2458 | 344 | for cmdlinee in list(ose.iterfind('cmdline')): | ||
2459 | 345 | ose.remove(cmdlinee) | ||
2460 | 346 | cmdlinee = ElementTree.Element('cmdline') | ||
2461 | 347 | cmdlinee.text = cmdline_txt | ||
2462 | 348 | ose.append(cmdlinee) | ||
2463 | 349 | self.logger.debug('Setting up devices') | ||
2464 | 350 | devices = xmlt.find('devices') | ||
2465 | 351 | self.logger.debug('Setting up disks') | ||
2466 | 352 | for disk in list(devices.iterfind('disk')): | ||
2467 | 353 | if disk.get('device') == 'disk': | ||
2468 | 354 | devices.remove(disk) | ||
2469 | 355 | self.logger.debug('Removed existing disk') | ||
2470 | 356 | #TODO: Add a cdrom if none exists | ||
2471 | 357 | if disk.get('device') == 'cdrom': | ||
2472 | 358 | if disk.find('source') is not None: | ||
2473 | 359 | disk.find('source').set('file', image) | ||
2474 | 360 | self.logger.debug('Rewrote existing CD-ROM') | ||
2475 | 361 | else: | ||
2476 | 362 | source = ElementTree.Element('source') | ||
2477 | 363 | source.set('file', image) | ||
2478 | 364 | disk.append(source) | ||
2479 | 365 | self.logger.debug('Added source to existing ' | ||
2480 | 366 | 'CD-ROM') | ||
2481 | 367 | for disk in self.disks: | ||
2482 | 368 | diske = ElementTree.Element('disk') | ||
2483 | 369 | diske.set('type', 'file') | ||
2484 | 370 | diske.set('device', 'disk') | ||
2485 | 371 | driver = ElementTree.Element('driver') | ||
2486 | 372 | driver.set('name', 'qemu') | ||
2487 | 373 | driver.set('type', disk['type']) | ||
2488 | 374 | diske.append(driver) | ||
2489 | 375 | source = ElementTree.Element('source') | ||
2490 | 376 | source.set('file', disk['file']) | ||
2491 | 377 | diske.append(source) | ||
2492 | 378 | target = ElementTree.Element('target') | ||
2493 | 379 | dev = "vd%s" % (string.ascii_lowercase[self.disks.index(disk)]) | ||
2494 | 380 | target.set('dev', dev) | ||
2495 | 381 | target.set('bus', disk['bus']) | ||
2496 | 382 | diske.append(target) | ||
2497 | 383 | devices.append(diske) | ||
2498 | 384 | self.logger.debug('Added ' + str(disk['size']) + ' disk') | ||
2499 | 385 | macs = list(self.macs) | ||
2500 | 386 | for interface in devices.iterfind('interface'): | ||
2501 | 387 | if interface.get('type') in ['network', 'bridge']: | ||
2502 | 388 | if len(macs) > 0: | ||
2503 | 389 | mac = macs.pop(0) | ||
2504 | 390 | interface.find('mac').set('address', mac) | ||
2505 | 391 | self.logger.debug('Rewrote interface ' | ||
2506 | 392 | 'to use specified mac address ' + mac) | ||
2507 | 393 | else: | ||
2508 | 394 | mac = random_mac_address() | ||
2509 | 395 | interface.find('mac').set('address', mac) | ||
2510 | 396 | self.macs.append(mac) | ||
2511 | 397 | self.logger.debug('Rewrote interface ' | ||
2512 | 398 | 'to use random mac address ' + mac) | ||
2513 | 399 | if interface.get('type') == 'bridge': | ||
2514 | 400 | interface.find('source').set('bridge', config.bridge) | ||
2515 | 401 | serial = ElementTree.Element('serial') | ||
2516 | 402 | serial.set('type', 'file') | ||
2517 | 403 | source = ElementTree.Element('source') | ||
2518 | 404 | log_filename = os.path.join(config.logpath, self.name + '.syslog.log') | ||
2519 | 405 | source.set('path', log_filename) | ||
2520 | 406 | serial.append(source) | ||
2521 | 407 | target = ElementTree.Element('target') | ||
2522 | 408 | target.set('port', '0') | ||
2523 | 409 | serial.append(target) | ||
2524 | 410 | devices.append(serial) | ||
2525 | 411 | |||
2526 | 412 | def _installvm(self, lv=None, tmpdir=None, xml=None): | ||
2527 | 413 | """ | ||
2528 | 414 | Install a VM, then undefine it in libvirt. | ||
2529 | 415 | The final installation will recreate the VM using the existing disks. | ||
2530 | 416 | """ | ||
2531 | 417 | self.logger.info('Creating VM') | ||
2532 | 418 | if lv is None: | ||
2533 | 419 | lv = self.lv | ||
2534 | 420 | if xml is None: | ||
2535 | 421 | xml = self.xml | ||
2536 | 422 | if tmpdir is None: | ||
2537 | 423 | tmpdir = self.tmpdir | ||
2538 | 424 | vm = lv.defineXML(ElementTree.tostring(xml.getroot())) | ||
2539 | 425 | os.chmod(tmpdir, 0755) | ||
2540 | 426 | vm.create() | ||
2541 | 427 | self.logger.info('Installing system on VM (may take over an hour)') | ||
2542 | 428 | self.logger.info('You can watch the progress with virt-viewer') | ||
2543 | 429 | log_filename = os.path.join(config.logpath, self.name + '.syslog.log') | ||
2544 | 430 | self.logger.info('Logs will be written to ' + log_filename) | ||
2545 | 431 | |||
2546 | 432 | while vm.isActive() is not 0: | ||
2547 | 433 | pass | ||
2548 | 434 | |||
2549 | 435 | vm.undefine() | ||
2550 | 436 | self.logger.info('Installation complete') | ||
2551 | 437 | |||
2552 | 438 | def _finalxml(self, tmpdir=None, xml=None): | ||
2553 | 439 | """ | ||
2554 | 440 | Create the XML to be used for the post-installation VM. | ||
2555 | 441 | This may be a transformation of the installation XML. | ||
2556 | 442 | """ | ||
2557 | 443 | self.logger.info('Creating final VM XML') | ||
2558 | 444 | if xml is None: | ||
2559 | 445 | xml = ElementTree.ElementTree(file=self.xml) | ||
2560 | 446 | if tmpdir is None: | ||
2561 | 447 | tmpdir = self.tmpdir | ||
2562 | 448 | if self.rewrite in ['all', 'minimal']: | ||
2563 | 449 | self.logger.debug('Setting VM to reboot normally on reboot') | ||
2564 | 450 | xml.find('on_reboot').text = 'restart' | ||
2565 | 451 | if self.rewrite == 'all': | ||
2566 | 452 | self.logger.debug('Removing VM install parameters') | ||
2567 | 453 | ose = xml.find('os') | ||
2568 | 454 | for kernel in ose.iterfind('kernel'): | ||
2569 | 455 | ose.remove(kernel) | ||
2570 | 456 | for initrd in ose.iterfind('initrd'): | ||
2571 | 457 | ose.remove(initrd) | ||
2572 | 458 | for cmdline in ose.iterfind('cmdline'): | ||
2573 | 459 | ose.remove(cmdline) | ||
2574 | 460 | devices = xml.find('devices') | ||
2575 | 461 | devices.remove(devices.find('serial')) | ||
2576 | 462 | for disk in list(devices.iterfind('disk')): | ||
2577 | 463 | if disk.get('device') == 'cdrom': | ||
2578 | 464 | disk.remove(disk.find('source')) | ||
2579 | 465 | else: | ||
2580 | 466 | self.logger.info('Not rewriting XML because rewrite is ' + | ||
2581 | 467 | self.rewrite) | ||
2582 | 468 | if self.debug: | ||
2583 | 469 | xml.write(os.path.join(tmpdir, 'final.xml')) | ||
2584 | 470 | return xml | ||
2585 | 471 | |||
2586 | 472 | def _tmpimage(self, image=None, tmpdir=None): | ||
2587 | 473 | """ | ||
2588 | 474 | Create a temporary copy of the image so libvirt will lock that copy. | ||
2589 | 475 | This allows other simultaneous processes to update the cached image. | ||
2590 | 476 | """ | ||
2591 | 477 | if image is None: | ||
2592 | 478 | image = self.image.image | ||
2593 | 479 | if tmpdir is None: | ||
2594 | 480 | tmpdir = self.tmpdir | ||
2595 | 481 | self.logger.info('Making temp copy of install image') | ||
2596 | 482 | tmpimage = os.path.join(tmpdir, os.path.basename(image)) | ||
2597 | 483 | self.logger.debug('Copying ' + image + ' to ' + tmpimage) | ||
2598 | 484 | shutil.copyfile(image, tmpimage) | ||
2599 | 485 | return tmpimage | ||
2600 | 486 | |||
2601 | 487 | def _create(self): | ||
2602 | 488 | """ | ||
2603 | 489 | Create the VM, install the system, and prepare it to boot. | ||
2604 | 490 | This primarily calls functions from CustomInstallMixin and CustomVM. | ||
2605 | 491 | """ | ||
2606 | 492 | self.logger.info('Creating custom virtual machine') | ||
2607 | 493 | |||
2608 | 494 | tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_') | ||
2609 | 495 | self.logger.debug('Working dir: ' + tmpdir) | ||
2610 | 496 | os.chdir(tmpdir) | ||
2611 | 497 | |||
2612 | 498 | kernel = self._preparekernel(kernel=self.kernel, tmpdir=tmpdir) | ||
2613 | 499 | |||
2614 | 500 | initrd = self._prepareinitrd(initrd=self.initrd, tmpdir=tmpdir) | ||
2615 | 501 | |||
2616 | 502 | self._unpackinitrd(initrd=initrd, tmpdir=tmpdir) | ||
2617 | 503 | |||
2618 | 504 | self._setuplatecommand(tmpdir=tmpdir) | ||
2619 | 505 | |||
2620 | 506 | self._setuppreseed(tmpdir=tmpdir) | ||
2621 | 507 | |||
2622 | 508 | if self.rewrite == 'all': | ||
2623 | 509 | self._setuplogging(tmpdir=tmpdir) | ||
2624 | 510 | else: | ||
2625 | 511 | self.logger.debug('Skipping logging setup because rewrite is' + | ||
2626 | 512 | self.rewrite) | ||
2627 | 513 | |||
2628 | 514 | initrd = self._repackinitrd(tmpdir=tmpdir) | ||
2629 | 515 | |||
2630 | 516 | self._createdisks() | ||
2631 | 517 | |||
2632 | 518 | image = self._tmpimage(image=self.image.image, tmpdir=tmpdir) | ||
2633 | 519 | |||
2634 | 520 | xml = self._installxml(cmdline=self.cmdline, image=image, | ||
2635 | 521 | initrd=initrd, kernel=kernel, | ||
2636 | 522 | tmpdir=tmpdir, xml=self.xml) | ||
2637 | 523 | |||
2638 | 524 | self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml) | ||
2639 | 525 | |||
2640 | 526 | xml = self._finalxml(tmpdir=tmpdir, xml=xml) | ||
2641 | 527 | |||
2642 | 528 | self.logger.info('Setting up final VM') | ||
2643 | 529 | self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot())) | ||
2644 | 530 | |||
2645 | 531 | if self.debug: | ||
2646 | 532 | self.logger.info('Leaving temp directory ' | ||
2647 | 533 | 'because debug is enabled: ' + tmpdir) | ||
2648 | 534 | else: | ||
2649 | 535 | self.logger.info('Cleaning up temp directory') | ||
2650 | 536 | shutil.rmtree(tmpdir) | ||
2651 | 537 | return True | ||
2652 | 538 | |||
2653 | 539 | def _start(self): | ||
2654 | 540 | """ | ||
2655 | 541 | Start the VM. | ||
2656 | 542 | """ | ||
2657 | 543 | self.logger.info('Starting CustomVM') | ||
2658 | 544 | if self.vm is not None: | ||
2659 | 545 | if self.vm.isActive() == 0: | ||
2660 | 546 | self.vm.create() | ||
2661 | 547 | else: | ||
2662 | 548 | raise UTAHVMProvisioningException('Failed to provision VM') | ||
2663 | 549 | self.logger.info('Waiting ' + str(self.boottimeout) + | ||
2664 | 550 | ' seconds to allow machine to boot') | ||
2665 | 551 | try: | ||
2666 | 552 | self.pingpoll(timeout=self.boottimeout) | ||
2667 | 553 | except UTAHTimeout: | ||
2668 | 554 | # Ignore timeout for ping, since depending on the network | ||
2669 | 555 | # configuration ssh might still work despite of the ping failure. | ||
2670 | 556 | self.logger.warning('Network connectivity (ping) failure') | ||
2671 | 557 | self.sshpoll(timeout=self.boottimeout) | ||
2672 | 558 | self.active = True | ||
2673 | 559 | |||
2674 | 560 | def destroy(self, *args, **kw): | ||
2675 | 561 | """ | ||
2676 | 562 | Remove the machine from libvirt and remove all the disk files. | ||
2677 | 563 | """ | ||
2678 | 564 | # TODO: make this use standard cleanup | ||
2679 | 565 | super(CustomVM, self).destroy(*args, **kw) | ||
2680 | 566 | self.stop(force=True) | ||
2681 | 567 | if self.vm is not None: | ||
2682 | 568 | self.vm.undefine() | ||
2683 | 569 | else: | ||
2684 | 570 | self.logger.info('VM not created') | ||
2685 | 571 | for disk in self.disks: | ||
2686 | 572 | os.unlink(disk['file']) | ||
2687 | 573 | shutil.rmtree(self.directory) | ||
2688 | 574 | |||
2689 | 575 | |||
2690 | 576 | # See http://kennethreitz.com/blog/generate-a-random-mac-address-in-python/ | ||
2691 | 577 | def random_mac_address(): | ||
2692 | 578 | """Returns a completely random Mac Address""" | ||
2693 | 579 | mac = [0x52, 0x54, 0x00, | ||
2694 | 580 | random.randint(0x00, 0xff), | ||
2695 | 581 | random.randint(0x00, 0xff), | ||
2696 | 582 | random.randint(0x00, 0xff)] | ||
2697 | 583 | return ':'.join(map(lambda x: "%02x" % x, mac)) | ||
2698 | 584 | |||
2699 | 585 | |||
2700 | 586 | class TinySQLiteInventory(SQLiteInventory): | ||
2701 | 587 | """ | ||
2702 | 588 | Tiny SQLite inventory that implements request, release, and destroy. | ||
2703 | 589 | No authentication or conflict checking currently exists. | ||
2704 | 590 | Only suitable for VMs at present. | ||
2705 | 591 | """ | ||
2706 | 592 | def __init__(self, *args, **kw): | ||
2707 | 593 | """ | ||
2708 | 594 | Initialize simple database. | ||
2709 | 595 | """ | ||
2710 | 596 | super(TinySQLiteInventory, self).__init__(*args, **kw) | ||
2711 | 597 | self.connection.execute( | ||
2712 | 598 | 'CREATE TABLE IF NOT EXISTS ' | ||
2713 | 599 | 'machines(machineid INTEGER PRIMARY KEY, state TEXT)') | ||
2714 | 600 | |||
2715 | 601 | def request(self, machinetype=CustomVM, *args, **kw): | ||
2716 | 602 | """ | ||
2717 | 603 | Takes a Machine class as machinetype, and passes the newly generated | ||
2718 | 604 | machineid along with all other arguments to that class's constructor, | ||
2719 | 605 | returning the resulting object. | ||
2720 | 606 | """ | ||
2721 | 607 | cursor = self.connection.cursor() | ||
2722 | 608 | cursor.execute("INSERT INTO machines (state) VALUES ('provisioned')") | ||
2723 | 609 | machineid = cursor.lastrowid | ||
2724 | 610 | return machinetype(machineid=machineid, *args, **kw) | ||
2725 | 611 | |||
2726 | 612 | def release(self, machineid): | ||
2727 | 613 | """ | ||
2728 | 614 | Updates the database to indicate the machine is available. | ||
2729 | 615 | """ | ||
2730 | 616 | if self.connection.execute( | ||
2731 | 617 | "UPDATE machines SET state='available' WHERE machineid=?", | ||
2732 | 618 | [machineid]): | ||
2733 | 619 | return True | ||
2734 | 620 | else: | ||
2735 | 621 | return False | ||
2736 | 622 | |||
2737 | 623 | def destroy(self, machineid): | ||
2738 | 624 | """ | ||
2739 | 625 | Updates the database to indicate the machine is destroyed, but does not | ||
2740 | 626 | destroy the machine. | ||
2741 | 627 | """ | ||
2742 | 628 | if self.connection.execute( | ||
2743 | 629 | "UPDATE machines SET state='destroyed' ""WHERE machineid=?", | ||
2744 | 630 | [machineid]): | ||
2745 | 631 | return True | ||
2746 | 632 | else: | ||
2747 | 633 | return False |
Looks good. I've checked that the packages and the documentation build
correctly and that the pass run list succeeds.