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