blob: 53f5aeea82bb6021f19fa4d63e6457276239f15d [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
Artem Panchenkodb0a97f2017-06-27 19:09:13 +030016import StringIO
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030017
18from devops.helpers import helpers
19from devops.helpers import ssh_client
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +030020from devops.helpers import subprocess_runner
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030021from paramiko import rsakey
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +030022import yaml
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030023
24from tcp_tests import logger
25from tcp_tests.helpers import utils
26
27LOG = logger.logger
28
29
30class UnderlaySSHManager(object):
31 """Keep the list of SSH access credentials to Underlay nodes.
32
33 This object is initialized using config.underlay.ssh.
34
35 :param config_ssh: JSONList of SSH access credentials for nodes:
36 [
37 {
38 node_name: node1,
39 address_pool: 'public-pool01',
40 host: ,
41 port: ,
42 keys: [],
43 keys_source_host: None,
44 login: ,
45 password: ,
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030046 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030047 },
48 {
49 node_name: node1,
50 address_pool: 'private-pool01',
51 host:
52 port:
53 keys: []
54 keys_source_host: None,
55 login:
56 password:
Dennis Dmitriev474e3f72016-10-21 16:46:09 +030057 roles: [],
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030058 },
59 {
60 node_name: node2,
61 address_pool: 'public-pool01',
62 keys_source_host: node1
63 ...
64 }
65 ,
66 ...
67 ]
68
69 self.node_names(): list of node names registered in underlay.
70 self.remote(): SSHClient object by a node name (w/wo address pool)
71 or by a hostname.
72 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020073 __config = None
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030074 config_ssh = None
75 config_lvm = None
76
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020077 def __init__(self, config):
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030078 """Read config.underlay.ssh object
79
80 :param config_ssh: dict
81 """
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020082 self.__config = config
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030083 if self.config_ssh is None:
84 self.config_ssh = []
85
86 if self.config_lvm is None:
87 self.config_lvm = {}
88
Dennis Dmitriev2a13a132016-11-04 00:56:23 +020089 self.add_config_ssh(self.__config.underlay.ssh)
Dennis Dmitriev6f59add2016-10-18 13:45:27 +030090
91 def add_config_ssh(self, config_ssh):
92
93 if config_ssh is None:
94 config_ssh = []
95
96 for ssh in config_ssh:
97 ssh_data = {
98 # Required keys:
99 'node_name': ssh['node_name'],
100 'host': ssh['host'],
101 'login': ssh['login'],
102 'password': ssh['password'],
103 # Optional keys:
104 'address_pool': ssh.get('address_pool', None),
105 'port': ssh.get('port', None),
106 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300107 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300108 }
109
110 if 'keys_source_host' in ssh:
111 node_name = ssh['keys_source_host']
112 remote = self.remote(node_name)
113 keys = self.__get_keys(remote)
114 ssh_data['keys'].extend(keys)
115
116 self.config_ssh.append(ssh_data)
117
118 def remove_config_ssh(self, config_ssh):
119 if config_ssh is None:
120 config_ssh = []
121
122 for ssh in config_ssh:
123 ssh_data = {
124 # Required keys:
125 'node_name': ssh['node_name'],
126 'host': ssh['host'],
127 'login': ssh['login'],
128 'password': ssh['password'],
129 # Optional keys:
130 'address_pool': ssh.get('address_pool', None),
131 'port': ssh.get('port', None),
132 'keys': ssh.get('keys', []),
Dennis Dmitriev474e3f72016-10-21 16:46:09 +0300133 'roles': ssh.get('roles', []),
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300134 }
135 self.config_ssh.remove(ssh_data)
136
137 def __get_keys(self, remote):
138 keys = []
139 remote.execute('cd ~')
140 key_string = './.ssh/id_rsa'
141 if remote.exists(key_string):
142 with remote.open(key_string) as f:
143 keys.append(rsakey.RSAKey.from_private_key(f))
144 return keys
145
146 def __ssh_data(self, node_name=None, host=None, address_pool=None):
147
148 ssh_data = None
149
150 if host is not None:
151 for ssh in self.config_ssh:
152 if host == ssh['host']:
153 ssh_data = ssh
154 break
155
156 elif node_name is not None:
157 for ssh in self.config_ssh:
158 if node_name == ssh['node_name']:
159 if address_pool is not None:
160 if address_pool == ssh['address_pool']:
161 ssh_data = ssh
162 break
163 else:
164 ssh_data = ssh
165 if ssh_data is None:
166 raise Exception('Auth data for node was not found using '
167 'node_name="{}" , host="{}" , address_pool="{}"'
168 .format(node_name, host, address_pool))
169 return ssh_data
170
171 def node_names(self):
172 """Get list of node names registered in config.underlay.ssh"""
173
174 names = [] # List is used to keep the original order of names
175 for ssh in self.config_ssh:
176 if ssh['node_name'] not in names:
177 names.append(ssh['node_name'])
178 return names
179
180 def enable_lvm(self, lvmconfig):
181 """Method for enabling lvm oh hosts in environment
182
183 :param lvmconfig: dict with ids or device' names of lvm storage
184 :raises: devops.error.DevopsCalledProcessError,
185 devops.error.TimeoutError, AssertionError, ValueError
186 """
187 def get_actions(lvm_id):
188 return [
189 "systemctl enable lvm2-lvmetad.service",
190 "systemctl enable lvm2-lvmetad.socket",
191 "systemctl start lvm2-lvmetad.service",
192 "systemctl start lvm2-lvmetad.socket",
193 "pvcreate {} && pvs".format(lvm_id),
194 "vgcreate default {} && vgs".format(lvm_id),
195 "lvcreate -L 1G -T default/pool && lvs",
196 ]
197 lvmpackages = ["lvm2", "liblvm2-dev", "thin-provisioning-tools"]
198 for node_name in self.node_names():
199 lvm = lvmconfig.get(node_name, None)
200 if not lvm:
201 continue
202 if 'id' in lvm:
203 lvmdevice = '/dev/disk/by-id/{}'.format(lvm['id'])
204 elif 'device' in lvm:
205 lvmdevice = '/dev/{}'.format(lvm['device'])
206 else:
207 raise ValueError("Unknown LVM device type")
208 if lvmdevice:
209 self.apt_install_package(
210 packages=lvmpackages, node_name=node_name, verbose=True)
211 for command in get_actions(lvmdevice):
212 self.sudo_check_call(command, node_name=node_name,
213 verbose=True)
214 self.config_lvm = dict(lvmconfig)
215
216 def host_by_node_name(self, node_name, address_pool=None):
217 ssh_data = self.__ssh_data(node_name=node_name,
218 address_pool=address_pool)
219 return ssh_data['host']
220
221 def remote(self, node_name=None, host=None, address_pool=None):
222 """Get SSHClient by a node name or hostname.
223
224 One of the following arguments should be specified:
225 - host (str): IP address or hostname. If specified, 'node_name' is
226 ignored.
227 - node_name (str): Name of the node stored to config.underlay.ssh
228 - address_pool (str): optional for node_name.
229 If None, use the first matched node_name.
230 """
231 ssh_data = self.__ssh_data(node_name=node_name, host=host,
232 address_pool=address_pool)
233 return ssh_client.SSHClient(
234 host=ssh_data['host'],
235 port=ssh_data['port'] or 22,
236 username=ssh_data['login'],
237 password=ssh_data['password'],
Artem Panchenkodb0a97f2017-06-27 19:09:13 +0300238 private_keys=[rsakey.RSAKey(file_obj=StringIO.StringIO(key))
239 for key in ssh_data['keys']])
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300240
Dennis Dmitriev2dfb8ef2017-07-21 20:19:38 +0300241 def local(self):
242 """Get Subprocess instance for local operations like:
243
244 underlay.local.execute(command, verbose=False, timeout=None)
245 underlay.local.check_call(
246 command, verbose=False, timeout=None,
247 error_info=None, expected=None, raise_on_err=True)
248 underlay.local.check_stderr(
249 command, verbose=False, timeout=None,
250 error_info=None, raise_on_err=True)
251 """
252 return subprocess_runner.Subprocess()
253
Dennis Dmitriev6f59add2016-10-18 13:45:27 +0300254 def check_call(
255 self, cmd,
256 node_name=None, host=None, address_pool=None,
257 verbose=False, timeout=None,
258 error_info=None,
259 expected=None, raise_on_err=True):
260 """Execute command on the node_name/host and check for exit code
261
262 :type cmd: str
263 :type node_name: str
264 :type host: str
265 :type verbose: bool
266 :type timeout: int
267 :type error_info: str
268 :type expected: list
269 :type raise_on_err: bool
270 :rtype: list stdout
271 :raises: devops.error.DevopsCalledProcessError
272 """
273 remote = self.remote(node_name=node_name, host=host,
274 address_pool=address_pool)
275 return remote.check_call(
276 command=cmd, verbose=verbose, timeout=timeout,
277 error_info=error_info, expected=expected,
278 raise_on_err=raise_on_err)
279
280 def apt_install_package(self, packages=None, node_name=None, host=None,
281 **kwargs):
282 """Method to install packages on ubuntu nodes
283
284 :type packages: list
285 :type node_name: str
286 :type host: str
287 :raises: devops.error.DevopsCalledProcessError,
288 devops.error.TimeoutError, AssertionError, ValueError
289
290 Other params of check_call and sudo_check_call are allowed
291 """
292 expected = kwargs.pop('expected', None)
293 if not packages or not isinstance(packages, list):
294 raise ValueError("packages list should be provided!")
295 install = "apt-get install -y {}".format(" ".join(packages))
296 # Should wait until other 'apt' jobs are finished
297 pgrep_expected = [0, 1]
298 pgrep_command = "pgrep -a -f apt"
299 helpers.wait(
300 lambda: (self.check_call(
301 pgrep_command, expected=pgrep_expected, host=host,
302 node_name=node_name, **kwargs).exit_code == 1
303 ), interval=30, timeout=1200,
304 timeout_msg="Timeout reached while waiting for apt lock"
305 )
306 # Install packages
307 self.sudo_check_call("apt-get update", node_name=node_name, host=host,
308 **kwargs)
309 self.sudo_check_call(install, expected=expected, node_name=node_name,
310 host=host, **kwargs)
311
312 def sudo_check_call(
313 self, cmd,
314 node_name=None, host=None, address_pool=None,
315 verbose=False, timeout=None,
316 error_info=None,
317 expected=None, raise_on_err=True):
318 """Execute command with sudo on node_name/host and check for exit code
319
320 :type cmd: str
321 :type node_name: str
322 :type host: str
323 :type verbose: bool
324 :type timeout: int
325 :type error_info: str
326 :type expected: list
327 :type raise_on_err: bool
328 :rtype: list stdout
329 :raises: devops.error.DevopsCalledProcessError
330 """
331 remote = self.remote(node_name=node_name, host=host,
332 address_pool=address_pool)
333 with remote.get_sudo(remote):
334 return remote.check_call(
335 command=cmd, verbose=verbose, timeout=timeout,
336 error_info=error_info, expected=expected,
337 raise_on_err=raise_on_err)
338
339 def dir_upload(self, host, source, destination):
340 """Upload local directory content to remote host
341
342 :param host: str, remote node name
343 :param source: str, local directory path
344 :param destination: str, local directory path
345 """
346 with self.remote(node_name=host) as remote:
347 remote.upload(source, destination)
348
349 def get_random_node(self):
350 """Get random node name
351
352 :return: str, name of node
353 """
354 return random.choice(self.node_names())
355
356 def yaml_editor(self, file_path, node_name=None, host=None,
357 address_pool=None):
358 """Returns an initialized YamlEditor instance for context manager
359
360 Usage (with 'underlay' fixture):
361
362 # Local YAML file
363 with underlay.yaml_editor('/path/to/file') as editor:
364 editor.content[key] = "value"
365
366 # Remote YAML file on TCP host
367 with underlay.yaml_editor('/path/to/file',
368 host=config.tcp.tcp_host) as editor:
369 editor.content[key] = "value"
370 """
371 # Local YAML file
372 if node_name is None and host is None:
373 return utils.YamlEditor(file_path=file_path)
374
375 # Remote YAML file
376 ssh_data = self.__ssh_data(node_name=node_name, host=host,
377 address_pool=address_pool)
378 return utils.YamlEditor(
379 file_path=file_path,
380 host=ssh_data['host'],
381 port=ssh_data['port'] or 22,
382 username=ssh_data['login'],
383 password=ssh_data['password'],
384 private_keys=ssh_data['keys'])
Dennis Dmitriev010f4cd2016-11-01 20:43:51 +0200385
Dennis Dmitriev99b26fe2017-04-26 12:34:44 +0300386 def read_template(self, file_path):
387 """Read yaml as a jinja template"""
388 options = {
389 'config': self.__config,
390 }
391 template = utils.render_template(file_path, options=options)
392 return yaml.load(template)