forked from canonical/lxd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlxc-to-lxd
executable file
·421 lines (338 loc) · 13 KB
/
lxc-to-lxd
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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/usr/bin/python3
import argparse
import json
import lxc
import os
import subprocess
from pylxd.client import Client
# Fetch a config key as a list
def config_get(config, key, default=None):
result = []
for line in config:
fields = line.split("=", 1)
if fields[0].strip() == key:
result.append(fields[-1].strip())
if len(result) == 0:
return default
else:
return result
# Parse a LXC configuration file, called recursively for includes
def config_parse(path):
config = []
with open(path, "r") as fd:
for line in fd:
line = line.strip()
key = line.split("=", 1)[0].strip()
value = line.split("=", 1)[-1].strip()
# Parse user-added includes
if key == "lxc.include":
# Ignore our own default configs
if value.startswith("/usr/share/lxc/config/"):
continue
if os.path.isfile(value):
config += config_parse(value)
continue
elif os.path.isdir(value):
for entry in os.listdir(value):
if not entry.endswith(".conf"):
continue
config += config_parse(os.path.join(value, entry))
continue
else:
print("Invalid include: %s", line)
# Expand any fstab
if key == "lxc.mount":
if not os.path.exists(value):
print("Container fstab file doesn't exist, skipping...")
return False
with open(value, "r") as fd:
for line in fd:
line = line.strip()
if line and not line.startswith("#"):
config.append("lxc.mount.entry = %s" % line)
continue
# Proces normal configuration keys
if line and not line.startswith("#"):
config.append(line)
return config
# Convert a LXC container to a LXD one
def convert_container(container_name, args):
# Connect to LXD
if args.lxdpath:
os.environ['LXD_DIR'] = args.lxdpath
lxd = Client()
print("==> Processing container: %s" % container_name)
# Load the container
try:
container = lxc.Container(container_name, args.lxcpath)
except:
print("Invalid container configuration, skipping...")
return False
if container.running:
print("Only stopped containers can be migrated, skipping...")
return False
# As some keys can't be queried over the API, parse the config ourselves
print("Parsing LXC configuration")
lxc_config = config_parse(container.config_file_name)
if args.debug:
print("Container configuration:")
print(" ", end="")
print("\n ".join(lxc_config))
print("")
if config_get(lxc_config, "lxd.migrated"):
print("Container has already been migrated, skipping...")
return False
# Make sure we don't have a conflict
print("Checking for existing containers")
try:
lxd.containers.get(container_name)
print("Container already exists, skipping...")
return False
except NameError:
pass
# Validate lxc.utsname
print("Validating container name")
value = config_get(lxc_config, "lxc.utsname")
if value and value[0] != container_name:
print("Container name doesn't match lxc.utsname, skipping...")
return False
# Detect privileged containers
print("Validating container mode")
if config_get(lxc_config, "lxc.id_map"):
print("Unprivileged containers aren't supported, skipping...")
return False
# Detect hooks in config
for line in lxc_config:
if line.startswith("lxc.hook."):
print("Hooks aren't supported, skipping...")
return False
# Extract and valid rootfs key
print("Validating container rootfs")
value = config_get(lxc_config, "lxc.rootfs")
if not value:
print("Invalid container, missing lxc.rootfs key, skipping...")
return False
rootfs = value[0]
if not os.path.exists(rootfs):
print("Couldn't find the container rootfs '%s', skipping..." % rootfs)
return False
# Base config
config = {}
config['security.privileged'] = "true"
devices = {}
devices['eth0'] = {'type': "none"}
# Convert network configuration
print("Processing network configuration")
try:
count = len(container.get_config_item("lxc.network"))
except:
count = 0
for i in range(count):
device = {"type": "nic"}
# Get the device type
device["nictype"] = container.get_config_item("lxc.network")[i]
# Get everything else
dev = container.network[i]
# Validate configuration
if dev.ipv4 or dev.ipv4_gateway:
print("IPv4 network configuration isn't supported, skipping...")
return False
if dev.ipv6 or dev.ipv6_gateway:
print("IPv6 network configuration isn't supported, skipping...")
return False
if dev.script_up or dev.script_down:
print("Network config scripts aren't supported, skipping...")
return False
if device["nictype"] == "none":
print("\"none\" network mode isn't supported, skipping...")
return False
if device["nictype"] == "vlan":
print("\"vlan\" network mode isn't supported, skipping...")
return False
# Convert the configuration
if dev.hwaddr:
device['hwaddr'] = dev.hwaddr
if dev.link:
device['parent'] = dev.link
if dev.mtu:
device['mtu'] = int(dev.mtu)
if dev.name:
device['name'] = dev.name
if dev.veth_pair:
device['host_name'] = dev.veth_pair
if device["nictype"] == "veth":
if "parent" in device:
device["nictype"] = "bridged"
else:
device["nictype"] = "p2p"
if device["nictype"] == "phys":
device["nictype"] = "physical"
if device["nictype"] == "empty":
continue
devices['convert_net%d' % i] = device
count += 1
# Convert storage configuration
value = config_get(lxc_config, "lxc.mount.entry", [])
i = 0
for entry in value:
mount = entry.split(" ")
if len(mount) < 4:
print("Invalid mount configuration, skipping...")
return False
device = {'type': "disk"}
# Deal with read-only mounts
if "ro" in mount[3].split(","):
device['readonly'] = "true"
# Deal with optional mounts
if "optional" in mount[3].split(","):
device['optional'] = "true"
# Set the source
device['source'] = mount[0]
# Figure out the target
if mount[1][0] != "/":
device['path'] = "/%s" % mount[1]
else:
device['path'] = mount[1].split(rootfs, 1)[-1]
devices['convert_mount%d' % i] = device
i += 1
# Convert environment
print("Processing environment configuration")
value = config_get(lxc_config, "lxc.environment", [])
for env in value:
entry = env.split("=", 1)
config['environment.%s' % entry[0].strip()] = entry[-1].strip()
# Convert auto-start
print("Processing container boot configuration")
value = config_get(lxc_config, "lxc.start.auto")
if value and int(value[0]) > 0:
config['boot.autostart'] = "true"
value = config_get(lxc_config, "lxc.start.delay")
if value and int(value[0]) > 0:
config['boot.autostart.delay'] = value[0]
value = config_get(lxc_config, "lxc.start.order")
if value and int(value[0]) > 0:
config['boot.autostart.priority'] = value[0]
# Convert apparmor
print("Processing container apparmor configuration")
value = config_get(lxc_config, "lxc.aa_profile")
if value:
if value[0] == "lxc-container-default-with-nesting":
config['security.nesting'] = "true"
elif value[0] != "lxc-container-default":
print("Unsupported custom apparmor profile, skipping...")
return False
# Convert seccomp
print("Processing container seccomp configuration")
value = config_get(lxc_config, "lxc.seccomp")
if value:
print("Custom seccomp profiles aren't supported, skipping...")
return False
# Convert SELinux
print("Processing container SELinux configuration")
value = config_get(lxc_config, "lxc.se_context")
if value:
print("Custom SELinux policies aren't supported, skipping...")
return False
# Convert capabilities
print("Processing container capabilities configuration")
value = config_get(lxc_config, "lxc.cap.drop")
if value:
print("Custom capabilities aren't supported, skipping...")
return False
value = config_get(lxc_config, "lxc.cap.keep")
if value:
print("Custom capabilities aren't supported, skipping...")
return False
# Setup the container creation request
new = {'name': container_name,
'source': {'type': 'none'},
'config': config,
'devices': devices,
'profiles': ["default"]}
# Set the container architecture if set in LXC
print("Converting container architecture configuration")
arches = {'i686': "i686",
'x86_64': "x86_64",
'armhf': "armv7l",
'arm64': "aarch64",
'powerpc': "ppc",
'powerpc64': "ppc64",
'ppc64el': "ppc64le",
's390x': "s390x"}
arch = None
try:
arch = config_get(lxc_config, "lxc.arch", None)
if arch and arch[0] in arches:
new['architecture'] = arches[arch[0]]
else:
print("Unknown architecture, assuming native.")
except:
print("Couldn't find container architecture, assuming native.")
# Define the container in LXD
if args.debug:
print("LXD container config:")
print(json.dumps(new, indent=True, sort_keys=True))
if args.dry_run:
return True
try:
print("Creating the container")
lxd.containers.create(new, wait=True)
except Exception as e:
raise
print("Failed to create the container: %s" % e)
return False
# Transfer the filesystem
lxd_rootfs = os.path.join("/var/lib/lxd/", "containers",
container_name, "rootfs")
if args.copy_rootfs:
print("Copying container rootfs")
if not os.path.exists(lxd_rootfs):
os.mkdir(lxd_rootfs)
if subprocess.call(["rsync", "-Aa", "--sparse",
"--acls", "--numeric-ids", "--hard-links",
"%s/" % rootfs, "%s/" % lxd_rootfs]) != 0:
print("Failed to transfer the container rootfs, skipping...")
return False
else:
if os.path.exists(lxd_rootfs):
os.rmdir(lxd_rootfs)
if subprocess.call(["mv", rootfs, lxd_rootfs]) != 0:
print("Failed to move the container rootfs, skipping...")
return False
os.mkdir(rootfs)
# Delete the source
if args.delete:
print("Deleting source container")
container.delete()
# Mark the container as migrated
with open(container.config_file_name, "a") as fd:
fd.write("lxd.migrated=true\n")
print("Container is ready to use")
# Argument parsing
parser = argparse.ArgumentParser()
parser.add_argument("--dry-run", action="store_true", default=False,
help="Dry run mode")
parser.add_argument("--debug", action="store_true", default=False,
help="Print debugging output")
parser.add_argument("--all", action="store_true", default=False,
help="Import all containers")
parser.add_argument("--delete", action="store_true", default=False,
help="Delete the source container")
parser.add_argument("--copy-rootfs", action="store_true", default=False,
help="Copy the container rootfs rather than moving it")
parser.add_argument("--lxcpath", type=str, default=False,
help="Alternate LXC path")
parser.add_argument("--lxdpath", type=str, default=False,
help="Alternate LXD path")
parser.add_argument(dest='containers', metavar="CONTAINER", type=str,
help="Container to import", nargs="*")
args = parser.parse_args()
# Sanity checks
if not os.geteuid() == 0:
parser.error("You must be root to run this tool")
if (not args.containers and not args.all) or (args.containers and args.all):
parser.error("You must either pass container names or --all")
for container_name in lxc.list_containers(config_path=args.lxcpath):
if args.containers and container_name not in args.containers:
continue
convert_container(container_name, args)