3 # Copyright 2016 RIFT.IO Inc
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
9 # http://www.apache.org/licenses/LICENSE-2.0
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.
32 logger
= logging
.getLogger(__name__
)
35 class ValidationError(Exception):
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
))
46 shell
.command('umount {}'.format(path
))
47 logger
.debug('umount {}'.format(path
))
50 def create_container(name
, template_path
, volume
, rootfs_qcow2file
):
51 """Create a new container
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
60 A Container object for the new snapshot
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
)
68 loop_volume
= lvm
.get(volume
)
69 loop_volume
.extend_mbytes(virtual_size_mbytes
)
71 shell
.command(cmd
.format(
72 template_path
, name
, virtual_size_mbytes
, volume
75 return Container(name
, volume
=volume
, size_mbytes
=virtual_size_mbytes
)
78 def create_snapshot(base
, name
, volume
, size_mbytes
):
79 """Create a clone of an existing container
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
87 A Container object for the new snapshot
90 cmd
= '/bin/bash lxc-clone -o {} -n {} --vgname {} --snapshot'
92 loop_volume
= lvm
.get(volume
)
93 loop_volume
.extend_mbytes(size_mbytes
)
96 shell
.command(cmd
.format(base
, name
, volume
))
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.
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
)
112 elif "mkdir: cannot create directory" not in str(e
):
115 return Container(name
, volume
=volume
, size_mbytes
=size_mbytes
)
119 """Removes any cached templates"""
120 shell
.command('rm -rf /var/cache/lxc/*')
124 """Force cleanup of the lxc directory"""
126 lxc_dir
= "/var/lib/lxc/"
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
))
139 """Returns a list of containers"""
140 return [c
for c
in shell
.command('lxc-ls') if c
]
144 """Destroys a container
147 name - the name of the container to destroy
150 shell
.command('lxc-destroy -n {}'.format(name
))
154 """Starts a container
157 name - the name of the container to start
160 shell
.command('lxc-start -d -n {} -l DEBUG'.format(name
))
167 name - the name of the container to start
170 shell
.command('lxc-stop -n {}'.format(name
))
174 """Returns the current state of a container
177 name - the name of the container whose state is retuned
180 A string describing the state of the container
183 _
, state
= shell
.command('lxc-info -s -n {}'.format(name
))[0].split()
188 """Prints the output from 'lxc-ls --fancy'"""
189 print('\n'.join(shell
.command('lxc-ls --fancy')))
193 lxc_info
= shell
.command('lxc-ls --fancy --active --fancy-format=name,ipv4')
197 line_regex
= re
.compile("(.*?)\.(.*?)\.(.*?)\.(.*?)\.")
199 if line_regex
.match(lxc
):
200 lxc_name
= lxc
.split()[0]
202 ips
= lxc
.split()[1:]
203 lxc_to_ip
[lxc_name
] = [ip
.replace(",", "") for ip
in ips
]
210 This decorator is used to check that a given container exists. If the
211 container does not exist, a ValidationError is raised.
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
)
220 return f(self
, *args
, **kwargs
)
225 class Container(object):
227 This class provides an interface to an existing container on the system.
230 def __init__(self
, name
, size_mbytes
=4096, volume
="rift", hostname
=None):
232 self
._size
_mbytes
= size_mbytes
233 self
._volume
= volume
234 self
.hostname
= name
if hostname
is None else hostname
238 """The name of the container"""
243 """The virtual size of the container"""
244 return self
._size
_mbytes
248 """The volume that the container is a part of"""
252 def loopback_volume(self
):
253 """ Instance of lvm.LoopbackVolumeGroup """
254 return lvm
.get(self
.volume
)
259 """The current state of the container"""
260 return state(self
.name
)
264 """Starts the container"""
269 """Stops the container"""
274 """Destroys the container"""
279 """Returns info about the container"""
280 return shell
.command('lxc-info -n {}'.format(self
.name
))
283 def snapshot(self
, name
):
284 """Create a snapshot of this container
287 name - the name of the snapshot
290 A Container representing the new snapshot
293 return create_snapshot(self
.name
, name
, self
.volume
, self
.size
)
296 def configure(self
, config
, volume
='rift', userdata
=None):
297 """Configures the container
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
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
))
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)
316 with
mount('/dev/rift/{}'.format(self
.name
), rootfs
):
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')
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')
329 # Disable autofs (conflicts with lxc workspace mount bind)
330 autofs_service_file
= os
.path
.join(
332 "etc/systemd/system/multi-user.target.wants/autofs.service",
334 if os
.path
.exists(autofs_service_file
):
335 os
.remove(autofs_service_file
)
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)
342 # Copy the cloud-init script into the nocloud seed directory
343 if userdata
is not None:
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
:
351 with
open(userdata_dst
, 'w') as fp
:
353 except Exception as e
:
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
)
361 metadata_dst
= os
.path
.join(rootfs
, 'var/lib/cloud/seed/nocloud/meta-data')
362 with
open(metadata_dst
, 'w') as fp
:
365 except Exception as e
:
369 class ContainerConfig(object):
371 This class represents the config file that is used to define the interfaces
375 def __init__(self
, name
, volume
='rift'):
379 self
.mount_points
= []
380 self
.cgroups
= ControlGroupsConfig()
382 def add_network_config(self
, network_config
):
383 """Add a network config object
386 network_config - the network config object to add
389 self
.networks
.append(network_config
)
391 def add_mount_point_config(self
, mount_point_config
):
392 """Add a mount point to the configuration
395 mount_point_config - a MountPointConfig object
398 self
.mount_points
.append(mount_point_config
)
402 lxc.rootfs = /dev/{volume}/{name}
403 lxc.utsname = {utsname}
406 lxc.mount = /var/lib/lxc/{name}/fstab
407 lxc.cap.drop = sys_module mac_admin mac_override sys_time
411 """.format(volume
=self
.volume
, name
=self
.name
, utsname
=self
.name
)
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
)
418 return '\n'.join((fields
, cgroups
, networks
, mount_points
))
421 class ControlGroupsConfig(object):
423 This class represents the control group configuration for a container
429 lxc.cgroup.devices.deny = a
432 lxc.cgroup.devices.allow = c 1:3 rwm
433 lxc.cgroup.devices.allow = c 1:5 rwm
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
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
448 lxc.cgroup.devices.allow = c 254:0 rm
452 class NetworkConfig(collections
.namedtuple(
464 This class represents a network interface configuration for a container.
476 return super(NetworkConfig
, cls
).__new
__(
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
),
495 if self
.veth_pair
is not None:
496 fields
.append("lxc.network.veth.pair = {}".format(self
.veth_pair
))
498 if self
.ipv4
is not None:
499 fields
.append("lxc.network.ipv4 = {}/24".format(self
.ipv4
))
501 if self
.ipv4_gateway
is not None:
502 fields
.append("lxc.network.ipv4.gateway = {}".format(self
.ipv4_gateway
))
504 header
= ["# Start {} configuration".format(self
.name
)]
505 footer
= ["# End {} configuration\n".format(self
.name
)]
507 return '\n'.join(header
+ fields
+ footer
)
510 class MountConfig(collections
.namedtuple(
511 "ContainerMountConfig", [
518 This class represents a mount point configuration for a container.
521 def __new__(cls
, local
, remote
, read_only
=True):
522 return super(MountConfig
, cls
).__new
__(
530 return "lxc.mount.entry = {} {} none {}bind 0 0\n".format(
533 "" if not self
.read_only
else "ro,"