# 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 VolumeQuotaStatus: DISABLED = 'DISABLED' OK = 'OK' EXCEEDED = 'EXCEEDED' 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 _parseVolumeQuotaStatus(out): for line in out: if line.startswith('quota: No quota') or line.find('not enabled') > -1: return VolumeQuotaStatus.DISABLED if line.find('Yes') > -1: return VolumeQuotaStatus.EXCEEDED return VolumeQuotaStatus.OK def volumeQuotaStatus(volumeName, remoteServer=None): """ Returns: STATUS """ command = _getGlusterVolCmd() + ["quota", volumeName, "list"] if remoteServer: command += ['--remote-host=%s' % remoteServer] rc, out, err = _execGluster(command) if rc == 0: return _parseVolumeQuotaStatus(out) else: if len(err) > 0 and err[0].find("Quota is disabled") > -1: return VolumeQuotaStatus.DISABLED raise GlusterCmdFailedException(rc, err) 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)])