Fix for Functest 'BarometerCollectd ERROR - Private key file not found'
[barometer.git] / baro_tests / config_server.py
1 """Classes used by client.py"""
2 # -*- coding: utf-8 -*-
3
4 #Licensed under the Apache License, Version 2.0 (the "License"); you may
5 # not use this file except in compliance with the License. You may obtain
6 # a copy of the License at
7 #
8 #      http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13 # License for the specific language governing permissions and limitations
14 # under the License.
15
16 import paramiko
17 import time
18 import string
19 import os.path
20
21 ID_RSA_PATH = '/home/opnfv/.ssh/id_rsa'
22 SSH_KEYS_SCRIPT = '/home/opnfv/barometer/baro_utils/get_ssh_keys.sh'
23 DEF_PLUGIN_INTERVAL = 10
24 COLLECTD_CONF = '/etc/collectd/collectd.conf'
25 COLLECTD_CONF_DIR = '/etc/collectd/collectd.conf.d'
26
27
28 class Node(object):
29     """Node configuration class"""
30     def __init__(self, attrs):
31         self.__id = int(attrs[0])
32         self.__status = attrs[1]
33         self.__name = attrs[2]
34         self.__cluster = int(attrs[3]) if attrs[3] else None
35         self.__ip = attrs[4]
36         self.__mac = attrs[5]
37         self.__roles = [x.strip(' ') for x in attrs[6].split(',')]
38         self.__pending_roles = attrs[7]
39         self.__online = int(attrs[8]) if attrs[3] and attrs[8]else None
40         self.__group_id = int(attrs[9]) if attrs[3] else None
41
42     def get_name(self):
43         """Get node name"""
44         return self.__name
45
46     def get_id(self):
47         """Get node ID"""
48         return self.__id
49
50     def get_ip(self):
51         """Get node IP address"""
52         return self.__ip
53
54     def get_roles(self):
55         """Get node roles"""
56         return self.__roles
57
58
59 class ConfigServer(object):
60     """Class to get env configuration"""
61     def __init__(self, host, user, logger, passwd=None):
62         self.__host = host
63         self.__user = user
64         self.__passwd = passwd
65         self.__priv_key = None
66         self.__nodes = list()
67         self.__logger = logger
68
69         self.__private_key_file = ID_RSA_PATH
70         if not os.path.isfile(self.__private_key_file):
71             self.__logger.error(
72                 "Private key file '{}' not found.".format(self.__private_key_file))
73             raise IOError("Private key file '{}' not found.".format(self.__private_key_file))
74
75         # get list of available nodes
76         ssh, sftp = self.__open_sftp_session(self.__host, self.__user, self.__passwd)
77         attempt = 1
78         fuel_node_passed = False
79
80         while (attempt <= 10) and not fuel_node_passed:
81             stdin, stdout, stderr = ssh.exec_command("fuel node")
82             stderr_lines = stderr.readlines()
83             if stderr_lines:
84                 self.__logger.warning("'fuel node' command failed (try {}):".format(attempt))
85                 for line in stderr_lines:
86                     self.__logger.debug(line.strip())
87             else:
88                 fuel_node_passed = True
89                 if attempt > 1:
90                     self.__logger.info("'fuel node' command passed (try {})".format(attempt))
91             attempt += 1
92         if not fuel_node_passed:
93             self.__logger.error("'fuel node' command failed. This was the last try.")
94             raise OSError("'fuel node' command failed. This was the last try.")
95         node_table = stdout.readlines()\
96
97         # skip table title and parse table values
98         for entry in node_table[2:]:
99             self.__nodes.append(Node([str(x.strip(' \n')) for x in entry.split('|')]))
100
101     def get_controllers(self):
102         """Get list of controllers"""
103         return [node for node in self.__nodes if 'controller' in node.get_roles()]
104
105     def get_computes(self):
106         """Get list of computes"""
107         return [node for node in self.__nodes if 'compute' in node.get_roles()]
108
109     def get_nodes(self):
110         """Get list of nodes"""
111         return self.__nodes
112
113     def __open_sftp_session(self, host, user, passwd=None):
114         """Connect to given host.
115
116         Keyword arguments:
117         host -- host to connect
118         user -- user to use
119         passwd -- password to use
120
121         Return tuple of SSH and SFTP client instances.
122         """
123         # create SSH client
124         ssh = paramiko.SSHClient()
125         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
126
127         # try a direct access using password or private key
128         if not passwd and not self.__priv_key:
129             # get private key
130             self.__priv_key = paramiko.RSAKey.from_private_key_file(self.__private_key_file)
131
132         # connect to the server
133         ssh.connect(host, username=user, password=passwd, pkey=self.__priv_key)
134         sftp = ssh.open_sftp()
135
136         # return SFTP client instance
137         return ssh, sftp
138
139     def get_plugin_interval(self, compute, plugin):
140         """Find the plugin interval in collectd configuration.
141
142         Keyword arguments:
143         compute -- compute node instance
144         plugin -- plug-in name
145
146         If found, return interval value, otherwise the default value"""
147         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
148         in_plugin = False
149         plugin_name = ''
150         default_interval = DEF_PLUGIN_INTERVAL
151         config_files = [COLLECTD_CONF] \
152             + [COLLECTD_CONF_DIR + '/' + conf_file for conf_file in sftp.listdir(COLLECTD_CONF_DIR)]
153         for config_file in config_files:
154             try:
155                 with sftp.open(config_file) as config:
156                     for line in config.readlines():
157                         words = line.split()
158                         if len(words) > 1 and words[0] == '<LoadPlugin':
159                             in_plugin = True
160                             plugin_name = words[1].strip('">')
161                         if words and words[0] == '</LoadPlugin>':
162                             in_plugin = False
163                         if words and words[0] == 'Interval':
164                             if in_plugin and plugin_name == plugin:
165                                 return int(words[1])
166                             if not in_plugin:
167                                 default_interval = int(words[1])
168             except IOError:
169                 self.__logger.error("Could not open collectd.conf file.")
170         return default_interval
171
172     def get_plugin_config_values(self, compute, plugin, parameter):
173         """Get parameter values from collectd config file.
174
175         Keyword arguments:
176         compute -- compute node instance
177         plugin -- plug-in name
178         parameter -- plug-in parameter
179
180         Return list of found values."""
181         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
182         # find the plugin value
183         in_plugin = False
184         plugin_name = ''
185         default_values = []
186         config_files = [COLLECTD_CONF] \
187             + [COLLECTD_CONF_DIR + '/' + conf_file for conf_file in sftp.listdir(COLLECTD_CONF_DIR)]
188         for config_file in config_files:
189             try:
190                 with sftp.open(config_file) as config:
191                     for line in config.readlines():
192                         words = line.split()
193                         if len(words) > 1 and words[0] == '<Plugin':
194                             in_plugin = True
195                             plugin_name = words[1].strip('">')
196                         if len(words) > 0 and words[0] == '</Plugin>':
197                             in_plugin = False
198                         if len(words) > 0 and words[0] == parameter:
199                             if in_plugin and plugin_name == plugin:
200                                 return [word.strip('"') for word in words[1:]]
201             except IOError:
202                 self.__logger.error("Could not open collectd.conf file.")
203         return default_values
204
205     def execute_command(self, command, host_ip=None, ssh=None):
206         """Execute command on node and return list of lines of standard output.
207
208         Keyword arguments:
209         command -- command
210         host_ip -- IP of the node
211         ssh -- existing open SSH session to use
212
213         One of host_ip or ssh must not be None. If both are not None, existing ssh session is used.
214         """
215         if host_ip is None and ssh is None:
216             raise ValueError('One of host_ip or ssh must not be None.')
217         if ssh is None:
218             ssh, sftp = self.__open_sftp_session(host_ip, 'root')
219         stdin, stdout, stderr = ssh.exec_command(command)
220         return stdout.readlines()
221
222     def get_ovs_interfaces(self, compute):
223         """Get list of configured OVS interfaces
224
225         Keyword arguments:
226         compute -- compute node instance
227         """
228         stdout = self.execute_command("ovs-vsctl list-br", compute.get_ip())
229         return [interface.strip() for interface in stdout]
230
231     def is_ceilometer_running(self, controller):
232         """Check whether Ceilometer is running on controller.
233
234         Keyword arguments:
235         controller -- controller node instance
236
237         Return boolean value whether Ceilometer is running.
238         """
239         lines = self.execute_command('service --status-all | grep ceilometer', controller.get_ip())
240         agent = False
241         collector = False
242         for line in lines:
243             if '[ + ]  ceilometer-agent-notification' in line:
244                 agent = True
245             if '[ + ]  ceilometer-collector' in line:
246                 collector = True
247         return agent and collector
248
249     def is_installed(self, compute, package):
250         """Check whether package exists on compute node.
251
252         Keyword arguments:
253         compute -- compute node instance
254         package -- Linux package to search for
255
256         Return boolean value whether package is installed.
257         """
258         stdout = self.execute_command('dpkg -l | grep {}'.format(package), compute.get_ip())
259         return len(stdout) > 0
260
261     def check_ceil_plugin_included(self, compute):
262         """Check if ceilometer plugin is included in collectd.conf file If not,
263         try to enable it.
264
265         Keyword arguments:
266         compute -- compute node instance
267
268         Return boolean value whether ceilometer plugin is included or it's enabling was successful.
269         """
270         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
271         try:
272             config = sftp.open(COLLECTD_CONF, mode='r')
273         except IOError:
274             self.__logger.error(
275                 'Cannot open {} on node {}'.format(COLLECTD_CONF, compute.get_id()))
276             return False
277         in_lines = config.readlines()
278         out_lines = in_lines[:]
279         include_section_indexes = [
280             (start, end) for start in range(len(in_lines)) for end in range(len(in_lines))
281             if (start < end)
282             and '<Include' in in_lines[start]
283             and COLLECTD_CONF_DIR in in_lines[start]
284             and '#' not in in_lines[start]
285             and '</Include>' in in_lines[end]
286             and '#' not in in_lines[end]
287             and len([i for i in in_lines[start + 1: end]
288                 if 'Filter' in i and '*.conf' in i and '#' not in i]) > 0]
289         if len(include_section_indexes) == 0:
290             out_lines.append('<Include "{}">\n'.format(COLLECTD_CONF_DIR))
291             out_lines.append('        Filter "*.conf"\n')
292             out_lines.append('</Include>\n')
293             config.close()
294             config = sftp.open(COLLECTD_CONF, mode='w')
295             config.writelines(out_lines)
296         config.close()
297         self.__logger.info('Creating backup of collectd.conf...')
298         config = sftp.open(COLLECTD_CONF + '.backup', mode='w')
299         config.writelines(in_lines)
300         config.close()
301         return True
302
303     def enable_plugins(self, compute, plugins, error_plugins, create_backup=True):
304         """Enable plugins on compute node
305
306         Keyword arguments:
307         compute -- compute node instance
308         plugins -- list of plugins to be enabled
309         error_plugins -- list of tuples with found errors, new entries may be added there
310             (plugin, error_description, is_critical):
311                 plugin -- plug-in name
312                 error_decription -- description of the error
313                 is_critical -- boolean value indicating whether error is critical
314         create_backup -- boolean value indicating whether backup shall be created
315
316         Return boolean value indicating whether function was successful.
317         """
318         plugins = sorted(plugins)
319         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
320         plugins_to_enable = plugins[:]
321         for plugin in plugins:
322             plugin_file = '/usr/lib/collectd/{}.so'.format(plugin)
323             try:
324                 sftp.stat(plugin_file)
325             except IOError:
326                 self.__logger.debug(
327                     'Plugin file {0} not found on node {1}, plugin {2} will not be enabled'.format(
328                         plugin_file, compute.get_id(), plugin))
329                 error_plugins.append((plugin, 'plugin file {} not found'.format(plugin_file), True))
330                 plugins_to_enable.remove(plugin)
331         self.__logger.debug('Following plugins will be enabled on node {}: {}'.format(
332             compute.get_id(), ', '.join(plugins_to_enable)))
333         try:
334             config = sftp.open(COLLECTD_CONF, mode='r')
335         except IOError:
336             self.__logger.warning(
337                 'Cannot open {} on node {}'.format(COLLECTD_CONF, compute.get_id()))
338             return False
339         in_lines = config.readlines()
340         out_lines = []
341         enabled_plugins = []
342         enabled_sections = []
343         in_section = 0
344         comment_section = False
345         uncomment_section = False
346         for line in in_lines:
347             if 'LoadPlugin' in line:
348                 for plugin in plugins_to_enable:
349                     if plugin in line:
350                         commented = '#' in line
351                         #list of uncommented lines which contain LoadPlugin for this plugin
352                         loadlines = [
353                             ll for ll in in_lines if 'LoadPlugin' in ll
354                             and plugin in ll and '#' not in ll]
355                         if len(loadlines) == 0:
356                             if plugin not in enabled_plugins:
357                                 line = line.lstrip(string.whitespace + '#')
358                                 enabled_plugins.append(plugin)
359                                 error_plugins.append((
360                                     plugin, 'plugin not enabled in '
361                                     + '{}, trying to enable it'.format(COLLECTD_CONF), False))
362                         elif not commented:
363                             if plugin not in enabled_plugins:
364                                 enabled_plugins.append(plugin)
365                             else:
366                                 line = '#' + line
367                                 error_plugins.append((
368                                     plugin, 'plugin enabled more than once '
369                                     + '(additional occurrence of LoadPlugin found in '
370                                     + '{}), trying to comment it out.'.format(
371                                         COLLECTD_CONF), False))
372             elif line.lstrip(string.whitespace + '#').find('<Plugin') == 0:
373                 in_section += 1
374                 for plugin in plugins_to_enable:
375                     if plugin in line:
376                         commented = '#' in line
377                         #list of uncommented lines which contain Plugin for this plugin
378                         pluginlines = [
379                             pl for pl in in_lines if '<Plugin' in pl
380                             and plugin in pl and '#' not in pl]
381                         if len(pluginlines) == 0:
382                             if plugin not in enabled_sections:
383                                 line = line[line.rfind('#') + 1:]
384                                 uncomment_section = True
385                                 enabled_sections.append(plugin)
386                                 error_plugins.append((
387                                     plugin, 'plugin section found in '
388                                     + '{}, but commented out, trying to uncomment it.'.format(
389                                         COLLECTD_CONF), False))
390                         elif not commented:
391                             if plugin not in enabled_sections:
392                                 enabled_sections.append(plugin)
393                             else:
394                                 line = '#' + line
395                                 comment_section = True
396                                 error_plugins.append((
397                                     plugin,
398                                     'additional occurrence of plugin section found in '
399                                     + '{}, trying to comment it out.'.format(COLLECTD_CONF),
400                                     False))
401             elif in_section > 0:
402                 if comment_section and '#' not in line:
403                     line = '#' + line
404                 if uncomment_section and '#' in line:
405                     line = line[line.rfind('#') + 1:]
406                 if '</Plugin>' in line:
407                     in_section -= 1
408                     if in_section == 0:
409                         comment_section = False
410                         uncomment_section = False
411             elif '</Plugin>' in line:
412                 self.__logger.error(
413                     'Unexpected closure os plugin section on line'
414                     + ' {} in collectd.conf, matching section start not found.'.format(
415                         len(out_lines) + 1))
416                 return False
417             out_lines.append(line)
418         if in_section > 0:
419             self.__logger.error(
420                 'Unexpected end of file collectd.conf, '
421                 + 'closure of last plugin section not found.')
422             return False
423         out_lines = [
424             'LoadPlugin {}\n'.format(plugin) for plugin in plugins_to_enable
425             if plugin not in enabled_plugins] + out_lines
426         for plugin in plugins_to_enable:
427             if plugin not in enabled_plugins:
428                 error_plugins.append((
429                     plugin,
430                     'plugin not enabled in {}, trying to enable it.'.format(COLLECTD_CONF),
431                     False))
432         unenabled_sections = [
433             plugin for plugin in plugins_to_enable if plugin not in enabled_sections]
434         if unenabled_sections:
435             self.__logger.error('Plugin sections for following plugins not found: {}'.format(
436                 ', '.join(unenabled_sections)))
437             return False
438
439         config.close()
440         if create_backup:
441             self.__logger.info('Creating backup of collectd.conf...')
442             config = sftp.open(COLLECTD_CONF + '.backup', mode='w')
443             config.writelines(in_lines)
444             config.close()
445         self.__logger.info('Updating collectd.conf...')
446         config = sftp.open(COLLECTD_CONF, mode='w')
447         config.writelines(out_lines)
448         config.close()
449         diff_command = "diff {} {}.backup".format(COLLECTD_CONF, COLLECTD_CONF)
450         stdin, stdout, stderr = ssh.exec_command(diff_command)
451         self.__logger.debug(diff_command)
452         for line in stdout.readlines():
453             self.__logger.debug(line.strip())
454         return True
455
456     def restore_config(self, compute):
457         """Restore collectd config file from backup on compute node.
458
459         Keyword arguments:
460         compute -- compute node instance
461         """
462         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
463
464         self.__logger.info('Restoring config file from backup...')
465         ssh.exec_command("cp {0} {0}.used".format(COLLECTD_CONF))
466         ssh.exec_command("cp {0}.backup {0}".format(COLLECTD_CONF))
467
468     def restart_collectd(self, compute):
469         """Restart collectd on compute node.
470
471         Keyword arguments:
472         compute -- compute node instance
473
474         Retrun tuple with boolean indicating success and list of warnings received
475         during collectd start.
476         """
477
478         def get_collectd_processes(ssh_session):
479             """Get number of running collectd processes.
480
481             Keyword arguments:
482             ssh_session -- instance of SSH session in which to check for processes
483             """
484             stdin, stdout, stderr = ssh_session.exec_command("pgrep collectd")
485             return len(stdout.readlines())
486
487         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
488
489         self.__logger.info('Stopping collectd service...')
490         stdout = self.execute_command("service collectd stop", ssh=ssh)
491         time.sleep(10)
492         if get_collectd_processes(ssh):
493             self.__logger.error('Collectd is still running...')
494             return False, []
495         self.__logger.info('Starting collectd service...')
496         stdout = self.execute_command("service collectd start", ssh=ssh)
497         time.sleep(10)
498         warning = [output.strip() for output in stdout if 'WARN: ' in output]
499         if get_collectd_processes(ssh) == 0:
500             self.__logger.error('Collectd is still not running...')
501             return False, warning
502         return True, warning