1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
|
# This file is part of cloud-init. See LICENSE file for license information.
"""LXD Image Base Class."""
import os
import shutil
import tempfile
from ..images import Image
from .snapshot import LXDSnapshot
from cloudinit import subp
from cloudinit import util as c_util
from tests.cloud_tests import util
class LXDImage(Image):
"""LXD backed image."""
platform_name = "lxd"
def __init__(self, platform, config, pylxd_image):
"""Set up image.
@param platform: platform object
@param config: image configuration
"""
self.modified = False
self._img_instance = None
self._pylxd_image = None
self.pylxd_image = pylxd_image
super(LXDImage, self).__init__(platform, config)
@property
def pylxd_image(self):
"""Property function."""
if self._pylxd_image:
self._pylxd_image.sync()
return self._pylxd_image
@pylxd_image.setter
def pylxd_image(self, pylxd_image):
if self._img_instance:
self._instance.destroy()
self._img_instance = None
if (self._pylxd_image and
(self._pylxd_image is not pylxd_image) and
(not self.config.get('cache_base_image') or self.modified)):
self._pylxd_image.delete(wait=True)
self.modified = False
self._pylxd_image = pylxd_image
@property
def _instance(self):
"""Internal use only, returns a instance
This starts an lxc instance from the image, so it is "dirty".
Better would be some way to modify this "at rest".
lxc-pstart would be an option."""
if not self._img_instance:
self._img_instance = self.platform.launch_container(
self.properties, self.config, self.features,
use_desc='image-modification', image_desc=str(self),
image=self.pylxd_image.fingerprint)
self._img_instance.start()
return self._img_instance
@property
def properties(self):
"""{} containing: 'arch', 'os', 'version', 'release'."""
properties = self.pylxd_image.properties
return {
'arch': properties.get('architecture'),
'os': properties.get('os'),
'version': properties.get('version'),
'release': properties.get('release'),
}
def export_image(self, output_dir):
"""Export image from lxd image store to (split) tarball on disk.
@param output_dir: dir to store tarballs in
@return_value: tuple of path to metadata tarball and rootfs tarball
"""
# pylxd's image export feature doesn't do split exports, so use cmdline
subp.subp(['lxc', 'image', 'export', self.pylxd_image.fingerprint,
output_dir], capture=True)
tarballs = [p for p in os.listdir(output_dir) if p.endswith('tar.xz')]
metadata = os.path.join(
output_dir, next(p for p in tarballs if p.startswith('meta-')))
rootfs = os.path.join(
output_dir, next(p for p in tarballs if not p.startswith('meta-')))
return (metadata, rootfs)
def import_image(self, metadata, rootfs):
"""Import image to lxd image store from (split) tarball on disk.
Note, this will replace and delete the current pylxd_image
@param metadata: metadata tarball
@param rootfs: rootfs tarball
@return_value: imported image fingerprint
"""
alias = util.gen_instance_name(
image_desc=str(self), use_desc='update-metadata')
subp.subp(['lxc', 'image', 'import', metadata, rootfs,
'--alias', alias], capture=True)
self.pylxd_image = self.platform.query_image_by_alias(alias)
return self.pylxd_image.fingerprint
def update_templates(self, template_config, template_data):
"""Update the image's template configuration.
Note, this will replace and delete the current pylxd_image
@param template_config: config overrides for template metadata
@param template_data: template data to place into templates/
"""
# set up tmp files
export_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
extract_dir = tempfile.mkdtemp(prefix='cloud_test_util_')
new_metadata = os.path.join(export_dir, 'new-meta.tar.xz')
metadata_yaml = os.path.join(extract_dir, 'metadata.yaml')
template_dir = os.path.join(extract_dir, 'templates')
try:
# extract old data
(metadata, rootfs) = self.export_image(export_dir)
shutil.unpack_archive(metadata, extract_dir)
# update metadata
metadata = c_util.read_conf(metadata_yaml)
templates = metadata.get('templates', {})
templates.update(template_config)
metadata['templates'] = templates
util.yaml_dump(metadata, metadata_yaml)
# write out template files
for name, content in template_data.items():
path = os.path.join(template_dir, name)
c_util.write_file(path, content)
# store new data, mark new image as modified
util.flat_tar(new_metadata, extract_dir)
self.import_image(new_metadata, rootfs)
self.modified = True
finally:
# remove tmpfiles
shutil.rmtree(export_dir)
shutil.rmtree(extract_dir)
def _execute(self, *args, **kwargs):
"""Execute command in image, modifying image."""
return self._instance._execute(*args, **kwargs)
def push_file(self, local_path, remote_path):
"""Copy file at 'local_path' to instance at 'remote_path'."""
return self._instance.push_file(local_path, remote_path)
def run_script(self, *args, **kwargs):
"""Run script in image, modifying image.
@return_value: script output
"""
return self._instance.run_script(*args, **kwargs)
def snapshot(self):
"""Create snapshot of image, block until done."""
# get empty user data to pass in to instance
# if overrides for user data provided, use them
empty_userdata = util.update_user_data(
{}, self.config.get('user_data_overrides', {}))
conf = {'user.user-data': empty_userdata}
# clone current instance
instance = self.platform.launch_container(
self.properties, self.config, self.features,
container=self._instance.name, image_desc=str(self),
use_desc='snapshot', container_config=conf)
# wait for cloud-init before boot_clean_script is run to ensure
# /var/lib/cloud is removed cleanly
instance.start(wait=True, wait_for_cloud_init=True)
if self.config.get('boot_clean_script'):
instance.run_script(self.config.get('boot_clean_script'))
# freeze current instance and return snapshot
instance.freeze()
return LXDSnapshot(self.platform, self.properties, self.config,
self.features, instance)
def destroy(self):
"""Clean up data associated with image."""
self.pylxd_image = None
super(LXDImage, self).destroy()
# vi: ts=4 expandtab
|