Change the default plugin behavior in perf tests
[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', 'default')
331         vpp_config.add_plugin('enable', 'memif_plugin.so')
332
333         self.execute('mkdir -p /etc/vpp/')
334         self.execute('echo "{c}" | tee {f}'
335                      .format(c=vpp_config.get_config_str(),
336                              f=config_filename))
337
338     def create_vpp_exec_config(self, vat_template_file, **kwargs):
339         """Create VPP exec configuration on container.
340
341         :param vat_template_file: File name of a VAT template script.
342         :param kwargs: Parameters for VAT script.
343         :type vat_template_file: str
344         :type kwargs: dict
345         """
346         vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
347                                          f=vat_template_file)
348
349         with open(vat_file_path, 'r') as template_file:
350             cmd_template = template_file.readlines()
351             for line_tmpl in cmd_template:
352                 vat_cmd = line_tmpl.format(**kwargs)
353                 self.execute('echo "{c}" >> /tmp/running.exec'
354                              .format(c=vat_cmd.replace('\n', '')))
355
356     def is_container_running(self):
357         """Check if container is running."""
358         raise NotImplementedError
359
360     def is_container_present(self):
361         """Check if container is present."""
362         raise NotImplementedError
363
364     def _configure_cgroup(self, name):
365         """Configure the control group associated with a container.
366
367         By default the cpuset cgroup is using exclusive CPU/MEM. When Docker
368         container is initialized a new cgroup /docker or /lxc is created under
369         cpuset parent tree. This newly created cgroup is inheriting parent
370         setting for cpu/mem exclusive parameter and thus cannot be overriden
371         within /docker or /lxc cgroup. This patch is supposed to set cpu/mem
372         exclusive parameter for both parent and subgroup.
373
374         :param name: Name of cgroup.
375         :type name: str
376         :raises RuntimeError: If applying cgroup settings via cgset failed.
377         """
378         ret, _, _ = self.container.ssh.exec_command_sudo(
379             'cgset -r cpuset.cpu_exclusive=0 /')
380         if int(ret) != 0:
381             raise RuntimeError('Failed to apply cgroup settings.')
382
383         ret, _, _ = self.container.ssh.exec_command_sudo(
384             'cgset -r cpuset.mem_exclusive=0 /')
385         if int(ret) != 0:
386             raise RuntimeError('Failed to apply cgroup settings.')
387
388         ret, _, _ = self.container.ssh.exec_command_sudo(
389             'cgcreate -g cpuset:/{name}'.format(name=name))
390         if int(ret) != 0:
391             raise RuntimeError('Failed to copy cgroup settings from root.')
392
393         ret, _, _ = self.container.ssh.exec_command_sudo(
394             'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
395         if int(ret) != 0:
396             raise RuntimeError('Failed to apply cgroup settings.')
397
398         ret, _, _ = self.container.ssh.exec_command_sudo(
399             'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
400         if int(ret) != 0:
401             raise RuntimeError('Failed to apply cgroup settings.')
402
403
404 class LXC(ContainerEngine):
405     """LXC implementation."""
406
407     def __init__(self):
408         """Initialize LXC object."""
409         super(LXC, self).__init__()
410
411     def acquire(self, force=True):
412         """Acquire a privileged system object where configuration is stored.
413
414         :param force: If a container exists, destroy it and create a new
415         container.
416         :type force: bool
417         :raises RuntimeError: If creating the container or writing the container
418         config fails.
419         """
420         if self.is_container_present():
421             if force:
422                 self.destroy()
423             else:
424                 return
425
426         image = self.container.image if self.container.image else\
427             "-d ubuntu -r xenial -a amd64"
428
429         cmd = 'lxc-create -t download --name {c.name} -- {image} '\
430             '--no-validate'.format(c=self.container, image=image)
431
432         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
433         if int(ret) != 0:
434             raise RuntimeError('Failed to create container.')
435
436         if self.container.host_dir and self.container.guest_dir:
437             entry = 'lxc.mount.entry = '\
438                 '{c.host_dir} /var/lib/lxc/{c.name}/rootfs{c.guest_dir} ' \
439                 'none bind,create=dir 0 0'.format(c=self.container)
440             ret, _, _ = self.container.ssh.exec_command_sudo(
441                 "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'"
442                 .format(e=entry, c=self.container))
443             if int(ret) != 0:
444                 raise RuntimeError('Failed to write {c.name} config.'
445                                    .format(c=self.container))
446         self._configure_cgroup('lxc')
447
448     def create(self):
449         """Create/deploy an application inside a container on system.
450
451         :raises RuntimeError: If creating the container fails.
452         """
453         cpuset_cpus = '{0}'.format(
454             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
455             if self.container.cpuset_cpus else ''
456
457         cmd = 'lxc-start --name {c.name} --daemon'.format(c=self.container)
458
459         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
460         if int(ret) != 0:
461             raise RuntimeError('Failed to start container {c.name}.'
462                                .format(c=self.container))
463         self._lxc_wait('RUNNING')
464
465         # Workaround for LXC to be able to allocate all cpus including isolated.
466         cmd = 'cgset --copy-from / lxc/'
467         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
468         if int(ret) != 0:
469             raise RuntimeError('Failed to copy cgroup to LXC')
470
471         cmd = 'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'\
472             .format(c=self.container, cpus=cpuset_cpus)
473         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
474         if int(ret) != 0:
475             raise RuntimeError('Failed to set cpuset.cpus to container '
476                                '{c.name}.'.format(c=self.container))
477
478     def execute(self, command):
479         """Start a process inside a running container.
480
481         Runs the specified command inside the container specified by name. The
482         container has to be running already.
483
484         :param command: Command to run inside container.
485         :type command: str
486         :raises RuntimeError: If running the command failed.
487         """
488         env = '--keep-env {0}'.format(
489             ' '.join('--set-var %s' % env for env in self.container.env))\
490             if self.container.env else ''
491
492         cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}'"\
493             .format(env=env, c=self.container, command=command)
494
495         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
496         if int(ret) != 0:
497             raise RuntimeError('Failed to run command inside container '
498                                '{c.name}.'.format(c=self.container))
499
500     def stop(self):
501         """Stop a container.
502
503         :raises RuntimeError: If stopping the container failed.
504         """
505         cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
506
507         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
508         if int(ret) != 0:
509             raise RuntimeError('Failed to stop container {c.name}.'
510                                .format(c=self.container))
511         self._lxc_wait('STOPPED|FROZEN')
512
513     def destroy(self):
514         """Destroy a container.
515
516         :raises RuntimeError: If destroying container failed.
517         """
518         cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
519
520         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
521         if int(ret) != 0:
522             raise RuntimeError('Failed to destroy container {c.name}.'
523                                .format(c=self.container))
524
525     def info(self):
526         """Query and shows information about a container.
527
528         :raises RuntimeError: If getting info about a container failed.
529         """
530         cmd = 'lxc-info --name {c.name}'.format(c=self.container)
531
532         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
533         if int(ret) != 0:
534             raise RuntimeError('Failed to get info about container {c.name}.'
535                                .format(c=self.container))
536
537     def system_info(self):
538         """Check the current kernel for LXC support.
539
540         :raises RuntimeError: If checking LXC support failed.
541         """
542         cmd = 'lxc-checkconfig'
543
544         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
545         if int(ret) != 0:
546             raise RuntimeError('Failed to check LXC support.')
547
548     def is_container_running(self):
549         """Check if container is running on node.
550
551         :returns: True if container is running.
552         :rtype: bool
553         :raises RuntimeError: If getting info about a container failed.
554         """
555         cmd = 'lxc-info --no-humanize --state --name {c.name}'\
556             .format(c=self.container)
557
558         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
559         if int(ret) != 0:
560             raise RuntimeError('Failed to get info about container {c.name}.'
561                                .format(c=self.container))
562         return True if 'RUNNING' in stdout else False
563
564     def is_container_present(self):
565         """Check if container is existing on node.
566
567         :returns: True if container is present.
568         :rtype: bool
569         :raises RuntimeError: If getting info about a container failed.
570         """
571         cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
572
573         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
574         return False if int(ret) else True
575
576     def _lxc_wait(self, state):
577         """Wait for a specific container state.
578
579         :param state: Specify the container state(s) to wait for.
580         :type state: str
581         :raises RuntimeError: If waiting for state of a container failed.
582         """
583         cmd = 'lxc-wait --name {c.name} --state "{s}"'\
584             .format(c=self.container, s=state)
585
586         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
587         if int(ret) != 0:
588             raise RuntimeError('Failed to wait for state "{s}" of container '
589                                '{c.name}.'.format(s=state, c=self.container))
590
591
592 class Docker(ContainerEngine):
593     """Docker implementation."""
594
595     def __init__(self):
596         """Initialize Docker object."""
597         super(Docker, self).__init__()
598
599     def acquire(self, force=True):
600         """Pull an image or a repository from a registry.
601
602         :param force: Destroy a container if exists.
603         :type force: bool
604         :raises RuntimeError: If pulling a container failed.
605         """
606         if self.is_container_present():
607             if force:
608                 self.destroy()
609             else:
610                 return
611
612         cmd = 'docker pull {c.image}'.format(c=self.container)
613
614         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
615         if int(ret) != 0:
616             raise RuntimeError('Failed to create container {c.name}.'
617                                .format(c=self.container))
618         self._configure_cgroup('docker')
619
620     def create(self):
621         """Create/deploy container.
622
623         :raises RuntimeError: If creating a container failed.
624         """
625         cpuset_cpus = '--cpuset-cpus={0}'.format(
626             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
627             if self.container.cpuset_cpus else ''
628
629         cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
630             if self.container.cpuset_mems is not None else ''
631
632         env = '{0}'.format(
633             ' '.join('--env %s' % env for env in self.container.env))\
634             if self.container.env else ''
635
636         command = '{0}'.format(self.container.command)\
637             if self.container.command else ''
638
639         publish = '{0}'.format(
640             ' '.join('--publish %s' % var for var in self.container.publish))\
641             if self.container.publish else ''
642
643         volume = '--volume {c.host_dir}:{c.guest_dir}'.format(c=self.container)\
644             if self.container.host_dir and self.container.guest_dir else ''
645
646         cmd = 'docker run '\
647             '--privileged --detach --interactive --tty --rm '\
648             '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
649             '{env} {volume} --name {container.name} {container.image} '\
650             '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
651                                container=self.container, command=command,
652                                env=env, publish=publish, volume=volume)
653
654         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
655         if int(ret) != 0:
656             raise RuntimeError('Failed to create container {c.name}'
657                                .format(c=self.container))
658
659         self.info()
660
661     def execute(self, command):
662         """Start a process inside a running container.
663
664         Runs the specified command inside the container specified by name. The
665         container has to be running already.
666
667         :param command: Command to run inside container.
668         :type command: str
669         :raises RuntimeError: If runnig the command in a container failed.
670         """
671         cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}'"\
672             .format(c=self.container, command=command)
673
674         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
675         if int(ret) != 0:
676             raise RuntimeError('Failed to execute command in container '
677                                '{c.name}.'.format(c=self.container))
678
679     def stop(self):
680         """Stop running container.
681
682         :raises RuntimeError: If stopping a container failed.
683         """
684         cmd = 'docker stop {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 stop container {c.name}.'
689                                .format(c=self.container))
690
691     def destroy(self):
692         """Remove a container.
693
694         :raises RuntimeError: If removing a container failed.
695         """
696         cmd = 'docker rm --force {c.name}'.format(c=self.container)
697
698         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
699         if int(ret) != 0:
700             raise RuntimeError('Failed to destroy container {c.name}.'
701                                .format(c=self.container))
702
703     def info(self):
704         """Return low-level information on Docker objects.
705
706         :raises RuntimeError: If getting info about a container failed.
707         """
708         cmd = 'docker inspect {c.name}'.format(c=self.container)
709
710         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
711         if int(ret) != 0:
712             raise RuntimeError('Failed to get info about container {c.name}.'
713                                .format(c=self.container))
714
715     def system_info(self):
716         """Display the docker system-wide information.
717
718         :raises RuntimeError: If displaying system information failed.
719         """
720         cmd = 'docker system info'
721
722         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
723         if int(ret) != 0:
724             raise RuntimeError('Failed to get system info.')
725
726     def is_container_present(self):
727         """Check if container is present on node.
728
729         :returns: True if container is present.
730         :rtype: bool
731         :raises RuntimeError: If getting info about a container failed.
732         """
733         cmd = 'docker ps --all --quiet --filter name={c.name}'\
734             .format(c=self.container)
735
736         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
737         if int(ret) != 0:
738             raise RuntimeError('Failed to get info about container {c.name}.'
739                                .format(c=self.container))
740         return True if stdout else False
741
742     def is_container_running(self):
743         """Check if container is running on node.
744
745         :returns: True if container is running.
746         :rtype: bool
747         :raises RuntimeError: If getting info about a container failed.
748         """
749         cmd = 'docker ps --quiet --filter name={c.name}'\
750             .format(c=self.container)
751
752         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
753         if int(ret) != 0:
754             raise RuntimeError('Failed to get info about container {c.name}.'
755                                .format(c=self.container))
756         return True if stdout else False
757
758
759 class Container(object):
760     """Container class."""
761
762     def __init__(self):
763         """Initialize Container object."""
764         pass
765
766     def __getattr__(self, attr):
767         """Get attribute custom implementation.
768
769         :param attr: Attribute to get.
770         :type attr: str
771         :returns: Attribute value or None.
772         :rtype: any
773         """
774         try:
775             return self.__dict__[attr]
776         except KeyError:
777             return None
778
779     def __setattr__(self, attr, value):
780         """Set attribute custom implementation.
781
782         :param attr: Attribute to set.
783         :param value: Value to set.
784         :type attr: str
785         :type value: any
786         """
787         try:
788             # Check if attribute exists
789             self.__dict__[attr]
790         except KeyError:
791             # Creating new attribute
792             if attr == 'node':
793                 self.__dict__['ssh'] = SSH()
794                 self.__dict__['ssh'].connect(value)
795             self.__dict__[attr] = value
796         else:
797             # Updating attribute base of type
798             if isinstance(self.__dict__[attr], list):
799                 self.__dict__[attr].append(value)
800             else:
801                 self.__dict__[attr] = value