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