1 # Copyright (c) 2019 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
6 # http://www.apache.org/licenses/LICENSE-2.0
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
14 # Bug workaround in pylint for abstract classes.
15 # pylint: disable=W0223
17 """Library to manipulate Containers."""
19 from collections import OrderedDict, Counter
21 from resources.libraries.python.ssh import SSH
22 from resources.libraries.python.constants import Constants
23 from resources.libraries.python.topology import Topology
24 from resources.libraries.python.VppConfigGenerator import VppConfigGenerator
27 __all__ = ["ContainerManager", "ContainerEngine", "LXC", "Docker", "Container"]
29 SUPERVISOR_CONF = '/etc/supervisord.conf'
32 class ContainerManager(object):
33 """Container lifecycle management class."""
35 def __init__(self, engine):
36 """Initialize Container Manager class.
38 :param engine: Container technology used (LXC/Docker/...).
40 :raises NotImplementedError: If container technology is not implemented.
43 self.engine = globals()[engine]()
45 raise NotImplementedError('{engine} is not implemented.'.
46 format(engine=engine))
47 self.containers = OrderedDict()
49 def get_container_by_name(self, name):
50 """Get container instance.
52 :param name: Container name.
54 :returns: Container instance.
56 :raises RuntimeError: If failed to get container with name.
59 return self.containers[name]
61 raise RuntimeError('Failed to get container with name: {name}'.
64 def construct_container(self, **kwargs):
65 """Construct container object on node with specified parameters.
67 :param kwargs: Key-value pairs used to construct container.
71 self.engine.initialize()
74 setattr(self.engine.container, key, kwargs[key])
76 # Set additional environmental variables
77 setattr(self.engine.container, 'env',
78 'MICROSERVICE_LABEL={label}'.format(label=kwargs['name']))
80 # Store container instance
81 self.containers[kwargs['name']] = self.engine.container
83 def construct_containers(self, **kwargs):
84 """Construct 1..N container(s) on node with specified name.
86 Ordinal number is automatically added to the name of container as
89 :param kwargs: Named parameters.
93 for i in range(kwargs['count']):
94 # Name will contain ordinal suffix
95 kwargs['name'] = ''.join([name, str(i+1)])
97 self.construct_container(i=i, **kwargs)
99 def acquire_all_containers(self):
100 """Acquire all containers."""
101 for container in self.containers:
102 self.engine.container = self.containers[container]
103 self.engine.acquire()
105 def build_all_containers(self):
106 """Build all containers."""
107 for container in self.containers:
108 self.engine.container = self.containers[container]
111 def create_all_containers(self):
112 """Create all containers."""
113 for container in self.containers:
114 self.engine.container = self.containers[container]
117 def execute_on_container(self, name, command):
118 """Execute command on container with name.
120 :param name: Container name.
121 :param command: Command to execute.
125 self.engine.container = self.get_container_by_name(name)
126 self.engine.execute(command)
128 def execute_on_all_containers(self, command):
129 """Execute command on all containers.
131 :param command: Command to execute.
134 for container in self.containers:
135 self.engine.container = self.containers[container]
136 self.engine.execute(command)
138 def install_vpp_in_all_containers(self):
139 """Install VPP into all containers."""
140 for container in self.containers:
141 self.engine.container = self.containers[container]
142 # We need to install supervisor client/server system to control VPP
144 self.engine.execute('apt-get update')
145 self.engine.install_supervisor()
146 self.engine.install_vpp()
147 self.engine.restart_vpp()
149 def restart_vpp_in_all_containers(self):
150 """Restart VPP on all containers."""
151 for container in self.containers:
152 self.engine.container = self.containers[container]
153 self.engine.restart_vpp()
155 def configure_vpp_in_all_containers(self, chain_topology, **kwargs):
156 """Configure VPP in all containers.
158 :param chain_topology: Topology used for chaining containers can be
159 chain or cross_horiz. Chain topology is using 1 memif pair per
160 container. Cross_horiz topology is using 1 memif and 1 physical
161 interface in container (only single container can be configured).
162 :param kwargs: Named parameters.
163 :type chain_topology: str
166 # Count number of DUTs based on node's host information
167 dut_cnt = len(Counter([self.containers[container].node['host']
168 for container in self.containers]))
169 mod = len(self.containers)/dut_cnt
171 for i, container in enumerate(self.containers):
174 sid1 = i % mod * 2 + 1
175 sid2 = i % mod * 2 + 2
176 self.engine.container = self.containers[container]
177 guest_dir = self.engine.container.mnt[0].split(':')[1]
179 if chain_topology == 'chain':
180 self._configure_vpp_chain_l2xc(mid1=mid1, mid2=mid2,
181 sid1=sid1, sid2=sid2,
184 elif chain_topology == 'cross_horiz':
185 self._configure_vpp_cross_horiz(mid1=mid1, mid2=mid2,
186 sid1=sid1, sid2=sid2,
189 elif chain_topology == 'chain_functional':
190 self._configure_vpp_chain_functional(mid1=mid1, mid2=mid2,
191 sid1=sid1, sid2=sid2,
194 elif chain_topology == 'chain_ip4':
195 self._configure_vpp_chain_ip4(mid1=mid1, mid2=mid2,
196 sid1=sid1, sid2=sid2,
199 elif chain_topology == 'pipeline_ip4':
200 self._configure_vpp_pipeline_ip4(mid1=mid1, mid2=mid2,
201 sid1=sid1, sid2=sid2,
205 raise RuntimeError('Container topology {name} not implemented'.
206 format(name=chain_topology))
208 def _configure_vpp_chain_l2xc(self, **kwargs):
209 """Configure VPP in chain topology with l2xc.
211 :param kwargs: Named parameters.
214 self.engine.create_vpp_startup_config()
215 self.engine.create_vpp_exec_config(
216 'memif_create_chain_l2xc.vat',
217 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
218 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
219 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
220 format(c=self.engine.container, **kwargs),
221 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
222 format(c=self.engine.container, **kwargs))
224 def _configure_vpp_cross_horiz(self, **kwargs):
225 """Configure VPP in cross horizontal topology (single memif).
227 :param kwargs: Named parameters.
230 if 'DUT1' in self.engine.container.name:
231 if_pci = Topology.get_interface_pci_addr(
232 self.engine.container.node, kwargs['dut1_if'])
233 if_name = Topology.get_interface_name(
234 self.engine.container.node, kwargs['dut1_if'])
235 if 'DUT2' in self.engine.container.name:
236 if_pci = Topology.get_interface_pci_addr(
237 self.engine.container.node, kwargs['dut2_if'])
238 if_name = Topology.get_interface_name(
239 self.engine.container.node, kwargs['dut2_if'])
240 self.engine.create_vpp_startup_config_dpdk_dev(if_pci)
241 self.engine.create_vpp_exec_config(
242 'memif_create_cross_horizon.vat',
243 mid1=kwargs['mid1'], sid1=kwargs['sid1'], if_name=if_name,
244 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
245 format(c=self.engine.container, **kwargs))
247 def _configure_vpp_chain_functional(self, **kwargs):
248 """Configure VPP in chain topology with l2xc (functional).
250 :param kwargs: Named parameters.
253 self.engine.create_vpp_startup_config_func_dev()
254 self.engine.create_vpp_exec_config(
255 'memif_create_chain_functional.vat',
256 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
257 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
258 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
259 format(c=self.engine.container, **kwargs),
260 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
261 format(c=self.engine.container, **kwargs),
264 def _configure_vpp_chain_ip4(self, **kwargs):
265 """Configure VPP in chain topology with ip4.
267 :param kwargs: Named parameters.
270 self.engine.create_vpp_startup_config()
272 vif1_mac = kwargs['tg_if1_mac'] \
273 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
274 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
275 vif2_mac = kwargs['tg_if2_mac'] \
276 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
277 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
278 self.engine.create_vpp_exec_config(
279 'memif_create_chain_ip4.vat',
280 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
281 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
282 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
283 format(c=self.engine.container, **kwargs),
284 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
285 format(c=self.engine.container, **kwargs),
286 mac1='52:54:00:00:{0:02X}:01'.format(kwargs['mid1']),
287 mac2='52:54:00:00:{0:02X}:02'.format(kwargs['mid2']),
288 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
290 def _configure_vpp_pipeline_ip4(self, **kwargs):
291 """Configure VPP in pipeline topology with ip4.
293 :param kwargs: Named parameters.
296 self.engine.create_vpp_startup_config()
297 node = (kwargs['mid1'] - 1) % kwargs['nodes'] + 1
298 mid1 = kwargs['mid1']
299 mid2 = kwargs['mid2']
302 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
304 kwargs['mid2'] = kwargs['mid2'] \
305 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
306 else kwargs['mid2'] + 1
307 vif1_mac = kwargs['tg_if1_mac'] \
308 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
309 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
310 vif2_mac = kwargs['tg_if2_mac'] \
311 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
312 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
313 socket1 = '{guest_dir}/memif-{c.name}-{sid1}'.\
314 format(c=self.engine.container, **kwargs) \
315 if node == 1 else '{guest_dir}/memif-pipe-{mid1}'.\
316 format(c=self.engine.container, **kwargs)
317 socket2 = '{guest_dir}/memif-{c.name}-{sid2}'.\
318 format(c=self.engine.container, **kwargs) \
319 if node == 1 and kwargs['nodes'] == 1 or node == kwargs['nodes'] \
320 else '{guest_dir}/memif-pipe-{mid2}'.\
321 format(c=self.engine.container, **kwargs)
323 self.engine.create_vpp_exec_config(
324 'memif_create_pipeline_ip4.vat',
325 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
326 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
327 socket1=socket1, socket2=socket2, role1=role1, role2=role2,
328 mac1='52:54:00:00:{0:02X}:01'.format(mid1),
329 mac2='52:54:00:00:{0:02X}:02'.format(mid2),
330 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
332 def stop_all_containers(self):
333 """Stop all containers."""
334 for container in self.containers:
335 self.engine.container = self.containers[container]
338 def destroy_all_containers(self):
339 """Destroy all containers."""
340 for container in self.containers:
341 self.engine.container = self.containers[container]
342 self.engine.destroy()
345 class ContainerEngine(object):
346 """Abstract class for container engine."""
349 """Init ContainerEngine object."""
350 self.container = None
352 def initialize(self):
353 """Initialize container object."""
354 self.container = Container()
356 def acquire(self, force):
357 """Acquire/download container.
359 :param force: Destroy a container if exists and create.
362 raise NotImplementedError
365 """Build container (compile)."""
366 raise NotImplementedError
369 """Create/deploy container."""
370 raise NotImplementedError
372 def execute(self, command):
373 """Execute process inside container.
375 :param command: Command to run inside container.
378 raise NotImplementedError
381 """Stop container."""
382 raise NotImplementedError
385 """Destroy/remove container."""
386 raise NotImplementedError
389 """Info about container."""
390 raise NotImplementedError
392 def system_info(self):
394 raise NotImplementedError
396 def install_supervisor(self):
397 """Install supervisord inside a container."""
398 self.execute('apt-get install -y supervisor')
399 self.execute('echo "{config}" > {config_file} && '
400 'supervisord -c {config_file}'.
402 config='[unix_http_server]\n'
403 'file = /tmp/supervisor.sock\n\n'
404 '[rpcinterface:supervisor]\n'
405 'supervisor.rpcinterface_factory = '
406 'supervisor.rpcinterface:make_main_rpcinterface\n\n'
408 'serverurl = unix:///tmp/supervisor.sock\n\n'
410 'pidfile = /tmp/supervisord.pid\n'
411 'identifier = supervisor\n'
413 'logfile=/tmp/supervisord.log\n'
415 'nodaemon=false\n\n',
416 config_file=SUPERVISOR_CONF))
418 def install_vpp(self):
419 """Install VPP inside a container."""
420 self.execute('ln -s /dev/null /etc/sysctl.d/80-vpp.conf')
421 # Workaround for install xenial vpp build on bionic ubuntu.
422 self.execute('apt-get install -y wget')
423 self.execute('deb=$(mktemp) && wget -O "${deb}" '
424 'http://launchpadlibrarian.net/336117627/'
425 'libmbedcrypto0_2.5.1-1ubuntu1_amd64.deb && '
426 'dpkg -i "${deb}" && '
428 self.execute('deb=$(mktemp) && wget -O "${deb}" '
429 'http://launchpadlibrarian.net/252876048/'
430 'libboost-system1.58.0_1.58.0+dfsg-5ubuntu3_amd64.deb && '
431 'dpkg -i "${deb}" && '
434 'dpkg -i --force-all '
435 '{guest_dir}/openvpp-testing/download_dir/*.deb'.
436 format(guest_dir=self.container.mnt[0].split(':')[1]))
437 self.execute('apt-get -f install -y')
438 self.execute('apt-get install -y ca-certificates')
439 self.execute('echo "{config}" >> {config_file}'.
441 config='[program:vpp]\n'
442 'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
443 'autorestart=false\n'
444 'redirect_stderr=true\n'
446 config_file=SUPERVISOR_CONF))
447 self.execute('supervisorctl reload')
449 def restart_vpp(self):
450 """Restart VPP service inside a container."""
451 self.execute('supervisorctl restart vpp')
452 self.execute('cat /tmp/supervisord.log')
454 def create_base_vpp_startup_config(self):
455 """Create base startup configuration of VPP on container.
457 :returns: Base VPP startup configuration.
458 :rtype: VppConfigGenerator
460 cpuset_cpus = self.container.cpuset_cpus
462 # Create config instance
463 vpp_config = VppConfigGenerator()
464 vpp_config.set_node(self.container.node)
465 vpp_config.add_unix_cli_listen()
466 vpp_config.add_unix_nodaemon()
467 vpp_config.add_unix_exec('/tmp/running.exec')
468 # We will pop the first core from the list to be a main core
469 vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
470 # If more cores in the list, the rest will be used as workers.
472 corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
473 vpp_config.add_cpu_corelist_workers(corelist_workers)
477 def create_vpp_startup_config(self):
478 """Create startup configuration of VPP without DPDK on container.
480 vpp_config = self.create_base_vpp_startup_config()
481 vpp_config.add_plugin('disable', 'dpdk_plugin.so')
483 # Apply configuration
484 self.execute('mkdir -p /etc/vpp/')
485 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
486 .format(config=vpp_config.get_config_str()))
488 def create_vpp_startup_config_dpdk_dev(self, *devices):
489 """Create startup configuration of VPP with DPDK on container.
491 :param devices: List of PCI devices to add.
494 vpp_config = self.create_base_vpp_startup_config()
495 vpp_config.add_dpdk_dev(*devices)
496 vpp_config.add_dpdk_no_tx_checksum_offload()
497 vpp_config.add_dpdk_log_level('debug')
498 vpp_config.add_plugin('disable', 'default')
499 vpp_config.add_plugin('enable', 'dpdk_plugin.so')
500 vpp_config.add_plugin('enable', 'memif_plugin.so')
502 # Apply configuration
503 self.execute('mkdir -p /etc/vpp/')
504 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
505 .format(config=vpp_config.get_config_str()))
507 def create_vpp_startup_config_func_dev(self):
508 """Create startup configuration of VPP on container for functional
511 # Create config instance
512 vpp_config = VppConfigGenerator()
513 vpp_config.set_node(self.container.node)
514 vpp_config.add_unix_cli_listen()
515 vpp_config.add_unix_nodaemon()
516 vpp_config.add_unix_exec('/tmp/running.exec')
517 vpp_config.add_plugin('disable', 'dpdk_plugin.so')
519 # Apply configuration
520 self.execute('mkdir -p /etc/vpp/')
521 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
522 .format(config=vpp_config.get_config_str()))
524 def create_vpp_exec_config(self, vat_template_file, **kwargs):
525 """Create VPP exec configuration on container.
527 :param vat_template_file: File name of a VAT template script.
528 :param kwargs: Parameters for VAT script.
529 :type vat_template_file: str
532 vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
535 with open(vat_file_path, 'r') as template_file:
536 cmd_template = template_file.readlines()
537 for line_tmpl in cmd_template:
538 vat_cmd = line_tmpl.format(**kwargs)
539 self.execute('echo "{c}" >> /tmp/running.exec'
540 .format(c=vat_cmd.replace('\n', '')))
542 def is_container_running(self):
543 """Check if container is running."""
544 raise NotImplementedError
546 def is_container_present(self):
547 """Check if container is present."""
548 raise NotImplementedError
550 def _configure_cgroup(self, name):
551 """Configure the control group associated with a container.
553 By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
554 container is initialized a new cgroup /docker or /lxc is created under
555 cpuset parent tree. This newly created cgroup is inheriting parent
556 setting for cpu/mem exclusive parameter and thus cannot be overriden
557 within /docker or /lxc cgroup. This function is supposed to set cgroups
558 to allow coexistence of both engines.
560 :param name: Name of cgroup.
562 :raises RuntimeError: If applying cgroup settings via cgset failed.
564 ret, _, _ = self.container.ssh.exec_command_sudo(
565 'cgset -r cpuset.cpu_exclusive=0 /')
567 raise RuntimeError('Failed to apply cgroup settings.')
569 ret, _, _ = self.container.ssh.exec_command_sudo(
570 'cgset -r cpuset.mem_exclusive=0 /')
572 raise RuntimeError('Failed to apply cgroup settings.')
574 ret, _, _ = self.container.ssh.exec_command_sudo(
575 'cgcreate -g cpuset:/{name}'.format(name=name))
577 raise RuntimeError('Failed to copy cgroup settings from root.')
579 ret, _, _ = self.container.ssh.exec_command_sudo(
580 'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
582 raise RuntimeError('Failed to apply cgroup settings.')
584 ret, _, _ = self.container.ssh.exec_command_sudo(
585 'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
587 raise RuntimeError('Failed to apply cgroup settings.')
590 class LXC(ContainerEngine):
591 """LXC implementation."""
593 # Implicit constructor is inherited.
595 def acquire(self, force=True):
596 """Acquire a privileged system object where configuration is stored.
598 :param force: If a container exists, destroy it and create a new
601 :raises RuntimeError: If creating the container or writing the container
604 if self.is_container_present():
610 image = self.container.image if self.container.image else\
611 "-d ubuntu -r xenial -a amd64"
613 cmd = 'lxc-create -t download --name {c.name} -- {image} '\
614 '--no-validate'.format(c=self.container, image=image)
616 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
618 raise RuntimeError('Failed to create container.')
620 self._configure_cgroup('lxc')
623 """Create/deploy an application inside a container on system.
625 :raises RuntimeError: If creating the container fails.
627 if self.container.mnt:
628 for mount in self.container.mnt:
629 host_dir, guest_dir = mount.split(':')
630 entry = 'lxc.mount.entry = {host_dir} '\
631 '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
632 'bind,create=dir 0 0'.format(c=self.container,
635 ret, _, _ = self.container.ssh.exec_command_sudo(
636 "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
637 format(e=entry, c=self.container))
639 raise RuntimeError('Failed to write {c.name} config.'
640 .format(c=self.container))
642 cpuset_cpus = '{0}'.format(
643 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
644 if self.container.cpuset_cpus else ''
646 ret, _, _ = self.container.ssh.exec_command_sudo(
647 'lxc-start --name {c.name} --daemon'.
648 format(c=self.container))
650 raise RuntimeError('Failed to start container {c.name}.'.
651 format(c=self.container))
652 self._lxc_wait('RUNNING')
654 # Workaround for LXC to be able to allocate all cpus including isolated.
655 ret, _, _ = self.container.ssh.exec_command_sudo(
656 'cgset --copy-from / lxc/')
658 raise RuntimeError('Failed to copy cgroup to LXC')
660 ret, _, _ = self.container.ssh.exec_command_sudo(
661 'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
662 format(c=self.container, cpus=cpuset_cpus))
664 raise RuntimeError('Failed to set cpuset.cpus to container '
665 '{c.name}.'.format(c=self.container))
667 def execute(self, command):
668 """Start a process inside a running container.
670 Runs the specified command inside the container specified by name. The
671 container has to be running already.
673 :param command: Command to run inside container.
675 :raises RuntimeError: If running the command failed.
677 env = '--keep-env {0}'.format(
678 ' '.join('--set-var %s' % env for env in self.container.env))\
679 if self.container.env else ''
681 cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}; "\
682 "exit $?'".format(env=env, c=self.container, command=command)
684 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
686 raise RuntimeError('Failed to run command inside container '
687 '{c.name}.'.format(c=self.container))
692 :raises RuntimeError: If stopping the container failed.
694 cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
696 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
698 raise RuntimeError('Failed to stop container {c.name}.'
699 .format(c=self.container))
700 self._lxc_wait('STOPPED|FROZEN')
703 """Destroy a container.
705 :raises RuntimeError: If destroying container failed.
707 cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
709 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
711 raise RuntimeError('Failed to destroy container {c.name}.'
712 .format(c=self.container))
715 """Query and shows information about a container.
717 :raises RuntimeError: If getting info about a container failed.
719 cmd = 'lxc-info --name {c.name}'.format(c=self.container)
721 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
723 raise RuntimeError('Failed to get info about container {c.name}.'
724 .format(c=self.container))
726 def system_info(self):
727 """Check the current kernel for LXC support.
729 :raises RuntimeError: If checking LXC support failed.
731 cmd = 'lxc-checkconfig'
733 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
735 raise RuntimeError('Failed to check LXC support.')
737 def is_container_running(self):
738 """Check if container is running on node.
740 :returns: True if container is running.
742 :raises RuntimeError: If getting info about a container failed.
744 cmd = 'lxc-info --no-humanize --state --name {c.name}'\
745 .format(c=self.container)
747 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
749 raise RuntimeError('Failed to get info about container {c.name}.'
750 .format(c=self.container))
751 return True if 'RUNNING' in stdout else False
753 def is_container_present(self):
754 """Check if container is existing on node.
756 :returns: True if container is present.
758 :raises RuntimeError: If getting info about a container failed.
760 cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
762 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
763 return False if int(ret) else True
765 def _lxc_wait(self, state):
766 """Wait for a specific container state.
768 :param state: Specify the container state(s) to wait for.
770 :raises RuntimeError: If waiting for state of a container failed.
772 cmd = 'lxc-wait --name {c.name} --state "{s}"'\
773 .format(c=self.container, s=state)
775 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
777 raise RuntimeError('Failed to wait for state "{s}" of container '
778 '{c.name}.'.format(s=state, c=self.container))
781 class Docker(ContainerEngine):
782 """Docker implementation."""
784 # Implicit constructor is inherited.
786 def acquire(self, force=True):
787 """Pull an image or a repository from a registry.
789 :param force: Destroy a container if exists.
791 :raises RuntimeError: If pulling a container failed.
793 if self.is_container_present():
799 if not self.container.image:
800 setattr(self.container, 'image', 'snergster/csit-sut:latest')
802 cmd = 'docker pull {image}'.format(image=self.container.image)
804 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
806 raise RuntimeError('Failed to create container {c.name}.'
807 .format(c=self.container))
808 if self.container.cpuset_cpus:
809 self._configure_cgroup('docker')
812 """Create/deploy container.
814 :raises RuntimeError: If creating a container failed.
816 cpuset_cpus = '--cpuset-cpus={0}'.format(
817 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
818 if self.container.cpuset_cpus else ''
820 cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
821 if self.container.cpuset_mems is not None else ''
822 # Temporary workaround - disabling due to bug in memif
826 ' '.join('--env %s' % env for env in self.container.env))\
827 if self.container.env else ''
829 command = '{0}'.format(self.container.command)\
830 if self.container.command else ''
832 publish = '{0}'.format(
833 ' '.join('--publish %s' % var for var in self.container.publish))\
834 if self.container.publish else ''
836 volume = '{0}'.format(
837 ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
838 if self.container.mnt else ''
841 '--privileged --detach --interactive --tty --rm '\
842 '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
843 '{env} {volume} --name {container.name} {container.image} '\
844 '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
845 container=self.container, command=command,
846 env=env, publish=publish, volume=volume)
848 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
850 raise RuntimeError('Failed to create container {c.name}'
851 .format(c=self.container))
855 def execute(self, command):
856 """Start a process inside a running container.
858 Runs the specified command inside the container specified by name. The
859 container has to be running already.
861 :param command: Command to run inside container.
863 :raises RuntimeError: If runnig the command in a container failed.
865 cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}; "\
866 "exit $?'".format(c=self.container, command=command)
868 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
870 raise RuntimeError('Failed to execute command in container '
871 '{c.name}.'.format(c=self.container))
874 """Stop running container.
876 :raises RuntimeError: If stopping a container failed.
878 cmd = 'docker stop {c.name}'.format(c=self.container)
880 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
882 raise RuntimeError('Failed to stop container {c.name}.'
883 .format(c=self.container))
886 """Remove a container.
888 :raises RuntimeError: If removing a container failed.
890 cmd = 'docker rm --force {c.name}'.format(c=self.container)
892 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
894 raise RuntimeError('Failed to destroy container {c.name}.'
895 .format(c=self.container))
898 """Return low-level information on Docker objects.
900 :raises RuntimeError: If getting info about a container failed.
902 cmd = 'docker inspect {c.name}'.format(c=self.container)
904 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
906 raise RuntimeError('Failed to get info about container {c.name}.'
907 .format(c=self.container))
909 def system_info(self):
910 """Display the docker system-wide information.
912 :raises RuntimeError: If displaying system information failed.
914 cmd = 'docker system info'
916 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
918 raise RuntimeError('Failed to get system info.')
920 def is_container_present(self):
921 """Check if container is present on node.
923 :returns: True if container is present.
925 :raises RuntimeError: If getting info about a container failed.
927 cmd = 'docker ps --all --quiet --filter name={c.name}'\
928 .format(c=self.container)
930 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
932 raise RuntimeError('Failed to get info about container {c.name}.'
933 .format(c=self.container))
934 return True if stdout else False
936 def is_container_running(self):
937 """Check if container is running on node.
939 :returns: True if container is running.
941 :raises RuntimeError: If getting info about a container failed.
943 cmd = 'docker ps --quiet --filter name={c.name}'\
944 .format(c=self.container)
946 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
948 raise RuntimeError('Failed to get info about container {c.name}.'
949 .format(c=self.container))
950 return True if stdout else False
953 class Container(object):
954 """Container class."""
957 """Initialize Container object."""
960 def __getattr__(self, attr):
961 """Get attribute custom implementation.
963 :param attr: Attribute to get.
965 :returns: Attribute value or None.
969 return self.__dict__[attr]
973 def __setattr__(self, attr, value):
974 """Set attribute custom implementation.
976 :param attr: Attribute to set.
977 :param value: Value to set.
982 # Check if attribute exists
985 # Creating new attribute
987 self.__dict__['ssh'] = SSH()
988 self.__dict__['ssh'].connect(value)
989 self.__dict__[attr] = value
991 # Updating attribute base of type
992 if isinstance(self.__dict__[attr], list):
993 self.__dict__[attr].append(value)
995 self.__dict__[attr] = value