From 2873ff21e4f99b35ab88595a96c0ee45c83d26c3 Mon Sep 17 00:00:00 2001 From: Shubhendu Tripathi Date: Tue, 1 Apr 2014 15:07:44 +0530 Subject: gluster-nagios-common: Added gluster cli module Introduced gluster cli module to add all the gluster related get methods Change-Id: I440ae89ac3f93f961024a6e78870154f57b7dfbd Signed-off-by: Shubhendu Tripathi Reviewed-on: https://code.engineering.redhat.com/gerrit/22253 Reviewed-by: Darshan Narayana Murthy Reviewed-by: Timothy Asir Jeyasingh Reviewed-by: Balamurugan Arumugam Reviewed-by: Sahina Bose Tested-by: Sahina Bose --- glusternagios/Makefile.am | 2 + glusternagios/glustercli.py | 469 ++++++++++++++++++++++++++++++++++++++++++++ glusternagios/hostname.py | 41 ++++ 3 files changed, 512 insertions(+) create mode 100755 glusternagios/glustercli.py create mode 100644 glusternagios/hostname.py (limited to 'glusternagios') diff --git a/glusternagios/Makefile.am b/glusternagios/Makefile.am index 55f8642..7f46e08 100644 --- a/glusternagios/Makefile.am +++ b/glusternagios/Makefile.am @@ -1,4 +1,6 @@ dist_glusternagioscommonpylib_PYTHON = \ __init__.py \ + glustercli.py \ + hostname.py \ utils.py \ $(NULL) diff --git a/glusternagios/glustercli.py b/glusternagios/glustercli.py new file mode 100755 index 0000000..0a126e7 --- /dev/null +++ b/glusternagios/glustercli.py @@ -0,0 +1,469 @@ +# Copyright 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Refer to the README and COPYING files for full details of the license +# + +import xml.etree.cElementTree as etree +import ethtool + +import utils +from utils import CommandPath +from hostname import getHostNameFqdn, HostNameException + +glusterCmdPath = CommandPath("gluster", + "/usr/sbin/gluster") + + +# Class for exception definition +class GlusterCmdFailedException(Exception): + message = "command execution failed" + + def __init__(self, rc=0, out=(), err=()): + self.rc = rc + self.out = out + self.err = err + + def __str__(self): + o = '\n'.join(self.out) + e = '\n'.join(self.err) + if o and e: + m = o + '\n' + e + else: + m = o or e + + s = self.message + if m: + s += '\nerror: ' + m + if self.rc: + s += '\nreturn code: %s' % self.rc + return s + + +if hasattr(etree, 'ParseError'): + _etreeExceptions = (etree.ParseError, AttributeError, ValueError) +else: + _etreeExceptions = (SyntaxError, AttributeError, ValueError) + + +def _getGlusterVolCmd(): + return [glusterCmdPath.cmd, "--mode=script", "volume"] + + +def _getGlusterPeerCmd(): + return [glusterCmdPath.cmd, "--mode=script", "peer"] + + +def _getGlusterSystemCmd(): + return [glusterCmdPath.cmd, "system::"] + + +class HostStatus: + CONNECTED = 'CONNECTED' + DISCONNECTED = 'DISCONNECTED' + UNKNOWN = 'UNKNOWN' + + +class VolumeStatus: + ONLINE = 'ONLINE' + OFFLINE = 'OFFLINE' + + +class TransportType: + TCP = 'TCP' + RDMA = 'RDMA' + + +class TaskType: + REBALANCE = 'REBALANCE' + REPLACE_BRICK = 'REPLACE_BRICK' + REMOVE_BRICK = 'REMOVE_BRICK' + + +def _getaddr(dev): + dev_info_list = ethtool.get_interfaces_info(dev.encode('utf8')) + addr = dev_info_list[0].ipv4_address + if addr is None: + addr = '' + return addr + + +def _getIpAddresses(): + devinfo = {} + for dev in ethtool.get_active_devices(): + try: + devinfo[dev] = ethtool.get_ipaddr(dev) + except IOError, e: + print e + + return devinfo + + +def _getGlusterHostName(): + try: + return getHostNameFqdn() + except HostNameException: + return '' + + +def _getLocalIpAddress(): + for ip in _getIpAddresses(): + if not ip.startswith('127.'): + return ip + return '' + + +def _execGluster(cmd): + return utils.execCmd(cmd) + + +def _execGlusterXml(cmd): + cmd.append('--xml') + rc, out, err = utils.execCmd(cmd) + if rc != 0: + raise GlusterCmdFailedException(rc, out, err) + try: + tree = etree.fromstring('\n'.join(out)) + rv = int(tree.find('opRet').text) + msg = tree.find('opErrstr').text + errNo = int(tree.find('opErrno').text) + except _etreeExceptions: + raise GlusterCmdFailedException(err=out) + if rv == 0: + return tree + else: + if errNo != 0: + rv = errNo + raise GlusterCmdFailedException(rc=rv, err=[msg]) + + +def hostUUIDGet(): + command = _getGlusterSystemCmd() + ["uuid", "get"] + rc, out, err = _execGluster(command) + if rc == 0: + for line in out: + if line.startswith('UUID: '): + return line[6:] + + raise GlusterCmdFailedException() + + +def _parseVolumeStatus(tree): + status = {'name': tree.find('volStatus/volumes/volume/volName').text, + 'bricks': [], + 'nfs': [], + 'shd': []} + hostname = _getLocalIpAddress() or _getGlusterHostName() + for el in tree.findall('volStatus/volumes/volume/node'): + value = {} + + for ch in el.getchildren(): + value[ch.tag] = ch.text or '' + + if value['path'] == 'localhost': + value['path'] = hostname + + if value['status'] == '1': + value['status'] = 'ONLINE' + else: + value['status'] = 'OFFLINE' + + if value['hostname'] == 'NFS Server': + status['nfs'].append({'hostname': value['path'], + 'port': value['port'], + 'status': value['status'], + 'pid': value['pid']}) + elif value['hostname'] == 'Self-heal Daemon': + status['shd'].append({'hostname': value['path'], + 'status': value['status'], + 'pid': value['pid']}) + else: + status['bricks'].append({'brick': '%s:%s' % (value['hostname'], + value['path']), + 'port': value['port'], + 'status': value['status'], + 'pid': value['pid']}) + return status + + +def _parseVolumeStatusDetail(tree): + status = {'name': tree.find('volStatus/volumes/volume/volName').text, + 'bricks': []} + for el in tree.findall('volStatus/volumes/volume/node'): + value = {} + + for ch in el.getchildren(): + value[ch.tag] = ch.text or '' + + sizeTotal = int(value['sizeTotal']) + value['sizeTotal'] = sizeTotal / (1024.0 * 1024.0) + sizeFree = int(value['sizeFree']) + value['sizeFree'] = sizeFree / (1024.0 * 1024.0) + status['bricks'].append({'brick': '%s:%s' % (value['hostname'], + value['path']), + 'sizeTotal': '%.3f' % (value['sizeTotal'],), + 'sizeFree': '%.3f' % (value['sizeFree'],), + 'device': value['device'], + 'blockSize': value['blockSize'], + 'mntOptions': value['mntOptions'], + 'fsName': value['fsName']}) + return status + + +def _parseVolumeStatusClients(tree): + status = {'name': tree.find('volStatus/volumes/volume/volName').text, + 'bricks': []} + for el in tree.findall('volStatus/volumes/volume/node'): + hostname = el.find('hostname').text + path = el.find('path').text + + clientsStatus = [] + for c in el.findall('clientsStatus/client'): + clientValue = {} + for ch in c.getchildren(): + clientValue[ch.tag] = ch.text or '' + clientsStatus.append({'hostname': clientValue['hostname'], + 'bytesRead': clientValue['bytesRead'], + 'bytesWrite': clientValue['bytesWrite']}) + + status['bricks'].append({'brick': '%s:%s' % (hostname, path), + 'clientsStatus': clientsStatus}) + return status + + +def _parseVolumeStatusMem(tree): + status = {'name': tree.find('volStatus/volumes/volume/volName').text, + 'bricks': []} + for el in tree.findall('volStatus/volumes/volume/node'): + brick = {'brick': '%s:%s' % (el.find('hostname').text, + el.find('path').text), + 'mallinfo': {}, + 'mempool': []} + + for ch in el.find('memStatus/mallinfo').getchildren(): + brick['mallinfo'][ch.tag] = ch.text or '' + + for c in el.findall('memStatus/mempool/pool'): + mempool = {} + for ch in c.getchildren(): + mempool[ch.tag] = ch.text or '' + brick['mempool'].append(mempool) + + status['bricks'].append(brick) + return status + + +def volumeStatus(volumeName, brick=None, option=None): + """ + Get volume status + + Arguments: + * VolumeName + * brick + * option = 'detail' or 'clients' or 'mem' or None + Returns: + When option=None, + {'name': NAME, + 'bricks': [{'brick': BRICK, + 'port': PORT, + 'status': STATUS, + 'pid': PID}, ...], + 'nfs': [{'hostname': HOST, + 'port': PORT, + 'status': STATUS, + 'pid': PID}, ...], + 'shd: [{'hostname': HOST, + 'status': STATUS, + 'pid': PID}, ...]} + + When option='detail', + {'name': NAME, + 'bricks': [{'brick': BRICK, + 'sizeTotal': SIZE, + 'sizeFree': FREESIZE, + 'device': DEVICE, + 'blockSize': BLOCKSIZE, + 'mntOptions': MOUNTOPTIONS, + 'fsName': FSTYPE}, ...]} + + When option='clients': + {'name': NAME, + 'bricks': [{'brick': BRICK, + 'clientsStatus': [{'hostname': HOST, + 'bytesRead': BYTESREAD, + 'bytesWrite': BYTESWRITE}, ...]}, + ...]} + + When option='mem': + {'name': NAME, + 'bricks': [{'brick': BRICK, + 'mallinfo': {'arena': int, + 'fordblks': int, + 'fsmblks': int, + 'hblkhd': int, + 'hblks': int, + 'keepcost': int, + 'ordblks': int, + 'smblks': int, + 'uordblks': int, + 'usmblks': int}, + 'mempool': [{'allocCount': int, + 'coldCount': int, + 'hotCount': int, + 'maxAlloc': int, + 'maxStdAlloc': int, + 'name': NAME, + 'padddedSizeOf': int, + 'poolMisses': int},...]}, ...]} + """ + command = _getGlusterVolCmd() + ["status", volumeName] + if brick: + command.append(brick) + if option: + command.append(option) + try: + xmltree = _execGlusterXml(command) + except GlusterCmdFailedException as e: + raise GlusterCmdFailedException(rc=e.rc, err=e.err) + try: + if option == 'detail': + return _parseVolumeStatusDetail(xmltree) + elif option == 'clients': + return _parseVolumeStatusClients(xmltree) + elif option == 'mem': + return _parseVolumeStatusMem(xmltree) + else: + return _parseVolumeStatus(xmltree) + except _etreeExceptions: + raise GlusterCmdFailedException(err=[etree.tostring(xmltree)]) + + +def _parseVolumeInfo(tree): + """ + {VOLUMENAME: {'brickCount': BRICKCOUNT, + 'bricks': [BRICK1, BRICK2, ...], + 'options': {OPTION: VALUE, ...}, + 'transportType': [TCP,RDMA, ...], + 'uuid': UUID, + 'volumeName': NAME, + 'volumeStatus': STATUS, + 'volumeType': TYPE}, ...} + """ + volumes = {} + for el in tree.findall('volInfo/volumes/volume'): + value = {} + value['volumeName'] = el.find('name').text + value['uuid'] = el.find('id').text + value['volumeType'] = el.find('typeStr').text.upper().replace('-', '_') + status = el.find('statusStr').text.upper() + if status == 'STARTED': + value["volumeStatus"] = VolumeStatus.ONLINE + else: + value["volumeStatus"] = VolumeStatus.OFFLINE + value['brickCount'] = el.find('brickCount').text + value['distCount'] = el.find('distCount').text + value['stripeCount'] = el.find('stripeCount').text + value['replicaCount'] = el.find('replicaCount').text + transportType = el.find('transport').text + if transportType == '0': + value['transportType'] = [TransportType.TCP] + elif transportType == '1': + value['transportType'] = [TransportType.RDMA] + else: + value['transportType'] = [TransportType.TCP, TransportType.RDMA] + value['bricks'] = [] + value['options'] = {} + value['bricksInfo'] = [] + for b in el.findall('bricks/brick'): + value['bricks'].append(b.text) + for o in el.findall('options/option'): + value['options'][o.find('name').text] = o.find('value').text + for d in el.findall('bricks/brick'): + brickDetail = {} + #this try block is to maintain backward compatibility + #it returns an empty list when gluster doesnot return uuid + try: + brickDetail['name'] = d.find('name').text + #brickDetail['hostUuid'] = d.find('hostUuid').text + value['bricksInfo'].append(brickDetail) + except AttributeError: + break + volumes[value['volumeName']] = value + return volumes + + +def volumeInfo(volumeName=None, remoteServer=None): + """ + Returns: + {VOLUMENAME: {'brickCount': BRICKCOUNT, + 'bricks': [BRICK1, BRICK2, ...], + 'options': {OPTION: VALUE, ...}, + 'transportType': [TCP,RDMA, ...], + 'uuid': UUID, + 'volumeName': NAME, + 'volumeStatus': STATUS, + 'volumeType': TYPE}, ...} + """ + command = _getGlusterVolCmd() + ["info"] + if remoteServer: + command += ['--remote-host=%s' % remoteServer] + if volumeName: + command.append(volumeName) + try: + xmltree = _execGlusterXml(command) + except GlusterCmdFailedException as e: + raise GlusterCmdFailedException(rc=e.rc, err=e.err) + try: + return _parseVolumeInfo(xmltree) + except _etreeExceptions: + raise GlusterCmdFailedException(err=[etree.tostring(xmltree)]) + + +def _parsePeerStatus(tree, gHostName, gUuid, gStatus): + hostList = [{'hostname': gHostName, + 'uuid': gUuid, + 'status': gStatus}] + + for el in tree.findall('peerStatus/peer'): + if el.find('state').text != '3': + status = HostStatus.UNKNOWN + elif el.find('connected').text == '1': + status = HostStatus.CONNECTED + else: + status = HostStatus.DISCONNECTED + hostList.append({'hostname': el.find('hostname').text, + 'uuid': el.find('uuid').text, + 'status': status}) + + return hostList + + +def peerStatus(): + """ + Returns: + [{'hostname': HOSTNAME, 'uuid': UUID, 'status': STATE}, ...] + """ + command = _getGlusterPeerCmd() + ["status"] + try: + xmltree = _execGlusterXml(command) + except GlusterCmdFailedException as e: + raise GlusterCmdFailedException(rc=e.rc, err=e.err) + try: + return _parsePeerStatus(xmltree, + _getLocalIpAddress() or _getGlusterHostName(), + hostUUIDGet(), HostStatus.CONNECTED) + except _etreeExceptions: + raise GlusterCmdFailedException(err=[etree.tostring(xmltree)]) diff --git a/glusternagios/hostname.py b/glusternagios/hostname.py new file mode 100644 index 0000000..6277569 --- /dev/null +++ b/glusternagios/hostname.py @@ -0,0 +1,41 @@ +# Copyright 2014 Red Hat, Inc. +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA +# +# Refer to the README and COPYING files for full details of the license +# + +import utils + +_hostNameCommandPath = utils.CommandPath("hostname", + "/bin/hostname", + ) + + +class HostNameException(Exception): + def __init__(self, rc): + self.rc = rc + self.message = 'hostname execution failed with error code %s' % self.rc + + def __str__(self): + return self.message + + +def getHostNameFqdn(): + rc, out, err = utils.execCmd([_hostNameCommandPath.cmd, '--fqdn']) + if rc: + raise HostNameException(rc) + else: + return out[0] -- cgit