CSIT-1389 Refactor current keywords for multichaining - CNF
[csit.git] / resources / libraries / python / ContainerUtils.py
1 # Copyright (c) 2018 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:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
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.
13
14 # Bug workaround in pylint for abstract classes.
15 # pylint: disable=W0223
16
17 """Library to manipulate Containers."""
18
19 from collections import OrderedDict, Counter
20
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
25
26
27 __all__ = ["ContainerManager", "ContainerEngine", "LXC", "Docker", "Container"]
28
29 SUPERVISOR_CONF = '/etc/supervisord.conf'
30
31
32 class ContainerManager(object):
33     """Container lifecycle management class."""
34
35     def __init__(self, engine):
36         """Initialize Container Manager class.
37
38         :param engine: Container technology used (LXC/Docker/...).
39         :type engine: str
40         :raises NotImplementedError: If container technology is not implemented.
41         """
42         try:
43             self.engine = globals()[engine]()
44         except KeyError:
45             raise NotImplementedError('{engine} is not implemented.'.
46                                       format(engine=engine))
47         self.containers = OrderedDict()
48
49     def get_container_by_name(self, name):
50         """Get container instance.
51
52         :param name: Container name.
53         :type name: str
54         :returns: Container instance.
55         :rtype: Container
56         :raises RuntimeError: If failed to get container with name.
57         """
58         try:
59             return self.containers[name]
60         except KeyError:
61             raise RuntimeError('Failed to get container with name: {name}'.
62                                format(name=name))
63
64     def construct_container(self, **kwargs):
65         """Construct container object on node with specified parameters.
66
67         :param kwargs: Key-value pairs used to construct container.
68         :param kwargs: dict
69         """
70         # Create base class
71         self.engine.initialize()
72         # Set parameters
73         for key in kwargs:
74             setattr(self.engine.container, key, kwargs[key])
75
76         # Set additional environmental variables
77         setattr(self.engine.container, 'env',
78                 'MICROSERVICE_LABEL={label}'.format(label=kwargs['name']))
79
80         # Store container instance
81         self.containers[kwargs['name']] = self.engine.container
82
83     def construct_containers(self, **kwargs):
84         """Construct 1..N container(s) on node with specified name.
85
86         Ordinal number is automatically added to the name of container as
87         suffix.
88
89         :param kwargs: Named parameters.
90         :param kwargs: dict
91         """
92         name = kwargs['name']
93         for i in range(kwargs['count']):
94             # Name will contain ordinal suffix
95             kwargs['name'] = ''.join([name, str(i+1)])
96             # Create container
97             self.construct_container(i=i, **kwargs)
98
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()
104
105     def build_all_containers(self):
106         """Build all containers."""
107         for container in self.containers:
108             self.engine.container = self.containers[container]
109             self.engine.build()
110
111     def create_all_containers(self):
112         """Create all containers."""
113         for container in self.containers:
114             self.engine.container = self.containers[container]
115             self.engine.create()
116
117     def execute_on_container(self, name, command):
118         """Execute command on container with name.
119
120         :param name: Container name.
121         :param command: Command to execute.
122         :type name: str
123         :type command: str
124         """
125         self.engine.container = self.get_container_by_name(name)
126         self.engine.execute(command)
127
128     def execute_on_all_containers(self, command):
129         """Execute command on all containers.
130
131         :param command: Command to execute.
132         :type command: str
133         """
134         for container in self.containers:
135             self.engine.container = self.containers[container]
136             self.engine.execute(command)
137
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
143             # as a service
144             self.engine.install_supervisor()
145             self.engine.install_vpp()
146             self.engine.restart_vpp()
147
148     def restart_vpp_in_all_containers(self):
149         """Restart VPP on all containers."""
150         for container in self.containers:
151             self.engine.container = self.containers[container]
152             self.engine.restart_vpp()
153
154     def configure_vpp_in_all_containers(self, chain_topology,
155                                         dut1_if=None, dut2_if=None):
156         """Configure VPP in all containers.
157
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 dut1_if: Interface on DUT1 directly connected to DUT2.
163         :param dut2_if: Interface on DUT2 directly connected to DUT1.
164         :type container_topology: str
165         :type dut1_if: str
166         :type dut2_if: str
167         """
168         # Count number of DUTs based on node's host information
169         dut_cnt = len(Counter([self.containers[container].node['host']
170                                for container in self.containers]))
171         mod = len(self.containers)/dut_cnt
172         container_vat_template = 'memif_create_{topology}.vat'.format(
173             topology=chain_topology)
174
175         if chain_topology == 'chain':
176             for i, container in enumerate(self.containers):
177                 mid1 = i % mod + 1
178                 mid2 = i % mod + 1
179                 sid1 = i % mod * 2 + 1
180                 sid2 = i % mod * 2 + 2
181                 self.engine.container = self.containers[container]
182                 self.engine.create_vpp_startup_config()
183                 self.engine.create_vpp_exec_config(container_vat_template, \
184                     mid1=mid1, mid2=mid2, sid1=sid1, sid2=sid2, \
185                     socket1='memif-{c.name}-{sid}'. \
186                     format(c=self.engine.container, sid=sid1), \
187                     socket2='memif-{c.name}-{sid}'. \
188                     format(c=self.engine.container, sid=sid2))
189         elif chain_topology == 'cross_horiz':
190             if mod > 1:
191                 raise RuntimeError('Container chain topology {topology} '
192                                    'supports only single container.'.
193                                    format(topology=chain_topology))
194             for i, container in enumerate(self.containers):
195                 mid1 = i % mod + 1
196                 sid1 = i % mod * 2 + 1
197                 self.engine.container = self.containers[container]
198                 if 'DUT1' in self.engine.container.name:
199                     if_pci = Topology.get_interface_pci_addr( \
200                         self.engine.container.node, dut1_if)
201                     if_name = Topology.get_interface_name( \
202                         self.engine.container.node, dut1_if)
203                 if 'DUT2' in self.engine.container.name:
204                     if_pci = Topology.get_interface_pci_addr( \
205                         self.engine.container.node, dut2_if)
206                     if_name = Topology.get_interface_name( \
207                         self.engine.container.node, dut2_if)
208                 self.engine.create_vpp_startup_config_dpdk_dev(if_pci)
209                 self.engine.create_vpp_exec_config(container_vat_template, \
210                     mid1=mid1, sid1=sid1, if_name=if_name, \
211                     socket1='memif-{c.name}-{sid}'. \
212                     format(c=self.engine.container, sid=sid1))
213         else:
214             raise RuntimeError('Container topology {topology} not implemented'.
215                                format(topology=chain_topology))
216
217     def stop_all_containers(self):
218         """Stop all containers."""
219         for container in self.containers:
220             self.engine.container = self.containers[container]
221             self.engine.stop()
222
223     def destroy_all_containers(self):
224         """Destroy all containers."""
225         for container in self.containers:
226             self.engine.container = self.containers[container]
227             self.engine.destroy()
228
229
230 class ContainerEngine(object):
231     """Abstract class for container engine."""
232
233     def __init__(self):
234         """Init ContainerEngine object."""
235         self.container = None
236
237     def initialize(self):
238         """Initialize container object."""
239         self.container = Container()
240
241     def acquire(self, force):
242         """Acquire/download container.
243
244         :param force: Destroy a container if exists and create.
245         :type force: bool
246         """
247         raise NotImplementedError
248
249     def build(self):
250         """Build container (compile)."""
251         raise NotImplementedError
252
253     def create(self):
254         """Create/deploy container."""
255         raise NotImplementedError
256
257     def execute(self, command):
258         """Execute process inside container.
259
260         :param command: Command to run inside container.
261         :type command: str
262         """
263         raise NotImplementedError
264
265     def stop(self):
266         """Stop container."""
267         raise NotImplementedError
268
269     def destroy(self):
270         """Destroy/remove container."""
271         raise NotImplementedError
272
273     def info(self):
274         """Info about container."""
275         raise NotImplementedError
276
277     def system_info(self):
278         """System info."""
279         raise NotImplementedError
280
281     def install_supervisor(self):
282         """Install supervisord inside a container."""
283         self.execute('sleep 3')
284         self.execute('apt-get update')
285         self.execute('apt-get install -y supervisor')
286         self.execute('echo "{config}" > {config_file}'.
287                      format(
288                          config='[unix_http_server]\n'
289                          'file  = /tmp/supervisor.sock\n\n'
290                          '[rpcinterface:supervisor]\n'
291                          'supervisor.rpcinterface_factory = '
292                          'supervisor.rpcinterface:make_main_rpcinterface\n\n'
293                          '[supervisorctl]\n'
294                          'serverurl = unix:///tmp/supervisor.sock\n\n'
295                          '[supervisord]\n'
296                          'pidfile = /tmp/supervisord.pid\n'
297                          'identifier = supervisor\n'
298                          'directory = /tmp\n'
299                          'logfile=/tmp/supervisord.log\n'
300                          'loglevel=debug\n'
301                          'nodaemon=false\n\n',
302                          config_file=SUPERVISOR_CONF))
303         self.execute('supervisord -c {config_file}'.
304                      format(config_file=SUPERVISOR_CONF))
305
306     def install_vpp(self):
307         """Install VPP inside a container."""
308         self.execute('ln -s /dev/null /etc/sysctl.d/80-vpp.conf')
309         self.execute('apt-get update')
310         # Workaround for install xenial vpp build on bionic ubuntu.
311         self.execute('apt-get install -y wget')
312         self.execute('deb=$(mktemp) && wget -O "${deb}" '
313                      'http://launchpadlibrarian.net/336117627/'
314                      'libmbedcrypto0_2.5.1-1ubuntu1_amd64.deb && '
315                      'dpkg -i "${deb}" && '
316                      'rm -f "${deb}"')
317         self.execute('deb=$(mktemp) && wget -O "${deb}" '
318                      'http://launchpadlibrarian.net/252876048/'
319                      'libboost-system1.58.0_1.58.0+dfsg-5ubuntu3_amd64.deb && '
320                      'dpkg -i "${deb}" && '
321                      'rm -f "${deb}"')
322         self.execute(
323             'dpkg -i --force-all '
324             '{guest_dir}/openvpp-testing/download_dir/*.deb'.
325             format(guest_dir=self.container.mnt[0].split(':')[1]))
326         self.execute('apt-get -f install -y')
327         self.execute('apt-get install -y ca-certificates')
328         self.execute('echo "{config}" >> {config_file}'.
329                      format(
330                          config='[program:vpp]\n'
331                          'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
332                          'autorestart=false\n'
333                          'redirect_stderr=true\n'
334                          'priority=1',
335                          config_file=SUPERVISOR_CONF))
336         self.execute('supervisorctl reload')
337         self.execute('supervisorctl restart vpp')
338
339     def restart_vpp(self):
340         """Restart VPP service inside a container."""
341         self.execute('supervisorctl restart vpp')
342         self.execute('cat /tmp/supervisord.log')
343
344     def create_base_vpp_startup_config(self):
345         """Create base startup configuration of VPP on container.
346
347         :returns: Base VPP startup configuration.
348         :rtype: VppConfigGenerator
349         """
350         cpuset_cpus = self.container.cpuset_cpus
351
352         # Create config instance
353         vpp_config = VppConfigGenerator()
354         vpp_config.set_node(self.container.node)
355         vpp_config.add_unix_cli_listen()
356         vpp_config.add_unix_nodaemon()
357         vpp_config.add_unix_exec('/tmp/running.exec')
358         # We will pop first core from list to be main core
359         vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
360         # if this is not only core in list, the rest will be used as workers.
361         if cpuset_cpus:
362             corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
363             vpp_config.add_cpu_corelist_workers(corelist_workers)
364
365         return vpp_config
366
367     def create_vpp_startup_config(self):
368         """Create startup configuration of VPP without DPDK on container.
369         """
370         vpp_config = self.create_base_vpp_startup_config()
371         vpp_config.add_plugin('disable', 'dpdk_plugin.so')
372
373         # Apply configuration
374         self.execute('mkdir -p /etc/vpp/')
375         self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
376                      .format(config=vpp_config.get_config_str()))
377
378     def create_vpp_startup_config_dpdk_dev(self, *devices):
379         """Create startup configuration of VPP with DPDK on container.
380
381         :param devices: List of PCI devices to add.
382         :type devices: list
383         """
384         vpp_config = self.create_base_vpp_startup_config()
385         vpp_config.add_dpdk_dev(*devices)
386         vpp_config.add_dpdk_no_tx_checksum_offload()
387         vpp_config.add_dpdk_log_level('debug')
388         vpp_config.add_plugin('disable', 'default')
389         vpp_config.add_plugin('enable', 'dpdk_plugin.so')
390         vpp_config.add_plugin('enable', 'memif_plugin.so')
391
392         # Apply configuration
393         self.execute('mkdir -p /etc/vpp/')
394         self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
395                      .format(config=vpp_config.get_config_str()))
396
397     def create_vpp_exec_config(self, vat_template_file, **kwargs):
398         """Create VPP exec configuration on container.
399
400         :param vat_template_file: File name of a VAT template script.
401         :param kwargs: Parameters for VAT script.
402         :type vat_template_file: str
403         :type kwargs: dict
404         """
405         vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
406                                          f=vat_template_file)
407
408         with open(vat_file_path, 'r') as template_file:
409             cmd_template = template_file.readlines()
410             for line_tmpl in cmd_template:
411                 vat_cmd = line_tmpl.format(**kwargs)
412                 self.execute('echo "{c}" >> /tmp/running.exec'
413                              .format(c=vat_cmd.replace('\n', '')))
414
415     def is_container_running(self):
416         """Check if container is running."""
417         raise NotImplementedError
418
419     def is_container_present(self):
420         """Check if container is present."""
421         raise NotImplementedError
422
423     def _configure_cgroup(self, name):
424         """Configure the control group associated with a container.
425
426         By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
427         container is initialized a new cgroup /docker or /lxc is created under
428         cpuset parent tree. This newly created cgroup is inheriting parent
429         setting for cpu/mem exclusive parameter and thus cannot be overriden
430         within /docker or /lxc cgroup. This function is supposed to set cgroups
431         to allow coexistence of both engines.
432
433         :param name: Name of cgroup.
434         :type name: str
435         :raises RuntimeError: If applying cgroup settings via cgset failed.
436         """
437         ret, _, _ = self.container.ssh.exec_command_sudo(
438             'cgset -r cpuset.cpu_exclusive=0 /')
439         if int(ret) != 0:
440             raise RuntimeError('Failed to apply cgroup settings.')
441
442         ret, _, _ = self.container.ssh.exec_command_sudo(
443             'cgset -r cpuset.mem_exclusive=0 /')
444         if int(ret) != 0:
445             raise RuntimeError('Failed to apply cgroup settings.')
446
447         ret, _, _ = self.container.ssh.exec_command_sudo(
448             'cgcreate -g cpuset:/{name}'.format(name=name))
449         if int(ret) != 0:
450             raise RuntimeError('Failed to copy cgroup settings from root.')
451
452         ret, _, _ = self.container.ssh.exec_command_sudo(
453             'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
454         if int(ret) != 0:
455             raise RuntimeError('Failed to apply cgroup settings.')
456
457         ret, _, _ = self.container.ssh.exec_command_sudo(
458             'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
459         if int(ret) != 0:
460             raise RuntimeError('Failed to apply cgroup settings.')
461
462
463 class LXC(ContainerEngine):
464     """LXC implementation."""
465
466     # Implicit constructor is inherited.
467
468     def acquire(self, force=True):
469         """Acquire a privileged system object where configuration is stored.
470
471         :param force: If a container exists, destroy it and create a new
472             container.
473         :type force: bool
474         :raises RuntimeError: If creating the container or writing the container
475             config fails.
476         """
477         if self.is_container_present():
478             if force:
479                 self.destroy()
480             else:
481                 return
482
483         image = self.container.image if self.container.image else\
484             "-d ubuntu -r xenial -a amd64"
485
486         cmd = 'lxc-create -t download --name {c.name} -- {image} '\
487             '--no-validate'.format(c=self.container, image=image)
488
489         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
490         if int(ret) != 0:
491             raise RuntimeError('Failed to create container.')
492
493         self._configure_cgroup('lxc')
494
495     def create(self):
496         """Create/deploy an application inside a container on system.
497
498         :raises RuntimeError: If creating the container fails.
499         """
500         if self.container.mnt:
501             for mount in self.container.mnt:
502                 host_dir, guest_dir = mount.split(':')
503                 entry = 'lxc.mount.entry = {host_dir} '\
504                     '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
505                     'bind,create=dir 0 0'.format(c=self.container,
506                                                  host_dir=host_dir,
507                                                  guest_dir=guest_dir)
508                 ret, _, _ = self.container.ssh.exec_command_sudo(
509                     "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
510                     format(e=entry, c=self.container))
511                 if int(ret) != 0:
512                     raise RuntimeError('Failed to write {c.name} config.'
513                                        .format(c=self.container))
514
515         cpuset_cpus = '{0}'.format(
516             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
517             if self.container.cpuset_cpus else ''
518
519         ret, _, _ = self.container.ssh.exec_command_sudo(
520             'lxc-start --name {c.name} --daemon'.
521             format(c=self.container))
522         if int(ret) != 0:
523             raise RuntimeError('Failed to start container {c.name}.'.
524                                format(c=self.container))
525         self._lxc_wait('RUNNING')
526
527         # Workaround for LXC to be able to allocate all cpus including isolated.
528         ret, _, _ = self.container.ssh.exec_command_sudo(
529             'cgset --copy-from / lxc/')
530         if int(ret) != 0:
531             raise RuntimeError('Failed to copy cgroup to LXC')
532
533         ret, _, _ = self.container.ssh.exec_command_sudo(
534             'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
535             format(c=self.container, cpus=cpuset_cpus))
536         if int(ret) != 0:
537             raise RuntimeError('Failed to set cpuset.cpus to container '
538                                '{c.name}.'.format(c=self.container))
539
540     def execute(self, command):
541         """Start a process inside a running container.
542
543         Runs the specified command inside the container specified by name. The
544         container has to be running already.
545
546         :param command: Command to run inside container.
547         :type command: str
548         :raises RuntimeError: If running the command failed.
549         """
550         env = '--keep-env {0}'.format(
551             ' '.join('--set-var %s' % env for env in self.container.env))\
552             if self.container.env else ''
553
554         cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}; "\
555             "exit $?'".format(env=env, c=self.container, command=command)
556
557         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
558         if int(ret) != 0:
559             raise RuntimeError('Failed to run command inside container '
560                                '{c.name}.'.format(c=self.container))
561
562     def stop(self):
563         """Stop a container.
564
565         :raises RuntimeError: If stopping the container failed.
566         """
567         cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
568
569         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
570         if int(ret) != 0:
571             raise RuntimeError('Failed to stop container {c.name}.'
572                                .format(c=self.container))
573         self._lxc_wait('STOPPED|FROZEN')
574
575     def destroy(self):
576         """Destroy a container.
577
578         :raises RuntimeError: If destroying container failed.
579         """
580         cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
581
582         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
583         if int(ret) != 0:
584             raise RuntimeError('Failed to destroy container {c.name}.'
585                                .format(c=self.container))
586
587     def info(self):
588         """Query and shows information about a container.
589
590         :raises RuntimeError: If getting info about a container failed.
591         """
592         cmd = 'lxc-info --name {c.name}'.format(c=self.container)
593
594         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
595         if int(ret) != 0:
596             raise RuntimeError('Failed to get info about container {c.name}.'
597                                .format(c=self.container))
598
599     def system_info(self):
600         """Check the current kernel for LXC support.
601
602         :raises RuntimeError: If checking LXC support failed.
603         """
604         cmd = 'lxc-checkconfig'
605
606         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
607         if int(ret) != 0:
608             raise RuntimeError('Failed to check LXC support.')
609
610     def is_container_running(self):
611         """Check if container is running on node.
612
613         :returns: True if container is running.
614         :rtype: bool
615         :raises RuntimeError: If getting info about a container failed.
616         """
617         cmd = 'lxc-info --no-humanize --state --name {c.name}'\
618             .format(c=self.container)
619
620         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
621         if int(ret) != 0:
622             raise RuntimeError('Failed to get info about container {c.name}.'
623                                .format(c=self.container))
624         return True if 'RUNNING' in stdout else False
625
626     def is_container_present(self):
627         """Check if container is existing on node.
628
629         :returns: True if container is present.
630         :rtype: bool
631         :raises RuntimeError: If getting info about a container failed.
632         """
633         cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
634
635         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
636         return False if int(ret) else True
637
638     def _lxc_wait(self, state):
639         """Wait for a specific container state.
640
641         :param state: Specify the container state(s) to wait for.
642         :type state: str
643         :raises RuntimeError: If waiting for state of a container failed.
644         """
645         cmd = 'lxc-wait --name {c.name} --state "{s}"'\
646             .format(c=self.container, s=state)
647
648         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
649         if int(ret) != 0:
650             raise RuntimeError('Failed to wait for state "{s}" of container '
651                                '{c.name}.'.format(s=state, c=self.container))
652
653
654 class Docker(ContainerEngine):
655     """Docker implementation."""
656
657     # Implicit constructor is inherited.
658
659     def acquire(self, force=True):
660         """Pull an image or a repository from a registry.
661
662         :param force: Destroy a container if exists.
663         :type force: bool
664         :raises RuntimeError: If pulling a container failed.
665         """
666         if self.is_container_present():
667             if force:
668                 self.destroy()
669             else:
670                 return
671
672         if not self.container.image:
673             setattr(self.container, 'image', 'snergster/csit-sut:latest')
674
675         cmd = 'docker pull {image}'.format(image=self.container.image)
676
677         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
678         if int(ret) != 0:
679             raise RuntimeError('Failed to create container {c.name}.'
680                                .format(c=self.container))
681         self._configure_cgroup('docker')
682
683     def create(self):
684         """Create/deploy container.
685
686         :raises RuntimeError: If creating a container failed.
687         """
688         cpuset_cpus = '--cpuset-cpus={0}'.format(
689             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
690             if self.container.cpuset_cpus else ''
691
692         cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
693             if self.container.cpuset_mems is not None else ''
694         # Temporary workaround - disabling due to bug in memif
695         cpuset_mems = ''
696
697         env = '{0}'.format(
698             ' '.join('--env %s' % env for env in self.container.env))\
699             if self.container.env else ''
700
701         command = '{0}'.format(self.container.command)\
702             if self.container.command else ''
703
704         publish = '{0}'.format(
705             ' '.join('--publish %s' % var for var in self.container.publish))\
706             if self.container.publish else ''
707
708         volume = '{0}'.format(
709             ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
710             if self.container.mnt else ''
711
712         cmd = 'docker run '\
713             '--privileged --detach --interactive --tty --rm '\
714             '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
715             '{env} {volume} --name {container.name} {container.image} '\
716             '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
717                                container=self.container, command=command,
718                                env=env, publish=publish, volume=volume)
719
720         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
721         if int(ret) != 0:
722             raise RuntimeError('Failed to create container {c.name}'
723                                .format(c=self.container))
724
725         self.info()
726
727     def execute(self, command):
728         """Start a process inside a running container.
729
730         Runs the specified command inside the container specified by name. The
731         container has to be running already.
732
733         :param command: Command to run inside container.
734         :type command: str
735         :raises RuntimeError: If runnig the command in a container failed.
736         """
737         cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}; "\
738             "exit $?'".format(c=self.container, command=command)
739
740         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
741         if int(ret) != 0:
742             raise RuntimeError('Failed to execute command in container '
743                                '{c.name}.'.format(c=self.container))
744
745     def stop(self):
746         """Stop running container.
747
748         :raises RuntimeError: If stopping a container failed.
749         """
750         cmd = 'docker stop {c.name}'.format(c=self.container)
751
752         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
753         if int(ret) != 0:
754             raise RuntimeError('Failed to stop container {c.name}.'
755                                .format(c=self.container))
756
757     def destroy(self):
758         """Remove a container.
759
760         :raises RuntimeError: If removing a container failed.
761         """
762         cmd = 'docker rm --force {c.name}'.format(c=self.container)
763
764         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
765         if int(ret) != 0:
766             raise RuntimeError('Failed to destroy container {c.name}.'
767                                .format(c=self.container))
768
769     def info(self):
770         """Return low-level information on Docker objects.
771
772         :raises RuntimeError: If getting info about a container failed.
773         """
774         cmd = 'docker inspect {c.name}'.format(c=self.container)
775
776         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
777         if int(ret) != 0:
778             raise RuntimeError('Failed to get info about container {c.name}.'
779                                .format(c=self.container))
780
781     def system_info(self):
782         """Display the docker system-wide information.
783
784         :raises RuntimeError: If displaying system information failed.
785         """
786         cmd = 'docker system info'
787
788         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
789         if int(ret) != 0:
790             raise RuntimeError('Failed to get system info.')
791
792     def is_container_present(self):
793         """Check if container is present on node.
794
795         :returns: True if container is present.
796         :rtype: bool
797         :raises RuntimeError: If getting info about a container failed.
798         """
799         cmd = 'docker ps --all --quiet --filter name={c.name}'\
800             .format(c=self.container)
801
802         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
803         if int(ret) != 0:
804             raise RuntimeError('Failed to get info about container {c.name}.'
805                                .format(c=self.container))
806         return True if stdout else False
807
808     def is_container_running(self):
809         """Check if container is running on node.
810
811         :returns: True if container is running.
812         :rtype: bool
813         :raises RuntimeError: If getting info about a container failed.
814         """
815         cmd = 'docker ps --quiet --filter name={c.name}'\
816             .format(c=self.container)
817
818         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
819         if int(ret) != 0:
820             raise RuntimeError('Failed to get info about container {c.name}.'
821                                .format(c=self.container))
822         return True if stdout else False
823
824
825 class Container(object):
826     """Container class."""
827
828     def __init__(self):
829         """Initialize Container object."""
830         pass
831
832     def __getattr__(self, attr):
833         """Get attribute custom implementation.
834
835         :param attr: Attribute to get.
836         :type attr: str
837         :returns: Attribute value or None.
838         :rtype: any
839         """
840         try:
841             return self.__dict__[attr]
842         except KeyError:
843             return None
844
845     def __setattr__(self, attr, value):
846         """Set attribute custom implementation.
847
848         :param attr: Attribute to set.
849         :param value: Value to set.
850         :type attr: str
851         :type value: any
852         """
853         try:
854             # Check if attribute exists
855             self.__dict__[attr]
856         except KeyError:
857             # Creating new attribute
858             if attr == 'node':
859                 self.__dict__['ssh'] = SSH()
860                 self.__dict__['ssh'].connect(value)
861             self.__dict__[attr] = value
862         else:
863             # Updating attribute base of type
864             if isinstance(self.__dict__[attr], list):
865                 self.__dict__[attr].append(value)
866             else:
867                 self.__dict__[attr] = value