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