Merge "Added Functest testcases for Barometer project"
[barometer.git] / baro_tests / config_server.py
1 # -*- coding: utf-8 -*-
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
15 """Classes used by collectd.py"""
16
17 import paramiko
18 import time
19 import string
20 import os.path
21 import os
22 import re
23 ID_RSA_PATH = '/home/opnfv/.ssh/id_rsa'
24 SSH_KEYS_SCRIPT = '/home/opnfv/barometer/baro_utils/get_ssh_keys.sh'
25 DEF_PLUGIN_INTERVAL = 10
26 COLLECTD_CONF = '/etc/collectd.conf'
27 COLLECTD_CONF_DIR = '/etc/collectd/collectd.conf.d'
28 NOTIFICATION_FILE = '/var/log/python-notifications.dump'
29 COLLECTD_NOTIFICATION = '/etc/collectd_notification_dump.py'
30
31
32 class Node(object):
33     """Node configuration class"""
34     def __init__(self, attrs):
35         self.__null = attrs[0]
36         self.__id = attrs[1]
37         self.__name = attrs[2]
38         self.__status = attrs[3] if attrs[3] else None
39         self.__taskState = attrs[4]
40         self.__pwrState = attrs[5]
41         self.__ip = re.sub('^[a-z]+=', '', attrs[6])
42
43     def get_name(self):
44         """Get node name"""
45         return self.__name
46
47     def get_id(self):
48         """Get node ID"""
49         return self.__id
50
51     def get_ip(self):
52         """Get node IP address"""
53         return self.__ip
54
55     def get_roles(self):
56         """Get node role"""
57         return self.__roles
58
59
60 class ConfigServer(object):
61     """Class to get env configuration"""
62     def __init__(self, host, user, logger, priv_key=None):
63         self.__host = host
64         self.__user = user
65         self.__passwd = None
66         self.__priv_key = priv_key
67         self.__nodes = list()
68         self.__logger = logger
69
70         self.__private_key_file = ID_RSA_PATH
71         if not os.path.isfile(self.__private_key_file):
72             self.__logger.error(
73                 "Private key file '{}'".format(self.__private_key_file)
74                 + " not found.")
75             raise IOError("Private key file '{}' not found.".format(
76                 self.__private_key_file))
77
78         # get list of available nodes
79         ssh, sftp = self.__open_sftp_session(
80             self.__host, self.__user, self.__passwd)
81         attempt = 1
82         fuel_node_passed = False
83
84         while (attempt <= 10) and not fuel_node_passed:
85             stdin, stdout, stderr = ssh.exec_command(
86                 "source stackrc; nova list")
87             stderr_lines = stderr.readlines()
88             if stderr_lines:
89                 self.__logger.warning(
90                     "'fuel node' command failed (try {}):".format(attempt))
91                 for line in stderr_lines:
92                     self.__logger.debug(line.strip())
93             else:
94                 fuel_node_passed = True
95                 if attempt > 1:
96                     self.__logger.info(
97                         "'fuel node' command passed (try {})".format(attempt))
98             attempt += 1
99         if not fuel_node_passed:
100             self.__logger.error(
101                 "'fuel node' command failed. This was the last try.")
102             raise OSError(
103                 "'fuel node' command failed. This was the last try.")
104         node_table = stdout.readlines()\
105
106         # skip table title and parse table values
107
108         for entry in node_table[3:]:
109             if entry[0] == '+' or entry[0] == '\n':
110                 print entry
111                 pass
112             else:
113                 self.__nodes.append(
114                     Node([str(x.strip(' \n')) for x in entry.split('|')]))
115
116     def get_controllers(self):
117         # Get list of controllers
118         print self.__nodes[0]._Node__ip
119         return (
120             [node for node in self.__nodes if 'controller' in node.get_name()])
121
122     def get_computes(self):
123         # Get list of computes
124         return (
125             [node for node in self.__nodes if 'compute' in node.get_name()])
126
127     def get_nodes(self):
128         # Get list of nodes
129         return self.__nodes
130
131     def __open_sftp_session(self, host, user, passwd=None):
132         # Connect to given host.
133         """Keyword arguments:
134         host -- host to connect
135         user -- user to use
136         passwd -- password to use
137
138         Return tuple of SSH and SFTP client instances.
139         """
140         # create SSH client
141         ssh = paramiko.SSHClient()
142         ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
143
144         # try a direct access using password or private key
145         if not passwd and not self.__priv_key:
146             # get private key
147             self.__priv_key = paramiko.RSAKey.from_private_key_file(
148                 self.__private_key_file)
149
150         # connect to the server
151         ssh.connect(
152             host, username=user, password=passwd, pkey=self.__priv_key)
153         sftp = ssh.open_sftp()
154
155         # return SFTP client instance
156         return ssh, sftp
157
158     def get_plugin_interval(self, compute, plugin):
159         """Find the plugin interval in collectd configuration.
160
161         Keyword arguments:
162         compute -- compute node instance
163         plugin -- plug-in name
164
165         If found, return interval value, otherwise the default value"""
166         ssh, sftp = self.__open_sftp_session(
167             compute.get_ip(), 'root', 'opnfvapex')
168         in_plugin = False
169         plugin_name = ''
170         default_interval = DEF_PLUGIN_INTERVAL
171         config_files = [COLLECTD_CONF] + [
172             COLLECTD_CONF_DIR + '/'
173             + conf_file for conf_file in sftp.listdir(COLLECTD_CONF_DIR)]
174         for config_file in config_files:
175             try:
176                 with sftp.open(config_file) as config:
177                     for line in config.readlines():
178                         words = line.split()
179                         if len(words) > 1 and words[0] == '<LoadPlugin':
180                             in_plugin = True
181                             plugin_name = words[1].strip('">')
182                         if words and words[0] == '</LoadPlugin>':
183                             in_plugin = False
184                         if words and words[0] == 'Interval':
185                             if in_plugin and plugin_name == plugin:
186                                 return int(words[1])
187                             if not in_plugin:
188                                 default_interval = int(words[1])
189             except IOError:
190                 self.__logger.error("Could not open collectd.conf file.")
191         return default_interval
192
193     def get_plugin_config_values(self, compute, plugin, parameter):
194         """Get parameter values from collectd config file.
195
196         Keyword arguments:
197         compute -- compute node instance
198         plugin -- plug-in name
199         parameter -- plug-in parameter
200
201         Return list of found values."""
202         ssh, sftp = self.__open_sftp_session(
203             compute.get_ip(), 'root', 'opnfvapex')
204         # find the plugin value
205         in_plugin = False
206         plugin_name = ''
207         default_values = []
208         config_files = [COLLECTD_CONF] + [
209             COLLECTD_CONF_DIR + '/'
210             + conf_file for conf_file in sftp.listdir(COLLECTD_CONF_DIR)]
211         for config_file in config_files:
212             try:
213                 with sftp.open(config_file) as config:
214                     for line in config.readlines():
215                         words = line.split()
216                         if len(words) > 1 and words[0] == '<Plugin':
217                             in_plugin = True
218                             plugin_name = words[1].strip('">')
219                         if len(words) > 0 and words[0] == '</Plugin>':
220                             in_plugin = False
221                         if len(words) > 0 and words[0] == parameter:
222                             if in_plugin and plugin_name == plugin:
223                                 return [word.strip('"') for word in words[1:]]
224             except IOError:
225                 self.__logger.error("Could not open collectd.conf file.")
226         return default_values
227
228     def execute_command(self, command, host_ip=None, ssh=None):
229         """Execute command on node and return list of lines of standard output.
230
231         Keyword arguments:
232         command -- command
233         host_ip -- IP of the node
234         ssh -- existing open SSH session to use
235
236         One of host_ip or ssh must not be None. If both are not None,
237         existing ssh session is used.
238         """
239         if host_ip is None and ssh is None:
240             raise ValueError('One of host_ip or ssh must not be None.')
241         if ssh is None:
242             ssh, sftp = self.__open_sftp_session(host_ip, 'root', 'opnfvapex')
243         stdin, stdout, stderr = ssh.exec_command(command)
244         return stdout.readlines()
245
246     def get_ovs_interfaces(self, compute):
247         """Get list of configured OVS interfaces
248
249         Keyword arguments:
250         compute -- compute node instance
251         """
252         stdout = self.execute_command("ovs-vsctl list-br", compute.get_ip())
253         return [interface.strip() for interface in stdout]
254
255     def is_gnocchi_running(self, controller):
256         """Check whether Gnocchi is running on controller.
257
258         Keyword arguments:
259         controller -- controller node instance
260
261         Return boolean value whether Gnocchi is running.
262         """
263         gnocchi_present = False
264         lines = self.execute_command(
265             'source overcloudrc.v3;openstack service list | grep gnocchi',
266             controller.get_ip())
267         for line in lines:
268             if 'gnocchi' in line:
269                 gnocchi_present = True
270         return not gnocchi_present
271
272     def is_installed(self, compute, package):
273         """Check whether package exists on compute node.
274
275         Keyword arguments:
276         compute -- compute node instance
277         package -- Linux package to search for
278
279         Return boolean value whether package is installed.
280         """
281         stdout = self.execute_command(
282             'yum list installed | grep {}'.format(package),
283             compute.get_ip())
284         return len(stdout) > 0
285
286     def is_libpqos_on_node(self, compute):
287         """Check whether libpqos is present on compute node"""
288         ssh, sftp = self.__open_sftp_session(
289             compute.get_ip(), 'root', 'opnfvapex')
290         stdin, stdout, stderr = \
291             ssh.exec_command("ls /usr/local/lib/ | grep libpqos")
292         output = stdout.readlines()
293         for lib in output:
294             if 'libpqos' in lib:
295                 return True
296         return False
297
298     def check_gnocchi_plugin_included(self, compute):
299         """Check if gnocchi plugin is included in collectd.conf file.
300         If not, try to enable it.
301
302         Keyword arguments:
303         compute -- compute node instance
304
305         Return boolean value whether gnocchi plugin is included
306         or it's enabling was successful.
307         """
308         ssh, sftp = self.__open_sftp_session(
309             compute.get_ip(), 'root', 'opnfvapex')
310         try:
311             config = sftp.open(COLLECTD_CONF, mode='r')
312         except IOError:
313             self.__logger.error(
314                 'Cannot open {} on node {}'.format(
315                     COLLECTD_CONF, compute.get_name()))
316             return False
317         in_lines = config.readlines()
318         out_lines = in_lines[:]
319         include_section_indexes = [
320             (start, end) for start in range(len(in_lines))
321             for end in range(len(in_lines))
322             if (start < end)
323             and '<Include' in in_lines[start]
324             and COLLECTD_CONF_DIR in in_lines[start]
325             and '#' not in in_lines[start]
326             and '</Include>' in in_lines[end]
327             and '#' not in in_lines[end]
328             and len([
329                 i for i in in_lines[start + 1: end]
330                 if 'Filter' in i and '*.conf' in i and '#' not in i]) > 0]
331         if len(include_section_indexes) == 0:
332             out_lines.append('<Include "{}">\n'.format(COLLECTD_CONF_DIR))
333             out_lines.append('        Filter "*.conf"\n')
334             out_lines.append('</Include>\n')
335             config.close()
336             config = sftp.open(COLLECTD_CONF, mode='w')
337             config.writelines(out_lines)
338         config.close()
339         self.__logger.info('Creating backup of collectd.conf...')
340         config = sftp.open(COLLECTD_CONF + '.backup', mode='w')
341         config.writelines(in_lines)
342         config.close()
343         return True
344
345     def check_ceil_plugin_included(self, compute):
346         """Check if ceilometer plugin is included in collectd.conf file.
347         If not, try to enable it.
348
349         Keyword arguments:
350         compute -- compute node instance
351
352         Return boolean value whether ceilometer plugin is included
353         or it's enabling was successful.
354         """
355         ssh, sftp = self.__open_sftp_session(compute.get_ip(), 'root')
356         try:
357             config = sftp.open(COLLECTD_CONF, mode='r')
358         except IOError:
359             self.__logger.error(
360                 'Cannot open {} on node {}'.format(
361                     COLLECTD_CONF, compute.get_id()))
362             return False
363         in_lines = config.readlines()
364         out_lines = in_lines[:]
365         include_section_indexes = [
366             (start, end) for start in range(len(in_lines))
367             for end in range(len(in_lines))
368             if (start < end)
369             and '<Include' in in_lines[start]
370             and COLLECTD_CONF_DIR in in_lines[start]
371             and '#' not in in_lines[start]
372             and '</Include>' in in_lines[end]
373             and '#' not in in_lines[end]
374             and len([
375                 i for i in in_lines[start + 1: end]
376                 if 'Filter' in i and '*.conf' in i and '#' not in i]) > 0]
377         if len(include_section_indexes) == 0:
378             out_lines.append('<Include "{}">\n'.format(COLLECTD_CONF_DIR))
379             out_lines.append('        Filter "*.conf"\n')
380             out_lines.append('</Include>\n')
381             config.close()
382             config = sftp.open(COLLECTD_CONF, mode='w')
383             config.writelines(out_lines)
384         config.close()
385         self.__logger.info('Creating backup of collectd.conf...')
386         config = sftp.open(COLLECTD_CONF + '.backup', mode='w')
387         config.writelines(in_lines)
388         config.close()
389         return True
390
391     def enable_plugins(
392             self, compute, plugins, error_plugins, create_backup=True):
393         """Enable plugins on compute node
394
395         Keyword arguments:
396         compute -- compute node instance
397         plugins -- list of plugins to be enabled
398         error_plugins -- list of tuples with found errors, new entries
399             may be added there (plugin, error_description, is_critical):
400                 plugin -- plug-in name
401                 error_decription -- description of the error
402                 is_critical -- boolean value indicating whether error
403                     is critical
404         create_backup -- boolean value indicating whether backup
405             shall be created
406
407         Return boolean value indicating whether function was successful.
408         """
409         plugins = sorted(plugins)
410         ssh, sftp = self.__open_sftp_session(
411             compute.get_ip(), 'root', 'opnfvapex')
412         plugins_to_enable = plugins[:]
413         for plugin in plugins:
414             plugin_file = '/usr/lib64/collectd/{}.so'.format(plugin)
415             try:
416                 sftp.stat(plugin_file)
417             except IOError:
418                 self.__logger.debug(
419                     'Plugin file {} not found on node'.format(plugin_file)
420                     + ' {0}, plugin {1} will not be enabled'.format(
421                         compute.get_name(), plugin))
422                 error_plugins.append((
423                     plugin, 'plugin file {} not found'.format(plugin_file),
424                     True))
425                 plugins_to_enable.remove(plugin)
426         self.__logger.debug(
427             'Following plugins will be enabled on node {}: {}'.format(
428                 compute.get_name(), ', '.join(plugins_to_enable)))
429         try:
430             config = sftp.open(COLLECTD_CONF, mode='r')
431         except IOError:
432             self.__logger.warning(
433                 'Cannot open {} on node {}'.format(
434                     COLLECTD_CONF, compute.get_name()))
435             return False
436         in_lines = config.readlines()
437         out_lines = []
438         enabled_plugins = []
439         enabled_sections = []
440         in_section = 0
441         comment_section = False
442         uncomment_section = False
443         for line in in_lines:
444             if 'LoadPlugin' in line:
445                 for plugin in plugins_to_enable:
446                     if plugin in line:
447                         commented = '#' in line
448                         # list of uncommented lines which contain LoadPlugin
449                         # for this plugin
450                         loadlines = [
451                             ll for ll in in_lines if 'LoadPlugin' in ll
452                             and plugin in ll and '#' not in ll]
453                         if len(loadlines) == 0:
454                             if plugin not in enabled_plugins:
455                                 line = line.lstrip(string.whitespace + '#')
456                                 enabled_plugins.append(plugin)
457                                 error_plugins.append((
458                                     plugin, 'plugin not enabled in '
459                                     + '{}, trying to enable it'.format(
460                                         COLLECTD_CONF), False))
461                         elif not commented:
462                             if plugin not in enabled_plugins:
463                                 enabled_plugins.append(plugin)
464                             else:
465                                 line = '#' + line
466                                 error_plugins.append((
467                                     plugin, 'plugin enabled more than once '
468                                     + '(additional occurrence of LoadPlugin '
469                                     + 'found in {}), '.format(COLLECTD_CONF)
470                                     + 'trying to comment it out.', False))
471             elif line.lstrip(string.whitespace + '#').find('<Plugin') == 0:
472                 in_section += 1
473                 for plugin in plugins_to_enable:
474                     if plugin in line:
475                         commented = '#' in line
476                         # list of uncommented lines which contain Plugin for
477                         # this plugin
478                         pluginlines = [
479                             pl for pl in in_lines if '<Plugin' in pl
480                             and plugin in pl and '#' not in pl]
481                         if len(pluginlines) == 0:
482                             if plugin not in enabled_sections:
483                                 line = line[line.rfind('#') + 1:]
484                                 uncomment_section = True
485                                 enabled_sections.append(plugin)
486                                 error_plugins.append((
487                                     plugin, 'plugin section found in '
488                                     + '{}, but commented'.format(COLLECTD_CONF)
489                                     + ' out, trying to uncomment it.', False))
490                         elif not commented:
491                             if plugin not in enabled_sections:
492                                 enabled_sections.append(plugin)
493                             else:
494                                 line = '#' + line
495                                 comment_section = True
496                                 error_plugins.append((
497                                     plugin, 'additional occurrence of plugin '
498                                     + 'section found in {}'.format(
499                                         COLLECTD_CONF)
500                                     + ', trying to comment it out.', False))
501             elif in_section > 0:
502                 if comment_section and '#' not in line:
503                     line = '#' + line
504                 if uncomment_section and '#' in line:
505                     line = line[line.rfind('#') + 1:]
506                 if '</Plugin>' in line:
507                     in_section -= 1
508                     if in_section == 0:
509                         comment_section = False
510                         uncomment_section = False
511             elif '</Plugin>' in line:
512                 self.__logger.error(
513                     'Unexpected closure os plugin section on line'
514                     + ' {} in collectd.conf'.format(len(out_lines) + 1)
515                     + ', matching section start not found.')
516                 return False
517             out_lines.append(line)
518         if in_section > 0:
519             self.__logger.error(
520                 'Unexpected end of file collectd.conf, '
521                 + 'closure of last plugin section not found.')
522             return False
523         out_lines = [
524             'LoadPlugin {}\n'.format(plugin) for plugin in plugins_to_enable
525             if plugin not in enabled_plugins] + out_lines
526         for plugin in plugins_to_enable:
527             if plugin not in enabled_plugins:
528                 error_plugins.append((
529                     plugin, 'plugin not enabled in {},'.format(COLLECTD_CONF)
530                     + ' trying to enable it.', False))
531         unenabled_sections = [plugin for plugin in plugins_to_enable
532                               if plugin not in enabled_sections]
533         if unenabled_sections:
534             self.__logger.error(
535                 'Plugin sections for following plugins not found: {}'.format(
536                     ', '.join(unenabled_sections)))
537             return False
538
539         config.close()
540         if create_backup:
541             self.__logger.info('Creating backup of collectd.conf...')
542             config = sftp.open(COLLECTD_CONF + '.backup', mode='w')
543             config.writelines(in_lines)
544             config.close()
545         self.__logger.info('Updating collectd.conf...')
546         config = sftp.open(COLLECTD_CONF, mode='w')
547         config.writelines(out_lines)
548         config.close()
549         diff_command = \
550             "diff {} {}.backup".format(COLLECTD_CONF, COLLECTD_CONF)
551         stdin, stdout, stderr = ssh.exec_command(diff_command)
552         self.__logger.debug(diff_command)
553         for line in stdout.readlines():
554             self.__logger.debug(line.strip())
555         return True
556
557     def restore_config(self, compute):
558         """Restore collectd config file from backup on compute node.
559
560         Keyword arguments:
561         compute -- compute node instance
562         """
563         ssh, sftp = self.__open_sftp_session(
564             compute.get_ip(), 'root', 'opnfvapex')
565
566         self.__logger.info('Restoring config file from backup...')
567         ssh.exec_command("cp {0} {0}.used".format(COLLECTD_CONF))
568         ssh.exec_command("cp {0}.backup {0}".format(COLLECTD_CONF))
569
570     def restart_collectd(self, compute):
571         """Restart collectd on compute node.
572
573         Keyword arguments:
574         compute -- compute node instance
575
576         Retrun tuple with boolean indicating success and list of warnings
577         received during collectd start.
578         """
579
580         def get_collectd_processes(ssh_session):
581             """Get number of running collectd processes.
582
583             Keyword arguments:
584             ssh_session -- instance of SSH session in which to check
585                 for processes
586             """
587             stdin, stdout, stderr = ssh_session.exec_command(
588                 "pgrep collectd")
589             return len(stdout.readlines())
590
591         ssh, sftp = self.__open_sftp_session(
592             compute.get_ip(), 'root', 'opnfvapex')
593
594         self.__logger.info('Stopping collectd service...')
595         stdout = self.execute_command("service collectd stop", ssh=ssh)
596         time.sleep(10)
597         if get_collectd_processes(ssh):
598             self.__logger.error('Collectd is still running...')
599             return False, []
600         self.__logger.info('Starting collectd service...')
601         stdout = self.execute_command("service collectd start", ssh=ssh)
602         time.sleep(10)
603         warning = [output.strip() for output in stdout if 'WARN: ' in output]
604         if get_collectd_processes(ssh) == 0:
605             self.__logger.error('Collectd is still not running...')
606             return False, warning
607         return True, warning
608
609     def test_gnocchi_is_sending_data(self, controller):
610         """ Checking if Gnocchi is sending metrics to controller"""
611         metric_ids = []
612         timestamps1 = {}
613         timestamps2 = {}
614         ssh, sftp = self.__open_sftp_session(
615             controller.get_ip(), 'root', 'opnfvapex')
616
617         self.__logger.info('Getting gnocchi metric list on{}'.format(
618             controller.get_name()))
619         stdout = self.execute_command(
620             "source overcloudrc.v3;gnocchi metric list | grep if_packets",
621             ssh=ssh)
622         for line in stdout:
623             metric_ids = [r.split('|')[1] for r in stdout]
624         self.__logger.info("Metric ids = {}" .format(metric_ids))
625         for metric_id in metric_ids:
626             metric_id = metric_id.replace("u", "")
627             stdout = self.execute_command(
628                 "source overcloudrc.v3;gnocchi measures show {}" .format(
629                     metric_id), ssh=ssh)
630             self.__logger.info("stdout measures ={}" .format(stdout))
631             for line in stdout:
632                 if line[0] == '+':
633                     pass
634                 else:
635                     self.__logger.info("Line = {}" .format(line))
636                     timestamps1 = [line.split('|')[1]]
637             self.__logger.info("Last line timetamp1 = {}" .format(timestamps1))
638             time.sleep(10)
639             stdout = self.execute_command(
640                 "source overcloudrc.v3;gnocchi measures show {}" .format(
641                     metric_id), ssh=ssh)
642             for line in stdout:
643                 if line[0] == '+':
644                     pass
645                 else:
646                     timestamps2 = [line.split('|')[1]]
647             self.__logger.info("Last line timetamp2 = {}" .format(timestamps2))
648             if timestamps1 == timestamps2:
649                 self.__logger.info("False")
650                 # return False
651                 return True
652             else:
653                 self.__logger.info("True")
654                 return True