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

Proposed by Vincent Ladeuil
Status: Merged
Merged at revision: 51
Proposed branch: lp:~vila/u1-test-utils/setup-vm
Merge into: lp:u1-test-utils
Diff against target: 3676 lines (+3529/-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 (+51/-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 (+62/-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:~vila/u1-test-utils/setup-vm
Reviewer Review Type Date Requested Status
Canonical ISD hackers Pending
Review via email: mp+158657@code.launchpad.net

Description of the change

This merges my previous work on setup_vm into u1-test-utils.

I went the easy way by putting everything into a subdir named... setup_vm
(how original ;).

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added directory 'setup_vm'
2=== added file 'setup_vm/ISSUES'
3--- setup_vm/ISSUES 1970-01-01 00:00:00 +0000
4+++ setup_vm/ISSUES 2013-04-12 15:44:25 +0000
5@@ -0,0 +1,9 @@
6+* while setting up sso.local .env/db/postgresql.log ends up with:
7+
8+FATAL: role "postgres" does not exist
9+
10+=> This may be "expected" according to mfoord
11+
12+
13+* 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.
14+
15
16=== added file 'setup_vm/NOTES'
17--- setup_vm/NOTES 1970-01-01 00:00:00 +0000
18+++ setup_vm/NOTES 2013-04-12 15:44:25 +0000
19@@ -0,0 +1,259 @@
20+Here we keep notes about the tests we will run on these vms. Once finished all
21+the TODOs, this will be probably moved to moztrap, or immediately and
22+magically automated.
23+
24+== On the host ==
25+1. Install the requirements:
26+ $ sudo apt-get install libvirt-bin qemu virtinst virt-manager
27+
28+2. Install the apt cache:
29+ $ sudo apt-get install squid-deb-proxy
30+
31+3. Enable the cache for launchpad's private ppas:
32+ See point 9 of the README.
33+
34+4. Configure the vms:
35+
36+ # This directory will be used as download cache for the Ubuntu images.
37+ $ mkdir ~/installers/ubuntu
38+ # This directory will store the disk images for the virtual machines.
39+ $ mkdir ~/images
40+ $ editor ~/vms.conf
41+
42+ vm.ram_size=2048
43+ vm.cpus=2
44+ # Tweak the cpu model according to your needs
45+ vm.cpu_model=amd64
46+ vm.download_cache=~/installers/ubuntu
47+ vm.images_dir=~/images
48+ # Tweak according to your squid-deb-proxy setup, 8000 is the default port.
49+ # Use {your-ip} or any address reachable by the vms (keeping in mind that
50+ # avahi's .local domain may not be up in the early stages of the install
51+ vm.apt_proxy = http://{your-ip}:800
52+ vm.launchpad_id=your-launchpad-id
53+ # This is the ssh key of your host machine. Make sure that you have
54+ uploaded it to https://launchpad.net/~/+editsshkeys
55+ vm.ssh_authorized_keys = ~/.ssh/id_rsa.pub
56+ # This is the ssh key for your VMs. It might be safer if it's different from
57+ # your machine's key. Make sure that you have uploaded it to launchpad too.
58+ vm.ssh_keys=~/.ssh/rsa-vms
59+ # A default user (ubuntu) is created, here is its password
60+ vm.password = you-re-on-you-own-use-a-simple-or-complex-password
61+ # Go to https://launchpad.net/~/+archivesubscriptions to get the password for
62+ # the Ubuntu One hackers PPA. Click the View link on the PPA row, and on the
63+ # sources list entries you will see something like
64+ # https://your-launchpad-id:the-password@...
65+ ppa.ubuntuone_hackers.password=the-password
66+
67+ sso.address=sso.local
68+ pay.address=pay.local
69+ u1.address=u1.local
70+
71+5. Get the branch:
72+
73+ $ bzr branch lp:~online-services-qa/u1-test-utils/setup_vm
74+ $ cd setup_vm
75+
76+6. Download the image for the server (do so every time you want to use a
77+ fresh image):
78+
79+ $ ./bin/setup_vm.py precise-server-pristine --download
80+
81+# TODO use LXCs insteal of virtual machines for the servers.
82+7. Install the pristine server vm:
83+
84+ $ ./bin/setup_vm.py raring-pristine --install
85+
86+8. Download the image for the desktop (do so every time you want to use a
87+ fresh image)::
88+
89+ $ ./bin/setup_vm.py raring-desktop-pristine --download
90+
91+8. Install the pristine desktop vm:
92+
93+ $ ./bin/setup_vm.py raring-pristine --install
94+
95+# TODO now it might be better to start all the servers on the same VM.
96+9. Set the SSO server (Only needed for in-dash payments tests):
97+
98+ $ ./bin/setup_vm.py sso --install
99+ $ virsh start sso
100+ $ ssh ubuntu@sso.local ~/bin/run-for-u1
101+
102+10. Set up the Pay server (Only needed for in-dash payments tests):
103+
104+ $ ./bin/setup_vm.py pay --install
105+ $ virsh start pay
106+ $ ssh ubuntu@pay.local ~/bin/run-for-u1
107+
108+11. Set up the U1 server (Only needed for in-dash payments tests):
109+
110+ $ ./bin/setup_vm.py u1 --install
111+ $ virsh start u1
112+ $ ssh ubuntu@u1.local ~/bin/run
113+
114+12. Set up the Filesync server (Only needed for in-dash payments tests):
115+
116+ TODO. See below the notes to set it up on the same u1 server.
117+ TODO. Explore how to set it up in a different server.
118+
119+13. Set up the Music Search server:
120+
121+ TODO. Do we need it?
122+
123+13. Install the CurucĂș server (Only needed for Smart Scopes tests):
124+
125+ TODO.
126+
127+17. Install the desktop machine that will run the tests:
128+
129+ $ ./bin/setup_vm.py unity-prevalidation --install
130+
131+
132+Set up the in-dash payments tests against the local servers
133+===========================================================
134+
135+This is for the happy path.
136+
137+1. Sign in to the unity-prevalidation machine using virt-manager.
138+
139+2. Kill syncdaemon if it is running. (As this is a pristine machine, this is not necessary)
140+
141+3. Open seahorse and delete the Ubuntu One credentials, if present. (As this is a pristine machine, this is not necessary)
142+
143+3. Log in with Staging Ubuntu SSO:
144+
145+ # TODO: Currently we can just connect to production. This is a regression,
146+ # see bug http://pad.lv/1161067
147+ $ ~/bin/run-sso-client
148+
149+ Click the "Log-in with my existing account." link.
150+ Fill the form with:
151+ # TODO we need to create the user with the API helpers on u1-test-utils.
152+ Email address: u1test+local-only@canonical.com
153+ Password: Hola123*
154+ Click the "Sign In" button.
155+
156+ Autopilot test:
157+ http://bazaar.launchpad.net/~elopio/ubuntu-sso-client/autopilot/view/head:/ubuntu_sso/tests/acceptance/test_ubuntu_sso_client.py
158+
159+4. Start syncdaemon:
160+
161+ # TODO we currently don't have a filesync server.
162+ $ ~/bin/run-syncdaemon
163+
164+5. Start the unity musicstore daemon:
165+
166+ $ ~/bin/run-unity-lens-music
167+
168+6. Start the control panel.
169+
170+ # TODO probably not neccessary.
171+
172+7. Add a credit card to the user.
173+
174+ # Use the API helpers on u1-test-utils.
175+ # We still need to log in to the pay webiste first. See http://pad.lv/1144523
176+
177+8. Enable the automatic payments for the user.
178+
179+ # TODO wait for http://ur1.ca/d6z4n to land and then extend the u1-test-utils
180+ # API helpers to do this.
181+
182+# For paypal payments, we would still need to do a lot of stuff on the website.
183+# TODO do we need to test paypal payments?
184+
185+Run the in-dash payments tests against the local servers
186+===========================================================
187+
188+This is the happy path.
189+
190+1. Super+M.
191+2. Search for 'hendrix'.
192+3. Wait for the search to complete.
193+4. Click the first album
194+5. Click the Download button.
195+6. Enter the password.
196+7. Click the Purchase button.
197+
198+All the tests are now documented in moztrap.
199+
200+Set up the filesync server on the same machine as ubuntuone-servers
201+====================================================================
202+
203+ # On the host
204+ $ ssh ubuntu@u1.local
205+ # On the vm.
206+ $ bzr branch lp:ubuntuone-filesync
207+ $ cd ubuntuone-filesync
208+ $ make link-sourcedeps
209+ $ editor lib/u1backends/db/config.py
210+ Change line 10 from:
211+ db_dir = os.path.abspath(os.path.join(get_tmpdir(), 'db1'))
212+ To:
213+ db_dir = os.path.abspath(
214+ os.path.join('/home/ubuntu/ubuntuone-servers/tmp', 'db1'))
215+ $ make start
216+ # Use ubuntuone-servers S4, statsd and AMQP.
217+ $ ln -s ../../ubuntuone-servers/tmp/rabbitmq-ubuntuone.port tmp/rabbitmq-ubuntuone.port
218+ $ ln -s ../../ubuntuone-servers/tmp/statsd.port tmp/statsd.port
219+ $ ln -s ../../ubuntuone-servers/tmp/s4.port tmp/s4.port
220+ $ make start-supervisor start-filesync-dummy-group
221+ # TODO we can also start-filesync-oauth-group. Ask #u1-di.
222+
223+Set up the curucu server
224+========================
225+
226+ # There is a dummy website that accesses the server:
227+ # https://productsearch.ubuntu.com/smartscopes/v1/dashmock?geo_store=US
228+ # On ~pedronis/curucu/canonistack-deploy, there's a README with the
229+ # instructios to deploy curucu on a canonistack machine with Juju.
230+ # TODO try to deploy it with juju on our vms.
231+ # TODO we need an amazon key.
232+ # On the host
233+ $ ./bin/setup_vm.py precise-curucu-server --install
234+ $ virsh start precise-curucu-server
235+ $ ssh ubuntu@precise-curucu-server
236+ # On the vm.
237+ # TODO install the dependencies, what are the dependencies?
238+ $ bzr branch lp:curucu
239+ $ cd curucu
240+ $ editor try.cfg
241+
242+ [amazon]
243+ key = amazon key
244+ secret = amazon secret
245+
246+ [u1ms]
247+ service_url = http://musicsearch.ubuntu.com/v1/
248+
249+ [feedback_store]
250+ interval = 4
251+ # when set to empty storing feedback is disabled
252+ store_directory = /tmp/feedback
253+
254+ $ 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
255+
256+Mounting guest disk images on the host
257+======================================
258+
259+This requires root access (what did you expect ;-p) and the current
260+directory should contain the vm disk images.
261+
262+apt-get install qemu-nbd
263+
264+root@saw:/# modprobe nbd # once
265+root@saw:/# mkdir /mnt/disk1
266+root@saw:/# mkdir /mnt/seed
267+
268+root@saw:/# qemu-nbd -c /dev/nbd0 raring-pristine.qcow2
269+root@saw:/# mount /dev/nbd0p1 /mnt/disk1
270+root@saw:/# umount /mnt/disk1/
271+root@saw:/# qemu-nbd -d /dev/nbd0
272+
273+root@saw:/# qemu-nbd -c /dev/nbd1 raring-test.seed
274+root@saw:/# mount /dev/nbd1 /mnt/seed
275+mount: warning: /mnt/seed seems to be mounted read-only.
276+root@saw:/# umount /mnt/seed
277+root@saw:/# qemu-nbd -d /dev/nbd1
278+/dev/nbd1 disconnected
279
280=== added file 'setup_vm/README'
281--- setup_vm/README 1970-01-01 00:00:00 +0000
282+++ setup_vm/README 2013-04-12 15:44:25 +0000
283@@ -0,0 +1,241 @@
284+Getting started:
285+================
286+
287+1. Install the dependencies:
288+
289+ sudo apt-get install bzr python-testtools python-yaml
290+ sudp apt-get install libvirt-bin qemu qemu-utils virtinst
291+ sudo apt-get install qemu-kvm-spice python-spice-client-gtk
292+
293+ (Optional):
294+ To use a gui manager to see the desktop:
295+ sudo apt-get install virt-manager
296+
297+ To use Apt proxy to speed up multiple downloads of the same packages:
298+ sudo apt-get install squid-deb-proxy
299+ (See point 9 about configuring the apt cache.)
300+
301+2. Reboot to allow kvm to activate on the running kernel.
302+
303+3. Get the code:
304+
305+ bzr branch lp:~online-services-qa/u1-test-utils/setup_vm
306+
307+4. Run the tests:
308+
309+ cd setup_vm
310+ ./selftest.py
311+
312+ (The test_install_from_seed will require you to enter your password
313+ because it executes a command with sudo. Your user must be a sudoer.)
314+
315+5. Configure a virtual machine:
316+
317+ Write the file ~/vms.conf with something like this:
318+
319+ vm.ram_size=2048
320+ vm.cpus=2
321+ vm.cpu_model=amd64
322+
323+ [raring-pristine]
324+ vm.name = raring-pristine
325+ vm.update = True
326+ vm.packages = bzr, ubuntu-desktop, avahi-daemon
327+ vm.release=raring
328+ vm.ssh_authorized_keys = {your SSH public key path (~ is allowed, eg ~/.ssh/id_rsa.pub)}
329+
330+ Create the ~/.config/setup_vm directory where cloud-init configuration
331+ files will be stored for each vm. Alternatively, you can create a
332+ directory (~/vms) where you want and add the following line in ~/vms.conf:
333+
334+ vm.vms_dir=~/vms
335+
336+ Optionally, you can setup scripts to be executed as root or as the
337+ ubuntu user just before the vm is powered off:
338+
339+ vm.root_script = {path to a script on the host}
340+ vm.ubuntu_script = {path to a script on the host}
341+
342+ These scripts *must* specify a shebang line and can be written in any
343+ language that can be run from a shebang line.
344+
345+ You can also ask for some local scripts to be uploaded with:
346+
347+ vm.uploaded_scripts = sso/run, sso/run-for-pay
348+
349+ The config options in these scripts will be expanded before the upload.
350+
351+ PPAS needs a bit of care to setup, as an example, the unity experimental
352+ prevalidation PPA is configured by going to
353+ https://launchpad.net/~ubuntu-unity/+archive/experimental-prevalidation
354+ Click the "Technical details about this PPA" link.
355+ Select the distribution from the combo box.
356+ Copy the apt line below:
357+
358+ vm.apt_sources=deb http://ppa.launchpad.net/ubuntu-unity/experimental-prevalidation/ubuntu {vm.release} main|52D62F45
359+
360+ The page displays Signing Key: 1024R/52D62F45. Please note that only
361+ 52D62F45 should be specified, and that the url and the key are separated
362+ by '|' with no intervening spaces.
363+
364+ For a private PPA, make sure to include your launchpad id and your
365+ password for that PPA in the URL. It would look something like this:
366+
367+ vm.apt_sources = deb https://<lp id>:<ppa password>@private-ppa.launchpad.net/a-user/ppa-name/ubuntu {vm.release} main|<ppa key>
368+
369+6. (Optional) Create a system-wide vms.conf.
370+
371+ In some cases, some options are better defined in a system-wide config
372+ file (/etc/libvirt/vms.conf). This file is queried if no definitions are
373+ found in ~/vms.conf and can define a no-name section and vm sections.
374+
375+7. (Optional) You can configure the location where the image will be
376+ downloaded with something like this in the vms.conf file:
377+
378+ vm.download_cache=~/installers/ubuntu
379+
380+8. (Optional) You can configure the location where the virtual machines will
381+ be stored with something like this in the vms.conf file:
382+
383+ vm.images_dir=~/images
384+
385+9. (Optional) Set up an apt cache, so repeated virtual machine installs will
386+ be faster, downloading the packages from the cache instead of an Ubuntu
387+ archive mirror:
388+
389+ Add this to the vms.conf file:
390+
391+ vm.apt_proxy = http://{your-squid-deb-proxy-ip}:8000
392+
393+ If you need to install packages from non official Ubuntu repositories, you
394+ will need to configure the proxy. For example, common tasks would require
395+ to access Launchpad public and private PPAs. For that, write the file
396+ /etc/squid-deb-proxy/mirror-dstdomain.acl.d/20-local-vms with:
397+
398+ # /etc/squid-deb-proxy/mirror-dstdomain.acl.d/20-local-vms
399+
400+ # network destinations that are allowed by this cache targeted at
401+ # locally installed vms
402+
403+ # launchpad personal package archives
404+ ppa.launchpad.net
405+ # launchpad private personal package archives
406+ private-ppa.launchpad.net
407+
408+ After that, restart the proxy:
409+
410+ sudo restart squid-deb-proxy
411+
412+ Each time you modify some file under /etc/squid-deb-proxy, don't forget to
413+ restart the service.
414+
415+10. Download the image:
416+
417+ ./bin/setup_vm.py --download raring-pristine
418+
419+ (This command will require you to enter your password because the
420+ directory where the image will be downloaded might be under control of
421+ the root user. Your user must be a sudoer. A pending task is to ask for
422+ the password just when needed.)
423+
424+11. Install the virtual machine:
425+
426+ ./bin/setup_vm.py --install raring-pristine
427+
428+ (This command will require you to enter your password because some of the
429+ operations it executes require root access. Your user must be a sudoer.)
430+
431+12. You can ssh into the virtual machine:
432+
433+ virsh start raring-pristine
434+ ssh ubuntu@raring-pristine.local
435+
436+ No password is needed because your SSH public key is authorized.
437+
438+13. You can run the virtual machine from virt-manager to get a graphical user
439+ interface:
440+
441+ Open virt-manager.
442+ Right-click on the machine and select Run.
443+ You will be presented with the display manager greeter.
444+ Log in with the user ubuntu, password ubuntu.
445+
446+ (You may need to do the following if virt-manager says it can't connect,
447+ sudo usermod -a -G libvirtd $USER (replace $USER with your username)
448+ and reboot the system)
449+
450+14. (Optional) You can set up a "throw away" virtual machine on top of
451+ another. We call it "throw away" because all modifications happening there
452+ won't affect the disk image of the backing on virtual machine.
453+
454+ In the vms.conf described above add:
455+
456+ [raring-test]
457+ vm.name = raring-test
458+ vm.update = False
459+ vm.release = raring
460+ vm.ssh_authorized_keys = {your SSH public key}
461+ # The name of the disk image used as a base
462+ vm.backing = raring-pristine.qcow2
463+
464+ Create the new vm with:
465+
466+ ./bin/setup_vm.py --install raring-test
467+
468+ The vm creation and boot should be faster.
469+
470+15. (Very optional) A few commands for virsh that may be of use:
471+ virsh list (shows what is running)
472+ virsh start x (start a vm with the name x)
473+ virsh destroy x (force shutdowns a vm with the name x)
474+ virsh undefine x [--remove-all-storage] (Deletes a vm with the name x)
475+
476+16. (Optional) Raise sudo timeout.
477+
478+ If you run into vm installs taking too long and waiting for your
479+ password to fisnish, you can change the default value (15 minutes) by
480+ adding a file in /etc/sudoers.d containing:
481+
482+ Defaults:<your login here> timestamp_timeout=60
483+
484+ This will setup the timeout to 60 minutes.
485+
486+17. (Optional) Setup launchpad access for the guests
487+
488+ If you need to access launchpad private branches from the guests, you'll
489+ need to setup ssh launchpad access (if you only need access to public
490+ branches, http is good enough and you don't even need to 'bzr
491+ launchpad-login'):
492+
493+ - You need to create an ssh key dedicated to the guests, it has to be
494+ passwordless and the public part uploaded to your launchpad profile.
495+ You can generate a new key pair (replacing <user> with your launchpad
496+ id) with:
497+
498+ $ (cd ~/.ssh ; ssh-keygen -f <user>@setup_vm -N '' -C '<user>@setup_vm')
499+
500+ This will create two files: '<user>@setup_vm' and
501+ '<user>@setup_vm.pub' in your ~/.ssh directory.
502+
503+ Upload the later at https://launchpad.net/~/+editsshkeys
504+
505+ The keys are created in your .ssh directory so you can test that they
506+ work against launchpad without involving a vm.
507+
508+ Note that if you create vms from different hosts, you'll need to either
509+ copy the same keys on all the hosts or create a pair on each of them
510+ (or any combination as long as the public keys are uploaded to
511+ launchpad ;).
512+
513+ - You need to set vm.launchpad_id to <user>. This will trigger running
514+ 'bzr launchpad-login <user>' in the guest and copy
515+ ~/.ssh/<user>@setup_vm (the private key) from your host to the guest.
516+
517+ - The bazaar.launchpad.net host ssh key needs to be known or you'll get
518+ prompted to add it (which is not nice for scripts). This can be fixed
519+ by issuing the following command from the {vm.ubuntu_script}:
520+
521+ $ ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
522+
523+ This will probably be automated at some point in the future.
524+
525
526=== added file 'setup_vm/TODO'
527--- setup_vm/TODO 1970-01-01 00:00:00 +0000
528+++ setup_vm/TODO 2013-04-12 15:44:25 +0000
529@@ -0,0 +1,129 @@
530+* running setup_vm --install I-dont-exist raises an obscure error. Checking
531+ that the config section exist for the vm would allow reporting a better
532+ error.
533+
534+* Too many tests become too hard to write because their execution requires a
535+ real vm. This can be addressed in much the same way than
536+ requires_known_reference_image(), i.e. setup a real vm once (outside of
537+ selftest execution for now) and then use throw-away vms. But even that
538+ may be too costly and may need to wait for lxc/chroot support.
539+
540+* running --ssh-keygen twice gives an awful error message.
541+
542+* Find whether or not we should really support chroots or if lxcs are good
543+ enough (roughly: if they can be set up as fast as chroots but provide
544+ more features, just optimize the backing-on scenario).
545+
546+* Investigate btrfs support to use snapshots for nested backing.
547+ This may not be appropriate with kvms but will surely shine for
548+ lxc/chroot.
549+
550+* copying a file (with expanded options or not) from the host to the guest
551+ is hard (even internally). There should be a way to more simply describe
552+ a list of file/directories to install (with user:group and chmod bits).
553+
554+* Alternatively, we can allow the guest to access the host via ssh (after
555+ all, we're installing a private key in the guest so we trust it enough
556+ for that already).
557+
558+* As a first step, we can define vm.scripts as a list of relative paths on
559+ the host, that will be option expanded into vm.config_dir and uploaded
560+ from there into ~ubuntu/bin.
561+
562+* Provide the guest with config file containing the values used to build
563+ this vm. From there, the guest itself would be able to expanded options
564+ in files acquired from the host (including files modified after the vm
565+ has been built/started which will help during dev/debug).
566+
567+* vm.ubuntu_script is kind of an implementation leak from cloud-init, that's
568+ the default user there and comes with some nice properties but strictly
569+ speaking setup_vm cares about having *a* user, no matter how it is named
570+ so the option could be named vm.user_script. In any case, the features we
571+ rely on from using ubuntu should be tested if only to document them.
572+
573+* we need a way to run scripts on the host while expanding the config option
574+ for a given vm (see sso/test that needs at least the sso.url).
575+
576+* launchpad interaction requires the launchpad host key.
577+
578+ => ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts does the
579+ trick but it would be nice to automate it.
580+
581+* document the /etc/avahi/ fix required to use avahi with vms
582+
583+* lag times between significant hosts should be collected (not specific to
584+ setup_vm but related to the use of the vms).
585+
586+* Add a way to display a vm configuration Ă  la 'bzr config'
587+
588+* from the addresses below, find a way to test if some fixed subspace can be
589+ safely used (de:ad:be:ef or something...) or just steal some unused MAC
590+ prefix (vbox's one ? or vmware's one ? or... the sky is the limit ;)
591+
592+
593+$ sudo grep -n 'mac address' /etc/libvirt/qemu/*.xml
594+/etc/libvirt/qemu/essex-precise.xml:45: <mac address='52:54:00:26:3c:20'/>
595+/etc/libvirt/qemu/freebsd8.xml:39: <mac address='08:00:27:5f:9f:06'/>
596+/etc/libvirt/qemu/gentoo.xml:39: <mac address='08:00:27:da:65:cd'/>
597+/etc/libvirt/qemu/indicator-sync.xml:45: <mac address='52:54:00:43:15:9c'/>
598+/etc/libvirt/qemu/pkgimporter-lucid.xml:45: <mac address='52:54:00:95:4e:dc'/>
599+/etc/libvirt/qemu/precise-cloud.xml:48: <mac address='52:54:00:68:aa:af'/>
600+/etc/libvirt/qemu/precise-pristine.xml:48: <mac address='52:54:00:d5:52:06'/>
601+/etc/libvirt/qemu/precise-pristine.xml:48: <mac address='52:54:00:70:a3:64'/>
602+/etc/libvirt/qemu/precise-server-pristine.xml:51: <mac address='52:54:00:14:d5:be'/>
603+/etc/libvirt/qemu/precise-test.xml:48: <mac address='52:54:00:25:8b:56'/>
604+/etc/libvirt/qemu/quantal-cloud.xml:48: <mac address='52:54:00:e6:6a:df'/>
605+/etc/libvirt/qemu/quantal-pristine.xml:48: <mac address='52:54:00:f5:95:0e'/>
606+/etc/libvirt/qemu/quantal-pristine.xml:48: <mac address='52:54:00:b9:68:11'/>
607+/etc/libvirt/qemu/quantal-test.xml:48: <mac address='52:54:00:21:3b:7b'/>
608+/etc/libvirt/qemu/raring-current.xml <mac address='52:54:00:d9:ca:70'/>
609+/etc/libvirt/qemu/raring-in-dash-pristine.xml:51: <mac address='52:54:00:c7:f9:ee'/>
610+/etc/libvirt/qemu/raring-in-dash-test.xml:51: <mac address='52:54:00:a5:83:b9'/>
611+/etc/libvirt/qemu/raring-pristine.xml:48: <mac address='52:54:00:07:09:cb'/>
612+/etc/libvirt/qemu/raring-pristine.xml:48: <mac address='52:54:00:34:7f:62'/>
613+/etc/libvirt/qemu/raring-scope-base.xml:42: <mac address='52:54:00:9c:0d:1c'/>
614+/etc/libvirt/qemu/raring-scope-test.xml:42: <mac address='52:54:00:8a:b1:d6'/>
615+/etc/libvirt/qemu/raring-test.xml:48: <mac address='52:54:00:23:b6:8d'/>
616+/etc/libvirt/qemu/raring-test.xml:51: <mac address='52:54:00:e3:05:db'/>
617+/etc/libvirt/qemu/sso.xml:51: <mac address='52:54:00:f1:88:84'/>
618+/etc/libvirt/qemu/u1test-quantal.xml:48: <mac address='52:54:00:04:7b:45'/>
619+/etc/libvirt/qemu/u1test-quantal.xml:48: <mac address='52:54:00:04:7b:45'/>
620+/etc/libvirt/qemu/u1test2-quantal.xml:45: <mac address='52:54:00:df:35:6c'/>
621+/etc/libvirt/qemu/u1test2-quantal.xml:45: <mac address='52:54:00:df:35:6c'/>
622+/etc/libvirt/qemu/xp-32bits.xml:58: <mac address='52:54:00:86:aa:99'/>
623+/etc/libvirt/qemu/xp64bits.xml:46: <mac address='52:54:00:0a:5e:7c'/>
624+
625+* while using fixed IP addresses is one way to address known_hosts
626+ stability, another way is to rely on `ssh-keygen -R` when installing the
627+ host (to remove the previous mapping between the key and the ip) and then
628+ add the new key with ssh-keyscan (or something else that doesn't require
629+ the guest to be up and running).
630+
631+* make actions verbose and obey -q (at least for tests) so we get some
632+ feeback about what is executed.
633+
634+* add a --delete action to make sure we clean up the config_dir
635+
636+* rework FileMonitor, ConsoleMonitor design, the actual result smells (pass
637+ in _wait_for_install_with_seed, really ?).
638+
639+* investigate using an upstart job like MAAS:
640+
641+ write_poweroff_job() {
642+ cat >/etc/init/maas-poweroff.conf <<EOF
643+ description "poweroff when maas task is done"
644+ start on stopped cloud-final
645+ console output
646+ task
647+ script
648+ [ ! -e /tmp/block-poweroff ] || exit 0
649+ poweroff
650+ end script
651+EOF
652+ # reload required due to lack of inotify in overlayfs (LP: #882147)
653+ initctl reload-configuration
654+}
655+
656+* look at PXE, interesting read may include:
657+
658+ http://ubuntuforums.org/archive/index.php/t-1713845.html
659
660=== added directory 'setup_vm/bin'
661=== added file 'setup_vm/bin/__init__.py'
662=== added file 'setup_vm/bin/setup_vm.py'
663--- setup_vm/bin/setup_vm.py 1970-01-01 00:00:00 +0000
664+++ setup_vm/bin/setup_vm.py 2013-04-12 15:44:25 +0000
665@@ -0,0 +1,1203 @@
666+#!/usr/bin/env python
667+"""
668+Setup a virtual machine from a config file.
669+
670+Note: Most of the operations requires root access and this script uses ``sudo``
671+to get them.
672+
673+"""
674+import argparse
675+import base64
676+from cStringIO import StringIO
677+import errno
678+import os
679+import subprocess
680+import sys
681+import tempfile
682+import time
683+
684+
685+from bzrlib import (
686+ config,
687+ osutils,
688+ transport,
689+ urlutils,
690+ )
691+import yaml
692+
693+# Work around a bug in bzrlib.config forbidding some constructs in templates.
694+# Namely, spaces are invalid as an identifier and therefore should not match
695+# below.
696+config._option_ref_re = config.lazy_regex.lazy_compile('({[^ {},\n]+})')
697+
698+
699+class VmMatcher(config.NameMatcher):
700+
701+ def match(self, section):
702+ if section.id is None:
703+ # The no name section contains default values
704+ return True
705+ return super(VmMatcher, self).match(section)
706+
707+ def get_sections(self):
708+ matching_sections = super(VmMatcher, self).get_sections()
709+ return reversed(list(matching_sections))
710+
711+
712+class VmStore(config.LockableIniFileStore):
713+ """A config store for options specific to a directory."""
714+
715+ def __init__(self, directory, file_name, possible_transports=None):
716+ t = transport.get_transport_from_path(
717+ directory, possible_transports=possible_transports)
718+ super(VmStore, self).__init__(t, file_name)
719+ self.id = 'vm'
720+
721+
722+def system_config_dir():
723+ return '/etc/libvirt'
724+
725+
726+class VmStack(config.Stack):
727+ """Per-directory options."""
728+
729+ def __init__(self, name):
730+ """Make a new stack for a given vm.
731+
732+ The following sections are queried:
733+
734+ * the ``name`` section in ./vms.conf,
735+ * the no-name section in ./vms.conf
736+ * the ``name`` section in ~/vms.conf,
737+ * the no-name section in ~/vms.conf
738+ * the ``name`` section in /etc/libvirt/vms.conf,
739+ * the no-name section in /etc/libvirt/vms.conf
740+
741+ :param name: The name of a virtual machine.
742+ """
743+ self.local_store = VmStore('.', 'vms.conf')
744+ user_store = VmStore(os.environ['HOME'], 'vms.conf')
745+ self.system_store = VmStore(system_config_dir(), 'vms.conf')
746+ # FIXME: Only available in bzr-2.6b3 :-/ -- vila 2012-01-31
747+ # dstore = self.get_shared_store()
748+ super(VmStack, self).__init__(
749+ [VmMatcher(self.local_store, name).get_sections,
750+ VmMatcher(user_store, name).get_sections,
751+ VmMatcher(self.system_store, name).get_sections,
752+ ],
753+ user_store, mutable_section_id=name)
754+
755+
756+def path_from_unicode(path_string):
757+ if not isinstance(path_string, basestring):
758+ raise TypeError
759+ return os.path.expanduser(path_string)
760+
761+
762+class PathOption(config.Option):
763+
764+ def __init__(self, *args, **kwargs):
765+ """A path option definition.
766+
767+ This possibly expands the user home directory.
768+ """
769+ super(PathOption, self).__init__(
770+ *args, from_unicode=path_from_unicode, **kwargs)
771+
772+
773+def register(option):
774+ config.option_registry.register(option)
775+
776+
777+register(config.Option(
778+ 'vm', default=None,
779+ help='''The name space defining a virtual machine.
780+
781+This option is a place holder to document the options that defines a virtual
782+machine and the options defining the infrastructure used to manage them all.
783+
784+For qemu based vms, the definition of a vm is stored in an xml file under
785+'/etc/libvirt/qemu/{vm.name}.xml'. This is under the libvirt package control
786+and is out of scope for setup_vm.py.
787+
788+There are 3 other significant files used for a given vm:
789+
790+- a disk image mounted at '/' from '/dev/sda1':
791+ '{vm.images_dir}/{vm.name}.qcow2'
792+
793+- a iso image available from '/dev/sdb' labeled 'cidata':
794+ {vm.images_dir}/{vm.name}.seed which contains the cloud-init data used to
795+ configure/install/update the vm.
796+
797+- a console: {vm.images_dir}/{vm.name}.console which can be 'tail -f'ed from
798+ the host.
799+
800+The data used to create the seed above are stored in a vm specific
801+configuration directory for easier debug and reference:
802+- {vm.config_dir}/user-data
803+- {vm.config_dir}/meta-data
804+- {vm.config_dir}/ecdsa
805+- {vm.config_dir}/ecdsa.pub
806+'''))
807+
808+# The directory where we store vm files related to their configuration with
809+# cloud-init (user-data, meta-data, ssh keys).
810+register(config.Option(
811+ 'vm.vms_dir', default='~/.config/setup_vm',
812+ help='''Where vm related config files are stored.
813+
814+This includes user-data and meta-data for cloud-init and ssh server keys.
815+
816+This directory must exist.
817+
818+Each vm get a specific directory (automatically created) there based on its
819+name.
820+'''))
821+# The base directories where vms are stored for kvm
822+register(PathOption(
823+ 'vm.images_dir', default='/var/lib/libvirt/images',
824+ help="Where vm disk images are stored.",
825+ ))
826+register(config.Option(
827+ 'vm.qemu_etc_dir',
828+ default='/etc/libvirt/qemu',
829+ help="Where libvirt (qemu) stores the vms config files."
830+ ))
831+
832+# Isos and images download handling
833+register(config.Option(
834+ 'vm.iso_url',
835+ default='http://cdimage.ubuntu.com/daily-live/current/' ,
836+ help="Where an iso can be downloaded from."
837+ ))
838+register(config.Option(
839+ 'vm.iso_name',
840+ default='{vm.release}-desktop-{vm.cpu_model}.iso',
841+ help="The name of the iso."
842+ ))
843+register(config.Option(
844+ 'vm.cloud_image_url',
845+ default='http://cloud-images.ubuntu.com/{vm.release}/current/',
846+ help="Where a cloud image can be downloaded from."
847+ ))
848+register(config.Option(
849+ 'vm.cloud_image_name',
850+ default='{vm.release}-server-cloudimg-{vm.cpu_model}-disk1.img',
851+ help="The name of the cloud image."
852+ ))
853+register(PathOption(
854+ 'vm.download_cache',
855+ default='{vm.images_dir}',
856+ help="Where downloads end up.",
857+ ))
858+
859+# The ubiquitous vm name
860+register(config.Option(
861+ 'vm.name', default=None, invalid='error',
862+ help="The vm name, used as a prefix for related files."
863+ ))
864+# The second most important bit to define a vm: which ubuntu release ?
865+register(config.Option(
866+ 'vm.release', default=None, invalid='error',
867+ help="The ubuntu release name."
868+ ))
869+# The third important piece to define a vm: where to store files like the
870+# console, the user-data and meta-data files, the ssh server keys, etc.
871+register(config.Option(
872+ 'vm.config_dir', default='{vm.vms_dir}/{vm.name}',
873+ invalid='error',
874+ help='''The directory where files specific to a vm are stored.
875+
876+This includes the user-data and meta-data files used at install time (for
877+reference and easier debug) as well as the optional ssh server keys.
878+
879+By default this is {vm.vms_dir}/{vm.name}. You can put it somewhere else by
880+redifining it as long as it ends up being unique for the vm.
881+
882+{vm.vms_dir}/{vm.release}/{vm.name} may better suit your taste for example.
883+'''
884+ ))
885+# The options defining the vm physical characteristics
886+register(config.Option(
887+ 'vm.ram_size', default='1024',
888+ help="The ram size in megabytes."
889+ ))
890+register(config.Option(
891+ 'vm.disk_size', default='8G',
892+ help='''The disk image size in bytes.
893+
894+Optional suffixes "k" or "K" (kilobyte, 1024) "M" (megabyte, 1024k) "G"
895+(gigabyte, 1024M) and T (terabyte, 1024G) are supported.
896+'''))
897+register(config.Option(
898+ 'vm.cpus', default='1',
899+ help="The number of cpus."
900+ ))
901+register(config.Option(
902+ 'vm.cpu_model', default=None, invalid='error',
903+ help="The number of cpus."))
904+register(config.Option(
905+ 'vm.network', default='network=default', invalid='error',
906+ help="""The --network parameter for virt-install.
907+
908+This can be specialized for each machine but the default should work in most
909+setups. Watch for your DHCP server exhausting its address space if you create a
910+lot of vms with random MAC addresses.
911+"""))
912+
913+register(config.Option(
914+ 'vm.meta_data', default='''\
915+instance-id: {vm.name}
916+local-hostname: {vm.name}
917+''',
918+ invalid='error',
919+ help="The meta data for cloud-init to put in the seed."
920+ ))
921+
922+# Some bits that may added to user-data but are optional
923+
924+register(config.ListOption(
925+ 'vm.packages', default=None,
926+ help='''A list of package names to be installed.
927+'''))
928+register(config.Option(
929+ 'vm.apt_proxy', default=None, invalid='error',
930+ help='''A local proxy for apt to avoid repeated .deb downloads.
931+
932+Example:
933+
934+ vm.apt_proxy = http://192.168.0.42:8000
935+
936+'''))
937+register(config.ListOption(
938+ 'vm.apt_sources', default=None,
939+ help='''A list of apt sources entries to be added to the default ones.
940+
941+Cloud-init already setup /etc/apt/sources.list with appropriate entries. Only
942+additional entries need to be specified here.
943+'''))
944+register(config.ListOption(
945+ 'vm.ssh_authorized_keys', default=None,
946+ help='A list of paths to public ssh keys to be authorized for'
947+ ' the default user.'))
948+register(config.ListOption(
949+ 'vm.ssh_keys', default=None,
950+ help='''A list of paths to server ssh keys.
951+
952+Both public and private keys can be provided. Accepted ssh key types are rsa,
953+dsa and ecdsa. The file names should match <type>.*[.pub].
954+'''))
955+register(config.Option(
956+ 'vm.update', default=False,
957+ from_unicode=config.bool_from_store,
958+ help='''Whether or not the vm should be updated.
959+Both apt-get update and apt-get upgrade are called if this option is set.
960+'''))
961+register(config.Option(
962+ 'vm.password', default='ubuntu', invalid='error',
963+ help="The ubuntu user password."
964+ ))
965+register(config.Option(
966+ 'vm.launchpad_id',
967+ help="The launchpad login used for launchpad ssh access from the guest."
968+ ))
969+# The scripts that are executed before powering off
970+register(PathOption(
971+ 'vm.root_script', default=None,
972+ help='''The path to a script executed as root before powering off.
973+
974+This script is executed before {vm.ubuntu_script}.
975+'''
976+ ))
977+register(PathOption(
978+ 'vm.ubuntu_script', default=None,
979+ help='''The path to a script executed as ubuntu before powering off.
980+
981+This script is excuted after {vm.root_script}.
982+'''))
983+register(config.ListOption(
984+ 'vm.uploaded_scripts', default=None,
985+ help='''A list of scripts to be uploaded to the guest.
986+
987+Scripts can use config options from their vm, they will be expanded before
988+upload. All scripts are uploaded into {vm.uploaded_scripts.guest_dir} under
989+their base name.
990+'''))
991+register(config.Option(
992+ 'vm.uploaded_scripts.guest_dir', default='~ubuntu/bin',
993+ help='''Where {vm.uploaded_scripts} are uploaded on the guest.'''
994+ ))
995+
996+
997+class SetupVmError(Exception):
998+
999+ msg = 'setup_vm Generic Error: %r'
1000+
1001+ def __init__(self, msg=None, **kwds):
1002+ if msg is not None:
1003+ self.msg = msg
1004+ for key, value in kwds.items():
1005+ setattr(self, key, value)
1006+
1007+ def __str__(self):
1008+ return self.msg.format((), **self.__dict__)
1009+
1010+ __repr__ = __str__
1011+
1012+
1013+class CommandError(SetupVmError):
1014+
1015+ msg = '''
1016+ command: {joined_cmd}
1017+ retcode: {retcode}
1018+ output: {out}
1019+ error: {err}
1020+'''
1021+
1022+ def __init__(self, cmd, retcode, out, err):
1023+ super(CommandError, self).__init__(joined_cmd=' '.join(cmd),
1024+ retcode=retcode, err=err, out=out)
1025+
1026+class ConfigValueError(SetupVmError):
1027+
1028+ msg = 'Bad value "{value}" for option "{name}".'
1029+
1030+ def __init__(self, name, value):
1031+ super(ConfigValueError, self).__init__(name=name, value=value)
1032+
1033+
1034+class ConfigPathNotFound(SetupVmError):
1035+
1036+ msg = 'No such file: {path} from {name}'
1037+
1038+ def __init__(self, path, name):
1039+ super(ConfigPathNotFound, self).__init__(path=path, name=name)
1040+
1041+
1042+def run_subprocess(args):
1043+ proc = subprocess.Popen(args,
1044+ stdout=subprocess.PIPE,
1045+ stderr=subprocess.PIPE,
1046+ stdin=subprocess.PIPE)
1047+ out, err = proc.communicate()
1048+ if proc.returncode:
1049+ raise CommandError(args, proc.returncode, out, err)
1050+ return proc.returncode, out, err
1051+
1052+
1053+def pipe_subprocess(args):
1054+ proc = subprocess.Popen(args,
1055+ stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
1056+ return proc
1057+
1058+def ssh_infos_from_path(key_path):
1059+ """Analyze path to find ssh key type and kind.
1060+
1061+ The basename should begin with ssh type used to create the key. and end
1062+ with '.pub' for a public key.
1063+
1064+ If the type is neither of rds, dsa or ecdsa, None if returned.
1065+
1066+ :param key_path: A path to an ssh key.
1067+
1068+ :return: (type, kind) 'type' is the ssh key type or None if neither of rds,
1069+ dsa or ecdsa. 'kind' is 'public' if the path ends with '.pub',
1070+ 'private' otherwise.
1071+ """
1072+ base = os.path.basename(key_path)
1073+ for p in ('rsa', 'dsa', 'ecdsa'):
1074+ if base.startswith(p):
1075+ ssh_type = p
1076+ break
1077+ else:
1078+ ssh_type = None
1079+ if base.endswith('.pub'):
1080+ kind = 'public'
1081+ else:
1082+ kind = 'private'
1083+ return ssh_type, kind
1084+
1085+
1086+class ConsoleEOFError(SetupVmError):
1087+
1088+ msg = 'Encountered EOF on console, something went wrong'
1089+
1090+
1091+class CloudInitError(SetupVmError):
1092+
1093+ msg = 'cloud-init reported: {line} check your config'
1094+
1095+ def __init__(self, line):
1096+ super(CloudInitError, self).__init__(line=line)
1097+
1098+
1099+class ConsoleMonitor(object):
1100+ """Monitor a console to identify known events."""
1101+
1102+ def __init__(self, stream):
1103+ super(ConsoleMonitor, self).__init__()
1104+ self.stream = stream
1105+
1106+
1107+ def parse(self):
1108+ while True:
1109+ line = self.stream.readline()
1110+ yield line
1111+ if not line:
1112+ raise ConsoleEOFError()
1113+ elif line.startswith(' * Will now halt'):
1114+ # That's our final_message, we're done
1115+ return
1116+ elif ('Failed loading yaml blob' in line
1117+ or 'Unhandled non-multipart userdata starting' in line
1118+ or 'failed to render string to stdout:' in line
1119+ or 'Failed loading of cloud config' in line):
1120+ raise CloudInitError(line)
1121+
1122+
1123+class FileMonitor(ConsoleMonitor):
1124+
1125+ def __init__(self, path):
1126+ cmd = ['tail', '-f', path]
1127+ proc = pipe_subprocess(cmd)
1128+ super(FileMonitor, self).__init__(proc.stdout)
1129+ self.path = path
1130+ self.cmd = cmd
1131+ self.proc = proc
1132+ self.lines = []
1133+
1134+ def parse(self):
1135+ try:
1136+ for line in super(FileMonitor, self).parse():
1137+ self.lines.append(line)
1138+ yield line
1139+ finally:
1140+ self.proc.terminate()
1141+
1142+
1143+class CIUserData(object):
1144+ """Maps configuration data into cloud-init user-data.
1145+
1146+ This is a container for the data that will ultimately be encoded into a
1147+ cloud-config-archive user-data file.
1148+ """
1149+
1150+ def __init__(self, conf):
1151+ super(CIUserData, self).__init__()
1152+ self.conf = conf
1153+ # The objects we will populate before creating a yaml encoding as a
1154+ # cloud-config-archive file
1155+ self.cloud_config = {}
1156+ self.root_hook = None
1157+ self.ubuntu_hook = None
1158+ self.launchpad_hook = None
1159+ self.uploaded_scripts_hook = None
1160+
1161+ def set(self, ud_name, conf_name=None, value=None):
1162+ """Set a user-data option from it's corresponding configuration one.
1163+
1164+ :param ud_name: user-data key.
1165+
1166+ :param conf_name: configuration key, If set to None, `value` should be
1167+ provided.
1168+
1169+ :param value: value to use if `conf_name` is None.
1170+ """
1171+ if value is None and conf_name is not None:
1172+ value = self.conf.get(conf_name)
1173+ if value is not None:
1174+ self.cloud_config[ud_name] = value
1175+
1176+ def _file_content(self, path, option_name):
1177+ full_path = os.path.expanduser(path)
1178+ try:
1179+ with open(full_path) as f:
1180+ content = f.read()
1181+ except IOError, e:
1182+ if e.args[0] == errno.ENOENT:
1183+ raise ConfigPathNotFound(path, option_name)
1184+ else:
1185+ raise
1186+ return content
1187+
1188+ def set_list_of_paths(self, ud_name, conf_name):
1189+ """Set a user-data option from its corresponding configuration one.
1190+
1191+ The configuration option is a list of paths and the user-data option
1192+ will be a list of each file content.
1193+
1194+ :param ud_name: user-data key.
1195+
1196+ :param conf_name: configuration key.
1197+ """
1198+ paths = self.conf.get(conf_name)
1199+ if paths:
1200+ contents = []
1201+ for p in paths:
1202+ contents.append(self._file_content(p, conf_name))
1203+ self.set(ud_name, None, contents)
1204+
1205+ def _key_from_path(self, path, option_name):
1206+ """Infer user-data key from file name."""
1207+ ssh_type, kind = ssh_infos_from_path(path)
1208+ if ssh_type is None:
1209+ raise ConfigValueError(option_name, path)
1210+ return '%s_%s' % (ssh_type, kind)
1211+
1212+ def set_ssh_keys(self):
1213+ """Set the server ssh keys from a list of paths.
1214+
1215+ Provided paths should respect some coding:
1216+
1217+ - the base name should start with the ssh type of their key (rsa, dsa,
1218+ ecdsa),
1219+
1220+ - base names ending with '.pub' are for public keys, the others are for
1221+ private keys.
1222+ """
1223+ key_paths = self.conf.get('vm.ssh_keys')
1224+ if key_paths:
1225+ ssh_keys = {}
1226+ for p in key_paths:
1227+ key = self._key_from_path(p, 'vm.ssh_keys')
1228+ ssh_keys[key] = self._file_content(p, 'vm.ssh_keys')
1229+ self.set('ssh_keys', None, ssh_keys)
1230+
1231+ def set_apt_sources(self):
1232+ sources = self.conf.get('vm.apt_sources')
1233+ if sources:
1234+ apt_sources = []
1235+ for src in sources:
1236+ # '|' should not appear in urls nor keys so it should be safe
1237+ # to use it as a separator.
1238+ parts = src.split('|')
1239+ if len(parts) == 1:
1240+ apt_sources.append({'source': parts[0]})
1241+ else:
1242+ # For PPAs, an additional GPG key should be imported in the
1243+ # guest.
1244+ apt_sources.append({'source': parts[0], 'keyid': parts[1]})
1245+ self.cloud_config['apt_sources'] = apt_sources
1246+
1247+ def append_cmd(self, cmd):
1248+ cmds = self.cloud_config.get('runcmd', [])
1249+ cmds.append(cmd)
1250+ self.cloud_config['runcmd'] = cmds
1251+
1252+ def _hook_script_path(self, user):
1253+ return '~%s/setup_vm_post_install' % (user,)
1254+
1255+ def _hook_content(self, option_name, user, hook_path, mode='0700'):
1256+ # FIXME: Add more tests towards properly creating a tree on the guest
1257+ # from a tree on the host. There seems to be three kind of items worth
1258+ # caring about here: file content (output path, owner, chmod), file
1259+ # (input and output paths, owner, chmod) and directory (path, owner,
1260+ # chmod). There are also some subtle traps involved about when files
1261+ # are created across various vm generations (one vm creates a dir, a mv
1262+ # on top of that one doesn't, but still creates a file in this dir,
1263+ # without realizing it can fail in a fresh vm). -- vila 2013-03-10
1264+ host_path = self.conf.get(option_name)
1265+ if host_path is None:
1266+ return None
1267+ fcontent = self._file_content(host_path, option_name)
1268+ # Expand options in the provided content so we report better errors
1269+ expanded_content = self.conf.expand_options(fcontent)
1270+ # The following will generate an additional newline at the end of the
1271+ # script. I can't think of a case where it matters and it makes this
1272+ # code more robust (and/or simpler) if the script/file *doesn't* end up
1273+ # with a proper newline.
1274+ # FIXME: This may be worth fixing if we provide a more generic way to
1275+ # create a remote tree. -- vila 2013-03-10
1276+ hook_content = '''#!/bin/sh
1277+cat >{__guest_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
1278+{__fcontent}
1279+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
1280+chown {__user}:{__user} {__guest_path}
1281+chmod {__mode} {__guest_path}
1282+'''
1283+ return hook_content.format(__user=user, __fcontent=expanded_content,
1284+ __mode=mode,
1285+ __guest_path=hook_path)
1286+
1287+ def set_boot_hook(self):
1288+ # FIXME: Needs a test ensuring we execute as root -- vila 2013-03-07
1289+ hook_path = self._hook_script_path('root')
1290+ content = self._hook_content('vm.root_script', 'root', hook_path)
1291+ if content is not None:
1292+ self.root_hook = content
1293+ self.append_cmd(hook_path)
1294+
1295+ def set_ubuntu_hook(self):
1296+ # FIXME: Needs a test ensuring we execute as ubuntu -- vila 2013-03-07
1297+ hook_path = self._hook_script_path('ubuntu')
1298+ content = self._hook_content('vm.ubuntu_script', 'ubuntu', hook_path)
1299+ if content is not None:
1300+ self.ubuntu_hook = content
1301+ self.append_cmd(['su', '-l', '-c', hook_path, 'ubuntu'])
1302+
1303+ def set_launchpad_access(self):
1304+ # FIXME: Needs a test that we can really access launchpad properly via
1305+ # ssh. Can only be done as a real launchpad user and as such requires
1306+ # cooperation :) I.e. Some configuration option set by the user will
1307+ # trigger the test -- vila 2013-03-14
1308+ lp_id = self.conf.get('vm.launchpad_id')
1309+ if lp_id is None:
1310+ return
1311+ # Use the specified ssh key found in ~/.ssh as the private key. The
1312+ # user is supposed to have uploaded the public one.
1313+ local_path = os.path.join('~', '.ssh', '%s@setup_vm' % (lp_id,))
1314+ # Force id_rsa or we'll need a .ssh/config to point to user@setup_vm
1315+ # for .lauchpad.net.
1316+ hook_path = '/home/ubuntu/.ssh/id_rsa'
1317+ dir_path = os.path.dirname(hook_path)
1318+ try:
1319+ fcontent = self._file_content(local_path, 'vm.launchpad_id')
1320+ except ConfigPathNotFound, e:
1321+ e.msg = ('You need to create the {p} keypair and upload {p}.pub to'
1322+ ' launchpad.\n'
1323+ 'See vm.launchpad_id in README.'.format(p=local_path))
1324+ raise e
1325+ # FIXME: ~Duplicated from _hook_content. -- vila 2013-03-10
1326+
1327+ # FIXME: If this hook is executed before the ubuntu user is created we
1328+ # need to chown/chmod ~ubuntu which is bad. This happens when a
1329+ # -pristine vm is created and lead to GUI login failing because it
1330+ # can't create any dir/file there. The fix is to only create a script
1331+ # that will be executed via runcmd so it will run later and avoid the
1332+ # issue. -- vila 2013-03-21
1333+ hook_content = '''#!/bin/sh
1334+mkdir -p {dir_path}
1335+chown {user}:{user} ~ubuntu
1336+chmod {dir_mode} ~ubuntu
1337+chown {user}:{user} {dir_path}
1338+chmod {dir_mode} {dir_path}
1339+cat >{guest_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
1340+{fcontent}
1341+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
1342+chown {user}:{user} {guest_path}
1343+chmod {file_mode} {guest_path}
1344+'''
1345+ self.launchpad_hook = self.conf.expand_options(
1346+ hook_content,
1347+ env=dict(user='ubuntu', fcontent=fcontent,
1348+ file_mode='0400', guest_path=hook_path,
1349+ dir_mode='0700', dir_path=dir_path))
1350+ self.append_cmd(['sudo', '-u', 'ubuntu',
1351+ 'bzr', 'launchpad-login', lp_id])
1352+
1353+ def set_uploaded_scripts(self):
1354+ script_paths = self.conf.get('vm.uploaded_scripts')
1355+ if not script_paths:
1356+ return
1357+ hook_path = '~ubuntu/setup_vm_uploads'
1358+ bindir = self.conf.get('vm.uploaded_scripts.guest_dir')
1359+ out = StringIO()
1360+ out.write('''#!/bin/sh
1361+cat >{hook_path} <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
1362+mkdir -p {bindir}
1363+cd {bindir}
1364+'''.format(**locals()))
1365+ for path in script_paths:
1366+ fcontent = self._file_content(path, 'vm.uploaded_scripts')
1367+ expanded = self.conf.expand_options(fcontent)
1368+ base = os.path.basename(path)
1369+ # FIXME: ~Duplicated from _hook_content. -- vila 2012-03-15
1370+ out.write('''cat >{base} <<'EOF{base}'
1371+{expanded}
1372+EOF{base}
1373+chmod 0755 {base}
1374+'''.format(**locals()))
1375+
1376+ out.write('''EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
1377+chown {user}:{user} {hook_path}
1378+chmod 0700 {hook_path}
1379+'''.format(user='ubuntu',**locals()))
1380+ self.uploaded_scripts_hook = out.getvalue()
1381+ self.append_cmd(['su', '-l', '-c', hook_path, 'ubuntu'])
1382+
1383+ def set_poweroff(self):
1384+ # We want to shutdown properly after installing. This is safe to set
1385+ # here as subsequent boots will ignore this setting, letting us use the
1386+ # vm ;)
1387+ if self.conf.get('vm.release') in ('precise', 'quantal'):
1388+ # Curse cloud-init lack of compatibility
1389+ self.append_cmd('halt')
1390+ else:
1391+ self.set('power_state', None, {'mode': 'poweroff'})
1392+
1393+ def populate(self):
1394+ # Common and non-configurable options
1395+ if self.conf.get('vm.release') == 'precise':
1396+ # Curse cloud-init lack of compatibility
1397+ msg = 'setup_vm finished installing in $UPTIME seconds.'
1398+ else:
1399+ msg = 'setup_vm finished installing in ${uptime} seconds.'
1400+ self.set('final_message', None, msg)
1401+ self.set('manage_etc_hosts', None, True)
1402+ self.set('chpasswd', None, dict(expire=False))
1403+ # Configurable options
1404+ self.set('password', 'vm.password')
1405+ self.set_list_of_paths('ssh_authorized_keys', 'vm.ssh_authorized_keys')
1406+ self.set_ssh_keys()
1407+ self.set('apt_proxy', 'vm.apt_proxy')
1408+ # Both user-data keys are set from the same config key, we don't
1409+ # provide a finer access.
1410+ self.set('apt_update', 'vm.update')
1411+ self.set('apt_upgrade', 'vm.update')
1412+ self.set_apt_sources()
1413+ self.set('packages', 'vm.packages')
1414+ self.set_launchpad_access()
1415+ # uploaded scripts
1416+ self.set_uploaded_scripts()
1417+ # The commands executed before powering off
1418+ self.set_boot_hook()
1419+ self.set_ubuntu_hook()
1420+ # This must be called last so previous commands (for precise and
1421+ # quantal) can be executed before powering off
1422+ self.set_poweroff()
1423+
1424+ def add_boot_hook(self, parts, hook):
1425+ if hook is not None:
1426+ parts.append({'content': '#cloud-boothook\n' + hook})
1427+
1428+ def dump(self):
1429+ parts = [{'content': '#cloud-config\n'
1430+ + yaml.safe_dump(self.cloud_config)}]
1431+ self.add_boot_hook(parts, self.root_hook)
1432+ self.add_boot_hook(parts, self.ubuntu_hook)
1433+ self.add_boot_hook(parts, self.launchpad_hook)
1434+ self.add_boot_hook(parts, self.uploaded_scripts_hook)
1435+ # Wrap the lot into a cloud config archive
1436+ return '#cloud-config-archive\n' + yaml.safe_dump(parts)
1437+
1438+
1439+def vm_states(source=None):
1440+ """A dict of states for vms indexed by name.
1441+
1442+ :param source: A list of lines as produced by virsh list --all without
1443+ decorations (header/footer).
1444+ """
1445+ if source is None:
1446+ retcode, out, err = run_subprocess(['virsh', 'list', '--all'])
1447+ # Get rid of header/footer
1448+ source = out.splitlines()[2:-1]
1449+ states = {}
1450+ for line in source:
1451+ caret_or_id, name, state = line.split(None, 2)
1452+ states[name] = state
1453+ return states
1454+
1455+
1456+class VM(object):
1457+
1458+ def __init__(self, conf):
1459+ self.conf = conf
1460+ self._config_dir = None
1461+
1462+ def ensure_dir(self, path):
1463+ try:
1464+ os.mkdir(path)
1465+ except OSError, e:
1466+ # FIXME: Try to create the parent dir ?
1467+ if e.errno == errno.EEXIST:
1468+ pass
1469+ else:
1470+ raise
1471+
1472+ def ensure_config_dir(self):
1473+ if self._config_dir is None:
1474+ # FIXME: expanduser is not tested
1475+ self._config_dir = os.path.expanduser(
1476+ self.conf.get('vm.config_dir'))
1477+ self.ensure_dir(self._config_dir)
1478+
1479+ def _ssh_keygen(self, key_path):
1480+ ssh_type, kind = ssh_infos_from_path(key_path)
1481+ path = os.path.expanduser(key_path) # Just in case
1482+ if kind == 'private': # public will be generated at the same time
1483+ run_subprocess(
1484+ ['ssh-keygen', '-f', path, '-N', '', '-t', ssh_type,
1485+ '-C', self.conf.get('vm.name')])
1486+
1487+ def ssh_keygen(self):
1488+ self.ensure_config_dir()
1489+ keys = self.conf.get('vm.ssh_keys')
1490+ for key in keys:
1491+ self._ssh_keygen(key)
1492+
1493+
1494+class Kvm(VM):
1495+
1496+ def __init__(self, conf):
1497+ super(Kvm, self).__init__(conf)
1498+ # Seed files
1499+ self._meta_data_path = None
1500+ self._user_data_path = None
1501+ # Disk paths
1502+ self._seed_path = None
1503+ self._disk_image_path = None
1504+
1505+ self._console_path = None
1506+
1507+ def _download_in_cache(self, source_url, name, force=False):
1508+ """Download ``name`` from ``source_url`` in ``vm.download_cache``.
1509+
1510+ :param source_url: The url where the file to download is located
1511+
1512+ :param name: The name of the file to download (also used as the name
1513+ for the downloaded file).
1514+
1515+ :param force: Remove the file from the cache if present.
1516+
1517+ :return: False if the file is in the download cache, True if a download
1518+ occurred.
1519+ """
1520+ source = urlutils.join(source_url, name)
1521+ download_dir = self.conf.get('vm.download_cache')
1522+ if not os.path.exists(download_dir):
1523+ raise ConfigValueError('vm.download_cache', download_dir)
1524+ target = os.path.join(download_dir, name)
1525+ # FIXME: By default the download dir may be under root control, but if
1526+ # a user chose to use a different one under his own control, it would
1527+ # be nice to not require sudo usage. -- vila 2013-02-06
1528+ if force:
1529+ run_subprocess(['sudo', 'rm', '-f', target])
1530+ if not os.path.exists(target):
1531+ # FIXME: We do ask for a progress bar but it's not displayed
1532+ # (run_subprocess capture both stdout and stderr) ! At least while
1533+ # used interactively, it should. -- vila 2013-02-06
1534+ run_subprocess(['sudo', 'wget', '--progress=dot:mega','-O',
1535+ target, source])
1536+ return True
1537+ else:
1538+ return False
1539+
1540+ def download_iso(self, force=False):
1541+ """Download the iso to install the vm.
1542+
1543+ :return: False if the iso is in the download cache, True if a download
1544+ occurred.
1545+ """
1546+ return self._download_in_cache(self.conf.get('vm.iso_url'),
1547+ self.conf.get('vm.iso_name'),
1548+ force=force)
1549+
1550+ def download_cloud_image(self, force=False):
1551+ """Download the cloud image to install the vm.
1552+
1553+ :return: False if the image is in the download cache, True if a
1554+ download occurred.
1555+ """
1556+ return self._download_in_cache(self.conf.get('vm.cloud_image_url'),
1557+ self.conf.get('vm.cloud_image_name'),
1558+ force=force)
1559+
1560+ def create_meta_data(self):
1561+ self.ensure_config_dir()
1562+ self._meta_data_path = os.path.join(self._config_dir, 'meta-data')
1563+ with open(self._meta_data_path, 'w') as f:
1564+ f.write(self.conf.get('vm.meta_data'))
1565+
1566+ def create_user_data(self):
1567+ ci_user_data = CIUserData(self.conf)
1568+ ci_user_data.populate()
1569+ self.ensure_config_dir()
1570+ self._user_data_path = os.path.join(self._config_dir, 'user-data')
1571+ with open(self._user_data_path, 'w') as f:
1572+ f.write(ci_user_data.dump())
1573+
1574+ def create_seed(self):
1575+ if self._meta_data_path is None:
1576+ self.create_meta_data()
1577+ if self._user_data_path is None:
1578+ self.create_user_data()
1579+ images_dir = self.conf.get('vm.images_dir')
1580+ seed_path = os.path.join(
1581+ images_dir, self.conf.expand_options('{vm.name}.seed'))
1582+ run_subprocess(
1583+ # We create the seed in the ``vm.images_dir`` directory, so
1584+ # ``sudo`` is required
1585+ ['sudo',
1586+ 'genisoimage', '-output', seed_path, '-volid', 'cidata',
1587+ '-joliet', '-rock', '-input-charset', 'default',
1588+ '-graft-points',
1589+ 'user-data=%s' % (self._user_data_path,),
1590+ 'meta-data=%s' % (self._meta_data_path,),
1591+ ])
1592+ self._seed_path = seed_path
1593+
1594+ def create_disk_image(self):
1595+ raise NotImplementedError(self.create_disk_image)
1596+
1597+ def _wait_for_install_with_seed(self):
1598+ # The console is created by virt-install which requires sudo but creates
1599+ # the file 0600 for libvirt-qemu. We give read access to all otherwise
1600+ # 'tail -f' requires sudo and can't be killed anymore.
1601+ run_subprocess(['sudo', 'chmod', '0644', self._console_path])
1602+ # While `virt-install` is running, let's connect to the console
1603+ console = FileMonitor(self._console_path)
1604+ try:
1605+ for line in console.parse():
1606+# FIXME: We need some way to activate this dynamically (conf var defaulting to
1607+# env var OR cmdline parameter ? -- vila 2013-02-11
1608+# print "read: [%s]" % (line,) # so useful for debug...
1609+ pass
1610+ except (ConsoleEOFError, CloudInitError), e:
1611+ # FIXME: No test covers this path -- vila 2013-02-15
1612+ err_lines = ['Suspicious line from cloud-init.\n',
1613+ '\t' + console.lines[-1],
1614+ 'Check the configuration:\n']
1615+ with open(self._meta_data_path) as f:
1616+ err_lines.append('meta-data content:\n')
1617+ err_lines.extend(f.readlines())
1618+ with open(self._user_data_path) as f:
1619+ err_lines.append('user-data content:\n')
1620+ err_lines.extend(f.readlines())
1621+ raise CommandError(console.cmd, console.proc.returncode,
1622+ '\n'.join(console.lines),
1623+ ''.join(err_lines))
1624+
1625+ def install(self):
1626+ # Create a kvm, relying on cloud-init to customize the base image.
1627+ #
1628+ # There are two processes involvded here:
1629+ # - virt-install creates the vm and boots it.
1630+ # - progress is monitored via the console to detect cloud-final.
1631+ #
1632+ # Once cloud-init has finished, the vm can be powered off.
1633+
1634+ # FIXME: If the install doesn't finish after $time, emit a warning and
1635+ # terminate self.install_proc.
1636+ # FIXME: If we can't connect to the console, emit a warning and
1637+ # terminate console and self.install_proc.
1638+ # FIXME: If we don't receive anything on the console after $time2, emit
1639+ # a warning and terminate console and self.install_proc.
1640+ # -- vila 2013-02-07
1641+ if self._seed_path is None:
1642+ self.create_seed()
1643+ if self._disk_image_path is None:
1644+ self.create_disk_image()
1645+ # FIXME: Install time is probably a good time to delete the
1646+ # console. While it makes sense to accumulate for all runs for a given
1647+ # install, keeping them without any limit nor roration is likely to
1648+ # cause issues at some point... -- vila 2013-02-20
1649+ self._console_path = os.path.join(
1650+ self.conf.get('vm.images_dir'),
1651+ '%s.console' % (self.conf.get('vm.name'),))
1652+ run_subprocess(
1653+ ['sudo', 'virt-install',
1654+ # To ensure we're not bitten again by http://pad.lv/1157272 where
1655+ # virt-install wrongly detect virtualbox. -- vila 2013-03-20
1656+ '--connect', 'qemu:///system',
1657+ # Without --noautoconsole, virt-install will relay the console,
1658+ # that's not appropriate for our needs so we'll connect later
1659+ # ourselves
1660+ '--noautoconsole',
1661+ # We define the console as a file so we can monitor the install
1662+ # via 'tail -f'
1663+ '--serial', 'file,path=%s' % (self._console_path,),
1664+ '--network', self.conf.get('vm.network'),
1665+ # Anticipate that we'll need a graphic card defined
1666+ '--graphics', 'spice',
1667+ '--name', self.conf.get('vm.name'),
1668+ '--ram', self.conf.get('vm.ram_size'),
1669+ '--vcpus', self.conf.get('vm.cpus'),
1670+ '--disk', 'path=%s,format=qcow2' % (self._disk_image_path,),
1671+ '--disk', 'path=%s' % (self._seed_path,),
1672+ # We just boot, cloud-init will handle the installs we need
1673+ '--boot', 'hd', '--hvm',
1674+ ])
1675+ self._wait_for_install_with_seed()
1676+ # We've seen the console signaling halt, but the vm will need a bit
1677+ # more time to get there so we help it a bit.
1678+ if self.conf.get('vm.release') in ('precise', 'quantal'):
1679+ # cloud-init doesn't implement power_state until raring and need a
1680+ # gentle nudge.
1681+ self.poweroff()
1682+ vm_name = self.conf.get('vm.name')
1683+ while True:
1684+ state = vm_states()[vm_name]
1685+ # We expect the vm's state to be 'in shutdown' but in some rare
1686+ # occasions we may catch 'running' before getting 'in shutdown'.
1687+ if state in ('in shutdown', 'running'):
1688+ # Ok, querying the state takes time, this regulates the polling
1689+ # enough that we don't need to sleep.
1690+ continue
1691+ elif state == 'shut off':
1692+ # Good, we're done
1693+ break
1694+ # FIXME: No idea on how to test the following. Manually tested by
1695+ # altering the expected state above and running 'selftest.py -v'
1696+ # which errors out for test_install_with_seed and
1697+ # test_install_backing. Also reproduced when 'running' wasn't
1698+ # expected before 'in shutdown' -- vila 2013-02-19
1699+ # Unexpected state reached, bad.
1700+ raise SetupVmError('Something went wrong during {name} install\n'
1701+ 'The vm ended in state: {state}\n'
1702+ 'Check the console at {path}',
1703+ name=vm_name, state=state,
1704+ path=self._console_path)
1705+
1706+ def poweroff(self):
1707+ return run_subprocess(
1708+ ['sudo', 'virsh', 'destroy', self.conf.get('vm.name')])
1709+
1710+ def undefine(self):
1711+ return run_subprocess(
1712+ ['sudo', 'virsh', 'undefine', self.conf.get('vm.name'),
1713+ '--remove-all-storage'])
1714+
1715+
1716+class KvmFromCloudImage(Kvm):
1717+
1718+ def create_disk_image(self, src_name=None, dst_name=None):
1719+ """Create a disk image from a cloud one."""
1720+ if src_name is None:
1721+ src_name = self.conf.get('vm.cloud_image_name')
1722+ if dst_name is None:
1723+ dst_name = self.conf.expand_options('{vm.name}.qcow2')
1724+ cloud_image_path = os.path.join(
1725+ self.conf.get('vm.download_cache'), src_name)
1726+ disk_image_path = os.path.join(
1727+ self.conf.get('vm.images_dir'), dst_name)
1728+ run_subprocess(
1729+ ['sudo', 'qemu-img', 'convert',
1730+ '-O', 'qcow2', cloud_image_path, disk_image_path])
1731+ run_subprocess(
1732+ ['sudo', 'qemu-img', 'resize',
1733+ disk_image_path, self.conf.get('vm.disk_size')])
1734+ self._disk_image_path = disk_image_path
1735+
1736+
1737+class KvmFromBacking(Kvm):
1738+
1739+ def create_disk_image(self, src_name=None, dst_name=None):
1740+ """Create a disk image backed by an existing one."""
1741+ backing_image_path = os.path.join(
1742+ self.conf.get('vm.images_dir'),
1743+ self.conf.expand_options('{vm.backing}'))
1744+ disk_image_path = os.path.join(
1745+ self.conf.get('vm.images_dir'),
1746+ self.conf.expand_options('{vm.name}.qcow2'))
1747+ run_subprocess(
1748+ ['sudo', 'qemu-img', 'create', '-f', 'qcow2',
1749+ '-b', backing_image_path, disk_image_path])
1750+ run_subprocess(
1751+ ['sudo', 'qemu-img', 'resize',
1752+ disk_image_path, self.conf.get('vm.disk_size')])
1753+ self._disk_image_path = disk_image_path
1754+
1755+
1756+class ArgParser(argparse.ArgumentParser):
1757+ """A parser for the setup_vm script."""
1758+
1759+ def __init__(self):
1760+ description = 'Set up virtual machines from a configuration file.'
1761+ super(ArgParser, self).__init__(
1762+ prog='setup_vm.py', description=description)
1763+ self.add_argument(
1764+ 'name', help='Virtual machine section in the configuration file.')
1765+ self.add_argument('--download', '-d', action="store_true",
1766+ help='Force download of the required image.')
1767+ self.add_argument('--ssh-keygen', '-k', action="store_true",
1768+ help='Generate the ssh server keys (if any).')
1769+ self.add_argument('--install', '-i', action="store_true",
1770+ help='Install the virtual machine.')
1771+
1772+ def parse_args(self, args=None, out=None, err=None):
1773+ """Parse arguments, overridding stdout/stderr if provided.
1774+
1775+ Overridding stdout/stderr is provided for tests.
1776+
1777+ :params args: Defaults to sys.argv[1:].
1778+
1779+ :param out: Defaults to sys.stdout.
1780+
1781+ :param err: Defaults to sys.stderr.
1782+ """
1783+ out_orig = sys.stdout
1784+ err_orig = sys.stderr
1785+ try:
1786+ if out is not None:
1787+ sys.stdout = out
1788+ if err is not None:
1789+ sys.stderr = err
1790+ return super(ArgParser, self).parse_args(args)
1791+ finally:
1792+ sys.stdout = out_orig
1793+ sys.stderr = err_orig
1794+
1795+
1796+
1797+arg_parser = ArgParser()
1798+
1799+class Command(object):
1800+
1801+ def __init__(self, vm):
1802+ self.vm = vm
1803+
1804+
1805+class Download(Command):
1806+
1807+ def run(self):
1808+ # FIXME: what needs to be downloaded should depend on the type of the
1809+ # vm (possibly errors if there is nothing to download). -- vila
1810+ # 2013-02-06
1811+ self.vm.download_cloud_image(force=True)
1812+
1813+
1814+class SshKeyGen(Command):
1815+
1816+ def run(self):
1817+ self.vm.ssh_keygen()
1818+
1819+
1820+class Install(Command):
1821+
1822+ def run(self):
1823+ vm_name = self.vm.conf.get('vm.name')
1824+ state = vm_states().get(vm_name, None)
1825+ if state == 'shut off':
1826+ self.vm.undefine()
1827+ elif state == 'running':
1828+ raise SetupVmError('{name} is running', name=vm_name)
1829+ # FIXME: The installation method may vary depending on the vm type.
1830+ # -- vila 2013-02-06
1831+ self.vm.install()
1832+
1833+
1834+def build_commands(args=None, out=None, err=None):
1835+ cmds = []
1836+ if args is None:
1837+ args = sys.argv[1:]
1838+
1839+ ns = arg_parser.parse_args(args, out=out, err=err)
1840+
1841+ conf = VmStack(ns.name)
1842+ with_backing = conf.get('vm.backing')
1843+ if with_backing is None:
1844+ vm = KvmFromCloudImage(conf)
1845+ else:
1846+ vm = KvmFromBacking(conf)
1847+ if ns.download:
1848+ cmds.append(Download(vm))
1849+ if ns.ssh_keygen:
1850+ cmds.append(SshKeyGen(vm))
1851+ if ns.install:
1852+ cmds.append(Install(vm))
1853+ return cmds
1854+
1855+
1856+def run(args=None):
1857+ cmds = build_commands(args)
1858+ for cmd in cmds:
1859+ try:
1860+ cmd.run()
1861+ except SetupVmError, e:
1862+ # Stop on first error
1863+ print 'ERROR: %s' % e
1864+ exit(-1)
1865+
1866+
1867+if __name__ == "__main__":
1868+ run()
1869
1870=== added file 'setup_vm/bin/ubuntu_admin.sh'
1871--- setup_vm/bin/ubuntu_admin.sh 1970-01-01 00:00:00 +0000
1872+++ setup_vm/bin/ubuntu_admin.sh 2013-04-12 15:44:25 +0000
1873@@ -0,0 +1,2 @@
1874+#!/bin/sh
1875+adduser ubuntu admin
1876
1877=== added directory 'setup_vm/pay'
1878=== added file 'setup_vm/pay/install'
1879--- setup_vm/pay/install 1970-01-01 00:00:00 +0000
1880+++ setup_vm/pay/install 2013-04-12 15:44:25 +0000
1881@@ -0,0 +1,40 @@
1882+#!/bin/sh -ex
1883+
1884+# Allow ssh access to launchpad.
1885+# This should probably be provided by setup_vm. -- vila 2013-03-10
1886+ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
1887+# Get the branch.
1888+bzr branch lp:canonical-payment-service {pay.src_dir}
1889+# Get the download cache.
1890+bzr branch lp:~canonical-isd-hackers/+junk/download-cache
1891+# Setup the environment.
1892+cd {pay.src_dir}
1893+# Get the version controlled configs.
1894+bzr branch lp:~canonical-isd-hackers/isd-configs/payments-config branches/project
1895+# Bootstrap the dependencies
1896+fab bootstrap:download_cache_path=~/download-cache
1897+# Set up the correct Django configuration.
1898+cat <<EOF >django_project/local.cfg
1899+[__noschema__]
1900+db_host = /home/ubuntu/{pay.src_dir}/.env/db
1901+hostname = {pay.address}:{pay.port}
1902+
1903+[__main__]
1904+includes =
1905+ config/devel.cfg
1906+ ../branches/project/config/acceptance.cfg
1907+
1908+[django]
1909+debug = false
1910+internal_ips =
1911+
1912+[testing]
1913+imap_server = {sso.address}
1914+imap_port = {sso.imap_port}
1915+imap_use_ssl = False
1916+
1917+[openid]
1918+openid_sso_server_url = {sso.url}
1919+openid_trust_root = {pay.url}
1920+
1921+EOF
1922
1923=== added file 'setup_vm/pay/run'
1924--- setup_vm/pay/run 1970-01-01 00:00:00 +0000
1925+++ setup_vm/pay/run 2013-04-12 15:44:25 +0000
1926@@ -0,0 +1,9 @@
1927+#!/bin/sh
1928+
1929+cd {pay.src_dir}
1930+
1931+# Setup the database.
1932+fab setup_postgresql_server
1933+fab manage:loaddata,test
1934+# Start the PAY server, accessible from the local network.
1935+fab run:0.0.0.0:{pay.port}
1936
1937=== added file 'setup_vm/pay/run-for-u1'
1938--- setup_vm/pay/run-for-u1 1970-01-01 00:00:00 +0000
1939+++ setup_vm/pay/run-for-u1 2013-04-12 15:44:25 +0000
1940@@ -0,0 +1,51 @@
1941+#!/bin/sh
1942+
1943+cd {pay.src_dir}
1944+
1945+# Setup the database.
1946+fab setup_postgresql_server
1947+# Add the API user for U1.
1948+# We generated this json file with:
1949+# Go to {pay.url}/admin
1950+# Sign in with the admin/admin.
1951+# Click the more link next to the Model Admin heading.
1952+# On the Paymentservice section, click the +Add link next to API Users.
1953+# Fill the form with:
1954+# username: ubuntuone
1955+# password: apipassword
1956+# Click the Save button.
1957+# Select the Ubuntu One (U1) Consumer.
1958+# Click the Save button.
1959+# Do the same with:
1960+# username: u1ms_staging
1961+# password: testing
1962+# $ fab manage:dumpdata,paymentservice.APIUser
1963+cat <<EOF >src/paymentservice/fixtures/apiuser.json
1964+[
1965+ {
1966+ "pk": 2,
1967+ "model": "paymentservice.apiuser",
1968+ "fields": {
1969+ "username": "ubuntuone",
1970+ "created_at": "2013-03-16 23:14:53",
1971+ "password": "sha1$4a862$25b1ab7c5c23254057abf7573aa52aa75ad0b849",
1972+ "consumer": "U1",
1973+ "updated_at": "2013-03-16 23:15:00"
1974+ }
1975+ },
1976+ {
1977+ "pk": 3,
1978+ "model": "paymentservice.apiuser",
1979+ "fields": {
1980+ "username": "u1ms_staging",
1981+ "created_at": "2013-03-16 23:18:39",
1982+ "password": "sha1$11785$f76c98b0ea2b5ef5ea92b4c11f8305fe4ef1e8dd",
1983+ "consumer": "U1",
1984+ "updated_at": "2013-03-16 23:18:44"
1985+ }
1986+ }
1987+]
1988+EOF
1989+fab manage:loaddata,apiuser
1990+# Start the PAY server, accessible from the local network.
1991+fab run:0.0.0.0:{pay.port}
1992
1993=== added file 'setup_vm/pay/test'
1994--- setup_vm/pay/test 1970-01-01 00:00:00 +0000
1995+++ setup_vm/pay/test 2013-04-12 15:44:25 +0000
1996@@ -0,0 +1,5 @@
1997+#!/bin/sh
1998+
1999+cd {pay.src_dir}
2000+
2001+SST_BASE_URL={pay.url} fab acceptance:screenshot=true,report=xml,extended=true
2002
2003=== added file 'setup_vm/selftest.py'
2004--- setup_vm/selftest.py 1970-01-01 00:00:00 +0000
2005+++ setup_vm/selftest.py 2013-04-12 15:44:25 +0000
2006@@ -0,0 +1,24 @@
2007+#!/usr/bin/env python
2008+
2009+import sys
2010+
2011+import testtools.run
2012+import unittest
2013+
2014+
2015+class TestProgram(testtools.run.TestProgram):
2016+
2017+ def __init__(self, module, argv, stdout=None, testRunner=None, exit=True):
2018+ if testRunner is None:
2019+ testRunner = unittest.TextTestRunner
2020+ super(TestProgram, self).__init__(module, argv=argv, stdout=stdout,
2021+ testRunner=testRunner, exit=exit)
2022+
2023+
2024+# We discover tests under './tests', the python 'load_test' protocol can be
2025+# used in test modules for more fancy stuff.
2026+discover_args = ['discover',
2027+ '--start-directory', './tests',
2028+ '--top-level-directory', '.',
2029+ ]
2030+TestProgram(__name__, argv=[sys.argv[0]] + discover_args + sys.argv[1:])
2031
2032=== added directory 'setup_vm/sso'
2033=== added file 'setup_vm/sso/install'
2034--- setup_vm/sso/install 1970-01-01 00:00:00 +0000
2035+++ setup_vm/sso/install 2013-04-12 15:44:25 +0000
2036@@ -0,0 +1,45 @@
2037+#!/bin/sh -ex
2038+
2039+# Allow ssh access to launchpad.
2040+# This should probably be provided by setup_vm. -- vila 2013-03-10
2041+ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
2042+# Get the branch.
2043+bzr branch lp:canonical-identity-provider {sso.src_dir}
2044+# Get the download cache.
2045+bzr branch lp:~canonical-isd-hackers/+junk/download-cache
2046+# Setup the environment.
2047+cd {sso.src_dir}
2048+# Get the version controlled configs.
2049+bzr branch lp:~canonical-isd-hackers/isd-configs/sso-config branches/project
2050+# Bootstrap the dependencies
2051+fab bootstrap:download_cache_path=~/download-cache
2052+# Set up the correct Django configuration.
2053+# In order to set the db_host to a directory in .env, we need to use the full
2054+# path. Otherwise, fab setup_postgresql_server will fail.
2055+# TODO we can either configure the postgresql authentication and pass db_host
2056+# as empty, or use cat just to append to the end of the default local.cfg
2057+# that will contain the full path we need, or pass the user name in a config
2058+# variable.
2059+cat <<EOF >django_project/local.cfg
2060+[__noschema__]
2061+basedir = .
2062+db_host = /home/ubuntu/{sso.src_dir}/.env/db
2063+hostname = {sso.address}:{sso.port}
2064+
2065+[__main__]
2066+includes =
2067+ config/devel.cfg
2068+ ../branches/project/config/acceptance-dev.cfg
2069+
2070+[django]
2071+debug = false
2072+email_port = {sso.smtp_port}
2073+
2074+[testing]
2075+imap_server = {sso.address}
2076+imap_port = {sso.imap_port}
2077+# needs to be a full email
2078+imap_username = whatever@we.dont.care
2079+imap_use_ssl = False
2080+
2081+EOF
2082
2083=== added file 'setup_vm/sso/run'
2084--- setup_vm/sso/run 1970-01-01 00:00:00 +0000
2085+++ setup_vm/sso/run 2013-04-12 15:44:25 +0000
2086@@ -0,0 +1,16 @@
2087+#!/bin/sh
2088+
2089+cd ~/{sso.src_dir}
2090+# We need an SMTP server to send emails.
2091+.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
2092+
2093+# Setup the database.
2094+fab setup_postgresql_server
2095+fab manage:loaddata,test
2096+fab manage:create_test_team
2097+# get gargoyle flags from their use in the code
2098+SST_FLAGS=`grep -rho --exclude 'test_*.py' "is_active([\"']\(.*\)[\"']" identityprovider/ webui/ | sed -E "s/is_active\(['\"](.*)['\"]/\1/" | awk '{print tolower($0)}' | sort | uniq | tr '\n' ','`
2099+# We need to remove the trailing ','
2100+fab gargoyle_flags:${SST_FLAGS%,}
2101+# Start the SSO server, accessible from the local network.
2102+fab run:0.0.0.0:{sso.port}
2103
2104=== added file 'setup_vm/sso/run-for-pay'
2105--- setup_vm/sso/run-for-pay 1970-01-01 00:00:00 +0000
2106+++ setup_vm/sso/run-for-pay 2013-04-12 15:44:25 +0000
2107@@ -0,0 +1,14 @@
2108+#!/bin/sh
2109+
2110+cd ~/{sso.src_dir}
2111+# We need an SMTP server to send emails.
2112+.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
2113+
2114+# Setup the database.
2115+fab setup_postgresql_server
2116+fab manage:loaddata,isdtest
2117+fab manage:loaddata,allow_unverified
2118+# Set the allow-unverified config for Pay.
2119+fab manage:add_openid_rp_config,{pay.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
2120+# Start the SSO server, accessible from the local network.
2121+fab run:0.0.0.0:{sso.port}
2122
2123=== added file 'setup_vm/sso/run-for-u1'
2124--- setup_vm/sso/run-for-u1 1970-01-01 00:00:00 +0000
2125+++ setup_vm/sso/run-for-u1 2013-04-12 15:44:25 +0000
2126@@ -0,0 +1,43 @@
2127+#!/bin/sh
2128+
2129+cd ~/{sso.src_dir}
2130+# We need an SMTP server to send emails.
2131+.env/bin/twistd localmail --imap {sso.imap_port} --smtp {sso.smtp_port}
2132+
2133+# Setup the database.
2134+fab setup_postgresql_server
2135+fab manage:loaddata,allow_unverified
2136+# Set the allow-unverified config for Pay.
2137+fab manage:add_openid_rp_config,{pay.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
2138+# Set the allow-unverified config for U1.
2139+fab manage:add_openid_rp_config,{u1.url},--allow-unverified,--allowed-sreg="fullname\,nickname\,email\,language"
2140+# Add the API user for U1.
2141+# We generated this json file with:
2142+# $ fab manage:createsuperuser
2143+# Go to {sso.url}/admin
2144+# Sign in with the super user you have just created.
2145+# Click the more link next to the Model Admin heading.
2146+# On the Identityprovider section, click the +Add link next to API Users.
2147+# Fill the form with:
2148+# username: ubuntuone
2149+# password: apipassword
2150+# Click the Save button.
2151+# $ fab manage:dumpdata,identityprovider.APIUser
2152+cat <<EOF >identityprovider/fixtures/apiuser.json
2153+[
2154+ {
2155+ "pk": 1,
2156+ "model": "identityprovider.apiuser",
2157+ "fields": {
2158+ "username": "ubuntuone",
2159+ "created_at": "2013-03-16 22:36:27",
2160+ "password": "uq3QfNWLHRV5TfJT+Dul+X/Iodfj2mV/+1HHn7LDw1QQe9dd6To/cQ==",
2161+ "updated_at": "2013-03-16 22:36:27"
2162+ }
2163+ }
2164+]
2165+
2166+EOF
2167+fab manage:loaddata,apiuser
2168+# Start the SSO server, accessible from the local network.
2169+fab run:0.0.0.0:{sso.port}
2170
2171=== added file 'setup_vm/sso/test'
2172--- setup_vm/sso/test 1970-01-01 00:00:00 +0000
2173+++ setup_vm/sso/test 2013-04-12 15:44:25 +0000
2174@@ -0,0 +1,11 @@
2175+#!/bin/sh
2176+
2177+# FIXME: This should run on the host and get config options expanded
2178+# -- vila 2013-03-12
2179+
2180+cd {sso.src_dir}
2181+
2182+# get gargoyle flags from their use in the code
2183+SST_FLAGS=`grep -rho --exclude 'test_*.py' "is_active([\"']\(.*\)[\"']" identityprovider/ webui/ | sed -E "s/is_active\(['\"](.*)['\"]/\1/" | awk '{print tolower($0)}' | sort | uniq | tr '\n' ';'`
2184+# run tests
2185+SST_BASE_URL={sso.url} fab acceptance:screenshot=true,report=xml,extended=true,flags=$SST_FLAGS
2186
2187=== added directory 'setup_vm/tests'
2188=== added file 'setup_vm/tests/__init__.py'
2189--- setup_vm/tests/__init__.py 1970-01-01 00:00:00 +0000
2190+++ setup_vm/tests/__init__.py 2013-04-12 15:44:25 +0000
2191@@ -0,0 +1,75 @@
2192+import os
2193+import shutil
2194+import tempfile
2195+
2196+from bzrlib import osutils
2197+
2198+def override_env_var(name, value):
2199+ """Modify the environment, setting or removing the env_variable.
2200+
2201+ :param name: The environment variable to set.
2202+
2203+ :param value: The value to set the environment to. If None, then
2204+ the variable will be removed.
2205+
2206+ :return: The original value of the environment variable.
2207+ """
2208+ orig = os.environ.get(name)
2209+ if value is None:
2210+ if orig is not None:
2211+ del os.environ[name]
2212+ else:
2213+ # FIXME: supporting unicode values requires a way to acquire the
2214+ # user encoding, punting for now -- vila 2013-01-30
2215+ os.environ[name] = value
2216+ return orig
2217+
2218+
2219+def override_env(test, name, new):
2220+ """Set an environment variable, and reset it after the test.
2221+
2222+ :param name: The environment variable name.
2223+
2224+ :param new: The value to set the variable to. If None, the
2225+ variable is deleted from the environment.
2226+
2227+ :returns: The actual variable value.
2228+ """
2229+ value = override_env_var(name, new)
2230+ test.addCleanup(override_env_var, name, value)
2231+ return value
2232+
2233+
2234+isolated_environ = {
2235+ 'HOME': None,
2236+}
2237+
2238+
2239+def isolate_env(test, env=None):
2240+ """Isolate test from the environment variables.
2241+
2242+ This is usually called in setUp for tests that needs to modify the
2243+ environment variables and restore them after the test is run.
2244+
2245+ :param test: A test instance
2246+
2247+ :param env: A dict containing variable definitions to be installed. Only
2248+ the variables present there are protected. They are initialized with
2249+ the provided values.
2250+ """
2251+ if env is None:
2252+ env = isolated_environ
2253+ for name, value in env.items():
2254+ override_env(test, name, value)
2255+
2256+
2257+def set_cwd_to_tmp(test):
2258+ """Create a temp dir an cd into it for the test duration.
2259+
2260+ This is generally called during a test setup.
2261+ """
2262+ test.test_base_dir = tempfile.mkdtemp(prefix='mytests-', suffix='.tmp')
2263+ test.addCleanup(shutil.rmtree, test.test_base_dir, True)
2264+ current_dir = os.getcwdu()
2265+ test.addCleanup(os.chdir, current_dir)
2266+ os.chdir(test.test_base_dir)
2267
2268=== added file 'setup_vm/tests/test_setup_vm.py'
2269--- setup_vm/tests/test_setup_vm.py 1970-01-01 00:00:00 +0000
2270+++ setup_vm/tests/test_setup_vm.py 2013-04-12 15:44:25 +0000
2271@@ -0,0 +1,1074 @@
2272+from cStringIO import StringIO
2273+import os
2274+
2275+from bzrlib import errors
2276+import testtools
2277+
2278+import tests
2279+from bin import setup_vm
2280+
2281+
2282+def requires_known_reference_image(test):
2283+ # We need a pre-seeded download cache from the user running the tests
2284+ # as downloading the cloud image is too long.
2285+ user_conf = setup_vm.VmStack(None)
2286+ download_cache = user_conf.get('vm.download_cache')
2287+ if download_cache is None:
2288+ test.skip('vm.download_cache is not set')
2289+ # We use some known reference
2290+ reference_cloud_image_name = 'raring-server-cloudimg-amd64-disk1.img'
2291+ cloud_image_path = os.path.join(
2292+ download_cache, reference_cloud_image_name)
2293+ if not os.path.exists(cloud_image_path):
2294+ test.skip('%s is not available' % (cloud_image_path,))
2295+ return download_cache, reference_cloud_image_name
2296+
2297+
2298+class TestCaseWithHome(testtools.TestCase):
2299+ """Provide an isolated disk-based environment.
2300+
2301+ A $HOME directory is setup as well as an /etc/ one so tests can setup
2302+ config files.
2303+ """
2304+
2305+ def setUp(self):
2306+ super(TestCaseWithHome, self).setUp()
2307+ tests.set_cwd_to_tmp(self)
2308+ tests.isolate_env(self)
2309+ # Isolate tests from the user environment
2310+ self.home_dir = os.path.join(self.test_base_dir, 'home')
2311+ os.mkdir(self.home_dir)
2312+ os.environ['HOME'] = self.home_dir
2313+ # Also isolate from the system environment
2314+ self.etc_dir = os.path.join(self.test_base_dir, 'etc')
2315+ os.mkdir(self.etc_dir)
2316+ self.patch(setup_vm, 'system_config_dir', lambda: self.etc_dir)
2317+
2318+
2319+class TestVmMatcher(TestCaseWithHome):
2320+
2321+ def setUp(self):
2322+ super(TestVmMatcher, self).setUp()
2323+ self.store = setup_vm.VmStore('.', 'foo.conf')
2324+ self.matcher = setup_vm.VmMatcher(self.store, 'test')
2325+
2326+ def test_empty_section_always_matches(self):
2327+ self.store._load_from_string('foo=bar')
2328+ matching = list(self.matcher.get_sections())
2329+ self.assertEqual(1, len(matching))
2330+
2331+ def test_specific_before_generic(self):
2332+ self.store._load_from_string('foo=bar\n[test]\nfoo=baz')
2333+ matching = list(self.matcher.get_sections())
2334+ self.assertEqual(2, len(matching))
2335+ # First matching section is for test
2336+ self.assertEqual(self.store, matching[0][0])
2337+ base_section = matching[0][1]
2338+ self.assertEqual('test', base_section.id)
2339+ # Second matching section is the no-name one
2340+ self.assertEqual(self.store, matching[0][0])
2341+ no_name_section = matching[1][1]
2342+ self.assertIs(None, no_name_section.id)
2343+
2344+
2345+class TestVmStores(TestCaseWithHome):
2346+
2347+ def setUp(self):
2348+ super(TestVmStores, self).setUp()
2349+ self.conf = setup_vm.VmStack('foo')
2350+
2351+
2352+ def test_default_in_empty_stack(self):
2353+ self.assertEqual('1024', self.conf.get('vm.ram_size'))
2354+
2355+
2356+ def test_system_overrides_internal(self):
2357+ self.conf.system_store._load_from_string('vm.ram_size = 42')
2358+ self.assertEqual('42', self.conf.get('vm.ram_size'))
2359+
2360+ def test_user_overrides_system(self):
2361+ self.conf.system_store._load_from_string('vm.ram_size = 42')
2362+ self.conf.store._load_from_string('vm.ram_size = 4201')
2363+ self.assertEqual('4201', self.conf.get('vm.ram_size'))
2364+
2365+ def test_local_overrides_user(self):
2366+ self.conf.system_store._load_from_string('vm.ram_size = 42')
2367+ self.conf.store._load_from_string('vm.ram_size = 4201')
2368+ self.conf.local_store._load_from_string('vm.ram_size = 8402')
2369+ self.assertEqual('8402', self.conf.get('vm.ram_size'))
2370+
2371+
2372+class TestVmStack(TestCaseWithHome):
2373+
2374+ def setUp(self):
2375+ super(TestVmStack, self).setUp()
2376+ self.conf = setup_vm.VmStack('foo')
2377+ self.conf.store._load_from_string('''
2378+vm.release=raring
2379+vm.cpu_model=amd64
2380+''')
2381+
2382+ def assertValue(self, expected_value, option):
2383+ self.assertEqual(expected_value, self.conf.get(option))
2384+
2385+ def test_raring_iso_url(self):
2386+ self.assertValue('http://cdimage.ubuntu.com/daily-live/current/',
2387+ 'vm.iso_url' )
2388+
2389+ def test_raring_iso_name(self):
2390+ self.assertValue( 'raring-desktop-amd64.iso', 'vm.iso_name')
2391+
2392+ def test_raring_cloud_image_url(self):
2393+ self.assertValue('http://cloud-images.ubuntu.com/raring/current/',
2394+ 'vm.cloud_image_url')
2395+
2396+ def test_raring_cloud_image_name(self):
2397+ self.assertValue('raring-server-cloudimg-amd64-disk1.img',
2398+ 'vm.cloud_image_name')
2399+
2400+ def test_apt_proxy_set(self):
2401+ proxy = 'apt_proxy: http://example.org:4321'
2402+ self.conf.set('vm.apt_proxy', proxy)
2403+ self.assertEqual(proxy, self.conf.expand_options('{vm.apt_proxy}'))
2404+
2405+ def test_download_cache_with_user_expansion(self):
2406+ download_cache = '~/installers'
2407+ self.conf.set('vm.download_cache', download_cache)
2408+ self.assertValue(os.path.join(self.home_dir, 'installers'),
2409+ 'vm.download_cache')
2410+
2411+ def test_images_dir_with_user_expansion(self):
2412+ images_dir = '~/images'
2413+ self.conf.set('vm.images_dir', images_dir)
2414+ self.assertValue(os.path.join(self.home_dir, 'images'),
2415+ 'vm.images_dir')
2416+
2417+
2418+class TestPathOption(TestCaseWithHome):
2419+
2420+ def assertConverted(self, expected, value):
2421+ option = setup_vm.PathOption('foo', help='A path.')
2422+ self.assertEquals(expected, option.convert_from_unicode(None, value))
2423+
2424+ def test_absolute_path(self):
2425+ self.assertConverted('/test/path', '/test/path')
2426+
2427+ def test_home_path_with_expansion(self):
2428+ self.assertConverted(self.home_dir, '~')
2429+
2430+ def test_path_in_home_with_expansion(self):
2431+ self.assertConverted(os.path.join(self.home_dir, 'test/path'),
2432+ '~/test/path')
2433+
2434+
2435+class TestDownload(TestCaseWithHome):
2436+
2437+# FIXME: Needs parametrization against vm.{cloud_image_name|iso_name} and
2438+# {download_iso|download_cloud_image} -- vila 2013-02-07
2439+
2440+ def setUp(self):
2441+ # Downloading real isos or images is too long for tests, instead, we
2442+ # fake it by downloading a small but known to exist file: MD5SUMS
2443+ super(TestDownload, self).setUp()
2444+ download_cache = os.path.join(self.test_base_dir, 'downloads')
2445+ os.mkdir(download_cache)
2446+ self.conf = setup_vm.VmStack('foo')
2447+ self.conf.store._load_from_string('''
2448+vm.iso_name=MD5SUMS
2449+vm.cloud_image_name=MD5SUMS
2450+vm.release=raring
2451+vm.cpu_model=amd64
2452+vm.download_cache=%s
2453+''' % (download_cache,))
2454+
2455+ def test_download_iso(self):
2456+ vm = setup_vm.Kvm(self.conf)
2457+ self.assertTrue(vm.download_iso())
2458+ # Trying to download again will find the file in the cache
2459+ self.assertFalse(vm.download_iso())
2460+ # Forcing the download even when the file is present
2461+ self.assertTrue(vm.download_iso(force=True))
2462+
2463+ def test_download_cloud_image(self):
2464+ vm = setup_vm.Kvm(self.conf)
2465+ self.assertTrue(vm.download_cloud_image())
2466+ # Trying to download again will find the file in the cache
2467+ self.assertFalse(vm.download_cloud_image())
2468+ # Forcing the download even when the file is present
2469+ self.assertTrue(vm.download_cloud_image(force=True))
2470+
2471+ def test_download_unknown_iso_fail(self):
2472+ self.conf.set('vm.iso_name', 'I-dont-exist')
2473+ vm = setup_vm.Kvm(self.conf)
2474+ self.assertRaises(setup_vm.CommandError, vm.download_iso)
2475+
2476+ def test_download_unknown_cloud_image_fail(self):
2477+ self.conf.set('vm.cloud_image_name', 'I-dont-exist')
2478+ vm = setup_vm.Kvm(self.conf)
2479+ self.assertRaises(setup_vm.CommandError, vm.download_cloud_image)
2480+
2481+ def test_download_iso_with_unknown_cache_fail(self):
2482+ dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
2483+ self.conf.set('vm.download_cache', dl_cache)
2484+ vm = setup_vm.Kvm(self.conf)
2485+ self.assertRaises(setup_vm.ConfigValueError, vm.download_iso)
2486+
2487+ def test_download_cloud_image_with_unknown_cache_fail(self):
2488+ dl_cache = os.path.join(self.test_base_dir, 'I-dont-exist')
2489+ self.conf.set('vm.download_cache', dl_cache)
2490+ vm = setup_vm.Kvm(self.conf)
2491+ self.assertRaises(setup_vm.ConfigValueError, vm.download_cloud_image)
2492+
2493+
2494+class TestMetaData(TestCaseWithHome):
2495+
2496+ def setUp(self):
2497+ super(TestMetaData, self).setUp()
2498+ self.conf = setup_vm.VmStack('foo')
2499+ self.vm = setup_vm.Kvm(self.conf)
2500+ images_dir = os.path.join(self.test_base_dir, 'images')
2501+ os.mkdir(images_dir)
2502+ config_dir = os.path.join(self.test_base_dir, 'config')
2503+ self.conf.store._load_from_string('''
2504+vm.name=foo
2505+vm.images_dir=%s
2506+vm.config_dir=%s
2507+''' % (images_dir, config_dir,))
2508+
2509+ def test_create_meta_data(self):
2510+ self.vm.create_meta_data()
2511+ self.assertTrue(os.path.exists(self.vm._config_dir))
2512+ self.assertTrue(os.path.exists(self.vm._meta_data_path))
2513+ with open(self.vm._meta_data_path) as f:
2514+ meta_data = f.readlines()
2515+ self.assertEqual(2, len(meta_data))
2516+ self.assertEqual('instance-id: foo\n', meta_data[0])
2517+ self.assertEqual('local-hostname: foo\n', meta_data[1])
2518+
2519+
2520+class TestYaml(testtools.TestCase):
2521+
2522+ def yaml_load(self, *args, **kwargs):
2523+ return setup_vm.yaml.safe_load(*args, **kwargs)
2524+
2525+ def yaml_dump(self, *args, **kwargs):
2526+ return setup_vm.yaml.safe_dump(*args, **kwargs)
2527+
2528+ def test_load_scalar(self):
2529+ self.assertEqual({'foo': 'bar'}, self.yaml_load(StringIO('{foo: bar}')))
2530+ # Surprisingly the enclosing braces are not needed, probably a special
2531+ # case for the highest level
2532+ self.assertEqual({'foo': 'bar'}, self.yaml_load(StringIO('foo: bar')))
2533+
2534+ def test_dump_scalar(self):
2535+ self.assertEqual('{foo: bar}\n', self.yaml_dump(dict(foo='bar')))
2536+
2537+ def test_load_list(self):
2538+ self.assertEqual({'foo': ['a', 'b', 'c']},
2539+ # Single space indentation is enough
2540+ self.yaml_load(StringIO('''\
2541+foo:
2542+ - a
2543+ - b
2544+ - c
2545+''')))
2546+
2547+ def test_dump_list(self):
2548+ # No more enclosing braces... yeah for consistency :-/
2549+ self.assertEqual('foo: [a, b, c]\n',
2550+ self.yaml_dump(dict(foo=['a', 'b', 'c'])))
2551+
2552+ def test_load_dict(self):
2553+ self.assertEqual({'foo': {'bar': 'baz'}},
2554+ self.yaml_load(StringIO('{foo: {bar: baz}}')))
2555+ multiple_lines = '''\
2556+foo: {bar: multiple
2557+ lines}
2558+'''
2559+ self.assertEqual({'foo': {'bar': 'multiple lines'}},
2560+ self.yaml_load(StringIO(multiple_lines)))
2561+
2562+
2563+
2564+class TestLaunchpadAccess(TestCaseWithHome):
2565+
2566+ def setUp(self):
2567+ super(TestLaunchpadAccess, self).setUp()
2568+ self.conf = setup_vm.VmStack('foo')
2569+ self.vm = setup_vm.Kvm(self.conf)
2570+ self.ci_data = setup_vm.CIUserData(self.conf)
2571+
2572+ def test_cant_find_private_key(self):
2573+ self.conf.store._load_from_string('vm.launchpad_id = I-dont-exist')
2574+ e = self.assertRaises(setup_vm.ConfigPathNotFound,
2575+ self.ci_data.set_launchpad_access)
2576+ key_path = '~/.ssh/I-dont-exist@setup_vm'
2577+ self.assertEqual(key_path, e.path)
2578+ self.assertTrue(unicode(e).startswith(
2579+ 'You need to create the {p} keypair'.format(p=key_path)))
2580+
2581+ def test_id_with_key(self):
2582+ ssh_dir = os.path.join(self.home_dir, '.ssh')
2583+ os.mkdir(ssh_dir)
2584+ key_path = os.path.join(ssh_dir, 'user@setup_vm')
2585+ with open(key_path, 'w') as f:
2586+ f.write('key content')
2587+ self.conf.store._load_from_string('vm.launchpad_id = user')
2588+ self.assertEqual(None, self.ci_data.launchpad_hook)
2589+ self.ci_data.set_launchpad_access()
2590+ self.assertEqual('''\
2591+#!/bin/sh
2592+mkdir -p /home/ubuntu/.ssh
2593+chown ubuntu:ubuntu ~ubuntu
2594+chmod 0700 ~ubuntu
2595+chown ubuntu:ubuntu /home/ubuntu/.ssh
2596+chmod 0700 /home/ubuntu/.ssh
2597+cat >/home/ubuntu/.ssh/id_rsa <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
2598+key content
2599+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
2600+chown ubuntu:ubuntu /home/ubuntu/.ssh/id_rsa
2601+chmod 0400 /home/ubuntu/.ssh/id_rsa
2602+''',
2603+ self.ci_data.launchpad_hook)
2604+ cc = self.ci_data.cloud_config
2605+ self.assertEquals([['sudo', '-u', 'ubuntu',
2606+ 'bzr', 'launchpad-login', 'user']],
2607+ cc['runcmd'])
2608+
2609+
2610+class TestCIUserData(TestCaseWithHome):
2611+
2612+ def setUp(self):
2613+ super(TestCIUserData, self).setUp()
2614+ self.conf = setup_vm.VmStack('foo')
2615+ self.ci_data = setup_vm.CIUserData(self.conf)
2616+
2617+ def test_empty_config(self):
2618+ self.ci_data.populate()
2619+ # Check default values
2620+ self.assertIs(None, self.ci_data.root_hook)
2621+ self.assertIs(None, self.ci_data.ubuntu_hook)
2622+ cc = self.ci_data.cloud_config
2623+ self.assertFalse(cc['apt_update'])
2624+ self.assertFalse(cc['apt_upgrade'])
2625+ self.assertEqual({'expire': False}, cc['chpasswd'])
2626+ self.assertEqual('setup_vm finished installing in ${uptime} seconds.',
2627+ cc['final_message'])
2628+ self.assertTrue(cc['manage_etc_hosts'])
2629+ self.assertEqual('ubuntu', cc['password'])
2630+ self.assertEqual({'mode': 'poweroff'}, cc['power_state'])
2631+
2632+ def test_password(self):
2633+ self.conf.store._load_from_string('vm.password = tagada')
2634+ self.ci_data.populate()
2635+ self.assertEquals('tagada', self.ci_data.cloud_config['password'])
2636+
2637+ def test_apt_proxy(self):
2638+ self.conf.store._load_from_string('vm.apt_proxy = tagada')
2639+ self.ci_data.populate()
2640+ self.assertEquals('tagada', self.ci_data.cloud_config['apt_proxy'])
2641+
2642+ def test_final_message_precise(self):
2643+ self.conf.store._load_from_string('vm.release = precise')
2644+ self.ci_data.populate()
2645+ self.assertEqual('setup_vm finished installing in $UPTIME seconds.',
2646+ self.ci_data.cloud_config['final_message'])
2647+
2648+ def test_poweroff_precise(self):
2649+ self.conf.store._load_from_string('vm.release = precise')
2650+ self.ci_data.populate()
2651+ self.assertEqual(['halt'], self.ci_data.cloud_config['runcmd'])
2652+
2653+ def test_poweroff_quantal(self):
2654+ self.conf.store._load_from_string('vm.release = quantal')
2655+ self.ci_data.populate()
2656+ self.assertEqual(['halt'], self.ci_data.cloud_config['runcmd'])
2657+
2658+ def test_poweroff_other(self):
2659+ self.conf.store._load_from_string('vm.release = raring')
2660+ self.ci_data.populate()
2661+ self.assertEqual({'mode': 'poweroff'},
2662+ self.ci_data.cloud_config['power_state'])
2663+ self.assertIs(None, self.ci_data.cloud_config.get('runcmd'))
2664+
2665+ def test_update_true(self):
2666+ self.conf.store._load_from_string('vm.update = True')
2667+ self.ci_data.populate()
2668+ cc = self.ci_data.cloud_config
2669+ self.assertTrue(cc['apt_update'])
2670+ self.assertTrue(cc['apt_upgrade'])
2671+
2672+ def test_packages(self):
2673+ self.conf.store._load_from_string('vm.packages = bzr,ubuntu-desktop')
2674+ self.ci_data.populate()
2675+ self.assertEqual(['bzr', 'ubuntu-desktop'],
2676+ self.ci_data.cloud_config['packages'])
2677+
2678+ def test_apt_sources(self):
2679+ self.conf.store._load_from_string('''\
2680+vm.release = raring
2681+# Ensure options are properly expanded (and comments supported ;)
2682+_archive_url = http://archive.ubuntu.com/ubuntu
2683+_ppa_url = https://u:p@ppa.lp.net/user/ppa/ubuntu
2684+vm.apt_sources = deb {_archive_url} {vm.release} partner,\
2685+ deb {_archive_url} {vm.release} main,\
2686+ deb {_ppa_url} {vm.release} main|ABCDEF
2687+''')
2688+ self.ci_data.populate()
2689+ self.assertEqual(
2690+ [{'source': 'deb http://archive.ubuntu.com/ubuntu raring partner'},
2691+ {'source': 'deb http://archive.ubuntu.com/ubuntu raring main'},
2692+ {'source':
2693+ 'deb https://u:p@ppa.lp.net/user/ppa/ubuntu raring main',
2694+ 'keyid': 'ABCDEF'}],
2695+ self.ci_data.cloud_config['apt_sources'])
2696+
2697+ def create_file(self, path, content):
2698+ with open(path, 'wb') as f:
2699+ f.write(content)
2700+
2701+ def test_good_ssh_keys(self):
2702+ paths = ('rsa', 'rsa.pub', 'dsa', 'dsa.pub', 'ecdsa', 'ecdsa.pub')
2703+ for path in paths:
2704+ self.create_file(path, '%s\ncontent\n' % (path,))
2705+ paths_as_list = ','.join(paths)
2706+ self.conf.store._load_from_string(
2707+ 'vm.ssh_keys = %s' % (paths_as_list,))
2708+ self.ci_data.populate()
2709+ self.assertEqual({'dsa_private': 'dsa\ncontent\n',
2710+ 'dsa_public': 'dsa.pub\ncontent\n',
2711+ 'ecdsa_private': 'ecdsa\ncontent\n',
2712+ 'ecdsa_public': 'ecdsa.pub\ncontent\n',
2713+ 'rsa_private': 'rsa\ncontent\n',
2714+ 'rsa_public': 'rsa.pub\ncontent\n'},
2715+ self.ci_data.cloud_config['ssh_keys'])
2716+
2717+ def test_bad_type_ssh_keys(self):
2718+ self.conf.store._load_from_string('vm.ssh_keys = I-dont-exist')
2719+ self.assertRaises(setup_vm.ConfigValueError, self.ci_data.populate)
2720+
2721+ def test_unknown_ssh_keys(self):
2722+ self.conf.store._load_from_string('vm.ssh_keys = rsa.pub')
2723+ self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
2724+
2725+ def test_good_ssh_authorized_keys(self):
2726+ paths = ('home.pub', 'work.pub')
2727+ for path in paths:
2728+ self.create_file(path, '%s\ncontent\n' % (path,))
2729+ paths_as_list = ','.join(paths)
2730+ self.conf.store._load_from_string(
2731+ 'vm.ssh_authorized_keys = %s' % (paths_as_list,))
2732+ self.ci_data.populate()
2733+ self.assertEqual(['home.pub\ncontent\n', 'work.pub\ncontent\n'],
2734+ self.ci_data.cloud_config['ssh_authorized_keys'])
2735+
2736+ def test_unknown_ssh_authorized_keys(self):
2737+ self.conf.store._load_from_string('vm.ssh_authorized_keys = rsa.pub')
2738+ self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
2739+
2740+ def test_unknown_root_script(self):
2741+ self.conf.store._load_from_string('vm.root_script = I-dont-exist')
2742+ self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
2743+
2744+ def test_unknown_ubuntu_script(self):
2745+ self.conf.store._load_from_string('vm.ubuntu_script = I-dont-exist')
2746+ self.assertRaises(setup_vm.ConfigPathNotFound, self.ci_data.populate)
2747+
2748+ def test_expansion_error_in_script(self):
2749+ root_script_content = '''#!/bin/sh
2750+echo Hello {I_dont_exist}
2751+'''
2752+ with open('root_script.sh', 'w') as f:
2753+ f.write(root_script_content)
2754+ self.conf.store._load_from_string('''\
2755+vm.root_script = root_script.sh
2756+''')
2757+ e = self.assertRaises(errors.ExpandingUnknownOption,
2758+ self.ci_data.populate)
2759+ self.assertEqual(root_script_content, e.string)
2760+
2761+ def test_unknown_uploaded_scripts(self):
2762+ self.conf.store._load_from_string(
2763+ '''vm.uploaded_scripts = I-dont-exist ''')
2764+ e = self.assertRaises(setup_vm.ConfigPathNotFound,
2765+ self.ci_data.populate)
2766+
2767+ def test_root_script(self):
2768+ with open('root_script.sh', 'w') as f:
2769+ f.write('''#!/bin/sh
2770+echo Hello {user}
2771+''')
2772+ self.conf.store._load_from_string('''\
2773+vm.root_script = root_script.sh
2774+user=root
2775+''')
2776+ self.ci_data.populate()
2777+ # The additional newline after the script is expected
2778+ self.assertEqual('''\
2779+#!/bin/sh
2780+cat >~root/setup_vm_post_install <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
2781+#!/bin/sh
2782+echo Hello root
2783+
2784+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
2785+chown root:root ~root/setup_vm_post_install
2786+chmod 0700 ~root/setup_vm_post_install
2787+''', self.ci_data.root_hook)
2788+ self.assertEqual(['~root/setup_vm_post_install'],
2789+ self.ci_data.cloud_config['runcmd'])
2790+
2791+ def test_ubuntu_script(self):
2792+ with open('ubuntu_script.sh', 'w') as f:
2793+ f.write('''#!/bin/sh
2794+echo Hello {user}
2795+''')
2796+ self.conf.store._load_from_string('''\
2797+vm.ubuntu_script = ubuntu_script.sh
2798+user = ubuntu
2799+''')
2800+ self.ci_data.populate()
2801+ # The additional newline after the script is expected
2802+ self.assertEqual('''\
2803+#!/bin/sh
2804+cat >~ubuntu/setup_vm_post_install <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
2805+#!/bin/sh
2806+echo Hello ubuntu
2807+
2808+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
2809+chown ubuntu:ubuntu ~ubuntu/setup_vm_post_install
2810+chmod 0700 ~ubuntu/setup_vm_post_install
2811+''', self.ci_data.ubuntu_hook)
2812+ # The command is run as root, so we need to 'su ubuntu' first
2813+ self.assertEqual([['su', '-l',
2814+ '-c', '~ubuntu/setup_vm_post_install',
2815+ 'ubuntu']],
2816+ self.ci_data.cloud_config['runcmd'])
2817+
2818+ def test_uploaded_scripts(self):
2819+ paths = ('foo', 'bar')
2820+ for path in paths:
2821+ self.create_file(path, '%s\ncontent\n' % (path,))
2822+ paths_as_list = ','.join(paths)
2823+ self.conf.store._load_from_string(
2824+ 'vm.uploaded_scripts = %s' % (paths_as_list,))
2825+ self.ci_data.populate()
2826+ self.assertEqual('''\
2827+#!/bin/sh
2828+cat >~ubuntu/setup_vm_uploads <<'EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN'
2829+mkdir -p ~ubuntu/bin
2830+cd ~ubuntu/bin
2831+cat >foo <<'EOFfoo'
2832+foo
2833+content
2834+
2835+EOFfoo
2836+chmod 0755 foo
2837+cat >bar <<'EOFbar'
2838+bar
2839+content
2840+
2841+EOFbar
2842+chmod 0755 bar
2843+EOSETUPVMCONTENTREALLYUNIQUEDONTBREAKFORFUN
2844+chown ubuntu:ubuntu ~ubuntu/setup_vm_uploads
2845+chmod 0700 ~ubuntu/setup_vm_uploads
2846+''',
2847+ self.ci_data.uploaded_scripts_hook)
2848+ self.assertEqual([['su', '-l',
2849+ '-c', '~ubuntu/setup_vm_uploads',
2850+ 'ubuntu']],
2851+ self.ci_data.cloud_config['runcmd'])
2852+
2853+
2854+class TestCreateUserData(TestCaseWithHome):
2855+
2856+ def setUp(self):
2857+ super(TestCreateUserData, self).setUp()
2858+ self.conf = setup_vm.VmStack('foo')
2859+ self.vm = setup_vm.Kvm(self.conf)
2860+
2861+ def test_empty_config(self):
2862+ config_dir = os.path.join(self.test_base_dir, 'config')
2863+ os.mkdir(config_dir)
2864+ # The config is *almost* empty, we need to set config_dir though as the
2865+ # user-data needs to be stored there.
2866+ self.conf.store._load_from_string('vm.config_dir=%s' % (config_dir,))
2867+ self.vm.create_user_data()
2868+ self.assertTrue(os.path.exists(self.vm._config_dir))
2869+ self.assertTrue(os.path.exists(self.vm._user_data_path))
2870+ with open(self.vm._user_data_path) as f:
2871+ user_data = f.readlines()
2872+ # We care about the two first lines only here, checking the format (or
2873+ # cloud-init is confused)
2874+ self.assertEqual('#cloud-config-archive\n', user_data[0])
2875+ self.assertEqual("- {content: '#cloud-config\n", user_data[1])
2876+
2877+
2878+class TestSeed(TestCaseWithHome):
2879+
2880+ def setUp(self):
2881+ super(TestSeed, self).setUp()
2882+ self.conf = setup_vm.VmStack('foo')
2883+ self.vm = setup_vm.Kvm(self.conf)
2884+ images_dir = os.path.join(self.test_base_dir, 'images')
2885+ os.mkdir(images_dir)
2886+ config_dir = os.path.join(self.test_base_dir, 'config')
2887+ self.conf.store._load_from_string('''
2888+vm.name=foo
2889+vm.release=raring
2890+vm.config_dir=%s
2891+vm.images_dir=%s
2892+''' % (config_dir, images_dir,))
2893+
2894+ def test_create_meta_data(self):
2895+ self.vm.create_meta_data()
2896+ self.assertTrue(os.path.exists(self.vm._meta_data_path))
2897+
2898+ def test_create_user_data(self):
2899+ self.vm.create_user_data()
2900+ self.assertTrue(os.path.exists(self.vm._user_data_path))
2901+
2902+ def test_create_seed(self):
2903+ self.assertTrue(self.vm._seed_path is None)
2904+ self.vm.create_seed()
2905+ self.assertFalse(self.vm._seed_path is None)
2906+ self.assertTrue(os.path.exists(self.vm._seed_path))
2907+
2908+
2909+class TestImageFromCloud(TestCaseWithHome):
2910+
2911+ def setUp(self):
2912+ super(TestImageFromCloud, self).setUp()
2913+ self.conf = setup_vm.VmStack('foo')
2914+ self.vm = setup_vm.KvmFromCloudImage(self.conf)
2915+ images_dir = os.path.join(self.test_base_dir, 'images')
2916+ os.mkdir(images_dir)
2917+ download_cache_dir = os.path.join(self.test_base_dir, 'download')
2918+ os.mkdir(download_cache_dir)
2919+ self.conf.store._load_from_string('''
2920+vm.name=foo
2921+vm.release=raring
2922+vm.images_dir=%s
2923+vm.download_cache=%s
2924+vm.cloud_image_name=fake.img
2925+vm.disk_size=1M
2926+''' % (images_dir, download_cache_dir))
2927+
2928+ def test_create_disk_image(self):
2929+ cloud_image_path = os.path.join(self.conf.get('vm.download_cache'),
2930+ self.conf.get('vm.cloud_image_name'))
2931+ # We need a fake cloud image that can be converted
2932+ setup_vm.run_subprocess(
2933+ ['sudo', 'qemu-img', 'create',
2934+ cloud_image_path, self.conf.get('vm.disk_size')])
2935+ self.assertTrue(self.vm._disk_image_path is None)
2936+ self.vm.create_disk_image()
2937+ self.assertFalse(self.vm._disk_image_path is None)
2938+ self.assertTrue(os.path.exists(self.vm._disk_image_path))
2939+
2940+
2941+class TestImageWithBacking(TestCaseWithHome):
2942+
2943+ def setUp(self):
2944+ (download_cache_dir,
2945+ reference_cloud_image_name) = requires_known_reference_image(self)
2946+ super(TestImageWithBacking, self).setUp()
2947+ # We'll share the images_dir between vms
2948+ images_dir = os.path.join(self.test_base_dir, 'images')
2949+ os.mkdir(images_dir)
2950+ # Create a shared config
2951+ conf = setup_vm.VmStack(None)
2952+ conf.store._load_from_string('''
2953+vm.release=raring
2954+vm.images_dir=%s
2955+vm.download_cache=%s
2956+vm.disk_size=2G
2957+[selftest-from-cloud]
2958+vm.name=selftest-from-cloud
2959+vm.cloud_image_name=%s
2960+[selftest-backing]
2961+vm.name=selftest-backing
2962+vm.backing=selftest-from-cloud.qcow2
2963+''' % (images_dir, download_cache_dir, reference_cloud_image_name))
2964+ conf.store.save()
2965+ # To bypass creating a real vm, we start from the cloud image that is a
2966+ # real and bootable one, so we just convert it. That also makes it
2967+ # available in vm.images_dir
2968+ temp_vm = setup_vm.KvmFromCloudImage(
2969+ setup_vm.VmStack('selftest-from-cloud'))
2970+ temp_vm.create_disk_image()
2971+
2972+ def test_create_image_with_backing(self):
2973+ vm = setup_vm.KvmFromBacking(setup_vm.VmStack('selftest-backing'))
2974+ self.assertTrue(vm._disk_image_path is None)
2975+ vm.create_disk_image()
2976+ self.assertFalse(vm._disk_image_path is None)
2977+ self.assertTrue(os.path.exists(vm._disk_image_path))
2978+
2979+
2980+class TestVmStates(testtools.TestCase):
2981+
2982+ def assertStates(self, expected, lines):
2983+ self.assertEqual(expected, setup_vm.vm_states(lines))
2984+
2985+ def test_empty(self):
2986+ self.assertStates({},[])
2987+
2988+ def test_garbage(self):
2989+ self.assertRaises(ValueError, self.assertStates, None, [''])
2990+
2991+ def test_known_states(self):
2992+ # From a real life sample
2993+ self.assertStates({'foo': 'shut off', 'bar': 'running'},
2994+ ['- foo shut off',
2995+ '19 bar running'])
2996+
2997+
2998+class TestConsoleParsing(testtools.TestCase):
2999+
3000+ def _parse_console_monitor(self, string):
3001+ mon = setup_vm.ConsoleMonitor(StringIO(string))
3002+ lines = []
3003+ for line in mon.parse():
3004+ lines.append(line)
3005+ return lines
3006+
3007+ def test_fails_on_empty(self):
3008+ self.assertRaises(setup_vm.ConsoleEOFError,
3009+ self._parse_console_monitor, '')
3010+
3011+ def test_fail_on_knwon_cloud_init_errors(self):
3012+ self.assertRaises(
3013+ setup_vm.CloudInitError,
3014+ self._parse_console_monitor,
3015+ 'Failed loading yaml blob\n')
3016+ self.assertRaises(
3017+ setup_vm.CloudInitError,
3018+ self._parse_console_monitor,
3019+ 'Unhandled non-multipart userdata starting\n')
3020+ self.assertRaises(
3021+ setup_vm.CloudInitError,
3022+ self._parse_console_monitor,
3023+ "failed to render string to stdout: cannot find 'uptime'\n")
3024+ self.assertRaises(
3025+ setup_vm.CloudInitError,
3026+ self._parse_console_monitor,
3027+ "Failed loading of cloud config "
3028+ "'/var/lib/cloud/instance/cloud-config.txt'. "
3029+ "Continuing with empty config\n")
3030+
3031+ def test_succeds_on_final_message(self):
3032+ lines = self._parse_console_monitor('''
3033+Lalala
3034+I'm doing my work
3035+It goes nicely
3036+setup_vm finished installing in 1 seconds.
3037+That was fast isn't it ?
3038+ * Will now halt
3039+[ 33.204755] Power down.
3040+''')
3041+ # We stop as soon as we get the final message and ignore the rest
3042+ self.assertEquals(' * Will now halt\n',
3043+ lines[-1])
3044+
3045+
3046+class TestConsoleParsingWithFile(TestCaseWithHome):
3047+
3048+ def _parse_file_monitor(self, string):
3049+ with open('console', 'w') as f:
3050+ f.write(string)
3051+ mon = setup_vm.FileMonitor('console')
3052+ for line in mon.parse():
3053+ pass
3054+ return mon.lines
3055+
3056+ def test_succeeds_with_file(self):
3057+ content = '''\
3058+Yet another install
3059+Going well
3060+setup_vm finished installing in 0.5 seconds.
3061+Wow, even faster !
3062+ * Will now halt
3063+Whatever, won't read that
3064+'''
3065+ lines = self._parse_file_monitor(content)
3066+
3067+ def xtest_fails_on_empty_file(self):
3068+ # FIXME: We need some sort of timeout there...
3069+ self.assertRaises(setup_vm.CommandError, self._parse_file_monitor, '')
3070+
3071+ def test_fail_on_knwon_cloud_init_errors_with_file(self):
3072+ self.assertRaises(
3073+ setup_vm.CloudInitError,
3074+ self._parse_file_monitor,
3075+ 'Failed loading yaml blob\n')
3076+ self.assertRaises(
3077+ setup_vm.CloudInitError,
3078+ self._parse_file_monitor,
3079+ 'Unhandled non-multipart userdata starting\n')
3080+ self.assertRaises(
3081+ setup_vm.CloudInitError,
3082+ self._parse_file_monitor,
3083+ "failed to render string to stdout: cannot find 'uptime'\n")
3084+
3085+
3086+class TestInstallWithSeed(TestCaseWithHome):
3087+
3088+ def setUp(self):
3089+ (download_cache,
3090+ reference_cloud_image_name) = requires_known_reference_image(self)
3091+ super(TestInstallWithSeed, self).setUp()
3092+ # We need to allow other users to read this dir
3093+ os.chmod(self.test_base_dir, 0755)
3094+ # We also need to sudo rm it as root created some files there
3095+ self.addCleanup(
3096+ setup_vm.run_subprocess,
3097+ ['sudo', 'rm', '-fr',
3098+ os.path.join(self.test_base_dir, 'home', '.virtinst')])
3099+ self.conf = setup_vm.VmStack('selftest-seed')
3100+ self.vm = setup_vm.KvmFromCloudImage(self.conf)
3101+ images_dir = os.path.join(self.test_base_dir, 'images')
3102+ os.mkdir(images_dir, 0755)
3103+ config_dir = os.path.join(self.test_base_dir, 'config')
3104+ self.conf.store._load_from_string('''
3105+vm.name=selftest-seed
3106+vm.update=False # Shorten install time
3107+vm.cpus=2,
3108+vm.release=raring
3109+vm.config_dir=%s
3110+vm.images_dir=%s
3111+vm.download_cache=%s
3112+vm.cloud_image_name=%s
3113+vm.disk_size=8G
3114+''' % (config_dir, images_dir, download_cache, reference_cloud_image_name))
3115+
3116+ def assertVmState(self, expected):
3117+ states = setup_vm.vm_states()
3118+ self.assertEqual(expected, states[self.vm.conf.get('vm.name')])
3119+
3120+ def test_install_with_seed(self):
3121+ self.addCleanup(self.vm.undefine)
3122+ self.vm.install()
3123+ self.assertVmState('shut off')
3124+
3125+
3126+class TestInstallWithBacking(TestCaseWithHome):
3127+
3128+ def setUp(self):
3129+ (download_cache_dir,
3130+ reference_cloud_image_name) = requires_known_reference_image(self)
3131+ super(TestInstallWithBacking, self).setUp()
3132+ # We need to allow other users to read this dir
3133+ os.chmod(self.test_base_dir, 0755)
3134+ # We also need to sudo rm it as root created some files there
3135+ self.addCleanup(
3136+ setup_vm.run_subprocess,
3137+ ['sudo', 'rm', '-fr',
3138+ os.path.join(self.test_base_dir, 'home', '.virtinst')])
3139+ self.conf = setup_vm.VmStack('selftest-backing')
3140+ self.vm = setup_vm.KvmFromBacking(self.conf)
3141+ # We'll share the images_dir between vms
3142+ images_dir = os.path.join(self.test_base_dir, 'images')
3143+ os.mkdir(images_dir, 0755)
3144+ config_dir = os.path.join(self.test_base_dir, 'config')
3145+ # Create a shared config
3146+ conf = setup_vm.VmStack(None)
3147+ conf.store._load_from_string('''
3148+vm.release=raring
3149+vm.config_dir=%s
3150+vm.images_dir=%s
3151+vm.download_cache=%s
3152+vm.disk_size=2G
3153+vm.update=False # Shorten install time
3154+[selftest-from-cloud]
3155+vm.name=selftest-from-cloud
3156+vm.cloud_image_name=%s
3157+[selftest-backing]
3158+vm.name=selftest-backing
3159+vm.backing=selftest-from-cloud.qcow2
3160+''' % (config_dir, images_dir, download_cache_dir, reference_cloud_image_name))
3161+ conf.store.save()
3162+ # Fake a previous install by just re-using the reference cloud image
3163+ temp_vm = setup_vm.KvmFromCloudImage(
3164+ setup_vm.VmStack('selftest-from-cloud'))
3165+ temp_vm.create_disk_image()
3166+
3167+ def assertVmState(self, vm, expected):
3168+ states = setup_vm.vm_states()
3169+ self.assertEqual(expected, states[vm.conf.get('vm.name')])
3170+
3171+ def test_install_with_backing(self):
3172+ vm = setup_vm.KvmFromBacking(setup_vm.VmStack('selftest-backing'))
3173+ self.addCleanup(vm.undefine)
3174+ vm.install()
3175+ self.assertVmState(vm, 'shut off')
3176+
3177+
3178+class TestSshKeyGen(TestCaseWithHome):
3179+
3180+ def setUp(self):
3181+ super(TestSshKeyGen, self).setUp()
3182+ self.conf = setup_vm.VmStack(None)
3183+ self.vm = setup_vm.VM(self.conf)
3184+ self.config_dir = os.path.join(self.test_base_dir, 'config')
3185+
3186+ def load_config(self, more):
3187+ content = '''\
3188+vm.config_dir=%s
3189+vm.name=foo
3190+''' % (self.config_dir,)
3191+ self.conf.store._load_from_string(content + more)
3192+
3193+ def generate_key(self, ssh_type, upper_type=None):
3194+ if upper_type is None:
3195+ upper_type = ssh_type.upper()
3196+ self.load_config('vm.ssh_keys={vm.config_dir}/%s' % (ssh_type,))
3197+ self.vm.ssh_keygen()
3198+ private_path = 'config/%s' % (ssh_type,)
3199+ self.assertTrue(os.path.exists(private_path))
3200+ public_path = 'config/%s.pub' % (ssh_type,)
3201+ self.assertTrue(os.path.exists(public_path))
3202+ public = file(public_path).read()
3203+ private = file(private_path).read()
3204+ self.assertTrue(private.startswith(
3205+ '-----BEGIN %s PRIVATE KEY-----\n' % (upper_type,)))
3206+ self.assertTrue(private.endswith(
3207+ '-----END %s PRIVATE KEY-----\n' % (upper_type,)))
3208+ return private, public
3209+
3210+ def test_dsa(self):
3211+ private, public = self.generate_key('dsa')
3212+ self.assertTrue(public.startswith('ssh-dss '))
3213+ self.assertTrue(public.endswith(' foo\n'))
3214+
3215+ def test_rsa(self):
3216+ private, public = self.generate_key('rsa')
3217+ self.assertTrue(public.startswith('ssh-rsa '))
3218+ self.assertTrue(public.endswith(' foo\n'))
3219+
3220+ def test_ecdsa(self):
3221+ private, public = self.generate_key('ecdsa', 'EC')
3222+ self.assertTrue(public.startswith('ecdsa-sha2-nistp256 '))
3223+ self.assertTrue(public.endswith(' foo\n'))
3224+
3225+
3226+class TestOptionParsing(testtools.TestCase):
3227+
3228+ def setUp(self):
3229+ super(TestOptionParsing, self).setUp()
3230+ self.out = StringIO()
3231+ self.err = StringIO()
3232+
3233+ def parse_args(self, args):
3234+ return setup_vm.arg_parser.parse_args(args, self.out, self.err)
3235+
3236+ def test_nothing(self):
3237+ self.assertRaises(SystemExit, self.parse_args, [])
3238+
3239+ def test_install(self):
3240+ ns = self.parse_args(['foo', '--install'])
3241+ self.assertEquals('foo', ns.name)
3242+ self.assertTrue(ns.install)
3243+ self.assertFalse(ns.download)
3244+
3245+ def test_download(self):
3246+ ns = self.parse_args(['foo', '--download'])
3247+ self.assertEquals('foo', ns.name)
3248+ self.assertFalse(ns.install)
3249+ self.assertTrue(ns.download)
3250+
3251+class TestBuildCommands(testtools.TestCase):
3252+
3253+ def setUp(self):
3254+ super(TestBuildCommands, self).setUp()
3255+ self.out = StringIO()
3256+ self.err = StringIO()
3257+
3258+ def build_commands(self, args):
3259+ return setup_vm.build_commands(args, self.out, self.err)
3260+
3261+ def test_install(self):
3262+ cmds = self.build_commands(['--install', 'foo'])
3263+ self.assertEqual(1, len(cmds))
3264+ self.assertTrue(isinstance(cmds[0], setup_vm.Install))
3265+
3266+ def test_download(self):
3267+ cmds = self.build_commands(['--download', 'foo'])
3268+ self.assertEqual(1, len(cmds))
3269+ self.assertTrue(isinstance(cmds[0], setup_vm.Download))
3270+
3271+ def test_ssh_keygen(self):
3272+ cmds = self.build_commands(['--ssh-keygen', 'foo'])
3273+ self.assertEqual(1, len(cmds))
3274+ self.assertTrue(isinstance(cmds[0], setup_vm.SshKeyGen))
3275+
3276+ def test_download_and_install(self):
3277+ cmds = self.build_commands(['--install', '--download', 'foo'])
3278+ self.assertEqual(2, len(cmds))
3279+ # Download comes first
3280+ self.assertTrue(isinstance(cmds[0], setup_vm.Download))
3281+ self.assertTrue(isinstance(cmds[1], setup_vm.Install))
3282+
3283+
3284+# FIXME: This needs to be parametrized for KvmFromCloudImage and
3285+# KvmFromBacking. Since we don't define vm.backing below, we're only testing
3286+# KvmFromCloudImage for now. -- vila 2013-02-13
3287+class TestInstall(TestCaseWithHome):
3288+
3289+ def setUp(self):
3290+ super(TestInstall, self).setUp()
3291+ self.conf = setup_vm.VmStack('I-dont-exist')
3292+ self.conf.store._load_from_string('''
3293+vm.name=I-dont-exist
3294+vm.release=raring
3295+vm.cpu_model=amd64
3296+''')
3297+ self.states = []
3298+
3299+ def vm_states(source=None):
3300+ return self.states
3301+ self.patch(setup_vm, 'vm_states', vm_states)
3302+ self.vm = None
3303+
3304+ def install(self):
3305+ class FakeKvm(setup_vm.Kvm):
3306+
3307+ def __init__(self, conf):
3308+ super(FakeKvm, self).__init__(conf)
3309+ self.undefine_called = False
3310+ self.install_called = False
3311+
3312+ # Make sure we avoid dangerous or costly calls
3313+ def poweroff(self):
3314+ pass
3315+
3316+ def undefine(self):
3317+ self.undefine_called = True
3318+
3319+ def install(self):
3320+ self.install_called = True
3321+
3322+
3323+ self.vm = FakeKvm(self.conf)
3324+ cmd = setup_vm.Install(self.vm)
3325+ cmd.run()
3326+
3327+ def test_install_while_running(self):
3328+ self.conf.set('vm.name', 'foo')
3329+ self.states = {'foo': 'running'}
3330+ self.assertRaises(setup_vm.SetupVmError, self.install)
3331+ self.assertFalse(self.vm.install_called)
3332+ self.assertFalse(self.vm.undefine_called)
3333+
3334+ def test_install_unknown(self):
3335+ self.states = {}
3336+ self.install()
3337+ self.assertTrue(self.vm.install_called)
3338+ self.assertFalse(self.vm.undefine_called)
3339+
3340+ def test_install_shutoff(self):
3341+ self.conf.set('vm.name', 'foo')
3342+ self.states = {'foo': 'shut off'}
3343+ self.install()
3344+ self.assertTrue(self.vm.install_called)
3345+ self.assertTrue(self.vm.undefine_called)
3346
3347=== added file 'setup_vm/tests/test_test.py'
3348--- setup_vm/tests/test_test.py 1970-01-01 00:00:00 +0000
3349+++ setup_vm/tests/test_test.py 2013-04-12 15:44:25 +0000
3350@@ -0,0 +1,58 @@
3351+import os
3352+
3353+import testtools
3354+
3355+import tests
3356+
3357+
3358+def assertTestSuccess(test, inner):
3359+ """The received test runs successfully."""
3360+ result = testtools.TestResult()
3361+ inner.run(result)
3362+ test.assertEqual(0, len(result.errors) + len(result.failures))
3363+ test.assertEqual(1, result.testsRun)
3364+ return result
3365+
3366+
3367+class TestEnv(testtools.TestCase):
3368+
3369+
3370+ def test_env_preserved(self):
3371+ os.environ['NOBODY_USES_THIS'] = 'foo'
3372+
3373+ class Inner(testtools.TestCase):
3374+
3375+ def test_overridden(self):
3376+ tests.isolate_env(self, {'NOBODY_USES_THIS': 'bar'})
3377+ self.assertEqual('bar', os.environ['NOBODY_USES_THIS'])
3378+
3379+ assertTestSuccess(self, Inner('test_overridden'))
3380+ self.assertEqual('foo', os.environ['NOBODY_USES_THIS'])
3381+
3382+ def test_env_var_deleted(self):
3383+ os.environ['NOBODY_USES_THIS'] = 'foo'
3384+
3385+ class Inner(testtools.TestCase):
3386+
3387+ def test_deleted(self):
3388+ tests.isolate_env(self, {'NOBODY_USES_THIS': None})
3389+ self.assertIs('deleted',
3390+ os.environ.get('NOBODY_USES_THIS', 'deleted'))
3391+ assertTestSuccess(self, Inner('test_deleted'))
3392+ self.assertEqual('foo', os.environ['NOBODY_USES_THIS'])
3393+
3394+
3395+class TestTmp(testtools.TestCase):
3396+
3397+ def test_cwd_in_tmp(self):
3398+
3399+ class Inner(testtools.TestCase):
3400+
3401+ def setUp(self):
3402+ super(Inner, self).setUp()
3403+ tests.set_cwd_to_tmp(self)
3404+
3405+ def test_cwd_in_tmp(self):
3406+ self.assertEqual(os.getcwdu(), self.test_base_dir)
3407+
3408+ assertTestSuccess(self, Inner('test_cwd_in_tmp'))
3409
3410=== added directory 'setup_vm/u1'
3411=== added file 'setup_vm/u1/install'
3412--- setup_vm/u1/install 1970-01-01 00:00:00 +0000
3413+++ setup_vm/u1/install 2013-04-12 15:44:25 +0000
3414@@ -0,0 +1,62 @@
3415+#!/bin/sh -ex
3416+
3417+# Allow ssh access to launchpad.
3418+# This should probably be provided by setup_vm. -- vila 2013-03-10
3419+ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
3420+# Use the openjdk.
3421+sudo update-alternatives --set java /usr/lib/jvm/java-7-openjdk-amd64/jre/bin/java
3422+# Get the branch.
3423+bzr branch lp:ubuntuone-servers {u1.src_dir}
3424+# Setup the environment.
3425+cd {u1.src_dir}
3426+# Set up the correct configuration.
3427+cat <<EOF >configs/local.conf
3428+[meta]
3429+extends: development-appserver-lazr.conf
3430+
3431+[general]
3432+port: {u1.port}
3433+django_module: u1servers.web.localsettings
3434+
3435+[upay]
3436+consumer_id: U1
3437+port: {pay.port}
3438+hostname: {pay.address}
3439+url_format: http://%(host)s:%(port)d/api/2.0
3440+
3441+[upay_u1ms]
3442+consumer_id: U1
3443+port: {pay.port}
3444+hostname: {pay.address}
3445+url_format: http://%(host)s:%(port)d/api/2.0
3446+
3447+[url]
3448+openid_sso_server: {sso.url}
3449+
3450+EOF
3451+cat <<EOF >servers/u1servers/web/localsettings.py
3452+from u1servers.web.devsettings import *
3453+
3454+OPENID_SSO_SERVER_URL = config.url.openid_sso_server
3455+OPENID_SSO_LOGOUT_URL = '%s/+logout?return_to=%s' % (
3456+ OPENID_SSO_SERVER_URL, BASE_URL)
3457+
3458+if __name__ == os.environ.get("DJANGO_SETTINGS_MODULE"):
3459+ # This only gets executed if the configured DJANGO_SETTINGS_MODULE matches
3460+ # the current module name.
3461+ from ubuntuone import dispatch
3462+ dispatch.connect_all(async=True)
3463+
3464+ from u1servers.web import email
3465+ email.connect_receivers()
3466+
3467+ # Triggered when the env variable U1_PAY_HOST is defined with
3468+ # "<hostname>:<port>"
3469+ if config.upay.hostname or config.upay_u1ms.hostname:
3470+ from u1backends.account.upayclient import init_payclient
3471+ init_payclient()
3472+EOF
3473+make update-sourcedeps
3474+# TODO ask on #u1-ops if there's a better way.
3475+sed -i 's/development-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
3476+sed -i 's/development-appserver-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
3477
3478=== added file 'setup_vm/u1/run'
3479--- setup_vm/u1/run 1970-01-01 00:00:00 +0000
3480+++ setup_vm/u1/run 2013-04-12 15:44:25 +0000
3481@@ -0,0 +1,4 @@
3482+#!/bin/sh
3483+
3484+cd ~/{u1.src_dir}
3485+HOSTNAME={u1.address} DJANGO_SETTINGS_MODULE=u1servers.web.localsettings U1CONFIG=`pwd`/configs/local.conf make start
3486
3487=== added file 'setup_vm/u1/test'
3488--- setup_vm/u1/test 1970-01-01 00:00:00 +0000
3489+++ setup_vm/u1/test 2013-04-12 15:44:25 +0000
3490@@ -0,0 +1,15 @@
3491+#!/bin/sh
3492+
3493+
3494+cd {u1.src_dir}
3495+# When run from the host against the u1 guest:
3496+# sudo apt-get install python-mocker
3497+# scp ubuntu@{u1.address}:~/ubuntuone-servers/configs/local.conf configs/local.conf
3498+# scp ubuntu@{u1.address}:~/ubuntuone-servers/servers/u1servers/web/localsettings.py servers/u1servers/web/localsettings.py
3499+# TODO ask on #u1-ops if there's a better way.
3500+# sed -i 's/development-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
3501+# sed -i 's/development-appserver-lazr.conf/local.conf/g' utilities/supervisor-dev.conf.tpl
3502+# echo 9999 >tmp/statsd.port
3503+# make update-sourcedeps
3504+U1CONFIG=`pwd`/configs/local.conf make smoke-test
3505+U1CONFIG=`pwd`/configs/local.conf make acceptance-test
3506
3507=== added directory 'setup_vm/unity'
3508=== added file 'setup_vm/unity/install-sources'
3509--- setup_vm/unity/install-sources 1970-01-01 00:00:00 +0000
3510+++ setup_vm/unity/install-sources 2013-04-12 15:44:25 +0000
3511@@ -0,0 +1,19 @@
3512+#!/bin/sh
3513+# Allow ssh access to launchpad.
3514+# This should probably be provided by setup_vm. -- vila 2013-03-10
3515+ssh-keyscan bazaar.launchpad.net >>~/.ssh/known_hosts
3516+# Install sst and dependencies from source
3517+mkdir src
3518+cd src
3519+bzr branch lp:~ubuntuone-hackers/ubuntuone-servers/selenium
3520+bzr branch lp:~canonical-isd-qa/selenium-simple-test/trunk selenium-simple-test
3521+cd ~/src/selenium
3522+python setup.py install --user
3523+cd ~/src/selenium-simple-test
3524+python setup.py install --user
3525+
3526+# If the need arise to use sst-run, it may become necessary to create a
3527+# symlink to /home/ubuntu/.local/bin/sst-run
3528+
3529+# Also note that unittest2 is installed as a side-effect of installing sst
3530+# even if we're already using pyton-2.7 (which includes unittest2 features).
3531
3532=== added file 'setup_vm/unity/run-sso-client'
3533--- setup_vm/unity/run-sso-client 1970-01-01 00:00:00 +0000
3534+++ setup_vm/unity/run-sso-client 2013-04-12 15:44:25 +0000
3535@@ -0,0 +1,11 @@
3536+#!/bin/sh -ex
3537+
3538+# We can use U1_DEBUG=True to get debug messages on the console.
3539+USSOC_SERVICE_URL={sso.url}/api/1.0/ /usr/lib/ubuntu-sso-client/ubuntu-sso-login &
3540+# XXX ugly sleep.
3541+sleep 5s
3542+# TODO x86_64 sounds like trouble.
3543+# This has just stopped working on raring. See http://pad.lv/1161067
3544+# TODO in order for the application to be accessible with testability, we need
3545+# TESTABILITY=1
3546+/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/
3547
3548=== added file 'setup_vm/unity/run-syncdaemon'
3549--- setup_vm/unity/run-syncdaemon 1970-01-01 00:00:00 +0000
3550+++ setup_vm/unity/run-syncdaemon 2013-04-12 15:44:25 +0000
3551@@ -0,0 +1,4 @@
3552+#!/bin/sh -ex
3553+
3554+/usr/lib/ubuntuone-client/ubuntuone-syncdaemon --disable_ssl_verify --dns_srv=None --host={filesync.address} &
3555+u1sdtool --connect
3556
3557=== added file 'setup_vm/unity/run-unity-lens-music'
3558--- setup_vm/unity/run-unity-lens-music 1970-01-01 00:00:00 +0000
3559+++ setup_vm/unity/run-unity-lens-music 2013-04-12 15:44:25 +0000
3560@@ -0,0 +1,6 @@
3561+#!/bin/sh -ex
3562+
3563+# TODO x86_64 sounds like trouble.
3564+# We can use G_MESSAGES_DEBUG=all to get debug messages on the console.
3565+# TODO change the name of the environment variables, it's not just staging.
3566+pkill unity-music; U1_STAGING_WEBAPI={u1.url} U1_STAGING_AUTHENTICATION={sso.url} /usr/lib/x86_64-linux-gnu/unity-lens-music/unity-musicstore-daemon
3567
3568=== added file 'setup_vm/unity/transient-dist-upgrade'
3569--- setup_vm/unity/transient-dist-upgrade 1970-01-01 00:00:00 +0000
3570+++ setup_vm/unity/transient-dist-upgrade 2013-04-12 15:44:25 +0000
3571@@ -0,0 +1,4 @@
3572+#!/bin/sh
3573+# For an unclear reason (probably a transient raring issue) we need to
3574+# dist-upgrade instead of just upgrade
3575+apt-get dist-upgrade -y
3576
3577=== added file 'setup_vm/vms.conf'
3578--- setup_vm/vms.conf 1970-01-01 00:00:00 +0000
3579+++ setup_vm/vms.conf 2013-04-12 15:44:25 +0000
3580@@ -0,0 +1,96 @@
3581+# This must be defined in some other vms.conf file (user or system)
3582+# sso.address=sso.local
3583+# pay.address=pay.local
3584+# u1.address=u1.local
3585+# ppa.ubuntuone-hackers.password
3586+
3587+sso.src_dir=canonical-identity-provider
3588+sso.port=8001
3589+sso.url=http://{sso.address}:{sso.port}
3590+sso.imap_port=2143
3591+sso.smtp_port=2025
3592+
3593+pay.src_dir=canonical-payment-service
3594+pay.port=8002
3595+pay.url=http://{pay.address}:{pay.port}
3596+
3597+ppa.ubuntuone_hackers=deb https://{vm.launchpad_id}:{ppa.ubuntuone_hackers.password}@private-ppa.launchpad.net/ubuntuone/hackers/ubuntu {vm.release} main|4BD0ECAE
3598+
3599+u1.src_dir=ubuntuone-servers
3600+u1.port=8003
3601+u1.url=http://{u1.address}:{u1.port}
3602+
3603+[precise-server-pristine]
3604+vm.name=precise-server-pristine
3605+vm.release=precise
3606+vm.packages=bzr, avahi-daemon, emacs23
3607+vm.update=True
3608+
3609+[sso]
3610+vm.name=sso
3611+vm.release=precise
3612+vm.backing=precise-server-pristine.qcow2
3613+vm.packages=config-manager, fabric, libpq-dev, make, memcached, postgresql-plpython, python-m2crypto, python-dev, python-setuptools, python-virtualenv, swig, wget, libxml2-dev, libxslt1-dev
3614+vm.ubuntu_script=sso/install
3615+vm.update=True
3616+vm.uploaded_scripts=sso/run, sso/run-for-pay, sso/run-for-u1
3617+
3618+[pay]
3619+vm.name=pay
3620+vm.release=precise
3621+vm.backing=precise-server-pristine.qcow2
3622+vm.packages=config-manager, fabric, libpq-dev, make, postgresql-plpython, python-dev, python-setuptools, python-virtualenv, wget, libxml2-dev, libxslt1-dev
3623+vm.ubuntu_script=pay/install
3624+vm.update=True
3625+vm.uploaded_scripts=pay/run, pay/run-for-u1
3626+
3627+[u1]
3628+vm.name=u1
3629+vm.release=precise
3630+vm.backing=precise-server-pristine.qcow2
3631+vm.apt_sources={ppa.ubuntuone_hackers}
3632+vm.packages=openjdk-7-jre,ubuntuone-developer-dependencies
3633+vm.ubuntu_script=u1/install
3634+vm.update=True
3635+vm.uploaded_scripts=u1/run
3636+
3637+[raring-desktop-pristine]
3638+vm.name=raring-desktop-pristine
3639+vm.release=raring
3640+# python-unittest2 is not strictly required here but works around sst
3641+# insisting on installing it locally.
3642+vm.packages=bzr, emacs23, python-setuptools, python-unittest2, python-autopilot, unity-autopilot, ubuntu-desktop, avahi-daemon
3643+vm.update=True
3644+# Roughly all vms installing ubuntu-desktop need to complete the
3645+# installation by making the ubuntu user part of the admin group.
3646+vm.root_script = bin/ubuntu_admin.sh
3647+
3648+[purchase-testing]
3649+vm.name=purchase-testing
3650+vm.release=raring
3651+vm.backing=raring-desktop-pristine.qcow2
3652+vm.apt_sources=deb http://ppa.launchpad.net/ubuntuone/dashpurchase-testing/ubuntu {vm.release} main|4BD0ECAE
3653+vm.update=True
3654+vm.ubuntu_script=purchase-testing/install
3655+
3656+[unity-prevalidation]
3657+vm.name=unity-prevalidation
3658+vm.release=raring
3659+vm.backing=raring-desktop-pristine.qcow2
3660+vm.apt_sources=deb http://ppa.launchpad.net/ubuntu-unity/experimental-prevalidation/ubuntu {vm.release} main|52D62F45
3661+vm.uploaded_scripts=unity/run-sso-client, unity/run-unity-lens-music
3662+# TODO unity/run-syncdaemon. We don't yet have the hermetic filesync server.
3663+vm.update=True
3664+vm.root_script=unity/transient-dist-upgrade
3665+vm.ubuntu_script=unity/install-sources
3666+
3667+[indash-didrocks]
3668+vm.name=indash-didrocks
3669+vm.release=raring
3670+vm.backing=raring-desktop-pristine.qcow2
3671+vm.apt_sources=ppa:didrocks/ppa
3672+vm.uploaded_scripts=unity/run-sso-client, unity/run-unity-lens-music
3673+# TODO unity/run-syncdaemon. We don't yet have the hermetic filesync server.
3674+vm.update=True
3675+vm.root_script=unity/transient-dist-upgrade
3676+vm.ubuntu_script=unity/install-sources

Subscribers

People subscribed via source and target branches

to all changes: