blob: 90f39246f9b1f3ea9476f01ca6ae73d9f69ccb93 [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
15import random
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +020016import time
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030017
18from devops.helpers import helpers
19from devops.helpers import ssh_client
20from paramiko import rsakey
21
22from tcp_tests import logger
23from tcp_tests.helpers import utils
24
25LOG = logger.logger
26
27
28class UnderlaySSHManager(object):
29 """Keep the list of SSH access credentials to Underlay nodes.
30
31 This object is initialized using config.underlay.ssh.
32
33 :param config_ssh: JSONList of SSH access credentials for nodes:
34 [
35 {
36 node_name: node1,
37 address_pool: 'public-pool01',
38 host: ,
39 port: ,
40 keys: [],
41 keys_source_host: None,
42 login: ,
43 password: ,
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030044 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030045 },
46 {
47 node_name: node1,
48 address_pool: 'private-pool01',
49 host:
50 port:
51 keys: []
52 keys_source_host: None,
53 login:
54 password:
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030055 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030056 },
57 {
58 node_name: node2,
59 address_pool: 'public-pool01',
60 keys_source_host: node1
61 ...
62 }
63 ,
64 ...
65 ]
66
67 self.node_names(): list of node names registered in underlay.
68 self.remote(): SSHClient object by a node name (w/wo address pool)
69 or by a hostname.
70 """
71 config_ssh = None
72 config_lvm = None
73
74 def __init__(self, config_ssh):
75 """Read config.underlay.ssh object
76
77 :param config_ssh: dict
78 """
79 if self.config_ssh is None:
80 self.config_ssh = []
81
82 if self.config_lvm is None:
83 self.config_lvm = {}
84
85 self.add_config_ssh(config_ssh)
86
87 def add_config_ssh(self, config_ssh):
88
89 if config_ssh is None:
90 config_ssh = []
91
92 for ssh in config_ssh:
93 ssh_data = {
94 # Required keys:
95 'node_name': ssh['node_name'],
96 'host': ssh['host'],
97 'login': ssh['login'],
98 'password': ssh['password'],
99 # Optional keys:
100 'address_pool': ssh.get('address_pool', None),
101 'port': ssh.get('port', None),
102 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300103 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300104 }
105
106 if 'keys_source_host' in ssh:
107 node_name = ssh['keys_source_host']
108 remote = self.remote(node_name)
109 keys = self.__get_keys(remote)
110 ssh_data['keys'].extend(keys)
111
112 self.config_ssh.append(ssh_data)
113
114 def remove_config_ssh(self, config_ssh):
115 if config_ssh is None:
116 config_ssh = []
117
118 for ssh in config_ssh:
119 ssh_data = {
120 # Required keys:
121 'node_name': ssh['node_name'],
122 'host': ssh['host'],
123 'login': ssh['login'],
124 'password': ssh['password'],
125 # Optional keys:
126 'address_pool': ssh.get('address_pool', None),
127 'port': ssh.get('port', None),
128 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300129 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300130 }
131 self.config_ssh.remove(ssh_data)
132
133 def __get_keys(self, remote):
134 keys = []
135 remote.execute('cd ~')
136 key_string = './.ssh/id_rsa'
137 if remote.exists(key_string):
138 with remote.open(key_string) as f:
139 keys.append(rsakey.RSAKey.from_private_key(f))
140 return keys
141
142 def __ssh_data(self, node_name=None, host=None, address_pool=None):
143
144 ssh_data = None
145
146 if host is not None:
147 for ssh in self.config_ssh:
148 if host == ssh['host']:
149 ssh_data = ssh
150 break
151
152 elif node_name is not None:
153 for ssh in self.config_ssh:
154 if node_name == ssh['node_name']:
155 if address_pool is not None:
156 if address_pool == ssh['address_pool']:
157 ssh_data = ssh
158 break
159 else:
160 ssh_data = ssh
161 if ssh_data is None:
162 raise Exception('Auth data for node was not found using '
163 'node_name="{}" , host="{}" , address_pool="{}"'
164 .format(node_name, host, address_pool))
165 return ssh_data
166
167 def node_names(self):
168 """Get list of node names registered in config.underlay.ssh"""
169
170 names = [] # List is used to keep the original order of names
171 for ssh in self.config_ssh:
172 if ssh['node_name'] not in names:
173 names.append(ssh['node_name'])
174 return names
175
176 def enable_lvm(self, lvmconfig):
177 """Method for enabling lvm oh hosts in environment
178
179 :param lvmconfig: dict with ids or device' names of lvm storage
180 :raises: devops.error.DevopsCalledProcessError,
181 devops.error.TimeoutError, AssertionError, ValueError
182 """
183 def get_actions(lvm_id):
184 return [
185 "systemctl enable lvm2-lvmetad.service",
186 "systemctl enable lvm2-lvmetad.socket",
187 "systemctl start lvm2-lvmetad.service",
188 "systemctl start lvm2-lvmetad.socket",
189 "pvcreate {} && pvs".format(lvm_id),
190 "vgcreate default {} && vgs".format(lvm_id),
191 "lvcreate -L 1G -T default/pool && lvs",
192 ]
193 lvmpackages = ["lvm2", "liblvm2-dev", "thin-provisioning-tools"]
194 for node_name in self.node_names():
195 lvm = lvmconfig.get(node_name, None)
196 if not lvm:
197 continue
198 if 'id' in lvm:
199 lvmdevice = '/dev/disk/by-id/{}'.format(lvm['id'])
200 elif 'device' in lvm:
201 lvmdevice = '/dev/{}'.format(lvm['device'])
202 else:
203 raise ValueError("Unknown LVM device type")
204 if lvmdevice:
205 self.apt_install_package(
206 packages=lvmpackages, node_name=node_name, verbose=True)
207 for command in get_actions(lvmdevice):
208 self.sudo_check_call(command, node_name=node_name,
209 verbose=True)
210 self.config_lvm = dict(lvmconfig)
211
212 def host_by_node_name(self, node_name, address_pool=None):
213 ssh_data = self.__ssh_data(node_name=node_name,
214 address_pool=address_pool)
215 return ssh_data['host']
216
217 def remote(self, node_name=None, host=None, address_pool=None):
218 """Get SSHClient by a node name or hostname.
219
220 One of the following arguments should be specified:
221 - host (str): IP address or hostname. If specified, 'node_name' is
222 ignored.
223 - node_name (str): Name of the node stored to config.underlay.ssh
224 - address_pool (str): optional for node_name.
225 If None, use the first matched node_name.
226 """
227 ssh_data = self.__ssh_data(node_name=node_name, host=host,
228 address_pool=address_pool)
229 return ssh_client.SSHClient(
230 host=ssh_data['host'],
231 port=ssh_data['port'] or 22,
232 username=ssh_data['login'],
233 password=ssh_data['password'],
234 private_keys=ssh_data['keys'])
235
236 def check_call(
237 self, cmd,
238 node_name=None, host=None, address_pool=None,
239 verbose=False, timeout=None,
240 error_info=None,
241 expected=None, raise_on_err=True):
242 """Execute command on the node_name/host and check for exit code
243
244 :type cmd: str
245 :type node_name: str
246 :type host: str
247 :type verbose: bool
248 :type timeout: int
249 :type error_info: str
250 :type expected: list
251 :type raise_on_err: bool
252 :rtype: list stdout
253 :raises: devops.error.DevopsCalledProcessError
254 """
255 remote = self.remote(node_name=node_name, host=host,
256 address_pool=address_pool)
257 return remote.check_call(
258 command=cmd, verbose=verbose, timeout=timeout,
259 error_info=error_info, expected=expected,
260 raise_on_err=raise_on_err)
261
262 def apt_install_package(self, packages=None, node_name=None, host=None,
263 **kwargs):
264 """Method to install packages on ubuntu nodes
265
266 :type packages: list
267 :type node_name: str
268 :type host: str
269 :raises: devops.error.DevopsCalledProcessError,
270 devops.error.TimeoutError, AssertionError, ValueError
271
272 Other params of check_call and sudo_check_call are allowed
273 """
274 expected = kwargs.pop('expected', None)
275 if not packages or not isinstance(packages, list):
276 raise ValueError("packages list should be provided!")
277 install = "apt-get install -y {}".format(" ".join(packages))
278 # Should wait until other 'apt' jobs are finished
279 pgrep_expected = [0, 1]
280 pgrep_command = "pgrep -a -f apt"
281 helpers.wait(
282 lambda: (self.check_call(
283 pgrep_command, expected=pgrep_expected, host=host,
284 node_name=node_name, **kwargs).exit_code == 1
285 ), interval=30, timeout=1200,
286 timeout_msg="Timeout reached while waiting for apt lock"
287 )
288 # Install packages
289 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
290 **kwargs)
291 self.sudo_check_call(install, expected=expected, node_name=node_name,
292 host=host, **kwargs)
293
294 def sudo_check_call(
295 self, cmd,
296 node_name=None, host=None, address_pool=None,
297 verbose=False, timeout=None,
298 error_info=None,
299 expected=None, raise_on_err=True):
300 """Execute command with sudo on node_name/host and check for exit code
301
302 :type cmd: str
303 :type node_name: str
304 :type host: str
305 :type verbose: bool
306 :type timeout: int
307 :type error_info: str
308 :type expected: list
309 :type raise_on_err: bool
310 :rtype: list stdout
311 :raises: devops.error.DevopsCalledProcessError
312 """
313 remote = self.remote(node_name=node_name, host=host,
314 address_pool=address_pool)
315 with remote.get_sudo(remote):
316 return remote.check_call(
317 command=cmd, verbose=verbose, timeout=timeout,
318 error_info=error_info, expected=expected,
319 raise_on_err=raise_on_err)
320
321 def dir_upload(self, host, source, destination):
322 """Upload local directory content to remote host
323
324 :param host: str, remote node name
325 :param source: str, local directory path
326 :param destination: str, local directory path
327 """
328 with self.remote(node_name=host) as remote:
329 remote.upload(source, destination)
330
331 def get_random_node(self):
332 """Get random node name
333
334 :return: str, name of node
335 """
336 return random.choice(self.node_names())
337
338 def yaml_editor(self, file_path, node_name=None, host=None,
339 address_pool=None):
340 """Returns an initialized YamlEditor instance for context manager
341
342 Usage (with 'underlay' fixture):
343
344 # Local YAML file
345 with underlay.yaml_editor('/path/to/file') as editor:
346 editor.content[key] = "value"
347
348 # Remote YAML file on TCP host
349 with underlay.yaml_editor('/path/to/file',
350 host=config.tcp.tcp_host) as editor:
351 editor.content[key] = "value"
352 """
353 # Local YAML file
354 if node_name is None and host is None:
355 return utils.YamlEditor(file_path=file_path)
356
357 # Remote YAML file
358 ssh_data = self.__ssh_data(node_name=node_name, host=host,
359 address_pool=address_pool)
360 return utils.YamlEditor(
361 file_path=file_path,
362 host=ssh_data['host'],
363 port=ssh_data['port'] or 22,
364 username=ssh_data['login'],
365 password=ssh_data['password'],
366 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200367
368 def ensure_running_service(self, service_name, node_name, check_cmd,
369 state_running='start/running'):
370 cmd = "service {0} status | grep -q '{1}'".format(
371 service_name, state_running)
372 with self.remote(node_name=node_name) as remote:
373 result = remote.execute(cmd)
374 if result.exit_code != 0:
375 LOG.info("{0} is not in running state on the node {1},"
376 " restarting".format(service_name, node_name))
377 cmd = ("service {0} stop;"
378 " sleep 3; killall -9 {0};"
379 "service {0} start; sleep 5;"
380 .format(service_name))
381 remote.execute(cmd)
382
383 remote.execute(check_cmd)
384 remote.execute(check_cmd)
385
386 def execute_commands(self, commands):
387 for n, step in enumerate(commands):
388 LOG.info(" ####################################################")
389 LOG.info(" *** [ Command #{0} ] {1} ***"
390 .format(n+1, step['description']))
391
392 with self.remote(node_name=step['node_name']) as remote:
393 for x in range(step['retry']['count'], 0, -1):
394 time.sleep(3)
395 result = remote.execute(step['cmd'], verbose=True)
396
397 # Workaround of exit code 0 from salt in case of failures
398 failed = 0
399 for s in result['stdout']:
400 if s.startswith("Failed:"):
401 failed += int(s.split("Failed:")[1])
402
403 if result.exit_code != 0:
404 time.sleep(step['retry']['delay'])
405 LOG.info(" === RETRY ({0}/{1}) ========================="
406 .format(x-1, step['retry']['count']))
407 elif failed != 0:
408 LOG.error(" === SALT returned exit code = 0 while "
409 "there are failed modules! ===")
410 LOG.info(" === RETRY ({0}/{1}) ======================="
411 .format(x-1, step['retry']['count']))
412 else:
413 # Workarounds for crashed services
414 self.ensure_running_service(
415 "salt-master",
416 "cfg01.mk22-lab-advanced.local",
417 "salt-call pillar.items",
418 'active (running)') # Hardcoded for now
419 self.ensure_running_service(
420 "salt-minion",
421 "cfg01.mk22-lab-advanced.local",
422 "salt 'cfg01*' pillar.items",
423 "active (running)") # Hardcoded for now
424 break
425
426 if x == 1 and step['skip_fail'] == False:
427 # In the last retry iteration, raise an exception
428 raise Exception("Step '{0}' failed"
429 .format(step['description']))