Merge lp:~elopio/u1-test-utils/setup-vm into lp:u1-test-utils

Proposed by Leo Arias
Status: Merged
Approved by: Vincent Ladeuil
Approved revision: 54
Merged at revision: 51
Proposed branch: lp:~elopio/u1-test-utils/setup-vm
Merge into: lp:u1-test-utils
Diff against target: 3691 lines (+3544/-0)
28 files modified
setup_vm/ISSUES (+9/-0)
setup_vm/NOTES (+259/-0)
setup_vm/README (+241/-0)
setup_vm/TODO (+129/-0)
setup_vm/bin/setup_vm.py (+1203/-0)
setup_vm/bin/ubuntu_admin.sh (+2/-0)
setup_vm/pay/install (+40/-0)
setup_vm/pay/run (+9/-0)
setup_vm/pay/run-for-u1 (+57/-0)
setup_vm/pay/test (+5/-0)
setup_vm/selftest.py (+24/-0)
setup_vm/sso/install (+45/-0)
setup_vm/sso/run (+16/-0)
setup_vm/sso/run-for-pay (+14/-0)
setup_vm/sso/run-for-u1 (+43/-0)
setup_vm/sso/test (+11/-0)
setup_vm/tests/__init__.py (+75/-0)
setup_vm/tests/test_setup_vm.py (+1074/-0)
setup_vm/tests/test_test.py (+58/-0)
setup_vm/u1/install (+71/-0)
setup_vm/u1/run (+4/-0)
setup_vm/u1/test (+15/-0)
setup_vm/unity/install-sources (+19/-0)
setup_vm/unity/run-sso-client (+11/-0)
setup_vm/unity/run-syncdaemon (+4/-0)
setup_vm/unity/run-unity-lens-music (+6/-0)
setup_vm/unity/transient-dist-upgrade (+4/-0)
setup_vm/vms.conf (+96/-0)
To merge this branch: bzr merge lp:~elopio/u1-test-utils/setup-vm
Reviewer Review Type Date Requested Status
Vincent Ladeuil (community) Approve
Review via email: mp+158828@code.launchpad.net

Commit message

Merged the setup_vm project into u1testutils.

To post a comment you must log in.
Revision history for this message
Leo Arias (elopio) wrote :

Currently blocked by: https://bugs.launchpad.net/ubuntuone-servers/+bug/1169218
We can work it around making a new Consumer and User on Ubuntu Pay.

Revision history for this message
Leo Arias (elopio) wrote :

Ready for review!

Revision history for this message
Vincent Ladeuil (vila) wrote :

Thanks, LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== added directory 'setup_vm'
=== added file 'setup_vm/ISSUES'
--- setup_vm/ISSUES 1970-01-01 00:00:00 +0000
+++ setup_vm/ISSUES 2013-04-17 01:29:27 +0000
@@ -0,0 +1,9 @@
1* while setting up sso.local .env/db/postgresql.log ends up with:
2
3FATAL: role "postgres" does not exist
4
5=> This may be "expected" according to mfoord
6
7
8* bzr+ssh://bazaar.launchpad.net/~canonical-isd-hackers/isd-configs/sso-config/ is missing in the sso config and local.cfg should also be generated to refer to that.
9
010
=== added file 'setup_vm/NOTES'
--- setup_vm/NOTES 1970-01-01 00:00:00 +0000
+++ setup_vm/NOTES 2013-04-17 01:29:27 +0000
@@ -0,0 +1,259 @@
1Here we keep notes about the tests we will run on these vms. Once finished all
2the TODOs, this will be probably moved to moztrap, or immediately and
3magically automated.
4
5== On the host ==
61. Install the requirements:
7 $ sudo apt-get install libvirt-bin qemu virtinst virt-manager
8
92. Install the apt cache:
10 $ sudo apt-get install squid-deb-proxy
11
123. Enable the cache for launchpad's private ppas:
13 See point 9 of the README.
14
154. Configure the vms:
16
17 # This directory will be used as download cache for the Ubuntu images.
18 $ mkdir ~/installers/ubuntu
19 # This directory will store the disk images for the virtual machines.
20 $ mkdir ~/images
21 $ editor ~/vms.conf
22
23 vm.ram_size=2048
24 vm.cpus=2
25 # Tweak the cpu model according to your needs
26 vm.cpu_model=amd64
27 vm.download_cache=~/installers/ubuntu
28 vm.images_dir=~/images
29 # Tweak according to your squid-deb-proxy setup, 8000 is the default port.
30 # Use {your-ip} or any address reachable by the vms (keeping in mind that
31 # avahi's .local domain may not be up in the early stages of the install
32 vm.apt_proxy = http://{your-ip}:800
33 vm.launchpad_id=your-launchpad-id
34 # This is the ssh key of your host machine. Make sure that you have
35 uploaded it to https://launchpad.net/~/+editsshkeys
36 vm.ssh_authorized_keys = ~/.ssh/id_rsa.pub
37 # This is the ssh key for your VMs. It might be safer if it's different from
38 # your machine's key. Make sure that you have uploaded it to launchpad too.
39 vm.ssh_keys=~/.ssh/rsa-vms
40 # A default user (ubuntu) is created, here is its password
41 vm.password = you-re-on-you-own-use-a-simple-or-complex-password
42 # Go to https://launchpad.net/~/+archivesubscriptions to get the password for
43 # the Ubuntu One hackers PPA. Click the View link on the PPA row, and on the
44 # sources list entries you will see something like
45 # https://your-launchpad-id:the-password@...
46 ppa.ubuntuone_hackers.password=the-password
47
48 sso.address=sso.local
49 pay.address=pay.local
50 u1.address=u1.local
51
525. Get the branch:
53
54 $ bzr branch lp:~online-services-qa/u1-test-utils/setup_vm
55 $ cd setup_vm
56
576. Download the image for the server (do so every time you want to use a
58 fresh image):
59
60 $ ./bin/setup_vm.py precise-server-pristine --download
61
62# TODO use LXCs insteal of virtual machines for the servers.
637. Install the pristine server vm:
64
65 $ ./bin/setup_vm.py raring-pristine --install
66
678. Download the image for the desktop (do so every time you want to use a
68 fresh image)::
69
70 $ ./bin/setup_vm.py raring-desktop-pristine --download
71
728. Install the pristine desktop vm:
73
74 $ ./bin/setup_vm.py raring-pristine --install
75
76# TODO now it might be better to start all the servers on the same VM.
779. Set the SSO server (Only needed for in-dash payments tests):
78
79 $ ./bin/setup_vm.py sso --install
80 $ virsh start sso
81 $ ssh ubuntu@sso.local ~/bin/run-for-u1
82
8310. Set up the Pay server (Only needed for in-dash payments tests):
84
85 $ ./bin/setup_vm.py pay --install
86 $ virsh start pay
87 $ ssh ubuntu@pay.local ~/bin/run-for-u1
88
8911. Set up the U1 server (Only needed for in-dash payments tests):
90
91 $ ./bin/setup_vm.py u1 --install
92 $ virsh start u1
93 $ ssh ubuntu@u1.local ~/bin/run
94
9512. Set up the Filesync server (Only needed for in-dash payments tests):
96
97 TODO. See below the notes to set it up on the same u1 server.
98 TODO. Explore how to set it up in a different server.
99
10013. Set up the Music Search server:
101
102 TODO. Do we need it?
103
10413. Install the CurucĂș server (Only needed for Smart Scopes tests):
105
106 TODO.
107
10817. Install the desktop machine that will run the tests:
109
110 $ ./bin/setup_vm.py unity-prevalidation --install
111
112
113Set up the in-dash payments tests against the local servers
114===========================================================
115
116This is for the happy path.
117
1181. Sign in to the unity-prevalidation machine using virt-manager.
119
1202. Kill syncdaemon if it is running. (As this is a pristine machine, this is not necessary)
121
1223. Open seahorse and delete the Ubuntu One credentials, if present. (As this is a pristine machine, this is not necessary)
123
1243. Log in with Staging Ubuntu SSO:
125
126 # TODO: Currently we can just connect to production. This is a regression,
127 # see bug http://pad.lv/1161067
128 $ ~/bin/run-sso-client
129
130 Click the "Log-in with my existing account." link.
131 Fill the form with:
132 # TODO we need to create the user with the API helpers on u1-test-utils.
133 Email address: u1test+local-only@canonical.com
134 Password: Hola123*
135 Click the "Sign In" button.
136
137 Autopilot test:
138 http://bazaar.launchpad.net/~elopio/ubuntu-sso-client/autopilot/view/head:/ubuntu_sso/tests/acceptance/test_ubuntu_sso_client.py
139
1404. Start syncdaemon:
141
142 # TODO we currently don't have a filesync server.
143 $ ~/bin/run-syncdaemon
144
1455. Start the unity musicstore daemon:
146
147 $ ~/bin/run-unity-lens-music
148
1496. Start the control panel.
150
151 # TODO probably not neccessary.
152
1537. Add a credit card to the user.
154
155 # Use the API helpers on u1-test-utils.
156 # We still need to log in to the pay webiste first. See http://pad.lv/1144523
157
1588. Enable the automatic payments for the user.
159
160 # TODO wait for http://ur1.ca/d6z4n to land and then extend the u1-test-utils
161 # API helpers to do this.
162
163# For paypal payments, we would still need to do a lot of stuff on the website.
164# TODO do we need to test paypal payments?
165
166Run the in-dash payments tests against the local servers
167===========================================================
168
169This is the happy path.
170
1711. Super+M.
1722. Search for 'hendrix'.
1733. Wait for the search to complete.
1744. Click the first album
1755. Click the Download button.
1766. Enter the password.
1777. Click the Purchase button.
178
179All the tests are now documented in moztrap.
180
181Set up the filesync server on the same machine as ubuntuone-servers
182====================================================================
183
184 # On the host
185 $ ssh ubuntu@u1.local
186 # On the vm.
187 $ bzr branch lp:ubuntuone-filesync
188 $ cd ubuntuone-filesync
189 $ make link-sourcedeps
190 $ editor lib/u1backends/db/config.py
191 Change line 10 from:
192 db_dir = os.path.abspath(os.path.join(get_tmpdir(), 'db1'))
193 To:
194 db_dir = os.path.abspath(
195 os.path.join('/home/ubuntu/ubuntuone-servers/tmp', 'db1'))
196 $ make start
197 # Use ubuntuone-servers S4, statsd and AMQP.
198 $ ln -s ../../ubuntuone-servers/tmp/rabbitmq-ubuntuone.port tmp/rabbitmq-ubuntuone.port
199 $ ln -s ../../ubuntuone-servers/tmp/statsd.port tmp/statsd.port
200 $ ln -s ../../ubuntuone-servers/tmp/s4.port tmp/s4.port
201 $ make start-supervisor start-filesync-dummy-group
202 # TODO we can also start-filesync-oauth-group. Ask #u1-di.
203
204Set up the curucu server
205========================
206
207 # There is a dummy website that accesses the server:
208 # https://productsearch.ubuntu.com/smartscopes/v1/dashmock?geo_store=US
209 # On ~pedronis/curucu/canonistack-deploy, there's a README with the
210 # instructios to deploy curucu on a canonistack machine with Juju.
211 # TODO try to deploy it with juju on our vms.
212 # TODO we need an amazon key.
213 # On the host
214 $ ./bin/setup_vm.py precise-curucu-server --install
215 $ virsh start precise-curucu-server
216 $ ssh ubuntu@precise-curucu-server
217 # On the vm.
218 # TODO install the dependencies, what are the dependencies?
219 $ bzr branch lp:curucu
220 $ cd curucu
221 $ editor try.cfg
222
223 [amazon]
224 key = amazon key
225 secret = amazon secret
226
227 [u1ms]
228 service_url = http://musicsearch.ubuntu.com/v1/
229
230 [feedback_store]
231 interval = 4
232 # when set to empty storing feedback is disabled
233 store_directory = /tmp/feedback
234
235 $ CURUCU_CFG=try.cfg U1CONFIG=configs/development-lazr.conf PYTHONPATH=.:lib gunicorn -w 2 -k gevent_wsgi -b 0.0.0.0:8000 curucu.wsgi:app
236
237Mounting guest disk images on the host
238======================================
239
240This requires root access (what did you expect ;-p) and the current
241directory should contain the vm disk images.
242
243apt-get install qemu-nbd
244
245root@saw:/# modprobe nbd # once
246root@saw:/# mkdir /mnt/disk1
247root@saw:/# mkdir /mnt/seed
248
249root@saw:/# qemu-nbd -c /dev/nbd0 raring-pristine.qcow2
250root@saw:/# mount /dev/nbd0p1 /mnt/disk1
251root@saw:/# umount /mnt/disk1/
252root@saw:/# qemu-nbd -d /dev/nbd0
253
254root@saw:/# qemu-nbd -c /dev/nbd1 raring-test.seed
255root@saw:/# mount /dev/nbd1 /mnt/seed
256mount: warning: /mnt/seed seems to be mounted read-only.
257root@saw:/# umount /mnt/seed
258root@saw:/# qemu-nbd -d /dev/nbd1
259/dev/nbd1 disconnected
0260
=== added file 'setup_vm/README'
--- setup_vm/README 1970-01-01 00:00:00 +0000
+++ setup_vm/README 2013-04-17 01:29:27 +0000
@@ -0,0 +1,241 @@
1Getting started:
2================
3
41. Install the dependencies:
5
6 sudo apt-get install bzr python-testtools python-yaml
7 sudp apt-get install libvirt-bin qemu qemu-utils virtinst
8 sudo apt-get install qemu-kvm-spice python-spice-client-gtk
9
10 (Optional):
11 To use a gui manager to see the desktop:
12 sudo apt-get install virt-manager
13
14 To use Apt proxy to speed up multiple downloads of the same packages:
15 sudo apt-get install squid-deb-proxy
16 (See point 9 about configuring the apt cache.)
17
182. Reboot to allow kvm to activate on the running kernel.
19
203. Get the code:
21
22 bzr branch lp:~online-services-qa/u1-test-utils/setup_vm
23
244. Run the tests:
25
26 cd setup_vm
27 ./selftest.py
28
29 (The test_install_from_seed will require you to enter your password
30 because it executes a command with sudo. Your user must be a sudoer.)
31
325. Configure a virtual machine:
33
34 Write the file ~/vms.conf with something like this:
35
36 vm.ram_size=2048
37 vm.cpus=2
38 vm.cpu_model=amd64
39
40 [raring-pristine]
41 vm.name = raring-pristine
42 vm.update = True
43 vm.packages = bzr, ubuntu-desktop, avahi-daemon
44 vm.release=raring
45 vm.ssh_authorized_keys = {your SSH public key path (~ is allowed, eg ~/.ssh/id_rsa.pub)}
46
47 Create the ~/.config/setup_vm directory where cloud-init configuration
48 files will be stored for each vm. Alternatively, you can create a
49 directory (~/vms) where you want and add the following line in ~/vms.conf:
50
51 vm.vms_dir=~/vms
52
53 Optionally, you can setup scripts to be executed as root or as the
54 ubuntu user just before the vm is powered off:
55
56 vm.root_script = {path to a script on the host}
57 vm.ubuntu_script = {path to a script on the host}
58
59 These scripts *must* specify a shebang line and can be written in any
60 language that can be run from a shebang line.
61
62 You can also ask for some local scripts to be uploaded with:
63
64 vm.uploaded_scripts = sso/run, sso/run-for-pay
65
66 The config options in these scripts will be expanded before the upload.
67
68 PPAS needs a bit of care to setup, as an example, the unity experimental
69 prevalidation PPA is configured by going to
70 https://launchpad.net/~ubuntu-unity/+archive/experimental-prevalidation
71 Click the "Technical details about this PPA" link.
72 Select the distribution from the combo box.
73 Copy the apt line below:
74
75 vm.apt_sources=deb http://ppa.launchpad.net/ubuntu-unity/experimental-prevalidation/ubuntu {vm.release} main|52D62F45
76
77 The page displays Signing Key: 1024R/52D62F45. Please note that only
78 52D62F45 should be specified, and that the url and the key are separated
79 by '|' with no intervening spaces.
80
81 For a private PPA, make sure to include your launchpad id and your
82 password for that PPA in the URL. It would look something like this:
83
84 vm.apt_sources = deb https://<lp id>:<ppa password>@private-ppa.launchpad.net/a-user/ppa-name/ubuntu {vm.release} main|<ppa key>
85
866. (Optional) Create a system-wide vms.conf.
87
88 In some cases, some options are better defined in a system-wide config
89 file (/etc/libvirt/vms.conf). This file is queried if no definitions are
90 found in ~/vms.conf and can define a no-name section and vm sections.
91
927. (Optional) You can configure the location where the image will be
93 downloaded with something like this in the vms.conf file:
94
95 vm.download_cache=~/installers/ubuntu
96
978. (Optional) You can configure the location where the virtual machines will
98 be stored with something like this in the vms.conf file:
99
100 vm.images_dir=~/images
101
1029. (Optional) Set up an apt cache, so repeated virtual machine installs will
103 be faster, downloading the packages from the cache instead of an Ubuntu
104 archive mirror:
105
106 Add this to the vms.conf file:
107
108 vm.apt_proxy = http://{your-squid-deb-proxy-ip}:8000
109
110 If you need to install packages from non official Ubuntu repositories, you
111 will need to configure the proxy. For example, common tasks would require
112 to access Launchpad public and private PPAs. For that, write the file
113 /etc/squid-deb-proxy/mirror-dstdomain.acl.d/20-local-vms with:
114
115 # /etc/squid-deb-proxy/mirror-dstdomain.acl.d/20-local-vms
116
117 # network destinations that are allowed by this cache targeted at
118 # locally installed vms
119
120 # launchpad personal package archives
121 ppa.launchpad.net
122 # launchpad private personal package archives
123 private-ppa.launchpad.net
124
125 After that, restart the proxy:
126
127 sudo restart squid-deb-proxy
128
129 Each time you modify some file under /etc/squid-deb-proxy, don't forget to
130 restart the service.
131
13210. Download the image:
133
134 ./bin/setup_vm.py --download raring-pristine
135
136 (This command will require you to enter your password because the
137 directory where the image will be downloaded might be under control of
138 the root user. Your user must be a sudoer. A pending task is to ask for
139 the password just when needed.)
140
14111. Install the virtual machine:
142
143 ./bin/setup_vm.py --install raring-pristine
144
145 (This command will require you to enter your password because some of the
146 operations it executes require root access. Your user must be a sudoer.)
147
14812. You can ssh into the virtual machine:
149
150 virsh start raring-pristine
151 ssh ubuntu@raring-pristine.local
152
153 No password is needed because your SSH public key is authorized.
154
15513. You can run the virtual machine from virt-manager to get a graphical user
156 interface:
157
158 Open virt-manager.
159 Right-click on the machine and select Run.
160 You will be presented with the display manager greeter.
161 Log in with the user ubuntu, password ubuntu.
162
163 (You may need to do the following if virt-manager says it can't connect,
164 sudo usermod -a -G libvirtd $USER (replace $USER with your username)
165 and reboot the system)
166
16714. (Optional) You can set up a "throw away" virtual machine on top of
168 another. We call it "throw away" because all modifications happening there
169 won't affect the disk image of the backing on virtual machine.
170
171 In the vms.conf described above add:
172
173 [raring-test]
174 vm.name = raring-test
175 vm.update = False
176 vm.release = raring
177 vm.ssh_authorized_keys = {your SSH public key}
178 # The name of the disk image used as a base
179 vm.backing = raring-pristine.qcow2
180
181 Create the new vm with:
182
183 ./bin/setup_vm.py --install raring-test
184
185 The vm creation and boot should be faster.
186
18715. (Very optional) A few commands for virsh that may be of use:
188 virsh list (shows what is running)
189 virsh start x (start a vm with the name x)
190 virsh destroy x (force shutdowns a vm with the name x)
191 virsh undefine x [--remove-all-storage] (Deletes a vm with the name x)
192
19316. (Optional) Raise sudo timeout.
194
195 If you run into vm installs taking too long and waiting for your
196 password to fisnish, you can change the default value (15 minutes) by
197 adding a file in /etc/sudoers.d containing:
198
199 Defaults:<your login here> timestamp_timeout=60
200
201 This will setup the timeout to 60 minutes.
202
20317. (Optional) Setup launchpad access for the guests
204
205 If you need to access launchpad private branches from the guests, you'll
206 need to setup ssh launchpad access (if you only need access to public
207 branches, http is good enough and you don't even need to 'bzr
208 launchpad-login'):
209
210 - You need to create an ssh key dedicated to the guests, it has to be
211 passwordless and the public part uploaded to your launchpad profile.
212 You can generate a new key pair (replacing <user> with your launchpad
213 id) with:
214
215 $ (cd ~/.ssh ; ssh-keygen -f <user>@setup_vm -N '' -C '<user>@setup_vm')
216
217 This will create two files: '<user>@setup_vm' and
218 '<user>@setup_vm.pub' in your ~/.ssh directory.
219
220 Upload the later at https://launchpad.net/~/+editsshkeys
221
222 The keys are created in your .ssh directory so you can test that they
223 work against launchpad without involving a vm.
224
225 Note that if you create vms from different hosts, you'll need to either
226 copy the same keys on all the hosts or create a pair on each of them
227 (or any combination as long as the public keys are uploaded to
228 launchpad ;).
229
230 - You need to set vm.launchpad_id to <user>. This will trigger running
231 'bzr launchpad-login <user>' in the guest and copy
232 ~/.ssh/<user>@setup_vm (the private key) from your host to the guest.
233
234 - The bazaar.launchpad.net host ssh key needs to be known or you'll get
235 prompted to add it (which is not nice for scripts). This can be fixed
236 by issuing the following command from the {vm.ubuntu_script}:
237
238 $ ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
239
240 This will probably be automated at some point in the future.
241
0242
=== added file 'setup_vm/TODO'
--- setup_vm/TODO 1970-01-01 00:00:00 +0000
+++ setup_vm/TODO 2013-04-17 01:29:27 +0000
@@ -0,0 +1,129 @@
1* running setup_vm --install I-dont-exist raises an obscure error. Checking
2 that the config section exist for the vm would allow reporting a better
3 error.
4
5* Too many tests become too hard to write because their execution requires a
6 real vm. This can be addressed in much the same way than
7 requires_known_reference_image(), i.e. setup a real vm once (outside of
8 selftest execution for now) and then use throw-away vms. But even that
9 may be too costly and may need to wait for lxc/chroot support.
10
11* running --ssh-keygen twice gives an awful error message.
12
13* Find whether or not we should really support chroots or if lxcs are good
14 enough (roughly: if they can be set up as fast as chroots but provide
15 more features, just optimize the backing-on scenario).
16
17* Investigate btrfs support to use snapshots for nested backing.
18 This may not be appropriate with kvms but will surely shine for
19 lxc/chroot.
20
21* copying a file (with expanded options or not) from the host to the guest
22 is hard (even internally). There should be a way to more simply describe
23 a list of file/directories to install (with user:group and chmod bits).
24
25* Alternatively, we can allow the guest to access the host via ssh (after
26 all, we're installing a private key in the guest so we trust it enough
27 for that already).
28
29* As a first step, we can define vm.scripts as a list of relative paths on
30 the host, that will be option expanded into vm.config_dir and uploaded
31 from there into ~ubuntu/bin.
32
33* Provide the guest with config file containing the values used to build
34 this vm. From there, the guest itself would be able to expanded options
35 in files acquired from the host (including files modified after the vm
36 has been built/started which will help during dev/debug).
37
38* vm.ubuntu_script is kind of an implementation leak from cloud-init, that's
39 the default user there and comes with some nice properties but strictly
40 speaking setup_vm cares about having *a* user, no matter how it is named
41 so the option could be named vm.user_script. In any case, the features we
42 rely on from using ubuntu should be tested if only to document them.
43
44* we need a way to run scripts on the host while expanding the config option
45 for a given vm (see sso/test that needs at least the sso.url).
46
47* launchpad interaction requires the launchpad host key.
48
49 => ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts does the
50 trick but it would be nice to automate it.
51
52* document the /etc/avahi/ fix required to use avahi with vms
53
54* lag times between significant hosts should be collected (not specific to
55 setup_vm but related to the use of the vms).
56
57* Add a way to display a vm configuration Ă  la 'bzr config'
58
59* from the addresses below, find a way to test if some fixed subspace can be
60 safely used (de:ad:be:ef or something...) or just steal some unused MAC
61 prefix (vbox's one ? or vmware's one ? or... the sky is the limit ;)
62
63
64$ sudo grep -n 'mac address' /etc/libvirt/qemu/*.xml
65/etc/libvirt/qemu/essex-precise.xml:45: <mac address='52:54:00:26:3c:20'/>
66/etc/libvirt/qemu/freebsd8.xml:39: <mac address='08:00:27:5f:9f:06'/>
67/etc/libvirt/qemu/gentoo.xml:39: <mac address='08:00:27:da:65:cd'/>
68/etc/libvirt/qemu/indicator-sync.xml:45: <mac address='52:54:00:43:15:9c'/>
69/etc/libvirt/qemu/pkgimporter-lucid.xml:45: <mac address='52:54:00:95:4e:dc'/>
70/etc/libvirt/qemu/precise-cloud.xml:48: <mac address='52:54:00:68:aa:af'/>
71/etc/libvirt/qemu/precise-pristine.xml:48: <mac address='52:54:00:d5:52:06'/>
72/etc/libvirt/qemu/precise-pristine.xml:48: <mac address='52:54:00:70:a3:64'/>
73/etc/libvirt/qemu/precise-server-pristine.xml:51: <mac address='52:54:00:14:d5:be'/>
74/etc/libvirt/qemu/precise-test.xml:48: <mac address='52:54:00:25:8b:56'/>
75/etc/libvirt/qemu/quantal-cloud.xml:48: <mac address='52:54:00:e6:6a:df'/>
76/etc/libvirt/qemu/quantal-pristine.xml:48: <mac address='52:54:00:f5:95:0e'/>
77/etc/libvirt/qemu/quantal-pristine.xml:48: <mac address='52:54:00:b9:68:11'/>
78/etc/libvirt/qemu/quantal-test.xml:48: <mac address='52:54:00:21:3b:7b'/>
79/etc/libvirt/qemu/raring-current.xml <mac address='52:54:00:d9:ca:70'/>
80/etc/libvirt/qemu/raring-in-dash-pristine.xml:51: <mac address='52:54:00:c7:f9:ee'/>
81/etc/libvirt/qemu/raring-in-dash-test.xml:51: <mac address='52:54:00:a5:83:b9'/>
82/etc/libvirt/qemu/raring-pristine.xml:48: <mac address='52:54:00:07:09:cb'/>
83/etc/libvirt/qemu/raring-pristine.xml:48: <mac address='52:54:00:34:7f:62'/>
84/etc/libvirt/qemu/raring-scope-base.xml:42: <mac address='52:54:00:9c:0d:1c'/>
85/etc/libvirt/qemu/raring-scope-test.xml:42: <mac address='52:54:00:8a:b1:d6'/>
86/etc/libvirt/qemu/raring-test.xml:48: <mac address='52:54:00:23:b6:8d'/>
87/etc/libvirt/qemu/raring-test.xml:51: <mac address='52:54:00:e3:05:db'/>
88/etc/libvirt/qemu/sso.xml:51: <mac address='52:54:00:f1:88:84'/>
89/etc/libvirt/qemu/u1test-quantal.xml:48: <mac address='52:54:00:04:7b:45'/>
90/etc/libvirt/qemu/u1test-quantal.xml:48: <mac address='52:54:00:04:7b:45'/>
91/etc/libvirt/qemu/u1test2-quantal.xml:45: <mac address='52:54:00:df:35:6c'/>
92/etc/libvirt/qemu/u1test2-quantal.xml:45: <mac address='52:54:00:df:35:6c'/>
93/etc/libvirt/qemu/xp-32bits.xml:58: <mac address='52:54:00:86:aa:99'/>
94/etc/libvirt/qemu/xp64bits.xml:46: <mac address='52:54:00:0a:5e:7c'/>
95
96* while using fixed IP addresses is one way to address known_hosts
97 stability, another way is to rely on `ssh-keygen -R` when installing the
98 host (to remove the previous mapping between the key and the ip) and then
99 add the new key with ssh-keyscan (or something else that doesn't require
100 the guest to be up and running).
101
102* make actions verbose and obey -q (at least for tests) so we get some
103 feeback about what is executed.
104
105* add a --delete action to make sure we clean up the config_dir
106
107* rework FileMonitor, ConsoleMonitor design, the actual result smells (pass
108 in _wait_for_install_with_seed, really ?).
109
110* investigate using an upstart job like MAAS:
111
112 write_poweroff_job() {
113 cat >/etc/init/maas-poweroff.conf <<EOF
114 description "poweroff when maas task is done"
115 start on stopped cloud-final
116 console output
117 task
118 script
119 [ ! -e /tmp/block-poweroff ] || exit 0
120 poweroff
121 end script
122EOF
123 # reload required due to lack of inotify in overlayfs (LP: #882147)
124 initctl reload-configuration
125}
126
127* look at PXE, interesting read may include:
128
129 http://ubuntuforums.org/archive/index.php/t-1713845.html
0130
=== added directory 'setup_vm/bin'
=== added file 'setup_vm/bin/__init__.py'
=== added file 'setup_vm/bin/setup_vm.py'
--- setup_vm/bin/setup_vm.py 1970-01-01 00:00:00 +0000
+++ setup_vm/bin/setup_vm.py 2013-04-17 01:29:27 +0000
@@ -0,0 +1,1203 @@
1#!/usr/bin/env python
2"""
3Setup a virtual machine from a config file.
4
5Note: Most of the operations requires root access and this script uses ``sudo``
6to get them.
7
8"""
9import argparse
10import base64
11from cStringIO import StringIO
12import errno
13import os
14import subprocess
15import sys
16import tempfile
17import time
18
19
20from bzrlib import (
21 config,
22 osutils,
23 transport,
24 urlutils,
25 )
26import yaml
27
28# Work around a bug in bzrlib.config forbidding some constructs in templates.
29# Namely, spaces are invalid as an identifier and therefore should not match
30# below.
31config._option_ref_re = config.lazy_regex.lazy_compile('({[^ {},\n]+})')
32
33
34class VmMatcher(config.NameMatcher):
35
36 def match(self, section):
37 if section.id is None:
38 # The no name section contains default values
39 return True
40 return super(VmMatcher, self).match(section)
41
42 def get_sections(self):
43 matching_sections = super(VmMatcher, self).get_sections()
44 return reversed(list(matching_sections))
45
46
47class VmStore(config.LockableIniFileStore):
48 """A config store for options specific to a directory."""
49
50 def __init__(self, directory, file_name, possible_transports=None):
51 t = transport.get_transport_from_path(
52 directory, possible_transports=possible_transports)
53 super(VmStore, self).__init__(t, file_name)
54 self.id = 'vm'
55
56
57def system_config_dir():
58 return '/etc/libvirt'
59
60
61class VmStack(config.Stack):
62 """Per-directory options."""
63
64 def __init__(self, name):
65 """Make a new stack for a given vm.
66
67 The following sections are queried:
68
69 * the ``name`` section in ./vms.conf,
70 * the no-name section in ./vms.conf
71 * the ``name`` section in ~/vms.conf,
72 * the no-name section in ~/vms.conf
73 * the ``name`` section in /etc/libvirt/vms.conf,
74 * the no-name section in /etc/libvirt/vms.conf
75
76 :param name: The name of a virtual machine.
77 """
78 self.local_store = VmStore('.', 'vms.conf')
79 user_store = VmStore(os.environ['HOME'], 'vms.conf')
80 self.system_store = VmStore(system_config_dir(), 'vms.conf')
81 # FIXME: Only available in bzr-2.6b3 :-/ -- vila 2012-01-31
82 # dstore = self.get_shared_store()
83 super(VmStack, self).__init__(
84 [VmMatcher(self.local_store, name).get_sections,
85 VmMatcher(user_store, name).get_sections,
86 VmMatcher(self.system_store, name).get_sections,
87 ],
88 user_store, mutable_section_id=name)
89
90
91def path_from_unicode(path_string):
92 if not isinstance(path_string, basestring):
93 raise TypeError
94 return os.path.expanduser(path_string)
95
96
97class PathOption(config.Option):
98
99 def __init__(self, *args, **kwargs):
100 """A path option definition.
101
102 This possibly expands the user home directory.
103 """
104 super(PathOption, self).__init__(
105 *args, from_unicode=path_from_unicode, **kwargs)
106
107
108def register(option):
109 config.option_registry.register(option)
110
111
112register(config.Option(
113 'vm', default=None,
114 help='''The name space defining a virtual machine.
115
116This option is a place holder to document the options that defines a virtual
117machine and the options defining the infrastructure used to manage them all.
118
119For qemu based vms, the definition of a vm is stored in an xml file under
120'/etc/libvirt/qemu/{vm.name}.xml'. This is under the libvirt package control
121and is out of scope for setup_vm.py.
122
123There are 3 other significant files used for a given vm:
124
125- a disk image mounted at '/' from '/dev/sda1':
126 '{vm.images_dir}/{vm.name}.qcow2'
127
128- a iso image available from '/dev/sdb' labeled 'cidata':
129 {vm.images_dir}/{vm.name}.seed which contains the cloud-init data used to
130 configure/install/update the vm.
131
132- a console: {vm.images_dir}/{vm.name}.console which can be 'tail -f'ed from
133 the host.
134
135The data used to create the seed above are stored in a vm specific
136configuration directory for easier debug and reference:
137- {vm.config_dir}/user-data
138- {vm.config_dir}/meta-data
139- {vm.config_dir}/ecdsa
140- {vm.config_dir}/ecdsa.pub
141'''))
142
143# The directory where we store vm files related to their configuration with
144# cloud-init (user-data, meta-data, ssh keys).
145register(config.Option(
146 'vm.vms_dir', default='~/.config/setup_vm',
147 help='''Where vm related config files are stored.
148
149This includes user-data and meta-data for cloud-init and ssh server keys.
150
151This directory must exist.
152
153Each vm get a specific directory (automatically created) there based on its
154name.
155'''))
156# The base directories where vms are stored for kvm
157register(PathOption(
158 'vm.images_dir', default='/var/lib/libvirt/images',
159 help="Where vm disk images are stored.",
160 ))
161register(config.Option(
162 'vm.qemu_etc_dir',
163 default='/etc/libvirt/qemu',
164 help="Where libvirt (qemu) stores the vms config files."
165 ))
166
167# Isos and images download handling
168register(config.Option(
169 'vm.iso_url',
170 default='http://cdimage.ubuntu.com/daily-live/current/' ,
171 help="Where an iso can be downloaded from."
172 ))
173register(config.Option(
174 'vm.iso_name',
175 default='{vm.release}-desktop-{vm.cpu_model}.iso',
176 help="The name of the iso."
177 ))
178register(config.Option(
179 'vm.cloud_image_url',
180 default='http://cloud-images.ubuntu.com/{vm.release}/current/',
181 help="Where a cloud image can be downloaded from."
182 ))
183register(config.Option(
184 'vm.cloud_image_name',
185 default='{vm.release}-server-cloudimg-{vm.cpu_model}-disk1.img',
186 help="The name of the cloud image."
187 ))
188register(PathOption(
189 'vm.download_cache',
190 default='{vm.images_dir}',
191 help="Where downloads end up.",
192 ))
193
194# The ubiquitous vm name
195register(config.Option(
196 'vm.name', default=None, invalid='error',
197 help="The vm name, used as a prefix for related files."
198 ))
199# The second most important bit to define a vm: which ubuntu release ?
200register(config.Option(
201 'vm.release', default=None, invalid='error',
202 help="The ubuntu release name."
203 ))
204# The third important piece to define a vm: where to store files like the
205# console, the user-data and meta-data files, the ssh server keys, etc.
206register(config.Option(
207 'vm.config_dir', default='{vm.vms_dir}/{vm.name}',
208 invalid='error',
209 help='''The directory where files specific to a vm are stored.
210
211This includes the user-data and meta-data files used at install time (for
212reference and easier debug) as well as the optional ssh server keys.
213
214By default this is {vm.vms_dir}/{vm.name}. You can put it somewhere else by
215redifining it as long as it ends up being unique for the vm.
216
217{vm.vms_dir}/{vm.release}/{vm.name} may better suit your taste for example.
218'''
219 ))
220# The options defining the vm physical characteristics
221register(config.Option(
222 'vm.ram_size', default='1024',
223 help="The ram size in megabytes."
224 ))
225register(config.Option(
226 'vm.disk_size', default='8G',
227 help='''The disk image size in bytes.
228
229Optional suffixes "k" or "K" (kilobyte, 1024) "M" (megabyte, 1024k) "G"
230(gigabyte, 1024M) and T (terabyte, 1024G) are supported.
231'''))
232register(config.Option(
233 'vm.cpus', default='1',
234 help="The number of cpus."
235 ))
236register(config.Option(
237 'vm.cpu_model', default=None, invalid='error',
238 help="The number of cpus."))
239register(config.Option(
240 'vm.network', default='network=default', invalid='error',
241 help="""The --network parameter for virt-install.
242
243This can be specialized for each machine but the default should work in most
244setups. Watch for your DHCP server exhausting its address space if you create a
245lot of vms with random MAC addresses.
246"""))
247
248register(config.Option(
249 'vm.meta_data', default='''\
250instance-id: {vm.name}
251local-hostname: {vm.name}
252''',
253 invalid='error',
254 help="The meta data for cloud-init to put in the seed."
255 ))
256
257# Some bits that may added to user-data but are optional
258
259register(config.ListOption(
260 'vm.packages', default=None,
261 help='''A list of package names to be installed.
262'''))
263register(config.Option(
264 'vm.apt_proxy', default=None, invalid='error',
265 help='''A local proxy for apt to avoid repeated .deb downloads.
266
267Example:
268
269 vm.apt_proxy = http://192.168.0.42:8000
270
271'''))
272register(config.ListOption(
273 'vm.apt_sources', default=None,
274 help='''A list of apt sources entries to be added to the default ones.
275
276Cloud-init already setup /etc/apt/sources.list with appropriate entries. Only
277additional entries need to be specified here.
278'''))
279register(config.ListOption(
280 'vm.ssh_authorized_keys', default=None,
281 help='A list of paths to public ssh keys to be authorized for'
282 ' the default user.'))
283register(config.ListOption(
284 'vm.ssh_keys', default=None,
285 help='''A list of paths to server ssh keys.
286
287Both public and private keys can be provided. Accepted ssh key types are rsa,
288dsa and ecdsa. The file names should match <type>.*[.pub].
289'''))
290register(config.Option(
291 'vm.update', default=False,
292 from_unicode=config.bool_from_store,
293 help='''Whether or not the vm should be updated.
294Both apt-get update and apt-get upgrade are called if this option is set.
295'''))
296register(config.Option(
297 'vm.password', default='ubuntu', invalid='error',
298 help="The ubuntu user password."
299 ))
300register(config.Option(
301 'vm.launchpad_id',
302 help="The launchpad login used for launchpad ssh access from the guest."
303 ))
304# The scripts that are executed before powering off
305register(PathOption(
306 'vm.root_script', default=None,
307 help='''The path to a script executed as root before powering off.
308
309This script is executed before {vm.ubuntu_script}.
310'''
311 ))
312register(PathOption(
313 'vm.ubuntu_script', default=None,
314 help='''The path to a script executed as ubuntu before powering off.
315
316This script is excuted after {vm.root_script}.
317'''))
318register(config.ListOption(
319 'vm.uploaded_scripts', default=None,
320 help='''A list of scripts to be uploaded to the guest.
321
322Scripts can use config options from their vm, they will be expanded before
323upload. All scripts are uploaded into {vm.uploaded_scripts.guest_dir} under
324their base name.
325'''))
326register(config.Option(
327 'vm.uploaded_scripts.guest_dir', default='~ubuntu/bin',
328 help='''Where {vm.uploaded_scripts} are uploaded on the guest.'''
329 ))
330
331
332class SetupVmError(Exception):
333
334 msg = 'setup_vm Generic Error: %r'
335
336 def __init__(self, msg=None, **kwds):
337 if msg is not None:
338 self.msg = msg
339 for key, value in kwds.items():
340 setattr(self, key, value)
341
342 def __str__(self):
343 return self.msg.format((), **self.__dict__)
344
345 __repr__ = __str__
346
347
348class CommandError(SetupVmError):
349
350 msg = '''
351 command: {joined_cmd}
352 retcode: {retcode}
353 output: {out}
354 error: {err}
355'''
356
357 def __init__(self, cmd, retcode, out, err):
358 super(CommandError, self).__init__(joined_cmd=' '.join(cmd),
359 retcode=retcode, err=err, out=out)
360
361class ConfigValueError(SetupVmError):
362
363 msg = 'Bad value "{value}" for option "{name}".'
364
365 def __init__(self, name, value):
366 super(ConfigValueError, self).__init__(name=name, value=value)
367
368
369class ConfigPathNotFound(SetupVmError):
370
371 msg = 'No such file: {path} from {name}'
372
373 def __init__(self, path, name):
374 super(ConfigPathNotFound, self).__init__(path=path, name=name)
375
376
377def run_subprocess(args):
378 proc = subprocess.Popen(args,
379 stdout=subprocess.PIPE,
380 stderr=subprocess.PIPE,
381 stdin=subprocess.PIPE)
382 out, err = proc.communicate()
383 if proc.returncode:
384 raise CommandError(args, proc.returncode, out, err)
385 return proc.returncode, out, err
386
387
388def pipe_subprocess(args):
389 proc = subprocess.Popen(args,
390 stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
391 return proc
392
393def ssh_infos_from_path(key_path):
394 """Analyze path to find ssh key type and kind.
395
396 The basename should begin with ssh type used to create the key. and end
397 with '.pub' for a public key.
398
399 If the type is neither of rds, dsa or ecdsa, None if returned.
400
401 :param key_path: A path to an ssh key.
402
403 :return: (type, kind) 'type' is the ssh key type or None if neither of rds,
404 dsa or ecdsa. 'kind' is 'public' if the path ends with '.pub',
405 'private' otherwise.
406 """
407 base = os.path.basename(key_path)
408 for p in ('rsa', 'dsa', 'ecdsa'):
409 if base.startswith(p):
410 ssh_type = p
411 break
412 else:
413 ssh_type = None
414 if base.endswith('.pub'):
415 kind = 'public'
416 else:
417 kind = 'private'
418 return ssh_type, kind
419
420
421class ConsoleEOFError(SetupVmError):
422
423 msg = 'Encountered EOF on console, something went wrong'
424
425
426class CloudInitError(SetupVmError):
427
428 msg = 'cloud-init reported: {line} check your config'
429
430 def __init__(self, line):
431 super(CloudInitError, self).__init__(line=line)
432
433
434class ConsoleMonitor(object):
435 """Monitor a console to identify known events."""
436
437 def __init__(self, stream):
438 super(ConsoleMonitor, self).__init__()
439 self.stream = stream
440
441
442 def parse(self):
443 while True:
444 line = self.stream.readline()
445 yield line
446 if not line:
447 raise ConsoleEOFError()
448 elif line.startswith(' * Will now halt'):
449 # That's our final_message, we're done
450 return
451 elif ('Failed loading yaml blob' in line
452 or 'Unhandled non-multipart userdata starting' in line
453 or 'failed to render string to stdout:' in line
454 or 'Failed loading of cloud config' in line):
455 raise CloudInitError(line)
456
457
458class FileMonitor(ConsoleMonitor):
459
460 def __init__(self, path):
461 cmd = ['tail', '-f', path]
462 proc = pipe_subprocess(cmd)
463 super(FileMonitor, self).__init__(proc.stdout)
464 self.path = path
465 self.cmd = cmd
466 self.proc = proc
467 self.lines = []
468
469 def parse(self):
470 try:
471 for line in super(FileMonitor, self).parse():
472 self.lines.append(line)
473 yield line
474 finally:
475 self.proc.terminate()
476
477
478class CIUserData(object):
479 """Maps configuration data into cloud-init user-data.
480
481 This is a container for the data that will ultimately be encoded into a
482 cloud-config-archive user-data file.
483 """
484
485 def __init__(self, conf):
486 super(CIUserData, self).__init__()
487 self.conf = conf
488 # The objects we will populate before creating a yaml encoding as a
489 # cloud-config-archive file
490 self.cloud_config = {}
491 self.root_hook = None
492 self.ubuntu_hook = None
493 self.launchpad_hook = None
494 self.uploaded_scripts_hook = None
495
496 def set(self, ud_name, conf_name=None, value=None):
497 """Set a user-data option from it's corresponding configuration one.
498
499 :param ud_name: user-data key.
500
501 :param conf_name: configuration key, If set to None, `value` should be
502 provided.
503
504 :param value: value to use if `conf_name` is None.
505 """
506 if value is None and conf_name is not None:
507 value = self.conf.get(conf_name)
508 if value is not None:
509 self.cloud_config[ud_name] = value
510
511 def _file_content(self, path, option_name):
512 full_path = os.path.expanduser(path)
513 try:
514 with open(full_path) as f:
515 content = f.read()
516 except IOError, e:
517 if e.args[0] == errno.ENOENT:
518 raise ConfigPathNotFound(path, option_name)
519 else:
520 raise
521 return content
522
523 def set_list_of_paths(self, ud_name, conf_name):
524 """Set a user-data option from its corresponding configuration one.
525
526 The configuration option is a list of paths and the user-data option
527 will be a list of each file content.
528
529 :param ud_name: user-data key.
530
531 :param conf_name: configuration key.
532 """
533 paths = self.conf.get(conf_name)
534 if paths:
535 contents = []
536 for p in paths:
537 contents.append(self._file_content(p, conf_name))
538 self.set(ud_name, None, contents)
539
540 def _key_from_path(self, path, option_name):
541 """Infer user-data key from file name."""
542 ssh_type, kind = ssh_infos_from_path(path)
543 if ssh_type is None:
544 raise ConfigValueError(option_name, path)
545 return '%s_%s' % (ssh_type, kind)
546
547 def set_ssh_keys(self):
548 """Set the server ssh keys from a list of paths.
549
550 Provided paths should respect some coding:
551
552 - the base name should start with the ssh type of their key (rsa, dsa,
553 ecdsa),
554
555 - base names ending with '.pub' are for public keys, the others are for
556 private keys.
557 """
558 key_paths = self.conf.get('vm.ssh_keys')
559 if key_paths:
560 ssh_keys = {}
561 for p in key_paths:
562 key = self._key_from_path(p, 'vm.ssh_keys')
563 ssh_keys[key] = self._file_content(p, 'vm.ssh_keys')
564 self.set('ssh_keys', None, ssh_keys)
565
566 def set_apt_sources(self):
567 sources = self.conf.get('vm.apt_sources')
568 if sources:
569 apt_sources = []
570 for src in sources:
571 # '|' should not appear in urls nor keys so it should be safe
572 # to use it as a separator.
573 parts = src.split('|')
574 if len(parts) == 1:
575 apt_sources.append({'source': parts[0]})
576 else:
577 # For PPAs, an additional GPG key should be imported in the
578 # guest.
579 apt_sources.append({'source': parts[0], 'keyid': parts[1]})
580 self.cloud_config['apt_sources'] = apt_sources
581
582 def append_cmd(self, cmd):
583 cmds = self.cloud_config.get('runcmd', [])
584 cmds.append(cmd)
585 self.cloud_config['runcmd'] = cmds
586
587 def _hook_script_path(self, user):
588 return '~%s/setup_vm_post_install' % (user,)
589
590 def _hook_content(self, option_name, user, hook_path, mode='0700'):
591 # FIXME: Add more tests towards properly creating a tree on the guest
592 # from a tree on the host. There seems to be three kind of items worth
593 # caring about here: file content (output path, owner, chmod), file
594 # (input and output paths, owner, chmod) and directory (path, owner,
595 # chmod). There are also some subtle traps involved about when files
596 # are created across various vm generations (one vm creates a dir, a mv
597 # on top of that one doesn't, but still creates a file in this dir,
598 # without realizing it can fail in a fresh vm). -- vila 2013-03-10
599 host_path = self.conf.get(option_name)
600 if host_path is None:
601 return None
602 fcontent = self._file_content(host_path, option_name)
603 # Expand options in the provided content so we report better errors
604 expanded_content = self.conf.expand_options(fcontent)
605 # The following will generate an additional newline at the end of the
606 # script. I can't think of a case where it matters and it makes this
607 # code more robust (and/or simpler) if the script/file *doesn't* end up
608 # with a proper newline.
609 # FIXME: This may be worth fixing if we provide a more generic way to
610 # create a remote tree. -- vila 2013-03-10
611 hook_content = '''#!/bin/sh
612cat >{__guest_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
613{__fcontent}
614EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
615chown {__user}:{__user} {__guest_path}
616chmod {__mode} {__guest_path}
617'''
618 return hook_content.format(__user=user, __fcontent=expanded_content,
619 __mode=mode,
620 __guest_path=hook_path)
621
622 def set_boot_hook(self):
623 # FIXME: Needs a test ensuring we execute as root -- vila 2013-03-07
624 hook_path = self._hook_script_path('root')
625 content = self._hook_content('vm.root_script', 'root', hook_path)
626 if content is not None:
627 self.root_hook = content
628 self.append_cmd(hook_path)
629
630 def set_ubuntu_hook(self):
631 # FIXME: Needs a test ensuring we execute as ubuntu -- vila 2013-03-07
632 hook_path = self._hook_script_path('ubuntu')
633 content = self._hook_content('vm.ubuntu_script', 'ubuntu', hook_path)
634 if content is not None:
635 self.ubuntu_hook = content
636 self.append_cmd(['su', '-l', '-c', hook_path, 'ubuntu'])
637
638 def set_launchpad_access(self):
639 # FIXME: Needs a test that we can really access launchpad properly via
640 # ssh. Can only be done as a real launchpad user and as such requires
641 # cooperation :) I.e. Some configuration option set by the user will
642 # trigger the test -- vila 2013-03-14
643 lp_id = self.conf.get('vm.launchpad_id')
644 if lp_id is None:
645 return
646 # Use the specified ssh key found in ~/.ssh as the private key. The
647 # user is supposed to have uploaded the public one.
648 local_path = os.path.join('~', '.ssh', '%s@setup_vm' % (lp_id,))
649 # Force id_rsa or we'll need a .ssh/config to point to user@setup_vm
650 # for .lauchpad.net.
651 hook_path = '/home/ubuntu/.ssh/id_rsa'
652 dir_path = os.path.dirname(hook_path)
653 try:
654 fcontent = self._file_content(local_path, 'vm.launchpad_id')
655 except ConfigPathNotFound, e:
656 e.msg = ('You need to create the {p} keypair and upload {p}.pub to'
657 ' launchpad.\n'
658 'See vm.launchpad_id in README.'.format(p=local_path))
659 raise e
660 # FIXME: ~Duplicated from _hook_content. -- vila 2013-03-10
661
662 # FIXME: If this hook is executed before the ubuntu user is created we
663 # need to chown/chmod ~ubuntu which is bad. This happens when a
664 # -pristine vm is created and lead to GUI login failing because it
665 # can't create any dir/file there. The fix is to only create a script
666 # that will be executed via runcmd so it will run later and avoid the
667 # issue. -- vila 2013-03-21
668 hook_content = '''#!/bin/sh
669mkdir -p {dir_path}
670chown {user}:{user} ~ubuntu
671chmod {dir_mode} ~ubuntu
672chown {user}:{user} {dir_path}
673chmod {dir_mode} {dir_path}
674cat >{guest_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
675{fcontent}
676EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
677chown {user}:{user} {guest_path}
678chmod {file_mode} {guest_path}
679'''
680 self.launchpad_hook = self.conf.expand_options(
681 hook_content,
682 env=dict(user='ubuntu', fcontent=fcontent,
683 file_mode='0400', guest_path=hook_path,
684 dir_mode='0700', dir_path=dir_path))
685 self.append_cmd(['sudo', '-u', 'ubuntu',
686 'bzr', 'launchpad-login', lp_id])
687
688 def set_uploaded_scripts(self):
689 script_paths = self.conf.get('vm.uploaded_scripts')
690 if not script_paths:
691 return
692 hook_path = '~ubuntu/setup_vm_uploads'
693 bindir = self.conf.get('vm.uploaded_scripts.guest_dir')
694 out = StringIO()
695 out.write('''#!/bin/sh
696cat >{hook_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
697mkdir -p {bindir}
698cd {bindir}
699'''.format(**locals()))
700 for path in script_paths:
701 fcontent = self._file_content(path, 'vm.uploaded_scripts')
702 expanded = self.conf.expand_options(fcontent)
703 base = os.path.basename(path)
704 # FIXME: ~Duplicated from _hook_content. -- vila 2012-03-15
705 out.write('''cat >{base} <<'EOF{base}'
706{expanded}
707EOF{base}
708chmod 0755 {base}
709'''.format(**locals()))
710
711 out.write('''EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
712chown {user}:{user} {hook_path}
713chmod 0700 {hook_path}
714'''.format(user='ubuntu',**locals()))
715 self.uploaded_scripts_hook = out.getvalue()
716 self.append_cmd(['su', '-l', '-c', hook_path, 'ubuntu'])
717
718 def set_poweroff(self):
719 # We want to shutdown properly after installing. This is safe to set
720 # here as subsequent boots will ignore this setting, letting us use the
721 # vm ;)
722 if self.conf.get('vm.release') in ('precise', 'quantal'):
723 # Curse cloud-init lack of compatibility
724 self.append_cmd('halt')
725 else:
726 self.set('power_state', None, {'mode': 'poweroff'})
727
728 def populate(self):
729 # Common and non-configurable options
730 if self.conf.get('vm.release') == 'precise':
731 # Curse cloud-init lack of compatibility
732 msg = 'setup_vm finished installing in $UPTIME seconds.'
733 else:
734 msg = 'setup_vm finished installing in ${uptime} seconds.'
735 self.set('final_message', None, msg)
736 self.set('manage_etc_hosts', None, True)
737 self.set('chpasswd', None, dict(expire=False))
738 # Configurable options
739 self.set('password', 'vm.password')
740 self.set_list_of_paths('ssh_authorized_keys', 'vm.ssh_authorized_keys')
741 self.set_ssh_keys()
742 self.set('apt_proxy', 'vm.apt_proxy')
743 # Both user-data keys are set from the same config key, we don't
744 # provide a finer access.
745 self.set('apt_update', 'vm.update')
746 self.set('apt_upgrade', 'vm.update')
747 self.set_apt_sources()
748 self.set('packages', 'vm.packages')
749 self.set_launchpad_access()
750 # uploaded scripts
751 self.set_uploaded_scripts()
752 # The commands executed before powering off
753 self.set_boot_hook()
754 self.set_ubuntu_hook()
755 # This must be called last so previous commands (for precise and
756 # quantal) can be executed before powering off
757 self.set_poweroff()
758
759 def add_boot_hook(self, parts, hook):
760 if hook is not None:
761 parts.append({'content': '#cloud-boothook\n' + hook})
762
763 def dump(self):
764 parts = [{'content': '#cloud-config\n'
765 + yaml.safe_dump(self.cloud_config)}]
766 self.add_boot_hook(parts, self.root_hook)
767 self.add_boot_hook(parts, self.ubuntu_hook)
768 self.add_boot_hook(parts, self.launchpad_hook)
769 self.add_boot_hook(parts, self.uploaded_scripts_hook)
770 # Wrap the lot into a cloud config archive
771 return '#cloud-config-archive\n' + yaml.safe_dump(parts)
772
773
774def vm_states(source=None):
775 """A dict of states for vms indexed by name.
776
777 :param source: A list of lines as produced by virsh list --all without
778 decorations (header/footer).
779 """
780 if source is None:
781 retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
782 # Get rid of header/footer
783 source = out.splitlines()[2:-1]
784 states = {}
785 for line in source:
786 caret_or_id, name, state = line.split(None, 2)
787 states[name] = state
788 return states
789
790
791class VM(object):
792
793 def __init__(self, conf):
794 self.conf = conf
795 self._config_dir = None
796
797 def ensure_dir(self, path):
798 try:
799 os.mkdir(path)
800 except OSError, e:
801 # FIXME: Try to create the parent dir ?
802 if e.errno == errno.EEXIST:
803 pass
804 else:
805 raise
806
807 def ensure_config_dir(self):
808 if self._config_dir is None:
809 # FIXME: expanduser is not tested
810 self._config_dir = os.path.expanduser(
811 self.conf.get('vm.config_dir'))
812 self.ensure_dir(self._config_dir)
813
814 def _ssh_keygen(self, key_path):
815 ssh_type, kind = ssh_infos_from_path(key_path)
816 path = os.path.expanduser(key_path) # Just in case
817 if kind == 'private': # public will be generated at the same time
818 run_subprocess(
819 ['ssh-keygen', '-f', path, '-N', '', '-t', ssh_type,
820 '-C', self.conf.get('vm.name')])
821
822 def ssh_keygen(self):
823 self.ensure_config_dir()
824 keys = self.conf.get('vm.ssh_keys')
825 for key in keys:
826 self._ssh_keygen(key)
827
828
829class Kvm(VM):
830
831 def __init__(self, conf):
832 super(Kvm, self).__init__(conf)
833 # Seed files
834 self._meta_data_path = None
835 self._user_data_path = None
836 # Disk paths
837 self._seed_path = None
838 self._disk_image_path = None
839
840 self._console_path = None
841
842 def _download_in_cache(self, source_url, name, force=False):
843 """Download ``name`` from ``source_url`` in ``vm.download_cache``.
844
845 :param source_url: The url where the file to download is located
846
847 :param name: The name of the file to download (also used as the name
848 for the downloaded file).
849
850 :param force: Remove the file from the cache if present.
851
852 :return: False if the file is in the download cache, True if a download
853 occurred.
854 """
855 source = urlutils.join(source_url, name)
856 download_dir = self.conf.get('vm.download_cache')
857 if not os.path.exists(download_dir):
858 raise ConfigValueError('vm.download_cache', download_dir)
859 target = os.path.join(download_dir, name)
860 # FIXME: By default the download dir may be under root control, but if
861 # a user chose to use a different one under his own control, it would
862 # be nice to not require sudo usage. -- vila 2013-02-06
863 if force:
864 run_subprocess(['sudo', 'rm', '-f', target])
865 if not os.path.exists(target):
866 # FIXME: We do ask for a progress bar but it's not displayed
867 # (run_subprocess capture both stdout and stderr) ! At least while
868 # used interactively, it should. -- vila 2013-02-06
869 run_subprocess(['sudo', 'wget', '--progress=dot:mega','-O',
870 target, source])
871 return True
872 else:
873 return False
874
875 def download_iso(self, force=False):
876 """Download the iso to install the vm.
877
878 :return: False if the iso is in the download cache, True if a download
879 occurred.
880 """
881 return self._download_in_cache(self.conf.get('vm.iso_url'),
882 self.conf.get('vm.iso_name'),
883 force=force)
884
885 def download_cloud_image(self, force=False):
886 """Download the cloud image to install the vm.
887
888 :return: False if the image is in the download cache, True if a
889 download occurred.
890 """
891 return self._download_in_cache(self.conf.get('vm.cloud_image_url'),
892 self.conf.get('vm.cloud_image_name'),
893 force=force)
894
895 def create_meta_data(self):
896 self.ensure_config_dir()
897 self._meta_data_path = os.path.join(self._config_dir, 'meta-data')
898 with open(self._meta_data_path, 'w') as f:
899 f.write(self.conf.get('vm.meta_data'))
900
901 def create_user_data(self):
902 ci_user_data = CIUserData(self.conf)
903 ci_user_data.populate()
904 self.ensure_config_dir()
905 self._user_data_path = os.path.join(self._config_dir, 'user-data')
906 with open(self._user_data_path, 'w') as f:
907 f.write(ci_user_data.dump())
908
909 def create_seed(self):
910 if self._meta_data_path is None:
911 self.create_meta_data()
912 if self._user_data_path is None:
913 self.create_user_data()
914 images_dir = self.conf.get('vm.images_dir')
915 seed_path = os.path.join(
916 images_dir, self.conf.expand_options('{vm.name}.seed'))
917 run_subprocess(
918 # We create the seed in the ``vm.images_dir`` directory, so
919 # ``sudo`` is required
920 ['sudo',
921 'genisoimage', '-output', seed_path, '-volid', 'cidata',
922 '-joliet', '-rock', '-input-charset', 'default',
923 '-graft-points',
924 'user-data=%s' % (self._user_data_path,),
925 'meta-data=%s' % (self._meta_data_path,),
926 ])
927 self._seed_path = seed_path
928
929 def create_disk_image(self):
930 raise NotImplementedError(self.create_disk_image)
931
932 def _wait_for_install_with_seed(self):
933 # The console is created by virt-install which requires sudo but creates
934 # the file 0600 for libvirt-qemu. We give read access to all otherwise
935 # 'tail -f' requires sudo and can't be killed anymore.
936 run_subprocess(['sudo', 'chmod', '0644', self._console_path])
937 # While `virt-install` is running, let's connect to the console
938 console = FileMonitor(self._console_path)
939 try:
940 for line in console.parse():
941# FIXME: We need some way to activate this dynamically (conf var defaulting to
942# env var OR cmdline parameter ? -- vila 2013-02-11
943# print "read: [%s]" % (line,) # so useful for debug...
944 pass
945 except (ConsoleEOFError, CloudInitError), e:
946 # FIXME: No test covers this path -- vila 2013-02-15
947 err_lines = ['Suspicious line from cloud-init.\n',
948 '\t' + console.lines[-1],
949 'Check the configuration:\n']
950 with open(self._meta_data_path) as f:
951 err_lines.append('meta-data content:\n')
952 err_lines.extend(f.readlines())
953 with open(self._user_data_path) as f:
954 err_lines.append('user-data content:\n')
955 err_lines.extend(f.readlines())
956 raise CommandError(console.cmd, console.proc.returncode,
957 '\n'.join(console.lines),
958 ''.join(err_lines))
959
960 def install(self):
961 # Create a kvm, relying on cloud-init to customize the base image.
962 #
963 # There are two processes involvded here:
964 # - virt-install creates the vm and boots it.
965 # - progress is monitored via the console to detect cloud-final.
966 #
967 # Once cloud-init has finished, the vm can be powered off.
968
969 # FIXME: If the install doesn't finish after $time, emit a warning and
970 # terminate self.install_proc.
971 # FIXME: If we can't connect to the console, emit a warning and
972 # terminate console and self.install_proc.
973 # FIXME: If we don't receive anything on the console after $time2, emit
974 # a warning and terminate console and self.install_proc.
975 # -- vila 2013-02-07
976 if self._seed_path is None:
977 self.create_seed()
978 if self._disk_image_path is None:
979 self.create_disk_image()
980 # FIXME: Install time is probably a good time to delete the
981 # console. While it makes sense to accumulate for all runs for a given
982 # install, keeping them without any limit nor roration is likely to
983 # cause issues at some point... -- vila 2013-02-20
984 self._console_path = os.path.join(
985 self.conf.get('vm.images_dir'),
986 '%s.console' % (self.conf.get('vm.name'),))
987 run_subprocess(
988 ['sudo', 'virt-install',
989 # To ensure we're not bitten again by http://pad.lv/1157272 where
990 # virt-install wrongly detect virtualbox. -- vila 2013-03-20
991 '--connect', 'qemu:///system',
992 # Without --noautoconsole, virt-install will relay the console,
993 # that's not appropriate for our needs so we'll connect later
994 # ourselves
995 '--noautoconsole',
996 # We define the console as a file so we can monitor the install
997 # via 'tail -f'
998 '--serial', 'file,path=%s' % (self._console_path,),
999 '--network', self.conf.get('vm.network'),
1000 # Anticipate that we'll need a graphic card defined
1001 '--graphics', 'spice',
1002 '--name', self.conf.get('vm.name'),
1003 '--ram', self.conf.get('vm.ram_size'),
1004 '--vcpus', self.conf.get('vm.cpus'),
1005 '--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
1006 '--disk', 'path=%s' % (self._seed_path,),
1007 # We just boot, cloud-init will handle the installs we need
1008 '--boot', 'hd', '--hvm',
1009 ])
1010 self._wait_for_install_with_seed()
1011 # We've seen the console signaling halt, but the vm will need a bit
1012 # more time to get there so we help it a bit.
1013 if self.conf.get('vm.release') in ('precise', 'quantal'):
1014 # cloud-init doesn't implement power_state until raring and need a
1015 # gentle nudge.
1016 self.poweroff()
1017 vm_name = self.conf.get('vm.name')
1018 while True:
1019 state = vm_states()[vm_name]
1020 # We expect the vm's state to be 'in shutdown' but in some rare
1021 # occasions we may catch 'running' before getting 'in shutdown'.
1022 if state in ('in shutdown', 'running'):
1023 # Ok, querying the state takes time, this regulates the polling
1024 # enough that we don't need to sleep.
1025 continue
1026 elif state == 'shut off':
1027 # Good, we're done
1028 break
1029 # FIXME: No idea on how to test the following. Manually tested by
1030 # altering the expected state above and running 'selftest.py -v'
1031 # which errors out for test_install_with_seed and
1032 # test_install_backing. Also reproduced when 'running' wasn't
1033 # expected before 'in shutdown' -- vila 2013-02-19
1034 # Unexpected state reached, bad.
1035 raise SetupVmError('Something went wrong during {name} install\n'
1036 'The vm ended in state: {state}\n'
1037 'Check the console at {path}',
1038 name=vm_name, state=state,
1039 path=self._console_path)
1040
1041 def poweroff(self):
1042 return run_subprocess(
1043 ['sudo', 'virsh', 'destroy', self.conf.get('vm.name')])
1044
1045 def undefine(self):
1046 return run_subprocess(
1047 ['sudo', 'virsh', 'undefine', self.conf.get('vm.name'),
1048 '--remove-all-storage'])
1049
1050
1051class KvmFromCloudImage(Kvm):
1052
1053 def create_disk_image(self, src_name=None, dst_name=None):
1054 """Create a disk image from a cloud one."""
1055 if src_name is None:
1056 src_name = self.conf.get('vm.cloud_image_name')
1057 if dst_name is None:
1058 dst_name = self.conf.expand_options('{vm.name}.qcow2')
1059 cloud_image_path = os.path.join(
1060 self.conf.get('vm.download_cache'), src_name)
1061 disk_image_path = os.path.join(
1062 self.conf.get('vm.images_dir'), dst_name)
1063 run_subprocess(
1064 ['sudo', 'qemu-img', 'convert',
1065 '-O', 'qcow2', cloud_image_path, disk_image_path])
1066 run_subprocess(
1067 ['sudo', 'qemu-img', 'resize',
1068 disk_image_path, self.conf.get('vm.disk_size')])
1069 self._disk_image_path = disk_image_path
1070
1071
1072class KvmFromBacking(Kvm):
1073
1074 def create_disk_image(self, src_name=None, dst_name=None):
1075 """Create a disk image backed by an existing one."""
1076 backing_image_path = os.path.join(
1077 self.conf.get('vm.images_dir'),
1078 self.conf.expand_options('{vm.backing}'))
1079 disk_image_path = os.path.join(
1080 self.conf.get('vm.images_dir'),
1081 self.conf.expand_options('{vm.name}.qcow2'))
1082 run_subprocess(
1083 ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
1084 '-b', backing_image_path, disk_image_path])
1085 run_subprocess(
1086 ['sudo', 'qemu-img', 'resize',
1087 disk_image_path, self.conf.get('vm.disk_size')])
1088 self._disk_image_path = disk_image_path
1089
1090
1091class ArgParser(argparse.ArgumentParser):
1092 """A parser for the setup_vm script."""
1093
1094 def __init__(self):
1095 description = 'Set up virtual machines from a configuration file.'
1096 super(ArgParser, self).__init__(
1097 prog='setup_vm.py', description=description)
1098 self.add_argument(
1099 'name', help='Virtual machine section in the configuration file.')
1100 self.add_argument('--download', '-d', action="store_true",
1101 help='Force download of the required image.')
1102 self.add_argument('--ssh-keygen', '-k', action="store_true",
1103 help='Generate the ssh server keys (if any).')
1104 self.add_argument('--install', '-i', action="store_true",
1105 help='Install the virtual machine.')
1106
1107 def parse_args(self, args=None, out=None, err=None):
1108 """Parse arguments, overridding stdout/stderr if provided.
1109
1110 Overridding stdout/stderr is provided for tests.
1111
1112 :params args: Defaults to sys.argv[1:].
1113
1114 :param out: Defaults to sys.stdout.
1115
1116 :param err: Defaults to sys.stderr.
1117 """
1118 out_orig = sys.stdout
1119 err_orig = sys.stderr
1120 try:
1121 if out is not None:
1122 sys.stdout = out
1123 if err is not None:
1124 sys.stderr = err
1125 return super(ArgParser, self).parse_args(args)
1126 finally:
1127 sys.stdout = out_orig
1128 sys.stderr = err_orig
1129
1130
1131
1132arg_parser = ArgParser()
1133
1134class Command(object):
1135
1136 def __init__(self, vm):
1137 self.vm = vm
1138
1139
1140class Download(Command):
1141
1142 def run(self):
1143 # FIXME: what needs to be downloaded should depend on the type of the
1144 # vm (possibly errors if there is nothing to download). -- vila
1145 # 2013-02-06
1146 self.vm.download_cloud_image(force=True)
1147
1148
1149class SshKeyGen(Command):
1150
1151 def run(self):
1152 self.vm.ssh_keygen()
1153
1154
1155class Install(Command):
1156
1157 def run(self):
1158 vm_name = self.vm.conf.get('vm.name')
1159 state = vm_states().get(vm_name, None)
1160 if state == 'shut off':
1161 self.vm.undefine()
1162 elif state == 'running':
1163 raise SetupVmError('{name} is running', name=vm_name)
1164 # FIXME: The installation method may vary depending on the vm type.
1165 # -- vila 2013-02-06
1166 self.vm.install()
1167
1168
1169def build_commands(args=None, out=None, err=None):
1170 cmds = []
1171 if args is None:
1172 args = sys.argv[1:]
1173
1174 ns = arg_parser.parse_args(args, out=out, err=err)
1175
1176 conf = VmStack(ns.name)
1177 with_backing = conf.get('vm.backing')
1178 if with_backing is None:
1179 vm = KvmFromCloudImage(conf)
1180 else:
1181 vm = KvmFromBacking(conf)
1182 if ns.download:
1183 cmds.append(Download(vm))
1184 if ns.ssh_keygen:
1185 cmds.append(SshKeyGen(vm))
1186 if ns.install:
1187 cmds.append(Install(vm))
1188 return cmds
1189
1190
1191def run(args=None):
1192 cmds = build_commands(args)
1193 for cmd in cmds:
1194 try:
1195 cmd.run()
1196 except SetupVmError, e:
1197 # Stop on first error
1198 print 'ERROR: %s' % e
1199 exit(-1)
1200
1201
1202if __name__ == "__main__":
1203 run()
01204
=== added file 'setup_vm/bin/ubuntu_admin.sh'
--- setup_vm/bin/ubuntu_admin.sh 1970-01-01 00:00:00 +0000
+++ setup_vm/bin/ubuntu_admin.sh 2013-04-17 01:29:27 +0000
@@ -0,0 +1,2 @@
1#!/bin/sh
2adduser ubuntu admin
03
=== added directory 'setup_vm/pay'
=== added file 'setup_vm/pay/install'
--- setup_vm/pay/install 1970-01-01 00:00:00 +0000
+++ setup_vm/pay/install 2013-04-17 01:29:27 +0000
@@ -0,0 +1,40 @@
1#!/bin/sh -ex
2
3# Allow ssh access to launchpad.
4# This should probably be provided by setup_vm. -- vila 2013-03-10
5ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
6# Get the branch.
7bzr branch lp:canonical-payment-service {pay.src_dir}
8# Get the download cache.
9bzr branch lp:~canonical-isd-hackers/+junk/download-cache
10# Setup the environment.
11cd {pay.src_dir}
12# Get the version controlled configs.
13bzr branch lp:~canonical-isd-hackers/isd-configs/payments-config branches/project
14# Bootstrap the dependencies
15fab bootstrap:download_cache_path=~/download-cache
16# Set up the correct Django configuration.
17cat <<EOF >django_project/local.cfg
18[__noschema__]
19db_host = /home/ubuntu/{pay.src_dir}/.env/db
20hostname = {pay.address}:{pay.port}
21
22[__main__]
23includes =
24 config/devel.cfg
25 ../branches/project/config/acceptance.cfg
26
27[django]
28debug = false
29internal_ips =
30
31[testing]
32imap_server = {sso.address}
33imap_port = {sso.imap_port}
34imap_use_ssl = False
35
36[openid]
37openid_sso_server_url = {sso.url}
38openid_trust_root = {pay.url}
39
40EOF
041
=== added file 'setup_vm/pay/run'
--- setup_vm/pay/run 1970-01-01 00:00:00 +0000
+++ setup_vm/pay/run 2013-04-17 01:29:27 +0000
@@ -0,0 +1,9 @@
1#!/bin/sh
2
3cd {pay.src_dir}
4
5# Setup the database.
6fab setup_postgresql_server
7fab manage:loaddata,test
8# Start the PAY server, accessible from the local network.
9fab run:0.0.0.0:{pay.port}
010
=== added file 'setup_vm/pay/run-for-u1'
--- setup_vm/pay/run-for-u1 1970-01-01 00:00:00 +0000
+++ setup_vm/pay/run-for-u1 2013-04-17 01:29:27 +0000
@@ -0,0 +1,57 @@
1#!/bin/sh
2
3cd {pay.src_dir}
4
5# Setup the database.
6fab setup_postgresql_server
7# Add the U1 consumer.
8cat <<EOF >src/paymentservice/fixtures/consumer.json
9[
10 {
11 "pk": "U1",
12 "model": "paymentservice.consumer",
13 "fields": {
14 "notification_url": "{u1.url}/notifications/",
15 "ip_address": "{u1.address}",
16 "name": "Ubuntu One",
17 "default_business_unit": "Online Services",
18 "email_footer": "Test footer.",
19 "theme": "ubuntuone"
20 }
21 }
22]
23EOF
24fab manage:loaddata,consumer
25# Add the API user for U1.
26# We generated this json file with:
27# Go to {pay.url}/admin
28# Sign in with the admin/admin.
29# Click the more link next to the Model Admin heading.
30# On the Paymentservice section, click the +Add link next to API Users.
31# Fill the form with:
32# username: u1qauser
33# password: u1qapassword
34# Click the Save button.
35# Select the Ubuntu One (U1) Consumer.
36# Click the Save button.
37# $ fab manage:dumpdata,paymentservice.APIUser
38cat <<EOF >src/paymentservice/fixtures/apiuser.json
39[
40 {
41 "pk": 2,
42 "model": "paymentservice.apiuser",
43 "fields": {
44 "username": "u1qauser",
45 "created_at": "2013-04-15 00:09:48",
46 "password": "sha1\$b2a8e\$0e06d9cb46583aa53d3bf144ae07018a7546f737",
47 "consumer": "U1",
48 "updated_at": "2013-04-15 00:09:54"
49 }
50 }
51]
52EOF
53fab manage:loaddata,apiuser
54# Start the PAY server, accessible from the local network.
55# We don't call the run task because it loads a fixture that overwrites our
56# consumer.
57fab manage:runserver,0.0.0.0:{pay.port}
058
=== added file 'setup_vm/pay/test'
--- setup_vm/pay/test 1970-01-01 00:00:00 +0000
+++ setup_vm/pay/test 2013-04-17 01:29:27 +0000
@@ -0,0 +1,5 @@
1#!/bin/sh
2
3cd {pay.src_dir}
4
5SST_BASE_URL={pay.url} fab acceptance:screenshot=true,report=xml,extended=true
06
=== added file 'setup_vm/selftest.py'
--- setup_vm/selftest.py 1970-01-01 00:00:00 +0000
+++ setup_vm/selftest.py 2013-04-17 01:29:27 +0000
@@ -0,0 +1,24 @@
1#!/usr/bin/env python
2
3import sys
4
5import testtools.run
6import unittest
7
8
9class TestProgram(testtools.run.TestProgram):
10
11 def __init__(self, module, argv, stdout=None, testRunner=None, exit=True):
12 if testRunner is None:
13 testRunner = unittest.TextTestRunner
14 super(TestProgram, self).__init__(module, argv=argv, stdout=stdout,
15 testRunner=testRunner, exit=exit)
16
17
18# We discover tests under './tests', the python 'load_test' protocol can be
19# used in test modules for more fancy stuff.
20discover_args = ['discover',
21 '--start-directory', './tests',
22 '--top-level-directory', '.',
23 ]
24TestProgram(__name__, argv=[sys.argv[0]] + discover_args + sys.argv[1:])
025
=== added directory 'setup_vm/sso'
=== added file 'setup_vm/sso/install'
--- setup_vm/sso/install 1970-01-01 00:00:00 +0000
+++ setup_vm/sso/install 2013-04-17 01:29:27 +0000
@@ -0,0 +1,45 @@
1#!/bin/sh -ex
2
3# Allow ssh access to launchpad.
4# This should probably be provided by setup_vm. -- vila 2013-03-10
5ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
6# Get the branch.
7bzr branch lp:canonical-identity-provider {sso.src_dir}
8# Get the download cache.
9bzr branch lp:~canonical-isd-hackers/+junk/download-cache
10# Setup the environment.
11cd {sso.src_dir}
12# Get the version controlled configs.
13bzr branch lp:~canonical-isd-hackers/isd-configs/sso-config branches/project
14# Bootstrap the dependencies
15fab bootstrap:download_cache_path=~/download-cache
16# Set up the correct Django configuration.
17# In order to set the db_host to a directory in .env, we need to use the full
18# path. Otherwise, fab setup_postgresql_server will fail.
19# TODO we can either configure the postgresql authentication and pass db_host
20# as empty, or use cat just to append to the end of the default local.cfg
21# that will contain the full path we need, or pass the user name in a config
22# variable.
23cat <<EOF >django_project/local.cfg
24[__noschema__]
25basedir = .
26db_host = /home/ubuntu/{sso.src_dir}/.env/db
27hostname = {sso.address}:{sso.port}
28
29[__main__]
30includes =
31 config/devel.cfg
32 ../branches/project/config/acceptance-dev.cfg
33
34[django]
35debug = false
36email_port = {sso.smtp_port}
37
38[testing]
39imap_server = {sso.address}
40imap_port = {sso.imap_port}
41# needs to be a full email
42imap_username = whatever@we.dont.care
43imap_use_ssl = False
44
45EOF
046
=== added file 'setup_vm/sso/run'
--- setup_vm/sso/run 1970-01-01 00:00:00 +0000
+++ setup_vm/sso/run 2013-04-17 01:29:27 +0000
@@ -0,0 +1,16 @@
1#!/bin/sh
2
3cd ~/{sso.src_dir}
4# We need an SMTP server to send emails.
5.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
6
7# Setup the database.
8fab setup_postgresql_server
9fab manage:loaddata,test
10fab manage:create_test_team
11# get gargoyle flags from their use in the code
12SST_FLAGS=`grep -rho --exclude 'test_*.py' "is_active([\"']\(.*\)[\"']" identityprovider/ webui/ | sed -E "s/is_active\(['\"](.*)['\"]/\1/" | awk '{print tolower($0)}' | sort | uniq | tr '\n' ','`
13# We need to remove the trailing ','
14fab gargoyle_flags:${SST_FLAGS%,}
15# Start the SSO server, accessible from the local network.
16fab run:0.0.0.0:{sso.port}
017
=== added file 'setup_vm/sso/run-for-pay'
--- setup_vm/sso/run-for-pay 1970-01-01 00:00:00 +0000
+++ setup_vm/sso/run-for-pay 2013-04-17 01:29:27 +0000
@@ -0,0 +1,14 @@
1#!/bin/sh
2
3cd ~/{sso.src_dir}
4# We need an SMTP server to send emails.
5.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
6
7# Setup the database.
8fab setup_postgresql_server
9fab manage:loaddata,isdtest
10fab manage:loaddata,allow_unverified
11# Set the allow-unverified config for Pay.
12fab manage:add_openid_rp_config,{pay.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
13# Start the SSO server, accessible from the local network.
14fab run:0.0.0.0:{sso.port}
015
=== added file 'setup_vm/sso/run-for-u1'
--- setup_vm/sso/run-for-u1 1970-01-01 00:00:00 +0000
+++ setup_vm/sso/run-for-u1 2013-04-17 01:29:27 +0000
@@ -0,0 +1,43 @@
1#!/bin/sh
2
3cd ~/{sso.src_dir}
4# We need an SMTP server to send emails.
5.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
6
7# Setup the database.
8fab setup_postgresql_server
9fab manage:loaddata,allow_unverified
10# Set the allow-unverified config for Pay.
11fab manage:add_openid_rp_config,{pay.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
12# Set the allow-unverified config for U1.
13fab manage:add_openid_rp_config,{u1.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
14# Add the API user for U1.
15# We generated this json file with:
16# $ fab manage:createsuperuser
17# Go to {sso.url}/admin
18# Sign in with the super user you have just created.
19# Click the more link next to the Model Admin heading.
20# On the Identityprovider section, click the +Add link next to API Users.
21# Fill the form with:
22# username: u1qauser
23# password: u1qapassword
24# Click the Save button.
25# $ fab manage:dumpdata,identityprovider.APIUser
26cat <<EOF >identityprovider/fixtures/apiuser.json
27[
28 {
29 "pk": 1,
30 "model": "identityprovider.apiuser",
31 "fields": {
32 "username": "u1qauser",
33 "created_at": "2013-04-14 21:09:43",
34 "password": "k1B7nUTaEsrqAPHF/bWsLlNIPUsH7mViraFQBZPgNRRuvsZlRq8CZg==",
35 "updated_at": "2013-04-14 21:09:43"
36 }
37 }
38]
39
40EOF
41fab manage:loaddata,apiuser
42# Start the SSO server, accessible from the local network.
43fab run:0.0.0.0:{sso.port}
044
=== added file 'setup_vm/sso/test'
--- setup_vm/sso/test 1970-01-01 00:00:00 +0000
+++ setup_vm/sso/test 2013-04-17 01:29:27 +0000
@@ -0,0 +1,11 @@
1#!/bin/sh
2
3# FIXME: This should run on the host and get config options expanded
4# -- vila 2013-03-12
5
6cd {sso.src_dir}
7
8# get gargoyle flags from their use in the code
9SST_FLAGS=`grep -rho --exclude 'test_*.py' "is_active([\"']\(.*\)[\"']" identityprovider/ webui/ | sed -E "s/is_active\(['\"](.*)['\"]/\1/" | awk '{print tolower($0)}' | sort | uniq | tr '\n' ';'`
10# run tests
11SST_BASE_URL={sso.url} fab acceptance:screenshot=true,report=xml,extended=true,flags=$SST_FLAGS
012
=== added directory 'setup_vm/tests'
=== added file 'setup_vm/tests/__init__.py'
--- setup_vm/tests/__init__.py 1970-01-01 00:00:00 +0000
+++ setup_vm/tests/__init__.py 2013-04-17 01:29:27 +0000
@@ -0,0 +1,75 @@
1import os
2import shutil
3import tempfile
4
5from bzrlib import osutils
6
7def override_env_var(name, value):
8 """Modify the environment, setting or removing the env_variable.
9
10 :param name: The environment variable to set.
11
12 :param value: The value to set the environment to. If None, then
13 the variable will be removed.
14
15 :return: The original value of the environment variable.
16 """
17 orig = os.environ.get(name)
18 if value is None:
19 if orig is not None:
20 del os.environ[name]
21 else:
22 # FIXME: supporting unicode values requires a way to acquire the
23 # user encoding, punting for now -- vila 2013-01-30
24 os.environ[name] = value
25 return orig
26
27
28def override_env(test, name, new):
29 """Set an environment variable, and reset it after the test.
30
31 :param name: The environment variable name.
32
33 :param new: The value to set the variable to. If None, the
34 variable is deleted from the environment.
35
36 :returns: The actual variable value.
37 """
38 value = override_env_var(name, new)
39 test.addCleanup(override_env_var, name, value)
40 return value
41
42
43isolated_environ = {
44 'HOME': None,
45}
46
47
48def isolate_env(test, env=None):
49 """Isolate test from the environment variables.
50
51 This is usually called in setUp for tests that needs to modify the
52 environment variables and restore them after the test is run.
53
54 :param test: A test instance
55
56 :param env: A dict containing variable definitions to be installed. Only
57 the variables present there are protected. They are initialized with
58 the provided values.
59 """
60 if env is None:
61 env = isolated_environ
62 for name, value in env.items():
63 override_env(test, name, value)
64
65
66def set_cwd_to_tmp(test):
67 """Create a temp dir an cd into it for the test duration.
68
69 This is generally called during a test setup.
70 """
71 test.test_base_dir = tempfile.mkdtemp(prefix='mytests-', suffix='.tmp')
72 test.addCleanup(shutil.rmtree, test.test_base_dir, True)
73 current_dir = os.getcwdu()
74 test.addCleanup(os.chdir, current_dir)
75 os.chdir(test.test_base_dir)
076
=== added file 'setup_vm/tests/test_setup_vm.py'
--- setup_vm/tests/test_setup_vm.py 1970-01-01 00:00:00 +0000
+++ setup_vm/tests/test_setup_vm.py 2013-04-17 01:29:27 +0000
@@ -0,0 +1,1074 @@
1from cStringIO import StringIO
2import os
3
4from bzrlib import errors
5import testtools
6
7import tests
8from bin import setup_vm
9
10
11def requires_known_reference_image(test):
12 # We need a pre-seeded download cache from the user running the tests
13 # as downloading the cloud image is too long.
14 user_conf = setup_vm.VmStack(None)
15 download_cache = user_conf.get('vm.download_cache')
16 if download_cache is None:
17 test.skip('vm.download_cache is not set')
18 # We use some known reference
19 reference_cloud_image_name = 'raring-server-cloudimg-amd64-disk1.img'
20 cloud_image_path = os.path.join(
21 download_cache, reference_cloud_image_name)
22 if not os.path.exists(cloud_image_path):
23 test.skip('%s is not available' % (cloud_image_path,))
24 return download_cache, reference_cloud_image_name
25
26
27class TestCaseWithHome(testtools.TestCase):
28 """Provide an isolated disk-based environment.
29
30 A $HOME directory is setup as well as an /etc/ one so tests can setup
31 config files.
32 """
33
34 def setUp(self):
35 super(TestCaseWithHome, self).setUp()
36 tests.set_cwd_to_tmp(self)
37 tests.isolate_env(self)
38 # Isolate tests from the user environment
39 self.home_dir = os.path.join(self.test_base_dir, 'home')
40 os.mkdir(self.home_dir)
41 os.environ['HOME'] = self.home_dir
42 # Also isolate from the system environment
43 self.etc_dir = os.path.join(self.test_base_dir, 'etc')
44 os.mkdir(self.etc_dir)
45 self.patch(setup_vm, 'system_config_dir', lambda: self.etc_dir)
46
47
48class TestVmMatcher(TestCaseWithHome):
49
50 def setUp(self):
51 super(TestVmMatcher, self).setUp()
52 self.store = setup_vm.VmStore('.', 'foo.conf')
53 self.matcher = setup_vm.VmMatcher(self.store, 'test')
54
55 def test_empty_section_always_matches(self):
56 self.store._load_from_string('foo=bar')
57 matching = list(self.matcher.get_sections())
58 self.assertEqual(1, len(matching))
59
60 def test_specific_before_generic(self):
61 self.store._load_from_string('foo=bar\n[test]\nfoo=baz')
62 matching = list(self.matcher.get_sections())
63 self.assertEqual(2, len(matching))
64 # First matching section is for test
65 self.assertEqual(self.store, matching[0][0])
66 base_section = matching[0][1]
67 self.assertEqual('test', base_section.id)
68 # Second matching section is the no-name one
69 self.assertEqual(self.store, matching[0][0])
70 no_name_section = matching[1][1]
71 self.assertIs(None, no_name_section.id)
72
73
74class TestVmStores(TestCaseWithHome):
75
76 def setUp(self):
77 super(TestVmStores, self).setUp()
78 self.conf = setup_vm.VmStack('foo')
79
80
81 def test_default_in_empty_stack(self):
82 self.assertEqual('1024', self.conf.get('vm.ram_size'))
83
84
85 def test_system_overrides_internal(self):
86 self.conf.system_store._load_from_string('vm.ram_size = 42')
87 self.assertEqual('42', self.conf.get('vm.ram_size'))
88
89 def test_user_overrides_system(self):
90 self.conf.system_store._load_from_string('vm.ram_size = 42')
91 self.conf.store._load_from_string('vm.ram_size = 4201')
92 self.assertEqual('4201', self.conf.get('vm.ram_size'))
93
94 def test_local_overrides_user(self):
95 self.conf.system_store._load_from_string('vm.ram_size = 42')
96 self.conf.store._load_from_string('vm.ram_size = 4201')
97 self.conf.local_store._load_from_string('vm.ram_size = 8402')
98 self.assertEqual('8402', self.conf.get('vm.ram_size'))
99
100
101class TestVmStack(TestCaseWithHome):
102
103 def setUp(self):
104 super(TestVmStack, self).setUp()
105 self.conf = setup_vm.VmStack('foo')
106 self.conf.store._load_from_string('''
107vm.release=raring
108vm.cpu_model=amd64
109''')
110
111 def assertValue(self, expected_value, option):
112 self.assertEqual(expected_value, self.conf.get(option))
113
114 def test_raring_iso_url(self):
115 self.assertValue('http://cdimage.ubuntu.com/daily-live/current/',
116 'vm.iso_url' )
117
118 def test_raring_iso_name(self):
119 self.assertValue( 'raring-desktop-amd64.iso', 'vm.iso_name')
120
121 def test_raring_cloud_image_url(self):
122 self.assertValue('http://cloud-images.ubuntu.com/raring/current/',
123 'vm.cloud_image_url')
124
125 def test_raring_cloud_image_name(self):
126 self.assertValue('raring-server-cloudimg-amd64-disk1.img',
127 'vm.cloud_image_name')
128
129 def test_apt_proxy_set(self):
130 proxy = 'apt_proxy: http://example.org:4321'
131 self.conf.set('vm.apt_proxy', proxy)
132 self.assertEqual(proxy, self.conf.expand_options('{vm.apt_proxy}'))
133
134 def test_download_cache_with_user_expansion(self):
135 download_cache = '~/installers'
136 self.conf.set('vm.download_cache', download_cache)
137 self.assertValue(os.path.join(self.home_dir, 'installers'),
138 'vm.download_cache')
139
140 def test_images_dir_with_user_expansion(self):
141 images_dir = '~/images'
142 self.conf.set('vm.images_dir', images_dir)
143 self.assertValue(os.path.join(self.home_dir, 'images'),
144 'vm.images_dir')
145
146
147class TestPathOption(TestCaseWithHome):
148
149 def assertConverted(self, expected, value):
150 option = setup_vm.PathOption('foo', help='A path.')
151 self.assertEquals(expected, option.convert_from_unicode(None, value))
152
153 def test_absolute_path(self):
154 self.assertConverted('/test/path', '/test/path')
155
156 def test_home_path_with_expansion(self):
157 self.assertConverted(self.home_dir, '~')
158
159 def test_path_in_home_with_expansion(self):
160 self.assertConverted(os.path.join(self.home_dir, 'test/path'),
161 '~/test/path')
162
163
164class TestDownload(TestCaseWithHome):
165
166# FIXME: Needs parametrization against vm.{cloud_image_name|iso_name} and
167# {download_iso|download_cloud_image} -- vila 2013-02-07
168
169 def setUp(self):
170 # Downloading real isos or images is too long for tests, instead, we
171 # fake it by downloading a small but known to exist file: MD5SUMS
172 super(TestDownload, self).setUp()
173 download_cache = os.path.join(self.test_base_dir, 'downloads')
174 os.mkdir(download_cache)
175 self.conf = setup_vm.VmStack('foo')
176 self.conf.store._load_from_string('''
177vm.iso_name=MD5SUMS
178vm.cloud_image_name=MD5SUMS
179vm.release=raring
180vm.cpu_model=amd64
181vm.download_cache=%s
182''' % (download_cache,))
183
184 def test_download_iso(self):
185 vm = setup_vm.Kvm(self.conf)
186 self.assertTrue(vm.download_iso())
187 # Trying to download again will find the file in the cache
188 self.assertFalse(vm.download_iso())
189 # Forcing the download even when the file is present
190 self.assertTrue(vm.download_iso(force=True))
191
192 def test_download_cloud_image(self):
193 vm = setup_vm.Kvm(self.conf)
194 self.assertTrue(vm.download_cloud_image())
195 # Trying to download again will find the file in the cache
196 self.assertFalse(vm.download_cloud_image())
197 # Forcing the download even when the file is present
198 self.assertTrue(vm.download_cloud_image(force=True))
199
200 def test_download_unknown_iso_fail(self):
201 self.conf.set('vm.iso_name', 'I-dont-exist')
202 vm = setup_vm.Kvm(self.conf)
203 self.assertRaises(setup_vm.CommandError, vm.download_iso)
204
205 def test_download_unknown_cloud_image_fail(self):
206 self.conf.set('vm.cloud_image_name', 'I-dont-exist')
207 vm = setup_vm.Kvm(self.conf)
208 self.assertRaises(setup_vm.CommandError, vm.download_cloud_image)
209
210 def test_download_iso_with_unknown_cache_fail(self):
211 dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
212 self.conf.set('vm.download_cache', dl_cache)
213 vm = setup_vm.Kvm(self.conf)
214 self.assertRaises(setup_vm.ConfigValueError, vm.download_iso)
215
216 def test_download_cloud_image_with_unknown_cache_fail(self):
217 dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
218 self.conf.set('vm.download_cache', dl_cache)
219 vm = setup_vm.Kvm(self.conf)
220 self.assertRaises(setup_vm.ConfigValueError, vm.download_cloud_image)
221
222
223class TestMetaData(TestCaseWithHome):
224
225 def setUp(self):
226 super(TestMetaData, self).setUp()
227 self.conf = setup_vm.VmStack('foo')
228 self.vm = setup_vm.Kvm(self.conf)
229 images_dir = os.path.join(self.test_base_dir, 'images')
230 os.mkdir(images_dir)
231 config_dir = os.path.join(self.test_base_dir, 'config')
232 self.conf.store._load_from_string('''
233vm.name=foo
234vm.images_dir=%s
235vm.config_dir=%s
236''' % (images_dir, config_dir,))
237
238 def test_create_meta_data(self):
239 self.vm.create_meta_data()
240 self.assertTrue(os.path.exists(self.vm._config_dir))
241 self.assertTrue(os.path.exists(self.vm._meta_data_path))
242 with open(self.vm._meta_data_path) as f:
243 meta_data = f.readlines()
244 self.assertEqual(2, len(meta_data))
245 self.assertEqual('instance-id: foo\n', meta_data[0])
246 self.assertEqual('local-hostname: foo\n', meta_data[1])
247
248
249class TestYaml(testtools.TestCase):
250
251 def yaml_load(self, *args, **kwargs):
252 return setup_vm.yaml.safe_load(*args, **kwargs)
253
254 def yaml_dump(self, *args, **kwargs):
255 return setup_vm.yaml.safe_dump(*args, **kwargs)
256
257 def test_load_scalar(self):
258 self.assertEqual({'foo': 'bar'}, self.yaml_load(StringIO('{foo: bar}')))
259 # Surprisingly the enclosing braces are not needed, probably a special
260 # case for the highest level
261 self.assertEqual({'foo': 'bar'}, self.yaml_load(StringIO('foo: bar')))
262
263 def test_dump_scalar(self):
264 self.assertEqual('{foo: bar}\n', self.yaml_dump(dict(foo='bar')))
265
266 def test_load_list(self):
267 self.assertEqual({'foo': ['a', 'b', 'c']},
268 # Single space indentation is enough
269 self.yaml_load(StringIO('''\
270foo:
271 - a
272 - b
273 - c
274''')))
275
276 def test_dump_list(self):
277 # No more enclosing braces... yeah for consistency :-/
278 self.assertEqual('foo: [a, b, c]\n',
279 self.yaml_dump(dict(foo=['a', 'b', 'c'])))
280
281 def test_load_dict(self):
282 self.assertEqual({'foo': {'bar': 'baz'}},
283 self.yaml_load(StringIO('{foo: {bar: baz}}')))
284 multiple_lines = '''\
285foo: {bar: multiple
286 lines}
287'''
288 self.assertEqual({'foo': {'bar': 'multiple lines'}},
289 self.yaml_load(StringIO(multiple_lines)))
290
291
292
293class TestLaunchpadAccess(TestCaseWithHome):
294
295 def setUp(self):
296 super(TestLaunchpadAccess, self).setUp()
297 self.conf = setup_vm.VmStack('foo')
298 self.vm = setup_vm.Kvm(self.conf)
299 self.ci_data = setup_vm.CIUserData(self.conf)
300
301 def test_cant_find_private_key(self):
302 self.conf.store._load_from_string('vm.launchpad_id = I-dont-exist')
303 e = self.assertRaises(setup_vm.ConfigPathNotFound,
304 self.ci_data.set_launchpad_access)
305 key_path = '~/.ssh/I-dont-exist@setup_vm'
306 self.assertEqual(key_path, e.path)
307 self.assertTrue(unicode(e).startswith(
308 'You need to create the {p} keypair'.format(p=key_path)))
309
310 def test_id_with_key(self):
311 ssh_dir = os.path.join(self.home_dir, '.ssh')
312 os.mkdir(ssh_dir)
313 key_path = os.path.join(ssh_dir, 'user@setup_vm')
314 with open(key_path, 'w') as f:
315 f.write('key content')
316 self.conf.store._load_from_string('vm.launchpad_id = user')
317 self.assertEqual(None, self.ci_data.launchpad_hook)
318 self.ci_data.set_launchpad_access()
319 self.assertEqual('''\
320#!/bin/sh
321mkdir -p /home/ubuntu/.ssh
322chown ubuntu:ubuntu ~ubuntu
323chmod 0700 ~ubuntu
324chown ubuntu:ubuntu /home/ubuntu/.ssh
325chmod 0700 /home/ubuntu/.ssh
326cat >/home/ubuntu/.ssh/id_rsa <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
327key content
328EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
329chown ubuntu:ubuntu /home/ubuntu/.ssh/id_rsa
330chmod 0400 /home/ubuntu/.ssh/id_rsa
331''',
332 self.ci_data.launchpad_hook)
333 cc = self.ci_data.cloud_config
334 self.assertEquals([['sudo', '-u', 'ubuntu',
335 'bzr', 'launchpad-login', 'user']],
336 cc['runcmd'])
337
338
339class TestCIUserData(TestCaseWithHome):
340
341 def setUp(self):
342 super(TestCIUserData, self).setUp()
343 self.conf = setup_vm.VmStack('foo')
344 self.ci_data = setup_vm.CIUserData(self.conf)
345
346 def test_empty_config(self):
347 self.ci_data.populate()
348 # Check default values
349 self.assertIs(None, self.ci_data.root_hook)
350 self.assertIs(None, self.ci_data.ubuntu_hook)
351 cc = self.ci_data.cloud_config
352 self.assertFalse(cc['apt_update'])
353 self.assertFalse(cc['apt_upgrade'])
354 self.assertEqual({'expire': False}, cc['chpasswd'])
355 self.assertEqual('setup_vm finished installing in ${uptime} seconds.',
356 cc['final_message'])
357 self.assertTrue(cc['manage_etc_hosts'])
358 self.assertEqual('ubuntu', cc['password'])
359 self.assertEqual({'mode': 'poweroff'}, cc['power_state'])
360
361 def test_password(self):
362 self.conf.store._load_from_string('vm.password = tagada')
363 self.ci_data.populate()
364 self.assertEquals('tagada', self.ci_data.cloud_config['password'])
365
366 def test_apt_proxy(self):
367 self.conf.store._load_from_string('vm.apt_proxy = tagada')
368 self.ci_data.populate()
369 self.assertEquals('tagada', self.ci_data.cloud_config['apt_proxy'])
370
371 def test_final_message_precise(self):
372 self.conf.store._load_from_string('vm.release = precise')
373 self.ci_data.populate()
374 self.assertEqual('setup_vm finished installing in $UPTIME seconds.',
375 self.ci_data.cloud_config['final_message'])
376
377 def test_poweroff_precise(self):
378 self.conf.store._load_from_string('vm.release = precise')
379 self.ci_data.populate()
380 self.assertEqual(['halt'], self.ci_data.cloud_config['runcmd'])
381
382 def test_poweroff_quantal(self):
383 self.conf.store._load_from_string('vm.release = quantal')
384 self.ci_data.populate()
385 self.assertEqual(['halt'], self.ci_data.cloud_config['runcmd'])
386
387 def test_poweroff_other(self):
388 self.conf.store._load_from_string('vm.release = raring')
389 self.ci_data.populate()
390 self.assertEqual({'mode': 'poweroff'},
391 self.ci_data.cloud_config['power_state'])
392 self.assertIs(None, self.ci_data.cloud_config.get('runcmd'))
393
394 def test_update_true(self):
395 self.conf.store._load_from_string('vm.update = True')
396 self.ci_data.populate()
397 cc = self.ci_data.cloud_config
398 self.assertTrue(cc['apt_update'])
399 self.assertTrue(cc['apt_upgrade'])
400
401 def test_packages(self):
402 self.conf.store._load_from_string('vm.packages = bzr,ubuntu-desktop')
403 self.ci_data.populate()
404 self.assertEqual(['bzr', 'ubuntu-desktop'],
405 self.ci_data.cloud_config['packages'])
406
407 def test_apt_sources(self):
408 self.conf.store._load_from_string('''\
409vm.release = raring
410# Ensure options are properly expanded (and comments supported ;)
411_archive_url = http://archive.ubuntu.com/ubuntu
412_ppa_url = https://u:p@ppa.lp.net/user/ppa/ubuntu
413vm.apt_sources = deb {_archive_url} {vm.release} partner,\
414 deb {_archive_url} {vm.release} main,\
415 deb {_ppa_url} {vm.release} main|ABCDEF
416''')
417 self.ci_data.populate()
418 self.assertEqual(
419 [{'source': 'deb http://archive.ubuntu.com/ubuntu raring partner'},
420 {'source': 'deb http://archive.ubuntu.com/ubuntu raring main'},
421 {'source':
422 'deb https://u:p@ppa.lp.net/user/ppa/ubuntu raring main',
423 'keyid': 'ABCDEF'}],
424 self.ci_data.cloud_config['apt_sources'])
425
426 def create_file(self, path, content):
427 with open(path, 'wb') as f:
428 f.write(content)
429
430 def test_good_ssh_keys(self):
431 paths = ('rsa', 'rsa.pub', 'dsa', 'dsa.pub', 'ecdsa', 'ecdsa.pub')
432 for path in paths:
433 self.create_file(path, '%s\ncontent\n' % (path,))
434 paths_as_list = ','.join(paths)
435 self.conf.store._load_from_string(
436 'vm.ssh_keys = %s' % (paths_as_list,))
437 self.ci_data.populate()
438 self.assertEqual({'dsa_private': 'dsa\ncontent\n',
439 'dsa_public': 'dsa.pub\ncontent\n',
440 'ecdsa_private': 'ecdsa\ncontent\n',
441 'ecdsa_public': 'ecdsa.pub\ncontent\n',
442 'rsa_private': 'rsa\ncontent\n',
443 'rsa_public': 'rsa.pub\ncontent\n'},
444 self.ci_data.cloud_config['ssh_keys'])
445
446 def test_bad_type_ssh_keys(self):
447 self.conf.store._load_from_string('vm.ssh_keys = I-dont-exist')
448 self.assertRaises(setup_vm.ConfigValueError, self.ci_data.populate)
449
450 def test_unknown_ssh_keys(self):
451 self.conf.store._load_from_string('vm.ssh_keys = rsa.pub')
452 self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
453
454 def test_good_ssh_authorized_keys(self):
455 paths = ('home.pub', 'work.pub')
456 for path in paths:
457 self.create_file(path, '%s\ncontent\n' % (path,))
458 paths_as_list = ','.join(paths)
459 self.conf.store._load_from_string(
460 'vm.ssh_authorized_keys = %s' % (paths_as_list,))
461 self.ci_data.populate()
462 self.assertEqual(['home.pub\ncontent\n', 'work.pub\ncontent\n'],
463 self.ci_data.cloud_config['ssh_authorized_keys'])
464
465 def test_unknown_ssh_authorized_keys(self):
466 self.conf.store._load_from_string('vm.ssh_authorized_keys = rsa.pub')
467 self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
468
469 def test_unknown_root_script(self):
470 self.conf.store._load_from_string('vm.root_script = I-dont-exist')
471 self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
472
473 def test_unknown_ubuntu_script(self):
474 self.conf.store._load_from_string('vm.ubuntu_script = I-dont-exist')
475 self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
476
477 def test_expansion_error_in_script(self):
478 root_script_content = '''#!/bin/sh
479echo Hello {I_dont_exist}
480'''
481 with open('root_script.sh', 'w') as f:
482 f.write(root_script_content)
483 self.conf.store._load_from_string('''\
484vm.root_script = root_script.sh
485''')
486 e = self.assertRaises(errors.ExpandingUnknownOption,
487 self.ci_data.populate)
488 self.assertEqual(root_script_content, e.string)
489
490 def test_unknown_uploaded_scripts(self):
491 self.conf.store._load_from_string(
492 '''vm.uploaded_scripts = I-dont-exist ''')
493 e = self.assertRaises(setup_vm.ConfigPathNotFound,
494 self.ci_data.populate)
495
496 def test_root_script(self):
497 with open('root_script.sh', 'w') as f:
498 f.write('''#!/bin/sh
499echo Hello {user}
500''')
501 self.conf.store._load_from_string('''\
502vm.root_script = root_script.sh
503user=root
504''')
505 self.ci_data.populate()
506 # The additional newline after the script is expected
507 self.assertEqual('''\
508#!/bin/sh
509cat >~root/setup_vm_post_install <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
510#!/bin/sh
511echo Hello root
512
513EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
514chown root:root ~root/setup_vm_post_install
515chmod 0700 ~root/setup_vm_post_install
516''', self.ci_data.root_hook)
517 self.assertEqual(['~root/setup_vm_post_install'],
518 self.ci_data.cloud_config['runcmd'])
519
520 def test_ubuntu_script(self):
521 with open('ubuntu_script.sh', 'w') as f:
522 f.write('''#!/bin/sh
523echo Hello {user}
524''')
525 self.conf.store._load_from_string('''\
526vm.ubuntu_script = ubuntu_script.sh
527user = ubuntu
528''')
529 self.ci_data.populate()
530 # The additional newline after the script is expected
531 self.assertEqual('''\
532#!/bin/sh
533cat >~ubuntu/setup_vm_post_install <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
534#!/bin/sh
535echo Hello ubuntu
536
537EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
538chown ubuntu:ubuntu ~ubuntu/setup_vm_post_install
539chmod 0700 ~ubuntu/setup_vm_post_install
540''', self.ci_data.ubuntu_hook)
541 # The command is run as root, so we need to 'su ubuntu' first
542 self.assertEqual([['su', '-l',
543 '-c', '~ubuntu/setup_vm_post_install',
544 'ubuntu']],
545 self.ci_data.cloud_config['runcmd'])
546
547 def test_uploaded_scripts(self):
548 paths = ('foo', 'bar')
549 for path in paths:
550 self.create_file(path, '%s\ncontent\n' % (path,))
551 paths_as_list = ','.join(paths)
552 self.conf.store._load_from_string(
553 'vm.uploaded_scripts = %s' % (paths_as_list,))
554 self.ci_data.populate()
555 self.assertEqual('''\
556#!/bin/sh
557cat >~ubuntu/setup_vm_uploads <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
558mkdir -p ~ubuntu/bin
559cd ~ubuntu/bin
560cat >foo <<'EOFfoo'
561foo
562content
563
564EOFfoo
565chmod 0755 foo
566cat >bar <<'EOFbar'
567bar
568content
569
570EOFbar
571chmod 0755 bar
572EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
573chown ubuntu:ubuntu ~ubuntu/setup_vm_uploads
574chmod 0700 ~ubuntu/setup_vm_uploads
575''',
576 self.ci_data.uploaded_scripts_hook)
577 self.assertEqual([['su', '-l',
578 '-c', '~ubuntu/setup_vm_uploads',
579 'ubuntu']],
580 self.ci_data.cloud_config['runcmd'])
581
582
583class TestCreateUserData(TestCaseWithHome):
584
585 def setUp(self):
586 super(TestCreateUserData, self).setUp()
587 self.conf = setup_vm.VmStack('foo')
588 self.vm = setup_vm.Kvm(self.conf)
589
590 def test_empty_config(self):
591 config_dir = os.path.join(self.test_base_dir, 'config')
592 os.mkdir(config_dir)
593 # The config is *almost* empty, we need to set config_dir though as the
594 # user-data needs to be stored there.
595 self.conf.store._load_from_string('vm.config_dir=%s' % (config_dir,))
596 self.vm.create_user_data()
597 self.assertTrue(os.path.exists(self.vm._config_dir))
598 self.assertTrue(os.path.exists(self.vm._user_data_path))
599 with open(self.vm._user_data_path) as f:
600 user_data = f.readlines()
601 # We care about the two first lines only here, checking the format (or
602 # cloud-init is confused)
603 self.assertEqual('#cloud-config-archive\n', user_data[0])
604 self.assertEqual("- {content: '#cloud-config\n", user_data[1])
605
606
607class TestSeed(TestCaseWithHome):
608
609 def setUp(self):
610 super(TestSeed, self).setUp()
611 self.conf = setup_vm.VmStack('foo')
612 self.vm = setup_vm.Kvm(self.conf)
613 images_dir = os.path.join(self.test_base_dir, 'images')
614 os.mkdir(images_dir)
615 config_dir = os.path.join(self.test_base_dir, 'config')
616 self.conf.store._load_from_string('''
617vm.name=foo
618vm.release=raring
619vm.config_dir=%s
620vm.images_dir=%s
621''' % (config_dir, images_dir,))
622
623 def test_create_meta_data(self):
624 self.vm.create_meta_data()
625 self.assertTrue(os.path.exists(self.vm._meta_data_path))
626
627 def test_create_user_data(self):
628 self.vm.create_user_data()
629 self.assertTrue(os.path.exists(self.vm._user_data_path))
630
631 def test_create_seed(self):
632 self.assertTrue(self.vm._seed_path is None)
633 self.vm.create_seed()
634 self.assertFalse(self.vm._seed_path is None)
635 self.assertTrue(os.path.exists(self.vm._seed_path))
636
637
638class TestImageFromCloud(TestCaseWithHome):
639
640 def setUp(self):
641 super(TestImageFromCloud, self).setUp()
642 self.conf = setup_vm.VmStack('foo')
643 self.vm = setup_vm.KvmFromCloudImage(self.conf)
644 images_dir = os.path.join(self.test_base_dir, 'images')
645 os.mkdir(images_dir)
646 download_cache_dir = os.path.join(self.test_base_dir, 'download')
647 os.mkdir(download_cache_dir)
648 self.conf.store._load_from_string('''
649vm.name=foo
650vm.release=raring
651vm.images_dir=%s
652vm.download_cache=%s
653vm.cloud_image_name=fake.img
654vm.disk_size=1M
655''' % (images_dir, download_cache_dir))
656
657 def test_create_disk_image(self):
658 cloud_image_path = os.path.join(self.conf.get('vm.download_cache'),
659 self.conf.get('vm.cloud_image_name'))
660 # We need a fake cloud image that can be converted
661 setup_vm.run_subprocess(
662 ['sudo', 'qemu-img', 'create',
663 cloud_image_path, self.conf.get('vm.disk_size')])
664 self.assertTrue(self.vm._disk_image_path is None)
665 self.vm.create_disk_image()
666 self.assertFalse(self.vm._disk_image_path is None)
667 self.assertTrue(os.path.exists(self.vm._disk_image_path))
668
669
670class TestImageWithBacking(TestCaseWithHome):
671
672 def setUp(self):
673 (download_cache_dir,
674 reference_cloud_image_name) = requires_known_reference_image(self)
675 super(TestImageWithBacking, self).setUp()
676 # We'll share the images_dir between vms
677 images_dir = os.path.join(self.test_base_dir, 'images')
678 os.mkdir(images_dir)
679 # Create a shared config
680 conf = setup_vm.VmStack(None)
681 conf.store._load_from_string('''
682vm.release=raring
683vm.images_dir=%s
684vm.download_cache=%s
685vm.disk_size=2G
686[selftest-from-cloud]
687vm.name=selftest-from-cloud
688vm.cloud_image_name=%s
689[selftest-backing]
690vm.name=selftest-backing
691vm.backing=selftest-from-cloud.qcow2
692''' % (images_dir, download_cache_dir, reference_cloud_image_name))
693 conf.store.save()
694 # To bypass creating a real vm, we start from the cloud image that is a
695 # real and bootable one, so we just convert it. That also makes it
696 # available in vm.images_dir
697 temp_vm = setup_vm.KvmFromCloudImage(
698 setup_vm.VmStack('selftest-from-cloud'))
699 temp_vm.create_disk_image()
700
701 def test_create_image_with_backing(self):
702 vm = setup_vm.KvmFromBacking(setup_vm.VmStack('selftest-backing'))
703 self.assertTrue(vm._disk_image_path is None)
704 vm.create_disk_image()
705 self.assertFalse(vm._disk_image_path is None)
706 self.assertTrue(os.path.exists(vm._disk_image_path))
707
708
709class TestVmStates(testtools.TestCase):
710
711 def assertStates(self, expected, lines):
712 self.assertEqual(expected, setup_vm.vm_states(lines))
713
714 def test_empty(self):
715 self.assertStates({},[])
716
717 def test_garbage(self):
718 self.assertRaises(ValueError, self.assertStates, None, [''])
719
720 def test_known_states(self):
721 # From a real life sample
722 self.assertStates({'foo': 'shut off', 'bar': 'running'},
723 ['- foo shut off',
724 '19 bar running'])
725
726
727class TestConsoleParsing(testtools.TestCase):
728
729 def _parse_console_monitor(self, string):
730 mon = setup_vm.ConsoleMonitor(StringIO(string))
731 lines = []
732 for line in mon.parse():
733 lines.append(line)
734 return lines
735
736 def test_fails_on_empty(self):
737 self.assertRaises(setup_vm.ConsoleEOFError,
738 self._parse_console_monitor, '')
739
740 def test_fail_on_knwon_cloud_init_errors(self):
741 self.assertRaises(
742 setup_vm.CloudInitError,
743 self._parse_console_monitor,
744 'Failed loading yaml blob\n')
745 self.assertRaises(
746 setup_vm.CloudInitError,
747 self._parse_console_monitor,
748 'Unhandled non-multipart userdata starting\n')
749 self.assertRaises(
750 setup_vm.CloudInitError,
751 self._parse_console_monitor,
752 "failed to render string to stdout: cannot find 'uptime'\n")
753 self.assertRaises(
754 setup_vm.CloudInitError,
755 self._parse_console_monitor,
756 "Failed loading of cloud config "
757 "'/var/lib/cloud/instance/cloud-config.txt'. "
758 "Continuing with empty config\n")
759
760 def test_succeds_on_final_message(self):
761 lines = self._parse_console_monitor('''
762Lalala
763I'm doing my work
764It goes nicely
765setup_vm finished installing in 1 seconds.
766That was fast isn't it ?
767 * Will now halt
768[ 33.204755] Power down.
769''')
770 # We stop as soon as we get the final message and ignore the rest
771 self.assertEquals(' * Will now halt\n',
772 lines[-1])
773
774
775class TestConsoleParsingWithFile(TestCaseWithHome):
776
777 def _parse_file_monitor(self, string):
778 with open('console', 'w') as f:
779 f.write(string)
780 mon = setup_vm.FileMonitor('console')
781 for line in mon.parse():
782 pass
783 return mon.lines
784
785 def test_succeeds_with_file(self):
786 content = '''\
787Yet another install
788Going well
789setup_vm finished installing in 0.5 seconds.
790Wow, even faster !
791 * Will now halt
792Whatever, won't read that
793'''
794 lines = self._parse_file_monitor(content)
795
796 def xtest_fails_on_empty_file(self):
797 # FIXME: We need some sort of timeout there...
798 self.assertRaises(setup_vm.CommandError, self._parse_file_monitor, '')
799
800 def test_fail_on_knwon_cloud_init_errors_with_file(self):
801 self.assertRaises(
802 setup_vm.CloudInitError,
803 self._parse_file_monitor,
804 'Failed loading yaml blob\n')
805 self.assertRaises(
806 setup_vm.CloudInitError,
807 self._parse_file_monitor,
808 'Unhandled non-multipart userdata starting\n')
809 self.assertRaises(
810 setup_vm.CloudInitError,
811 self._parse_file_monitor,
812 "failed to render string to stdout: cannot find 'uptime'\n")
813
814
815class TestInstallWithSeed(TestCaseWithHome):
816
817 def setUp(self):
818 (download_cache,
819 reference_cloud_image_name) = requires_known_reference_image(self)
820 super(TestInstallWithSeed, self).setUp()
821 # We need to allow other users to read this dir
822 os.chmod(self.test_base_dir, 0755)
823 # We also need to sudo rm it as root created some files there
824 self.addCleanup(
825 setup_vm.run_subprocess,
826 ['sudo', 'rm', '-fr',
827 os.path.join(self.test_base_dir, 'home', '.virtinst')])
828 self.conf = setup_vm.VmStack('selftest-seed')
829 self.vm = setup_vm.KvmFromCloudImage(self.conf)
830 images_dir = os.path.join(self.test_base_dir, 'images')
831 os.mkdir(images_dir, 0755)
832 config_dir = os.path.join(self.test_base_dir, 'config')
833 self.conf.store._load_from_string('''
834vm.name=selftest-seed
835vm.update=False # Shorten install time
836vm.cpus=2,
837vm.release=raring
838vm.config_dir=%s
839vm.images_dir=%s
840vm.download_cache=%s
841vm.cloud_image_name=%s
842vm.disk_size=8G
843''' % (config_dir, images_dir, download_cache, reference_cloud_image_name))
844
845 def assertVmState(self, expected):
846 states = setup_vm.vm_states()
847 self.assertEqual(expected, states[self.vm.conf.get('vm.name')])
848
849 def test_install_with_seed(self):
850 self.addCleanup(self.vm.undefine)
851 self.vm.install()
852 self.assertVmState('shut off')
853
854
855class TestInstallWithBacking(TestCaseWithHome):
856
857 def setUp(self):
858 (download_cache_dir,
859 reference_cloud_image_name) = requires_known_reference_image(self)
860 super(TestInstallWithBacking, self).setUp()
861 # We need to allow other users to read this dir
862 os.chmod(self.test_base_dir, 0755)
863 # We also need to sudo rm it as root created some files there
864 self.addCleanup(
865 setup_vm.run_subprocess,
866 ['sudo', 'rm', '-fr',
867 os.path.join(self.test_base_dir, 'home', '.virtinst')])
868 self.conf = setup_vm.VmStack('selftest-backing')
869 self.vm = setup_vm.KvmFromBacking(self.conf)
870 # We'll share the images_dir between vms
871 images_dir = os.path.join(self.test_base_dir, 'images')
872 os.mkdir(images_dir, 0755)
873 config_dir = os.path.join(self.test_base_dir, 'config')
874 # Create a shared config
875 conf = setup_vm.VmStack(None)
876 conf.store._load_from_string('''
877vm.release=raring
878vm.config_dir=%s
879vm.images_dir=%s
880vm.download_cache=%s
881vm.disk_size=2G
882vm.update=False # Shorten install time
883[selftest-from-cloud]
884vm.name=selftest-from-cloud
885vm.cloud_image_name=%s
886[selftest-backing]
887vm.name=selftest-backing
888vm.backing=selftest-from-cloud.qcow2
889''' % (config_dir, images_dir, download_cache_dir, reference_cloud_image_name))
890 conf.store.save()
891 # Fake a previous install by just re-using the reference cloud image
892 temp_vm = setup_vm.KvmFromCloudImage(
893 setup_vm.VmStack('selftest-from-cloud'))
894 temp_vm.create_disk_image()
895
896 def assertVmState(self, vm, expected):
897 states = setup_vm.vm_states()
898 self.assertEqual(expected, states[vm.conf.get('vm.name')])
899
900 def test_install_with_backing(self):
901 vm = setup_vm.KvmFromBacking(setup_vm.VmStack('selftest-backing'))
902 self.addCleanup(vm.undefine)
903 vm.install()
904 self.assertVmState(vm, 'shut off')
905
906
907class TestSshKeyGen(TestCaseWithHome):
908
909 def setUp(self):
910 super(TestSshKeyGen, self).setUp()
911 self.conf = setup_vm.VmStack(None)
912 self.vm = setup_vm.VM(self.conf)
913 self.config_dir = os.path.join(self.test_base_dir, 'config')
914
915 def load_config(self, more):
916 content = '''\
917vm.config_dir=%s
918vm.name=foo
919''' % (self.config_dir,)
920 self.conf.store._load_from_string(content + more)
921
922 def generate_key(self, ssh_type, upper_type=None):
923 if upper_type is None:
924 upper_type = ssh_type.upper()
925 self.load_config('vm.ssh_keys={vm.config_dir}/%s' % (ssh_type,))
926 self.vm.ssh_keygen()
927 private_path = 'config/%s' % (ssh_type,)
928 self.assertTrue(os.path.exists(private_path))
929 public_path = 'config/%s.pub' % (ssh_type,)
930 self.assertTrue(os.path.exists(public_path))
931 public = file(public_path).read()
932 private = file(private_path).read()
933 self.assertTrue(private.startswith(
934 '-----BEGIN %s PRIVATE KEY-----\n' % (upper_type,)))
935 self.assertTrue(private.endswith(
936 '-----END %s PRIVATE KEY-----\n' % (upper_type,)))
937 return private, public
938
939 def test_dsa(self):
940 private, public = self.generate_key('dsa')
941 self.assertTrue(public.startswith('ssh-dss '))
942 self.assertTrue(public.endswith(' foo\n'))
943
944 def test_rsa(self):
945 private, public = self.generate_key('rsa')
946 self.assertTrue(public.startswith('ssh-rsa '))
947 self.assertTrue(public.endswith(' foo\n'))
948
949 def test_ecdsa(self):
950 private, public = self.generate_key('ecdsa', 'EC')
951 self.assertTrue(public.startswith('ecdsa-sha2-nistp256 '))
952 self.assertTrue(public.endswith(' foo\n'))
953
954
955class TestOptionParsing(testtools.TestCase):
956
957 def setUp(self):
958 super(TestOptionParsing, self).setUp()
959 self.out = StringIO()
960 self.err = StringIO()
961
962 def parse_args(self, args):
963 return setup_vm.arg_parser.parse_args(args, self.out, self.err)
964
965 def test_nothing(self):
966 self.assertRaises(SystemExit, self.parse_args, [])
967
968 def test_install(self):
969 ns = self.parse_args(['foo', '--install'])
970 self.assertEquals('foo', ns.name)
971 self.assertTrue(ns.install)
972 self.assertFalse(ns.download)
973
974 def test_download(self):
975 ns = self.parse_args(['foo', '--download'])
976 self.assertEquals('foo', ns.name)
977 self.assertFalse(ns.install)
978 self.assertTrue(ns.download)
979
980class TestBuildCommands(testtools.TestCase):
981
982 def setUp(self):
983 super(TestBuildCommands, self).setUp()
984 self.out = StringIO()
985 self.err = StringIO()
986
987 def build_commands(self, args):
988 return setup_vm.build_commands(args, self.out, self.err)
989
990 def test_install(self):
991 cmds = self.build_commands(['--install', 'foo'])
992 self.assertEqual(1, len(cmds))
993 self.assertTrue(isinstance(cmds[0], setup_vm.Install))
994
995 def test_download(self):
996 cmds = self.build_commands(['--download', 'foo'])
997 self.assertEqual(1, len(cmds))
998 self.assertTrue(isinstance(cmds[0], setup_vm.Download))
999
1000 def test_ssh_keygen(self):
1001 cmds = self.build_commands(['--ssh-keygen', 'foo'])
1002 self.assertEqual(1, len(cmds))
1003 self.assertTrue(isinstance(cmds[0], setup_vm.SshKeyGen))
1004
1005 def test_download_and_install(self):
1006 cmds = self.build_commands(['--install', '--download', 'foo'])
1007 self.assertEqual(2, len(cmds))
1008 # Download comes first
1009 self.assertTrue(isinstance(cmds[0], setup_vm.Download))
1010 self.assertTrue(isinstance(cmds[1], setup_vm.Install))
1011
1012
1013# FIXME: This needs to be parametrized for KvmFromCloudImage and
1014# KvmFromBacking. Since we don't define vm.backing below, we're only testing
1015# KvmFromCloudImage for now. -- vila 2013-02-13
1016class TestInstall(TestCaseWithHome):
1017
1018 def setUp(self):
1019 super(TestInstall, self).setUp()
1020 self.conf = setup_vm.VmStack('I-dont-exist')
1021 self.conf.store._load_from_string('''
1022vm.name=I-dont-exist
1023vm.release=raring
1024vm.cpu_model=amd64
1025''')
1026 self.states = []
1027
1028 def vm_states(source=None):
1029 return self.states
1030 self.patch(setup_vm, 'vm_states', vm_states)
1031 self.vm = None
1032
1033 def install(self):
1034 class FakeKvm(setup_vm.Kvm):
1035
1036 def __init__(self, conf):
1037 super(FakeKvm, self).__init__(conf)
1038 self.undefine_called = False
1039 self.install_called = False
1040
1041 # Make sure we avoid dangerous or costly calls
1042 def poweroff(self):
1043 pass
1044
1045 def undefine(self):
1046 self.undefine_called = True
1047
1048 def install(self):
1049 self.install_called = True
1050
1051
1052 self.vm = FakeKvm(self.conf)
1053 cmd = setup_vm.Install(self.vm)
1054 cmd.run()
1055
1056 def test_install_while_running(self):
1057 self.conf.set('vm.name', 'foo')
1058 self.states = {'foo': 'running'}
1059 self.assertRaises(setup_vm.SetupVmError, self.install)
1060 self.assertFalse(self.vm.install_called)
1061 self.assertFalse(self.vm.undefine_called)
1062
1063 def test_install_unknown(self):
1064 self.states = {}
1065 self.install()
1066 self.assertTrue(self.vm.install_called)
1067 self.assertFalse(self.vm.undefine_called)
1068
1069 def test_install_shutoff(self):
1070 self.conf.set('vm.name', 'foo')
1071 self.states = {'foo': 'shut off'}
1072 self.install()
1073 self.assertTrue(self.vm.install_called)
1074 self.assertTrue(self.vm.undefine_called)
01075
=== added file 'setup_vm/tests/test_test.py'
--- setup_vm/tests/test_test.py 1970-01-01 00:00:00 +0000
+++ setup_vm/tests/test_test.py 2013-04-17 01:29:27 +0000
@@ -0,0 +1,58 @@
1import os
2
3import testtools
4
5import tests
6
7
8def assertTestSuccess(test, inner):
9 """The received test runs successfully."""
10 result = testtools.TestResult()
11 inner.run(result)
12 test.assertEqual(0, len(result.errors) + len(result.failures))
13 test.assertEqual(1, result.testsRun)
14 return result
15
16
17class TestEnv(testtools.TestCase):
18
19
20 def test_env_preserved(self):
21 os.environ['NOBODY_USES_THIS'] = 'foo'
22
23 class Inner(testtools.TestCase):
24
25 def test_overridden(self):
26 tests.isolate_env(self, {'NOBODY_USES_THIS': 'bar'})
27 self.assertEqual('bar', os.environ['NOBODY_USES_THIS'])
28
29 assertTestSuccess(self, Inner('test_overridden'))
30 self.assertEqual('foo', os.environ['NOBODY_USES_THIS'])
31
32 def test_env_var_deleted(self):
33 os.environ['NOBODY_USES_THIS'] = 'foo'
34
35 class Inner(testtools.TestCase):
36
37 def test_deleted(self):
38 tests.isolate_env(self, {'NOBODY_USES_THIS': None})
39 self.assertIs('deleted',
40 os.environ.get('NOBODY_USES_THIS', 'deleted'))
41 assertTestSuccess(self, Inner('test_deleted'))
42 self.assertEqual('foo', os.environ['NOBODY_USES_THIS'])
43
44
45class TestTmp(testtools.TestCase):
46
47 def test_cwd_in_tmp(self):
48
49 class Inner(testtools.TestCase):
50
51 def setUp(self):
52 super(Inner, self).setUp()
53 tests.set_cwd_to_tmp(self)
54
55 def test_cwd_in_tmp(self):
56 self.assertEqual(os.getcwdu(), self.test_base_dir)
57
58 assertTestSuccess(self, Inner('test_cwd_in_tmp'))
059
=== added directory 'setup_vm/u1'
=== added file 'setup_vm/u1/install'
--- setup_vm/u1/install 1970-01-01 00:00:00 +0000
+++ setup_vm/u1/install 2013-04-17 01:29:27 +0000
@@ -0,0 +1,71 @@
1#!/bin/sh -ex
2
3# Allow ssh access to launchpad.
4# This should probably be provided by setup_vm. -- vila 2013-03-10
5ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
6# Use the openjdk.
7sudo update-alternatives --set java /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
8# Get the branch.
9bzr branch lp:ubuntuone-servers {u1.src_dir}
10# Setup the environment.
11cd {u1.src_dir}
12# Set up the correct configuration.
13cat <<EOF >configs/local.conf
14[meta]
15extends: development-appserver-lazr.conf
16
17[general]
18port: {u1.port}
19django_module: u1servers.web.localsettings
20
21[upay]
22consumer_id: U1
23port: {pay.port}
24hostname: {pay.address}
25url_format: http://%(host)s:%(port)d/api/2.0
26
27[upay_u1ms]
28consumer_id: U1
29port: {pay.port}
30hostname: {pay.address}
31url_format: http://%(host)s:%(port)d/api/2.0
32
33[url]
34openid_sso_server: {sso.url}
35
36EOF
37# XXX The secrets file is overlayed, so we can't use the config file.
38# This is an ugly way to overwrite the default values.
39cat <<EOF >>configs/dev_secrets-lazr.conf
40ubuntu_pay_username: u1qauser
41ubuntu_pay_password: u1qapassword
42ubuntu_pay_username_u1ms: u1qauser
43ubuntu_pay_password_u1ms: u1qapassword
44
45EOF
46cat <<EOF >servers/u1servers/web/localsettings.py
47from u1servers.web.devsettings import *
48
49OPENID_SSO_SERVER_URL = config.url.openid_sso_server
50OPENID_SSO_LOGOUT_URL = '%s/+logout?return_to=%s' % (
51 OPENID_SSO_SERVER_URL, BASE_URL)
52
53if __name__ == os.environ.get("DJANGO_SETTINGS_MODULE"):
54 # This only gets executed if the configured DJANGO_SETTINGS_MODULE matches
55 # the current module name.
56 from ubuntuone import dispatch
57 dispatch.connect_all(async=True)
58
59 from u1servers.web import email
60 email.connect_receivers()
61
62 # Triggered when the env variable U1_PAY_HOST is defined with
63 # "<hostname>:<port>"
64 if config.upay.hostname or config.upay_u1ms.hostname:
65 from u1backends.account.upayclient import init_payclient
66 init_payclient()
67EOF
68make update-sourcedeps
69# TODO ask on #u1-ops if there's a better way.
70sed -i 's/development-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
71sed -i 's/development-appserver-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
072
=== added file 'setup_vm/u1/run'
--- setup_vm/u1/run 1970-01-01 00:00:00 +0000
+++ setup_vm/u1/run 2013-04-17 01:29:27 +0000
@@ -0,0 +1,4 @@
1#!/bin/sh
2
3cd ~/{u1.src_dir}
4HOSTNAME={u1.address} DJANGO_SETTINGS_MODULE=u1servers.web.localsettings U1CONFIG=`pwd`/configs/local.conf make start
05
=== added file 'setup_vm/u1/test'
--- setup_vm/u1/test 1970-01-01 00:00:00 +0000
+++ setup_vm/u1/test 2013-04-17 01:29:27 +0000
@@ -0,0 +1,15 @@
1#!/bin/sh
2
3
4cd {u1.src_dir}
5# When run from the host against the u1 guest:
6# sudo apt-get install python-mocker
7# scp ubuntu@{u1.address}:~/ubuntuone-servers/configs/local.conf configs/local.conf
8# scp ubuntu@{u1.address}:~/ubuntuone-servers/servers/u1servers/web/localsettings.py servers/u1servers/web/localsettings.py
9# TODO ask on #u1-ops if there's a better way.
10# sed -i 's/development-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
11# sed -i 's/development-appserver-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
12# echo 9999 >tmp/statsd.port
13# make update-sourcedeps
14U1CONFIG=`pwd`/configs/local.conf make smoke-test
15U1CONFIG=`pwd`/configs/local.conf make acceptance-test
016
=== added directory 'setup_vm/unity'
=== added file 'setup_vm/unity/install-sources'
--- setup_vm/unity/install-sources 1970-01-01 00:00:00 +0000
+++ setup_vm/unity/install-sources 2013-04-17 01:29:27 +0000
@@ -0,0 +1,19 @@
1#!/bin/sh
2# Allow ssh access to launchpad.
3# This should probably be provided by setup_vm. -- vila 2013-03-10
4ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
5# Install sst and dependencies from source
6mkdir src
7cd src
8bzr branch lp:~ubuntuone-hackers/ubuntuone-servers/selenium
9bzr branch lp:~canonical-isd-qa/selenium-simple-test/trunk selenium-simple-test
10cd ~/src/selenium
11python setup.py install --user
12cd ~/src/selenium-simple-test
13python setup.py install --user
14
15# If the need arise to use sst-run, it may become necessary to create a
16# symlink to /home/ubuntu/.local/bin/sst-run
17
18# Also note that unittest2 is installed as a side-effect of installing sst
19# even if we're already using pyton-2.7 (which includes unittest2 features).
020
=== added file 'setup_vm/unity/run-sso-client'
--- setup_vm/unity/run-sso-client 1970-01-01 00:00:00 +0000
+++ setup_vm/unity/run-sso-client 2013-04-17 01:29:27 +0000
@@ -0,0 +1,11 @@
1#!/bin/sh -ex
2
3# We can use U1_DEBUG=True to get debug messages on the console.
4USSOC_SERVICE_URL={sso.url}/api/1.0/ /usr/lib/ubuntu-sso-client/ubuntu-sso-login &
5# XXX ugly sleep.
6sleep 5s
7# TODO x86_64 sounds like trouble.
8# This has just stopped working on raring. See http://pad.lv/1161067
9# TODO in order for the application to be accessible with testability, we need
10# TESTABILITY=1
11/usr/lib/ubuntu-sso-client/ubuntu-sso-login-qt --app_name 'Ubuntu One' --help_text '...' --ping_url '{u1.url}/oauth/sso-finished-so-get-tokens/%7Bemail%7D?platform_version=3.8.0-2-generic&platform=Linux&client_version=4.1.90&platform_arch=x86_64' --policy_url {u1.url}/privacy/ --tc_url {u1.url}/terms/
012
=== added file 'setup_vm/unity/run-syncdaemon'
--- setup_vm/unity/run-syncdaemon 1970-01-01 00:00:00 +0000
+++ setup_vm/unity/run-syncdaemon 2013-04-17 01:29:27 +0000
@@ -0,0 +1,4 @@
1#!/bin/sh -ex
2
3/usr/lib/ubuntuone-client/ubuntuone-syncdaemon --disable_ssl_verify --dns_srv=None --host={filesync.address} &
4u1sdtool --connect
05
=== added file 'setup_vm/unity/run-unity-lens-music'
--- setup_vm/unity/run-unity-lens-music 1970-01-01 00:00:00 +0000
+++ setup_vm/unity/run-unity-lens-music 2013-04-17 01:29:27 +0000
@@ -0,0 +1,6 @@
1#!/bin/sh -ex
2
3# TODO x86_64 sounds like trouble.
4# We can use G_MESSAGES_DEBUG=all to get debug messages on the console.
5# TODO change the name of the environment variables, it's not just staging.
6pkill unity-music; U1_STAGING_WEBAPI={u1.url} U1_STAGING_AUTHENTICATION={sso.url} /usr/lib/x86_64-linux-gnu/unity-lens-music/unity-musicstore-daemon
07
=== added file 'setup_vm/unity/transient-dist-upgrade'
--- setup_vm/unity/transient-dist-upgrade 1970-01-01 00:00:00 +0000
+++ setup_vm/unity/transient-dist-upgrade 2013-04-17 01:29:27 +0000
@@ -0,0 +1,4 @@
1#!/bin/sh
2# For an unclear reason (probably a transient raring issue) we need to
3# dist-upgrade instead of just upgrade
4apt-get dist-upgrade -y
05
=== added file 'setup_vm/vms.conf'
--- setup_vm/vms.conf 1970-01-01 00:00:00 +0000
+++ setup_vm/vms.conf 2013-04-17 01:29:27 +0000
@@ -0,0 +1,96 @@
1# This must be defined in some other vms.conf file (user or system)
2# sso.address=sso.local
3# pay.address=pay.local
4# u1.address=u1.local
5# ppa.ubuntuone-hackers.password
6
7sso.src_dir=canonical-identity-provider
8sso.port=8001
9sso.url=http://{sso.address}:{sso.port}
10sso.imap_port=2143
11sso.smtp_port=2025
12
13pay.src_dir=canonical-payment-service
14pay.port=8002
15pay.url=http://{pay.address}:{pay.port}
16
17ppa.ubuntuone_hackers=deb https://{vm.launchpad_id}:{ppa.ubuntuone_hackers.password}@private-ppa.launchpad.net/ubuntuone/hackers/ubuntu {vm.release} main|4BD0ECAE
18
19u1.src_dir=ubuntuone-servers
20u1.port=8003
21u1.url=http://{u1.address}:{u1.port}
22
23[precise-server-pristine]
24vm.name=precise-server-pristine
25vm.release=precise
26vm.packages=bzr, avahi-daemon, emacs23
27vm.update=True
28
29[sso]
30vm.name=sso
31vm.release=precise
32vm.backing=precise-server-pristine.qcow2
33vm.packages=config-manager, fabric, libpq-dev, make, memcached, postgresql-plpython, python-m2crypto, python-dev, python-setuptools, python-virtualenv, swig, wget, libxml2-dev, libxslt1-dev
34vm.ubuntu_script=sso/install
35vm.update=True
36vm.uploaded_scripts=sso/run, sso/run-for-pay, sso/run-for-u1
37
38[pay]
39vm.name=pay
40vm.release=precise
41vm.backing=precise-server-pristine.qcow2
42vm.packages=config-manager, fabric, libpq-dev, make, postgresql-plpython, python-dev, python-setuptools, python-virtualenv, wget, libxml2-dev, libxslt1-dev
43vm.ubuntu_script=pay/install
44vm.update=True
45vm.uploaded_scripts=pay/run, pay/run-for-u1
46
47[u1]
48vm.name=u1
49vm.release=precise
50vm.backing=precise-server-pristine.qcow2
51vm.apt_sources={ppa.ubuntuone_hackers}
52vm.packages=openjdk-7-jre,ubuntuone-developer-dependencies
53vm.ubuntu_script=u1/install
54vm.update=True
55vm.uploaded_scripts=u1/run
56
57[raring-desktop-pristine]
58vm.name=raring-desktop-pristine
59vm.release=raring
60# python-unittest2 is not strictly required here but works around sst
61# insisting on installing it locally.
62vm.packages=bzr, emacs23, python-setuptools, python-unittest2, python-autopilot, unity-autopilot, ubuntu-desktop, avahi-daemon
63vm.update=True
64# Roughly all vms installing ubuntu-desktop need to complete the
65# installation by making the ubuntu user part of the admin group.
66vm.root_script = bin/ubuntu_admin.sh
67
68[purchase-testing]
69vm.name=purchase-testing
70vm.release=raring
71vm.backing=raring-desktop-pristine.qcow2
72vm.apt_sources=deb http://ppa.launchpad.net/ubuntuone/dashpurchase-testing/ubuntu {vm.release} main|4BD0ECAE
73vm.update=True
74vm.ubuntu_script=purchase-testing/install
75
76[unity-prevalidation]
77vm.name=unity-prevalidation
78vm.release=raring
79vm.backing=raring-desktop-pristine.qcow2
80vm.apt_sources=deb http://ppa.launchpad.net/ubuntu-unity/experimental-prevalidation/ubuntu {vm.release} main|52D62F45
81vm.uploaded_scripts=unity/run-sso-client, unity/run-unity-lens-music
82# TODO unity/run-syncdaemon. We don't yet have the hermetic filesync server.
83vm.update=True
84vm.root_script=unity/transient-dist-upgrade
85vm.ubuntu_script=unity/install-sources
86
87[indash-didrocks]
88vm.name=indash-didrocks
89vm.release=raring
90vm.backing=raring-desktop-pristine.qcow2
91vm.apt_sources=ppa:didrocks/ppa
92vm.uploaded_scripts=unity/run-sso-client, unity/run-unity-lens-music
93# TODO unity/run-syncdaemon. We don't yet have the hermetic filesync server.
94vm.update=True
95vm.root_script=unity/transient-dist-upgrade
96vm.ubuntu_script=unity/install-sources

Subscribers

People subscribed via source and target branches

to all changes: