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