9f4254098653aff198e8b1e76e02c28b5bda27ca
[csit.git] / resources / libraries / python / LXCUtils.py
1 # Copyright (c) 2017 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 """Library to manipulate LXC."""
15
16 from resources.libraries.python.ssh import SSH
17 from resources.libraries.python.constants import Constants
18 from resources.libraries.python.topology import NodeType
19
20
21 __all__ = ["LXCUtils"]
22
23 class LXCUtils(object):
24     """LXC utilities."""
25
26     def __init__(self, container_name='slave'):
27         # LXC container name
28         self._container_name = container_name
29         self._node = None
30         # Host hugepages dir that will be mounted inside LXC
31         self._host_hugepages_dir = '/dev/hugepages'
32         # Host dir that will be mounted inside LXC
33         self._host_dir = '/tmp/'
34         # Guest dir to mount host dir to
35         self._guest_dir = '/mnt/host'
36         # LXC container env variables
37         self._env_vars = ['LC_ALL="en_US.UTF-8"',
38                           'DEBIAN_FRONTEND=noninteractive']
39
40     def set_node(self, node):
41         """Set node for LXC execution.
42
43         :param node: Node to execute LXC on.
44         :type node: dict
45         :raises RuntimeError: If Node type is not DUT.
46         """
47         if node['type'] != NodeType.DUT:
48             raise RuntimeError('Node type is not DUT.')
49         self._node = node
50
51     def set_host_dir(self, node, host_dir):
52         """Set shared dir on parent node for LXC.
53
54         :param node: Node to control LXC on.
55         :type node: dict
56         :raises RuntimeError: If Node type is not DUT.
57         """
58         if node['type'] != NodeType.DUT:
59             raise RuntimeError('Node type is not DUT.')
60         self._host_dir = host_dir
61
62     def set_guest_dir(self, node, guest_dir):
63         """Set mount dir on LXC.
64
65         :param node: Node to control LXC on.
66         :param guest_dir: Guest dir for mount.
67         :type node: dict
68         :type guest_dir: str
69         :raises RuntimeError: If Node type is not DUT.
70         """
71         if node['type'] != NodeType.DUT:
72             raise RuntimeError('Node type is not DUT.')
73         self._guest_dir = guest_dir
74
75     def _lxc_checkconfig(self):
76         """Check the current kernel for LXC support.
77
78         :raises RuntimeError: If failed to check LXC support.
79         """
80
81         ssh = SSH()
82         ssh.connect(self._node)
83
84         ret, _, _ = ssh.exec_command_sudo('lxc-checkconfig')
85         if int(ret) != 0:
86             raise RuntimeError('Failed to check LXC support.')
87
88     def _lxc_create(self, distro='ubuntu', release='xenial', arch='amd64'):
89         """Creates a privileged system object where is stored the configuration
90         information and where can be stored user information.
91
92         :param distro: Linux distribution name.
93         :param release: Linux distribution release.
94         :param arch: Linux distribution architecture.
95         :type distro: str
96         :type release: str
97         :type arch: str
98         :raises RuntimeError: If failed to create a container.
99         """
100
101         ssh = SSH()
102         ssh.connect(self._node)
103
104         ret, _, _ = ssh.exec_command_sudo(
105             'lxc-create -t download --name {0} -- -d {1} -r {2} -a {3}'
106             .format(self._container_name, distro, release, arch), timeout=1800)
107         if int(ret) != 0:
108             raise RuntimeError('Failed to create LXC container.')
109
110     def _lxc_info(self):
111         """Queries and shows information about a container.
112
113         :raises RuntimeError: If failed to get info about a container.
114         """
115
116         ssh = SSH()
117         ssh.connect(self._node)
118
119         ret, _, _ = ssh.exec_command_sudo(
120             'lxc-info --name {0}'.format(self._container_name))
121         if int(ret) != 0:
122             raise RuntimeError('Failed to get info about LXC container {0}.'
123                                .format(self._container_name))
124
125     def _lxc_start(self):
126         """Start an application inside a container.
127
128         :raises RuntimeError: If failed to start container.
129         """
130
131         ssh = SSH()
132         ssh.connect(self._node)
133
134         ret, _, _ = ssh.exec_command_sudo(
135             'lxc-start --name {0} --daemon'.format(self._container_name))
136         if int(ret) != 0:
137             raise RuntimeError('Failed to start LXC container {0}.'
138                                .format(self._container_name))
139
140     def _lxc_stop(self):
141         """Stop an application inside a container.
142
143         :raises RuntimeError: If failed to stop container.
144         """
145
146         ssh = SSH()
147         ssh.connect(self._node)
148
149         ret, _, _ = ssh.exec_command_sudo(
150             'lxc-stop --name {0}'.format(self._container_name))
151         if int(ret) != 0:
152             raise RuntimeError('Failed to stop LXC container {}.'
153                                .format(self._container_name))
154
155     def _lxc_destroy(self):
156         """Destroy a container.
157
158         :raises RuntimeError: If failed to destroy container.
159         """
160
161         ssh = SSH()
162         ssh.connect(self._node)
163
164         ret, _, _ = ssh.exec_command_sudo(
165             'lxc-destroy --force --name {0}'.format(self._container_name))
166         if int(ret) != 0:
167             raise RuntimeError('Failed to destroy LXC container {}.'
168                                .format(self._container_name))
169
170     def _lxc_wait(self, state):
171         """Wait for a specific container state.
172
173         :param state: Specify the container state(s) to wait for.
174         :type state: str
175         :raises RuntimeError: If failed to wait for state of a container.
176         """
177
178         ssh = SSH()
179         ssh.connect(self._node)
180
181         ret, _, _ = ssh.exec_command_sudo(
182             'lxc-wait --name {0} --state "{1}"'
183             .format(self._container_name, state))
184         if int(ret) != 0:
185             raise RuntimeError('Failed to wait for "{0}" of LXC container {1}.'
186                                .format(state, self._container_name))
187
188     def _lxc_cgroup(self, state_object, value=''):
189         """Manage the control group associated with a container.
190
191         :param state_object: Specify the state object name.
192         :param value: Specify the value to assign to the state object. If empty,
193         then action is GET, otherwise is action SET.
194         :type state_object: str
195         :type value: str
196         :raises RuntimeError: If failed to get/set for state of a container.
197         """
198
199         ssh = SSH()
200         ssh.connect(self._node)
201
202         ret, _, _ = ssh.exec_command_sudo(
203             'lxc-cgroup --name {0} {1} {2}'
204             .format(self._container_name, state_object, value))
205         if int(ret) != 0:
206             if value:
207                 raise RuntimeError('Failed to set {0} of LXC container {1}.'
208                                    .format(state_object, self._container_name))
209             else:
210                 raise RuntimeError('Failed to get {0} of LXC container {1}.'
211                                    .format(state_object, self._container_name))
212
213     def lxc_attach(self, command):
214         """Start a process inside a running container. Runs the specified
215         command inside the container specified by name. The container has to
216         be running already.
217
218         :param command: Command to run inside container.
219         :type command: str
220         :raises RuntimeError: If container is not running.
221         :raises RuntimeError: If failed to run the command.
222         """
223         env_var = '--keep-env {0}'\
224             .format(' '.join('--set-var %s' % var for var in self._env_vars))
225
226         ssh = SSH()
227         ssh.connect(self._node)
228
229         if not self.is_container_running():
230             raise RuntimeError('LXC {0} is not running.'
231                                .format(self._container_name))
232
233         ret, _, _ = ssh.exec_command_lxc(lxc_cmd=command,
234                                          lxc_name=self._container_name,
235                                          lxc_params=env_var, timeout=180)
236         if int(ret) != 0:
237             raise RuntimeError('Failed to run "{0}" on LXC container {1}.'
238                                .format(command, self._container_name))
239
240     def is_container_present(self):
241         """Check if LXC container is existing on node."""
242
243         ssh = SSH()
244         ssh.connect(self._node)
245
246         ret, _, _ = ssh.exec_command_sudo(
247             'lxc-info --name {0}'.format(self._container_name))
248         return False if int(ret) else True
249
250     def create_container(self, force_create=True):
251         """Create and start a container.
252
253         :param force_create: Destroy a container if exists and create.
254         :type force_create: bool
255         """
256         if self.is_container_present():
257             if force_create:
258                 self.destroy_container()
259             else:
260                 return
261
262         self._lxc_checkconfig()
263         self._lxc_create(distro='ubuntu', release='xenial', arch='amd64')
264         self.start_container()
265
266     def start_container(self):
267         """Start a container and wait for running state."""
268
269         self._lxc_start()
270         self._lxc_wait('RUNNING')
271         self._lxc_info()
272
273     def is_container_running(self):
274         """Check if LXC container is running on node.
275
276         :raises RuntimeError: If failed to get info about a container.
277         """
278
279         ssh = SSH()
280         ssh.connect(self._node)
281
282         ret, stdout, _ = ssh.exec_command_sudo(
283             'lxc-info --state --name {0}'.format(self._container_name))
284         if int(ret) != 0:
285             raise RuntimeError('Failed to get info about LXC container {0}.'
286                                .format(self._container_name))
287
288         return True if 'RUNNING' in stdout else False
289
290     def stop_container(self):
291         """Stop a container and wait for stopped state."""
292
293         self._lxc_stop()
294         self._lxc_wait('STOPPED|FROZEN')
295         self._lxc_info()
296
297     def restart_container(self):
298         """Restart container."""
299
300         self.stop_container()
301         self.start_container()
302
303     def destroy_container(self):
304         """Stop and destroy a container."""
305
306         self._lxc_destroy()
307
308     def container_cpuset_cpus(self, container_cpu):
309         """Set cpuset.cpus control group associated with a container.
310
311         :param container_cpu: Cpuset.cpus string.
312         :type container_cpu: str
313         :raises RuntimeError: If failed to set cgroup for a container.
314         """
315
316         ssh = SSH()
317         ssh.connect(self._node)
318
319         ret, _, _ = ssh.exec_command_sudo('cgset --copy-from / lxc')
320         if int(ret) != 0:
321             raise RuntimeError('Failed to copy cgroup settings from root.')
322
323         self._lxc_cgroup(state_object='cpuset.cpus')
324         self._lxc_cgroup(state_object='cpuset.cpus', value=container_cpu)
325         self._lxc_cgroup(state_object='cpuset.cpus')
326
327     def mount_host_dir_in_container(self):
328         """Mount shared folder inside container.
329
330         :raises RuntimeError: If failed to mount host dir in a container.
331         """
332
333         ssh = SSH()
334         ssh.connect(self._node)
335
336         mnt_cfg = 'lxc.mount.entry = {0} /var/lib/lxc/{1}/rootfs{2} ' \
337             'none bind,create=dir 0 0'.format(self._host_dir,
338                                               self._container_name,
339                                               self._guest_dir)
340         ret, _, _ = ssh.exec_command_sudo(
341             "sh -c 'echo \"{0}\" >> /var/lib/lxc/{1}/config'"
342             .format(mnt_cfg, self._container_name))
343         if int(ret) != 0:
344             raise RuntimeError('Failed to mount {0} in lxc: {1}'
345                                .format(self._host_dir, self._container_name))
346
347         self.restart_container()
348
349     def mount_hugepages_in_container(self):
350         """Mount hugepages inside container.
351
352         :raises RuntimeError: If failed to mount hugepages in a container.
353         """
354
355         ssh = SSH()
356         ssh.connect(self._node)
357
358         mnt_cfg = 'lxc.mount.entry = {0} dev/hugepages ' \
359             'none bind,create=dir 0 0'.format(self._host_hugepages_dir)
360         ret, _, _ = ssh.exec_command_sudo(
361             "sh -c 'echo \"{0}\" >> /var/lib/lxc/{1}/config'"
362             .format(mnt_cfg, self._container_name))
363         if int(ret) != 0:
364             raise RuntimeError('Failed to mount {0} in lxc: {1}'
365                                .format(self._host_hugepages_dir,
366                                        self._container_name))
367
368         self.restart_container()
369
370     def install_vpp_in_container(self, install_dkms=False):
371         """Install vpp inside a container.
372
373         :param install_dkms: If install dkms package. This will impact install
374         time. Dkms is required for installation of vpp-dpdk-dkms. Default is
375         false.
376         :type install_dkms: bool
377         """
378
379         ssh = SSH()
380         ssh.connect(self._node)
381
382         self.lxc_attach('apt-get update')
383         if install_dkms:
384             self.lxc_attach('apt-get install -y dkms && '
385                             'dpkg -i --force-all {0}/install_dir/*.deb'
386                             .format(self._guest_dir))
387         else:
388             self.lxc_attach('for i in $(ls -I \"*dkms*\" {0}/install_dir/); '
389                             'do dpkg -i --force-all {0}/install_dir/$i; done'
390                             .format(self._guest_dir))
391         self.lxc_attach('apt-get -f install -y')
392
393     def restart_vpp_in_container(self):
394         """Restart vpp service inside a container."""
395
396         ssh = SSH()
397         ssh.connect(self._node)
398
399         self.lxc_attach('service vpp restart')
400
401     def create_vpp_cfg_in_container(self, vat_template_file, **args):
402         """Create VPP exec config for a container on given node.
403
404         :param vat_template_file: Template file name of a VAT script.
405         :param args: Parameters for VAT script.
406         :type vat_template_file: str
407         :type args: dict
408         """
409         ssh = SSH()
410         ssh.connect(self._node)
411
412         vat_file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT,
413                                        vat_template_file)
414
415         with open(vat_file_path, 'r') as template_file:
416             cmd_template = template_file.readlines()
417             for line_tmpl in cmd_template:
418                 vat_cmd = line_tmpl.format(**args)
419                 ssh.exec_command('echo "{0}" | '
420                                  'sudo lxc-attach --name {1} -- '
421                                  '/bin/sh -c "/bin/cat >> /tmp/running.exec"'
422                                  .format(vat_cmd.replace('\n', ''),
423                                          self._container_name))