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