CSIT-1036 Expose option to install DKMS in container
[csit.git] / resources / libraries / python / ContainerUtils.py
1 # Copyright (c) 2018 Cisco and/or its affiliates.
2 # Licensed under the Apache License, Version 2.0 (the "License");
3 # you may not use this file except in compliance with the License.
4 # You may obtain a copy of the License at:
5 #
6 #     http://www.apache.org/licenses/LICENSE-2.0
7 #
8 # Unless required by applicable law or agreed to in writing, software
9 # distributed under the License is distributed on an "AS IS" BASIS,
10 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11 # See the License for the specific language governing permissions and
12 # limitations under the License.
13
14 # Bug workaround in pylint for abstract classes.
15 # pylint: disable=W0223
16
17 """Library to manipulate Containers."""
18
19 from collections import OrderedDict, Counter
20
21 from resources.libraries.python.ssh import SSH
22 from resources.libraries.python.constants import Constants
23 from resources.libraries.python.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('{engine} is not implemented.'.
46                                       format(engine=engine))
47         self.containers = OrderedDict()
48
49     def get_container_by_name(self, name):
50         """Get container instance.
51
52         :param name: Container name.
53         :type name: str
54         :returns: Container instance.
55         :rtype: Container
56         :raises RuntimeError: If failed to get container with name.
57         """
58         try:
59             return self.containers[name]
60         except KeyError:
61             raise RuntimeError('Failed to get container with name: {name}'.
62                                format(name=name))
63
64     def construct_container(self, **kwargs):
65         """Construct container object on node with specified parameters.
66
67         :param kwargs: Key-value pairs used to construct container.
68         :param kwargs: dict
69         """
70         # Create base class
71         self.engine.initialize()
72         # Set parameters
73         for key in kwargs:
74             setattr(self.engine.container, key, kwargs[key])
75
76         # Set additional environmental variables
77         setattr(self.engine.container, 'env',
78                 'MICROSERVICE_LABEL={label}'.format(label=kwargs['name']))
79
80         # 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 "{config}" > {config_file}'.
263                      format(
264                          config='[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                          config_file=SUPERVISOR_CONF))
279         self.execute('supervisord -c {config_file}'.
280                      format(config_file=SUPERVISOR_CONF))
281
282     def install_vpp(self):
283         """Install VPP inside a container."""
284         self.execute('ln -s /dev/null /etc/sysctl.d/80-vpp.conf')
285         self.execute('apt-get update')
286         if self.container.install_dkms:
287             self.execute(
288                 'apt-get install -y dkms && '
289                 'dpkg -i --force-all {guest_dir}/install_dir/*.deb'.
290                 format(guest_dir=self.container.mnt[0].split(':')[1]))
291         else:
292             self.execute(
293                 'for i in $(ls -I \"*dkms*\" {guest_dir}/install_dir/); do '
294                 'dpkg -i --force-all {guest_dir}/install_dir/$i; done'.
295                 format(guest_dir=self.container.mnt[0].split(':')[1]))
296         self.execute('apt-get -f install -y')
297         self.execute('apt-get install -y ca-certificates')
298         self.execute('echo "{config}" >> {config_file}'.
299                      format(
300                          config='[program:vpp]\n'
301                          'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
302                          'autorestart=false\n'
303                          'redirect_stderr=true\n'
304                          'priority=1',
305                          config_file=SUPERVISOR_CONF))
306         self.execute('supervisorctl reload')
307
308     def restart_vpp(self):
309         """Restart VPP service inside a container."""
310         self.execute('supervisorctl restart vpp')
311         self.execute('cat /tmp/supervisord.log')
312
313     def create_vpp_startup_config(self,
314                                   config_filename='/etc/vpp/startup.conf'):
315         """Create base startup configuration of VPP on container.
316
317         :param config_filename: Startup configuration file name.
318         :type config_filename: str
319         """
320         cpuset_cpus = self.container.cpuset_cpus
321
322         # Create config instance
323         vpp_config = VppConfigGenerator()
324         vpp_config.set_node(self.container.node)
325         vpp_config.add_unix_cli_listen()
326         vpp_config.add_unix_nodaemon()
327         vpp_config.add_unix_exec('/tmp/running.exec')
328         # We will pop first core from list to be main core
329         vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
330         # if this is not only core in list, the rest will be used as workers.
331         if cpuset_cpus:
332             corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
333             vpp_config.add_cpu_corelist_workers(corelist_workers)
334         vpp_config.add_plugin('disable', 'dpdk_plugin.so')
335
336         self.execute('mkdir -p /etc/vpp/')
337         self.execute('echo "{c}" | tee {f}'
338                      .format(c=vpp_config.get_config_str(),
339                              f=config_filename))
340
341     def create_vpp_exec_config(self, vat_template_file, **kwargs):
342         """Create VPP exec configuration on container.
343
344         :param vat_template_file: File name of a VAT template script.
345         :param kwargs: Parameters for VAT script.
346         :type vat_template_file: str
347         :type kwargs: dict
348         """
349         vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
350                                          f=vat_template_file)
351
352         with open(vat_file_path, 'r') as template_file:
353             cmd_template = template_file.readlines()
354             for line_tmpl in cmd_template:
355                 vat_cmd = line_tmpl.format(**kwargs)
356                 self.execute('echo "{c}" >> /tmp/running.exec'
357                              .format(c=vat_cmd.replace('\n', '')))
358
359     def is_container_running(self):
360         """Check if container is running."""
361         raise NotImplementedError
362
363     def is_container_present(self):
364         """Check if container is present."""
365         raise NotImplementedError
366
367     def _configure_cgroup(self, name):
368         """Configure the control group associated with a container.
369
370         By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
371         container is initialized a new cgroup /docker or /lxc is created under
372         cpuset parent tree. This newly created cgroup is inheriting parent
373         setting for cpu/mem exclusive parameter and thus cannot be overriden
374         within /docker or /lxc cgroup. This function is supposed to set cgroups
375         to allow coexistence of both engines.
376
377         :param name: Name of cgroup.
378         :type name: str
379         :raises RuntimeError: If applying cgroup settings via cgset failed.
380         """
381         ret, _, _ = self.container.ssh.exec_command_sudo(
382             'cgset -r cpuset.cpu_exclusive=0 /')
383         if int(ret) != 0:
384             raise RuntimeError('Failed to apply cgroup settings.')
385
386         ret, _, _ = self.container.ssh.exec_command_sudo(
387             'cgset -r cpuset.mem_exclusive=0 /')
388         if int(ret) != 0:
389             raise RuntimeError('Failed to apply cgroup settings.')
390
391         ret, _, _ = self.container.ssh.exec_command_sudo(
392             'cgcreate -g cpuset:/{name}'.format(name=name))
393         if int(ret) != 0:
394             raise RuntimeError('Failed to copy cgroup settings from root.')
395
396         ret, _, _ = self.container.ssh.exec_command_sudo(
397             'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
398         if int(ret) != 0:
399             raise RuntimeError('Failed to apply cgroup settings.')
400
401         ret, _, _ = self.container.ssh.exec_command_sudo(
402             'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
403         if int(ret) != 0:
404             raise RuntimeError('Failed to apply cgroup settings.')
405
406
407 class LXC(ContainerEngine):
408     """LXC implementation."""
409
410     def __init__(self):
411         """Initialize LXC object."""
412         super(LXC, self).__init__()
413
414     def acquire(self, force=True):
415         """Acquire a privileged system object where configuration is stored.
416
417         :param force: If a container exists, destroy it and create a new
418             container.
419         :type force: bool
420         :raises RuntimeError: If creating the container or writing the container
421             config fails.
422         """
423         if self.is_container_present():
424             if force:
425                 self.destroy()
426             else:
427                 return
428
429         image = self.container.image if self.container.image else\
430             "-d ubuntu -r xenial -a amd64"
431
432         cmd = 'lxc-create -t download --name {c.name} -- {image} '\
433             '--no-validate'.format(c=self.container, image=image)
434
435         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
436         if int(ret) != 0:
437             raise RuntimeError('Failed to create container.')
438
439         self._configure_cgroup('lxc')
440
441     def create(self):
442         """Create/deploy an application inside a container on system.
443
444         :raises RuntimeError: If creating the container fails.
445         """
446         if self.container.mnt:
447             for mount in self.container.mnt:
448                 host_dir, guest_dir = mount.split(':')
449                 entry = 'lxc.mount.entry = {host_dir} '\
450                     '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
451                     'bind,create=dir 0 0'.format(c=self.container,
452                                                  host_dir=host_dir,
453                                                  guest_dir=guest_dir)
454                 ret, _, _ = self.container.ssh.exec_command_sudo(
455                     "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
456                     format(e=entry, c=self.container))
457                 if int(ret) != 0:
458                     raise RuntimeError('Failed to write {c.name} config.'
459                                        .format(c=self.container))
460
461         cpuset_cpus = '{0}'.format(
462             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
463             if self.container.cpuset_cpus else ''
464
465         ret, _, _ = self.container.ssh.exec_command_sudo(
466             'lxc-start --name {c.name} --daemon'.
467             format(c=self.container))
468         if int(ret) != 0:
469             raise RuntimeError('Failed to start container {c.name}.'.
470                                format(c=self.container))
471         self._lxc_wait('RUNNING')
472
473         # Workaround for LXC to be able to allocate all cpus including isolated.
474         ret, _, _ = self.container.ssh.exec_command_sudo(
475             'cgset --copy-from / lxc/')
476         if int(ret) != 0:
477             raise RuntimeError('Failed to copy cgroup to LXC')
478
479         ret, _, _ = self.container.ssh.exec_command_sudo(
480             'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
481             format(c=self.container, cpus=cpuset_cpus))
482         if int(ret) != 0:
483             raise RuntimeError('Failed to set cpuset.cpus to container '
484                                '{c.name}.'.format(c=self.container))
485
486     def execute(self, command):
487         """Start a process inside a running container.
488
489         Runs the specified command inside the container specified by name. The
490         container has to be running already.
491
492         :param command: Command to run inside container.
493         :type command: str
494         :raises RuntimeError: If running the command failed.
495         """
496         env = '--keep-env {0}'.format(
497             ' '.join('--set-var %s' % env for env in self.container.env))\
498             if self.container.env else ''
499
500         cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}; "\
501             "exit $?'".format(env=env, c=self.container, command=command)
502
503         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
504         if int(ret) != 0:
505             raise RuntimeError('Failed to run command inside container '
506                                '{c.name}.'.format(c=self.container))
507
508     def stop(self):
509         """Stop a container.
510
511         :raises RuntimeError: If stopping the container failed.
512         """
513         cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
514
515         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
516         if int(ret) != 0:
517             raise RuntimeError('Failed to stop container {c.name}.'
518                                .format(c=self.container))
519         self._lxc_wait('STOPPED|FROZEN')
520
521     def destroy(self):
522         """Destroy a container.
523
524         :raises RuntimeError: If destroying container failed.
525         """
526         cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
527
528         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
529         if int(ret) != 0:
530             raise RuntimeError('Failed to destroy container {c.name}.'
531                                .format(c=self.container))
532
533     def info(self):
534         """Query and shows information about a container.
535
536         :raises RuntimeError: If getting info about a container failed.
537         """
538         cmd = 'lxc-info --name {c.name}'.format(c=self.container)
539
540         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
541         if int(ret) != 0:
542             raise RuntimeError('Failed to get info about container {c.name}.'
543                                .format(c=self.container))
544
545     def system_info(self):
546         """Check the current kernel for LXC support.
547
548         :raises RuntimeError: If checking LXC support failed.
549         """
550         cmd = 'lxc-checkconfig'
551
552         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
553         if int(ret) != 0:
554             raise RuntimeError('Failed to check LXC support.')
555
556     def is_container_running(self):
557         """Check if container is running on node.
558
559         :returns: True if container is running.
560         :rtype: bool
561         :raises RuntimeError: If getting info about a container failed.
562         """
563         cmd = 'lxc-info --no-humanize --state --name {c.name}'\
564             .format(c=self.container)
565
566         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
567         if int(ret) != 0:
568             raise RuntimeError('Failed to get info about container {c.name}.'
569                                .format(c=self.container))
570         return True if 'RUNNING' in stdout else False
571
572     def is_container_present(self):
573         """Check if container is existing on node.
574
575         :returns: True if container is present.
576         :rtype: bool
577         :raises RuntimeError: If getting info about a container failed.
578         """
579         cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
580
581         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
582         return False if int(ret) else True
583
584     def _lxc_wait(self, state):
585         """Wait for a specific container state.
586
587         :param state: Specify the container state(s) to wait for.
588         :type state: str
589         :raises RuntimeError: If waiting for state of a container failed.
590         """
591         cmd = 'lxc-wait --name {c.name} --state "{s}"'\
592             .format(c=self.container, s=state)
593
594         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
595         if int(ret) != 0:
596             raise RuntimeError('Failed to wait for state "{s}" of container '
597                                '{c.name}.'.format(s=state, c=self.container))
598
599
600 class Docker(ContainerEngine):
601     """Docker implementation."""
602
603     def __init__(self):
604         """Initialize Docker object."""
605         super(Docker, self).__init__()
606
607     def acquire(self, force=True):
608         """Pull an image or a repository from a registry.
609
610         :param force: Destroy a container if exists.
611         :type force: bool
612         :raises RuntimeError: If pulling a container failed.
613         """
614         if self.is_container_present():
615             if force:
616                 self.destroy()
617             else:
618                 return
619
620         cmd = 'docker pull {c.image}'.format(c=self.container)
621
622         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
623         if int(ret) != 0:
624             raise RuntimeError('Failed to create container {c.name}.'
625                                .format(c=self.container))
626         self._configure_cgroup('docker')
627
628     def create(self):
629         """Create/deploy container.
630
631         :raises RuntimeError: If creating a container failed.
632         """
633         cpuset_cpus = '--cpuset-cpus={0}'.format(
634             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
635             if self.container.cpuset_cpus else ''
636
637         cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
638             if self.container.cpuset_mems is not None else ''
639         # Temporary workaround - disabling due to bug in memif
640         cpuset_mems = ''
641
642         env = '{0}'.format(
643             ' '.join('--env %s' % env for env in self.container.env))\
644             if self.container.env else ''
645
646         command = '{0}'.format(self.container.command)\
647             if self.container.command else ''
648
649         publish = '{0}'.format(
650             ' '.join('--publish %s' % var for var in self.container.publish))\
651             if self.container.publish else ''
652
653         volume = '{0}'.format(
654             ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
655             if self.container.mnt else ''
656
657         cmd = 'docker run '\
658             '--privileged --detach --interactive --tty --rm '\
659             '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
660             '{env} {volume} --name {container.name} {container.image} '\
661             '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
662                                container=self.container, command=command,
663                                env=env, publish=publish, volume=volume)
664
665         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
666         if int(ret) != 0:
667             raise RuntimeError('Failed to create container {c.name}'
668                                .format(c=self.container))
669
670         self.info()
671
672     def execute(self, command):
673         """Start a process inside a running container.
674
675         Runs the specified command inside the container specified by name. The
676         container has to be running already.
677
678         :param command: Command to run inside container.
679         :type command: str
680         :raises RuntimeError: If runnig the command in a container failed.
681         """
682         cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}; "\
683             "exit $?'".format(c=self.container, command=command)
684
685         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
686         if int(ret) != 0:
687             raise RuntimeError('Failed to execute command in container '
688                                '{c.name}.'.format(c=self.container))
689
690     def stop(self):
691         """Stop running container.
692
693         :raises RuntimeError: If stopping a container failed.
694         """
695         cmd = 'docker stop {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 stop container {c.name}.'
700                                .format(c=self.container))
701
702     def destroy(self):
703         """Remove a container.
704
705         :raises RuntimeError: If removing a container failed.
706         """
707         cmd = 'docker rm --force {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 destroy container {c.name}.'
712                                .format(c=self.container))
713
714     def info(self):
715         """Return low-level information on Docker objects.
716
717         :raises RuntimeError: If getting info about a container failed.
718         """
719         cmd = 'docker inspect {c.name}'.format(c=self.container)
720
721         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
722         if int(ret) != 0:
723             raise RuntimeError('Failed to get info about container {c.name}.'
724                                .format(c=self.container))
725
726     def system_info(self):
727         """Display the docker system-wide information.
728
729         :raises RuntimeError: If displaying system information failed.
730         """
731         cmd = 'docker system info'
732
733         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
734         if int(ret) != 0:
735             raise RuntimeError('Failed to get system info.')
736
737     def is_container_present(self):
738         """Check if container is present on node.
739
740         :returns: True if container is present.
741         :rtype: bool
742         :raises RuntimeError: If getting info about a container failed.
743         """
744         cmd = 'docker ps --all --quiet --filter name={c.name}'\
745             .format(c=self.container)
746
747         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
748         if int(ret) != 0:
749             raise RuntimeError('Failed to get info about container {c.name}.'
750                                .format(c=self.container))
751         return True if stdout else False
752
753     def is_container_running(self):
754         """Check if container is running on node.
755
756         :returns: True if container is running.
757         :rtype: bool
758         :raises RuntimeError: If getting info about a container failed.
759         """
760         cmd = 'docker ps --quiet --filter name={c.name}'\
761             .format(c=self.container)
762
763         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
764         if int(ret) != 0:
765             raise RuntimeError('Failed to get info about container {c.name}.'
766                                .format(c=self.container))
767         return True if stdout else False
768
769
770 class Container(object):
771     """Container class."""
772
773     def __init__(self):
774         """Initialize Container object."""
775         pass
776
777     def __getattr__(self, attr):
778         """Get attribute custom implementation.
779
780         :param attr: Attribute to get.
781         :type attr: str
782         :returns: Attribute value or None.
783         :rtype: any
784         """
785         try:
786             return self.__dict__[attr]
787         except KeyError:
788             return None
789
790     def __setattr__(self, attr, value):
791         """Set attribute custom implementation.
792
793         :param attr: Attribute to set.
794         :param value: Value to set.
795         :type attr: str
796         :type value: any
797         """
798         try:
799             # Check if attribute exists
800             self.__dict__[attr]
801         except KeyError:
802             # Creating new attribute
803             if attr == 'node':
804                 self.__dict__['ssh'] = SSH()
805                 self.__dict__['ssh'].connect(value)
806             self.__dict__[attr] = value
807         else:
808             # Updating attribute base of type
809             if isinstance(self.__dict__[attr], list):
810                 self.__dict__[attr].append(value)
811             else:
812                 self.__dict__[attr] = value