Repository: ansible/ansible

Issue:
storage: Add remove-brick operation for gluster_volume.
##### ISSUE TYPE
 - Feature Idea
Add volume operation: remove-brick.

##### COMPONENT NAME
system/gluster_volume
storage/glusterfs

##### ANSIBLE VERSION
```
ansible 2.4.1.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/home/devyani/work/gdeploy/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /bin/ansible
  python version = 2.7.13 (default, Dec  1 2017, 09:21:53) [GCC 6.4.1 20170727 (Red Hat 6.4.1-1)]

```

##### CONFIGURATION
ANSIBLE_SSH_CONTROL_PATH(/etc/ansible/ansible.cfg) = %(directory)s/ansible-ssh-%%C
DEFAULT_MODULE_PATH(env: ANSIBLE_LIBRARY) = [u'/home/devyani/work/gluster-ansible/modules']
DEFAULT_ROLES_PATH(/etc/ansible/ansible.cfg) = [u'/home/devyani/work/gluster-ansible/roles']

##### OS / ENVIRONMENT
OS running Ansible: Fedora release 25 (Twenty Five)
OS of Nodes: Red Hat Enterprise Linux Server release 7.4 (Maipo)

##### SUMMARY
With the existing gluster_volume module, remove-brick operation fails when trying
to remove a brick from gluster volume.
Remove-brick operation for Gluster volumes need to be handled.

##### STEPS TO REPRODUCE
The following playbook can be used to perform remove-brick operation.

```yaml
---
- name: Remove a brick
  gluster_volume:
           state=present
           name="{{ volname }}"
           bricks="{{ bricks }}"
           remove_bricks=True
           rebalance=True
           cluster="{{ gluster_hosts }}"
           transport="{{ transport }}"
           replicas="{{ replica_count }}"
           arbiters="{{ arbiter_count }}"
           disperses="{{ disperse_count }}"
           redundancies="{{ redundancy_count }}"
           force="{{ force }}"

```

##### EXPECTED RESULTS
Using the following playbook, the 'bricks' should be removed from Gluster volume.

```
$ gluster volume info
Volume Name: sample_vol
Type: Distributed-Replicate
Volume ID: d7161ff8-1bed-4a67-8e84-5394d57c3c0d
Status: Started
Snapshot Count: 0
Number of Bricks: 2 x 2 = 4
Transport-type: tcp
Bricks:
Brick1: 10.70.41.212:/mnt/brick3/b3
Brick2: 10.70.41.212:/mnt/brick4/b4
Brick3: 10.70.42.156:/mnt/brick3/b3
Brick4: 10.70.42.156:/mnt/brick4/b4
Options Reconfigured:
transport.address-family: inet
nfs.disable: on
```

##### ACTUAL RESULTS
Bricks are not removed from the Gluster volume.




Discussion:
Files identified in the description:
* [lib/ansible/modules/system/gluster_volume.py](https://github.com/ansible/ansible/blob/devel/lib/ansible/modules/system/gluster_volume.py)

If these files are inaccurate, please update the `component name` section of the description or use the `!component` bot command.

[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: components_banner --->
cc @rosmo
[click here for bot help](https://github.com/ansible/ansibullbot/blob/master/ISSUE_HELP.md)
<!--- boilerplate: notify --->

Resolving patch:
diff --git a/lib/ansible/modules/storage/glusterfs/gluster_volume.py b/lib/ansible/modules/storage/glusterfs/gluster_volume.py
--- a/lib/ansible/modules/storage/glusterfs/gluster_volume.py
+++ b/lib/ansible/modules/storage/glusterfs/gluster_volume.py
@@ -153,6 +153,27 @@
       - 192.0.2.10
       - 192.0.2.11
   run_once: true
+
+- name: Remove the bricks from gluster volume
+  gluster_volume:
+    state: present
+    name: testvol
+    bricks: /bricks/brick1/b1,/bricks/brick2/b2
+    cluster:
+      - 10.70.42.85
+    force: true
+  run_once: true
+
+- name: Reduce cluster configuration
+  gluster_volume:
+    state: present
+    name: testvol
+    bricks: /bricks/brick3/b1,/bricks/brick4/b2
+    replicas: 2
+    cluster:
+      - 10.70.42.85
+    force: true
+  run_once: true
 """
 
 import re
@@ -243,6 +264,8 @@ def get_volumes():
                     volume['arbiters'] = []
                 value = value[:-10]
                 volume['arbiters'].append(value)
+            elif key.lower() == 'number of bricks':
+                volume['replicas'] = value[-1:]
             if key.lower() != 'bricks' and key.lower()[:5] == 'brick':
                 if 'bricks' not in volume:
                     volume['bricks'] = []
@@ -354,6 +377,57 @@ def add_bricks(name, new_bricks, stripe, replica, force):
     run_gluster(args)
 
 
+def remove_bricks(name, removed_bricks, force):
+    # max-tries=12 with default_interval=10 secs
+    max_tries = 12
+    retries = 0
+    success = False
+    args = ['volume', 'remove-brick', name]
+    args.extend(removed_bricks)
+    # create a copy of args to use for commit operation
+    args_c = args[:]
+    args.append('start')
+    run_gluster(args)
+    # remove-brick operation needs to be followed by commit operation.
+    if not force:
+        module.fail_json(msg="Force option is mandatory.")
+    else:
+        while retries < max_tries:
+            last_brick = removed_bricks[-1]
+            out = run_gluster(['volume', 'remove-brick', name, last_brick, 'status'])
+            for row in out.split('\n')[1:]:
+                if 'completed' in row:
+                    # remove-brick successful, call commit operation.
+                    args_c.append('commit')
+                    out = run_gluster(args_c)
+                    success = True
+                    break
+                else:
+                    time.sleep(10)
+            if success:
+                break
+            retries += 1
+        if not success:
+            # remove-brick still in process, needs to be committed after completion.
+            module.fail_json(msg="Exceeded number of tries, check remove-brick status.\n"
+                                 "Commit operation needs to be followed.")
+
+
+def reduce_config(name, removed_bricks, replicas, force):
+    out = run_gluster(['volume', 'heal', name, 'info'])
+    summary = out.split("\n")
+    for line in summary:
+        if 'Number' in line and int(line.split(":")[1].strip()) != 0:
+            module.fail_json(msg="Operation aborted, self-heal in progress.")
+    args = ['volume', 'remove-brick', name, 'replica', replicas]
+    args.extend(removed_bricks)
+    if force:
+        args.append('force')
+    else:
+        module.fail_json(msg="Force option is mandatory")
+    run_gluster(args)
+
+
 def do_rebalance(name):
     run_gluster(['volume', 'rebalance', name, 'start'])
 
@@ -465,22 +539,31 @@ def main():
             new_bricks = []
             removed_bricks = []
             all_bricks = []
+            bricks_in_volume = volumes[volume_name]['bricks']
+
             for node in cluster:
                 for brick_path in brick_paths:
                     brick = '%s:%s' % (node, brick_path)
                     all_bricks.append(brick)
-                    if brick not in volumes[volume_name]['bricks']:
+                    if brick not in bricks_in_volume:
                         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)
+            if not new_bricks and len(all_bricks) < bricks_in_volume:
+                for brick in bricks_in_volume:
+                    if brick not in all_bricks:
+                        removed_bricks.append(brick)
 
             if new_bricks:
                 add_bricks(volume_name, new_bricks, stripes, replicas, force)
                 changed = True
 
+            if removed_bricks:
+                if replicas and int(replicas) < int(volumes[volume_name]['replicas']):
+                    reduce_config(volume_name, removed_bricks, str(replicas), force)
+                else:
+                    remove_bricks(volume_name, removed_bricks, force)
+                changed = True
+
             # handle quotas
             if quota:
                 if not volumes[volume_name]['quota']:


Test patch: