Merge lp:~jeffmarcom/checkbox/kvm-cloud_data into lp:checkbox

Proposed by Jeff Marcom
Status: Merged
Approved by: Zygmunt Krynicki
Approved revision: 1996
Merged at revision: 1994
Proposed branch: lp:~jeffmarcom/checkbox/kvm-cloud_data
Merge into: lp:checkbox
Diff against target: 199 lines (+75/-29)
2 files modified
debian/changelog (+3/-1)
scripts/virtualization (+72/-28)
To merge this branch: bzr merge lp:~jeffmarcom/checkbox/kvm-cloud_data
Reviewer Review Type Date Requested Status
Zygmunt Krynicki (community) Approve
Review via email: mp+153878@code.launchpad.net

Description of the change

This fixes an issue where a recent change in cloud-init in the latest 12.04.2 precise cloud images prevented the VM from booting successfully if cloud config data was not supplied.

The virtualization test now creates a cloud data disk that includes generic user data and meta data files to assist during cloud-init stages of boot process. Supplying this data is dependent on whether the "cloud" string is part of the image to test.

Output from run:
DEBUG:root:Downloading precise-server-cloudimg-i386-disk1.img, from http://cloud-images.ubuntu.com
DEBUG:root:Creating cloud user-data
DEBUG:root:Creating cloud meta-data
DEBUG:root:Attempting boot for:precise-server-cloudimg-i386-disk1.img
DEBUG:root:Attaching Cloud config disk
DEBUG:root:Using params:kvm -m 256 -net nic -net user,net=10.0.0.0/8,host=10.0.0.1,hostfwd=tcp::2222-:22 -drive file=precise-server-cloudimg-i386-disk1.img,if=virtio -drive file=seed.iso,if=virtio -display none -nographic

Cloud-init v. 0.7.2 running 'modules:final' at Mon, 18 Mar 2013 17:21:40 +0000. Up 201.27 seconds.
========= CERTIFICATION TEST =========
ci-info: no authorized ssh keys fingerprints found for user ubuntu.ci-info: no authorized ssh keys fingerprints found for user ubuntu.ec2:
ec2: #############################################################
ec2: -----BEGIN SSH HOST KEY FINGERPRINTS-----
ec2: 1024 19:d3:e4:39:17:27:f2:c0:67:50:58:1b:f8:5c:72:0e root@ubuntu (DSA)
ec2: 256 13:74:68:e2:0c:fd:2a:55:e5:af:97:12:0e:69:32:75 root@ubuntu (ECDSA)
ec2: 2048 6c:f1:e8:36:3e:76:b7:a6:1f:5f:58:a1:b9:f6:58:02 root@ubuntu (RSA)
ec2: -----END SSH HOST KEY FINGERPRINTS-----
ec2: #############################################################
-----BEGIN SSH HOST KEY KEYS-----
ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLVPgiCLVPYPvQJCf5ZpDkceNFt4pFXYael3DCMu23Nm6MM1IDLr+75jic834O4TEK7laYSHu6WJ/yb+i+/zzJg= root@ubuntu
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDCpAF4MBa3N+L7UF5tgb0BPBUu1RutdaEnZLuSI2MwpJCIeqv57H/haysbZmVGo+T+irIUa2DQgDdQLDCJyEvBbLZFD356oV34lT5JoC20+h9xv9WjCQnxw0s5YoOsMO284j0/acCBKMAufMhe7CXVxcJGd/HE7feH2c8oaZult1yZSREqEBJoUj88TFxrEUZB7oda9Uvs1W1FWeaSr1vYRnPkXzWQRgJj2sRnpqJ4Qv6X5tAbsykeIpfuSd8uvW9qQnN50xwLkP17dJL1qSyX1XC1RBx0Uqq3rfUoAc4I8cACL+zVMinxad5KGxxhsVqK7rx/Ci5Lt6FrajYOql4j root@ubuntu
-----END SSH HOST KEY KEYS-----
Cloud-init v. 0.7.2 finished at Mon, 18 Mar 2013 17:21:44 +0000. Datasource DataSourceNoCloudNet [seed=/dev/vdb][dsmode=net]. Up 205.73 seconds

To post a comment you must log in.
Revision history for this message
Zygmunt Krynicki (zyga) wrote :

32 - logging.error("Failed download {}: {}".format(image_url, exception))
33 + logging.error("Failed download: %s", exception)

You may want to replace that with:

logging.exception("Failed to download image: %s", image_url)

This has the advantage of knowing everything there is to know about the exception _and_ displaying a traceback.

89 +#cloud-config
90 +
91 +# run commands
92 +# default: none
93 +# runcmd contains a list of either lists or a string
94 +# each item will be executed in order at rc.local like level with
95 +# output to the console
96 +# - if the item is a list, the items will be properly executed as if
97 +# passed to execve(3) (with the first arg as the command).
98 +# - if the item is a string, it will be simply written to the file and
99 +# will be interpreted by 'sh'
100 +#
101 +# Note, that the list has to be proper yaml, so you have to escape
102 +# any characters yaml would eat (':' can be problematic)

Perhaps the comment in the embedded data section is not that interesting, this is an automated test after all. I would move that comment to the method, indented it as with everything else.

113 + with open(file, "wt") as data_file:
114 + os.fchmod(data_file.fileno(), 0o777)

I would use mode=0o777 there, otherwise there is a race condition where an attacker can open the file before you change the permission. After that the attacker can read or write anything.

Still, what's the point of using 0o777?

111 + for file in ['user-data', 'meta-data']:
112 + logging.debug("Creating cloud %s", file)
113 + with open(file, "wt") as data_file:
114 + os.fchmod(data_file.fileno(), 0o777)
115 + data_file.write(vars()[file.replace("-", "_")])
116 +
117 + # Create Data ISO hosting user & meta cloud config data
118 + iso_cmd = \
119 + '''
120 + genisoimage -output seed.iso -volid
121 + cidata -joliet -rock user-data meta-data
122 + '''
123 +
124 + iso_build = Popen(
125 + shlex.split(iso_cmd), stderr=PIPE, stdout=PIPE,
126 + universal_newlines=True)

I think it's much better to just use ['genisoimage', '-output', 'seed.iso', '-volid', ...] and not use shlex for that.

128 + error, output = iso_build.communicate()
129 +
130 + if iso_build.returncode != 0:
131 + logging.error("Cloud data disk creation failed")

Perhaps what you want instead is subprocess.check_output() it does everything above in one call

None of that is a critical problem though, I'm not -1 this

Revision history for this message
Jeff Marcom (jeffmarcom) wrote :

> 32 - logging.error("Failed download {}: {}".format(image_url, exception))
> 33 + logging.error("Failed download: %s", exception)
>
> You may want to replace that with:
>
> logging.exception("Failed to download image: %s", image_url)

Fixed
>
> This has the advantage of knowing everything there is to know about the
> exception _and_ displaying a traceback.
>
> 89 +#cloud-config
> 90 +
> 91 +# run commands
> 92 +# default: none
> 93 +# runcmd contains a list of either lists or a string
> 94 +# each item will be executed in order at rc.local like level with
> 95 +# output to the console
> 96 +# - if the item is a list, the items will be properly executed as if
> 97 +# passed to execve(3) (with the first arg as the command).
> 98 +# - if the item is a string, it will be simply written to the file
> and
> 99 +# will be interpreted by 'sh'
> 100 +#
> 101 +# Note, that the list has to be proper yaml, so you have to escape
> 102 +# any characters yaml would eat (':' can be problematic)
>
> Perhaps the comment in the embedded data section is not that interesting, this
> is an automated test after all. I would move that comment to the method,
> indented it as with everything else.

Removed extraneous comments. I believe the cloud-data comment at the top MUST be there.
>
> 113 + with open(file, "wt") as data_file:
> 114 + os.fchmod(data_file.fileno(), 0o777)
>
> I would use mode=0o777 there, otherwise there is a race condition where an
> attacker can open the file before you change the permission. After that the
> attacker can read or write anything.
K, fixed
>
> Still, what's the point of using 0o777?
Has to be executable. It's a temporary file so I thought it would be the simplest way. I don't see the race condition ever being an issue.
.
>
>
> 111 + for file in ['user-data', 'meta-data']:
> 112 + logging.debug("Creating cloud %s", file)
> 113 + with open(file, "wt") as data_file:
> 114 + os.fchmod(data_file.fileno(), 0o777)
> 115 + data_file.write(vars()[file.replace("-", "_")])
> 116 +
> 117 + # Create Data ISO hosting user & meta cloud config data
> 118 + iso_cmd = \
> 119 + '''
> 120 + genisoimage -output seed.iso -volid
> 121 + cidata -joliet -rock user-data meta-data
> 122 + '''
> 123 +
> 124 + iso_build = Popen(
> 125 + shlex.split(iso_cmd), stderr=PIPE, stdout=PIPE,
> 126 + universal_newlines=True)
>
> I think it's much better to just use ['genisoimage', '-output', 'seed.iso',
> '-volid', ...] and not use shlex for that.
>
> 128 + error, output = iso_build.communicate()
> 129 +
> 130 + if iso_build.returncode != 0:
> 131 + logging.error("Cloud data disk creation failed")

I changed it but I'm curious why exactly? Shlex was made for this kind of argument processing correct? I can't promise I won't make the same mistake again... :(
>
> Perhaps what you want instead is subprocess.check_output() it does everything
> above in one call

Actually used the error and output grabbed from communicate this time.
>
> None of that is a critical problem though, I'm not -1 this

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

> Removed extraneous comments. I believe the cloud-data comment at the top MUST
> be there.

Sure, I meant that the comment could be in the _function_ and not in the generated _file_

> > 128 + error, output = iso_build.communicate()
> > 129 +
> > 130 + if iso_build.returncode != 0:
> > 131 + logging.error("Cloud data disk creation failed")
>
> I changed it but I'm curious why exactly? Shlex was made for this kind of
> argument processing correct? I can't promise I won't make the same mistake
> again... :(

19:28 < zyga> spideyman: as for shlex, it's just redundant: compre: data = json.loads("{'a': 5}") # data = {"a": 5}
19:28 < zyga> spideyman: so shlex is just a wrapper around the data you wanted anyway
19:30 < spideyman> zyga: understood, doing ['var', 'var', 'var', 'var', 'var', 'var', 'var', 'var' ,'var'] is tiring
19:30 < spideyman> zyga: that's why I use it, and with me at least, typing all that is error prone.

standard syntax checkers should alert you about any mistakes, hopefully you won't have to type _too_ much shell :)

> > Perhaps what you want instead is subprocess.check_output() it does
> everything
> > above in one call
>
> Actually used the error and output grabbed from communicate this time.

19:27 < zyga> spideyman: so check_output() is still shorter
19:27 < zyga> spideyman: it returns stdout
19:27 < zyga> spideyman: and raises CalledProcessErorr if retval != 0
19:27 < zyga> spideyman: that's the point I was trying to make there
19:28 < zyga> spideyman: as for shlex, it's just redundant: compre: data = json.loads("{'a': 5}") # data = {"a": 5}
19:28 < zyga> spideyman: so shlex is just a wrapper around the data you wanted anyway

If you want to land this +1, I'm fine with the code as before

1994. By Jeff Marcom

Add classmethod for generating cloud config data based on ISO in use

Signed-off-by: Jeff Marcom <email address hidden>

1995. By Jeff Marcom

pep8 fixes

1996. By Jeff Marcom

Updated Changelog

Revision history for this message
Zygmunt Krynicki (zyga) wrote :

Looks good +1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/changelog'
2--- debian/changelog 2013-03-15 20:31:24 +0000
3+++ debian/changelog 2013-03-18 18:48:19 +0000
4@@ -20,8 +20,10 @@
5 * jobs/input.txt.in: Added manual check job for accelerometer hardware
6 * scripts/virtualization: Fixed issue where specifying test timeout via
7 vitualization.cfg was pulled in as a string rather than integer.
8+ * scripts/virtualization: Added classmethod for generating cloud
9+ config data based on ISO in use
10
11- -- Jeff Marcom <jeff.marcom@canonical.com> Fri, 15 Mar 2013 16:04:46 +0000
12+ -- Jeff Marcom <jeff.marcom@canonical.com> Mon, 18 Mar 2013 15:04:46 +0000
13
14 checkbox (0.15.4) raring; urgency=low
15
16
17=== modified file 'scripts/virtualization'
18--- scripts/virtualization 2013-03-15 15:26:35 +0000
19+++ scripts/virtualization 2013-03-18 18:48:19 +0000
20@@ -28,7 +28,7 @@
21 import logging
22 import lsb_release
23 import signal
24-from subprocess import Popen, PIPE
25+from subprocess import Popen, PIPE, CalledProcessError, check_output
26 import sys
27 import tempfile
28 import time
29@@ -67,7 +67,7 @@
30 try:
31 resp = urllib.request.urlretrieve(image_url, cloud_iso)
32 except urllib.error.HTTPError as exception:
33- logging.error("Failed download {}: {}".format(image_url, exception))
34+ logging.exception("Failed download of image from:", image_url)
35 return False
36
37 if not os.path.isfile(cloud_iso):
38@@ -90,22 +90,28 @@
39 hostfwd = "tcp::2222-:22"
40
41 params = \
42- '''
43- kvm -m {0} -net nic -net user,net={1},host={2},
44- hostfwd={3} -drive file={4},if=virtio -display none -nographic
45- '''.format(
46+ '''
47+ kvm -m {0} -net nic -net user,net={1},host={2},
48+ hostfwd={3} -drive file={4},if=virtio -display none -nographic
49+ '''.format(
50 "256",
51 netrange,
52 image_ip,
53 hostfwd,
54 data_disk).replace("\n", "").replace(" ", "")
55
56+ if os.path.isfile("seed.iso"):
57+ logging.debug("Attaching Cloud config disk")
58+
59+ params = params.replace(
60+ "if=virtio", "if=virtio -drive file=seed.iso,if=virtio")
61+
62 logging.debug("Using params:{}".format(params))
63
64 # Default file location for log file is in checkbox output directory
65 checkbox_dir = os.getenv("CHECKBOX_DATA")
66
67- if checkbox_dir != None:
68+ if checkbox_dir is not None:
69 self.debug_file = os.path.join(checkbox_dir, self.debug_file)
70
71 # Open VM STDERR/STDOUT log file for writing
72@@ -116,22 +122,63 @@
73 return False
74
75 # Start Virtual machine
76- self.process = Popen(params, stderr=file,
77- stdout=file, universal_newlines=True, shell=True)
78+ self.process = Popen(
79+ params, stderr=file, stdout=file,
80+ universal_newlines=True, shell=True)
81+
82+ @classmethod
83+ def create_cloud_disk(cls):
84+ """
85+ Generate Cloud meta data and creates an iso object
86+ to be mounted as virtual device to instance during boot.
87+ """
88+
89+ user_data = """\
90+#cloud-config
91+
92+runcmd:
93+ - [ sh, -c, echo "========= CERTIFICATION TEST =========" ]
94+"""
95+
96+ meta_data = """\
97+{ echo instance-id: iid-local01; echo local-hostname, certification; }
98+"""
99+
100+ for file in ['user-data', 'meta-data']:
101+ logging.debug("Creating cloud %s", file)
102+ with open(file, "wt") as data_file:
103+ os.fchmod(data_file.fileno(), 0o777)
104+ data_file.write(vars()[file.replace("-", "_")])
105+
106+ # Create Data ISO hosting user & meta cloud config data
107+ try:
108+ iso_build = check_output(
109+ ['genisoimage', '-output', 'seed.iso', '-volid',
110+ 'cidata', '-joliet', '-rock', 'user-data', 'meta-data'],
111+ universal_newlines=True)
112+ except CalledProcessError as exception:
113+ logging.exception("Cloud data disk creation failed")
114+
115
116 def start(self):
117 status = 1
118 # Create temp directory:
119- with tempfile.TemporaryDirectory("_kvm_test",
120- time.strftime("%b_%d_%Y_")) as temp_dir:
121-
122+
123+ date = time.strftime("%b_%d_%Y_")
124+ with tempfile.TemporaryDirectory("_kvm_test", date) as temp_dir:
125+
126 os.chdir(temp_dir)
127 if self.image is None:
128 # Download cloud image
129 self.image = self.download_image()
130
131 if os.path.isfile(self.image):
132-
133+
134+ if "cloud" in self.image:
135+ # Will assume we need to supply cloud meta data
136+ # for instance boot to be successful
137+ self.create_cloud_disk()
138+
139 # Boot Virtual Machine
140 instance = self.boot_image(self.image)
141
142@@ -148,8 +195,8 @@
143 else:
144 print("Could not find: {}".format(self.image), file=sys.stderr)
145
146- return status
147-
148+ return status
149+
150
151 def test_kvm(args):
152 print("Executing KVM Test", file=sys.stderr)
153@@ -164,7 +211,7 @@
154 try:
155 config.readfp(open(config_file))
156 timeout = int(config.get("KVM", "timeout"))
157- image = config.get("KVM", "image")
158+ image = config.get("KVM", "image")
159 except IOError:
160 logging.warn("No config file found")
161 except Exception as exception:
162@@ -187,19 +234,19 @@
163 subparsers = parser.add_subparsers()
164
165 # Main cli options
166- kvm_test_parser = subparsers.add_parser('kvm',
167- help=("Run kvm virtualization test"))
168+ kvm_test_parser = subparsers.add_parser(
169+ 'kvm', help=("Run kvm virtualization test"))
170
171 #xen_test_parser = subparsers.add_parser('xen',
172 # help=("Run xen virtualization test"))
173
174 # Sub test options
175- kvm_test_parser.add_argument('-i', '--image',
176- type=str, default=None)
177- kvm_test_parser.add_argument('-t', '--timeout',
178- type=int, default=500)
179- kvm_test_parser.add_argument('--debug',
180- action="store_true")
181+ kvm_test_parser.add_argument(
182+ '-i', '--image', type=str, default=None)
183+ kvm_test_parser.add_argument(
184+ '-t', '--timeout', type=int, default=500)
185+ kvm_test_parser.add_argument(
186+ '--debug', action="store_true")
187 kvm_test_parser.set_defaults(func=test_kvm)
188
189 args = parser.parse_args()
190@@ -209,8 +256,5 @@
191
192 args.func(args)
193
194-
195-
196-
197 if __name__ == "__main__":
198- main();
199+ main()

Subscribers

People subscribed via source and target branches