blob: 12ea22d8fa17029d5a8d9bb2f3c41eaa7db6779e [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
148 def __ssh_data(self, node_name=None, host=None, address_pool=None):
149
150 ssh_data = None
151
152 if host is not None:
153 for ssh in self.config_ssh:
154 if host == ssh['host']:
155 ssh_data = ssh
156 break
157
158 elif node_name is not None:
159 for ssh in self.config_ssh:
160 if node_name == ssh['node_name']:
161 if address_pool is not None:
162 if address_pool == ssh['address_pool']:
163 ssh_data = ssh
164 break
165 else:
166 ssh_data = ssh
167 if ssh_data is None:
168 raise Exception('Auth data for node was not found using '
169 'node_name="{}" , host="{}" , address_pool="{}"'
170 .format(node_name, host, address_pool))
171 return ssh_data
172
173 def node_names(self):
174 """Get list of node names registered in config.underlay.ssh"""
175
176 names = [] # List is used to keep the original order of names
177 for ssh in self.config_ssh:
178 if ssh['node_name'] not in names:
179 names.append(ssh['node_name'])
180 return names
181
182 def enable_lvm(self, lvmconfig):
183 """Method for enabling lvm oh hosts in environment
184
185 :param lvmconfig: dict with ids or device' names of lvm storage
186 :raises: devops.error.DevopsCalledProcessError,
187 devops.error.TimeoutError, AssertionError, ValueError
188 """
189 def get_actions(lvm_id):
190 return [
191 "systemctl enable lvm2-lvmetad.service",
192 "systemctl enable lvm2-lvmetad.socket",
193 "systemctl start lvm2-lvmetad.service",
194 "systemctl start lvm2-lvmetad.socket",
195 "pvcreate {} && pvs".format(lvm_id),
196 "vgcreate default {} && vgs".format(lvm_id),
197 "lvcreate -L 1G -T default/pool && lvs",
198 ]
199 lvmpackages = ["lvm2", "liblvm2-dev", "thin-provisioning-tools"]
200 for node_name in self.node_names():
201 lvm = lvmconfig.get(node_name, None)
202 if not lvm:
203 continue
204 if 'id' in lvm:
205 lvmdevice = '/dev/disk/by-id/{}'.format(lvm['id'])
206 elif 'device' in lvm:
207 lvmdevice = '/dev/{}'.format(lvm['device'])
208 else:
209 raise ValueError("Unknown LVM device type")
210 if lvmdevice:
211 self.apt_install_package(
212 packages=lvmpackages, node_name=node_name, verbose=True)
213 for command in get_actions(lvmdevice):
214 self.sudo_check_call(command, node_name=node_name,
215 verbose=True)
216 self.config_lvm = dict(lvmconfig)
217
218 def host_by_node_name(self, node_name, address_pool=None):
219 ssh_data = self.__ssh_data(node_name=node_name,
220 address_pool=address_pool)
221 return ssh_data['host']
222
223 def remote(self, node_name=None, host=None, address_pool=None):
224 """Get SSHClient by a node name or hostname.
225
226 One of the following arguments should be specified:
227 - host (str): IP address or hostname. If specified, 'node_name' is
228 ignored.
229 - node_name (str): Name of the node stored to config.underlay.ssh
230 - address_pool (str): optional for node_name.
231 If None, use the first matched node_name.
232 """
233 ssh_data = self.__ssh_data(node_name=node_name, host=host,
234 address_pool=address_pool)
235 return ssh_client.SSHClient(
236 host=ssh_data['host'],
237 port=ssh_data['port'] or 22,
238 username=ssh_data['login'],
239 password=ssh_data['password'],
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300240 private_keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
241 for key in ssh_data['keys']])
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300242
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300243 def local(self):
244 """Get Subprocess instance for local operations like:
245
246 underlay.local.execute(command, verbose=False, timeout=None)
247 underlay.local.check_call(
248 command, verbose=False, timeout=None,
249 error_info=None, expected=None, raise_on_err=True)
250 underlay.local.check_stderr(
251 command, verbose=False, timeout=None,
252 error_info=None, raise_on_err=True)
253 """
254 return subprocess_runner.Subprocess()
255
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300256 def check_call(
257 self, cmd,
258 node_name=None, host=None, address_pool=None,
259 verbose=False, timeout=None,
260 error_info=None,
261 expected=None, raise_on_err=True):
262 """Execute command on the node_name/host and check for exit code
263
264 :type cmd: str
265 :type node_name: str
266 :type host: str
267 :type verbose: bool
268 :type timeout: int
269 :type error_info: str
270 :type expected: list
271 :type raise_on_err: bool
272 :rtype: list stdout
273 :raises: devops.error.DevopsCalledProcessError
274 """
275 remote = self.remote(node_name=node_name, host=host,
276 address_pool=address_pool)
277 return remote.check_call(
278 command=cmd, verbose=verbose, timeout=timeout,
279 error_info=error_info, expected=expected,
280 raise_on_err=raise_on_err)
281
282 def apt_install_package(self, packages=None, node_name=None, host=None,
283 **kwargs):
284 """Method to install packages on ubuntu nodes
285
286 :type packages: list
287 :type node_name: str
288 :type host: str
289 :raises: devops.error.DevopsCalledProcessError,
290 devops.error.TimeoutError, AssertionError, ValueError
291
292 Other params of check_call and sudo_check_call are allowed
293 """
294 expected = kwargs.pop('expected', None)
295 if not packages or not isinstance(packages, list):
296 raise ValueError("packages list should be provided!")
297 install = "apt-get install -y {}".format(" ".join(packages))
298 # Should wait until other 'apt' jobs are finished
299 pgrep_expected = [0, 1]
300 pgrep_command = "pgrep -a -f apt"
301 helpers.wait(
302 lambda: (self.check_call(
303 pgrep_command, expected=pgrep_expected, host=host,
304 node_name=node_name, **kwargs).exit_code == 1
305 ), interval=30, timeout=1200,
306 timeout_msg="Timeout reached while waiting for apt lock"
307 )
308 # Install packages
309 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
310 **kwargs)
311 self.sudo_check_call(install, expected=expected, node_name=node_name,
312 host=host, **kwargs)
313
314 def sudo_check_call(
315 self, cmd,
316 node_name=None, host=None, address_pool=None,
317 verbose=False, timeout=None,
318 error_info=None,
319 expected=None, raise_on_err=True):
320 """Execute command with sudo on node_name/host and check for exit code
321
322 :type cmd: str
323 :type node_name: str
324 :type host: str
325 :type verbose: bool
326 :type timeout: int
327 :type error_info: str
328 :type expected: list
329 :type raise_on_err: bool
330 :rtype: list stdout
331 :raises: devops.error.DevopsCalledProcessError
332 """
333 remote = self.remote(node_name=node_name, host=host,
334 address_pool=address_pool)
335 with remote.get_sudo(remote):
336 return remote.check_call(
337 command=cmd, verbose=verbose, timeout=timeout,
338 error_info=error_info, expected=expected,
339 raise_on_err=raise_on_err)
340
341 def dir_upload(self, host, source, destination):
342 """Upload local directory content to remote host
343
344 :param host: str, remote node name
345 :param source: str, local directory path
346 :param destination: str, local directory path
347 """
348 with self.remote(node_name=host) as remote:
349 remote.upload(source, destination)
350
351 def get_random_node(self):
352 """Get random node name
353
354 :return: str, name of node
355 """
356 return random.choice(self.node_names())
357
358 def yaml_editor(self, file_path, node_name=None, host=None,
359 address_pool=None):
360 """Returns an initialized YamlEditor instance for context manager
361
362 Usage (with 'underlay' fixture):
363
364 # Local YAML file
365 with underlay.yaml_editor('/path/to/file') as editor:
366 editor.content[key] = "value"
367
368 # Remote YAML file on TCP host
369 with underlay.yaml_editor('/path/to/file',
370 host=config.tcp.tcp_host) as editor:
371 editor.content[key] = "value"
372 """
373 # Local YAML file
374 if node_name is None and host is None:
375 return utils.YamlEditor(file_path=file_path)
376
377 # Remote YAML file
378 ssh_data = self.__ssh_data(node_name=node_name, host=host,
379 address_pool=address_pool)
380 return utils.YamlEditor(
381 file_path=file_path,
382 host=ssh_data['host'],
383 port=ssh_data['port'] or 22,
384 username=ssh_data['login'],
385 password=ssh_data['password'],
386 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200387
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300388 def read_template(self, file_path):
389 """Read yaml as a jinja template"""
390 options = {
391 'config': self.__config,
392 }
393 template = utils.render_template(file_path, options=options)
394 return yaml.load(template)
Tatyana Leontovichab47e162017-10-06 16:53:30 +0300395
396 def get_logs(self, artifact_name,
397 node_role=ext.UNDERLAY_NODE_ROLES.salt_master):
398 master_node = [ssh for ssh in self.config_ssh
399 if node_role in ssh['roles']][0]
400 cmd = ("dpkg -l | grep formula > "
401 "/var/log/{0}_packages.output".format(master_node['node_name']))
402
403 tar_cmd = ('tar --absolute-names'
404 ' --warning=no-file-changed '
405 '-czf {t} {d}'.format(
406 t='{0}_log.tar.gz'.format(artifact_name), d='/var/log'))
407 minion_nodes = [ssh for ssh in self.config_ssh
408 if node_role not in ssh['roles']]
409 for node in minion_nodes:
410 with self.remote(host=node['host']) as r_node:
411 r_node.check_call(('tar '
412 '--absolute-names '
413 '--warning=no-file-changed '
414 '-czf {t} {d}'.format(
415 t='{0}.tar.gz'.format(node['node_name']), d='/var/log')),
416 verbose=True, raise_on_err=False)
417 with self.remote(master_node['node_name']) as r:
418 for node in minion_nodes:
419 packages_minion_cmd = ("salt '{0}*' cmd.run "
420 "'dpkg -l' > /var/log/"
421 "{0}_packages.output".format(
422 node['node_name']))
423 r.check_call(packages_minion_cmd)
424 r.check_call("rsync {0}:/root/*.tar.gz "
425 "/var/log/".format(node['node_name']),
426 verbose=True, raise_on_err=False)
427 r.check_call(cmd)
428
429 r.check_call(tar_cmd)
430 r.download(destination='{0}_log.tar.gz'.format(artifact_name),
431 target=os.getcwd())