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