Revert "Full Juju Charm support"
[osm/SO.git] / rwcal / plugins / vala / rwcal_cloudsim / rift / rwcal / cloudsim / lxc.py
1
2 #
3 # Copyright 2016 RIFT.IO Inc
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 #
17
18 import collections
19 import contextlib
20 import functools
21 import logging
22 import os
23 import re
24 import shutil
25 import uuid
26
27 from . import shell
28 from . import image
29 from . import lvm
30
31
32 logger = logging.getLogger(__name__)
33
34
35 class ValidationError(Exception):
36 pass
37
38
39 @contextlib.contextmanager
40 def mount(mountpoint, path):
41 """Mounts a device and unmounts it upon exit"""
42 shell.command('mount {} {}'.format(mountpoint, path))
43 logger.debug('mount {} {}'.format(mountpoint, path))
44 yield
45 # os.sync()
46 shell.command('umount {}'.format(path))
47 logger.debug('umount {}'.format(path))
48
49
50 def create_container(name, template_path, volume, rootfs_qcow2file):
51 """Create a new container
52
53 Arguments:
54 name - the name of the new container
55 template_path - the template defines the type of container to create
56 volume - the volume group that the container will be in
57 roots_tarfile - a path to a tarfile that contains the rootfs
58
59 Returns:
60 A Container object for the new snapshot
61
62 """
63 cmd = 'lxc-create -t {} -n {} -B lvm --fssize {}M --vgname {}'
64 cmd += " -- --rootfs-qcow2file {}".format(rootfs_qcow2file)
65 cmd += " 2>&1 | tee -a /var/log/rift_lxc.log"
66 virtual_size_mbytes = image.qcow2_virtual_size_mbytes(rootfs_qcow2file)
67
68 loop_volume = lvm.get(volume)
69 loop_volume.extend_mbytes(virtual_size_mbytes)
70
71 shell.command(cmd.format(
72 template_path, name, virtual_size_mbytes, volume
73 ))
74
75 return Container(name, volume=volume, size_mbytes=virtual_size_mbytes)
76
77
78 def create_snapshot(base, name, volume, size_mbytes):
79 """Create a clone of an existing container
80
81 Arguments:
82 base - the name of the existing container
83 name - the name to give to the clone
84 volume - the volume group that the container will be in
85
86 Returns:
87 A Container object for the new snapshot
88
89 """
90 cmd = '/bin/bash lxc-clone -o {} -n {} --vgname {} --snapshot'
91
92 loop_volume = lvm.get(volume)
93 loop_volume.extend_mbytes(size_mbytes)
94
95 try:
96 shell.command(cmd.format(base, name, volume))
97
98 except shell.ProcessError as e:
99 # Skip the error that occurs here. It is corrected during configuration
100 # and results from a bug in the lxc script.
101
102 # In lxc-clone, when cloning multiple times from the same container
103 # it is possible that the lvrename operation fails to rename the
104 # file in /dev/rift (but the logical volume is renamed).
105 # This logic below resolves this particular scenario.
106 if "lxc-clone: failed to mount new rootfs" in str(e):
107 os.rmdir("/dev/rift/{name}".format(name=name))
108 shutil.move("/dev/rift/{name}_snapshot".format(name=name),
109 "/dev/rift/{name}".format(name=name)
110 )
111
112 elif "mkdir: cannot create directory" not in str(e):
113 raise
114
115 return Container(name, volume=volume, size_mbytes=size_mbytes)
116
117
118 def purge_cache():
119 """Removes any cached templates"""
120 shell.command('rm -rf /var/cache/lxc/*')
121
122
123 def force_clean():
124 """Force cleanup of the lxc directory"""
125
126 lxc_dir = "/var/lib/lxc/"
127 try:
128 shell.command('rm -rf {}*'.format(lxc_dir))
129 except shell.ProcessError:
130 for directory in os.listdir(lxc_dir):
131 path = os.path.join(lxc_dir, directory, "rootfs")
132 # Sometimes we might not be able to destroy container, if the
133 # device is still mounted so unmount it first.
134 shell.command("umount {}".format(path))
135 shell.command('rm -rf {}*'.format(lxc_dir))
136
137
138 def containers():
139 """Returns a list of containers"""
140 return [c for c in shell.command('lxc-ls') if c]
141
142
143 def destroy(name):
144 """Destroys a container
145
146 Arguments:
147 name - the name of the container to destroy
148
149 """
150 shell.command('lxc-destroy -n {}'.format(name))
151
152
153 def start(name):
154 """Starts a container
155
156 Arguments:
157 name - the name of the container to start
158
159 """
160 shell.command('lxc-start -d -n {} -l DEBUG'.format(name))
161
162
163 def stop(name):
164 """Stops a container
165
166 Arguments
167 name - the name of the container to start
168
169 """
170 shell.command('lxc-stop -n {}'.format(name))
171
172
173 def state(name):
174 """Returns the current state of a container
175
176 Arguments:
177 name - the name of the container whose state is retuned
178
179 Returns:
180 A string describing the state of the container
181
182 """
183 _, state = shell.command('lxc-info -s -n {}'.format(name))[0].split()
184 return state
185
186
187 def ls():
188 """Prints the output from 'lxc-ls --fancy'"""
189 print('\n'.join(shell.command('lxc-ls --fancy')))
190
191
192 def ls_info():
193 lxc_info = shell.command('lxc-ls --fancy --active --fancy-format=name,ipv4')
194
195 lxc_to_ip = {}
196
197 line_regex = re.compile("(.*?)\.(.*?)\.(.*?)\.(.*?)\.")
198 for lxc in lxc_info:
199 if line_regex.match(lxc):
200 lxc_name = lxc.split()[0]
201
202 ips = lxc.split()[1:]
203 lxc_to_ip[lxc_name] = [ip.replace(",", "") for ip in ips]
204
205 return lxc_to_ip
206
207
208 def validate(f):
209 """
210 This decorator is used to check that a given container exists. If the
211 container does not exist, a ValidationError is raised.
212
213 """
214 @functools.wraps(f)
215 def impl(self, *args, **kwargs):
216 if self.name not in containers():
217 msg = 'container ({}) does not exist'.format(self.name)
218 raise ValidationError(msg)
219
220 return f(self, *args, **kwargs)
221
222 return impl
223
224
225 class Container(object):
226 """
227 This class provides an interface to an existing container on the system.
228 """
229
230 def __init__(self, name, size_mbytes=4096, volume="rift", hostname=None):
231 self._name = name
232 self._size_mbytes = size_mbytes
233 self._volume = volume
234 self.hostname = name if hostname is None else hostname
235
236 @property
237 def name(self):
238 """The name of the container"""
239 return self._name
240
241 @property
242 def size(self):
243 """The virtual size of the container"""
244 return self._size_mbytes
245
246 @property
247 def volume(self):
248 """The volume that the container is a part of"""
249 return self._volume
250
251 @property
252 def loopback_volume(self):
253 """ Instance of lvm.LoopbackVolumeGroup """
254 return lvm.get(self.volume)
255
256 @property
257 @validate
258 def state(self):
259 """The current state of the container"""
260 return state(self.name)
261
262 @validate
263 def start(self):
264 """Starts the container"""
265 start(self.name)
266
267 @validate
268 def stop(self):
269 """Stops the container"""
270 stop(self.name)
271
272 @validate
273 def destroy(self):
274 """Destroys the container"""
275 destroy(self.name)
276
277 @validate
278 def info(self):
279 """Returns info about the container"""
280 return shell.command('lxc-info -n {}'.format(self.name))
281
282 @validate
283 def snapshot(self, name):
284 """Create a snapshot of this container
285
286 Arguments:
287 name - the name of the snapshot
288
289 Returns:
290 A Container representing the new snapshot
291
292 """
293 return create_snapshot(self.name, name, self.volume, self.size)
294
295 @validate
296 def configure(self, config, volume='rift', userdata=None):
297 """Configures the container
298
299 Arguments:
300 config - a container configuration object
301 volume - the volume group that the container will belong to
302 userdata - a string containing userdata that will be passed to
303 cloud-init for execution
304
305 """
306 # Create the LXC config file
307 with open("/var/lib/lxc/{}/config".format(self.name), "w") as fp:
308 fp.write(str(config))
309 logger.debug('created /var/lib/lxc/{}/config'.format(self.name))
310
311 # Mount the rootfs of the container and configure the hosts and
312 # hostname files of the container.
313 rootfs = '/var/lib/lxc/{}/rootfs'.format(self.name)
314 os.makedirs(rootfs, exist_ok=True)
315
316 with mount('/dev/rift/{}'.format(self.name), rootfs):
317
318 # Create /etc/hostname
319 with open(os.path.join(rootfs, 'etc/hostname'), 'w') as fp:
320 fp.write(self.hostname + '\n')
321 logger.debug('created /etc/hostname')
322
323 # Create /etc/hostnames
324 with open(os.path.join(rootfs, 'etc/hosts'), 'w') as fp:
325 fp.write("127.0.0.1 localhost {}\n".format(self.hostname))
326 fp.write("::1 localhost {}\n".format(self.hostname))
327 logger.debug('created /etc/hosts')
328
329 # Disable autofs (conflicts with lxc workspace mount bind)
330 autofs_service_file = os.path.join(
331 rootfs,
332 "etc/systemd/system/multi-user.target.wants/autofs.service",
333 )
334 if os.path.exists(autofs_service_file):
335 os.remove(autofs_service_file)
336
337 # Setup the mount points
338 for mount_point in config.mount_points:
339 mount_point_path = os.path.join(rootfs, mount_point.remote)
340 os.makedirs(mount_point_path, exist_ok=True)
341
342 # Copy the cloud-init script into the nocloud seed directory
343 if userdata is not None:
344 try:
345 userdata_dst = os.path.join(rootfs, 'var/lib/cloud/seed/nocloud/user-data')
346 os.makedirs(os.path.dirname(userdata_dst))
347 except FileExistsError:
348 pass
349
350 try:
351 with open(userdata_dst, 'w') as fp:
352 fp.write(userdata)
353 except Exception as e:
354 logger.exception(e)
355
356 # Cloud init requires a meta-data file in the seed location
357 metadata = "instance_id: {}\n".format(str(uuid.uuid4()))
358 metadata += "local-hostname: {}\n".format(self.hostname)
359
360 try:
361 metadata_dst = os.path.join(rootfs, 'var/lib/cloud/seed/nocloud/meta-data')
362 with open(metadata_dst, 'w') as fp:
363 fp.write(metadata)
364
365 except Exception as e:
366 logger.exception(e)
367
368
369 class ContainerConfig(object):
370 """
371 This class represents the config file that is used to define the interfaces
372 on a container.
373 """
374
375 def __init__(self, name, volume='rift'):
376 self.name = name
377 self.volume = volume
378 self.networks = []
379 self.mount_points = []
380 self.cgroups = ControlGroupsConfig()
381
382 def add_network_config(self, network_config):
383 """Add a network config object
384
385 Arguments:
386 network_config - the network config object to add
387
388 """
389 self.networks.append(network_config)
390
391 def add_mount_point_config(self, mount_point_config):
392 """Add a mount point to the configuration
393
394 Arguments,
395 mount_point_config - a MountPointConfig object
396
397 """
398 self.mount_points.append(mount_point_config)
399
400 def __repr__(self):
401 fields = """
402 lxc.rootfs = /dev/{volume}/{name}
403 lxc.utsname = {utsname}
404 lxc.tty = 4
405 lxc.pts = 1024
406 lxc.mount = /var/lib/lxc/{name}/fstab
407 lxc.cap.drop = sys_module mac_admin mac_override sys_time
408 lxc.kmsg = 0
409 lxc.autodev = 1
410 lxc.kmsg = 0
411 """.format(volume=self.volume, name=self.name, utsname=self.name)
412
413 fields = '\n'.join(n.strip() for n in fields.splitlines())
414 cgroups = '\n'.join(n.strip() for n in str(self.cgroups).splitlines())
415 networks = '\n'.join(str(n) for n in self.networks)
416 mount_points = '\n'.join(str(n) for n in self.mount_points)
417
418 return '\n'.join((fields, cgroups, networks, mount_points))
419
420
421 class ControlGroupsConfig(object):
422 """
423 This class represents the control group configuration for a container
424 """
425
426 def __repr__(self):
427 return """
428 #cgroups
429 lxc.cgroup.devices.deny = a
430
431 # /dev/null and zero
432 lxc.cgroup.devices.allow = c 1:3 rwm
433 lxc.cgroup.devices.allow = c 1:5 rwm
434
435 # consoles
436 lxc.cgroup.devices.allow = c 5:1 rwm
437 lxc.cgroup.devices.allow = c 5:0 rwm
438 lxc.cgroup.devices.allow = c 4:0 rwm
439 lxc.cgroup.devices.allow = c 4:1 rwm
440
441 # /dev/{,u}random
442 lxc.cgroup.devices.allow = c 1:9 rwm
443 lxc.cgroup.devices.allow = c 1:8 rwm
444 lxc.cgroup.devices.allow = c 136:* rwm
445 lxc.cgroup.devices.allow = c 5:2 rwm
446
447 # rtc
448 lxc.cgroup.devices.allow = c 254:0 rm
449 """
450
451
452 class NetworkConfig(collections.namedtuple(
453 "NetworkConfig", [
454 "type",
455 "link",
456 "flags",
457 "name",
458 "veth_pair",
459 "ipv4",
460 "ipv4_gateway",
461 ]
462 )):
463 """
464 This class represents a network interface configuration for a container.
465 """
466
467 def __new__(cls,
468 type,
469 link,
470 name,
471 flags='up',
472 veth_pair=None,
473 ipv4=None,
474 ipv4_gateway=None,
475 ):
476 return super(NetworkConfig, cls).__new__(
477 cls,
478 type,
479 link,
480 flags,
481 name,
482 veth_pair,
483 ipv4,
484 ipv4_gateway,
485 )
486
487 def __repr__(self):
488 fields = [
489 "lxc.network.type = {}".format(self.type),
490 "lxc.network.link = {}".format(self.link),
491 "lxc.network.flags = {}".format(self.flags),
492 "lxc.network.name = {}".format(self.name),
493 ]
494
495 if self.veth_pair is not None:
496 fields.append("lxc.network.veth.pair = {}".format(self.veth_pair))
497
498 if self.ipv4 is not None:
499 fields.append("lxc.network.ipv4 = {}/24".format(self.ipv4))
500
501 if self.ipv4_gateway is not None:
502 fields.append("lxc.network.ipv4.gateway = {}".format(self.ipv4_gateway))
503
504 header = ["# Start {} configuration".format(self.name)]
505 footer = ["# End {} configuration\n".format(self.name)]
506
507 return '\n'.join(header + fields + footer)
508
509
510 class MountConfig(collections.namedtuple(
511 "ContainerMountConfig", [
512 "local",
513 "remote",
514 "read_only",
515 ]
516 )):
517 """
518 This class represents a mount point configuration for a container.
519 """
520
521 def __new__(cls, local, remote, read_only=True):
522 return super(MountConfig, cls).__new__(
523 cls,
524 local,
525 remote,
526 read_only,
527 )
528
529 def __repr__(self):
530 return "lxc.mount.entry = {} {} none {}bind 0 0\n".format(
531 self.local,
532 self.remote,
533 "" if not self.read_only else "ro,"
534 )