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