CSIT-1193 De-duplicate bootstrap scripts into one
[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.topology import Topology
25 from resources.libraries.python.VppConfigGenerator import VppConfigGenerator
26
27
28 __all__ = ["ContainerManager", "ContainerEngine", "LXC", "Docker", "Container"]
29
30 SUPERVISOR_CONF = '/etc/supervisord.conf'
31
32
33 class ContainerManager(object):
34     """Container lifecycle management class."""
35
36     def __init__(self, engine):
37         """Initialize Container Manager class.
38
39         :param engine: Container technology used (LXC/Docker/...).
40         :type engine: str
41         :raises NotImplementedError: If container technology is not implemented.
42         """
43         try:
44             self.engine = globals()[engine]()
45         except KeyError:
46             raise NotImplementedError('{engine} is not implemented.'.
47                                       format(engine=engine))
48         self.containers = OrderedDict()
49
50     def get_container_by_name(self, name):
51         """Get container instance.
52
53         :param name: Container name.
54         :type name: str
55         :returns: Container instance.
56         :rtype: Container
57         :raises RuntimeError: If failed to get container with name.
58         """
59         try:
60             return self.containers[name]
61         except KeyError:
62             raise RuntimeError('Failed to get container with name: {name}'.
63                                format(name=name))
64
65     def construct_container(self, **kwargs):
66         """Construct container object on node with specified parameters.
67
68         :param kwargs: Key-value pairs used to construct container.
69         :param kwargs: dict
70         """
71         # Create base class
72         self.engine.initialize()
73         # Set parameters
74         for key in kwargs:
75             setattr(self.engine.container, key, kwargs[key])
76
77         # Set additional environmental variables
78         setattr(self.engine.container, 'env',
79                 'MICROSERVICE_LABEL={label}'.format(label=kwargs['name']))
80
81         # Set cpuset.cpus cgroup
82         skip_cnt = kwargs['cpu_skip']
83         smt_used = CpuUtils.is_smt_enabled(kwargs['node']['cpuinfo'])
84         if not kwargs['cpu_shared']:
85             skip_cnt += kwargs['i'] * kwargs['cpu_count']
86         self.engine.container.cpuset_cpus = \
87             CpuUtils.cpu_slice_of_list_per_node(node=kwargs['node'],
88                                                 cpu_node=kwargs['cpuset_mems'],
89                                                 skip_cnt=skip_cnt,
90                                                 cpu_cnt=1,
91                                                 smt_used=False) \
92             + \
93             CpuUtils.cpu_slice_of_list_per_node(node=kwargs['node'],
94                                                 cpu_node=kwargs['cpuset_mems'],
95                                                 skip_cnt=skip_cnt+1,
96                                                 cpu_cnt=kwargs['cpu_count']-1,
97                                                 smt_used=smt_used)
98
99         # Store container instance
100         self.containers[kwargs['name']] = self.engine.container
101
102     def construct_containers(self, **kwargs):
103         """Construct 1..N container(s) on node with specified name.
104
105         Ordinal number is automatically added to the name of container as
106         suffix.
107
108         :param kwargs: Named parameters.
109         :param kwargs: dict
110         """
111         name = kwargs['name']
112         for i in range(kwargs['count']):
113             # Name will contain ordinal suffix
114             kwargs['name'] = ''.join([name, str(i+1)])
115             # Create container
116             self.construct_container(i=i, **kwargs)
117
118     def acquire_all_containers(self):
119         """Acquire all containers."""
120         for container in self.containers:
121             self.engine.container = self.containers[container]
122             self.engine.acquire()
123
124     def build_all_containers(self):
125         """Build all containers."""
126         for container in self.containers:
127             self.engine.container = self.containers[container]
128             self.engine.build()
129
130     def create_all_containers(self):
131         """Create all containers."""
132         for container in self.containers:
133             self.engine.container = self.containers[container]
134             self.engine.create()
135
136     def execute_on_container(self, name, command):
137         """Execute command on container with name.
138
139         :param name: Container name.
140         :param command: Command to execute.
141         :type name: str
142         :type command: str
143         """
144         self.engine.container = self.get_container_by_name(name)
145         self.engine.execute(command)
146
147     def execute_on_all_containers(self, command):
148         """Execute command on all containers.
149
150         :param command: Command to execute.
151         :type command: str
152         """
153         for container in self.containers:
154             self.engine.container = self.containers[container]
155             self.engine.execute(command)
156
157     def install_vpp_in_all_containers(self):
158         """Install VPP into all containers."""
159         for container in self.containers:
160             self.engine.container = self.containers[container]
161             # We need to install supervisor client/server system to control VPP
162             # as a service
163             self.engine.install_supervisor()
164             self.engine.install_vpp()
165             self.engine.restart_vpp()
166
167     def restart_vpp_in_all_containers(self):
168         """Restart VPP on all containers."""
169         for container in self.containers:
170             self.engine.container = self.containers[container]
171             self.engine.restart_vpp()
172
173     def configure_vpp_in_all_containers(self, chain_topology,
174                                         dut1_if=None, dut2_if=None):
175         """Configure VPP in all containers.
176
177         :param chain_topology: Topology used for chaining containers can be
178             chain or cross_horiz. Chain topology is using 1 memif pair per
179             container. Cross_horiz topology is using 1 memif and 1 physical
180             interface in container (only single container can be configured).
181         :param dut1_if: Interface on DUT1 directly connected to DUT2.
182         :param dut2_if: Interface on DUT2 directly connected to DUT1.
183         :type container_topology: str
184         :type dut1_if: str
185         :type dut2_if: str
186         """
187         # Count number of DUTs based on node's host information
188         dut_cnt = len(Counter([self.containers[container].node['host']
189                                for container in self.containers]))
190         mod = len(self.containers)/dut_cnt
191         container_vat_template = 'memif_create_{topology}.vat'.format(
192             topology=chain_topology)
193
194         if chain_topology == 'chain':
195             for i, container in enumerate(self.containers):
196                 mid1 = i % mod + 1
197                 mid2 = i % mod + 1
198                 sid1 = i % mod * 2 + 1
199                 sid2 = i % mod * 2 + 2
200                 self.engine.container = self.containers[container]
201                 self.engine.create_vpp_startup_config()
202                 self.engine.create_vpp_exec_config(container_vat_template, \
203                     mid1=mid1, mid2=mid2, sid1=sid1, sid2=sid2, \
204                     socket1='memif-{c.name}-{sid}'. \
205                     format(c=self.engine.container, sid=sid1), \
206                     socket2='memif-{c.name}-{sid}'. \
207                     format(c=self.engine.container, sid=sid2))
208         elif chain_topology == 'cross_horiz':
209             if mod > 1:
210                 raise RuntimeError('Container chain topology {topology} '
211                                    'supports only single container.'.
212                                    format(topology=chain_topology))
213             for i, container in enumerate(self.containers):
214                 mid1 = i % mod + 1
215                 sid1 = i % mod * 2 + 1
216                 self.engine.container = self.containers[container]
217                 if 'DUT1' in self.engine.container.name:
218                     if_pci = Topology.get_interface_pci_addr( \
219                         self.engine.container.node, dut1_if)
220                     if_name = Topology.get_interface_name( \
221                         self.engine.container.node, dut1_if)
222                 if 'DUT2' in self.engine.container.name:
223                     if_pci = Topology.get_interface_pci_addr( \
224                         self.engine.container.node, dut2_if)
225                     if_name = Topology.get_interface_name( \
226                         self.engine.container.node, dut2_if)
227                 self.engine.create_vpp_startup_config_dpdk_dev(if_pci)
228                 self.engine.create_vpp_exec_config(container_vat_template, \
229                     mid1=mid1, sid1=sid1, if_name=if_name, \
230                     socket1='memif-{c.name}-{sid}'. \
231                     format(c=self.engine.container, sid=sid1))
232         else:
233             raise RuntimeError('Container topology {topology} not implemented'.
234                                format(topology=chain_topology))
235
236     def stop_all_containers(self):
237         """Stop all containers."""
238         for container in self.containers:
239             self.engine.container = self.containers[container]
240             self.engine.stop()
241
242     def destroy_all_containers(self):
243         """Destroy all containers."""
244         for container in self.containers:
245             self.engine.container = self.containers[container]
246             self.engine.destroy()
247
248
249 class ContainerEngine(object):
250     """Abstract class for container engine."""
251
252     def __init__(self):
253         """Init ContainerEngine object."""
254         self.container = None
255
256     def initialize(self):
257         """Initialize container object."""
258         self.container = Container()
259
260     def acquire(self, force):
261         """Acquire/download container.
262
263         :param force: Destroy a container if exists and create.
264         :type force: bool
265         """
266         raise NotImplementedError
267
268     def build(self):
269         """Build container (compile)."""
270         raise NotImplementedError
271
272     def create(self):
273         """Create/deploy container."""
274         raise NotImplementedError
275
276     def execute(self, command):
277         """Execute process inside container.
278
279         :param command: Command to run inside container.
280         :type command: str
281         """
282         raise NotImplementedError
283
284     def stop(self):
285         """Stop container."""
286         raise NotImplementedError
287
288     def destroy(self):
289         """Destroy/remove container."""
290         raise NotImplementedError
291
292     def info(self):
293         """Info about container."""
294         raise NotImplementedError
295
296     def system_info(self):
297         """System info."""
298         raise NotImplementedError
299
300     def install_supervisor(self):
301         """Install supervisord inside a container."""
302         self.execute('sleep 3')
303         self.execute('apt-get update')
304         self.execute('apt-get install -y supervisor')
305         self.execute('echo "{config}" > {config_file}'.
306                      format(
307                          config='[unix_http_server]\n'
308                          'file  = /tmp/supervisor.sock\n\n'
309                          '[rpcinterface:supervisor]\n'
310                          'supervisor.rpcinterface_factory = '
311                          'supervisor.rpcinterface:make_main_rpcinterface\n\n'
312                          '[supervisorctl]\n'
313                          'serverurl = unix:///tmp/supervisor.sock\n\n'
314                          '[supervisord]\n'
315                          'pidfile = /tmp/supervisord.pid\n'
316                          'identifier = supervisor\n'
317                          'directory = /tmp\n'
318                          'logfile=/tmp/supervisord.log\n'
319                          'loglevel=debug\n'
320                          'nodaemon=false\n\n',
321                          config_file=SUPERVISOR_CONF))
322         self.execute('supervisord -c {config_file}'.
323                      format(config_file=SUPERVISOR_CONF))
324
325     def install_vpp(self):
326         """Install VPP inside a container."""
327         self.execute('ln -s /dev/null /etc/sysctl.d/80-vpp.conf')
328         self.execute('apt-get update')
329         if self.container.install_dkms:
330             self.execute(
331                 'apt-get install -y dkms && '
332                 'dpkg -i --force-all '
333                 '{guest_dir}/openvpp-testing/download_dir/*.deb'.
334                 format(guest_dir=self.container.mnt[0].split(':')[1]))
335         else:
336             self.execute(
337                 'for i in $(ls -I \"*dkms*\" '
338                 '{guest_dir}/openvpp-testing/download_dir/); do '
339                 'dpkg -i --force-all '
340                 '{guest_dir}/openvpp-testing/download_dir/$i; done'.
341                 format(guest_dir=self.container.mnt[0].split(':')[1]))
342         self.execute('apt-get -f install -y')
343         self.execute('apt-get install -y ca-certificates')
344         self.execute('echo "{config}" >> {config_file}'.
345                      format(
346                          config='[program:vpp]\n'
347                          'command=/usr/bin/vpp -c /etc/vpp/startup.conf\n'
348                          'autorestart=false\n'
349                          'redirect_stderr=true\n'
350                          'priority=1',
351                          config_file=SUPERVISOR_CONF))
352         self.execute('supervisorctl reload')
353         self.execute('supervisorctl restart vpp')
354
355     def restart_vpp(self):
356         """Restart VPP service inside a container."""
357         self.execute('supervisorctl restart vpp')
358         self.execute('cat /tmp/supervisord.log')
359
360     def create_base_vpp_startup_config(self):
361         """Create base startup configuration of VPP on container.
362
363         :returns: Base VPP startup configuration.
364         :rtype: VppConfigGenerator
365         """
366         cpuset_cpus = self.container.cpuset_cpus
367
368         # Create config instance
369         vpp_config = VppConfigGenerator()
370         vpp_config.set_node(self.container.node)
371         vpp_config.add_unix_cli_listen()
372         vpp_config.add_unix_nodaemon()
373         vpp_config.add_unix_exec('/tmp/running.exec')
374         # We will pop first core from list to be main core
375         vpp_config.add_cpu_main_core(str(cpuset_cpus.pop(0)))
376         # if this is not only core in list, the rest will be used as workers.
377         if cpuset_cpus:
378             corelist_workers = ','.join(str(cpu) for cpu in cpuset_cpus)
379             vpp_config.add_cpu_corelist_workers(corelist_workers)
380
381         return vpp_config
382
383     def create_vpp_startup_config(self):
384         """Create startup configuration of VPP without DPDK on container.
385         """
386         vpp_config = self.create_base_vpp_startup_config()
387         vpp_config.add_plugin('disable', 'dpdk_plugin.so')
388
389         # Apply configuration
390         self.execute('mkdir -p /etc/vpp/')
391         self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
392                      .format(config=vpp_config.get_config_str()))
393
394     def create_vpp_startup_config_dpdk_dev(self, *devices):
395         """Create startup configuration of VPP with DPDK on container.
396
397         :param devices: List of PCI devices to add.
398         :type devices: list
399         """
400         vpp_config = self.create_base_vpp_startup_config()
401         vpp_config.add_dpdk_dev(*devices)
402         vpp_config.add_dpdk_no_tx_checksum_offload()
403         vpp_config.add_dpdk_log_level('debug')
404         vpp_config.add_plugin('disable', 'default')
405         vpp_config.add_plugin('enable', 'dpdk_plugin.so')
406         vpp_config.add_plugin('enable', 'memif_plugin.so')
407
408         # Apply configuration
409         self.execute('mkdir -p /etc/vpp/')
410         self.execute('echo "{config}" | tee /etc/vpp/startup.conf'
411                      .format(config=vpp_config.get_config_str()))
412
413     def create_vpp_exec_config(self, vat_template_file, **kwargs):
414         """Create VPP exec configuration on container.
415
416         :param vat_template_file: File name of a VAT template script.
417         :param kwargs: Parameters for VAT script.
418         :type vat_template_file: str
419         :type kwargs: dict
420         """
421         vat_file_path = '{p}/{f}'.format(p=Constants.RESOURCES_TPL_VAT,
422                                          f=vat_template_file)
423
424         with open(vat_file_path, 'r') as template_file:
425             cmd_template = template_file.readlines()
426             for line_tmpl in cmd_template:
427                 vat_cmd = line_tmpl.format(**kwargs)
428                 self.execute('echo "{c}" >> /tmp/running.exec'
429                              .format(c=vat_cmd.replace('\n', '')))
430
431     def is_container_running(self):
432         """Check if container is running."""
433         raise NotImplementedError
434
435     def is_container_present(self):
436         """Check if container is present."""
437         raise NotImplementedError
438
439     def _configure_cgroup(self, name):
440         """Configure the control group associated with a container.
441
442         By default the cpuset cgroup is using exclusive CPU/MEM. When Docker/LXC
443         container is initialized a new cgroup /docker or /lxc is created under
444         cpuset parent tree. This newly created cgroup is inheriting parent
445         setting for cpu/mem exclusive parameter and thus cannot be overriden
446         within /docker or /lxc cgroup. This function is supposed to set cgroups
447         to allow coexistence of both engines.
448
449         :param name: Name of cgroup.
450         :type name: str
451         :raises RuntimeError: If applying cgroup settings via cgset failed.
452         """
453         ret, _, _ = self.container.ssh.exec_command_sudo(
454             'cgset -r cpuset.cpu_exclusive=0 /')
455         if int(ret) != 0:
456             raise RuntimeError('Failed to apply cgroup settings.')
457
458         ret, _, _ = self.container.ssh.exec_command_sudo(
459             'cgset -r cpuset.mem_exclusive=0 /')
460         if int(ret) != 0:
461             raise RuntimeError('Failed to apply cgroup settings.')
462
463         ret, _, _ = self.container.ssh.exec_command_sudo(
464             'cgcreate -g cpuset:/{name}'.format(name=name))
465         if int(ret) != 0:
466             raise RuntimeError('Failed to copy cgroup settings from root.')
467
468         ret, _, _ = self.container.ssh.exec_command_sudo(
469             'cgset -r cpuset.cpu_exclusive=0 /{name}'.format(name=name))
470         if int(ret) != 0:
471             raise RuntimeError('Failed to apply cgroup settings.')
472
473         ret, _, _ = self.container.ssh.exec_command_sudo(
474             'cgset -r cpuset.mem_exclusive=0 /{name}'.format(name=name))
475         if int(ret) != 0:
476             raise RuntimeError('Failed to apply cgroup settings.')
477
478
479 class LXC(ContainerEngine):
480     """LXC implementation."""
481
482     def __init__(self):
483         """Initialize LXC object."""
484         super(LXC, self).__init__()
485
486     def acquire(self, force=True):
487         """Acquire a privileged system object where configuration is stored.
488
489         :param force: If a container exists, destroy it and create a new
490             container.
491         :type force: bool
492         :raises RuntimeError: If creating the container or writing the container
493             config fails.
494         """
495         if self.is_container_present():
496             if force:
497                 self.destroy()
498             else:
499                 return
500
501         image = self.container.image if self.container.image else\
502             "-d ubuntu -r xenial -a amd64"
503
504         cmd = 'lxc-create -t download --name {c.name} -- {image} '\
505             '--no-validate'.format(c=self.container, image=image)
506
507         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
508         if int(ret) != 0:
509             raise RuntimeError('Failed to create container.')
510
511         self._configure_cgroup('lxc')
512
513     def create(self):
514         """Create/deploy an application inside a container on system.
515
516         :raises RuntimeError: If creating the container fails.
517         """
518         if self.container.mnt:
519             for mount in self.container.mnt:
520                 host_dir, guest_dir = mount.split(':')
521                 entry = 'lxc.mount.entry = {host_dir} '\
522                     '/var/lib/lxc/{c.name}/rootfs{guest_dir} none ' \
523                     'bind,create=dir 0 0'.format(c=self.container,
524                                                  host_dir=host_dir,
525                                                  guest_dir=guest_dir)
526                 ret, _, _ = self.container.ssh.exec_command_sudo(
527                     "sh -c 'echo \"{e}\" >> /var/lib/lxc/{c.name}/config'".
528                     format(e=entry, c=self.container))
529                 if int(ret) != 0:
530                     raise RuntimeError('Failed to write {c.name} config.'
531                                        .format(c=self.container))
532
533         cpuset_cpus = '{0}'.format(
534             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
535             if self.container.cpuset_cpus else ''
536
537         ret, _, _ = self.container.ssh.exec_command_sudo(
538             'lxc-start --name {c.name} --daemon'.
539             format(c=self.container))
540         if int(ret) != 0:
541             raise RuntimeError('Failed to start container {c.name}.'.
542                                format(c=self.container))
543         self._lxc_wait('RUNNING')
544
545         # Workaround for LXC to be able to allocate all cpus including isolated.
546         ret, _, _ = self.container.ssh.exec_command_sudo(
547             'cgset --copy-from / lxc/')
548         if int(ret) != 0:
549             raise RuntimeError('Failed to copy cgroup to LXC')
550
551         ret, _, _ = self.container.ssh.exec_command_sudo(
552             'lxc-cgroup --name {c.name} cpuset.cpus {cpus}'.
553             format(c=self.container, cpus=cpuset_cpus))
554         if int(ret) != 0:
555             raise RuntimeError('Failed to set cpuset.cpus to container '
556                                '{c.name}.'.format(c=self.container))
557
558     def execute(self, command):
559         """Start a process inside a running container.
560
561         Runs the specified command inside the container specified by name. The
562         container has to be running already.
563
564         :param command: Command to run inside container.
565         :type command: str
566         :raises RuntimeError: If running the command failed.
567         """
568         env = '--keep-env {0}'.format(
569             ' '.join('--set-var %s' % env for env in self.container.env))\
570             if self.container.env else ''
571
572         cmd = "lxc-attach {env} --name {c.name} -- /bin/sh -c '{command}; "\
573             "exit $?'".format(env=env, c=self.container, command=command)
574
575         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
576         if int(ret) != 0:
577             raise RuntimeError('Failed to run command inside container '
578                                '{c.name}.'.format(c=self.container))
579
580     def stop(self):
581         """Stop a container.
582
583         :raises RuntimeError: If stopping the container failed.
584         """
585         cmd = 'lxc-stop --name {c.name}'.format(c=self.container)
586
587         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
588         if int(ret) != 0:
589             raise RuntimeError('Failed to stop container {c.name}.'
590                                .format(c=self.container))
591         self._lxc_wait('STOPPED|FROZEN')
592
593     def destroy(self):
594         """Destroy a container.
595
596         :raises RuntimeError: If destroying container failed.
597         """
598         cmd = 'lxc-destroy --force --name {c.name}'.format(c=self.container)
599
600         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
601         if int(ret) != 0:
602             raise RuntimeError('Failed to destroy container {c.name}.'
603                                .format(c=self.container))
604
605     def info(self):
606         """Query and shows information about a container.
607
608         :raises RuntimeError: If getting info about a container failed.
609         """
610         cmd = 'lxc-info --name {c.name}'.format(c=self.container)
611
612         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
613         if int(ret) != 0:
614             raise RuntimeError('Failed to get info about container {c.name}.'
615                                .format(c=self.container))
616
617     def system_info(self):
618         """Check the current kernel for LXC support.
619
620         :raises RuntimeError: If checking LXC support failed.
621         """
622         cmd = 'lxc-checkconfig'
623
624         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
625         if int(ret) != 0:
626             raise RuntimeError('Failed to check LXC support.')
627
628     def is_container_running(self):
629         """Check if container is running on node.
630
631         :returns: True if container is running.
632         :rtype: bool
633         :raises RuntimeError: If getting info about a container failed.
634         """
635         cmd = 'lxc-info --no-humanize --state --name {c.name}'\
636             .format(c=self.container)
637
638         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
639         if int(ret) != 0:
640             raise RuntimeError('Failed to get info about container {c.name}.'
641                                .format(c=self.container))
642         return True if 'RUNNING' in stdout else False
643
644     def is_container_present(self):
645         """Check if container is existing on node.
646
647         :returns: True if container is present.
648         :rtype: bool
649         :raises RuntimeError: If getting info about a container failed.
650         """
651         cmd = 'lxc-info --no-humanize --name {c.name}'.format(c=self.container)
652
653         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
654         return False if int(ret) else True
655
656     def _lxc_wait(self, state):
657         """Wait for a specific container state.
658
659         :param state: Specify the container state(s) to wait for.
660         :type state: str
661         :raises RuntimeError: If waiting for state of a container failed.
662         """
663         cmd = 'lxc-wait --name {c.name} --state "{s}"'\
664             .format(c=self.container, s=state)
665
666         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
667         if int(ret) != 0:
668             raise RuntimeError('Failed to wait for state "{s}" of container '
669                                '{c.name}.'.format(s=state, c=self.container))
670
671
672 class Docker(ContainerEngine):
673     """Docker implementation."""
674
675     def __init__(self):
676         """Initialize Docker object."""
677         super(Docker, self).__init__()
678
679     def acquire(self, force=True):
680         """Pull an image or a repository from a registry.
681
682         :param force: Destroy a container if exists.
683         :type force: bool
684         :raises RuntimeError: If pulling a container failed.
685         """
686         if self.is_container_present():
687             if force:
688                 self.destroy()
689             else:
690                 return
691
692         cmd = 'docker pull {c.image}'.format(c=self.container)
693
694         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=1800)
695         if int(ret) != 0:
696             raise RuntimeError('Failed to create container {c.name}.'
697                                .format(c=self.container))
698         self._configure_cgroup('docker')
699
700     def create(self):
701         """Create/deploy container.
702
703         :raises RuntimeError: If creating a container failed.
704         """
705         cpuset_cpus = '--cpuset-cpus={0}'.format(
706             ','.join('%s' % cpu for cpu in self.container.cpuset_cpus))\
707             if self.container.cpuset_cpus else ''
708
709         cpuset_mems = '--cpuset-mems={0}'.format(self.container.cpuset_mems)\
710             if self.container.cpuset_mems is not None else ''
711         # Temporary workaround - disabling due to bug in memif
712         cpuset_mems = ''
713
714         env = '{0}'.format(
715             ' '.join('--env %s' % env for env in self.container.env))\
716             if self.container.env else ''
717
718         command = '{0}'.format(self.container.command)\
719             if self.container.command else ''
720
721         publish = '{0}'.format(
722             ' '.join('--publish %s' % var for var in self.container.publish))\
723             if self.container.publish else ''
724
725         volume = '{0}'.format(
726             ' '.join('--volume %s' % mnt for mnt in self.container.mnt))\
727             if self.container.mnt else ''
728
729         cmd = 'docker run '\
730             '--privileged --detach --interactive --tty --rm '\
731             '--cgroup-parent docker {cpuset_cpus} {cpuset_mems} {publish} '\
732             '{env} {volume} --name {container.name} {container.image} '\
733             '{command}'.format(cpuset_cpus=cpuset_cpus, cpuset_mems=cpuset_mems,
734                                container=self.container, command=command,
735                                env=env, publish=publish, volume=volume)
736
737         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
738         if int(ret) != 0:
739             raise RuntimeError('Failed to create container {c.name}'
740                                .format(c=self.container))
741
742         self.info()
743
744     def execute(self, command):
745         """Start a process inside a running container.
746
747         Runs the specified command inside the container specified by name. The
748         container has to be running already.
749
750         :param command: Command to run inside container.
751         :type command: str
752         :raises RuntimeError: If runnig the command in a container failed.
753         """
754         cmd = "docker exec --interactive {c.name} /bin/sh -c '{command}; "\
755             "exit $?'".format(c=self.container, command=command)
756
757         ret, _, _ = self.container.ssh.exec_command_sudo(cmd, timeout=180)
758         if int(ret) != 0:
759             raise RuntimeError('Failed to execute command in container '
760                                '{c.name}.'.format(c=self.container))
761
762     def stop(self):
763         """Stop running container.
764
765         :raises RuntimeError: If stopping a container failed.
766         """
767         cmd = 'docker stop {c.name}'.format(c=self.container)
768
769         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
770         if int(ret) != 0:
771             raise RuntimeError('Failed to stop container {c.name}.'
772                                .format(c=self.container))
773
774     def destroy(self):
775         """Remove a container.
776
777         :raises RuntimeError: If removing a container failed.
778         """
779         cmd = 'docker rm --force {c.name}'.format(c=self.container)
780
781         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
782         if int(ret) != 0:
783             raise RuntimeError('Failed to destroy container {c.name}.'
784                                .format(c=self.container))
785
786     def info(self):
787         """Return low-level information on Docker objects.
788
789         :raises RuntimeError: If getting info about a container failed.
790         """
791         cmd = 'docker inspect {c.name}'.format(c=self.container)
792
793         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
794         if int(ret) != 0:
795             raise RuntimeError('Failed to get info about container {c.name}.'
796                                .format(c=self.container))
797
798     def system_info(self):
799         """Display the docker system-wide information.
800
801         :raises RuntimeError: If displaying system information failed.
802         """
803         cmd = 'docker system info'
804
805         ret, _, _ = self.container.ssh.exec_command_sudo(cmd)
806         if int(ret) != 0:
807             raise RuntimeError('Failed to get system info.')
808
809     def is_container_present(self):
810         """Check if container is present on node.
811
812         :returns: True if container is present.
813         :rtype: bool
814         :raises RuntimeError: If getting info about a container failed.
815         """
816         cmd = 'docker ps --all --quiet --filter name={c.name}'\
817             .format(c=self.container)
818
819         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
820         if int(ret) != 0:
821             raise RuntimeError('Failed to get info about container {c.name}.'
822                                .format(c=self.container))
823         return True if stdout else False
824
825     def is_container_running(self):
826         """Check if container is running on node.
827
828         :returns: True if container is running.
829         :rtype: bool
830         :raises RuntimeError: If getting info about a container failed.
831         """
832         cmd = 'docker ps --quiet --filter name={c.name}'\
833             .format(c=self.container)
834
835         ret, stdout, _ = self.container.ssh.exec_command_sudo(cmd)
836         if int(ret) != 0:
837             raise RuntimeError('Failed to get info about container {c.name}.'
838                                .format(c=self.container))
839         return True if stdout else False
840
841
842 class Container(object):
843     """Container class."""
844
845     def __init__(self):
846         """Initialize Container object."""
847         pass
848
849     def __getattr__(self, attr):
850         """Get attribute custom implementation.
851
852         :param attr: Attribute to get.
853         :type attr: str
854         :returns: Attribute value or None.
855         :rtype: any
856         """
857         try:
858             return self.__dict__[attr]
859         except KeyError:
860             return None
861
862     def __setattr__(self, attr, value):
863         """Set attribute custom implementation.
864
865         :param attr: Attribute to set.
866         :param value: Value to set.
867         :type attr: str
868         :type value: any
869         """
870         try:
871             # Check if attribute exists
872             self.__dict__[attr]
873         except KeyError:
874             # Creating new attribute
875             if attr == 'node':
876                 self.__dict__['ssh'] = SSH()
877                 self.__dict__['ssh'].connect(value)
878             self.__dict__[attr] = value
879         else:
880             # Updating attribute base of type
881             if isinstance(self.__dict__[attr], list):
882                 self.__dict__[attr].append(value)
883             else:
884                 self.__dict__[attr] = value