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