CSIT-843: Update actual topology in case of new/updated/deleted interface
[csit.git] / resources / libraries / python / ContainerUtils.py
1 # Copyright (c) 2017 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.CpuUtils import CpuUtils
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('{e} is not implemented.'
46                                       .format(e=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: {n}'
62                                .format(n=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={n}'.format(n=kwargs['name']))
79
80         # Set cpuset.cpus cgroup
81         skip_cnt = kwargs['cpu_skip']
82         if not kwargs['cpu_shared']:
83             skip_cnt += kwargs['i'] * kwargs['cpu_count']
84         self.engine.container.cpuset_cpus = \
85             CpuUtils.cpu_slice_of_list_per_node(node=kwargs['node'],
86                                                 cpu_node=kwargs['cpuset_mems'],
87                                                 skip_cnt=skip_cnt,
88                                                 cpu_cnt=kwargs['cpu_count'],
89                                                 smt_used=kwargs['smt_used'])
90
91         # Store container instance
92         self.containers[kwargs['name']] = self.engine.container
93
94     def construct_containers(self, **kwargs):
95         """Construct 1..N container(s) on node with specified name.
96
97         Ordinal number is automatically added to the name of container as
98         suffix.
99
100         :param kwargs: Named parameters.
101         :param kwargs: dict
102         """
103         name = kwargs['name']
104         for i in range(kwargs['count']):
105             # Name will contain ordinal suffix
106             kwargs['name'] = ''.join([name, str(i+1)])
107             # Create container
108             self.construct_container(i=i, **kwargs)
109
110     def acquire_all_containers(self):
111         """Acquire all containers."""
112         for container in self.containers:
113             self.engine.container = self.containers[container]
114             self.engine.acquire()
115
116     def build_all_containers(self):
117         """Build all containers."""
118         for container in self.containers:
119             self.engine.container = self.containers[container]
120             self.engine.build()
121
122     def create_all_containers(self):
123         """Create all containers."""
124         for container in self.containers:
125             self.engine.container = self.containers[container]
126             self.engine.create()
127
128     def execute_on_container(self, name, command):
129         """Execute command on container with name.
130
131         :param name: Container name.
132         :param command: Command to execute.
133         :type name: str
134         :type command: str
135         """
136         self.engine.container = self.get_container_by_name(name)
137         self.engine.execute(command)
138
139     def execute_on_all_containers(self, command):
140         """Execute command on all containers.
141
142         :param command: Command to execute.
143         :type command: str
144         """
145         for container in self.containers:
146             self.engine.container = self.containers[container]
147             self.engine.execute(command)
148
149     def install_vpp_in_all_containers(self):
150         """Install VPP into all containers."""
151         for container in self.containers:
152             self.engine.container = self.containers[container]
153             # We need to install supervisor client/server system to control VPP
154             # as a service
155             self.engine.install_supervisor()
156             self.engine.install_vpp()
157             self.engine.restart_vpp()
158
159     def configure_vpp_in_all_containers(self, vat_template_file):
160         """Configure VPP in all containers.
161
162         :param vat_template_file: Template file name of a VAT script.
163         :type vat_template_file: str
164         """
165         # Count number of DUTs based on node's host information
166         dut_cnt = len(Counter([self.containers[container].node['host']
167                                for container in self.containers]))
168         container_cnt = len(self.containers)
169         mod = dut_cnt/container_cnt
170
171         for i, container in enumerate(self.containers):
172             self.engine.container = self.containers[container]
173             self.engine.create_vpp_startup_config()
174             self.engine.create_vpp_exec_config(vat_template_file,
175                                                memif_id1=i % mod * 2 + 1,
176                                                memif_id2=i % mod * 2 + 2,
177                                                socket1='memif-{c.name}-1'
178                                                .format(c=self.engine.container),
179                                                socket2='memif-{c.name}-2'
180                                                .format(c=self.engine.container))
181
182     def stop_all_containers(self):
183         """Stop all containers."""
184         for container in self.containers:
185             self.engine.container = self.containers[container]
186             self.engine.stop()
187
188     def destroy_all_containers(self):
189         """Destroy all containers."""
190         for container in self.containers:
191             self.engine.container = self.containers[container]
192             self.engine.destroy()
193
194
195 class ContainerEngine(object):
196     """Abstract class for container engine."""
197
198     def __init__(self):
199         """Init ContainerEngine object."""
200         self.container = None
201
202     def initialize(self):
203         """Initialize container object."""
204         self.container = Container()
205
206     def acquire(self, force):
207         """Acquire/download container.
208
209         :param force: Destroy a container if exists and create.
210         :type force: bool
211         """
212         raise NotImplementedError
213
214     def build(self):
215         """Build container (compile)."""
216         raise NotImplementedError
217
218     def create(self):
219         """Create/deploy container."""
220         raise NotImplementedError
221
222     def execute(self, command):
223         """Execute process inside container.
224
225         :param command: Command to run inside container.
226         :type command: str
227         """
228         raise NotImplementedError
229
230     def stop(self):
231         """Stop container."""
232         raise NotImplementedError
233
234     def destroy(self):
235         """Destroy/remove container."""
236         raise NotImplementedError
237
238     def info(self):
239         """Info about container."""
240         raise NotImplementedError
241
242     def system_info(self):
243         """System info."""
244         raise NotImplementedError
245
246     def install_supervisor(self):
247         """Install supervisord inside a container."""
248         self.execute('sleep 3')
249         self.execute('apt-get update')
250         self.execute('apt-get install -y supervisor')
251         self.execute('echo "{0}" > {1}'
252                      .format(
253                          '[unix_http_server]\n'
254                          'file  = /tmp/supervisor.sock\n\n'
255                          '[rpcinterface:supervisor]\n'
256                          'supervisor.rpcinterface_factory = '
257                          'supervisor.rpcinterface:make_main_rpcinterface\n\n'
258                          '[supervisorctl]\n'
259                          'serverurl = unix:///tmp/supervisor.sock\n\n'
260                          '[supervisord]\n'
261                          'pidfile = /tmp/supervisord.pid\n'
262                          'identifier = supervisor\n'
263                          'directory = /tmp\n'
264                          'logfile=/tmp/supervisord.log\n'
265                          'loglevel=debug\n'
266                          'nodaemon=false\n\n',
267                          SUPERVISOR_CONF))
268         self.execute('supervisord -c {0}'.format(SUPERVISOR_CONF))
269
270     def install_vpp(self, install_dkms=False):
271         """Install VPP inside a container.
272
273         :param install_dkms: If install dkms package. This will impact install
274         time. Dkms is required for installation of vpp-dpdk-dkms. Default is
275         false.
276         :type install_dkms: bool
277         """
278         self.execute('ln -s /dev/null /etc/sysctl.d/80-vpp.conf')
279         self.execute('apt-get update')
280         if install_dkms:
281             self.execute('apt-get install -y dkms && '
282                          'dpkg -i --force-all {0}/install_dir/*.deb'
283                          .format(self.container.guest_dir))
284         else:
285             self.execute('for i in $(ls -I \"*dkms*\" {0}/install_dir/); '
286                          'do dpkg -i --force-all {0}/install_dir/$i; done'
287                          .format(self.container.guest_dir))
288         self.execute('apt-get -f install -y')
289         self.execute('echo "{0}" >> {1}'
290                      .format(
291                          '[program:vpp]\n'
292                          'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
293                          'autorestart=false\n'
294                          'redirect_stderr=true\n'
295                          'priority=1',
296                          SUPERVISOR_CONF))
297         self.execute('supervisorctl reload')
298
299     def restart_vpp(self):
300         """Restart VPP service inside a container."""
301         self.execute('supervisorctl restart vpp')
302
303     def create_vpp_startup_config(self,
304                                   config_filename='/etc/vpp/startup.conf'):
305         """Create base startup configuration of VPP on container.
306
307         :param config_filename: Startup configuration file name.
308         :type config_filename: str
309         """
310         cpuset_cpus = self.container.cpuset_cpus
311
312         # Create config instance
313         vpp_config = VppConfigGenerator()
314         vpp_config.set_node(self.container.node)
315         vpp_config.add_unix_cli_listen()
316         vpp_config.add_unix_nodaemon()
317         vpp_config.add_unix_exec('/tmp/running.exec')
318         # We will pop first core from list to be main core
319         vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
320         # if this is not only core in list, the rest will be used as workers.
321         if cpuset_cpus:
322             corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
323             vpp_config.add_cpu_corelist_workers(corelist_workers)
324         vpp_config.add_plugin_disable('dpdk_plugin.so')
325
326         self.execute('mkdir -p /etc/vpp/')
327         self.execute('echo "{c}" | tee {f}'
328                      .format(c=vpp_config.get_config_str(),
329                              f=config_filename))
330
331     def create_vpp_exec_config(self, vat_template_file, **kwargs):
332         """Create VPP exec configuration on container.
333
334         :param vat_template_file: File name of a VAT template script.
335         :param kwargs: Parameters for VAT script.
336         :type vat_template_file: str
337         :type kwargs: dict
338         """
339         vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
340                                          f=vat_template_file)
341
342         with open(vat_file_path, 'r') as template_file:
343             cmd_template = template_file.readlines()
344             for line_tmpl in cmd_template:
345                 vat_cmd = line_tmpl.format(**kwargs)
346                 self.execute('echo "{c}" >> /tmp/running.exec'
347                              .format(c=vat_cmd.replace('\n', '')))
348
349     def is_container_running(self):
350         """Check if container is running."""
351         raise NotImplementedError
352
353     def is_container_present(self):
354         """Check if container is present."""
355         raise NotImplementedError
356
357     def _configure_cgroup(self, name):
358         """Configure the control group associated with a container.
359
360         :param name: Name of cgroup.
361         :type name: str
362         :raises RuntimeError: If applying cgroup settings via cgset failed.
363         """
364         ret, _, _ = self.container.ssh.exec_command_sudo(
365             'cgcreate -g cpuset:/{name}'.format(name=name))
366         if int(ret) != 0:
367             raise RuntimeError('Failed to copy cgroup settings from root.')
368
369         ret, _, _ = self.container.ssh.exec_command_sudo(
370             'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
371         if int(ret) != 0:
372             raise RuntimeError('Failed to apply cgroup settings.')
373
374         ret, _, _ = self.container.ssh.exec_command_sudo(
375             'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
376         if int(ret) != 0:
377             raise RuntimeError('Failed to apply cgroup settings.')
378
379
380 class LXC(ContainerEngine):
381     """LXC implementation."""
382
383     def __init__(self):
384         """Initialize LXC object."""
385         super(LXC, self).__init__()
386
387     def acquire(self, force=True):
388         """Acquire a privileged system object where configuration is stored.
389
390         :param force: If a container exists, destroy it and create a new
391         container.
392         :type force: bool
393         :raises RuntimeError: If creating the container or writing the container
394         config fails.
395         """
396         if self.is_container_present():
397             if force:
398                 self.destroy()
399             else:
400                 return
401
402         image = self.container.image if self.container.image else\
403             "-d ubuntu -r xenial -a amd64"
404
405         cmd = 'lxc-create -t download --name {c.name} -- {image} '\
406             '--no-validate'.format(c=self.container, image=image)
407
408         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
409         if int(ret) != 0:
410             raise RuntimeError('Failed to create container.')
411
412         if self.container.host_dir and self.container.guest_dir:
413             entry = 'lxc.mount.entry = '\
414                 '{c.host_dir} /var/lib/lxc/{c.name}/rootfs{c.guest_dir} ' \
415                 'none bind,create=dir 0 0'.format(c=self.container)
416             ret, _, _ = self.container.ssh.exec_command_sudo(
417                 "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'"
418                 .format(e=entry, c=self.container))
419             if int(ret) != 0:
420                 raise RuntimeError('Failed to write {c.name} config.'
421                                    .format(c=self.container))
422         self._configure_cgroup('lxc')
423
424     def create(self):
425         """Create/deploy an application inside a container on system.
426
427         :raises RuntimeError: If creating the container fails.
428         """
429         cpuset_cpus = '{0}'.format(
430             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
431             if self.container.cpuset_cpus else ''
432
433         cmd = 'lxc-start --name {c.name} --daemon'.format(c=self.container)
434
435         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
436         if int(ret) != 0:
437             raise RuntimeError('Failed to start container {c.name}.'
438                                .format(c=self.container))
439         self._lxc_wait('RUNNING')
440
441         # Workaround for LXC to be able to allocate all cpus including isolated.
442         cmd = 'cgset --copy-from / lxc/'
443         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
444         if int(ret) != 0:
445             raise RuntimeError('Failed to copy cgroup to LXC')
446
447         cmd = 'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'\
448             .format(c=self.container, cpus=cpuset_cpus)
449         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
450         if int(ret) != 0:
451             raise RuntimeError('Failed to set cpuset.cpus to container '
452                                '{c.name}.'.format(c=self.container))
453
454     def execute(self, command):
455         """Start a process inside a running container.
456
457         Runs the specified command inside the container specified by name. The
458         container has to be running already.
459
460         :param command: Command to run inside container.
461         :type command: str
462         :raises RuntimeError: If running the command failed.
463         """
464         env = '--keep-env {0}'.format(
465             ' '.join('--set-var %s' % env for env in self.container.env))\
466             if self.container.env else ''
467
468         cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}'"\
469             .format(env=env, c=self.container, command=command)
470
471         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
472         if int(ret) != 0:
473             raise RuntimeError('Failed to run command inside container '
474                                '{c.name}.'.format(c=self.container))
475
476     def stop(self):
477         """Stop a container.
478
479         :raises RuntimeError: If stopping the container failed.
480         """
481         cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
482
483         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
484         if int(ret) != 0:
485             raise RuntimeError('Failed to stop container {c.name}.'
486                                .format(c=self.container))
487         self._lxc_wait('STOPPED|FROZEN')
488
489     def destroy(self):
490         """Destroy a container.
491
492         :raises RuntimeError: If destroying container failed.
493         """
494         cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
495
496         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
497         if int(ret) != 0:
498             raise RuntimeError('Failed to destroy container {c.name}.'
499                                .format(c=self.container))
500
501     def info(self):
502         """Query and shows information about a container.
503
504         :raises RuntimeError: If getting info about a container failed.
505         """
506         cmd = 'lxc-info --name {c.name}'.format(c=self.container)
507
508         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
509         if int(ret) != 0:
510             raise RuntimeError('Failed to get info about container {c.name}.'
511                                .format(c=self.container))
512
513     def system_info(self):
514         """Check the current kernel for LXC support.
515
516         :raises RuntimeError: If checking LXC support failed.
517         """
518         cmd = 'lxc-checkconfig'
519
520         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
521         if int(ret) != 0:
522             raise RuntimeError('Failed to check LXC support.')
523
524     def is_container_running(self):
525         """Check if container is running on node.
526
527         :returns: True if container is running.
528         :rtype: bool
529         :raises RuntimeError: If getting info about a container failed.
530         """
531         cmd = 'lxc-info --no-humanize --state --name {c.name}'\
532             .format(c=self.container)
533
534         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
535         if int(ret) != 0:
536             raise RuntimeError('Failed to get info about container {c.name}.'
537                                .format(c=self.container))
538         return True if 'RUNNING' in stdout else False
539
540     def is_container_present(self):
541         """Check if container is existing on node.
542
543         :returns: True if container is present.
544         :rtype: bool
545         :raises RuntimeError: If getting info about a container failed.
546         """
547         cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
548
549         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
550         return False if int(ret) else True
551
552     def _lxc_wait(self, state):
553         """Wait for a specific container state.
554
555         :param state: Specify the container state(s) to wait for.
556         :type state: str
557         :raises RuntimeError: If waiting for state of a container failed.
558         """
559         cmd = 'lxc-wait --name {c.name} --state "{s}"'\
560             .format(c=self.container, s=state)
561
562         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
563         if int(ret) != 0:
564             raise RuntimeError('Failed to wait for state "{s}" of container '
565                                '{c.name}.'.format(s=state, c=self.container))
566
567
568 class Docker(ContainerEngine):
569     """Docker implementation."""
570
571     def __init__(self):
572         """Initialize Docker object."""
573         super(Docker, self).__init__()
574
575     def acquire(self, force=True):
576         """Pull an image or a repository from a registry.
577
578         :param force: Destroy a container if exists.
579         :type force: bool
580         :raises RuntimeError: If pulling a container failed.
581         """
582         if self.is_container_present():
583             if force:
584                 self.destroy()
585             else:
586                 return
587
588         cmd = 'docker pull {c.image}'.format(c=self.container)
589
590         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
591         if int(ret) != 0:
592             raise RuntimeError('Failed to create container {c.name}.'
593                                .format(c=self.container))
594         self._configure_cgroup('docker')
595
596     def create(self):
597         """Create/deploy container.
598
599         :raises RuntimeError: If creating a container failed.
600         """
601         cpuset_cpus = '--cpuset-cpus={0}'.format(
602             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
603             if self.container.cpuset_cpus else ''
604
605         cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
606             if self.container.cpuset_mems is not None else ''
607
608         env = '{0}'.format(
609             ' '.join('--env %s' % env for env in self.container.env))\
610             if self.container.env else ''
611
612         command = '{0}'.format(self.container.command)\
613             if self.container.command else ''
614
615         publish = '{0}'.format(
616             ' '.join('--publish %s' % var for var in self.container.publish))\
617             if self.container.publish else ''
618
619         volume = '--volume {c.host_dir}:{c.guest_dir}'.format(c=self.container)\
620             if self.container.host_dir and self.container.guest_dir else ''
621
622         cmd = 'docker run '\
623             '--privileged --detach --interactive --tty --rm '\
624             '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
625             '{env} {volume} --name {container.name} {container.image} '\
626             '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
627                                container=self.container, command=command,
628                                env=env, publish=publish, volume=volume)
629
630         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
631         if int(ret) != 0:
632             raise RuntimeError('Failed to create container {c.name}'
633                                .format(c=self.container))
634
635         self.info()
636
637     def execute(self, command):
638         """Start a process inside a running container.
639
640         Runs the specified command inside the container specified by name. The
641         container has to be running already.
642
643         :param command: Command to run inside container.
644         :type command: str
645         :raises RuntimeError: If runnig the command in a container failed.
646         """
647         cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}'"\
648             .format(c=self.container, command=command)
649
650         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
651         if int(ret) != 0:
652             raise RuntimeError('Failed to execute command in container '
653                                '{c.name}.'.format(c=self.container))
654
655     def stop(self):
656         """Stop running container.
657
658         :raises RuntimeError: If stopping a container failed.
659         """
660         cmd = 'docker stop {c.name}'.format(c=self.container)
661
662         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
663         if int(ret) != 0:
664             raise RuntimeError('Failed to stop container {c.name}.'
665                                .format(c=self.container))
666
667     def destroy(self):
668         """Remove a container.
669
670         :raises RuntimeError: If removing a container failed.
671         """
672         cmd = 'docker rm --force {c.name}'.format(c=self.container)
673
674         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
675         if int(ret) != 0:
676             raise RuntimeError('Failed to destroy container {c.name}.'
677                                .format(c=self.container))
678
679     def info(self):
680         """Return low-level information on Docker objects.
681
682         :raises RuntimeError: If getting info about a container failed.
683         """
684         cmd = 'docker inspect {c.name}'.format(c=self.container)
685
686         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
687         if int(ret) != 0:
688             raise RuntimeError('Failed to get info about container {c.name}.'
689                                .format(c=self.container))
690
691     def system_info(self):
692         """Display the docker system-wide information.
693
694         :raises RuntimeError: If displaying system information failed.
695         """
696         cmd = 'docker system info'
697
698         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
699         if int(ret) != 0:
700             raise RuntimeError('Failed to get system info.')
701
702     def is_container_present(self):
703         """Check if container is present on node.
704
705         :returns: True if container is present.
706         :rtype: bool
707         :raises RuntimeError: If getting info about a container failed.
708         """
709         cmd = 'docker ps --all --quiet --filter name={c.name}'\
710             .format(c=self.container)
711
712         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
713         if int(ret) != 0:
714             raise RuntimeError('Failed to get info about container {c.name}.'
715                                .format(c=self.container))
716         return True if stdout else False
717
718     def is_container_running(self):
719         """Check if container is running on node.
720
721         :returns: True if container is running.
722         :rtype: bool
723         :raises RuntimeError: If getting info about a container failed.
724         """
725         cmd = 'docker ps --quiet --filter name={c.name}'\
726             .format(c=self.container)
727
728         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
729         if int(ret) != 0:
730             raise RuntimeError('Failed to get info about container {c.name}.'
731                                .format(c=self.container))
732         return True if stdout else False
733
734
735 class Container(object):
736     """Container class."""
737
738     def __init__(self):
739         """Initialize Container object."""
740         pass
741
742     def __getattr__(self, attr):
743         """Get attribute custom implementation.
744
745         :param attr: Attribute to get.
746         :type attr: str
747         :returns: Attribute value or None.
748         :rtype: any
749         """
750         try:
751             return self.__dict__[attr]
752         except KeyError:
753             return None
754
755     def __setattr__(self, attr, value):
756         """Set attribute custom implementation.
757
758         :param attr: Attribute to set.
759         :param value: Value to set.
760         :type attr: str
761         :type value: any
762         """
763         try:
764             # Check if attribute exists
765             self.__dict__[attr]
766         except KeyError:
767             # Creating new attribute
768             if attr == 'node':
769                 self.__dict__['ssh'] = SSH()
770                 self.__dict__['ssh'].connect(value)
771             self.__dict__[attr] = value
772         else:
773             # Updating attribute base of type
774             if isinstance(self.__dict__[attr], list):
775                 self.__dict__[attr].append(value)
776             else:
777                 self.__dict__[attr] = value