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