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