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