diff --git a/system/glusterfs.py b/system/glusterfs.py new file mode 100644 index 00000000000..238023bbe64 --- /dev/null +++ b/system/glusterfs.py @@ -0,0 +1,407 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, Taneli Leppä +# +# This file is part of Ansible (sort of) +# +# Ansible 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 3 of the License, or +# (at your option) any later version. +# +# Ansible 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 Ansible. If not, see . + + +DOCUMENTATION = """ +module: glusterfs +short_description: manage GlusterFS +description: + - Manage GlusterFS volumes +options: + action: + required: true + choices: [ 'create', 'start', 'stop', 'tune', 'rebalance', 'limit-usage' ] + description: + - Mode of operation: create volume, start/stop volume, set tuning options, rebalance or set quota + name: + required: true + description: + - The volume name + hosts: + required: false + description: + - List of hosts to use for probing and brick setup + host: + required: false + description: + - Override local hostname (for peer probing purposes) + glusterbin: + required: false + default: /usr/sbin/gluster + description: + - Override gluster cli path + replica: + required: false + description: + - Replica count for volume + stripe: + required: false + description: + - Stripe count for volume + transport: + required: false + choices: [ 'tcp', 'rdma', 'tcp,rdma' ] + description: + - Transport type for volume + brick: + required: false + description: + - Brick path on servers + start: + required: false + description: + - Controls whether the volume is started after creation or not + rebalance: + required: false + description: + - Controls whether the volume is rebalanced after adding bricks or not + option: + required: false + description: + - Tuning parameter name when action=tune + parameter: + required: false + description: + - Tuning parameter value when action=tune + directory: + required: false + description: + - Directory for limit-usage + value: + required: false + description: + - Quota value for limit-usage (be sure to use 10.0MB instead of 10MB, see quota list) +notes: + - "Requires cli tools for GlusterFS on servers" + - "Will add new bricks, but not remove them" +author: Taneli Leppä +""" + +EXAMPLES = """ +- name: create gluster volume + glusterfs: action=create name=test1 brick=/bricks/brick1/g1 rebalance=yes + args: + hosts: "{{ play_hosts }}" + run_once: true + +- name: tune + glusterfs: action=tune name=test1 option=performance.cache-size parameter=256MB + run_once: true + +- name: start gluster volume + glusterfs: action=start name=test1 + run_once: true + +- name: limit usage + glusterfs: action=limit-usage name=test1 directory=/foo value=20.0MB + run_once: true + +- name: stop gluster volume + glusterfs: action=stop name=test1 + run_once: true +""" + +import os +import shutil +import time +import socket +import re + +def main(): + module = AnsibleModule( + argument_spec=dict( + action=dict(required=True, default=None, choices=[ 'create', 'start', 'stop', 'tune', 'rebalance', 'limit-usage' ]), + name=dict(required=True, default=None, aliases=['volume']), + hosts=dict(required=False, default=None, type='list'), + host=dict(required=False, default=None), + stripe=dict(required=False, default=None, type='int'), + replica=dict(required=False, default=None, type='int'), + transport=dict(required=False, default='tcp', choices=[ 'tcp', 'rdma', 'tcp,rdma' ]), + brick=dict(required=False, default=None), + start=dict(required=False, default='1'), + rebalance=dict(required=False, default='0'), + option=dict(required=False, default=None), + parameter=dict(required=False, default=None), + value=dict(required=False, default=None), + directory=dict(required=False, default=None), + glusterbin=dict(required=False, default='/usr/sbin/gluster'), + ) + ) + + changed = False + action = module.params['action'] + volume_name = module.params['name'] + glusterbin = module.params['glusterbin'] + hosts = module.params['hosts'] + brick_path = module.params['brick'] + stripe = module.params['stripe'] + replica = module.params['replica'] + transport = module.params['transport'] + myhostname = module.params['host'] + start_volume = module.boolean(module.params['start']) + rebalance = module.boolean(module.params['rebalance']) + if not myhostname: + myhostname = socket.gethostname() + if not os.path.exists(glusterbin): + module.fail_json(msg='could not find gluster commandline client at %s' % glusterbin) + + set_option = module.params['option'] + set_parameter = module.params['parameter'] + value = module.params['value'] + directory = module.params['directory'] + + def run_gluster(gargs, **kwargs): + args = [glusterbin] + args.extend(gargs) + rc, out, err = module.run_command(args, **kwargs) + if rc != 0: + module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out if out != '' else err)) + return out + + def run_gluster_nofail(gargs, **kwargs): + args = [glusterbin] + args.extend(gargs) + rc, out, err = module.run_command(args, **kwargs) + if rc != 0: + return None + return out + + def run_gluster_yes(gargs): + args = [glusterbin] + args.extend(gargs) + rc, out, err = module.run_command(args, data='y\n') + if rc != 0: + module.fail_json(msg='error running gluster (%s) command (rc=%d): %s' % (' '.join(args), rc, out if out != '' else err)) + return out + + def get_peers(): + out = run_gluster([ 'peer', 'status']) + i = 0 + peers = {} + hostname = None + uuid = None + state = None + for row in out.split('\n'): + if ': ' in row: + key, value = row.split(': ') + if key.lower() == 'hostname': + hostname = value + if key.lower() == 'uuid': + uuid = value + if key.lower() == 'state': + state = value + peers[hostname] = [ uuid, state ] + return peers + + def get_volumes(): + out = run_gluster([ 'volume', 'info' ]) + + volumes = {} + volume = {} + for row in out.split('\n'): + if ': ' in row: + key, value = row.split(': ') + if key.lower() == 'volume name': + volume['name'] = value + volume['options'] = {} + volume['quota'] = False + if key.lower() == 'volume id': + volume['id'] = value + if key.lower() == 'status': + volume['status'] = value + if key.lower() == 'transport-type': + volume['transport'] = value + if key.lower() != 'bricks' and key.lower()[:5] == 'brick': + if not 'bricks' in volume: + volume['bricks'] = [] + volume['bricks'].append(value) + # Volume options + if '.' in key: + if not 'options' in volume: + volume['options'] = {} + volume['options'][key] = value + if key == 'features.quota' and value == 'on': + volume['quota'] = True + else: + if row.lower() != 'bricks:' and row.lower() != 'options reconfigured:': + if len(volume) > 0: + volumes[volume['name']] = volume + volume = {} + return volumes + + def get_quotas(name, nofail): + quotas = {} + if nofail: + out = run_gluster_nofail([ 'volume', 'quota', name, 'list' ]) + if not out: + return quotas + else: + out = run_gluster([ 'volume', 'quota', name, 'list' ]) + for row in out.split('\n'): + if row[:1] == '/': + q = re.split('\s+', row) + quotas[q[0]] = q[1] + return quotas + + def wait_for_peer(host): + for x in range(0, 4): + peers = get_peers() + if host in peers and peers[host][1].lower().find('peer in cluster') != -1: + return True + time.sleep(1) + return False + + def probe(host): + run_gluster([ 'peer', 'probe', host ]) + if not wait_for_peer(host): + module.fail_json(msg='failed to probe peer %s' % host) + changed = True + + def probe_all_peers(hosts, peers): + for host in hosts: + if host not in peers: + # dont probe ourselves + if myhostname != host: + probe(host) + + def create_volume(name, stripe, replica, transport, hosts, brick): + args = [ 'volume', 'create' ] + args.append(name) + if stripe: + args.append('stripe') + args.append(str(stripe)) + if replica: + args.append('replica') + args.append(str(replica)) + args.append('transport') + args.append(transport) + for host in hosts: + args.append(('%s:%s' % (host, brick))) + run_gluster(args) + + def start_volume(name): + run_gluster([ 'volume', 'start', name ]) + + def stop_volume(name): + run_gluster_yes([ 'volume', 'stop', name ]) + + def set_volume_option(name, option, parameter): + run_gluster([ 'volume', 'set', name, option, parameter ]) + + def add_brick(name, brick): + run_gluster([ 'volume', 'add-brick', name, brick ]) + + def rebalance(name): + run_gluster(['volume', 'rebalance', name, 'start']) + + def enable_quota(name): + run_gluster([ 'volume', 'quota', name, 'enable' ]) + + def set_quota(name, directory, value): + run_gluster([ 'volume', 'quota', name, 'limit-usage', directory, value ]) + + # + peers = get_peers() + volumes = get_volumes() + quotas = {} + if volume_name in volumes and volumes[volume_name]['quota'] and volumes[volume_name]['status'].lower() == 'started': + quotas = get_quotas(volume_name, True) + if action == 'create': + probe_all_peers(hosts, peers) + if volume_name not in volumes: + create_volume(volume_name, stripe, replica, transport, hosts, brick_path) + changed = True + volumes = get_volumes() + if volume_name in volumes: + if volumes[volume_name]['status'].lower() != 'started' and start_volume: + start_volume(volume_name) + changed = True + + # switch bricks + new_bricks = [] + removed_bricks = [] + all_bricks = [] + for host in hosts: + brick = '%s:%s' % (host, brick_path) + all_bricks.append(brick) + if brick not in volumes[volume_name]['bricks']: + new_bricks.append(brick) + + # this module does not yet remove bricks, but we check those anyways + for brick in volumes[volume_name]['bricks']: + if brick not in all_bricks: + removed_bricks.append(brick) + + for brick in new_bricks: + add_brick(volume_name, brick) + changed = True + + if len(new_bricks) > 0 and rebalance: + rebalance(volume_name) + + else: + module.fail_json(msg='failed to create volume %s' % volume_name) + if action == 'start': + if volume_name not in volumes: + module.fail_json(msg='volume not found %s' % volume_name) + if volumes[volume_name]['status'].lower() != 'started': + start_volume(volume_name) + volumes = get_volumes() + changed = True + if action == 'rebalance': + if volume_name not in volumes: + module.fail_json(msg='volume not found %s' % volume_name) + rebalance(volume_name) + changed = True + if action == 'stop': + if volume_name not in volumes: + module.fail_json(msg='volume not found %s' % volume_name) + if volumes[volume_name]['status'].lower() != 'stopped': + stop_volume(volume_name) + volumes = get_volumes() + changed = True + if action == 'tune': + if volume_name not in volumes: + module.fail_json(msg='volume not found %s' % volume_name) + if set_option not in volumes[volume_name]['options'] or volumes[volume_name]['options'][set_option] != set_parameter: + set_volume_option(volume_name, set_option, set_parameter) + volumes = get_volumes() + changed = True + if action == 'limit-usage': + if volume_name not in volumes: + module.fail_json(msg='volume not found %s' % volume_name) + if not volumes[volume_name]['quota']: + enable_quota(volume_name) + + quotas = get_quotas(volume_name, False) + if directory not in quotas: + set_quota(volume_name, directory, value) + changed = True + elif quotas[directory] != value: + set_quota(volume_name, directory, value) + changed = True + facts = {} + facts['glusterfs'] = { 'peers': peers, 'volumes': volumes, 'quotas': quotas } + + module.exit_json(changed=changed, ansible_facts=facts) + +# import module snippets +from ansible.module_utils.basic import * +main()