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 string import Template
20 from collections import OrderedDict, Counter
22 from resources.libraries.python.ssh import SSH
23 from resources.libraries.python.Constants import Constants
24 from resources.libraries.python.topology import Topology
25 from resources.libraries.python.VppConfigGenerator import VppConfigGenerator
28 __all__ = ["ContainerManager", "ContainerEngine", "LXC", "Docker", "Container"]
30 SUPERVISOR_CONF = '/etc/supervisord.conf'
33 class ContainerManager(object):
34 """Container lifecycle management class."""
36 def __init__(self, engine):
37 """Initialize Container Manager class.
39 :param engine: Container technology used (LXC/Docker/...).
41 :raises NotImplementedError: If container technology is not implemented.
44 self.engine = globals()[engine]()
46 raise NotImplementedError('{engine} is not implemented.'.
47 format(engine=engine))
48 self.containers = OrderedDict()
50 def get_container_by_name(self, name):
51 """Get container instance.
53 :param name: Container name.
55 :returns: Container instance.
57 :raises RuntimeError: If failed to get container with name.
60 return self.containers[name]
62 raise RuntimeError('Failed to get container with name: {name}'.
65 def construct_container(self, **kwargs):
66 """Construct container object on node with specified parameters.
68 :param kwargs: Key-value pairs used to construct container.
72 self.engine.initialize()
75 setattr(self.engine.container, key, kwargs[key])
77 # Set additional environmental variables
78 setattr(self.engine.container, 'env',
79 'MICROSERVICE_LABEL={label}'.format(label=kwargs['name']))
81 # Store container instance
82 self.containers[kwargs['name']] = self.engine.container
84 def construct_containers(self, **kwargs):
85 """Construct 1..N container(s) on node with specified name.
87 Ordinal number is automatically added to the name of container as
90 :param kwargs: Named parameters.
94 for i in range(kwargs['count']):
95 # Name will contain ordinal suffix
96 kwargs['name'] = ''.join([name, str(i+1)])
98 self.construct_container(i=i, **kwargs)
100 def acquire_all_containers(self):
101 """Acquire all containers."""
102 for container in self.containers:
103 self.engine.container = self.containers[container]
104 self.engine.acquire()
106 def build_all_containers(self):
107 """Build all containers."""
108 for container in self.containers:
109 self.engine.container = self.containers[container]
112 def create_all_containers(self):
113 """Create all containers."""
114 for container in self.containers:
115 self.engine.container = self.containers[container]
118 def execute_on_container(self, name, command):
119 """Execute command on container with name.
121 :param name: Container name.
122 :param command: Command to execute.
126 self.engine.container = self.get_container_by_name(name)
127 self.engine.execute(command)
129 def execute_on_all_containers(self, command):
130 """Execute command on all containers.
132 :param command: Command to execute.
135 for container in self.containers:
136 self.engine.container = self.containers[container]
137 self.engine.execute(command)
139 def start_vpp_in_all_containers(self):
140 """Start VPP in all containers."""
141 for container in self.containers:
142 self.engine.container = self.containers[container]
143 self.engine.start_vpp()
145 def restart_vpp_in_all_containers(self):
146 """Restart VPP in all containers."""
147 for container in self.containers:
148 self.engine.container = self.containers[container]
149 self.engine.restart_vpp()
151 def configure_vpp_in_all_containers(self, chain_topology, **kwargs):
152 """Configure VPP in all containers.
154 :param chain_topology: Topology used for chaining containers can be
155 chain or cross_horiz. Chain topology is using 1 memif pair per
156 container. Cross_horiz topology is using 1 memif and 1 physical
157 interface in container (only single container can be configured).
158 :param kwargs: Named parameters.
159 :type chain_topology: str
162 # Count number of DUTs based on node's host information
163 dut_cnt = len(Counter([self.containers[container].node['host']
164 for container in self.containers]))
165 mod = len(self.containers)/dut_cnt
167 for i, container in enumerate(self.containers):
170 sid1 = i % mod * 2 + 1
171 sid2 = i % mod * 2 + 2
172 self.engine.container = self.containers[container]
173 guest_dir = self.engine.container.mnt[0].split(':')[1]
175 if chain_topology == 'chain':
176 self._configure_vpp_chain_l2xc(mid1=mid1, mid2=mid2,
177 sid1=sid1, sid2=sid2,
180 elif chain_topology == 'cross_horiz':
181 self._configure_vpp_cross_horiz(mid1=mid1, mid2=mid2,
182 sid1=sid1, sid2=sid2,
185 elif chain_topology == 'chain_functional':
186 self._configure_vpp_chain_functional(mid1=mid1, mid2=mid2,
187 sid1=sid1, sid2=sid2,
190 elif chain_topology == 'chain_ip4':
191 self._configure_vpp_chain_ip4(mid1=mid1, mid2=mid2,
192 sid1=sid1, sid2=sid2,
195 elif chain_topology == 'pipeline_ip4':
196 self._configure_vpp_pipeline_ip4(mid1=mid1, mid2=mid2,
197 sid1=sid1, sid2=sid2,
201 raise RuntimeError('Container topology {name} not implemented'.
202 format(name=chain_topology))
204 def _configure_vpp_chain_l2xc(self, **kwargs):
205 """Configure VPP in chain topology with l2xc.
207 :param kwargs: Named parameters.
210 self.engine.create_vpp_startup_config()
211 self.engine.create_vpp_exec_config(
212 'memif_create_chain_l2xc.exec',
213 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
214 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
215 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
216 format(c=self.engine.container, **kwargs),
217 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
218 format(c=self.engine.container, **kwargs))
220 def _configure_vpp_cross_horiz(self, **kwargs):
221 """Configure VPP in cross horizontal topology (single memif).
223 :param kwargs: Named parameters.
226 if 'DUT1' in self.engine.container.name:
227 if_pci = Topology.get_interface_pci_addr(
228 self.engine.container.node, kwargs['dut1_if'])
229 if_name = Topology.get_interface_name(
230 self.engine.container.node, kwargs['dut1_if'])
231 if 'DUT2' in self.engine.container.name:
232 if_pci = Topology.get_interface_pci_addr(
233 self.engine.container.node, kwargs['dut2_if'])
234 if_name = Topology.get_interface_name(
235 self.engine.container.node, kwargs['dut2_if'])
236 self.engine.create_vpp_startup_config_dpdk_dev(if_pci)
237 self.engine.create_vpp_exec_config(
238 'memif_create_cross_horizon.exec',
239 mid1=kwargs['mid1'], sid1=kwargs['sid1'], if_name=if_name,
240 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
241 format(c=self.engine.container, **kwargs))
243 def _configure_vpp_chain_functional(self, **kwargs):
244 """Configure VPP in chain topology with l2xc (functional).
246 :param kwargs: Named parameters.
249 self.engine.create_vpp_startup_config()
250 self.engine.create_vpp_exec_config(
251 'memif_create_chain_functional.exec',
252 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
253 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
254 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
255 format(c=self.engine.container, **kwargs),
256 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
257 format(c=self.engine.container, **kwargs),
260 def _configure_vpp_chain_ip4(self, **kwargs):
261 """Configure VPP in chain topology with ip4.
263 :param kwargs: Named parameters.
266 self.engine.create_vpp_startup_config()
268 vif1_mac = kwargs['tg_if1_mac'] \
269 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
270 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
271 vif2_mac = kwargs['tg_if2_mac'] \
272 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
273 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
274 self.engine.create_vpp_exec_config(
275 'memif_create_chain_ip4.exec',
276 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
277 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
278 socket1='{guest_dir}/memif-{c.name}-{sid1}'.
279 format(c=self.engine.container, **kwargs),
280 socket2='{guest_dir}/memif-{c.name}-{sid2}'.
281 format(c=self.engine.container, **kwargs),
282 mac1='52:54:00:00:{0:02X}:01'.format(kwargs['mid1']),
283 mac2='52:54:00:00:{0:02X}:02'.format(kwargs['mid2']),
284 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
286 def _configure_vpp_pipeline_ip4(self, **kwargs):
287 """Configure VPP in pipeline topology with ip4.
289 :param kwargs: Named parameters.
292 self.engine.create_vpp_startup_config()
293 node = (kwargs['mid1'] - 1) % kwargs['nodes'] + 1
294 mid1 = kwargs['mid1']
295 mid2 = kwargs['mid2']
298 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
300 kwargs['mid2'] = kwargs['mid2'] \
301 if node == kwargs['nodes'] or node == kwargs['nodes'] and node == 1\
302 else kwargs['mid2'] + 1
303 vif1_mac = kwargs['tg_if1_mac'] \
304 if (kwargs['mid1'] - 1) % kwargs['nodes'] + 1 == 1 \
305 else '52:54:00:00:{0:02X}:02'.format(kwargs['mid1'] - 1)
306 vif2_mac = kwargs['tg_if2_mac'] \
307 if (kwargs['mid2'] - 1) % kwargs['nodes'] + 1 == kwargs['nodes'] \
308 else '52:54:00:00:{0:02X}:01'.format(kwargs['mid2'] + 1)
309 socket1 = '{guest_dir}/memif-{c.name}-{sid1}'.\
310 format(c=self.engine.container, **kwargs) \
311 if node == 1 else '{guest_dir}/memif-pipe-{mid1}'.\
312 format(c=self.engine.container, **kwargs)
313 socket2 = '{guest_dir}/memif-{c.name}-{sid2}'.\
314 format(c=self.engine.container, **kwargs) \
315 if node == 1 and kwargs['nodes'] == 1 or node == kwargs['nodes'] \
316 else '{guest_dir}/memif-pipe-{mid2}'.\
317 format(c=self.engine.container, **kwargs)
319 self.engine.create_vpp_exec_config(
320 'memif_create_pipeline_ip4.exec',
321 mid1=kwargs['mid1'], mid2=kwargs['mid2'],
322 sid1=kwargs['sid1'], sid2=kwargs['sid2'],
323 socket1=socket1, socket2=socket2, role1=role1, role2=role2,
324 mac1='52:54:00:00:{0:02X}:01'.format(mid1),
325 mac2='52:54:00:00:{0:02X}:02'.format(mid2),
326 vif1_mac=vif1_mac, vif2_mac=vif2_mac)
328 def stop_all_containers(self):
329 """Stop all containers."""
330 for container in self.containers:
331 self.engine.container = self.containers[container]
334 def destroy_all_containers(self):
335 """Destroy all containers."""
336 for container in self.containers:
337 self.engine.container = self.containers[container]
338 self.engine.destroy()
341 class ContainerEngine(object):
342 """Abstract class for container engine."""
345 """Init ContainerEngine object."""
346 self.container = None
348 def initialize(self):
349 """Initialize container object."""
350 self.container = Container()
352 def acquire(self, force):
353 """Acquire/download container.
355 :param force: Destroy a container if exists and create.
358 raise NotImplementedError
361 """Build container (compile)."""
362 raise NotImplementedError
365 """Create/deploy container."""
366 raise NotImplementedError
368 def execute(self, command):
369 """Execute process inside container.
371 :param command: Command to run inside container.
374 raise NotImplementedError
377 """Stop container."""
378 raise NotImplementedError
381 """Destroy/remove container."""
382 raise NotImplementedError
385 """Info about container."""
386 raise NotImplementedError
388 def system_info(self):
390 raise NotImplementedError
393 """Start VPP inside a container."""
395 u"setsid /usr/bin/vpp -c /etc/vpp/startup.conf "
396 u">/tmp/vppd.log 2>&1 < /dev/null &")
398 def restart_vpp(self):
399 """Restart VPP service inside a container."""
400 self.execute(u"pkill vpp")
402 self.execute(u"cat /tmp/vppd.log")
404 def create_base_vpp_startup_config(self, cpuset_cpus=None):
405 """Create base startup configuration of VPP on container.
407 :param cpuset_cpus: List of CPU cores to allocate.
409 :type cpuset_cpus: list.
410 :returns: Base VPP startup configuration.
411 :rtype: VppConfigGenerator
413 if cpuset_cpus is None:
414 cpuset_cpus = self.container.cpuset_cpus
415 # Create config instance
416 vpp_config = VppConfigGenerator()
417 vpp_config.set_node(self.container.node)
418 vpp_config.add_unix_cli_listen()
419 vpp_config.add_unix_nodaemon()
420 vpp_config.add_unix_exec(u"/tmp/running.exec")
421 vpp_config.add_statseg_per_node_counters(value=u"on")
423 # We will pop the first core from the list to be a main core
424 vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
425 # If more cores in the list, the rest will be used as workers.
426 corelist_workers = u",".join(str(cpu) for cpu in cpuset_cpus)
427 vpp_config.add_cpu_corelist_workers(corelist_workers)
428 vpp_config.add_buffers_per_numa(215040)
429 vpp_config.add_plugin(u"disable", u"default")
430 vpp_config.add_plugin(u"enable", u"memif_plugin.so")
431 vpp_config.add_heapsize(u"4G")
432 vpp_config.add_ip_heap_size(u"4G")
433 vpp_config.add_statseg_size(u"4G")
437 def create_vpp_startup_config(self):
438 """Create startup configuration of VPP without DPDK on container.
440 vpp_config = self.create_base_vpp_startup_config()
442 # Apply configuration
443 self.execute('mkdir -p /etc/vpp/')
444 self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
445 .format(config=vpp_config.get_config_str()))
447 def create_vpp_exec_config(self, template_file, **kwargs):
448 """Create VPP exec configuration on container.
450 :param template_file: File name of a template script.
451 :param kwargs: Parameters for script.
452 :type template_file: str
455 running = '/tmp/running.exec'
456 template = '{res}/{tpl}'.format(
457 res=Constants.RESOURCES_TPL_CONTAINER, tpl=template_file)
459 with open(template, 'r') as src_file:
460 src = Template(src_file.read())
461 self.execute('echo "{out}" > {running}'.format(
462 out=src.safe_substitute(**kwargs), running=running))
464 def is_container_running(self):
465 """Check if container is running."""
466 raise NotImplementedError
468 def is_container_present(self):
469 """Check if container is present."""
470 raise NotImplementedError
472 def _configure_cgroup(self, name):
473 """Configure the control group associated with a container.
475 By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
476 container is initialized a new cgroup /docker or /lxc is created under
477 cpuset parent tree. This newly created cgroup is inheriting parent
478 setting for cpu/mem exclusive parameter and thus cannot be overriden
479 within /docker or /lxc cgroup. This function is supposed to set cgroups
480 to allow coexistence of both engines.
482 :param name: Name of cgroup.
484 :raises RuntimeError: If applying cgroup settings via cgset failed.
486 ret, _, _ = self.container.ssh.exec_command_sudo(
487 'cgset -r cpuset.cpu_exclusive=0 /')
489 raise RuntimeError('Failed to apply cgroup settings.')
491 ret, _, _ = self.container.ssh.exec_command_sudo(
492 'cgset -r cpuset.mem_exclusive=0 /')
494 raise RuntimeError('Failed to apply cgroup settings.')
496 ret, _, _ = self.container.ssh.exec_command_sudo(
497 'cgcreate -g cpuset:/{name}'.format(name=name))
499 raise RuntimeError('Failed to copy cgroup settings from root.')
501 ret, _, _ = self.container.ssh.exec_command_sudo(
502 'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
504 raise RuntimeError('Failed to apply cgroup settings.')
506 ret, _, _ = self.container.ssh.exec_command_sudo(
507 'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
509 raise RuntimeError('Failed to apply cgroup settings.')
512 class LXC(ContainerEngine):
513 """LXC implementation."""
515 # Implicit constructor is inherited.
517 def acquire(self, force=True):
518 """Acquire a privileged system object where configuration is stored.
520 :param force: If a container exists, destroy it and create a new
523 :raises RuntimeError: If creating the container or writing the container
526 if self.is_container_present():
532 target_arch = 'arm64' \
533 if Topology.get_node_arch(self.container.node) == 'aarch64' \
536 image = self.container.image if self.container.image else\
537 "-d ubuntu -r bionic -a {arch}".format(arch=target_arch)
539 cmd = 'lxc-create -t download --name {c.name} -- {image} '\
540 '--no-validate'.format(c=self.container, image=image)
542 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
544 raise RuntimeError('Failed to create container.')
546 self._configure_cgroup('lxc')
549 """Build container (compile)."""
550 raise NotImplementedError
553 """Create/deploy an application inside a container on system.
555 :raises RuntimeError: If creating the container fails.
557 if self.container.mnt:
558 for mount in self.container.mnt:
559 host_dir, guest_dir = mount.split(':')
560 options = 'bind,create=dir' \
561 if guest_dir.endswith('/') else 'bind,create=file'
562 entry = 'lxc.mount.entry = {host_dir} '\
563 '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
564 '{options} 0 0'.format(c=self.container,
568 ret, _, _ = self.container.ssh.exec_command_sudo(
569 "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
570 format(e=entry, c=self.container))
572 raise RuntimeError('Failed to write {c.name} config.'
573 .format(c=self.container))
575 cpuset_cpus = '{0}'.format(
576 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
577 if self.container.cpuset_cpus else ''
579 ret, _, _ = self.container.ssh.exec_command_sudo(
580 'lxc-start --name {c.name} --daemon'.
581 format(c=self.container))
583 raise RuntimeError('Failed to start container {c.name}.'.
584 format(c=self.container))
585 self._lxc_wait('RUNNING')
587 # Workaround for LXC to be able to allocate all cpus including isolated.
588 ret, _, _ = self.container.ssh.exec_command_sudo(
589 'cgset --copy-from / lxc/')
591 raise RuntimeError('Failed to copy cgroup to LXC')
593 ret, _, _ = self.container.ssh.exec_command_sudo(
594 'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
595 format(c=self.container, cpus=cpuset_cpus))
597 raise RuntimeError('Failed to set cpuset.cpus to container '
598 '{c.name}.'.format(c=self.container))
600 def execute(self, command):
601 """Start a process inside a running container.
603 Runs the specified command inside the container specified by name. The
604 container has to be running already.
606 :param command: Command to run inside container.
608 :raises RuntimeError: If running the command failed.
610 env = '--keep-env {0}'.format(
611 ' '.join('--set-var %s' % env for env in self.container.env))\
612 if self.container.env else ''
614 cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}'"\
615 .format(env=env, c=self.container, command=command)
617 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
619 raise RuntimeError('Failed to run command inside container '
620 '{c.name}.'.format(c=self.container))
625 :raises RuntimeError: If stopping the container failed.
627 cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
629 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
631 raise RuntimeError('Failed to stop container {c.name}.'
632 .format(c=self.container))
633 self._lxc_wait('STOPPED|FROZEN')
636 """Destroy a container.
638 :raises RuntimeError: If destroying container failed.
640 cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
642 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
644 raise RuntimeError('Failed to destroy container {c.name}.'
645 .format(c=self.container))
648 """Query and shows information about a container.
650 :raises RuntimeError: If getting info about a container failed.
652 cmd = 'lxc-info --name {c.name}'.format(c=self.container)
654 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
656 raise RuntimeError('Failed to get info about container {c.name}.'
657 .format(c=self.container))
659 def system_info(self):
660 """Check the current kernel for LXC support.
662 :raises RuntimeError: If checking LXC support failed.
664 cmd = 'lxc-checkconfig'
666 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
668 raise RuntimeError('Failed to check LXC support.')
670 def is_container_running(self):
671 """Check if container is running on node.
673 :returns: True if container is running.
675 :raises RuntimeError: If getting info about a container failed.
677 cmd = 'lxc-info --no-humanize --state --name {c.name}'\
678 .format(c=self.container)
680 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
682 raise RuntimeError('Failed to get info about container {c.name}.'
683 .format(c=self.container))
684 return True if 'RUNNING' in stdout else False
686 def is_container_present(self):
687 """Check if container is existing on node.
689 :returns: True if container is present.
691 :raises RuntimeError: If getting info about a container failed.
693 cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
695 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
696 return False if int(ret) else True
698 def _lxc_wait(self, state):
699 """Wait for a specific container state.
701 :param state: Specify the container state(s) to wait for.
703 :raises RuntimeError: If waiting for state of a container failed.
705 cmd = 'lxc-wait --name {c.name} --state "{s}"'\
706 .format(c=self.container, s=state)
708 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
710 raise RuntimeError('Failed to wait for state "{s}" of container '
711 '{c.name}.'.format(s=state, c=self.container))
714 class Docker(ContainerEngine):
715 """Docker implementation."""
717 # Implicit constructor is inherited.
719 def acquire(self, force=True):
720 """Pull an image or a repository from a registry.
722 :param force: Destroy a container if exists.
724 :raises RuntimeError: If pulling a container failed.
726 if self.is_container_present():
732 if not self.container.image:
733 img = Constants.DOCKER_SUT_IMAGE_UBUNTU_ARM \
734 if Topology.get_node_arch(self.container.node) == 'aarch64' \
735 else Constants.DOCKER_SUT_IMAGE_UBUNTU
736 setattr(self.container, 'image', img)
738 cmd = 'docker pull {image}'.format(image=self.container.image)
740 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
742 raise RuntimeError('Failed to create container {c.name}.'
743 .format(c=self.container))
745 if self.container.cpuset_cpus:
746 self._configure_cgroup('docker')
749 """Create/deploy container.
751 :raises RuntimeError: If creating a container failed.
753 cpuset_cpus = '--cpuset-cpus={0}'.format(
754 ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
755 if self.container.cpuset_cpus else ''
757 cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
758 if self.container.cpuset_mems is not None else ''
759 # Temporary workaround - disabling due to bug in memif
763 ' '.join('--env %s' % env for env in self.container.env))\
764 if self.container.env else ''
766 command = '{0}'.format(self.container.command)\
767 if self.container.command else ''
769 publish = '{0}'.format(
770 ' '.join('--publish %s' % var for var in self.container.publish))\
771 if self.container.publish else ''
773 volume = '{0}'.format(
774 ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
775 if self.container.mnt else ''
778 '--privileged --detach --interactive --tty --rm '\
779 '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
780 '{env} {volume} --name {container.name} {container.image} '\
781 '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
782 container=self.container, command=command,
783 env=env, publish=publish, volume=volume)
785 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
787 raise RuntimeError('Failed to create container {c.name}'
788 .format(c=self.container))
792 def execute(self, command):
793 """Start a process inside a running container.
795 Runs the specified command inside the container specified by name. The
796 container has to be running already.
798 :param command: Command to run inside container.
800 :raises RuntimeError: If running the command in a container failed.
802 cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}'"\
803 .format(c=self.container, command=command)
805 ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
807 raise RuntimeError('Failed to execute command in container '
808 '{c.name}.'.format(c=self.container))
811 """Stop running container.
813 :raises RuntimeError: If stopping a container failed.
815 cmd = 'docker stop {c.name}'.format(c=self.container)
817 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
819 raise RuntimeError('Failed to stop container {c.name}.'
820 .format(c=self.container))
823 """Remove a container.
825 :raises RuntimeError: If removing a container failed.
827 cmd = 'docker rm --force {c.name}'.format(c=self.container)
829 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
831 raise RuntimeError('Failed to destroy container {c.name}.'
832 .format(c=self.container))
835 """Return low-level information on Docker objects.
837 :raises RuntimeError: If getting info about a container failed.
839 cmd = 'docker inspect {c.name}'.format(c=self.container)
841 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
843 raise RuntimeError('Failed to get info about container {c.name}.'
844 .format(c=self.container))
846 def system_info(self):
847 """Display the docker system-wide information.
849 :raises RuntimeError: If displaying system information failed.
851 cmd = 'docker system info'
853 ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
855 raise RuntimeError('Failed to get system info.')
857 def is_container_present(self):
858 """Check if container is present on node.
860 :returns: True if container is present.
862 :raises RuntimeError: If getting info about a container failed.
864 cmd = 'docker ps --all --quiet --filter name={c.name}'\
865 .format(c=self.container)
867 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
869 raise RuntimeError('Failed to get info about container {c.name}.'
870 .format(c=self.container))
871 return True if stdout else False
873 def is_container_running(self):
874 """Check if container is running on node.
876 :returns: True if container is running.
878 :raises RuntimeError: If getting info about a container failed.
880 cmd = 'docker ps --quiet --filter name={c.name}'\
881 .format(c=self.container)
883 ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
885 raise RuntimeError('Failed to get info about container {c.name}.'
886 .format(c=self.container))
887 return True if stdout else False
890 class Container(object):
891 """Container class."""
894 """Initialize Container object."""
897 def __getattr__(self, attr):
898 """Get attribute custom implementation.
900 :param attr: Attribute to get.
902 :returns: Attribute value or None.
906 return self.__dict__[attr]
910 def __setattr__(self, attr, value):
911 """Set attribute custom implementation.
913 :param attr: Attribute to set.
914 :param value: Value to set.
919 # Check if attribute exists
922 # Creating new attribute
924 self.__dict__['ssh'] = SSH()
925 self.__dict__['ssh'].connect(value)
926 self.__dict__[attr] = value
928 # Updating attribute base of type
929 if isinstance(self.__dict__[attr], list):
930 self.__dict__[attr].append(value)
932 self.__dict__[attr] = value