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