blob: 85d5eb523cafbbcb763a8e6a92800cbb15fbf09c [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):
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200417 dump_commands = (
418 "mkdir /root/$(hostname -f)/;"
419 "rsync -aruv /var/log/ /root/$(hostname -f)/;"
420 "dpkg -l > /root/$(hostname -f)/dump_dpkg_l.txt;"
421 "df -h > /root/$(hostname -f)/dump_df.txt;"
422 "mount > /root/$(hostname -f)/dump_mount.txt;"
423 "blkid -o list > /root/$(hostname -f)/dump_blkid_o_list.txt;"
424 "iptables -t nat -S > /root/$(hostname -f)/dump_iptables_nat.txt;"
425 "iptables -S > /root/$(hostname -f)/dump_iptables.txt;"
426 "ps auxwwf > /root/$(hostname -f)/dump_ps.txt;"
427 "docker images > /root/$(hostname -f)/dump_docker_images.txt;"
428 "docker ps > /root/$(hostname -f)/dump_docker_ps.txt;"
Mikhail Ivanov82da3392018-03-15 22:19:26 +0400429 "docker service ls > "
430 " /root/$(hostname -f)/dump_docker_services_ls.txt;"
431 "docker service ls | awk '{ print $2 }' | "
Dennis Dmitrievb35bc852018-03-15 22:58:44 +0200432 " xargs -I SERVICE docker service ps --no-trunc 2>&1 SERVICE >> "
Mikhail Ivanov82da3392018-03-15 22:19:26 +0400433 " /root/$(hostname -f)/dump_docker_service_ps.txt;"
434 "docker service ls | awk '{ print $2 }' | "
Dennis Dmitrievb35bc852018-03-15 22:58:44 +0200435 " xargs -I SERVICE docker service logs 2>&1 SERVICE > "
436 " /root/$(hostname -f)/dump_docker_service_SERVICE_logs;"
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200437 "vgdisplay > /root/$(hostname -f)/dump_vgdisplay.txt;"
438 "lvdisplay > /root/$(hostname -f)/dump_lvdisplay.txt;"
439 "ip a > /root/$(hostname -f)/dump_ip_a.txt;"
440 "ip r > /root/$(hostname -f)/dump_ip_r.txt;"
441 "netstat -anp > /root/$(hostname -f)/dump_netstat.txt;"
442 "brctl show > /root/$(hostname -f)/dump_brctl_show.txt;"
443 "arp -an > /root/$(hostname -f)/dump_arp.txt;"
444 "uname -a > /root/$(hostname -f)/dump_uname_a.txt;"
445 "lsmod > /root/$(hostname -f)/dump_lsmod.txt;"
446 "cat /proc/interrupts > /root/$(hostname -f)/dump_interrupts.txt;"
447 "cat /etc/*-release > /root/$(hostname -f)/dump_release.txt;"
448 # OpenStack specific, will fail on other nodes
449 # "rabbitmqctl report > /root/$(hostname -f)/dump_rabbitmqctl.txt;"
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300450
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200451 # "ceph health > /root/$(hostname -f)/dump_ceph_health.txt;"
452 # "ceph -s > /root/$(hostname -f)/dump_ceph_s.txt;"
453 # "ceph osd tree > /root/$(hostname -f)/dump_ceph_osd_tree.txt;"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200454
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200455 # "for ns in $(ip netns list);"
456 # " do echo Namespace: ${ns}; ip netns exec ${ns} ip a;"
457 # "done > /root/$(hostname -f)/dump_ip_a_ns.txt;"
458
459 # "for ns in $(ip netns list);"
460 # " do echo Namespace: ${ns}; ip netns exec ${ns} ip r;"
461 # "done > /root/$(hostname -f)/dump_ip_r_ns.txt;"
462
463 # "for ns in $(ip netns list);"
464 # " do echo Namespace: ${ns}; ip netns exec ${ns} netstat -anp;"
465 # "done > /root/$(hostname -f)/dump_netstat_ns.txt;"
466
467 "/usr/bin/haproxy-status.sh > "
468 " /root/$(hostname -f)/dump_haproxy.txt;"
469
470 # Archive the files
471 "cd /root/; tar --absolute-names --warning=no-file-changed "
472 " -czf $(hostname -f).tar.gz ./$(hostname -f)/;"
473 )
474
475 master_host = self.__config.salt.salt_master_host
476 with self.remote(host=master_host) as master:
477 # dump files
478 LOG.info("Archive artifacts on all nodes")
479 master.check_call("salt '*' cmd.run '{0}'".format(dump_commands),
480 raise_on_err=False)
481
482 # create target dir for archives
483 master.check_call("mkdir /root/dump/")
484
485 # get archived artifacts to the master node
486 for node in self.config_ssh:
487 LOG.info("Getting archived artifacts from the node {0}"
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200488 .format(node['node_name']))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200489 master.check_call("rsync -aruv {0}:/root/*.tar.gz "
490 "/root/dump/".format(node['node_name']),
491 raise_on_err=False)
Dennis Dmitriev0bc485b2017-12-13 12:49:54 +0200492
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200493 destination_name = '/root/{0}_dump.tar.gz'.format(artifact_name)
494 # Archive the artifacts from all nodes
495 master.check_call(
496 'cd /root/dump/;'
497 'tar --absolute-names --warning=no-file-changed -czf '
498 ' {0} ./'.format(destination_name))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300499
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200500 # Download the artifact to the host
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200501 LOG.info("Downloading the artifact {0}".format(destination_name))
Dennis Dmitrievf0b2afe2018-02-28 13:25:02 +0200502 master.download(destination=destination_name, target=os.getcwd())
Dennis Dmitriev2d643bc2017-12-04 12:23:47 +0200503
504 def delayed_call(
505 self, cmd,
506 node_name=None, host=None, address_pool=None,
507 verbose=True, timeout=5,
508 delay_min=None, delay_max=None):
509 """Delayed call of the specified command in background
510
511 :param delay_min: minimum delay in minutes before run
512 the command
513 :param delay_max: maximum delay in minutes before run
514 the command
515 The command will be started at random time in the range
516 from delay_min to delay_max in minutes from 'now'
517 using the command 'at'.
518
519 'now' is rounded to integer by 'at' command, i.e.:
520 now(28 min 59 sec) == 28 min 00 sec.
521
522 So, if delay_min=1 , the command may start in range from
523 1 sec to 60 sec.
524
525 If delay_min and delay_max are None, then the command will
526 be executed in the background right now.
527 """
528 time_min = delay_min or delay_max
529 time_max = delay_max or delay_min
530
531 delay = None
532 if time_min is not None and time_max is not None:
533 delay = random.randint(time_min, time_max)
534
535 delay_str = ''
536 if delay:
537 delay_str = " + {0} min".format(delay)
538
539 delay_cmd = "cat << EOF | at now {0}\n{1}\nEOF".format(delay_str, cmd)
540
541 self.check_call(delay_cmd, node_name=node_name, host=host,
542 address_pool=address_pool, verbose=verbose,
543 timeout=timeout)
544
545 def get_target_node_names(self, target='gtw01.'):
546 """Get all node names which names starts with <target>"""
547 return [node_name for node_name
548 in self.node_names()
549 if node_name.startswith(target)]