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 start_vpp_in_all_containers(self):
139 """Start VPP in 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.install_supervisor()
145 self.engine.start_vpp()
147 def restart_vpp_in_all_containers(self):
148 """Restart VPP in all containers."""
149 for container in self.containers:
150 self.engine.container = self.containers[container]
151 self.engine.restart_vpp()
153 def configure_vpp_in_all_containers(self, chain_topology, **kwargs):
154 """Configure VPP in all containers.
156 :param chain_topology: Topology used for chaining containers can be
157 chain or cross_horiz. Chain topology is using 1 memif pair per
158 container. Cross_horiz topology is using 1 memif and 1 physical
159 interface in container (only single container can be configured).
160 :param kwargs: Named parameters.
161 :type chain_topology: str
164 # Count number of DUTs based on node's host information
165 dut_cnt = len(Counter([self.containers[container].node['host']
166 for container in self.containers]))
167 mod = len(self.containers)/dut_cnt
169 for i, container in enumerate(self.containers):
172 sid1 = i % mod * 2 + 1
173 sid2 = i % mod * 2 + 2
174 self.engine.container = self.containers[container]
175 guest_dir = self.engine.container.mnt[0].split(':')[1]
177 if chain_topology == 'chain':
178 self._configure_vpp_chain_l2xc(mid1=mid1, mid2=mid2,
179 sid1=sid1, sid2=sid2,
182 elif chain_topology == 'cross_horiz':
183 self._configure_vpp_cross_horiz(mid1=mid1, mid2=mid2,
184 sid1=sid1, sid2=sid2,
187 elif chain_topology == 'chain_functional':
188 self._configure_vpp_chain_functional(mid1=mid1, mid2=mid2,
189 sid1=sid1, sid2=sid2,
192 elif chain_topology == 'chain_ip4':
193 self._configure_vpp_chain_ip4(mid1=mid1, mid2=mid2,
194 sid1=sid1, sid2=sid2,
197 elif chain_topology == 'pipeline_ip4':
198 self._configure_vpp_pipeline_ip4(mid1=mid1, mid2=mid2,
199 sid1=sid1, sid2=sid2,
203 raise RuntimeError('Container topology {name} not implemented'.
204 format(name=chain_topology))
206 def _configure_vpp_chain_l2xc(self, **kwargs):
207 """Configure VPP in chain topology with l2xc.
209 :param kwargs: Named parameters.
212 self.engine.create_vpp_startup_config()
213 self.engine.create_vpp_exec_config(
214 'memif_create_chain_l2xc.vat',
215 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
216 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
217 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
218 format(c=self.engine.container, **kwargs),
219 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
220 format(c=self.engine.container, **kwargs))
222 def _configure_vpp_cross_horiz(self, **kwargs):
223 """Configure VPP in cross horizontal topology (single memif).
225 :param kwargs: Named parameters.
228 if 'DUT1' in self.engine.container.name:
229 if_pci = Topology.get_interface_pci_addr(
230 self.engine.container.node, kwargs['dut1_if'])
231 if_name = Topology.get_interface_name(
232 self.engine.container.node, kwargs['dut1_if'])
233 if 'DUT2' in self.engine.container.name:
234 if_pci = Topology.get_interface_pci_addr(
235 self.engine.container.node, kwargs['dut2_if'])
236 if_name = Topology.get_interface_name(
237 self.engine.container.node, kwargs['dut2_if'])
238 self.engine.create_vpp_startup_config_dpdk_dev(if_pci)
239 self.engine.create_vpp_exec_config(
240 'memif_create_cross_horizon.vat',
241 mid1=kwargs['mid1'], sid1=kwargs['sid1'], if_name=if_name,
242 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
243 format(c=self.engine.container, **kwargs))
245 def _configure_vpp_chain_functional(self, **kwargs):
246 """Configure VPP in chain topology with l2xc (functional).
248 :param kwargs: Named parameters.
251 self.engine.create_vpp_startup_config_func_dev()
252 self.engine.create_vpp_exec_config(
253 'memif_create_chain_functional.vat',
254 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
255 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
256 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
257 format(c=self.engine.container, **kwargs),
258 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
259 format(c=self.engine.container, **kwargs),
262 def _configure_vpp_chain_ip4(self, **kwargs):
263 """Configure VPP in chain topology with ip4.
265 :param kwargs: Named parameters.
268 self.engine.create_vpp_startup_config()
270 vif1_mac = kwargs['tg_if1_mac'] \
271 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
272 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
273 vif2_mac = kwargs['tg_if2_mac'] \
274 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
275 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
276 self.engine.create_vpp_exec_config(
277 'memif_create_chain_ip4.vat',
278 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
279 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
280 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
281 format(c=self.engine.container, **kwargs),
282 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
283 format(c=self.engine.container, **kwargs),
284 mac1='52:54:00:00:{0:02X}:01'.format(kwargs['mid1']),
285 mac2='52:54:00:00:{0:02X}:02'.format(kwargs['mid2']),
286 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
288 def _configure_vpp_pipeline_ip4(self, **kwargs):
289 """Configure VPP in pipeline topology with ip4.
291 :param kwargs: Named parameters.
294 self.engine.create_vpp_startup_config()
295 node = (kwargs['mid1'] - 1) % kwargs['nodes'] + 1
296 mid1 = kwargs['mid1']
297 mid2 = kwargs['mid2']
300 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
302 kwargs['mid2'] = kwargs['mid2'] \
303 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
304 else kwargs['mid2'] + 1
305 vif1_mac = kwargs['tg_if1_mac'] \
306 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
307 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
308 vif2_mac = kwargs['tg_if2_mac'] \
309 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
310 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
311 socket1 = '{guest_dir}/memif-{c.name}-{sid1}'.\
312 format(c=self.engine.container, **kwargs) \
313 if node == 1 else '{guest_dir}/memif-pipe-{mid1}'.\
314 format(c=self.engine.container, **kwargs)
315 socket2 = '{guest_dir}/memif-{c.name}-{sid2}'.\
316 format(c=self.engine.container, **kwargs) \
317 if node == 1 and kwargs['nodes'] == 1 or node == kwargs['nodes'] \
318 else '{guest_dir}/memif-pipe-{mid2}'.\
319 format(c=self.engine.container, **kwargs)
321 self.engine.create_vpp_exec_config(
322 'memif_create_pipeline_ip4.vat',
323 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
324 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
325 socket1=socket1, socket2=socket2, role1=role1, role2=role2,
326 mac1='52:54:00:00:{0:02X}:01'.format(mid1),
327 mac2='52:54:00:00:{0:02X}:02'.format(mid2),
328 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
330 def stop_all_containers(self):
331 """Stop all containers."""
332 for container in self.containers:
333 self.engine.container = self.containers[container]
336 def destroy_all_containers(self):
337 """Destroy all containers."""
338 for container in self.containers:
339 self.engine.container = self.containers[container]
340 self.engine.destroy()
343 class ContainerEngine(object):
344 """Abstract class for container engine."""
347 """Init ContainerEngine object."""
348 self.container = None
350 def initialize(self):
351 """Initialize container object."""
352 self.container = Container()
354 def acquire(self, force):
355 """Acquire/download container.
357 :param force: Destroy a container if exists and create.
360 raise NotImplementedError
363 """Build container (compile)."""
364 raise NotImplementedError
367 """Create/deploy container."""
368 raise NotImplementedError
370 def execute(self, command):
371 """Execute process inside container.
373 :param command: Command to run inside container.
376 raise NotImplementedError
379 """Stop container."""
380 raise NotImplementedError
383 """Destroy/remove container."""
384 raise NotImplementedError
387 """Info about container."""
388 raise NotImplementedError
390 def system_info(self):
392 raise NotImplementedError
394 def install_supervisor(self):
395 """Install supervisord inside a container."""
396 if isinstance(self, LXC):
397 self.execute('sleep 3; apt-get update')
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))
419 """Start VPP inside a container."""
420 self.execute('echo "{config}" >> {config_file}'.
422 config='[program:vpp]\n'
423 'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
425 'autorestart=false\n'
426 'redirect_stderr=true\n'
428 config_file=SUPERVISOR_CONF))
429 self.execute('supervisorctl reload')
430 self.execute('supervisorctl start vpp')
432 def restart_vpp(self):
433 """Restart VPP service inside a container."""
434 self.execute('supervisorctl restart vpp')
435 self.execute('cat /tmp/supervisord.log')
437 def create_base_vpp_startup_config(self):
438 """Create base startup configuration of VPP on container.
440 :returns: Base VPP startup configuration.
441 :rtype: VppConfigGenerator
443 cpuset_cpus = self.container.cpuset_cpus
445 # Create config instance
446 vpp_config = VppConfigGenerator()
447 vpp_config.set_node(self.container.node)
448 vpp_config.add_unix_cli_listen()
449 vpp_config.add_unix_nodaemon()
450 vpp_config.add_unix_exec('/tmp/running.exec')
451 # We will pop the first core from the list to be a main core
452 vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
453 # If more cores in the list, the rest will be used as workers.
455 corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
456 vpp_config.add_cpu_corelist_workers(corelist_workers)
460 def create_vpp_startup_config(self):
461 """Create startup configuration of VPP without DPDK on container.
463 vpp_config = self.create_base_vpp_startup_config()
464 vpp_config.add_plugin('disable', 'dpdk_plugin.so')
466 # Apply configuration
467 self.execute('mkdir -p /etc/vpp/')
468 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
469 .format(config=vpp_config.get_config_str()))
471 def create_vpp_startup_config_dpdk_dev(self, *devices):
472 """Create startup configuration of VPP with DPDK on container.
474 :param devices: List of PCI devices to add.
477 vpp_config = self.create_base_vpp_startup_config()
478 vpp_config.add_dpdk_dev(*devices)
479 vpp_config.add_dpdk_no_tx_checksum_offload()
480 vpp_config.add_dpdk_log_level('debug')
481 vpp_config.add_plugin('disable', 'default')
482 vpp_config.add_plugin('enable', 'dpdk_plugin.so')
483 vpp_config.add_plugin('enable', 'memif_plugin.so')
485 # Apply configuration
486 self.execute('mkdir -p /etc/vpp/')
487 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
488 .format(config=vpp_config.get_config_str()))
490 def create_vpp_startup_config_func_dev(self):
491 """Create startup configuration of VPP on container for functional
494 # Create config instance
495 vpp_config = VppConfigGenerator()
496 vpp_config.set_node(self.container.node)
497 vpp_config.add_unix_cli_listen()
498 vpp_config.add_unix_nodaemon()
499 vpp_config.add_unix_exec('/tmp/running.exec')
500 vpp_config.add_plugin('disable', 'dpdk_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_exec_config(self, vat_template_file, **kwargs):
508 """Create VPP exec configuration on container.
510 :param vat_template_file: File name of a VAT template script.
511 :param kwargs: Parameters for VAT script.
512 :type vat_template_file: str
515 vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
518 with open(vat_file_path, 'r') as template_file:
519 cmd_template = template_file.readlines()
520 for line_tmpl in cmd_template:
521 vat_cmd = line_tmpl.format(**kwargs)
522 self.execute('echo "{c}" >> /tmp/running.exec'
523 .format(c=vat_cmd.replace('\n', '')))
525 def is_container_running(self):
526 """Check if container is running."""
527 raise NotImplementedError
529 def is_container_present(self):
530 """Check if container is present."""
531 raise NotImplementedError
533 def _configure_cgroup(self, name):
534 """Configure the control group associated with a container.
536 By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
537 container is initialized a new cgroup /docker or /lxc is created under
538 cpuset parent tree. This newly created cgroup is inheriting parent
539 setting for cpu/mem exclusive parameter and thus cannot be overriden
540 within /docker or /lxc cgroup. This function is supposed to set cgroups
541 to allow coexistence of both engines.
543 :param name: Name of cgroup.
545 :raises RuntimeError: If applying cgroup settings via cgset failed.
547 ret, _, _ = self.container.ssh.exec_command_sudo(
548 'cgset -r cpuset.cpu_exclusive=0 /')
550 raise RuntimeError('Failed to apply cgroup settings.')
552 ret, _, _ = self.container.ssh.exec_command_sudo(
553 'cgset -r cpuset.mem_exclusive=0 /')
555 raise RuntimeError('Failed to apply cgroup settings.')
557 ret, _, _ = self.container.ssh.exec_command_sudo(
558 'cgcreate -g cpuset:/{name}'.format(name=name))
560 raise RuntimeError('Failed to copy cgroup settings from root.')
562 ret, _, _ = self.container.ssh.exec_command_sudo(
563 'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
565 raise RuntimeError('Failed to apply cgroup settings.')
567 ret, _, _ = self.container.ssh.exec_command_sudo(
568 'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
570 raise RuntimeError('Failed to apply cgroup settings.')
573 class LXC(ContainerEngine):
574 """LXC implementation."""
576 # Implicit constructor is inherited.
578 def acquire(self, force=True):
579 """Acquire a privileged system object where configuration is stored.
581 :param force: If a container exists, destroy it and create a new
584 :raises RuntimeError: If creating the container or writing the container
587 if self.is_container_present():
593 image = self.container.image if self.container.image else\
594 "-d ubuntu -r bionic -a amd64"
596 cmd = 'lxc-create -t download --name {c.name} -- {image} '\
597 '--no-validate'.format(c=self.container, image=image)
599 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
601 raise RuntimeError('Failed to create container.')
603 self._configure_cgroup('lxc')
606 """Create/deploy an application inside a container on system.
608 :raises RuntimeError: If creating the container fails.
610 if self.container.mnt:
611 for mount in self.container.mnt:
612 host_dir, guest_dir = mount.split(':')
613 options = 'bind,create=dir' \
614 if guest_dir.endswith('/') else 'bind,create=file'
615 entry = 'lxc.mount.entry = {host_dir} '\
616 '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
617 '{options} 0 0'.format(c=self.container,
621 ret, _, _ = self.container.ssh.exec_command_sudo(
622 "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
623 format(e=entry, c=self.container))
625 raise RuntimeError('Failed to write {c.name} config.'
626 .format(c=self.container))
628 cpuset_cpus = '{0}'.format(
629 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
630 if self.container.cpuset_cpus else ''
632 ret, _, _ = self.container.ssh.exec_command_sudo(
633 'lxc-start --name {c.name} --daemon'.
634 format(c=self.container))
636 raise RuntimeError('Failed to start container {c.name}.'.
637 format(c=self.container))
638 self._lxc_wait('RUNNING')
640 # Workaround for LXC to be able to allocate all cpus including isolated.
641 ret, _, _ = self.container.ssh.exec_command_sudo(
642 'cgset --copy-from / lxc/')
644 raise RuntimeError('Failed to copy cgroup to LXC')
646 ret, _, _ = self.container.ssh.exec_command_sudo(
647 'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
648 format(c=self.container, cpus=cpuset_cpus))
650 raise RuntimeError('Failed to set cpuset.cpus to container '
651 '{c.name}.'.format(c=self.container))
653 def execute(self, command):
654 """Start a process inside a running container.
656 Runs the specified command inside the container specified by name. The
657 container has to be running already.
659 :param command: Command to run inside container.
661 :raises RuntimeError: If running the command failed.
663 env = '--keep-env {0}'.format(
664 ' '.join('--set-var %s' % env for env in self.container.env))\
665 if self.container.env else ''
667 cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}; "\
668 "exit $?'".format(env=env, c=self.container, command=command)
670 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
672 raise RuntimeError('Failed to run command inside container '
673 '{c.name}.'.format(c=self.container))
678 :raises RuntimeError: If stopping the container failed.
680 cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
682 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
684 raise RuntimeError('Failed to stop container {c.name}.'
685 .format(c=self.container))
686 self._lxc_wait('STOPPED|FROZEN')
689 """Destroy a container.
691 :raises RuntimeError: If destroying container failed.
693 cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
695 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
697 raise RuntimeError('Failed to destroy container {c.name}.'
698 .format(c=self.container))
701 """Query and shows information about a container.
703 :raises RuntimeError: If getting info about a container failed.
705 cmd = 'lxc-info --name {c.name}'.format(c=self.container)
707 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
709 raise RuntimeError('Failed to get info about container {c.name}.'
710 .format(c=self.container))
712 def system_info(self):
713 """Check the current kernel for LXC support.
715 :raises RuntimeError: If checking LXC support failed.
717 cmd = 'lxc-checkconfig'
719 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
721 raise RuntimeError('Failed to check LXC support.')
723 def is_container_running(self):
724 """Check if container is running on node.
726 :returns: True if container is running.
728 :raises RuntimeError: If getting info about a container failed.
730 cmd = 'lxc-info --no-humanize --state --name {c.name}'\
731 .format(c=self.container)
733 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
735 raise RuntimeError('Failed to get info about container {c.name}.'
736 .format(c=self.container))
737 return True if 'RUNNING' in stdout else False
739 def is_container_present(self):
740 """Check if container is existing on node.
742 :returns: True if container is present.
744 :raises RuntimeError: If getting info about a container failed.
746 cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
748 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
749 return False if int(ret) else True
751 def _lxc_wait(self, state):
752 """Wait for a specific container state.
754 :param state: Specify the container state(s) to wait for.
756 :raises RuntimeError: If waiting for state of a container failed.
758 cmd = 'lxc-wait --name {c.name} --state "{s}"'\
759 .format(c=self.container, s=state)
761 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
763 raise RuntimeError('Failed to wait for state "{s}" of container '
764 '{c.name}.'.format(s=state, c=self.container))
767 class Docker(ContainerEngine):
768 """Docker implementation."""
770 # Implicit constructor is inherited.
772 def acquire(self, force=True):
773 """Pull an image or a repository from a registry.
775 :param force: Destroy a container if exists.
777 :raises RuntimeError: If pulling a container failed.
779 if self.is_container_present():
785 if not self.container.image:
786 setattr(self.container, 'image', 'snergster/csit-sut:latest')
788 cmd = 'docker pull {image}'.format(image=self.container.image)
790 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
792 raise RuntimeError('Failed to create container {c.name}.'
793 .format(c=self.container))
794 if self.container.cpuset_cpus:
795 self._configure_cgroup('docker')
798 """Create/deploy container.
800 :raises RuntimeError: If creating a container failed.
802 cpuset_cpus = '--cpuset-cpus={0}'.format(
803 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
804 if self.container.cpuset_cpus else ''
806 cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
807 if self.container.cpuset_mems is not None else ''
808 # Temporary workaround - disabling due to bug in memif
812 ' '.join('--env %s' % env for env in self.container.env))\
813 if self.container.env else ''
815 command = '{0}'.format(self.container.command)\
816 if self.container.command else ''
818 publish = '{0}'.format(
819 ' '.join('--publish %s' % var for var in self.container.publish))\
820 if self.container.publish else ''
822 volume = '{0}'.format(
823 ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
824 if self.container.mnt else ''
827 '--privileged --detach --interactive --tty --rm '\
828 '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
829 '{env} {volume} --name {container.name} {container.image} '\
830 '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
831 container=self.container, command=command,
832 env=env, publish=publish, volume=volume)
834 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
836 raise RuntimeError('Failed to create container {c.name}'
837 .format(c=self.container))
841 def execute(self, command):
842 """Start a process inside a running container.
844 Runs the specified command inside the container specified by name. The
845 container has to be running already.
847 :param command: Command to run inside container.
849 :raises RuntimeError: If runnig the command in a container failed.
851 cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}; "\
852 "exit $?'".format(c=self.container, command=command)
854 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
856 raise RuntimeError('Failed to execute command in container '
857 '{c.name}.'.format(c=self.container))
860 """Stop running container.
862 :raises RuntimeError: If stopping a container failed.
864 cmd = 'docker stop {c.name}'.format(c=self.container)
866 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
868 raise RuntimeError('Failed to stop container {c.name}.'
869 .format(c=self.container))
872 """Remove a container.
874 :raises RuntimeError: If removing a container failed.
876 cmd = 'docker rm --force {c.name}'.format(c=self.container)
878 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
880 raise RuntimeError('Failed to destroy container {c.name}.'
881 .format(c=self.container))
884 """Return low-level information on Docker objects.
886 :raises RuntimeError: If getting info about a container failed.
888 cmd = 'docker inspect {c.name}'.format(c=self.container)
890 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
892 raise RuntimeError('Failed to get info about container {c.name}.'
893 .format(c=self.container))
895 def system_info(self):
896 """Display the docker system-wide information.
898 :raises RuntimeError: If displaying system information failed.
900 cmd = 'docker system info'
902 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
904 raise RuntimeError('Failed to get system info.')
906 def is_container_present(self):
907 """Check if container is present on node.
909 :returns: True if container is present.
911 :raises RuntimeError: If getting info about a container failed.
913 cmd = 'docker ps --all --quiet --filter name={c.name}'\
914 .format(c=self.container)
916 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
918 raise RuntimeError('Failed to get info about container {c.name}.'
919 .format(c=self.container))
920 return True if stdout else False
922 def is_container_running(self):
923 """Check if container is running on node.
925 :returns: True if container is running.
927 :raises RuntimeError: If getting info about a container failed.
929 cmd = 'docker ps --quiet --filter name={c.name}'\
930 .format(c=self.container)
932 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
934 raise RuntimeError('Failed to get info about container {c.name}.'
935 .format(c=self.container))
936 return True if stdout else False
939 class Container(object):
940 """Container class."""
943 """Initialize Container object."""
946 def __getattr__(self, attr):
947 """Get attribute custom implementation.
949 :param attr: Attribute to get.
951 :returns: Attribute value or None.
955 return self.__dict__[attr]
959 def __setattr__(self, attr, value):
960 """Set attribute custom implementation.
962 :param attr: Attribute to set.
963 :param value: Value to set.
968 # Check if attribute exists
971 # Creating new attribute
973 self.__dict__['ssh'] = SSH()
974 self.__dict__['ssh'].connect(value)
975 self.__dict__[attr] = value
977 # Updating attribute base of type
978 if isinstance(self.__dict__[attr], list):
979 self.__dict__[attr].append(value)
981 self.__dict__[attr] = value