blob: 51942398c62c22ad6db34bcd4257cb3a3cb4e2de [file] [log] [blame]
Dennis Dmitriev6f59add2016-10-18 13:45:27 +03001# Copyright 2016 Mirantis, Inc.
2#
3# Licensed under the Apache License, Version 2.0 (the "License"); you may
4# not use this file except in compliance with the License. You may obtain
5# a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12# License for the specific language governing permissions and limitations
13# under the License.
14
Tatyana Leontovichab47e162017-10-06 16:53:30 +030015import os
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030016import random
Artem Panchenkodb0a97f2017-06-27 19:09:13 +030017import StringIO
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030018
19from devops.helpers import helpers
20from devops.helpers import ssh_client
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030021from devops.helpers import subprocess_runner
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030022from paramiko import rsakey
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +030023import yaml
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030024
25from tcp_tests import logger
Tatyana Leontovichab47e162017-10-06 16:53:30 +030026from tcp_tests.helpers import ext
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030027from tcp_tests.helpers import utils
28
29LOG = logger.logger
30
31
32class UnderlaySSHManager(object):
33 """Keep the list of SSH access credentials to Underlay nodes.
34
35 This object is initialized using config.underlay.ssh.
36
37 :param config_ssh: JSONList of SSH access credentials for nodes:
38 [
39 {
40 node_name: node1,
41 address_pool: 'public-pool01',
42 host: ,
43 port: ,
44 keys: [],
45 keys_source_host: None,
46 login: ,
47 password: ,
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030048 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030049 },
50 {
51 node_name: node1,
52 address_pool: 'private-pool01',
53 host:
54 port:
55 keys: []
56 keys_source_host: None,
57 login:
58 password:
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030059 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030060 },
61 {
62 node_name: node2,
63 address_pool: 'public-pool01',
64 keys_source_host: node1
65 ...
66 }
67 ,
68 ...
69 ]
70
71 self.node_names(): list of node names registered in underlay.
72 self.remote(): SSHClient object by a node name (w/wo address pool)
73 or by a hostname.
74 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020075 __config = None
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030076 config_ssh = None
77 config_lvm = None
78
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020079 def __init__(self, config):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030080 """Read config.underlay.ssh object
81
82 :param config_ssh: dict
83 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020084 self.__config = config
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030085 if self.config_ssh is None:
86 self.config_ssh = []
87
88 if self.config_lvm is None:
89 self.config_lvm = {}
90
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020091 self.add_config_ssh(self.__config.underlay.ssh)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030092
93 def add_config_ssh(self, config_ssh):
94
95 if config_ssh is None:
96 config_ssh = []
97
98 for ssh in config_ssh:
99 ssh_data = {
100 # Required keys:
101 'node_name': ssh['node_name'],
102 'host': ssh['host'],
103 'login': ssh['login'],
104 'password': ssh['password'],
105 # Optional keys:
106 'address_pool': ssh.get('address_pool', None),
107 'port': ssh.get('port', None),
108 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300109 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300110 }
111
112 if 'keys_source_host' in ssh:
113 node_name = ssh['keys_source_host']
114 remote = self.remote(node_name)
115 keys = self.__get_keys(remote)
116 ssh_data['keys'].extend(keys)
117
118 self.config_ssh.append(ssh_data)
119
120 def remove_config_ssh(self, config_ssh):
121 if config_ssh is None:
122 config_ssh = []
123
124 for ssh in config_ssh:
125 ssh_data = {
126 # Required keys:
127 'node_name': ssh['node_name'],
128 'host': ssh['host'],
129 'login': ssh['login'],
130 'password': ssh['password'],
131 # Optional keys:
132 'address_pool': ssh.get('address_pool', None),
133 'port': ssh.get('port', None),
134 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300135 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300136 }
137 self.config_ssh.remove(ssh_data)
138
139 def __get_keys(self, remote):
140 keys = []
141 remote.execute('cd ~')
142 key_string = './.ssh/id_rsa'
143 if remote.exists(key_string):
144 with remote.open(key_string) as f:
145 keys.append(rsakey.RSAKey.from_private_key(f))
146 return keys
147
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300148 def __ssh_data(self, node_name=None, host=None, address_pool=None,
149 node_role=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300150
151 ssh_data = None
152
153 if host is not None:
154 for ssh in self.config_ssh:
155 if host == ssh['host']:
156 ssh_data = ssh
157 break
158
159 elif node_name is not None:
160 for ssh in self.config_ssh:
161 if node_name == ssh['node_name']:
162 if address_pool is not None:
163 if address_pool == ssh['address_pool']:
164 ssh_data = ssh
165 break
166 else:
167 ssh_data = ssh
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300168 elif node_role is not None:
169 for ssh in self.config_ssh:
170 if node_role in ssh['roles']:
171 if address_pool is not None:
172 if address_pool == ssh['address_pool']:
173 ssh_data = ssh
174 break
175 else:
176 ssh_data = ssh
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300177 if ssh_data is None:
178 raise Exception('Auth data for node was not found using '
179 'node_name="{}" , host="{}" , address_pool="{}"'
180 .format(node_name, host, address_pool))
181 return ssh_data
182
183 def node_names(self):
184 """Get list of node names registered in config.underlay.ssh"""
185
186 names = [] # List is used to keep the original order of names
187 for ssh in self.config_ssh:
188 if ssh['node_name'] not in names:
189 names.append(ssh['node_name'])
190 return names
191
192 def enable_lvm(self, lvmconfig):
193 """Method for enabling lvm oh hosts in environment
194
195 :param lvmconfig: dict with ids or device' names of lvm storage
196 :raises: devops.error.DevopsCalledProcessError,
197 devops.error.TimeoutError, AssertionError, ValueError
198 """
199 def get_actions(lvm_id):
200 return [
201 "systemctl enable lvm2-lvmetad.service",
202 "systemctl enable lvm2-lvmetad.socket",
203 "systemctl start lvm2-lvmetad.service",
204 "systemctl start lvm2-lvmetad.socket",
205 "pvcreate {} && pvs".format(lvm_id),
206 "vgcreate default {} && vgs".format(lvm_id),
207 "lvcreate -L 1G -T default/pool && lvs",
208 ]
209 lvmpackages = ["lvm2", "liblvm2-dev", "thin-provisioning-tools"]
210 for node_name in self.node_names():
211 lvm = lvmconfig.get(node_name, None)
212 if not lvm:
213 continue
214 if 'id' in lvm:
215 lvmdevice = '/dev/disk/by-id/{}'.format(lvm['id'])
216 elif 'device' in lvm:
217 lvmdevice = '/dev/{}'.format(lvm['device'])
218 else:
219 raise ValueError("Unknown LVM device type")
220 if lvmdevice:
221 self.apt_install_package(
222 packages=lvmpackages, node_name=node_name, verbose=True)
223 for command in get_actions(lvmdevice):
224 self.sudo_check_call(command, node_name=node_name,
225 verbose=True)
226 self.config_lvm = dict(lvmconfig)
227
228 def host_by_node_name(self, node_name, address_pool=None):
229 ssh_data = self.__ssh_data(node_name=node_name,
230 address_pool=address_pool)
231 return ssh_data['host']
232
Tatyana Leontovichecd491d2017-09-13 13:51:12 +0300233 def host_by_node_role(self, node_role, address_pool=None):
234 ssh_data = self.__ssh_data(node_role=node_role,
235 address_pool=address_pool)
236 return ssh_data['host']
237
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300238 def remote(self, node_name=None, host=None, address_pool=None):
239 """Get SSHClient by a node name or hostname.
240
241 One of the following arguments should be specified:
242 - host (str): IP address or hostname. If specified, 'node_name' is
243 ignored.
244 - node_name (str): Name of the node stored to config.underlay.ssh
245 - address_pool (str): optional for node_name.
246 If None, use the first matched node_name.
247 """
248 ssh_data = self.__ssh_data(node_name=node_name, host=host,
249 address_pool=address_pool)
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200250 ssh_auth = ssh_client.SSHAuth(
251 username=ssh_data['login'],
252 password=ssh_data['password'],
253 keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
254 for key in ssh_data['keys']])
255
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300256 return ssh_client.SSHClient(
257 host=ssh_data['host'],
258 port=ssh_data['port'] or 22,
Dmitry Tyzhnenko5a5d8da2017-12-14 14:14:42 +0200259 auth=ssh_auth)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300260
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300261 def local(self):
262 """Get Subprocess instance for local operations like:
263
264 underlay.local.execute(command, verbose=False, timeout=None)
265 underlay.local.check_call(
266 command, verbose=False, timeout=None,
267 error_info=None, expected=None, raise_on_err=True)
268 underlay.local.check_stderr(
269 command, verbose=False, timeout=None,
270 error_info=None, raise_on_err=True)
271 """
272 return subprocess_runner.Subprocess()
273
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300274 def check_call(
275 self, cmd,
276 node_name=None, host=None, address_pool=None,
277 verbose=False, timeout=None,
278 error_info=None,
279 expected=None, raise_on_err=True):
280 """Execute command on the node_name/host and check for exit code
281
282 :type cmd: str
283 :type node_name: str
284 :type host: str
285 :type verbose: bool
286 :type timeout: int
287 :type error_info: str
288 :type expected: list
289 :type raise_on_err: bool
290 :rtype: list stdout
291 :raises: devops.error.DevopsCalledProcessError
292 """
293 remote = self.remote(node_name=node_name, host=host,
294 address_pool=address_pool)
295 return remote.check_call(
296 command=cmd, verbose=verbose, timeout=timeout,
297 error_info=error_info, expected=expected,
298 raise_on_err=raise_on_err)
299
300 def apt_install_package(self, packages=None, node_name=None, host=None,
301 **kwargs):
302 """Method to install packages on ubuntu nodes
303
304 :type packages: list
305 :type node_name: str
306 :type host: str
307 :raises: devops.error.DevopsCalledProcessError,
308 devops.error.TimeoutError, AssertionError, ValueError
309
310 Other params of check_call and sudo_check_call are allowed
311 """
312 expected = kwargs.pop('expected', None)
313 if not packages or not isinstance(packages, list):
314 raise ValueError("packages list should be provided!")
315 install = "apt-get install -y {}".format(" ".join(packages))
316 # Should wait until other 'apt' jobs are finished
317 pgrep_expected = [0, 1]
318 pgrep_command = "pgrep -a -f apt"
319 helpers.wait(
320 lambda: (self.check_call(
321 pgrep_command, expected=pgrep_expected, host=host,
322 node_name=node_name, **kwargs).exit_code == 1
323 ), interval=30, timeout=1200,
324 timeout_msg="Timeout reached while waiting for apt lock"
325 )
326 # Install packages
327 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
328 **kwargs)
329 self.sudo_check_call(install, expected=expected, node_name=node_name,
330 host=host, **kwargs)
331
332 def sudo_check_call(
333 self, cmd,
334 node_name=None, host=None, address_pool=None,
335 verbose=False, timeout=None,
336 error_info=None,
337 expected=None, raise_on_err=True):
338 """Execute command with sudo on node_name/host and check for exit code
339
340 :type cmd: str
341 :type node_name: str
342 :type host: str
343 :type verbose: bool
344 :type timeout: int
345 :type error_info: str
346 :type expected: list
347 :type raise_on_err: bool
348 :rtype: list stdout
349 :raises: devops.error.DevopsCalledProcessError
350 """
351 remote = self.remote(node_name=node_name, host=host,
352 address_pool=address_pool)
353 with remote.get_sudo(remote):
354 return remote.check_call(
355 command=cmd, verbose=verbose, timeout=timeout,
356 error_info=error_info, expected=expected,
357 raise_on_err=raise_on_err)
358
359 def dir_upload(self, host, source, destination):
360 """Upload local directory content to remote host
361
362 :param host: str, remote node name
363 :param source: str, local directory path
364 :param destination: str, local directory path
365 """
366 with self.remote(node_name=host) as remote:
367 remote.upload(source, destination)
368
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200369 def get_random_node(self, node_names=None):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300370 """Get random node name
371
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200372 :param node_names: list of strings
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300373 :return: str, name of node
374 """
Dennis Dmitriev0f08d9a2017-12-19 02:27:59 +0200375 return random.choice(node_names or self.node_names())
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300376
377 def yaml_editor(self, file_path, node_name=None, host=None,
378 address_pool=None):
379 """Returns an initialized YamlEditor instance for context manager
380
381 Usage (with 'underlay' fixture):
382
383 # Local YAML file
384 with underlay.yaml_editor('/path/to/file') as editor:
385 editor.content[key] = "value"
386
387 # Remote YAML file on TCP host
388 with underlay.yaml_editor('/path/to/file',
389 host=config.tcp.tcp_host) as editor:
390 editor.content[key] = "value"
391 """
392 # Local YAML file
393 if node_name is None and host is None:
394 return utils.YamlEditor(file_path=file_path)
395
396 # Remote YAML file
397 ssh_data = self.__ssh_data(node_name=node_name, host=host,
398 address_pool=address_pool)
399 return utils.YamlEditor(
400 file_path=file_path,
401 host=ssh_data['host'],
402 port=ssh_data['port'] or 22,
403 username=ssh_data['login'],
404 password=ssh_data['password'],
405 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200406
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300407 def read_template(self, file_path):
408 """Read yaml as a jinja template"""
409 options = {
410 'config': self.__config,
411 }
412 template = utils.render_template(file_path, options=options)
413 return yaml.load(template)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300414
415 def get_logs(self, artifact_name,
416 node_role=ext.UNDERLAY_NODE_ROLES.salt_master):
417 master_node = [ssh for ssh in self.config_ssh
418 if node_role in ssh['roles']][0]
419 cmd = ("dpkg -l | grep formula > "
420 "/var/log/{0}_packages.output".format(master_node['node_name']))
421
422 tar_cmd = ('tar --absolute-names'
423 ' --warning=no-file-changed '
424 '-czf {t} {d}'.format(
Dennis Dmitriev9b02c8b2017-11-13 15:31:35 +0200425 t='{0}_log.tar.gz'.format(artifact_name), d='/var/log'))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300426 minion_nodes = [ssh for ssh in self.config_ssh
427 if node_role not in ssh['roles']]
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200428
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300429 with self.remote(master_node['node_name']) as r:
430 for node in minion_nodes:
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200431 LOG.info("Archiving logs on the node {0}"
432 .format(node['node_name']))
433 r.check_call((
434 "salt '{n}*' cmd.run "
435 "'tar "
436 "--absolute-names "
437 "--warning=no-file-changed "
438 "-czf {t} {d}'".format(
439 n=node['node_name'],
440 t='{0}.tar.gz'.format(node['node_name']),
441 d='/var/log')),
442 raise_on_err=False)
443
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200444 LOG.info("Copying logs from {0} to {1}"
445 .format(node['node_name'], master_node['node_name']))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300446 packages_minion_cmd = ("salt '{0}*' cmd.run "
447 "'dpkg -l' > /var/log/"
448 "{0}_packages.output".format(
Dennis Dmitriev9b02c8b2017-11-13 15:31:35 +0200449 node['node_name']))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300450 r.check_call(packages_minion_cmd)
451 r.check_call("rsync {0}:/root/*.tar.gz "
452 "/var/log/".format(node['node_name']),
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200453 raise_on_err=False)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300454
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200455 r.check_call(cmd)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300456 r.check_call(tar_cmd)
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200457
458 destination_name = '{0}_log.tar.gz'.format(artifact_name)
459 LOG.info("Downloading the artifact {0}".format(destination_name))
460 r.download(destination=destination_name, target=os.getcwd())
461
462 def delayed_call(
463 self, cmd,
464 node_name=None, host=None, address_pool=None,
465 verbose=True, timeout=5,
466 delay_min=None, delay_max=None):
467 """Delayed call of the specified command in background
468
469 :param delay_min: minimum delay in minutes before run
470 the command
471 :param delay_max: maximum delay in minutes before run
472 the command
473 The command will be started at random time in the range
474 from delay_min to delay_max in minutes from 'now'
475 using the command 'at'.
476
477 'now' is rounded to integer by 'at' command, i.e.:
478 now(28 min 59 sec) == 28 min 00 sec.
479
480 So, if delay_min=1 , the command may start in range from
481 1 sec to 60 sec.
482
483 If delay_min and delay_max are None, then the command will
484 be executed in the background right now.
485 """
486 time_min = delay_min or delay_max
487 time_max = delay_max or delay_min
488
489 delay = None
490 if time_min is not None and time_max is not None:
491 delay = random.randint(time_min, time_max)
492
493 delay_str = ''
494 if delay:
495 delay_str = " + {0} min".format(delay)
496
497 delay_cmd = "cat << EOF | at now {0}\n{1}\nEOF".format(delay_str, cmd)
498
499 self.check_call(delay_cmd, node_name=node_name, host=host,
500 address_pool=address_pool, verbose=verbose,
501 timeout=timeout)
502
503 def get_target_node_names(self, target='gtw01.'):
504 """Get all node names which names starts with <target>"""
505 return [node_name for node_name
506 in self.node_names()
507 if node_name.startswith(target)]