Merge lp:~nuclearbob/utah/reorg into lp:utah

Proposed by Max Brustkern
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
Reviewer Review Type Date Requested Status
Javier Collado (community) Approve
Review via email: mp+139596@code.launchpad.net

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.

ManualBaremetalSQLiteInventory was moved into an inventory.py file under baremetal.

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.

To post a comment you must log in.
Revision history for this message
Javier Collado (javier.collado) wrote :

Looks good. I've checked that the packages and the documentation build
correctly and that the pass run list succeeds.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'conf/config'
--- conf/config 2012-07-11 13:56:31 +0000
+++ conf/config 2012-12-13 00:38:21 +0000
@@ -1,1 +1,1 @@
1IdentityFile %d/.ssh/%u1IdentityFile %d/.ssh/utah
22
=== removed file 'conf/utah/uqt-vm-tools.conf'
--- conf/utah/uqt-vm-tools.conf 2012-11-07 19:52:52 +0000
+++ conf/utah/uqt-vm-tools.conf 1970-01-01 00:00:00 +0000
@@ -1,60 +0,0 @@
1# list of all active releases (included devel)
2vm_release_list="hardy lucid oneiric precise quantal raring"
3
4# used by vm-repo (ie 'umt repo' puts stuff in /var/www/debs/testing/..., so
5# vm_repo_url should be the URL to those files. The IP of the host is by
6# default 192.168.122.1, and guests are 192.168.122.2-254.
7vm_repo_url="http://192.168.122.1/debs/testing"
8
9# vm-tools specific settings
10vm_path="/var/lib/utah/vm" # where to store the VM images
11vm_aptproxy="" # set if you want to use a local proxy (like, say, apt-cacher-ng)
12vm_mirror="http://us.archive.ubuntu.com/ubuntu"
13vm_security_mirror="" # set if want to use a local mirror for security
14vm_mirror_host="us.archive.ubuntu.com" # Used with the mini iso
15vm_mirror_dir="/ubuntu" # Used with the mini iso
16vm_dir_iso="/var/cache/utah/iso" # set to directory containing .iso images
17vm_dir_iso_cache="/var/cache/utah/iso/cache" # set to directory for preseeded iso cache
18vm_image_size="8" # size in GB of vm-new disk images
19vm_memory="512" # 384 is needed for desktops, 256 for servers
20vm_ssh_key=~/.ssh/utah.pub # defaults to $HOME/.ssh/id_rsa.pub
21vm_connect="qemu:///system"
22vm_flavor="" # blank for default, set to override (eg 'rt')
23vm_archs="i386 amd64" # architectures to use when using '-p PREFIX'
24 # with some commands
25vm_extra_packages="python-yaml bzr git" # list of packages to also
26 # install via postinstall.sh
27vm_username="utah" # defaults to your username (`whoami`)
28vm_password="ubuntu" # defaults to "ubuntu"
29vm_latecmd="" # allows specifying an additional late command
30
31vm_root_size="4096" # Used by deprecated vm-new-vmbuilder tool
32vm_swap_size="1024" # Used by deprecated vm-new-vmbuilder tool
33
34# vm-iso specific settings (also uses vm-tools settings)
35vm_iso_ndisks="1" # number of disks
36vm_iso_vcpus="1" # number of virtual CPUs
37vm_iso_ostype="linux"
38vm_iso_osvariant="ubuntuLucid"
39# see 'man virt-install' for details on 'sparse', 'format' and 'cache'
40vm_iso_fully_allocate="yes" # fully allocate the disk image (usu. faster)
41vm_iso_disk_format="qcow2" # raw, qcow2, vmdk, etc
42vm_iso_disk_cache="none" # none, writethrough, writeback
43
44# vm-new locale
45vm_locale="en_US.UTF-8"
46
47# vm-new keyboard layout
48vm_setkeyboard="false" # set to "true" to enable the custom settings below
49vm_xkbmodel="pc105"
50vm_xkblayout="ca"
51vm_xkbvariant=""
52vm_xkboptions="lv3:ralt_switch"
53
54# Use an alternate viewer such as gvncviewer or xtightvncviewer. Defaults to
55# virt-viewer if unset. You can specify arguments to the viewer here.
56#vm_viewer="xvnc4viewer"
57#vm_viewer_args=""
58
59# Set to 'no' to disable '.local' mDNS (avahi) lookups for VMs
60vm_host_use_avahi="yes"
610
=== modified file 'debian/control'
--- debian/control 2012-12-11 14:28:43 +0000
+++ debian/control 2012-12-13 00:38:21 +0000
@@ -18,10 +18,34 @@
18 python-netifaces, python-paramiko, python-psutil,18 python-netifaces, python-paramiko, python-psutil,
19 utah-client (=${binary:Version})19 utah-client (=${binary:Version})
20Recommends: dl-ubuntu-test-iso, kvm20Recommends: dl-ubuntu-test-iso, kvm
21Suggests: cobbler, u-boot-tools, vm-tools
22Description: Ubuntu Test Automation Harness21Description: Ubuntu Test Automation Harness
23 Automation framework for testing in Ubuntu22 Automation framework for testing in Ubuntu
2423
24Package: utah-all
25Architecture: all
26Depends: ${misc:Depends}, ${python:Depends}, utah-bamboofeeder, utah-cobbler,
27 utah-parser
28Description: Ubuntu Test Automation Harness Complete Package
29 Automation framework for testing in Ubuntu, all sections
30
31Package: utah-bamboofeeder
32Architecture: all
33Depends: ${misc:Depends}, ${python:Depends}, u-boot-tools, utah-baremetal
34Description: Ubuntu Test Automation Harness Bamboo Feeder Support
35 Automation framework for testing in Ubuntu, bamboo feeder portion
36
37Package: utah-baremetal
38Architecture: all
39Depends: ${misc:Depends}, ${python:Depends}, utah
40Description: Ubuntu Test Automation Harness Bare Metal Support
41 Automation framework for testing in Ubuntu, bare metal portion
42
43Package: utah-cobbler
44Architecture: all
45Depends: ${misc:Depends}, ${python:Depends}, cobbler, utah-baremetal
46Description: Ubuntu Test Automation Harness Cobbler Support
47 Automation framework for testing in Ubuntu, cobbler portion
48
25Package: utah-client49Package: utah-client
26Architecture: all50Architecture: all
27Depends: ${misc:Depends}, ${python:Depends},51Depends: ${misc:Depends}, ${python:Depends},
2852
=== modified file 'debian/rules'
--- debian/rules 2012-11-30 14:03:47 +0000
+++ debian/rules 2012-12-13 00:38:21 +0000
@@ -22,6 +22,7 @@
22 # utah should only install the provisioning subdirectory, and utah-client should install everything else22 # utah should only install the provisioning subdirectory, and utah-client should install everything else
23 # except parser.py, which is now its own package23 # except parser.py, which is now its own package
24 # and __init__.py, which is now in the common package24 # and __init__.py, which is now in the common package
25 # The baremetal subdirectory in the provisioning directory is now 3 packages
25 # If we just use dh_auto_install, all packages will get everything26 # If we just use dh_auto_install, all packages will get everything
26 # We start by building the whole thing27 # We start by building the whole thing
27 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --install-layout=deb --root=$(CURDIR); done28 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --install-layout=deb --root=$(CURDIR); done
@@ -37,6 +38,8 @@
37 rm -r build/*/utah/*38 rm -r build/*/utah/*
38 # And we put provisioning back39 # And we put provisioning back
39 mv provisioning build/*/utah40 mv provisioning build/*/utah
41 # But remove baremetal
42 mv build/*/utah/provisioning/baremetal .
40 # Now we install just the provisioning directory, again using --skip-build into the utah package tree43 # Now we install just the provisioning directory, again using --skip-build into the utah package tree
41 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah; done44 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah; done
42 # And now we put back the parser and install that45 # And now we put back the parser and install that
@@ -47,24 +50,48 @@
47 rm -r build/*/utah/*50 rm -r build/*/utah/*
48 mv __init__.py build/*/utah51 mv __init__.py build/*/utah
49 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-common; done52 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-common; done
53 # Now we install just baremetal
54 rm -r build/*/utah/*
55 mkdir provisioning
56 mv baremetal provisioning
57 mv provisioning build/*/utah
58 # But without cobbler
59 mv build/*/utah/provisioning/baremetal/cobbler.py .
60 # And without bamboofeeder
61 mv build/*/utah/provisioning/baremetal/bamboofeeder.py .
62 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-baremetal; done
63 # Now just cobbler
64 rm -r build/*/utah/provisioning/baremetal/*
65 mv cobbler.py build/*/utah/provisioning/baremetal
66 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-cobbler; done
67 # Now just bamboofeeder
68 rm -r build/*/utah/provisioning/baremetal/*
69 mv bamboofeeder.py build/*/utah/provisioning/baremetal
70 set -e && for pyvers in $(PYVERS); do python$$pyvers setup.py install --skip-build --install-layout=deb --root=$(CURDIR)/debian/utah-bamboofeeder; done
50 # Since the client changes names from client.py to utah, we can't use utah-client.install for that71 # Since the client changes names from client.py to utah, we can't use utah-client.install for that
51 # We also need to make our directory since we do this before dh_auto_install72 # We also need to make our directory since we do this before dh_auto_install
52 mkdir -p $(CURDIR)/debian/utah-client/usr/bin73 mkdir -p $(CURDIR)/debian/utah-client/usr/bin
53 cp -aL client.py $(CURDIR)/debian/utah-client/usr/bin/utah74 cp -aL client.py $(CURDIR)/debian/utah-client/usr/bin/utah
54 # phoenix.py needs to be in here to lose the .py75 # phoenix.py needs to be in here to lose the .py
55 cp -aL utah/client/phoenix.py $(CURDIR)/debian/utah-client/usr/bin/phoenix76 cp -aL utah/client/phoenix.py $(CURDIR)/debian/utah-client/usr/bin/phoenix
56 # Since utah and utah-client both got installed using python distutils, they both get an egg info directory77 # Since all packages get installed using python distutils, they all get an egg info directory
57 # This will conflict if we leave it in both of them, so we remove it from the client78 # This will conflict if we leave it in all of them, so we remove it from everything but common
58 rm -r $(CURDIR)/debian/utah-client/usr/lib/python*/dist-packages/utah-*.egg-info79 for egg in $$(ls -d $(CURDIR)/debian/utah*/usr/lib/python*/dist-packages/utah-*.egg-info | grep -v "utah-common");\
59 # Let's remove it from the parser as well80 do rm -r $$egg;\
60 rm -r $(CURDIR)/debian/utah-parser/usr/lib/python*/dist-packages/utah-*.egg-info81 done
61 # We'll remove it from the server and leave it in the client package
62 rm -r $(CURDIR)/debian/utah/usr/lib/python*/dist-packages/utah-*.egg-info
63 # We want to symlink the utah example scripts into /usr/bin, and we need a directory for that82 # We want to symlink the utah example scripts into /usr/bin, and we need a directory for that
64 mkdir -p $(CURDIR)/debian/utah/usr/bin83 mkdir -p $(CURDIR)/debian/utah/usr/bin
84 mkdir -p $(CURDIR)/debian/utah-cobbler/usr/bin
85 mkdir -p $(CURDIR)/debian/utah-bamboofeeder/usr/bin
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\
66 if [ ! -h "$(CURDIR)/debian/utah/usr/bin/$$script" ]; then\87 if [ ! -h "$(CURDIR)/debian/utah/usr/bin/$$script" ]; then\
67 ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah/usr/bin/$$script;\88 if [ "$$script" = "run_test_cobbler.py" ]; then\
89 ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah-cobbler/usr/bin/$$script;\
90 elif [ "$$script" = "run_test_bamboo_feeder.py" ]; then\
91 ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah-bamboofeeder/usr/bin/$$script;\
92 else\
93 ln -s ../share/utah/examples/$$script $(CURDIR)/debian/utah/usr/bin/$$script;\
94 fi;\
68 fi;\95 fi;\
69 done96 done
70 dh_auto_install97 dh_auto_install
7198
=== modified file 'debian/utah.postinst'
--- debian/utah.postinst 2012-10-31 15:57:11 +0000
+++ debian/utah.postinst 2012-12-13 00:38:21 +0000
@@ -77,7 +77,6 @@
7777
78 usersetup78 usersetup
7979
80 ln -sf /etc/utah/uqt-vm-tools.conf ~utah/.uqt-vm-tools.conf
81 ln -sf /etc/utah/shell-profile ~utah/.profile80 ln -sf /etc/utah/shell-profile ~utah/.profile
8281
83 if ! ([ -f ~utah/.ssh/utah ] && [ -f ~utah/.ssh/utah.pub ])82 if ! ([ -f ~utah/.ssh/utah ] && [ -f ~utah/.ssh/utah.pub ])
8483
=== modified file 'docs/source/reference.rst'
--- docs/source/reference.rst 2012-11-30 09:36:56 +0000
+++ docs/source/reference.rst 2012-12-13 00:38:21 +0000
@@ -88,6 +88,9 @@
88.. automodule:: utah.provisioning.provisioning88.. automodule:: utah.provisioning.provisioning
89 :members:89 :members:
9090
91.. automodule:: utah.provisioning.ssh
92 :members:
93
91.. automodule:: utah.provisioning.exceptions94.. automodule:: utah.provisioning.exceptions
92 :members:95 :members:
9396
@@ -96,9 +99,15 @@
9699
97.. automodule:: utah.provisioning.baremetal100.. automodule:: utah.provisioning.baremetal
98101
102.. automodule:: utah.provisioning.baremetal.inventory
103 :members:
104
99.. automodule:: utah.provisioning.baremetal.cobbler105.. automodule:: utah.provisioning.baremetal.cobbler
100 :members:106 :members:
101107
108.. automodule:: utah.provisioning.baremetal.bamboofeeder
109 :members:
110
102.. automodule:: utah.provisioning.baremetal.exceptions111.. automodule:: utah.provisioning.baremetal.exceptions
103 :members:112 :members:
104113
@@ -124,9 +133,6 @@
124.. automodule:: utah.provisioning.vm.exceptions133.. automodule:: utah.provisioning.vm.exceptions
125 :members:134 :members:
126135
127.. automodule:: utah.provisioning.vm.libvirtvm
128 :members:
129
130.. automodule:: utah.provisioning.vm.vm136.. automodule:: utah.provisioning.vm.vm
131 :members:137 :members:
132138
133139
=== modified file 'examples/run_install_test.py'
--- examples/run_install_test.py 2012-12-08 02:10:12 +0000
+++ examples/run_install_test.py 2012-12-13 00:38:21 +0000
@@ -22,8 +22,7 @@
22from utah.exceptions import UTAHException22from utah.exceptions import UTAHException
23from utah.url import url_argument23from utah.url import url_argument
24from utah.group import check_user_group, print_group_error_message24from utah.group import check_user_group, print_group_error_message
25from utah.provisioning.inventory.sqlite import TinySQLiteInventory25from utah.provisioning.vm.vm import CustomVM, TinySQLiteInventory
26from utah.provisioning.vm.libvirtvm import CustomVM
27from utah.run import run_tests26from utah.run import run_tests
2827
2928
3029
=== modified file 'examples/run_test_bamboo_feeder.py'
--- examples/run_test_bamboo_feeder.py 2012-12-11 08:41:24 +0000
+++ examples/run_test_bamboo_feeder.py 2012-12-13 00:38:21 +0000
@@ -23,7 +23,8 @@
23from utah.exceptions import UTAHException23from utah.exceptions import UTAHException
24from utah.group import check_user_group, print_group_error_message24from utah.group import check_user_group, print_group_error_message
25from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine25from utah.provisioning.baremetal.bamboofeeder import BambooFeederMachine
26from utah.provisioning.inventory.sqlite import ManualBaremetalSQLiteInventory26from utah.provisioning.baremetal.inventory import \
27 ManualBaremetalSQLiteInventory
27from utah.run import run_tests28from utah.run import run_tests
28from utah.url import url_argument29from utah.url import url_argument
2930
3031
=== modified file 'examples/run_test_cobbler.py'
--- examples/run_test_cobbler.py 2012-12-11 18:56:17 +0000
+++ examples/run_test_cobbler.py 2012-12-13 00:38:21 +0000
@@ -21,7 +21,9 @@
21from utah import config21from utah import config
22from utah.exceptions import UTAHException22from utah.exceptions import UTAHException
23from utah.group import check_user_group, print_group_error_message23from utah.group import check_user_group, print_group_error_message
24from utah.provisioning.inventory.sqlite import ManualBaremetalSQLiteInventory24from utah.provisioning.baremetal.cobbler import CobblerMachine
25from utah.provisioning.baremetal.inventory import \
26 ManualBaremetalSQLiteInventory
25from utah.run import run_tests27from utah.run import run_tests
26from utah.url import url_argument28from utah.url import url_argument
2729
@@ -105,7 +107,8 @@
105 kw[arg] = value107 kw[arg] = value
106 if getattr(args, 'type') is not None:108 if getattr(args, 'type') is not None:
107 kw['installtype'] = args.type109 kw['installtype'] = args.type
108 machine = inventory.request(clean=(not args.no_destroy),110 machine = inventory.request(CobblerMachine,
111 clean=(not args.no_destroy),
109 debug=args.debug, dlpercentincrement=10,112 debug=args.debug, dlpercentincrement=10,
110 name=args.name, new=True, **kw)113 name=args.name, new=True, **kw)
111 exitstatus, locallogs = run_tests(args, machine)114 exitstatus, locallogs = run_tests(args, machine)
112115
=== modified file 'examples/run_test_vm.py'
--- examples/run_test_vm.py 2012-12-10 19:18:18 +0000
+++ examples/run_test_vm.py 2012-12-13 00:38:21 +0000
@@ -21,7 +21,7 @@
21from utah import config21from utah import config
22from utah.exceptions import UTAHException22from utah.exceptions import UTAHException
23from utah.group import check_user_group, print_group_error_message23from utah.group import check_user_group, print_group_error_message
24from utah.provisioning.inventory.sqlite import TinySQLiteInventory24from utah.provisioning.vm.vm import TinySQLiteInventory
25from utah.run import run_tests25from utah.run import run_tests
2626
2727
2828
=== modified file 'examples/utah-user-setup.sh'
--- examples/utah-user-setup.sh 2012-07-19 20:00:02 +0000
+++ examples/utah-user-setup.sh 2012-12-13 00:38:21 +0000
@@ -63,13 +63,6 @@
6363
64set +e64set +e
6565
66CMD=/usr/share/utah/examples/vmtools-user-setup.sh
67if [ -f "$CMD" ]
68then
69 echo "Setting up vm-tools config"
70 keepgoing $CMD $AUTO
71fi
72
73if [ "$LOGOUT" ]66if [ "$LOGOUT" ]
74then67then
75 echo "User groups were updated"68 echo "User groups were updated"
7669
=== removed file 'examples/vmtools-user-setup.sh'
--- examples/vmtools-user-setup.sh 2012-07-19 20:00:02 +0000
+++ examples/vmtools-user-setup.sh 1970-01-01 00:00:00 +0000
@@ -1,59 +0,0 @@
1#!/bin/bash
2
3set -e
4
5function keepgoing
6{
7 echo "Going to run:"
8 echo "$@"
9 if [ "$AUTO" ]
10 then
11 $@
12 else
13 echo "Hit enter to continue, enter any text to stop"
14 read ENTRY
15 if [ -n "$ENTRY" ]
16 then
17 exit 1
18 else
19 echo $@ | bash
20# echo "$@"
21# $@
22 fi
23 fi
24}
25
26if (echo "$@" | grep -iq "a")
27then
28 export AUTO="auto"
29else
30 export AUTO=""
31fi
32
33if ! [ -f ~/.uqt-vm-tools.conf ]
34then
35 echo "Running vm-new to create ~/.uqv-vm-tools.conf"
36 if [ "$AUTO" ]
37 then
38 CMD="echo \"y\" | vm-new"
39 else
40 CMD=vm-new
41 fi
42 keepgoing $CMD
43fi
44
45SSHKEY=$(echo -e "import utah.config\nprint(utah.config.sshpublickey)" | python)
46if ! (grep -q "^vm_ssh_key=$SSHKEY" ~/.uqt-vm-tools.conf)
47then
48 CMD="sed 's@^vm_ssh_key=[^#]*#@vm_ssh_key=$SSHKEY #@' -i ~/.uqt-vm-tools.conf"
49 echo "Adding SSH key to config file"
50 keepgoing $CMD
51fi
52
53if ! (grep "^vm_extra_packages=" ~/.uqt-vm-tools.conf | grep "python-yaml" | grep "bzr" | grep -q "git")
54then
55 CMD="sed 's/^vm_extra_packages=\"/vm_extra_packages=\"python-yaml bzr git /' -i ~/.uqt-vm-tools.conf"
56 echo "Adding client dependencies to config file"
57 keepgoing $CMD
58fi
59
600
=== modified file 'utah/provisioning/baremetal/bamboofeeder.py'
--- utah/provisioning/baremetal/bamboofeeder.py 2012-12-11 09:37:12 +0000
+++ utah/provisioning/baremetal/bamboofeeder.py 2012-12-13 00:38:21 +0000
@@ -25,13 +25,13 @@
25import tempfile25import tempfile
2626
27from utah import config27from utah import config
28from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
29from utah.provisioning.baremetal.power import PowerMixin
28from utah.provisioning.provisioning import (30from utah.provisioning.provisioning import (
29 CustomInstallMixin,31 CustomInstallMixin,
30 Machine,32 Machine,
31 SSHMixin,
32)33)
33from utah.provisioning.baremetal.power import PowerMixin34from utah.provisioning.ssh import SSHMixin
34from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
35from utah.retry import retry35from utah.retry import retry
3636
3737
3838
=== modified file 'utah/provisioning/baremetal/cobbler.py'
--- utah/provisioning/baremetal/cobbler.py 2012-12-11 18:38:12 +0000
+++ utah/provisioning/baremetal/cobbler.py 2012-12-13 00:38:21 +0000
@@ -26,13 +26,13 @@
26import time26import time
2727
28from utah import config28from utah import config
29from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException
30from utah.provisioning.baremetal.power import PowerMixin
29from utah.provisioning.provisioning import (31from utah.provisioning.provisioning import (
30 CustomInstallMixin,32 CustomInstallMixin,
31 Machine,33 Machine,
32 SSHMixin,
33)34)
34from utah.provisioning.baremetal.exceptions import UTAHBMProvisioningException35from utah.provisioning.ssh import SSHMixin
35from utah.provisioning.baremetal.power import PowerMixin
36from utah.retry import retry36from utah.retry import retry
3737
3838
3939
=== added file 'utah/provisioning/baremetal/inventory.py'
--- utah/provisioning/baremetal/inventory.py 1970-01-01 00:00:00 +0000
+++ utah/provisioning/baremetal/inventory.py 2012-12-13 00:38:21 +0000
@@ -0,0 +1,153 @@
1# Ubuntu Testing Automation Harness
2# Copyright 2012 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""
17Provide inventory functions specific to bare metal deployments.
18"""
19
20import os
21import psutil
22from utah.provisioning.inventory.exceptions import \
23 UTAHProvisioningInventoryException
24from utah.provisioning.inventory.sqlite import SQLiteInventory
25
26
27class ManualBaremetalSQLiteInventory(SQLiteInventory):
28 """
29 Keep an inventory of manually entered machines.
30 All columns other than machineid, name, and state are assumed to be
31 arguments for system creation (i.e., with cobbler).
32 """
33 # TODO: rename this
34 def __init__(self, db='~/.utah-baremetal-inventory',
35 lockfile='~/.utah-baremetal-lock', *args, **kw):
36 db = os.path.expanduser(db)
37 lockfile = os.path.expanduser(lockfile)
38 if not os.path.isfile(db):
39 raise UTAHProvisioningInventoryException(
40 'No machine database found at ' + db)
41 super(ManualBaremetalSQLiteInventory, self).__init__(*args, db=db,
42 lockfile=lockfile,
43 **kw)
44 machines_count = (self.connection
45 .execute('SELECT COUNT(*) FROM machines')
46 .fetchall()[0][0])
47 if machines_count == 0:
48 raise UTAHProvisioningInventoryException('No machines in database')
49 self.machines = []
50
51 def request(self, machinetype, name=None, *args, **kw):
52 query = 'SELECT * FROM machines'
53 queryvars = []
54 if name is not None:
55 query += ' WHERE name=?'
56 queryvars.append(name)
57 result = self.connection.execute(query, queryvars).fetchall()
58 if result is None:
59 raise UTAHProvisioningInventoryException(
60 'No machines meet criteria')
61 else:
62 for minfo in result:
63 machineinfo = dict(minfo)
64 if machineinfo['state'] == 'available':
65 return self._take(machineinfo, machinetype, *args, **kw)
66 for minfo in result:
67 machineinfo = dict(minfo)
68 pid = machineinfo['pid']
69 try:
70 if not (psutil.pid_exists(pid) and ('utah' in
71 ' '.join(psutil.Process(pid).cmdline)
72 or 'run_test' in
73 ' '.join(psutil.Process(pid).cmdline))):
74 return self._take(machineinfo, machinetype,
75 *args, **kw)
76 except ValueError:
77 continue
78
79 raise UTAHProvisioningInventoryException(
80 'All machines meeting criteria are currently unavailable')
81
82 def _take(self, machineinfo, machinetype, *args, **kw):
83 machineid = machineinfo.pop('machineid')
84 name = machineinfo.pop('name')
85 state = machineinfo.pop('state')
86 machineinfo.pop('pid')
87 update = self.connection.execute(
88 "UPDATE machines SET pid=?, state='provisioned' WHERE machineid=?"
89 "AND state=?",
90 [os.getpid(), machineid, state]).rowcount
91 if update == 1:
92 machine = machinetype(*args, inventory=self,
93 machineinfo=machineinfo, name=name, **kw)
94 self.machines.append(machine)
95 return machine
96 elif update == 0:
97 raise UTAHProvisioningInventoryException(
98 'Machine was requested by another process '
99 'before we could request it')
100 elif update > 1:
101 raise UTAHProvisioningInventoryException(
102 'Multiple machines exist '
103 'matching those criteria; '
104 'database ' + self.db + ' may be corrupt')
105 else:
106 raise UTAHProvisioningInventoryException(
107 'Negative rowcount returned '
108 'when attempting to request machine')
109
110 def release(self, machine=None, name=None):
111 if machine is not None:
112 name = machine.name
113 if name is None:
114 raise UTAHProvisioningInventoryException(
115 'name required to release a machine')
116 query = "UPDATE machines SET state='available' WHERE name=?"
117 queryvars = [name]
118 update = self.connection.execute(query, queryvars).rowcount
119 if update == 1:
120 if machine is not None:
121 if machine in self.machines:
122 self.machines.remove(machine)
123 elif update == 0:
124 raise UTAHProvisioningInventoryException(
125 'SERIOUS ERROR: Another process released this machine '
126 'before we could, which means two processes provisioned '
127 'the same machine simultaneously')
128 elif update > 1:
129 raise UTAHProvisioningInventoryException(
130 'Multiple machines exist matching those criteria; '
131 'database ' + self.db + ' may be corrupt')
132 else:
133 raise UTAHProvisioningInventoryException(
134 'Negative rowcount returned '
135 'when attempting to release machine')
136
137 # Here is how I currently create the database:
138 # CREATE TABLE machines (machineid INTEGER PRIMARY KEY,
139 # name TEXT NOT NULL UNIQUE,
140 # state TEXT default 'available',
141 # pid INT,
142 # [mac-address] TEXT NOT NULL UNIQUE,
143 # [power-address] TEXT DEFAULT '10.97.0.13',
144 # [power-id] TEXT,
145 # [power-user] TEXT DEFAULT 'ubuntu',
146 # [power-pass] TEXT DEFAULT 'ubuntu',
147 # [power-type] TEXT DEFAULT 'sentryswitch_cdu');
148
149 # Here is how I currently populate the database:
150 # INSERT INTO machines (name, [mac-address], [power-id])
151 # VALUES ('acer-veriton-01-Pete', 'd0:27:88:9f:73:ce', 'Veriton_1');
152 # INSERT INTO machines (name, [mac-address], [power-id])
153 # VALUES ('acer-veriton-02-Pete', 'd0:27:88:9b:84:5b', 'Veriton_2');
0154
=== modified file 'utah/provisioning/inventory/sqlite.py'
--- utah/provisioning/inventory/sqlite.py 2012-12-11 18:45:40 +0000
+++ utah/provisioning/inventory/sqlite.py 2012-12-13 00:38:21 +0000
@@ -17,12 +17,7 @@
1717
18import sqlite318import sqlite3
19import os19import os
20import psutil
21from utah.provisioning.inventory.exceptions import \
22 UTAHProvisioningInventoryException
23from utah.provisioning.inventory.inventory import Inventory20from utah.provisioning.inventory.inventory import Inventory
24from utah.provisioning.vm.libvirtvm import CustomVM
25from utah.provisioning.baremetal.cobbler import CobblerMachine
2621
2722
28class SQLiteInventory(Inventory):23class SQLiteInventory(Inventory):
@@ -41,181 +36,3 @@
41 def delete(self):36 def delete(self):
42 os.unlink(self.db)37 os.unlink(self.db)
43 super(SQLiteInventory, self).delete()38 super(SQLiteInventory, self).delete()
44
45
46class TinySQLiteInventory(SQLiteInventory):
47 """
48 Tiny SQLite inventory that implements request, release, and destroy.
49
50 No authentication or conflict checking currently exists.
51 """
52 def __init__(self, *args, **kw):
53 """
54 Initialize simple database.
55 """
56 super(TinySQLiteInventory, self).__init__(*args, **kw)
57 self.connection.execute(
58 'CREATE TABLE IF NOT EXISTS '
59 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')
60
61 def request(self, machinetype=CustomVM, *args, **kw):
62 """
63 Takes a Machine class as machinetype, and passes the newly generated
64 machineid along with all other arguments to that class's constructor,
65 returning the resulting object.
66 """
67 cursor = self.connection.cursor()
68 cursor.execute("INSERT INTO machines (state) VALUES ('provisioned')")
69 machineid = cursor.lastrowid
70 return machinetype(machineid=machineid, *args, **kw)
71
72 def release(self, machineid):
73 """
74 Updates the database to indicate the machine is available.
75 """
76 if self.connection.execute(
77 "UPDATE machines SET state='available' WHERE machineid=?",
78 [machineid]):
79 return True
80 else:
81 return False
82
83 def destroy(self, machineid):
84 """
85 Updates the database to indicate the machine is destroyed, but does not
86 destroy the machine.
87 """
88 if self.connection.execute(
89 "UPDATE machines SET state='destroyed' ""WHERE machineid=?",
90 [machineid]):
91 return True
92 else:
93 return False
94
95
96class ManualBaremetalSQLiteInventory(SQLiteInventory):
97 """
98 Keep an inventory of manually entered machines.
99 All columns other than machineid, name, and state are assumed to be
100 arguments for system creation (i.e., with cobbler).
101 """
102 # TODO: rename this
103 def __init__(self, db='~/.utah-baremetal-inventory',
104 lockfile='~/.utah-baremetal-lock', *args, **kw):
105 db = os.path.expanduser(db)
106 lockfile = os.path.expanduser(lockfile)
107 if not os.path.isfile(db):
108 raise UTAHProvisioningInventoryException(
109 'No machine database found at ' + db)
110 super(ManualBaremetalSQLiteInventory, self).__init__(*args, db=db,
111 lockfile=lockfile,
112 **kw)
113 machines_count = (self.connection
114 .execute('SELECT COUNT(*) FROM machines')
115 .fetchall()[0][0])
116 if machines_count == 0:
117 raise UTAHProvisioningInventoryException('No machines in database')
118 self.machines = []
119
120 def request(self, machinetype=CobblerMachine, name=None, *args, **kw):
121 query = 'SELECT * FROM machines'
122 queryvars = []
123 if name is not None:
124 query += ' WHERE name=?'
125 queryvars.append(name)
126 result = self.connection.execute(query, queryvars).fetchall()
127 if result is None:
128 raise UTAHProvisioningInventoryException(
129 'No machines meet criteria')
130 else:
131 for minfo in result:
132 machineinfo = dict(minfo)
133 if machineinfo['state'] == 'available':
134 return self._take(machineinfo, *args, **kw)
135 for minfo in result:
136 machineinfo = dict(minfo)
137 pid = machineinfo['pid']
138 try:
139 if not (psutil.pid_exists(pid) and ('utah' in
140 ' '.join(psutil.Process(pid).cmdline)
141 or 'run_test' in
142 ' '.join(psutil.Process(pid).cmdline))):
143 return self._take(machineinfo, *args, **kw)
144 except ValueError:
145 continue
146
147 raise UTAHProvisioningInventoryException(
148 'All machines meeting criteria are currently unavailable')
149
150 def _take(self, machineinfo, *args, **kw):
151 machineid = machineinfo.pop('machineid')
152 name = machineinfo.pop('name')
153 state = machineinfo.pop('state')
154 machineinfo.pop('pid')
155 update = self.connection.execute(
156 "UPDATE machines SET pid=?, state='provisioned' WHERE machineid=?"
157 "AND state=?",
158 [os.getpid(), machineid, state]).rowcount
159 if update == 1:
160 machine = CobblerMachine(*args, inventory=self,
161 machineinfo=machineinfo, name=name, **kw)
162 self.machines.append(machine)
163 return machine
164 elif update == 0:
165 raise UTAHProvisioningInventoryException(
166 'Machine was requested by another process '
167 'before we could request it')
168 elif update > 1:
169 raise UTAHProvisioningInventoryException(
170 'Multiple machines exist '
171 'matching those criteria; '
172 'database ' + self.db + ' may be corrupt')
173 else:
174 raise UTAHProvisioningInventoryException(
175 'Negative rowcount returned '
176 'when attempting to request machine')
177
178 def release(self, machine=None, name=None):
179 if machine is not None:
180 name = machine.name
181 if name is None:
182 raise UTAHProvisioningInventoryException(
183 'name required to release a machine')
184 query = "UPDATE machines SET state='available' WHERE name=?"
185 queryvars = [name]
186 update = self.connection.execute(query, queryvars).rowcount
187 if update == 1:
188 if machine is not None:
189 if machine in self.machines:
190 self.machines.remove(machine)
191 elif update == 0:
192 raise UTAHProvisioningInventoryException(
193 'SERIOUS ERROR: Another process released this machine '
194 'before we could, which means two processes provisioned '
195 'the same machine simultaneously')
196 elif update > 1:
197 raise UTAHProvisioningInventoryException(
198 'Multiple machines exist matching those criteria; '
199 'database ' + self.db + ' may be corrupt')
200 else:
201 raise UTAHProvisioningInventoryException(
202 'Negative rowcount returned '
203 'when attempting to release machine')
204
205 # Here is how I currently create the database:
206 # CREATE TABLE machines (machineid INTEGER PRIMARY KEY,
207 # name TEXT NOT NULL UNIQUE,
208 # state TEXT default 'available',
209 # pid TEXT,
210 # [mac-address] TEXT NOT NULL UNIQUE,
211 # [power-address] TEXT DEFAULT '10.97.0.13',
212 # [power-id] TEXT,
213 # [power-user] TEXT DEFAULT 'ubuntu',
214 # [power-pass] TEXT DEFAULT 'ubuntu',
215 # [power-type] TEXT DEFAULT 'sentryswitch_cdu');
216
217 # Here is how I currently populate the database:
218 # INSERT INTO machines (name, [mac-address], [power-id])
219 # VALUES ('acer-veriton-01-Pete', 'd0:27:88:9f:73:ce', 'Veriton_1');
220 # INSERT INTO machines (name, [mac-address], [power-id])
221 # VALUES ('acer-veriton-02-Pete', 'd0:27:88:9b:84:5b', 'Veriton_2');
22239
=== modified file 'utah/provisioning/provisioning.py'
--- utah/provisioning/provisioning.py 2012-12-11 09:50:08 +0000
+++ utah/provisioning/provisioning.py 2012-12-13 00:38:21 +0000
@@ -18,26 +18,24 @@
18Functions here should apply to multiple machine types (VM, bare metal, etc.)18Functions here should apply to multiple machine types (VM, bare metal, etc.)
19"""19"""
2020
21import apt.cache
21import logging22import logging
22import logging.handlers23import logging.handlers
23import os24import os
24import pipes25import pipes
26import re
25import shutil27import shutil
26import socket
27import subprocess28import subprocess
28import sys29import sys
29import time30import time
30import urllib31import urllib
31import uuid32import uuid
32import paramiko
33import re
34import apt.cache
3533
36from glob import glob34from glob import glob
37from stat import S_ISDIR
3835
39import utah.timeout36import utah.timeout
4037
38from utah import config
41from utah.commandstr import commandstr39from utah.commandstr import commandstr
42from utah.iso import ISO40from utah.iso import ISO
43from utah.orderedcollections import (41from utah.orderedcollections import (
@@ -47,7 +45,6 @@
47from utah.preseed import Preseed45from utah.preseed import Preseed
48from utah.provisioning.exceptions import UTAHProvisioningException46from utah.provisioning.exceptions import UTAHProvisioningException
49from utah.retry import retry47from utah.retry import retry
50from utah import config
5148
5249
53class Machine(object):50class Machine(object):
@@ -679,243 +676,6 @@
679 'Try rerunning install.')676 'Try rerunning install.')
680677
681678
682class SSHMixin(object):
683 """
684 Provide common commands for machines accessed via ssh.
685 """
686 def __init__(self, *args, **kwargs):
687 # Note: Since this is a mixin it doesn't expect any argument
688 # However, it calls super to initialize any other mixins in the mro
689 super(SSHMixin, self).__init__(*args, **kwargs)
690 ssh_client = paramiko.SSHClient()
691 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
692 self.ssh_client = ssh_client
693
694 def run(self, command, _quiet=None, root=False, timeout=None):
695 """
696 Run a command using ssh.
697 """
698 if isinstance(command, basestring):
699 commandstring = command
700 else:
701 commandstring = ' '.join(command)
702 if root:
703 user = 'root'
704 else:
705 user = config.user
706
707 self.activecheck()
708 # Some commands expect run to return the output status of the command
709 # We're going to try the method described here:
710 # http://stackoverflow.com/questions/3562403/
711 # With additions from here:
712 # http://od-eon.com/blogs/
713 # stefan/automating-remote-commands-over-ssh-paramiko/
714 self.logger.debug('Connecting SSH')
715 self.ssh_client.connect(self.name,
716 username=user,
717 key_filename=config.sshprivatekey)
718
719 self.logger.debug('Opening SSH session')
720 channel = self.ssh_client.get_transport().open_session()
721
722 self.logger.info('Running command through SSH: ' + commandstring)
723 stdout = channel.makefile('rb')
724 stderr = channel.makefile_stderr('rb')
725 if timeout is None:
726 channel.exec_command(commandstring)
727 else:
728 utah.timeout.timeout(timeout, channel.exec_command, commandstring)
729 retval = channel.recv_exit_status()
730
731 self.logger.debug('Closing SSH connection')
732 self.ssh_client.close()
733
734 log_level = logging.DEBUG if retval == 0 else logging.WARNING
735 log_message = 'Return code: {}'.format(retval)
736 self.logger.log(log_level, log_message)
737
738 self.logger.debug('Standard output follows:')
739 stdout_lines = stdout.readlines()
740 for line in stdout_lines:
741 self.logger.debug(line.strip())
742
743 self.logger.debug('Standard error follows:')
744 stderr_lines = stderr.readlines()
745 for line in stderr_lines:
746 self.logger.debug(line.strip())
747
748 return retval, ''.join(stdout_lines), ''.join(stderr_lines)
749
750 def uploadfiles(self, files, target=os.path.normpath('/tmp/')):
751 """
752 Copy a file or list of files to a target directory on the machine.
753 """
754 if isinstance(files, basestring):
755 files = [files]
756
757 self.activecheck()
758 self.ssh_client.connect(self.name,
759 username=config.user,
760 key_filename=config.sshprivatekey)
761 sftp_client = self.ssh_client.open_sftp()
762 failed = []
763 try:
764 for localpath in files:
765 if os.path.isfile(localpath):
766 self.logger.info('Uploading ' + localpath
767 + ' from the host to ' + target
768 + ' on the machine')
769 remotepath = os.path.join(target,
770 os.path.basename(localpath))
771 sftp_client.put(localpath, remotepath)
772 else:
773 failed.append(localpath)
774 finally:
775 sftp_client.close()
776 if len(failed) > 0:
777 err = UTAHProvisioningException('Files do not exist: '
778 + ' '.join(failed))
779 err.files = failed
780 raise err
781
782 def downloadfiles(self, files, target=os.path.normpath('/tmp/')):
783 """
784 Copy a file or list of files from the machine to a target directory on
785 the local system.
786 """
787 # TODO: check for directories and recurse into them
788 if isinstance(files, basestring):
789 files = [files]
790
791 self.activecheck()
792 self.ssh_client.connect(self.name,
793 username=config.user,
794 key_filename=config.sshprivatekey)
795 sftp_client = self.ssh_client.open_sftp()
796 if os.path.isdir(target):
797 get_localpath = lambda remotepath: \
798 os.path.join(target, os.path.basename(remotepath))
799 else:
800 get_localpath = lambda remotepath: target
801
802 try:
803 for remotepath in files:
804 localpath = get_localpath(remotepath)
805 self.logger.info('Downloading ' + remotepath
806 + ' from the machine to ' + target
807 + ' on the host')
808 sftp_client.get(remotepath, localpath)
809 finally:
810 sftp_client.close()
811
812 def downloadfilesrecursive(self, files, target=os.path.normpath('/tmp/')):
813 """
814 Recursively copy all files in files to the target directory target.
815 """
816 self.activecheck()
817 self.ssh_client.connect(self.name,
818 username=config.user,
819 key_filename=config.sshprivatekey)
820 sftp_client = self.ssh_client.open_sftp()
821 myfiles = []
822
823 if isinstance(files, basestring):
824 files = [files]
825
826 for myfile in files:
827 newtarget = os.path.join(target, os.path.basename(myfile))
828 if S_ISDIR(sftp_client.stat(myfile).st_mode):
829 self.logger.debug(myfile + ' is a directory, recursing')
830 if not os.path.isdir(newtarget):
831 self.logger.debug('Attempting to create ' + newtarget)
832 os.makedirs(newtarget)
833 myfiles = [os.path.join(myfile, x)
834 for x in sftp_client.listdir(myfile)]
835# for basename in sftp_client.listdir(dirname):
836# myfile = os.path.join(dirname, basename)
837# if S_ISDIR(sftp_client.stat(myfile).st_mode):
838# if not os.path.isdir(newtarget):
839# os.makedirs(newtarget)
840# self.downloadfilesrecursive(myfile, newtarget)
841# else:
842# myfiles.append(myfile)
843 self.downloadfilesrecursive(myfiles, newtarget)
844 else:
845 self.downloadfiles(myfile, newtarget)
846
847 def destroy(self, *args, **kw):
848 """
849 Clean up known hosts for machine.
850 """
851 # TODO: evaluate value of known_hosts with paramiko
852 self.logger.info('Removing machine addresses '
853 'from ssh known_hosts file')
854 addresses = [self.name]
855 try:
856 addresses.append(socket.gethostbyname(self.name))
857 except socket.gaierror as err:
858 if err.errno in [-2, -5]:
859 self.logger.debug(self.name
860 + ' is not resolvable, '
861 + 'so not removing from known_hosts')
862 else:
863 raise err
864
865 old_host_keys = self.ssh_client.get_host_keys()
866 new_host_keys = paramiko.HostKeys()
867 addresses = set(addresses)
868 for address, key in old_host_keys.iteritems():
869 # Skip keys so that they don't get added
870 # into the new keys (i.e. they're removed)
871 if address in addresses:
872 continue
873 new_host_keys[address] = key
874 new_host_keys.save(config.sshknownhosts)
875 self.ssh_client.close()
876
877 super(SSHMixin, self).destroy(*args, **kw)
878
879 def sshcheck(self, timeout=config.checktimeout):
880 """
881 Sleep for a while and check if the machine is available via ssh.
882 Return a retryable exception if it is not.
883 Intended for use with retry.
884 """
885 self.logger.info('Sleeping {timeout} seconds'
886 .format(timeout=timeout))
887 time.sleep(timeout)
888 self.logger.info('Checking for ssh availability')
889 try:
890 self.ssh_client.connect(self.name,
891 username=config.user,
892 key_filename=config.sshprivatekey)
893 except socket.error as err:
894 raise UTAHProvisioningException(str(err), retry=True)
895
896 def sshpoll(self, timeout=None,
897 checktimeout=config.checktimeout, logmethod=None):
898 """
899 Run sshcheck over and over until timeout expires.
900 """
901 if timeout is None:
902 timeout = self.boottimeout
903 if logmethod is None:
904 logmethod = self.logger.debug
905 utah.timeout.timeout(timeout, retry, self.sshcheck, checktimeout,
906 logmethod=logmethod)
907
908 def activecheck(self):
909 """
910 Start the machine if needed, and check for SSH login.
911 """
912 self.logger.debug('Checking if machine is active')
913 self.provisioncheck()
914 if not self.active:
915 self._start()
916 self.sshcheck()
917
918
919class CustomInstallMixin(object):679class CustomInstallMixin(object):
920 """680 """
921 Provide routines for unpacking necessary boot files from images,681 Provide routines for unpacking necessary boot files from images,
922682
=== added file 'utah/provisioning/ssh.py'
--- utah/provisioning/ssh.py 1970-01-01 00:00:00 +0000
+++ utah/provisioning/ssh.py 2012-12-13 00:38:21 +0000
@@ -0,0 +1,261 @@
1# Ubuntu Testing Automation Harness
2# Copyright 2012 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""
17Provide a mixin class for machines with SSH support.
18"""
19
20import logging
21import os
22import paramiko
23import socket
24import time
25
26from stat import S_ISDIR
27
28import utah.timeout
29
30from utah import config
31from utah.provisioning.exceptions import UTAHProvisioningException
32from utah.retry import retry
33
34
35class SSHMixin(object):
36 """
37 Provide common commands for machines accessed via ssh.
38 """
39 def __init__(self, *args, **kwargs):
40 # Note: Since this is a mixin it doesn't expect any argument
41 # However, it calls super to initialize any other mixins in the mro
42 super(SSHMixin, self).__init__(*args, **kwargs)
43 ssh_client = paramiko.SSHClient()
44 ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
45 self.ssh_client = ssh_client
46
47 def run(self, command, _quiet=None, root=False, timeout=None):
48 """
49 Run a command using ssh.
50 """
51 if isinstance(command, basestring):
52 commandstring = command
53 else:
54 commandstring = ' '.join(command)
55 if root:
56 user = 'root'
57 else:
58 user = config.user
59
60 self.activecheck()
61 # Some commands expect run to return the output status of the command
62 # We're going to try the method described here:
63 # http://stackoverflow.com/questions/3562403/
64 # With additions from here:
65 # http://od-eon.com/blogs/
66 # stefan/automating-remote-commands-over-ssh-paramiko/
67 self.logger.debug('Connecting SSH')
68 self.ssh_client.connect(self.name,
69 username=user,
70 key_filename=config.sshprivatekey)
71
72 self.logger.debug('Opening SSH session')
73 channel = self.ssh_client.get_transport().open_session()
74
75 self.logger.info('Running command through SSH: ' + commandstring)
76 stdout = channel.makefile('rb')
77 stderr = channel.makefile_stderr('rb')
78 if timeout is None:
79 channel.exec_command(commandstring)
80 else:
81 utah.timeout.timeout(timeout, channel.exec_command, commandstring)
82 retval = channel.recv_exit_status()
83
84 self.logger.debug('Closing SSH connection')
85 self.ssh_client.close()
86
87 log_level = logging.DEBUG if retval == 0 else logging.WARNING
88 log_message = 'Return code: {}'.format(retval)
89 self.logger.log(log_level, log_message)
90
91 self.logger.debug('Standard output follows:')
92 stdout_lines = stdout.readlines()
93 for line in stdout_lines:
94 self.logger.debug(line.strip())
95
96 self.logger.debug('Standard error follows:')
97 stderr_lines = stderr.readlines()
98 for line in stderr_lines:
99 self.logger.debug(line.strip())
100
101 return retval, ''.join(stdout_lines), ''.join(stderr_lines)
102
103 def uploadfiles(self, files, target=os.path.normpath('/tmp/')):
104 """
105 Copy a file or list of files to a target directory on the machine.
106 """
107 if isinstance(files, basestring):
108 files = [files]
109
110 self.activecheck()
111 self.ssh_client.connect(self.name,
112 username=config.user,
113 key_filename=config.sshprivatekey)
114 sftp_client = self.ssh_client.open_sftp()
115 failed = []
116 try:
117 for localpath in files:
118 if os.path.isfile(localpath):
119 self.logger.info('Uploading ' + localpath
120 + ' from the host to ' + target
121 + ' on the machine')
122 remotepath = os.path.join(target,
123 os.path.basename(localpath))
124 sftp_client.put(localpath, remotepath)
125 else:
126 failed.append(localpath)
127 finally:
128 sftp_client.close()
129 if len(failed) > 0:
130 err = UTAHProvisioningException('Files do not exist: '
131 + ' '.join(failed))
132 err.files = failed
133 raise err
134
135 def downloadfiles(self, files, target=os.path.normpath('/tmp/')):
136 """
137 Copy a file or list of files from the machine to a target directory on
138 the local system.
139 """
140 # TODO: check for directories and recurse into them
141 if isinstance(files, basestring):
142 files = [files]
143
144 self.activecheck()
145 self.ssh_client.connect(self.name,
146 username=config.user,
147 key_filename=config.sshprivatekey)
148 sftp_client = self.ssh_client.open_sftp()
149 if os.path.isdir(target):
150 get_localpath = lambda remotepath: \
151 os.path.join(target, os.path.basename(remotepath))
152 else:
153 get_localpath = lambda remotepath: target
154
155 try:
156 for remotepath in files:
157 localpath = get_localpath(remotepath)
158 self.logger.info('Downloading ' + remotepath
159 + ' from the machine to ' + target
160 + ' on the host')
161 sftp_client.get(remotepath, localpath)
162 finally:
163 sftp_client.close()
164
165 def downloadfilesrecursive(self, files, target=os.path.normpath('/tmp/')):
166 """
167 Recursively copy all files in files to the target directory target.
168 """
169 self.activecheck()
170 self.ssh_client.connect(self.name,
171 username=config.user,
172 key_filename=config.sshprivatekey)
173 sftp_client = self.ssh_client.open_sftp()
174 myfiles = []
175
176 if isinstance(files, basestring):
177 files = [files]
178
179 for myfile in files:
180 newtarget = os.path.join(target, os.path.basename(myfile))
181 if S_ISDIR(sftp_client.stat(myfile).st_mode):
182 self.logger.debug(myfile + ' is a directory, recursing')
183 if not os.path.isdir(newtarget):
184 self.logger.debug('Attempting to create ' + newtarget)
185 os.makedirs(newtarget)
186 myfiles = [os.path.join(myfile, x)
187 for x in sftp_client.listdir(myfile)]
188 self.downloadfilesrecursive(myfiles, newtarget)
189 else:
190 self.downloadfiles(myfile, newtarget)
191
192 def destroy(self, *args, **kw):
193 """
194 Clean up known hosts for machine.
195 """
196 # TODO: evaluate value of known_hosts with paramiko
197 self.logger.info('Removing machine addresses '
198 'from ssh known_hosts file')
199 addresses = [self.name]
200 try:
201 addresses.append(socket.gethostbyname(self.name))
202 except socket.gaierror as err:
203 if err.errno in [-2, -5]:
204 self.logger.debug(self.name
205 + ' is not resolvable, '
206 + 'so not removing from known_hosts')
207 else:
208 raise err
209
210 old_host_keys = self.ssh_client.get_host_keys()
211 new_host_keys = paramiko.HostKeys()
212 addresses = set(addresses)
213 for address, key in old_host_keys.iteritems():
214 # Skip keys so that they don't get added
215 # into the new keys (i.e. they're removed)
216 if address in addresses:
217 continue
218 new_host_keys[address] = key
219 new_host_keys.save(config.sshknownhosts)
220 self.ssh_client.close()
221
222 super(SSHMixin, self).destroy(*args, **kw)
223
224 def sshcheck(self, timeout=config.checktimeout):
225 """
226 Sleep for a while and check if the machine is available via ssh.
227 Return a retryable exception if it is not.
228 Intended for use with retry.
229 """
230 self.logger.info('Sleeping {timeout} seconds'
231 .format(timeout=timeout))
232 time.sleep(timeout)
233 self.logger.info('Checking for ssh availability')
234 try:
235 self.ssh_client.connect(self.name,
236 username=config.user,
237 key_filename=config.sshprivatekey)
238 except socket.error as err:
239 raise UTAHProvisioningException(str(err), retry=True)
240
241 def sshpoll(self, timeout=None,
242 checktimeout=config.checktimeout, logmethod=None):
243 """
244 Run sshcheck over and over until timeout expires.
245 """
246 if timeout is None:
247 timeout = self.boottimeout
248 if logmethod is None:
249 logmethod = self.logger.debug
250 utah.timeout.timeout(timeout, retry, self.sshcheck, checktimeout,
251 logmethod=logmethod)
252
253 def activecheck(self):
254 """
255 Start the machine if needed, and check for SSH login.
256 """
257 self.logger.debug('Checking if machine is active')
258 self.provisioncheck()
259 if not self.active:
260 self._start()
261 self.sshcheck()
0262
=== removed file 'utah/provisioning/vm/libvirtvm.py'
--- utah/provisioning/vm/libvirtvm.py 2012-12-03 14:02:18 +0000
+++ utah/provisioning/vm/libvirtvm.py 1970-01-01 00:00:00 +0000
@@ -1,777 +0,0 @@
1# Ubuntu Testing Automation Harness
2# Copyright 2012 Canonical Ltd.
3
4# This program is free software: you can redistribute it and/or modify it
5# under the terms of the GNU General Public License version 3, as published
6# by the Free Software Foundation.
7
8# This program is distributed in the hope that it will be useful, but
9# WITHOUT ANY WARRANTY; without even the implied warranties of
10# MERCHANTABILITY, SATISFACTORY QUALITY, or FITNESS FOR A PARTICULAR
11# PURPOSE. See the GNU General Public License for more details.
12
13# You should have received a copy of the GNU General Public License along
14# with this program. If not, see <http://www.gnu.org/licenses/>.
15
16"""
17Provide classes to provision libvirt-based virtual machines.
18"""
19
20import os
21import random
22import shutil
23import string
24import subprocess
25import tempfile
26
27from xml.etree import ElementTree
28
29import apt.cache
30import libvirt
31
32from utah.provisioning.provisioning import (
33 SSHMixin,
34 CustomInstallMixin,
35)
36from utah.provisioning.vm.vm import VM
37from utah.provisioning.vm.exceptions import UTAHVMProvisioningException
38from utah import config
39from utah.process import ProcessChecker
40from utah.timeout import UTAHTimeout
41
42
43class LibvirtVM(VM):
44 """
45 Provide a class to utilize VMs using libvirt.
46
47 Capable of utilizing existing VMs.
48 Creation currently handled by sublcasses.
49 """
50 def __init__(self, *args, **kw):
51 super(LibvirtVM, self).__init__(*args, **kw)
52 libvirt.registerErrorHandler(self.libvirterrorhandler, None)
53 self.lv = libvirt.open(config.qemupath)
54 if self.lv is None:
55 raise UTAHVMProvisioningException('Cannot connect to libvirt')
56 self.logger.debug('LibvirtVM init finished')
57
58 def _load(self):
59 """
60 Load an existing VM.
61 """
62 self.logger.info('Loading VM')
63 self.vm = self.lv.lookupByName(self.name)
64 self.logger.info('VM loaded')
65 return True
66
67 def _provision(self):
68 """
69 Make an existing VM available using libvirt to look up the VM by name.
70 """
71 self.logger.info('Provisioning VM')
72 if self.new:
73 self.logger.debug('New VM requested')
74 try:
75 self._load()
76 self.logger.error('VM already exists')
77 raise UTAHVMProvisioningException('Request new VM, but '
78 + self.name
79 + ' already exists')
80 except libvirt.libvirtError as err:
81 if err.get_error_code() == 42:
82 self._create()
83 else:
84 raise err
85
86 try:
87 self._load()
88 except libvirt.libvirtError as err:
89 if err.get_error_code() == 42:
90 self.logger.debug('Lookup failed')
91 try:
92 self._create()
93 self._load()
94 except UTAHVMProvisioningException as error:
95 self.logger.error('VM lookup failed')
96 raise UTAHVMProvisioningException('Cannot find VM named '
97 + self.name +
98 ' and ' + str(error))
99 else:
100 raise err
101 self.provisioned = True
102 self.logger.info('VM provisioned')
103
104 def activecheck(self):
105 """
106 Verify the machine is provisioned, then start it if it is not started.
107 """
108 self.logger.debug('Checking if VM is active')
109 self.provisioncheck()
110 if self.vm is not None:
111 if self.vm.isActive() == 0:
112 self._start()
113 else:
114 self.active = True
115 else:
116 raise UTAHVMProvisioningException('Failed to provision VM')
117
118 def _start(self):
119 """
120 Start the VM.
121 """
122 self.logger.info('Starting VM')
123 if self.vm is not None:
124 if self.vm.isActive() == 0:
125 self.vm.create()
126 else:
127 raise UTAHVMProvisioningException('Failed to provision VM')
128 self.active = True
129
130 def stop(self, force=False):
131 """
132 Stop the machine.
133 Setting force to true will do a hard shutdown instead of a graceful
134 one.
135 """
136 self.logger.info('Stopping VM')
137 if self.vm is not None:
138 if self.vm.isActive() == 0:
139 self.logger.info('VM is already stopped')
140 else:
141 if force:
142 self.logger.info('Forced shutdown requested')
143 self.vm.destroy()
144 else:
145 self.vm.shutdown()
146 else:
147 self.logger.info('VM not yet created')
148 self.active = False
149
150 def libvirterrorhandler(self, _context, err):
151 """
152 Log libvirt errors instead of sending them directly to the console.
153 """
154 errorcode = err.get_error_code()
155 if errorcode in [9, 42]:
156 # We see these as part of normal operations,
157 # so we send them to debug
158 # 9 is trying to create a VM that already exists
159 # 42 is trying to load a VM that doesn't exist
160 logmethod = self.logger.debug
161 else:
162 logmethod = self.logger.error
163 logmethod('libvirt error: ' + err['message'])
164 logmethod('libvirt error number is: ' + str(errorcode))
165
166
167class VMToolsVM(SSHMixin, LibvirtVM):
168 """
169 Provide a class to provision a VM using the ubuntu-qa-tools vm-tools.
170 """
171 def __init__(self, machineid=None, prefix='utah', *args, **kw):
172 if not apt.cache.Cache()['vm-tools'].is_installed:
173 raise UTAHVMProvisioningException(
174 'vm-tools is not installed. '
175 'Try: sudo apt-get install vm-tools')
176 super(VMToolsVM, self).__init__(*args, machineid=machineid, name=None,
177 **kw)
178 self.logger.debug('VMToolsVM init finished')
179
180 def _start(self):
181 """
182 Start the VM using vm-start, which will wait until SSH is up.
183 """
184 self.logger.info('Starting the vm using vm-start')
185 args = ['vm-start', '-v', '-w', self.name]
186 self.active = (self._runargs(args) == 0)
187 return self.active
188
189 def activecheck(self):
190 """
191 Verify the machine is provisioned, then start it if it is not started.
192 Use vm-wait to make sure it's up.
193 """
194 self.provisioncheck()
195 self.logger.debug('Checking if VMToolsVM is active')
196 self._start()
197 self.logger.info('Using vm-wait to ensure VM is active')
198 if (self._runargs(['vm-wait', self.name, '300']) != 0):
199 self.active = False
200 raise UTAHVMProvisioningException('Timed out waiting for VM '
201 'to be reachable')
202 else:
203 self.active = True
204
205 def _getcreationargs(self):
206 """
207 Return the vm-new syntax used to create the VM.
208 Can be used for debugging or instructional purposes.
209 """
210 args = ['vm-new', '-f', '-r', '-v', '-b', config.bridge,
211 '-t', self.installtype, self.series, self.arch, self.prefix]
212 self.logger.debug('VM Creation args: ' + str(args))
213 return args
214
215 def _create(self):
216 """
217 Run the command generated by getcreationargs
218 and either return a good status or raise an exception.
219 """
220 self.logger.info('Creating vm using vm-new')
221 self.logger.info('This may take up to two hours, '
222 'excluding download times, and may go over an hour '
223 'without output during virt-install')
224 args = self._getcreationargs()
225 percent = 0
226 p = subprocess.Popen(args,
227 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
228 while p.poll() is None:
229 line = p.stdout.readline().strip()
230 self.logger.debug(line)
231 if 'Fetching release iso' in line:
232 self.logger.info('Downloading ISO')
233 if len(line.split()) >= 3 and '%' in line.split()[-3]:
234 dlpercent = int(line.split()[-3].strip('%'))
235 if dlpercent >= percent:
236 self.logger.info('ISO ' + str(dlpercent) + '% downloaded, '
237 + line.split(' ')[-1] + ' remaining')
238 percent += self.dlpercentincrement
239 if ' saved ' in line:
240 self.logger.info('ISO download complete')
241 if 'Creating preseeded iso' in line:
242 self.logger.info('Preseeding ISO')
243 if 'virt-install' in line:
244 self.logger.info('Installing system on VM '
245 '(may take over an hour)')
246 self.logger.info('You can watch the progress with:')
247 self.logger.info("\tvm-view " + self.name)
248 self.logger.info('Take care not to interrupt the install')
249 if 'Verifying lsb_release' in line:
250 self.logger.info('Waiting for post-install '
251 'and verifying system '
252 '(may take over a half hour)')
253 if "No domains available for virt type 'hvm'" in line:
254 self.logger.error('No hardware virtual machine '
255 'support available')
256 self.logger.info('Please ensure the following:')
257 self.logger.info("\tYour processor supports hardware "
258 "virtualization extensions")
259 self.logger.info("\tHardware virtualization "
260 "is not disabled in the BIOS")
261 self.logger.info("\tkvm is installed")
262 self.logger.info("\tThe kvm and processor-specific "
263 "kvm kernel modules are installed and loaded")
264 self.logger.error('Software virtual machine support '
265 'is implemented in the CustomVM class')
266 raise UTAHVMProvisioningException(
267 'No hardware virtual machine support available')
268 if ('Could not find' in line
269 and ".uqt-vm-tools.conf' configuration file!" in line):
270 self.logger.error('No .uqt-vm-tools.conf configuration file '
271 'found in home directory')
272 self.logger.info('If you are running as the utah user, '
273 'you can use the packaged config file:')
274 self.logger.info("\tln -s /etc/utah/uqt-vm-tools.conf "
275 "~utah/.uqt-vm-tools.conf")
276 self.logger.info('As a different user, '
277 'run vm-new with no arguments, '
278 'and answer y when prompted '
279 'to create an initial config file')
280 self.logger.info('We recommend adding python-yaml '
281 'to the vm_extra_packages line '
282 'of this file, i.e.:')
283 self.logger.info("\tsed "
284 "'s/^vm_extra_packages=\""
285 "/vm_extra_packages=\"python-yaml /' "
286 "-i ~/.uqt_vm_tools.conf")
287 raise UTAHVMProvisioningException(
288 'No vm-tools config file available; '
289 'more info available in '
290 + self.filehandler.baseFilename)
291 if p.returncode == 0:
292 return True
293 else:
294 raise UTAHVMProvisioningException(
295 'Failed to create VM: vm-new exit status: '
296 + str(p.returncode))
297
298 def destroy(self):
299 """
300 Use vm-remove to destroy the vm.
301 """
302 self.logger.info('Destroying vm using vm-remove')
303 self.provisioned = False
304 args = ['vm-remove', '-f', self.name]
305 return (self._runargs(args) == 0)
306
307 def _getcommandargs(self, command, quiet=None, root=False, timeout=300):
308 """
309 Get the arguments to send to vm-cmd.
310 Used by multiple other functions.
311 """
312 if quiet is None:
313 quiet = not self.debug
314 args = ['vm-cmd', '-f']
315 if timeout is not None:
316 args.extend(['-s', '-t', str(timeout)])
317 if quiet:
318 args.append('-q')
319 if root:
320 args.append('-r')
321 args.append(self.name)
322 if isinstance(command, basestring):
323 args.append(command)
324 else:
325 args.extend(command)
326 self.logger.debug('vm-cmd command args: ' + str(args))
327 return args
328
329 def run(self, command, quiet=None, root=False, timeout=300):
330 """
331 Run a command using vm-cmd.
332 Timeout defaults to 300 seconds, but can be set to None to not send a
333 timeout argument.
334 """
335 if quiet is None:
336 quiet = not self.debug
337 self.activecheck()
338 args = self._getcommandargs(command=command, quiet=quiet,
339 root=root, timeout=timeout)
340 self.logger.info('Running command on VM: ' + ' '.join(args))
341 p = subprocess.Popen(args,
342 stdout=subprocess.PIPE, stderr=subprocess.PIPE)
343 while p.poll() is None:
344 pass
345 return p.returncode, p.communicate()[0], p.communicate()[1]
346
347 def uploadfiles(self, files, target=os.path.normpath('/tmp/')):
348 """
349 Copy a file or list of files to a target directory on the machine.
350 """
351 if isinstance(files, basestring):
352 files = [files]
353 if not target.endswith('/'):
354 target += '/'
355 self.activecheck()
356 success = True
357 for myfile in files:
358 self.logger.info('Uploading ' + myfile
359 + ' from the host to ' + target + ' on the VM')
360 returncode = self._runargs(['vm-scp', '-f', '-p',
361 self.name, myfile, target])
362 if returncode != 0:
363 success = False
364 return success
365
366
367class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM):
368 """
369 Install a VM from an image using libvirt direct kernel booting.
370 """
371 def __init__(self, diskbus=None, disksizes=None, emulator=None,
372 machineid=None, macs=None, name=None, prefix='utah', *args,
373 **kw):
374 # Make sure that no other virtualization solutions are running
375 # TODO: see if this is needed for qemu or just kvm
376 process_checker = ProcessChecker()
377 for cmdline, app in [('/usr/lib/virtualbox/VirtualBox', 'VirtualBox'),
378 ('/usr/lib/vmware/bin', 'VMware')]:
379 if process_checker.check_cmdline(cmdline):
380 message = process_checker.get_error_message(app)
381 raise UTAHVMProvisioningException(message)
382
383 if diskbus is None:
384 self.diskbus = config.diskbus
385 else:
386 self.diskbus = diskbus
387 if disksizes is None:
388 disksizes = config.disksizes
389 if disksizes is None:
390 self.disksizes = [8]
391 else:
392 self.disksizes = disksizes
393 self.disks = []
394 if name is None:
395 autoname = True
396 name = '-'.join([str(prefix), str(machineid)])
397 else:
398 autoname = False
399 super(CustomVM, self).__init__(machineid=machineid, name=name, *args,
400 **kw)
401 # TODO: do a better job of separating installation
402 # into _create rather than __init__
403 if self.image is None:
404 raise UTAHVMProvisioningException('Image file required '
405 'for custom VM installation')
406 self._custominit()
407 if autoname:
408 self._namesetup()
409 self._loggerunsetup()
410 self._loggersetup()
411 self._dirsetup()
412 if emulator is None:
413 emulator = config.emulator
414 if emulator is None:
415 if self._supportsdomaintype('kvm'):
416 self.logger.info('Setting type to kvm '
417 'since it is present in libvirt capabilities')
418 self.domaintype = 'kvm'
419 elif self._supportsdomaintype('qemu'):
420 self.logger.info('Setting type to qemu '
421 'since it is present in libvirt capabilities')
422 self.domaintype = 'qemu'
423 else:
424 raise UTAHVMProvisioningException(
425 'kvm and qemu not supported in libvirt capabilities; '
426 'please make sure qemu and/or kvm are installed '
427 'and libvirt is configured correctly')
428 else:
429 self.domaintype = emulator
430 if self.domaintype == 'qemu':
431 self.logger.debug('Raising boot timeout for qemu domain')
432 self.boottimeout *= 4
433 if macs is None:
434 macs = []
435 self.macs = macs
436 self.dircheck()
437 self.logger.debug('CustomVM init finished')
438
439 def _createdisks(self, disksizes=None):
440 """
441 Create disk files if needed and build a list of them.
442 """
443 self.logger.info('Creating disks')
444 if disksizes is None:
445 disksizes = self.disksizes
446 for index, size in enumerate(disksizes):
447 disksize = '{}G'.format(size)
448 basename = 'disk{}.qcow2'.format(index)
449 diskfile = os.path.join(self.directory, basename)
450 if not os.path.isfile(diskfile):
451 cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize]
452 self.logger.debug('Creating ' + disksize + ' disk using:')
453 self.logger.debug(' '.join(cmd))
454 if self._runargs(cmd) != 0:
455 raise UTAHVMProvisioningException(
456 'Could not create disk image at ' + diskfile)
457 disk = {'bus': self.diskbus,
458 'file': diskfile,
459 'size': disksize,
460 'type': 'qcow2'}
461 self.disks.append(disk)
462 self.logger.debug('Adding disk to list')
463
464 def _supportsdomaintype(self, domaintype):
465 """
466 Check emulator support in libvirt capabilities.
467 """
468 capabilities = ElementTree.fromstring(self.lv.getCapabilities())
469 for guest in capabilities.iterfind('guest'):
470 for arch in guest.iterfind('arch'):
471 for domain in arch.iterfind('domain'):
472 if domaintype in domain.get('type'):
473 return True
474 return False
475
476 def _installxml(self, cmdline=None, image=None, initrd=None,
477 kernel=None, tmpdir=None, xml=None):
478 """
479 Return the XML tree to be passed to libvirt for VM installation.
480 """
481 self.logger.info('Creating installation XML')
482 if cmdline is None:
483 cmdline = self.cmdline
484 if image is None:
485 image = self.image.image
486 if initrd is None:
487 initrd = self.initrd
488 if kernel is None:
489 kernel = self.kernel
490 if xml is None:
491 xml = self.xml
492 if tmpdir is None:
493 tmpdir = self.tmpdir
494 xmlt = ElementTree.ElementTree(file=xml)
495 if self.rewrite in ['all', 'minimal']:
496 self.logger.debug('Setting VM to shutdown on reboot')
497 xmlt.find('on_reboot').text = 'destroy'
498 if self.rewrite == 'all':
499 self._installxml_rewrite_all(cmdline, image, initrd, kernel,
500 xmlt)
501 else:
502 self.logger.info('Not rewriting XML because rewrite is ' +
503 self.rewrite)
504 if self.debug:
505 xmlt.write(os.path.join(tmpdir, 'install.xml'))
506 self.logger.info('Installation XML ready')
507 return xmlt
508
509 def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt,
510 kernel_txt, xmlt):
511 """
512 Rewrite the whole configuration file for the VM
513 """
514 self.logger.debug('Rewriting basic info')
515 xmlt.find('name').text = self.name
516 xmlt.find('uuid').text = self.uuid
517 self.logger.debug('Setting type to qemu in case no '
518 'hardware virtualization present')
519 xmlt.getroot().set('type', self.domaintype)
520 ose = xmlt.find('os')
521 if self.arch == ('i386'):
522 ose.find('type').set('arch', 'i686')
523 elif self.arch == ('amd64'):
524 ose.find('type').set('arch', 'x86_64')
525 else:
526 ose.find('type').set('arch', self.arch)
527 self.logger.debug('Setting up boot info')
528 for kernele in list(ose.iterfind('kernel')):
529 ose.remove(kernele)
530 kernele = ElementTree.Element('kernel')
531 kernele.text = kernel_txt
532 ose.append(kernele)
533 for initrde in list(ose.iterfind('initrd')):
534 ose.remove(initrde)
535 initrde = ElementTree.Element('initrd')
536 initrde.text = initrd_txt
537 ose.append(initrde)
538 for cmdlinee in list(ose.iterfind('cmdline')):
539 ose.remove(cmdlinee)
540 cmdlinee = ElementTree.Element('cmdline')
541 cmdlinee.text = cmdline_txt
542 ose.append(cmdlinee)
543 self.logger.debug('Setting up devices')
544 devices = xmlt.find('devices')
545 self.logger.debug('Setting up disks')
546 for disk in list(devices.iterfind('disk')):
547 if disk.get('device') == 'disk':
548 devices.remove(disk)
549 self.logger.debug('Removed existing disk')
550 #TODO: Add a cdrom if none exists
551 if disk.get('device') == 'cdrom':
552 if disk.find('source') is not None:
553 disk.find('source').set('file', image)
554 self.logger.debug('Rewrote existing CD-ROM')
555 else:
556 source = ElementTree.Element('source')
557 source.set('file', image)
558 disk.append(source)
559 self.logger.debug('Added source to existing '
560 'CD-ROM')
561 for disk in self.disks:
562 diske = ElementTree.Element('disk')
563 diske.set('type', 'file')
564 diske.set('device', 'disk')
565 driver = ElementTree.Element('driver')
566 driver.set('name', 'qemu')
567 driver.set('type', disk['type'])
568 diske.append(driver)
569 source = ElementTree.Element('source')
570 source.set('file', disk['file'])
571 diske.append(source)
572 target = ElementTree.Element('target')
573 dev = "vd%s" % (string.ascii_lowercase[self.disks.index(disk)])
574 target.set('dev', dev)
575 target.set('bus', disk['bus'])
576 diske.append(target)
577 devices.append(diske)
578 self.logger.debug('Added ' + str(disk['size']) + ' disk')
579 macs = list(self.macs)
580 for interface in devices.iterfind('interface'):
581 if interface.get('type') in ['network', 'bridge']:
582 if len(macs) > 0:
583 mac = macs.pop(0)
584 interface.find('mac').set('address', mac)
585 self.logger.debug('Rewrote interface '
586 'to use specified mac address ' + mac)
587 else:
588 mac = random_mac_address()
589 interface.find('mac').set('address', mac)
590 self.macs.append(mac)
591 self.logger.debug('Rewrote interface '
592 'to use random mac address ' + mac)
593 if interface.get('type') == 'bridge':
594 interface.find('source').set('bridge', config.bridge)
595 serial = ElementTree.Element('serial')
596 serial.set('type', 'file')
597 source = ElementTree.Element('source')
598 log_filename = os.path.join(config.logpath, self.name + '.syslog.log')
599 source.set('path', log_filename)
600 serial.append(source)
601 target = ElementTree.Element('target')
602 target.set('port', '0')
603 serial.append(target)
604 devices.append(serial)
605
606 def _installvm(self, lv=None, tmpdir=None, xml=None):
607 """
608 Install a VM, then undefine it in libvirt.
609 The final installation will recreate the VM using the existing disks.
610 """
611 self.logger.info('Creating VM')
612 if lv is None:
613 lv = self.lv
614 if xml is None:
615 xml = self.xml
616 if tmpdir is None:
617 tmpdir = self.tmpdir
618 vm = lv.defineXML(ElementTree.tostring(xml.getroot()))
619 os.chmod(tmpdir, 0755)
620 vm.create()
621 self.logger.info('Installing system on VM (may take over an hour)')
622 self.logger.info('You can watch the progress with virt-viewer')
623 log_filename = os.path.join(config.logpath, self.name + '.syslog.log')
624 self.logger.info('Logs will be written to ' + log_filename)
625
626 while vm.isActive() is not 0:
627 pass
628
629 vm.undefine()
630 self.logger.info('Installation complete')
631
632 def _finalxml(self, tmpdir=None, xml=None):
633 """
634 Create the XML to be used for the post-installation VM.
635 This may be a transformation of the installation XML.
636 """
637 self.logger.info('Creating final VM XML')
638 if xml is None:
639 xml = ElementTree.ElementTree(file=self.xml)
640 if tmpdir is None:
641 tmpdir = self.tmpdir
642 if self.rewrite in ['all', 'minimal']:
643 self.logger.debug('Setting VM to reboot normally on reboot')
644 xml.find('on_reboot').text = 'restart'
645 if self.rewrite == 'all':
646 self.logger.debug('Removing VM install parameters')
647 ose = xml.find('os')
648 for kernel in ose.iterfind('kernel'):
649 ose.remove(kernel)
650 for initrd in ose.iterfind('initrd'):
651 ose.remove(initrd)
652 for cmdline in ose.iterfind('cmdline'):
653 ose.remove(cmdline)
654 devices = xml.find('devices')
655 devices.remove(devices.find('serial'))
656 for disk in list(devices.iterfind('disk')):
657 if disk.get('device') == 'cdrom':
658 disk.remove(disk.find('source'))
659 else:
660 self.logger.info('Not rewriting XML because rewrite is ' +
661 self.rewrite)
662 if self.debug:
663 xml.write(os.path.join(tmpdir, 'final.xml'))
664 return xml
665
666 def _tmpimage(self, image=None, tmpdir=None):
667 """
668 Create a temporary copy of the image so libvirt will lock that copy.
669 This allows other simultaneous processes to update the cached image.
670 """
671 if image is None:
672 image = self.image.image
673 if tmpdir is None:
674 tmpdir = self.tmpdir
675 self.logger.info('Making temp copy of install image')
676 tmpimage = os.path.join(tmpdir, os.path.basename(image))
677 self.logger.debug('Copying ' + image + ' to ' + tmpimage)
678 shutil.copyfile(image, tmpimage)
679 return tmpimage
680
681 def _create(self):
682 """
683 Create the VM, install the system, and prepare it to boot.
684 This primarily calls functions from CustomInstallMixin and CustomVM.
685 """
686 self.logger.info('Creating custom virtual machine')
687
688 tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
689 self.logger.debug('Working dir: ' + tmpdir)
690 os.chdir(tmpdir)
691
692 kernel = self._preparekernel(kernel=self.kernel, tmpdir=tmpdir)
693
694 initrd = self._prepareinitrd(initrd=self.initrd, tmpdir=tmpdir)
695
696 self._unpackinitrd(initrd=initrd, tmpdir=tmpdir)
697
698 self._setuplatecommand(tmpdir=tmpdir)
699
700 self._setuppreseed(tmpdir=tmpdir)
701
702 if self.rewrite == 'all':
703 self._setuplogging(tmpdir=tmpdir)
704 else:
705 self.logger.debug('Skipping logging setup because rewrite is' +
706 self.rewrite)
707
708 initrd = self._repackinitrd(tmpdir=tmpdir)
709
710 self._createdisks()
711
712 image = self._tmpimage(image=self.image.image, tmpdir=tmpdir)
713
714 xml = self._installxml(cmdline=self.cmdline, image=image,
715 initrd=initrd, kernel=kernel,
716 tmpdir=tmpdir, xml=self.xml)
717
718 self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml)
719
720 xml = self._finalxml(tmpdir=tmpdir, xml=xml)
721
722 self.logger.info('Setting up final VM')
723 self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot()))
724
725 if self.debug:
726 self.logger.info('Leaving temp directory '
727 'because debug is enabled: ' + tmpdir)
728 else:
729 self.logger.info('Cleaning up temp directory')
730 shutil.rmtree(tmpdir)
731 return True
732
733 def _start(self):
734 """
735 Start the VM.
736 """
737 self.logger.info('Starting CustomVM')
738 if self.vm is not None:
739 if self.vm.isActive() == 0:
740 self.vm.create()
741 else:
742 raise UTAHVMProvisioningException('Failed to provision VM')
743 self.logger.info('Waiting ' + str(self.boottimeout) +
744 ' seconds to allow machine to boot')
745 try:
746 self.pingpoll(timeout=self.boottimeout)
747 except UTAHTimeout:
748 # Ignore timeout for ping, since depending on the network
749 # configuration ssh might still work despite of the ping failure.
750 self.logger.warning('Network connectivity (ping) failure')
751 self.sshpoll(timeout=self.boottimeout)
752 self.active = True
753
754 def destroy(self, *args, **kw):
755 """
756 Remove the machine from libvirt and remove all the disk files.
757 """
758 # TODO: make this use standard cleanup
759 super(CustomVM, self).destroy(*args, **kw)
760 self.stop(force=True)
761 if self.vm is not None:
762 self.vm.undefine()
763 else:
764 self.logger.info('VM not created')
765 for disk in self.disks:
766 os.unlink(disk['file'])
767 shutil.rmtree(self.directory)
768
769
770# See http://kennethreitz.com/blog/generate-a-random-mac-address-in-python/
771def random_mac_address():
772 """Returns a completely random Mac Address"""
773 mac = [0x52, 0x54, 0x00,
774 random.randint(0x00, 0xff),
775 random.randint(0x00, 0xff),
776 random.randint(0x00, 0xff)]
777 return ':'.join(map(lambda x: "%02x" % x, mac))
7780
=== modified file 'utah/provisioning/vm/vm.py'
--- utah/provisioning/vm/vm.py 2012-12-03 14:02:18 +0000
+++ utah/provisioning/vm/vm.py 2012-12-13 00:38:21 +0000
@@ -14,11 +14,26 @@
14# with this program. If not, see <http://www.gnu.org/licenses/>.14# with this program. If not, see <http://www.gnu.org/licenses/>.
1515
16"""16"""
17Consolidate functions specific to virtual machine provisioning,17Consolidate functions for virtual machine provisioning.
18but not specific to any type of virtual machine.18Non-libvirt VMs can be supported elsewhere if support for them is needed.
19"""19"""
2020
21from utah.provisioning.provisioning import Machine21import libvirt
22import os
23import random
24import shutil
25import string
26import tempfile
27
28from xml.etree import ElementTree
29
30from utah import config
31from utah.process import ProcessChecker
32from utah.provisioning.inventory.sqlite import SQLiteInventory
33from utah.provisioning.provisioning import CustomInstallMixin, Machine
34from utah.provisioning.ssh import SSHMixin
35from utah.provisioning.vm.exceptions import UTAHVMProvisioningException
36from utah.timeout import UTAHTimeout
2237
2338
24class VM(Machine):39class VM(Machine):
@@ -29,3 +44,590 @@
29 super(VM, self).__init__(*args, **kw)44 super(VM, self).__init__(*args, **kw)
30 self.vm = None45 self.vm = None
31 self.logger.debug('VM init finished')46 self.logger.debug('VM init finished')
47
48
49class LibvirtVM(VM):
50 """
51 Provide a class to utilize VMs using libvirt.
52
53 Capable of utilizing existing VMs.
54 Creation currently handled by sublcasses.
55 """
56 def __init__(self, *args, **kw):
57 super(LibvirtVM, self).__init__(*args, **kw)
58 libvirt.registerErrorHandler(self.libvirterrorhandler, None)
59 self.lv = libvirt.open(config.qemupath)
60 if self.lv is None:
61 raise UTAHVMProvisioningException('Cannot connect to libvirt')
62 self.logger.debug('LibvirtVM init finished')
63
64 def _load(self):
65 """
66 Load an existing VM.
67 """
68 self.logger.info('Loading VM')
69 self.vm = self.lv.lookupByName(self.name)
70 self.logger.info('VM loaded')
71 return True
72
73 def _provision(self):
74 """
75 Make an existing VM available using libvirt to look up the VM by name.
76 """
77 self.logger.info('Provisioning VM')
78 if self.new:
79 self.logger.debug('New VM requested')
80 try:
81 self._load()
82 self.logger.error('VM already exists')
83 raise UTAHVMProvisioningException('Request new VM, but '
84 + self.name
85 + ' already exists')
86 except libvirt.libvirtError as err:
87 if err.get_error_code() == 42:
88 self._create()
89 else:
90 raise err
91
92 try:
93 self._load()
94 except libvirt.libvirtError as err:
95 if err.get_error_code() == 42:
96 self.logger.debug('Lookup failed')
97 try:
98 self._create()
99 self._load()
100 except UTAHVMProvisioningException as error:
101 self.logger.error('VM lookup failed')
102 raise UTAHVMProvisioningException('Cannot find VM named '
103 + self.name +
104 ' and ' + str(error))
105 else:
106 raise err
107 self.provisioned = True
108 self.logger.info('VM provisioned')
109
110 def activecheck(self):
111 """
112 Verify the machine is provisioned, then start it if it is not started.
113 """
114 self.logger.debug('Checking if VM is active')
115 self.provisioncheck()
116 if self.vm is not None:
117 if self.vm.isActive() == 0:
118 self._start()
119 else:
120 self.active = True
121 else:
122 raise UTAHVMProvisioningException('Failed to provision VM')
123
124 def _start(self):
125 """
126 Start the VM.
127 """
128 self.logger.info('Starting VM')
129 if self.vm is not None:
130 if self.vm.isActive() == 0:
131 self.vm.create()
132 else:
133 raise UTAHVMProvisioningException('Failed to provision VM')
134 self.active = True
135
136 def stop(self, force=False):
137 """
138 Stop the machine.
139 Setting force to true will do a hard shutdown instead of a graceful
140 one.
141 """
142 self.logger.info('Stopping VM')
143 if self.vm is not None:
144 if self.vm.isActive() == 0:
145 self.logger.info('VM is already stopped')
146 else:
147 if force:
148 self.logger.info('Forced shutdown requested')
149 self.vm.destroy()
150 else:
151 self.vm.shutdown()
152 else:
153 self.logger.info('VM not yet created')
154 self.active = False
155
156 def libvirterrorhandler(self, _context, err):
157 """
158 Log libvirt errors instead of sending them directly to the console.
159 """
160 errorcode = err.get_error_code()
161 if errorcode in [9, 42]:
162 # We see these as part of normal operations,
163 # so we send them to debug
164 # 9 is trying to create a VM that already exists
165 # 42 is trying to load a VM that doesn't exist
166 logmethod = self.logger.debug
167 else:
168 logmethod = self.logger.error
169 logmethod('libvirt error: ' + err['message'])
170 logmethod('libvirt error number is: ' + str(errorcode))
171
172
173class CustomVM(CustomInstallMixin, SSHMixin, LibvirtVM):
174 """
175 Install a VM from an image using libvirt direct kernel booting.
176 """
177 def __init__(self, diskbus=None, disksizes=None, emulator=None,
178 machineid=None, macs=None, name=None, prefix='utah', *args,
179 **kw):
180 # Make sure that no other virtualization solutions are running
181 # TODO: see if this is needed for qemu or just kvm
182 process_checker = ProcessChecker()
183 for cmdline, app in [('/usr/lib/virtualbox/VirtualBox', 'VirtualBox'),
184 ('/usr/lib/vmware/bin', 'VMware')]:
185 if process_checker.check_cmdline(cmdline):
186 message = process_checker.get_error_message(app)
187 raise UTAHVMProvisioningException(message)
188
189 if diskbus is None:
190 self.diskbus = config.diskbus
191 else:
192 self.diskbus = diskbus
193 if disksizes is None:
194 disksizes = config.disksizes
195 if disksizes is None:
196 self.disksizes = [8]
197 else:
198 self.disksizes = disksizes
199 self.disks = []
200 if name is None:
201 autoname = True
202 name = '-'.join([str(prefix), str(machineid)])
203 else:
204 autoname = False
205 super(CustomVM, self).__init__(machineid=machineid, name=name, *args,
206 **kw)
207 # TODO: do a better job of separating installation
208 # into _create rather than __init__
209 if self.image is None:
210 raise UTAHVMProvisioningException('Image file required '
211 'for custom VM installation')
212 self._custominit()
213 if autoname:
214 self._namesetup()
215 self._loggerunsetup()
216 self._loggersetup()
217 self._dirsetup()
218 if emulator is None:
219 emulator = config.emulator
220 if emulator is None:
221 if self._supportsdomaintype('kvm'):
222 self.logger.info('Setting type to kvm '
223 'since it is present in libvirt capabilities')
224 self.domaintype = 'kvm'
225 elif self._supportsdomaintype('qemu'):
226 self.logger.info('Setting type to qemu '
227 'since it is present in libvirt capabilities')
228 self.domaintype = 'qemu'
229 else:
230 raise UTAHVMProvisioningException(
231 'kvm and qemu not supported in libvirt capabilities; '
232 'please make sure qemu and/or kvm are installed '
233 'and libvirt is configured correctly')
234 else:
235 self.domaintype = emulator
236 if self.domaintype == 'qemu':
237 self.logger.debug('Raising boot timeout for qemu domain')
238 self.boottimeout *= 4
239 if macs is None:
240 macs = []
241 self.macs = macs
242 self.dircheck()
243 self.logger.debug('CustomVM init finished')
244
245 def _createdisks(self, disksizes=None):
246 """
247 Create disk files if needed and build a list of them.
248 """
249 self.logger.info('Creating disks')
250 if disksizes is None:
251 disksizes = self.disksizes
252 for index, size in enumerate(disksizes):
253 disksize = '{}G'.format(size)
254 basename = 'disk{}.qcow2'.format(index)
255 diskfile = os.path.join(self.directory, basename)
256 if not os.path.isfile(diskfile):
257 cmd = ['qemu-img', 'create', '-f', 'qcow2', diskfile, disksize]
258 self.logger.debug('Creating ' + disksize + ' disk using:')
259 self.logger.debug(' '.join(cmd))
260 if self._runargs(cmd) != 0:
261 raise UTAHVMProvisioningException(
262 'Could not create disk image at ' + diskfile)
263 disk = {'bus': self.diskbus,
264 'file': diskfile,
265 'size': disksize,
266 'type': 'qcow2'}
267 self.disks.append(disk)
268 self.logger.debug('Adding disk to list')
269
270 def _supportsdomaintype(self, domaintype):
271 """
272 Check emulator support in libvirt capabilities.
273 """
274 capabilities = ElementTree.fromstring(self.lv.getCapabilities())
275 for guest in capabilities.iterfind('guest'):
276 for arch in guest.iterfind('arch'):
277 for domain in arch.iterfind('domain'):
278 if domaintype in domain.get('type'):
279 return True
280 return False
281
282 def _installxml(self, cmdline=None, image=None, initrd=None,
283 kernel=None, tmpdir=None, xml=None):
284 """
285 Return the XML tree to be passed to libvirt for VM installation.
286 """
287 self.logger.info('Creating installation XML')
288 if cmdline is None:
289 cmdline = self.cmdline
290 if image is None:
291 image = self.image.image
292 if initrd is None:
293 initrd = self.initrd
294 if kernel is None:
295 kernel = self.kernel
296 if xml is None:
297 xml = self.xml
298 if tmpdir is None:
299 tmpdir = self.tmpdir
300 xmlt = ElementTree.ElementTree(file=xml)
301 if self.rewrite in ['all', 'minimal']:
302 self.logger.debug('Setting VM to shutdown on reboot')
303 xmlt.find('on_reboot').text = 'destroy'
304 if self.rewrite == 'all':
305 self._installxml_rewrite_all(cmdline, image, initrd, kernel,
306 xmlt)
307 else:
308 self.logger.info('Not rewriting XML because rewrite is ' +
309 self.rewrite)
310 if self.debug:
311 xmlt.write(os.path.join(tmpdir, 'install.xml'))
312 self.logger.info('Installation XML ready')
313 return xmlt
314
315 def _installxml_rewrite_all(self, cmdline_txt, image, initrd_txt,
316 kernel_txt, xmlt):
317 """
318 Rewrite the whole configuration file for the VM
319 """
320 self.logger.debug('Rewriting basic info')
321 xmlt.find('name').text = self.name
322 xmlt.find('uuid').text = self.uuid
323 self.logger.debug('Setting type to qemu in case no '
324 'hardware virtualization present')
325 xmlt.getroot().set('type', self.domaintype)
326 ose = xmlt.find('os')
327 if self.arch == ('i386'):
328 ose.find('type').set('arch', 'i686')
329 elif self.arch == ('amd64'):
330 ose.find('type').set('arch', 'x86_64')
331 else:
332 ose.find('type').set('arch', self.arch)
333 self.logger.debug('Setting up boot info')
334 for kernele in list(ose.iterfind('kernel')):
335 ose.remove(kernele)
336 kernele = ElementTree.Element('kernel')
337 kernele.text = kernel_txt
338 ose.append(kernele)
339 for initrde in list(ose.iterfind('initrd')):
340 ose.remove(initrde)
341 initrde = ElementTree.Element('initrd')
342 initrde.text = initrd_txt
343 ose.append(initrde)
344 for cmdlinee in list(ose.iterfind('cmdline')):
345 ose.remove(cmdlinee)
346 cmdlinee = ElementTree.Element('cmdline')
347 cmdlinee.text = cmdline_txt
348 ose.append(cmdlinee)
349 self.logger.debug('Setting up devices')
350 devices = xmlt.find('devices')
351 self.logger.debug('Setting up disks')
352 for disk in list(devices.iterfind('disk')):
353 if disk.get('device') == 'disk':
354 devices.remove(disk)
355 self.logger.debug('Removed existing disk')
356 #TODO: Add a cdrom if none exists
357 if disk.get('device') == 'cdrom':
358 if disk.find('source') is not None:
359 disk.find('source').set('file', image)
360 self.logger.debug('Rewrote existing CD-ROM')
361 else:
362 source = ElementTree.Element('source')
363 source.set('file', image)
364 disk.append(source)
365 self.logger.debug('Added source to existing '
366 'CD-ROM')
367 for disk in self.disks:
368 diske = ElementTree.Element('disk')
369 diske.set('type', 'file')
370 diske.set('device', 'disk')
371 driver = ElementTree.Element('driver')
372 driver.set('name', 'qemu')
373 driver.set('type', disk['type'])
374 diske.append(driver)
375 source = ElementTree.Element('source')
376 source.set('file', disk['file'])
377 diske.append(source)
378 target = ElementTree.Element('target')
379 dev = "vd%s" % (string.ascii_lowercase[self.disks.index(disk)])
380 target.set('dev', dev)
381 target.set('bus', disk['bus'])
382 diske.append(target)
383 devices.append(diske)
384 self.logger.debug('Added ' + str(disk['size']) + ' disk')
385 macs = list(self.macs)
386 for interface in devices.iterfind('interface'):
387 if interface.get('type') in ['network', 'bridge']:
388 if len(macs) > 0:
389 mac = macs.pop(0)
390 interface.find('mac').set('address', mac)
391 self.logger.debug('Rewrote interface '
392 'to use specified mac address ' + mac)
393 else:
394 mac = random_mac_address()
395 interface.find('mac').set('address', mac)
396 self.macs.append(mac)
397 self.logger.debug('Rewrote interface '
398 'to use random mac address ' + mac)
399 if interface.get('type') == 'bridge':
400 interface.find('source').set('bridge', config.bridge)
401 serial = ElementTree.Element('serial')
402 serial.set('type', 'file')
403 source = ElementTree.Element('source')
404 log_filename = os.path.join(config.logpath, self.name + '.syslog.log')
405 source.set('path', log_filename)
406 serial.append(source)
407 target = ElementTree.Element('target')
408 target.set('port', '0')
409 serial.append(target)
410 devices.append(serial)
411
412 def _installvm(self, lv=None, tmpdir=None, xml=None):
413 """
414 Install a VM, then undefine it in libvirt.
415 The final installation will recreate the VM using the existing disks.
416 """
417 self.logger.info('Creating VM')
418 if lv is None:
419 lv = self.lv
420 if xml is None:
421 xml = self.xml
422 if tmpdir is None:
423 tmpdir = self.tmpdir
424 vm = lv.defineXML(ElementTree.tostring(xml.getroot()))
425 os.chmod(tmpdir, 0755)
426 vm.create()
427 self.logger.info('Installing system on VM (may take over an hour)')
428 self.logger.info('You can watch the progress with virt-viewer')
429 log_filename = os.path.join(config.logpath, self.name + '.syslog.log')
430 self.logger.info('Logs will be written to ' + log_filename)
431
432 while vm.isActive() is not 0:
433 pass
434
435 vm.undefine()
436 self.logger.info('Installation complete')
437
438 def _finalxml(self, tmpdir=None, xml=None):
439 """
440 Create the XML to be used for the post-installation VM.
441 This may be a transformation of the installation XML.
442 """
443 self.logger.info('Creating final VM XML')
444 if xml is None:
445 xml = ElementTree.ElementTree(file=self.xml)
446 if tmpdir is None:
447 tmpdir = self.tmpdir
448 if self.rewrite in ['all', 'minimal']:
449 self.logger.debug('Setting VM to reboot normally on reboot')
450 xml.find('on_reboot').text = 'restart'
451 if self.rewrite == 'all':
452 self.logger.debug('Removing VM install parameters')
453 ose = xml.find('os')
454 for kernel in ose.iterfind('kernel'):
455 ose.remove(kernel)
456 for initrd in ose.iterfind('initrd'):
457 ose.remove(initrd)
458 for cmdline in ose.iterfind('cmdline'):
459 ose.remove(cmdline)
460 devices = xml.find('devices')
461 devices.remove(devices.find('serial'))
462 for disk in list(devices.iterfind('disk')):
463 if disk.get('device') == 'cdrom':
464 disk.remove(disk.find('source'))
465 else:
466 self.logger.info('Not rewriting XML because rewrite is ' +
467 self.rewrite)
468 if self.debug:
469 xml.write(os.path.join(tmpdir, 'final.xml'))
470 return xml
471
472 def _tmpimage(self, image=None, tmpdir=None):
473 """
474 Create a temporary copy of the image so libvirt will lock that copy.
475 This allows other simultaneous processes to update the cached image.
476 """
477 if image is None:
478 image = self.image.image
479 if tmpdir is None:
480 tmpdir = self.tmpdir
481 self.logger.info('Making temp copy of install image')
482 tmpimage = os.path.join(tmpdir, os.path.basename(image))
483 self.logger.debug('Copying ' + image + ' to ' + tmpimage)
484 shutil.copyfile(image, tmpimage)
485 return tmpimage
486
487 def _create(self):
488 """
489 Create the VM, install the system, and prepare it to boot.
490 This primarily calls functions from CustomInstallMixin and CustomVM.
491 """
492 self.logger.info('Creating custom virtual machine')
493
494 tmpdir = tempfile.mkdtemp(prefix='/tmp/' + self.name + '_')
495 self.logger.debug('Working dir: ' + tmpdir)
496 os.chdir(tmpdir)
497
498 kernel = self._preparekernel(kernel=self.kernel, tmpdir=tmpdir)
499
500 initrd = self._prepareinitrd(initrd=self.initrd, tmpdir=tmpdir)
501
502 self._unpackinitrd(initrd=initrd, tmpdir=tmpdir)
503
504 self._setuplatecommand(tmpdir=tmpdir)
505
506 self._setuppreseed(tmpdir=tmpdir)
507
508 if self.rewrite == 'all':
509 self._setuplogging(tmpdir=tmpdir)
510 else:
511 self.logger.debug('Skipping logging setup because rewrite is' +
512 self.rewrite)
513
514 initrd = self._repackinitrd(tmpdir=tmpdir)
515
516 self._createdisks()
517
518 image = self._tmpimage(image=self.image.image, tmpdir=tmpdir)
519
520 xml = self._installxml(cmdline=self.cmdline, image=image,
521 initrd=initrd, kernel=kernel,
522 tmpdir=tmpdir, xml=self.xml)
523
524 self._installvm(lv=self.lv, tmpdir=tmpdir, xml=xml)
525
526 xml = self._finalxml(tmpdir=tmpdir, xml=xml)
527
528 self.logger.info('Setting up final VM')
529 self.vm = self.lv.defineXML(ElementTree.tostring(xml.getroot()))
530
531 if self.debug:
532 self.logger.info('Leaving temp directory '
533 'because debug is enabled: ' + tmpdir)
534 else:
535 self.logger.info('Cleaning up temp directory')
536 shutil.rmtree(tmpdir)
537 return True
538
539 def _start(self):
540 """
541 Start the VM.
542 """
543 self.logger.info('Starting CustomVM')
544 if self.vm is not None:
545 if self.vm.isActive() == 0:
546 self.vm.create()
547 else:
548 raise UTAHVMProvisioningException('Failed to provision VM')
549 self.logger.info('Waiting ' + str(self.boottimeout) +
550 ' seconds to allow machine to boot')
551 try:
552 self.pingpoll(timeout=self.boottimeout)
553 except UTAHTimeout:
554 # Ignore timeout for ping, since depending on the network
555 # configuration ssh might still work despite of the ping failure.
556 self.logger.warning('Network connectivity (ping) failure')
557 self.sshpoll(timeout=self.boottimeout)
558 self.active = True
559
560 def destroy(self, *args, **kw):
561 """
562 Remove the machine from libvirt and remove all the disk files.
563 """
564 # TODO: make this use standard cleanup
565 super(CustomVM, self).destroy(*args, **kw)
566 self.stop(force=True)
567 if self.vm is not None:
568 self.vm.undefine()
569 else:
570 self.logger.info('VM not created')
571 for disk in self.disks:
572 os.unlink(disk['file'])
573 shutil.rmtree(self.directory)
574
575
576# See http://kennethreitz.com/blog/generate-a-random-mac-address-in-python/
577def random_mac_address():
578 """Returns a completely random Mac Address"""
579 mac = [0x52, 0x54, 0x00,
580 random.randint(0x00, 0xff),
581 random.randint(0x00, 0xff),
582 random.randint(0x00, 0xff)]
583 return ':'.join(map(lambda x: "%02x" % x, mac))
584
585
586class TinySQLiteInventory(SQLiteInventory):
587 """
588 Tiny SQLite inventory that implements request, release, and destroy.
589 No authentication or conflict checking currently exists.
590 Only suitable for VMs at present.
591 """
592 def __init__(self, *args, **kw):
593 """
594 Initialize simple database.
595 """
596 super(TinySQLiteInventory, self).__init__(*args, **kw)
597 self.connection.execute(
598 'CREATE TABLE IF NOT EXISTS '
599 'machines(machineid INTEGER PRIMARY KEY, state TEXT)')
600
601 def request(self, machinetype=CustomVM, *args, **kw):
602 """
603 Takes a Machine class as machinetype, and passes the newly generated
604 machineid along with all other arguments to that class's constructor,
605 returning the resulting object.
606 """
607 cursor = self.connection.cursor()
608 cursor.execute("INSERT INTO machines (state) VALUES ('provisioned')")
609 machineid = cursor.lastrowid
610 return machinetype(machineid=machineid, *args, **kw)
611
612 def release(self, machineid):
613 """
614 Updates the database to indicate the machine is available.
615 """
616 if self.connection.execute(
617 "UPDATE machines SET state='available' WHERE machineid=?",
618 [machineid]):
619 return True
620 else:
621 return False
622
623 def destroy(self, machineid):
624 """
625 Updates the database to indicate the machine is destroyed, but does not
626 destroy the machine.
627 """
628 if self.connection.execute(
629 "UPDATE machines SET state='destroyed' ""WHERE machineid=?",
630 [machineid]):
631 return True
632 else:
633 return False

Subscribers

People subscribed via source and target branches