1
# vim: tabstop=4 shiftwidth=4 softtabstop=4
3
# Copyright 2010 United States Government as represented by the
4
# Administrator of the National Aeronautics and Space Administration.
6
# Copyright 2011, Piston Cloud Computing, Inc.
10
# Licensed under the Apache License, Version 2.0 (the "License"); you may
11
# not use this file except in compliance with the License. You may obtain
12
# a copy of the License at
14
# http://www.apache.org/licenses/LICENSE-2.0
16
# Unless required by applicable law or agreed to in writing, software
17
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
18
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
19
# License for the specific language governing permissions and limitations
22
Utility methods to resize, repartition, and modify disk images.
24
Includes injection of SSH PGP keys into authorized_keys file.
32
from nova import exception
33
from nova import flags
34
from nova import log as logging
35
from nova import utils
36
from nova.virt.disk import guestfs
37
from nova.virt.disk import loop
38
from nova.virt.disk import nbd
40
LOG = logging.getLogger('nova.compute.disk')
42
flags.DEFINE_integer('minimum_root_size', 1024 * 1024 * 1024 * 10,
43
'minimum size in bytes of root partition')
44
flags.DEFINE_string('injected_network_template',
45
utils.abspath('virt/interfaces.template'),
46
'Template file for injected network')
47
flags.DEFINE_list('img_handlers', ['loop', 'nbd', 'guestfs'],
48
'Order of methods used to mount disk images')
51
# NOTE(yamahata): DEFINE_list() doesn't work because the command may
52
# include ','. For example,
53
# mkfs.ext3 -O dir_index,extent -E stride=8,stripe-width=16
54
# --label %(fs_label)s %(target)s
56
# DEFINE_list() parses its argument by
57
# [s.strip() for s in argument.split(self._token)]
58
# where self._token = ','
59
# No escape nor exceptional handling for ','.
60
# DEFINE_list() doesn't give us what we need.
61
flags.DEFINE_multistring('virt_mkfs',
62
['windows=mkfs.ntfs --fast --label %(fs_label)s '
64
# NOTE(yamahata): vfat case
65
#'windows=mkfs.vfat -n %(fs_label)s %(target)s',
66
'linux=mkfs.ext3 -L %(fs_label)s -F %(target)s',
67
'default=mkfs.ext3 -L %(fs_label)s -F %(target)s'],
68
'mkfs commands for ephemeral device. The format is'
69
'<os_type>=<mkfs command>')
73
_DEFAULT_MKFS_COMMAND = None
76
for s in FLAGS.virt_mkfs:
77
# NOTE(yamahata): mkfs command may includes '=' for its options.
78
# So item.partition('=') doesn't work here
79
os_type, mkfs_command = s.split('=', 1)
81
_MKFS_COMMAND[os_type] = mkfs_command
82
if os_type == 'default':
83
_DEFAULT_MKFS_COMMAND = mkfs_command
86
def mkfs(os_type, fs_label, target):
87
mkfs_command = (_MKFS_COMMAND.get(os_type, _DEFAULT_MKFS_COMMAND) or
90
utils.execute(*mkfs_command.split())
93
def extend(image, size):
94
"""Increase image to size"""
95
file_size = os.path.getsize(image)
98
utils.execute('qemu-img', 'resize', image, size)
99
# NOTE(vish): attempts to resize filesystem
100
utils.execute('e2fsck', '-fp', image, check_exit_code=False)
101
utils.execute('resize2fs', image, check_exit_code=False)
104
class _DiskImage(object):
105
"""Provide operations on a disk image file."""
107
def __init__(self, image, partition=None, use_cow=False,
108
disable_auto_fsck=False, mount_dir=None):
109
# These passed to each mounter
111
self.partition = partition
112
self.disable_auto_fsck = disable_auto_fsck
113
self.mount_dir = mount_dir
120
# As a performance tweak, don't bother trying to
121
# directly loopback mount a cow image.
122
self.handlers = FLAGS.img_handlers[:]
124
self.handlers.remove('loop')
128
"""Return the collated errors from all operations."""
129
return '\n--\n'.join([''] + self._errors)
132
def _handler_class(mode):
133
"""Look up the appropriate class to use based on MODE."""
134
for cls in (loop.Mount, nbd.Mount, guestfs.Mount):
137
raise exception.Error(_("unknown disk image handler: %s" % mode))
140
"""Mount a disk image, using the object attributes.
142
The first supported means provided by the mount classes is used.
144
True, or False is returned and the 'errors' attribute
145
contains any diagnostics.
148
raise exception.Error(_('image already mounted'))
150
if not self.mount_dir:
151
self.mount_dir = tempfile.mkdtemp()
155
for h in self.handlers:
156
mounter_cls = self._handler_class(h)
157
mounter = mounter_cls(image=self.image,
158
partition=self.partition,
159
disable_auto_fsck=self.disable_auto_fsck,
160
mount_dir=self.mount_dir)
161
if mounter.do_mount():
162
self._mounter = mounter
165
LOG.debug(mounter.error)
166
self._errors.append(mounter.error)
168
if not self._mounter:
169
self.umount() # rmdir
171
return bool(self._mounter)
174
"""Unmount a disk image from the file system."""
177
self._mounter.do_umount()
180
os.rmdir(self.mount_dir)
183
# Public module functions
185
def inject_data(image, key=None, net=None, metadata=None,
186
partition=None, use_cow=False, disable_auto_fsck=True):
187
"""Injects a ssh key and optionally net data into a disk image.
189
it will mount the image as a fully partitioned disk and attempt to inject
190
into the specified partition number.
192
If partition is not specified it mounts the image as a single partition.
195
img = _DiskImage(image=image, partition=partition, use_cow=use_cow,
196
disable_auto_fsck=disable_auto_fsck)
199
inject_data_into_fs(img.mount_dir, key, net, metadata,
204
raise exception.Error(img.errors)
207
def setup_container(image, container_dir=None, use_cow=False):
208
"""Setup the LXC container.
210
It will mount the loopback image to the container directory in order
211
to create the root filesystem for the container.
213
LXC does not support qcow2 images yet.
216
img = _DiskImage(image=image, use_cow=use_cow, mount_dir=container_dir)
220
raise exception.Error(img.errors)
221
except Exception, exn:
222
LOG.exception(_('Failed to mount filesystem: %s'), exn)
225
def destroy_container(img):
226
"""Destroy the container once it terminates.
228
It will umount the container that is mounted,
229
and delete any linked devices.
231
LXC does not support qcow2 images yet.
236
except Exception, exn:
237
LOG.exception(_('Failed to remove container: %s'), exn)
240
def inject_data_into_fs(fs, key, net, metadata, execute):
241
"""Injects data into a filesystem already mounted by the caller.
242
Virt connections can call this directly if they mount their fs
243
in a different way to inject_data
246
_inject_key_into_fs(key, fs, execute=execute)
248
_inject_net_into_fs(net, fs, execute=execute)
250
_inject_metadata_into_fs(metadata, fs, execute=execute)
253
def _inject_metadata_into_fs(metadata, fs, execute=None):
254
metadata_path = os.path.join(fs, "meta.js")
255
metadata = dict([(m.key, m.value) for m in metadata])
257
utils.execute('tee', metadata_path,
258
process_input=json.dumps(metadata), run_as_root=True)
261
def _inject_key_into_fs(key, fs, execute=None):
262
"""Add the given public ssh key to root's authorized_keys.
264
key is an ssh key string.
265
fs is the path to the base of the filesystem into which to inject the key.
267
sshdir = os.path.join(fs, 'root', '.ssh')
268
utils.execute('mkdir', '-p', sshdir, run_as_root=True)
269
utils.execute('chown', 'root', sshdir, run_as_root=True)
270
utils.execute('chmod', '700', sshdir, run_as_root=True)
271
keyfile = os.path.join(sshdir, 'authorized_keys')
272
utils.execute('tee', '-a', keyfile,
273
process_input='\n' + key.strip() + '\n', run_as_root=True)
276
def _inject_net_into_fs(net, fs, execute=None):
277
"""Inject /etc/network/interfaces into the filesystem rooted at fs.
279
net is the contents of /etc/network/interfaces.
281
netdir = os.path.join(os.path.join(fs, 'etc'), 'network')
282
utils.execute('mkdir', '-p', netdir, run_as_root=True)
283
utils.execute('chown', 'root:root', netdir, run_as_root=True)
284
utils.execute('chmod', 755, netdir, run_as_root=True)
285
netfile = os.path.join(netdir, 'interfaces')
286
utils.execute('tee', netfile, process_input=net, run_as_root=True)