blob: ad3e4ad2ef68ec1467ba8a636b8bf4049705998a [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)
250 return ssh_client.SSHClient(
251 host=ssh_data['host'],
252 port=ssh_data['port'] or 22,
253 username=ssh_data['login'],
254 password=ssh_data['password'],
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300255 private_keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
256 for key in ssh_data['keys']])
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300257
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300258 def local(self):
259 """Get Subprocess instance for local operations like:
260
261 underlay.local.execute(command, verbose=False, timeout=None)
262 underlay.local.check_call(
263 command, verbose=False, timeout=None,
264 error_info=None, expected=None, raise_on_err=True)
265 underlay.local.check_stderr(
266 command, verbose=False, timeout=None,
267 error_info=None, raise_on_err=True)
268 """
269 return subprocess_runner.Subprocess()
270
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300271 def check_call(
272 self, cmd,
273 node_name=None, host=None, address_pool=None,
274 verbose=False, timeout=None,
275 error_info=None,
276 expected=None, raise_on_err=True):
277 """Execute command on the node_name/host and check for exit code
278
279 :type cmd: str
280 :type node_name: str
281 :type host: str
282 :type verbose: bool
283 :type timeout: int
284 :type error_info: str
285 :type expected: list
286 :type raise_on_err: bool
287 :rtype: list stdout
288 :raises: devops.error.DevopsCalledProcessError
289 """
290 remote = self.remote(node_name=node_name, host=host,
291 address_pool=address_pool)
292 return remote.check_call(
293 command=cmd, verbose=verbose, timeout=timeout,
294 error_info=error_info, expected=expected,
295 raise_on_err=raise_on_err)
296
297 def apt_install_package(self, packages=None, node_name=None, host=None,
298 **kwargs):
299 """Method to install packages on ubuntu nodes
300
301 :type packages: list
302 :type node_name: str
303 :type host: str
304 :raises: devops.error.DevopsCalledProcessError,
305 devops.error.TimeoutError, AssertionError, ValueError
306
307 Other params of check_call and sudo_check_call are allowed
308 """
309 expected = kwargs.pop('expected', None)
310 if not packages or not isinstance(packages, list):
311 raise ValueError("packages list should be provided!")
312 install = "apt-get install -y {}".format(" ".join(packages))
313 # Should wait until other 'apt' jobs are finished
314 pgrep_expected = [0, 1]
315 pgrep_command = "pgrep -a -f apt"
316 helpers.wait(
317 lambda: (self.check_call(
318 pgrep_command, expected=pgrep_expected, host=host,
319 node_name=node_name, **kwargs).exit_code == 1
320 ), interval=30, timeout=1200,
321 timeout_msg="Timeout reached while waiting for apt lock"
322 )
323 # Install packages
324 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
325 **kwargs)
326 self.sudo_check_call(install, expected=expected, node_name=node_name,
327 host=host, **kwargs)
328
329 def sudo_check_call(
330 self, cmd,
331 node_name=None, host=None, address_pool=None,
332 verbose=False, timeout=None,
333 error_info=None,
334 expected=None, raise_on_err=True):
335 """Execute command with sudo on node_name/host and check for exit code
336
337 :type cmd: str
338 :type node_name: str
339 :type host: str
340 :type verbose: bool
341 :type timeout: int
342 :type error_info: str
343 :type expected: list
344 :type raise_on_err: bool
345 :rtype: list stdout
346 :raises: devops.error.DevopsCalledProcessError
347 """
348 remote = self.remote(node_name=node_name, host=host,
349 address_pool=address_pool)
350 with remote.get_sudo(remote):
351 return remote.check_call(
352 command=cmd, verbose=verbose, timeout=timeout,
353 error_info=error_info, expected=expected,
354 raise_on_err=raise_on_err)
355
356 def dir_upload(self, host, source, destination):
357 """Upload local directory content to remote host
358
359 :param host: str, remote node name
360 :param source: str, local directory path
361 :param destination: str, local directory path
362 """
363 with self.remote(node_name=host) as remote:
364 remote.upload(source, destination)
365
366 def get_random_node(self):
367 """Get random node name
368
369 :return: str, name of node
370 """
371 return random.choice(self.node_names())
372
373 def yaml_editor(self, file_path, node_name=None, host=None,
374 address_pool=None):
375 """Returns an initialized YamlEditor instance for context manager
376
377 Usage (with 'underlay' fixture):
378
379 # Local YAML file
380 with underlay.yaml_editor('/path/to/file') as editor:
381 editor.content[key] = "value"
382
383 # Remote YAML file on TCP host
384 with underlay.yaml_editor('/path/to/file',
385 host=config.tcp.tcp_host) as editor:
386 editor.content[key] = "value"
387 """
388 # Local YAML file
389 if node_name is None and host is None:
390 return utils.YamlEditor(file_path=file_path)
391
392 # Remote YAML file
393 ssh_data = self.__ssh_data(node_name=node_name, host=host,
394 address_pool=address_pool)
395 return utils.YamlEditor(
396 file_path=file_path,
397 host=ssh_data['host'],
398 port=ssh_data['port'] or 22,
399 username=ssh_data['login'],
400 password=ssh_data['password'],
401 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200402
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300403 def read_template(self, file_path):
404 """Read yaml as a jinja template"""
405 options = {
406 'config': self.__config,
407 }
408 template = utils.render_template(file_path, options=options)
409 return yaml.load(template)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300410
411 def get_logs(self, artifact_name,
412 node_role=ext.UNDERLAY_NODE_ROLES.salt_master):
413 master_node = [ssh for ssh in self.config_ssh
414 if node_role in ssh['roles']][0]
415 cmd = ("dpkg -l | grep formula > "
416 "/var/log/{0}_packages.output".format(master_node['node_name']))
417
418 tar_cmd = ('tar --absolute-names'
419 ' --warning=no-file-changed '
420 '-czf {t} {d}'.format(
421 t='{0}_log.tar.gz'.format(artifact_name), d='/var/log'))
422 minion_nodes = [ssh for ssh in self.config_ssh
423 if node_role not in ssh['roles']]
424 for node in minion_nodes:
Tatyana Leontoviche5ccdb32017-10-09 20:10:43 +0300425 try:
426 with self.remote(host=node['host']) as r_node:
427 r_node.check_call(('tar '
428 '--absolute-names '
429 '--warning=no-file-changed '
430 '-czf {t} {d}'.format(
431 t='{0}.tar.gz'.format(node['node_name']),
432 d='/var/log')),
433 verbose=True, raise_on_err=False)
434 except:
435 LOG.info("Can not ssh for node {}".format(node))
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300436 with self.remote(master_node['node_name']) as r:
437 for node in minion_nodes:
438 packages_minion_cmd = ("salt '{0}*' cmd.run "
439 "'dpkg -l' > /var/log/"
440 "{0}_packages.output".format(
441 node['node_name']))
442 r.check_call(packages_minion_cmd)
443 r.check_call("rsync {0}:/root/*.tar.gz "
444 "/var/log/".format(node['node_name']),
445 verbose=True, raise_on_err=False)
446 r.check_call(cmd)
447
448 r.check_call(tar_cmd)
449 r.download(destination='{0}_log.tar.gz'.format(artifact_name),
450 target=os.getcwd())