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
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
|
# This file is part of cloud-init. See LICENSE file for license information.
"""Base LXD instance."""
import os
import shutil
import time
from tempfile import mkdtemp
from cloudinit.util import load_yaml, subp, ProcessExecutionError, which
from tests.cloud_tests import LOG
from tests.cloud_tests.util import PlatformError
from ..instances import Instance
from pylxd import exceptions as pylxd_exc
class LXDInstance(Instance):
"""LXD container backed instance."""
platform_name = "lxd"
_console_log_method = None
_console_log_file = None
def __init__(self, platform, name, properties, config, features,
pylxd_container):
"""Set up instance.
@param platform: platform object
@param name: hostname of instance
@param properties: image properties
@param config: image config
@param features: supported feature flags
"""
if not pylxd_container:
raise ValueError("Invalid value pylxd_container: %s" %
pylxd_container)
self._pylxd_container = pylxd_container
super(LXDInstance, self).__init__(
platform, name, properties, config, features)
self.tmpd = mkdtemp(prefix="%s-%s" % (type(self).__name__, name))
self.name = name
self._setup_console_log()
@property
def pylxd_container(self):
"""Property function."""
if self._pylxd_container is None:
raise RuntimeError(
"%s: Attempted use of pylxd_container after deletion." % self)
self._pylxd_container.sync()
return self._pylxd_container
def __str__(self):
return (
'%s(name=%s) status=%s' %
(self.__class__.__name__, self.name,
("deleted" if self._pylxd_container is None else
self.pylxd_container.status)))
def _execute(self, command, stdin=None, env=None):
if env is None:
env = {}
env_args = []
if env:
env_args = ['env'] + ["%s=%s" for k, v in env.items()]
# ensure instance is running and execute the command
self.start()
# Use cmdline client due to https://github.com/lxc/pylxd/issues/268
exit_code = 0
try:
stdout, stderr = subp(
['lxc', 'exec', self.name, '--'] + env_args + list(command),
data=stdin, decode=False)
except ProcessExecutionError as e:
exit_code = e.exit_code
stdout = e.stdout
stderr = e.stderr
return stdout, stderr, exit_code
def read_data(self, remote_path, decode=False):
"""Read data from instance filesystem.
@param remote_path: path in instance
@param decode: decode data before returning.
@return_value: content of remote_path as bytes if 'decode' is False,
and as string if 'decode' is True.
"""
data = self.pylxd_container.files.get(remote_path)
return data.decode() if decode else data
def write_data(self, remote_path, data):
"""Write data to instance filesystem.
@param remote_path: path in instance
@param data: data to write in bytes
"""
self.pylxd_container.files.put(remote_path, data)
@property
def console_log_method(self):
if self._console_log_method is not None:
return self._console_log_method
client = which('lxc')
if not client:
raise PlatformError("No 'lxc' client.")
elif _has_proper_console_support():
self._console_log_method = 'show-log'
elif client.startswith("/snap"):
self._console_log_method = 'logfile-snap'
else:
self._console_log_method = 'logfile-tmp'
LOG.debug("Set console log method to %s", self._console_log_method)
return self._console_log_method
def _setup_console_log(self):
method = self.console_log_method
if not method.startswith("logfile-"):
return
if method == "logfile-snap":
log_dir = "/var/snap/lxd/common/consoles"
if not os.path.exists(log_dir):
raise PlatformError(
"Unable to log with snap lxc. Please run:\n"
" sudo mkdir --mode=1777 -p %s" % log_dir)
elif method == "logfile-tmp":
log_dir = "/tmp"
else:
raise PlatformError(
"Unexpected value for console method: %s" % method)
# doing this ensures we can read it. Otherwise it ends up root:root.
log_file = os.path.join(log_dir, self.name)
with open(log_file, "w") as fp:
fp.write("# %s\n" % self.name)
cfg = "lxc.console.logfile=%s" % log_file
orig = self._pylxd_container.config.get('raw.lxc', "")
if orig:
orig += "\n"
self._pylxd_container.config['raw.lxc'] = orig + cfg
self._pylxd_container.save()
self._console_log_file = log_file
def console_log(self):
"""Console log.
@return_value: bytes of this instance's console
"""
if self._console_log_file:
if not os.path.exists(self._console_log_file):
raise NotImplementedError(
"Console log '%s' does not exist. If this is a remote "
"lxc, then this is really NotImplementedError. If it is "
"A local lxc, then this is a RuntimeError."
"https://github.com/lxc/lxd/issues/1129")
with open(self._console_log_file, "rb") as fp:
return fp.read()
try:
return subp(['lxc', 'console', '--show-log', self.name],
decode=False)[0]
except ProcessExecutionError as e:
raise PlatformError(
"console log",
"Console log failed [%d]: stdout=%s stderr=%s" % (
e.exit_code, e.stdout, e.stderr))
def reboot(self, wait=True):
"""Reboot instance."""
self.shutdown(wait=wait)
self.start(wait=wait)
def shutdown(self, wait=True, retry=1):
"""Shutdown instance."""
if self.pylxd_container.status == 'Stopped':
return
try:
LOG.debug("%s: shutting down (wait=%s)", self, wait)
self.pylxd_container.stop(wait=wait)
except (pylxd_exc.LXDAPIException, pylxd_exc.NotFound) as e:
# An exception happens here sometimes (LP: #1783198)
# LOG it, and try again.
LOG.warning(
("%s: shutdown(retry=%d) caught %s in shutdown "
"(response=%s): %s"),
self, retry, e.__class__.__name__, e.response, e)
if isinstance(e, pylxd_exc.NotFound):
LOG.debug("container_exists(%s) == %s",
self.name, self.platform.container_exists(self.name))
if retry == 0:
raise e
return self.shutdown(wait=wait, retry=retry - 1)
def start(self, wait=True, wait_for_cloud_init=False):
"""Start instance."""
if self.pylxd_container.status != 'Running':
self.pylxd_container.start(wait=wait)
if wait:
self._wait_for_system(wait_for_cloud_init)
def freeze(self):
"""Freeze instance."""
if self.pylxd_container.status != 'Frozen':
self.pylxd_container.freeze(wait=True)
def unfreeze(self):
"""Unfreeze instance."""
if self.pylxd_container.status == 'Frozen':
self.pylxd_container.unfreeze(wait=True)
def destroy(self):
"""Clean up instance."""
LOG.debug("%s: deleting container.", self)
self.unfreeze()
self.shutdown()
retries = [1] * 5
for attempt, wait in enumerate(retries):
try:
self.pylxd_container.delete(wait=True)
break
except Exception:
if attempt + 1 >= len(retries):
raise
LOG.debug('Failed to delete container %s (%s/%s) retrying...',
self, attempt + 1, len(retries))
time.sleep(wait)
self._pylxd_container = None
if self.platform.container_exists(self.name):
raise OSError('%s: container was not properly removed' % self)
if self._console_log_file and os.path.exists(self._console_log_file):
os.unlink(self._console_log_file)
shutil.rmtree(self.tmpd)
super(LXDInstance, self).destroy()
def _has_proper_console_support():
stdout, _ = subp(['lxc', 'info'])
info = load_yaml(stdout)
reason = None
if 'console' not in info.get('api_extensions', []):
reason = "LXD server does not support console api extension"
else:
dver = str(info.get('environment', {}).get('driver_version', ""))
if dver.startswith("2.") or dver.startswith("1."):
reason = "LXD Driver version not 3.x+ (%s)" % dver
else:
try:
stdout = subp(['lxc', 'console', '--help'], decode=False)[0]
if not (b'console' in stdout and b'log' in stdout):
reason = "no '--log' in lxc console --help"
except ProcessExecutionError:
reason = "no 'console' command in lxc client"
if reason:
LOG.debug("no console-support: %s", reason)
return False
else:
LOG.debug("console-support looks good")
return True
# vi: ts=4 expandtab
|