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