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