CSIT-843: Update actual topology in case of new/updated/deleted interface
[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             ' --no-validate'.format(self._container_name, distro, release,
107                                     arch), timeout=1800)
108         if int(ret) != 0:
109             raise RuntimeError('Failed to create LXC container.')
110
111     def _lxc_info(self):
112         """Queries and shows information about a container.
113
114         :raises RuntimeError: If failed to get info about a container.
115         """
116
117         ssh = SSH()
118         ssh.connect(self._node)
119
120         ret, _, _ = ssh.exec_command_sudo(
121             'lxc-info --name {0}'.format(self._container_name))
122         if int(ret) != 0:
123             raise RuntimeError('Failed to get info about LXC container {0}.'
124                                .format(self._container_name))
125
126     def _lxc_start(self):
127         """Start an application inside a container.
128
129         :raises RuntimeError: If failed to start container.
130         """
131
132         ssh = SSH()
133         ssh.connect(self._node)
134
135         ret, _, _ = ssh.exec_command_sudo(
136             'lxc-start --name {0} --daemon'.format(self._container_name))
137         if int(ret) != 0:
138             raise RuntimeError('Failed to start LXC container {0}.'
139                                .format(self._container_name))
140
141     def _lxc_stop(self):
142         """Stop an application inside a container.
143
144         :raises RuntimeError: If failed to stop container.
145         """
146
147         ssh = SSH()
148         ssh.connect(self._node)
149
150         ret, _, _ = ssh.exec_command_sudo(
151             'lxc-stop --name {0}'.format(self._container_name))
152         if int(ret) != 0:
153             raise RuntimeError('Failed to stop LXC container {}.'
154                                .format(self._container_name))
155
156     def _lxc_destroy(self):
157         """Destroy a container.
158
159         :raises RuntimeError: If failed to destroy container.
160         """
161
162         ssh = SSH()
163         ssh.connect(self._node)
164
165         ret, _, _ = ssh.exec_command_sudo(
166             'lxc-destroy --force --name {0}'.format(self._container_name))
167         if int(ret) != 0:
168             raise RuntimeError('Failed to destroy LXC container {}.'
169                                .format(self._container_name))
170
171     def _lxc_wait(self, state):
172         """Wait for a specific container state.
173
174         :param state: Specify the container state(s) to wait for.
175         :type state: str
176         :raises RuntimeError: If failed to wait for state of a container.
177         """
178
179         ssh = SSH()
180         ssh.connect(self._node)
181
182         ret, _, _ = ssh.exec_command_sudo(
183             'lxc-wait --name {0} --state "{1}"'
184             .format(self._container_name, state))
185         if int(ret) != 0:
186             raise RuntimeError('Failed to wait for "{0}" of LXC container {1}.'
187                                .format(state, self._container_name))
188
189     def _lxc_cgroup(self, state_object, value=''):
190         """Manage the control group associated with a container.
191
192         :param state_object: Specify the state object name.
193         :param value: Specify the value to assign to the state object. If empty,
194         then action is GET, otherwise is action SET.
195         :type state_object: str
196         :type value: str
197         :raises RuntimeError: If failed to get/set for state of a container.
198         """
199
200         ssh = SSH()
201         ssh.connect(self._node)
202
203         ret, _, _ = ssh.exec_command_sudo(
204             'lxc-cgroup --name {0} {1} {2}'
205             .format(self._container_name, state_object, value))
206         if int(ret) != 0:
207             if value:
208                 raise RuntimeError('Failed to set {0} of LXC container {1}.'
209                                    .format(state_object, self._container_name))
210             else:
211                 raise RuntimeError('Failed to get {0} of LXC container {1}.'
212                                    .format(state_object, self._container_name))
213
214     def lxc_attach(self, command):
215         """Start a process inside a running container. Runs the specified
216         command inside the container specified by name. The container has to
217         be running already.
218
219         :param command: Command to run inside container.
220         :type command: str
221         :raises RuntimeError: If container is not running.
222         :raises RuntimeError: If failed to run the command.
223         """
224         env_var = '--keep-env {0}'\
225             .format(' '.join('--set-var %s' % var for var in self._env_vars))
226
227         ssh = SSH()
228         ssh.connect(self._node)
229
230         if not self.is_container_running():
231             raise RuntimeError('LXC {0} is not running.'
232                                .format(self._container_name))
233
234         ret, _, _ = ssh.exec_command_lxc(lxc_cmd=command,
235                                          lxc_name=self._container_name,
236                                          lxc_params=env_var, timeout=180)
237         if int(ret) != 0:
238             raise RuntimeError('Failed to run "{0}" on LXC container {1}.'
239                                .format(command, self._container_name))
240
241     def is_container_present(self):
242         """Check if LXC container is existing on node."""
243
244         ssh = SSH()
245         ssh.connect(self._node)
246
247         ret, _, _ = ssh.exec_command_sudo(
248             'lxc-info --name {0}'.format(self._container_name))
249         return False if int(ret) else True
250
251     def create_container(self, force_create=True):
252         """Create and start a container.
253
254         :param force_create: Destroy a container if exists and create.
255         :type force_create: bool
256         """
257         if self.is_container_present():
258             if force_create:
259                 self.destroy_container()
260             else:
261                 return
262
263         self._lxc_checkconfig()
264         self._lxc_create(distro='ubuntu', release='xenial', arch='amd64')
265         self.start_container()
266
267     def start_container(self):
268         """Start a container and wait for running state."""
269
270         self._lxc_start()
271         self._lxc_wait('RUNNING')
272         self._lxc_info()
273
274     def is_container_running(self):
275         """Check if LXC container is running on node.
276
277         :raises RuntimeError: If failed to get info about a container.
278         """
279
280         ssh = SSH()
281         ssh.connect(self._node)
282
283         ret, stdout, _ = ssh.exec_command_sudo(
284             'lxc-info --state --name {0}'.format(self._container_name))
285         if int(ret) != 0:
286             raise RuntimeError('Failed to get info about LXC container {0}.'
287                                .format(self._container_name))
288
289         return True if 'RUNNING' in stdout else False
290
291     def stop_container(self):
292         """Stop a container and wait for stopped state."""
293
294         self._lxc_stop()
295         self._lxc_wait('STOPPED|FROZEN')
296         self._lxc_info()
297
298     def restart_container(self):
299         """Restart container."""
300
301         self.stop_container()
302         self.start_container()
303
304     def destroy_container(self):
305         """Stop and destroy a container."""
306
307         self._lxc_destroy()
308
309     def container_cpuset_cpus(self, container_cpu):
310         """Set cpuset.cpus control group associated with a container.
311
312         :param container_cpu: Cpuset.cpus string.
313         :type container_cpu: str
314         :raises RuntimeError: If failed to set cgroup for a container.
315         """
316
317         ssh = SSH()
318         ssh.connect(self._node)
319
320         ret, _, _ = ssh.exec_command_sudo('cgset --copy-from / lxc')
321         if int(ret) != 0:
322             raise RuntimeError('Failed to copy cgroup settings from root.')
323
324         self._lxc_cgroup(state_object='cpuset.cpus')
325         self._lxc_cgroup(state_object='cpuset.cpus', value=container_cpu)
326         self._lxc_cgroup(state_object='cpuset.cpus')
327
328     def mount_host_dir_in_container(self):
329         """Mount shared folder inside container.
330
331         :raises RuntimeError: If failed to mount host dir in a container.
332         """
333
334         ssh = SSH()
335         ssh.connect(self._node)
336
337         mnt_cfg = 'lxc.mount.entry = {0} /var/lib/lxc/{1}/rootfs{2} ' \
338             'none bind,create=dir 0 0'.format(self._host_dir,
339                                               self._container_name,
340                                               self._guest_dir)
341         ret, _, _ = ssh.exec_command_sudo(
342             "sh -c 'echo \"{0}\" >> /var/lib/lxc/{1}/config'"
343             .format(mnt_cfg, self._container_name))
344         if int(ret) != 0:
345             raise RuntimeError('Failed to mount {0} in lxc: {1}'
346                                .format(self._host_dir, self._container_name))
347
348         self.restart_container()
349
350     def mount_hugepages_in_container(self):
351         """Mount hugepages inside container.
352
353         :raises RuntimeError: If failed to mount hugepages in a container.
354         """
355
356         ssh = SSH()
357         ssh.connect(self._node)
358
359         mnt_cfg = 'lxc.mount.entry = {0} dev/hugepages ' \
360             'none bind,create=dir 0 0'.format(self._host_hugepages_dir)
361         ret, _, _ = ssh.exec_command_sudo(
362             "sh -c 'echo \"{0}\" >> /var/lib/lxc/{1}/config'"
363             .format(mnt_cfg, self._container_name))
364         if int(ret) != 0:
365             raise RuntimeError('Failed to mount {0} in lxc: {1}'
366                                .format(self._host_hugepages_dir,
367                                        self._container_name))
368
369         self.restart_container()
370
371     def install_vpp_in_container(self, install_dkms=False):
372         """Install vpp inside a container.
373
374         :param install_dkms: If install dkms package. This will impact install
375         time. Dkms is required for installation of vpp-dpdk-dkms. Default is
376         false.
377         :type install_dkms: bool
378         """
379
380         ssh = SSH()
381         ssh.connect(self._node)
382
383         self.lxc_attach('apt-get update')
384         if install_dkms:
385             self.lxc_attach('apt-get install -y dkms && '
386                             'dpkg -i --force-all {0}/install_dir/*.deb'
387                             .format(self._guest_dir))
388         else:
389             self.lxc_attach('for i in $(ls -I \"*dkms*\" {0}/install_dir/); '
390                             'do dpkg -i --force-all {0}/install_dir/$i; done'
391                             .format(self._guest_dir))
392         self.lxc_attach('apt-get -f install -y')
393
394     def restart_vpp_in_container(self):
395         """Restart vpp service inside a container."""
396
397         ssh = SSH()
398         ssh.connect(self._node)
399
400         self.lxc_attach('service vpp restart')
401
402     def create_vpp_cfg_in_container(self, vat_template_file, **args):
403         """Create VPP exec config for a container on given node.
404
405         :param vat_template_file: Template file name of a VAT script.
406         :param args: Parameters for VAT script.
407         :type vat_template_file: str
408         :type args: dict
409         """
410         ssh = SSH()
411         ssh.connect(self._node)
412
413         vat_file_path = '{}/{}'.format(Constants.RESOURCES_TPL_VAT,
414                                        vat_template_file)
415
416         with open(vat_file_path, 'r') as template_file:
417             cmd_template = template_file.readlines()
418             for line_tmpl in cmd_template:
419                 vat_cmd = line_tmpl.format(**args)
420                 ssh.exec_command('echo "{0}" | '
421                                  'sudo lxc-attach --name {1} -- '
422                                  '/bin/sh -c "/bin/cat >> /tmp/running.exec"'
423                                  .format(vat_cmd.replace('\n', ''),
424                                          self._container_name))