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