From 347f359f0cf378b21e8f3ec7e99d00d47af5fd27 Mon Sep 17 00:00:00 2001 From: Ashish Pandey Date: Thu, 2 May 2019 18:08:27 +0530 Subject: [PATCH] First version --- .gitignore | 6 + __init__.py | 0 extras/gf_pos_testing.py | 99 ++++++++++ new-node.txt | 12 ++ pos_main.py | 173 +++++++++++++++++ scale/__init__.py | 0 scale/brick-disk-map.py | 52 +++++ scale/gf_api.py | 301 +++++++++++++++++++++++++++++ scale/gf_exceptions.py | 17 ++ scale/gf_logs.py | 18 ++ scale/gf_plus_one_scale.py | 242 +++++++++++++++++++++++ setup.py | 39 ++++ test_data/__init__.py | 0 test_data/gf_plus_one_test_data.py | 146 ++++++++++++++ testing.py | 16 ++ 15 files changed, 1121 insertions(+) create mode 100644 .gitignore create mode 100644 __init__.py create mode 100644 extras/gf_pos_testing.py create mode 100644 new-node.txt create mode 100644 pos_main.py create mode 100644 scale/__init__.py create mode 100644 scale/brick-disk-map.py create mode 100644 scale/gf_api.py create mode 100644 scale/gf_exceptions.py create mode 100644 scale/gf_logs.py create mode 100755 scale/gf_plus_one_scale.py create mode 100644 setup.py create mode 100755 test_data/__init__.py create mode 100755 test_data/gf_plus_one_test_data.py create mode 100644 testing.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c207e8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__/ +*.pyc +*.pyo +*.log +dist + diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/extras/gf_pos_testing.py b/extras/gf_pos_testing.py new file mode 100644 index 0000000..e5ebffa --- /dev/null +++ b/extras/gf_pos_testing.py @@ -0,0 +1,99 @@ +from test_data.gf_plus_one_test_data import * +from scale.gf_plus_one_scale import * + +# Test code +def test(): + print ("***********************************************************") + fname = create_new_node(12,4) + v_info = gf_bricks_from_file(fname) + for br in v_info: + print(br) + + print ("***********************************************************") + + fname = create_gluster_v_info(6,4,2) + v_info = gf_bricks_from_file(fname) + + for br in v_info: + print(br) + + print ("***********************************************************") + + print ("volume_brick_list") + node_brick_list = gf_get_volume_bricks_list(v_info) + print (node_brick_list) + for elm in node_brick_list: + print (elm) + + + print ("***********************************************************") + print ("node_brick_dict") + node_brick = gf_get_node_bricks_dict(v_info) + for key,val in node_brick.items(): + print (key, val) + print ('\n') + + print ("***********************************************************") + print ("subvol_brick_list") + node_b = gf_get_volume_bricks_list(v_info) + + subvol_brick_list = gf_subvol_bricks_dict(node_b, 6) + count = 0 + for elm in subvol_brick_list: + print("ec -subvolume-"+ str (count)) + print(elm) + count += 1 + + print ("***********************************************************") +''' + print ("Volume info") + volume_info = gf_get_volume_info ('vol') + for elm in volume_info: + print (elm) + + print ("***********************************************************") + + print ("Volume config") + volume_config = gf_get_volume_config(volume_info) + print (volume_config) + + + print ("***********************************************************") + + print ("volume_brick_list") + node_brick_list = gf_get_volume_bricks_list(volume_info) + for elm in node_brick_list: + print (elm) + + + print ("***********************************************************") + + + print ("node_brick_dict") + node_brick = gf_get_node_bricks_dict(volume_info) + for key,val in node_brick.items(): + print (f"Host = {key} bricks = {val}") + + + print ("***********************************************************") + + print ("subvol_brick_list") + subvol_brick_list = gf_subvol_bricks_list(volume_info) + count = 0 + for elm in subvol_brick_list: + print("ec -subvolume-"+ str (count)) + print(elm) + count += 1 + print ("***********************************************************") +''' + + +if True: + test() + + + + + + + diff --git a/new-node.txt b/new-node.txt new file mode 100644 index 0000000..924cea4 --- /dev/null +++ b/new-node.txt @@ -0,0 +1,12 @@ +node-4:/root/brick-new-1 +node-4:/root/brick-new-2 +node-4:/root/brick-new-3 +node-4:/root/brick-new-4 +node-4:/root/brick-new-5 +node-4:/root/brick-new-6 +node-4:/root/brick-new-7 +node-4:/root/brick-new-8 +node-4:/root/brick-new-9 +node-4:/root/brick-new-10 +node-4:/root/brick-new-11 +node-4:/root/brick-new-12 diff --git a/pos_main.py b/pos_main.py new file mode 100644 index 0000000..bb05d38 --- /dev/null +++ b/pos_main.py @@ -0,0 +1,173 @@ +"""This script facilitates plus one scaling for disperse volume.""" + + +import os +import scale.gf_plus_one_scale as sc +import test_data.gf_plus_one_test_data as test +from scale.gf_logs import glogger +from scale.gf_exceptions import (VolumeNotHealthy, + GfCommandFailed) +from scale.gf_api import (gluster_volume_is_ready_to_scale, + gf_are_bricks_sufficient, + gluster_volume_is_ready_to_scale, + gluster_create_brick_map_to_swap, + gluster_volume_commit_drive_swap, + gluster_check_volume_health, + gluster_check_bricks_status) + +def yes(value): + """Return True if value is Y or Yes in any case.""" + return value.lower() in ["y", "yes"] + + +def gf_get_volume_and_bricks(): + """Get volume name, new node and file name from user.""" + volname = input("Enter name of the volume: ") + filename = input("Enter file name which contains new bricks: ") + hostname = input("Enter IP/hostname of new node: ") + + if not volname or not filename or not hostname: + glogger.debug(f'Invalid user input: volname = {volname}, filename = {filename}, hostname = {hostname}') + raise ValueError("volname or filename or hostname is not correct") + + return volname, filename, hostname + + +def gluster_volume_commit_all(volname, bricks_migration_map): + """Commit physical replacment of all the bricks. + + Input: + volname: Name of the volume + bricks_migration_map: Physical migration map of old and new bricks. + """ + for old_brick, new_brick in bricks_migration_map: + glogger.info(f'Commiting replace bricks for old_brick = {old_brick} and new_brick = {new_brick}') + print(f'Commiting replace bricks for old_brick = {old_brick} and new_brick = {new_brick}') + try: + gluster_volume_commit_drive_swap(volname, old_brick, new_brick) + except GfCommandFailed as er: + glogger.exception(f'replace brick failed for {old_brick} and {new_brick}') + continue + else: + glogger.info(f'replace brick is done for {old_brick} and {new_brick}') + + +def gf_swap_all_bricks(volname, bricks_migration_map): + """Swap all the bricks. + + This function helps to swap all the bricks first and then commit + the physical migration in one shot. + Input: + volname: Name of the volume + bricks_migration_map: Physical migration map of old and new bricks. + """ + while True: + print("Swap following disks : ") + for br in bricks_migration_map: + print(br) + done = input("Have you finished swapping all the disks: (y/n) ") + if yes(done): + gluster_volume_commit_all(volname, bricks_migration_map) + else: + continue + + +def gf_swap_one_by_one_brick(volname, bricks_migration_map): + """Swap bricks one by one. + + Ask user to swap a pair of old and new brick and then + commit that migration + Input: + volname: Name of the volume + bricks_migration_map: Physical migration map of old and new bricks. + """ + for old_brick, new_brick in bricks_migration_map: + while True: + try: + print(f'Swap {old_brick} and {new_brick}') + done = input("Have you finished swapping above disks: (y/n) ") + if yes(done): + gluster_volume_commit_drive_swap(volname, old_brick, new_brick) + break + else: + continue + except GfCommandFailed as er: + glogger.exception(f'replace brick failed for {old_brick} and {new_brick}') + continue + else: + glogger.info(f'replace brick is done for {old_brick} and {new_brick}') + + +def gf_migrate_bricks_using_swap_map(volname, bricks_migration_map): + """Swap bricks using migration map. + + Ask user how the migration should happen. + Input: + volname: Name of the volume + bricks_migration_map: Physical migration map of old and new bricks. + """ + while True: + try: + print("All together : 1") + print("One brick at a time : 0") + all = int(input("How do you want to swap device: (0 or 1) ")) + if all not in [0, 1]: + print("Please enter correct option.") + continue + else: + break + except: + print("Please enter valid option") + glogger.debug(f'Invalid swap option, enter an interger (0/1)') + done = input("Want to exit? y/N") + if yes(done): + exit(1) + + if (all): + gf_swap_all_bricks(volname, bricks_migration_map) + else: + gf_swap_one_by_one_brick(volname, bricks_migration_map) + + +def main(): + """Main function to start complete plus one scaling process.""" + while True: + try: + volname, filename, hostname = gf_get_volume_and_bricks() + except ValueError as er: + print(er) + glogger.exception(er) + else: + print(f'volname = {volname}') + print(f'filename = {filename}') + print(f'hostname = {hostname}') + break + try: + list_of_empty_drives = sc.gf_get_new_bricks_list(filename) + print ("**************New Bricks*****************") + for br in list_of_empty_drives: + print(br) + print ("****************************************") + except GfCommandFailed as e: + print(e.message) + exit(1) + + if not gluster_volume_is_ready_to_scale(volname, list_of_empty_drives): + print(f'EC volume, {volname}, can not be scaled. Volume not healthy or all the bricks are not UP') + exit(1) + else: + print(f'{volname} can be scaled') + + print ("****************************************") + bricks_migration_map = gluster_create_brick_map_to_swap( + vol_name=volname, new_host=hostname, + list_of_empty_drives=list_of_empty_drives) + + for old, new in bricks_migration_map: + print (f'old_brick={old} and new brick={new}') + + gf_migrate_bricks_using_swap_map(volname, bricks_migration_map) + + +if __name__ == "__main__": + main() diff --git a/scale/__init__.py b/scale/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scale/brick-disk-map.py b/scale/brick-disk-map.py new file mode 100644 index 0000000..e70b3e5 --- /dev/null +++ b/scale/brick-disk-map.py @@ -0,0 +1,52 @@ +"""Script to map a brick to the mounted disk.""" + + +import re +import subprocess as sp + + +def run_gluster_command(cmd): + """Execute gluster commands. + + args: + cmd: String of command. ex. "gluster volume info "" + returns: + tuple (ret,output,err) + ret = return status of command + output = output string + err = error message if any + """ + p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE, shell=True) + (output, err) = p.communicate() + ret = p.returncode + return (ret, output, err) + + +def get_device_file_dict(): + """Get the list of bricks and its mount location.""" + cmd = 'lshw -class disk' + desc = "description" + log_name = "logical name" + serial = "serial" + + dev = [] + dev_list = [] + + ret, output, err = run_gluster_command(cmd) + output = output.decode('ASCII') + dev_info = output.split('\n') + for line in dev_info: + if re.search(desc, line): + if dev: + dev_list.append(dev) + + dev = [] + if re.search(log_name, line) or re.search(serial, line): + temp = line.split(':') + temp[1] = temp[1].strip(' ') + dev.append(temp[1]) + dev_list.append(dev) + for line in dev_list: + print(line) + +get_device_file_dict() diff --git a/scale/gf_api.py b/scale/gf_api.py new file mode 100644 index 0000000..112ecd1 --- /dev/null +++ b/scale/gf_api.py @@ -0,0 +1,301 @@ +"""This module provides API's to scale EC volume.""" + + +import collections +import scale.gf_plus_one_scale as sc +import test_data.gf_plus_one_test_data as test +from scale.gf_logs import glogger +from scale.gf_exceptions import (VolumeNotHealthy, + GfCommandFailed) + + +def gluster_check_bricks_status(vol_name): + """Check gluster volume status. + + Input: + vol_name - Name of the volume. + return : + True - If all the bricks are UP + False - If even a single brick of the volume is not UP + """ + ret = True + bricks_status = sc.gf_get_volume_status_xml(vol_name) + for brick, status in bricks_status.items(): + if not status: + glogger.error(f'Brick Down: {brick}, {status}') + ret = False + + return ret + + +def gluster_check_volume_heal_summary(vol_name): + """Check if a gluster volume needs heal. + + Input: + vol_name - Name of the volume. + return : + True - Volume is healthy, No heal required. + False - Volume is not healthy, heal required. + """ + ret = True + heal_summary = sc.gf_get_volume_heal_summary_xml(vol_name) + if not heal_summary: + return False + + for brick, status in heal_summary.items(): + if status: + glogger.info(f'Need Heal: {brick}, {status}') + ret = False + + return ret + + +def gluster_check_volume_health(vol_name): + """Check if volume is healthy or not. + + Input : + vol_name - Name of the volume. + return : + True - If volume is healthy. All the bricks are UP and nothing + to heal for the volume. + False - If volume is not healthy. + """ + if not gluster_check_volume_heal_summary(vol_name): + glogger.info("{vol_name} is not healthy ") + return False + + if not gluster_check_bricks_status(vol_name): + glogger.info("All the bricks of, {vol_name}, are not UP ") + return False + + glogger.info("{vol_name}, is healthy, all the bricks are UP ") + return True + + +def gf_are_bricks_sufficient(vol_config, list_of_empty_drives): + """Check if the given bricks are sufficient for scaling.""" + """ + Input : volume config as dictionary + list of empty drives + return True - can be scaled + False - Can not scale + """ + disperse_count = vol_config['disperseCount'] + empty_bricks_count = len(list_of_empty_drives) + + if (not empty_bricks_count) or (empty_bricks_count % disperse_count): + glogger.warning(f'Number of empty bricks: {empty_bricks_count}') + return False + + return True + + +def gluster_volume_is_ready_to_scale(vol_name, list_of_empty_drives): + """Check if volume is ready to scale using new bricks. + + Input : + vol_name - Name of the volume needs to be scaled. + list_of_empty_drives - list of empty drives present on any node. + In the format hostname:/dev/drive{1..n} + + return : + True - If volume is ready to be scaled. + False - If volume can not be scaled. + """ + # if not gluster_check_volume_health(vol_name): + # return False + try: + if not gluster_check_volume_health(vol_name): + glogger.error("Can not scale {vol_name}. Volume is not healthy ") + return False + + vol_info = sc.gf_get_volume_info_xml(vol_name) + except GfCommandFailed as e: + glogger.exception(e.message) + return False + + vol_config = sc.gf_get_volume_config(vol_info) + if not gf_are_bricks_sufficient(vol_config, list_of_empty_drives): + glogger.error("Can not scale {vol_name}. \ + Not enough empty bricks: {empty_bricks_count} ") + return False + + return True + + +def gf_max_empty_drive_on_node(empty_disks_on_hosts, vol_config): + """Get maximum number of empty drives any host can have.""" + """ + Input: + empty_disks_on_hosts: Dict of host to empty drive. + vol_config: volume configuration + return: + max_bricks: int, maximum nuber of empty bricks + """ + red_bricks = vol_config['redundancyCount'] + disperse_bricks = vol_config['disperseCount'] + number_of_empty_bricks = int(0) + + for host, bricks in empty_disks_on_hosts.items(): + number_of_empty_bricks += len(bricks) + + max_new_subvols = (number_of_empty_bricks / disperse_bricks) + return max_new_subvols * red_bricks + + +def gf_host_has_max_empty_disks(empty_disks_on_hosts, host_name, vol_config): + """To check if a host already has maximum number of empty drives. + + Input: + empty_disks_on_hosts: Dict of host to list of empty drive. + host_name: hostname on which we want to check + vol_config: volume configuration + return: + True: host_name has maximum allowed empty drive + False: host_name does not have maximum allowed empty drive. + """ + max_new_bricks = gf_max_empty_drive_on_node(empty_disks_on_hosts, + vol_config) + curr_bricks = empty_disks_on_hosts[host_name] + + if len(curr_bricks) < max_new_bricks: + return False + else: + return True + + +def gf_initialize_empty_disks_per_host(empty_disks_on_hosts, + list_of_empty_drives): + """Fill dictionary with host name and empty disks on those hosts. + + Input : + empty_disks_on_hosts: dict containing hosts to list of empty bricks. + list_of_empty_drives: list of empty drives + + output: + Sets the empty_disks_on_hosts with respective empty drives on existing host + At the start this will only fill new node with new disks + """ + for brick in list_of_empty_drives: + host_brick = brick.split(':', 1) + empty_disks_on_hosts[host_brick[0]].append(host_brick[1]) + + for host, brick in empty_disks_on_hosts.items(): + glogger.info(f'host = {host}: empty disks = {brick}') + + +def gluster_create_brick_map_to_swap(vol_name="", new_host="", + list_of_empty_drives=[]): + """Get map of bricks which need to be replaced for scaling. + + Input : + vol_name - Name of the volume which needs to be scaled. + + new_host - Hostname of the new node. + + list_of_empty_drives - list of empty drives present on any node + in the format hostname:/dev/drive{1..n}. + If we have swapped 2 empty drive and the application gets killed + and we want to restart, we need to provide the list of all the + empty drives even if the drives have been swapped. We have to + provide new location of empty drives. + + Return : + bricks_migration_map: List of named tuple. Each tuple containes + old brick and new bricks which should be replaced. + + Example: + Swap_bricks(old_brick='node-1:/root/brick-0-4', new_brick='node-4:/root/brick-12') + + """ + if not vol_name or not new_host or not list_of_empty_drives: + glogger.debug(f'volume name = {vol_name} : new_host = {new_host}') + # return None + + bricks_migration_map = [] + empty_disks_on_hosts = collections.defaultdict(list) + Swap_bricks = collections.namedtuple('Swap_bricks', ["old_brick", "new_brick"]) + + # Get list of empty disks present on each node and create a dictionary + gf_initialize_empty_disks_per_host(empty_disks_on_hosts, list_of_empty_drives) + + # *REMOVE ME* : just for testing, get it from file. Keep else part only + if not vol_name: + vol_info = test.gf_get_test_volume_info(subvol=6, data=4, red=2) + for elm in vol_info["bricks_list"]: + print (elm) + else: + vol_info = sc.gf_get_volume_info_xml(vol_name) + + vol_brick_list = sc.gf_get_volume_bricks_list(vol_info) + + vol_config = sc.gf_get_volume_config(vol_info) + brcount = vol_config['disperseCount'] + + subvol_brick_list = sc.gf_subvol_host_to_bricks_dict(vol_brick_list, brcount) + + if not gf_are_bricks_sufficient(vol_config, list_of_empty_drives): + print("Not enough new drives to expend") + return + + glogger.info(subvol_brick_list) + # Add new_host in all the subvolume, we may have few bricks on this + # host + for subvol in subvol_brick_list: + subvol[new_host] = [] + + # start scanning subvol and whichever host has more than 1 brick for that subvol + # pick that and see if that can be swapped to empty brick + for subvol in subvol_brick_list: + for host, bricks in subvol.items(): + + if host == new_host: + continue + + # Check if this host already have enough new disks and we can + # not have any more new disks on this host + if gf_host_has_max_empty_disks(empty_disks_on_hosts, host, vol_config): + continue + + # Check if this host has enough old bricks on this host to replace + if not len(bricks) > 1: + continue + + # Check if this subvolume already have "redundancy" number of + # bricks on new node + if not len(subvol[new_host]) < vol_config['redundancyCount']: + continue + + newbr = empty_disks_on_hosts[new_host].pop() + oldbr = bricks.pop() + + # Add old brick to new host "new_host" of the subvol + subvol[new_host].insert(0, oldbr) + + # Add new brick to old "host" of the subvol + empty_disks_on_hosts[host].insert(0, newbr) + + oldbr = host + ":" + oldbr + newbr = new_host + ":" + newbr + + # Add old and new bricks, with new_host, to swap map as named tuple + swap = Swap_bricks(old_brick=oldbr, new_brick=newbr) + bricks_migration_map.append(swap) + + return bricks_migration_map + + +def gluster_volume_commit_drive_swap(volname, old_brick, new_brick): + """Commit changes to gluster EC volume. + + Input : + vol_name - Name of the volume needs to be scaled. + swap_map - This is the map/table of physically swapped drives. + return : + True - Commit passed + False - Commit failed + """ + cmd = "gluster v replace-brick " + volname + " " + old_brick + " " + new_brick + " commit force" + ret, output, err = sc.run_gluster_command(cmd) + + return ret diff --git a/scale/gf_exceptions.py b/scale/gf_exceptions.py new file mode 100644 index 0000000..fe10bc0 --- /dev/null +++ b/scale/gf_exceptions.py @@ -0,0 +1,17 @@ +"""This module provide custome excception to be used.""" + + +class VolumeNotHealthy(Exception): + """User defind exception. + + This exception could be used when a gluster volume + command execution fails. + """ + + +class GfCommandFailed(Exception): + """User defind exception. + + This exception could be used when a gluster volume + command execution fails. + """ diff --git a/scale/gf_logs.py b/scale/gf_logs.py new file mode 100644 index 0000000..52f084f --- /dev/null +++ b/scale/gf_logs.py @@ -0,0 +1,18 @@ +"""This module is handeling the logging of plus one scale in gluster.""" + + +import logging + +# logger decides what to log +glogger = logging.getLogger(__name__) +glogger.setLevel(logging.INFO) + +# file handler decides where and how to log those logs of logger +gf_log_handler = logging.FileHandler("/var/log/glusterfs/gluster-plus-one-scale.log") + +format_string = '%(asctime)s:%(levelname)s:%(lineno)d:%(funcName)s:%(message)s' +gf_log_formatter = logging.Formatter(format_string) +gf_log_handler.setFormatter(gf_log_formatter) + +glogger.addHandler(gf_log_handler) +# glogger can be used through out this project to log messages. diff --git a/scale/gf_plus_one_scale.py b/scale/gf_plus_one_scale.py new file mode 100755 index 0000000..1e2009e --- /dev/null +++ b/scale/gf_plus_one_scale.py @@ -0,0 +1,242 @@ +"""This module provides common volume functions. + +This module provide functions to create file with volume +information. Also, we can create a file with list of bricks of +new nodes. +""" + + +import re +import collections +import subprocess as sp +from scale.gf_exceptions import (VolumeNotHealthy, + GfCommandFailed) +from scale.gf_logs import glogger +import test_data.gf_plus_one_test_data as test +import xml.etree.ElementTree as ET + + +def run_gluster_command(cmd): + """Execute gluster commands. + + args: + cmd: String of command. ex. "gluster volume info "" + returns: + tuple (ret,output,err) + ret = return status of command + output = output string + err = error message if any + """ + p = sp.Popen(cmd, stdout=sp.PIPE, stderr=sp.PIPE, shell=True) + (output, err) = p.communicate() + ret = p.returncode + if ret: + raise GfCommandFailed(f'ret = {ret}, out = {output.decode("ascii")}, error = {err.decode("ascii")}') + + return (ret, output, err) + + +def gf_get_new_bricks_list(filename): + """Get list of bricks from file provided. + + Input: File Name which contains list of bricks + return: Brick list in the form of "LIST" + """ + bricks_list = [] + try: + with open(filename, 'r') as file_out: + bricks_list = file_out.readlines() + for i in range(0, len(bricks_list)): + bricks_list[i] = bricks_list[i].rstrip() + + except FileNotFoundError as er: + glogger.error(er) + raise GfCommandFailed(er) + else: + return bricks_list + + +def gf_get_volume_info_xml(volname): + """Get volume heal summary using gluster command. + + Input: volume name + return: Complete volume info in a dict of + bricks_list and vol_config + """ + vol_config = collections.defaultdict(int) + bricks_list = [] + + vol_info = collections.defaultdict() + + cmd = 'gluster v info ' + volname + ' --xml' + ret, output, err = run_gluster_command(cmd) + root = ET.fromstring(output) + + for brick in root.iter('brick'): + for name in brick.findall('name'): + brick_name = name.text + bricks_list.append(brick_name) + + for count in root.iter('brickCount'): + vol_config["brickCount"] = int(count.text) + + for count in root.iter('disperseCount'): + vol_config["disperseCount"] = int(count.text) + + for count in root.iter('redundancyCount'): + vol_config["redundancyCount"] = int(count.text) + + vol_config["subvolCount"] = int(vol_config["brickCount"] / vol_config["disperseCount"]) + + vol_info["bricks_list"] = bricks_list + vol_info["vol_config"] = vol_config + glogger.info(vol_info) + + return vol_info + + +def gf_get_volume_heal_summary_xml(volname): + """Get volume heal summary using gluster command. + + Input: volume name + return: Complete volume heal summary in a dictionary + 'brick_path' : status + """ + status = 0 + heal_status_dict = collections.defaultdict(int) + cmd = 'gluster v heal ' + volname + ' info summary --xml ' + ret, output, err = run_gluster_command(cmd) + root = ET.fromstring(output) + + for brick in root.iter('brick'): + for name in brick.findall('name'): + brick_name = name.text + + for entries in brick.findall('totalNumberOfEntries'): + status = int(entries.text) + + heal_status_dict[brick_name] = status + + glogger.debug(heal_status_dict) + return heal_status_dict + + +def gf_get_volume_status_xml(volname): + """Get volume status using gluster command. + + Input: volume name + return: + volume_status : Complete volume status in a dictionary + 'brick_path' : status + """ + brick_status_dict = collections.defaultdict(int) + + cmd = 'gluster v status --xml ' + volname + ret, output, err = run_gluster_command(cmd) + root = ET.fromstring(output) + + for node in root.iter('node'): + for path in node.findall('path'): + brick = path.text + + for status in node.findall('status'): + status = int(status.text) + + if re.search(brick, 'localhost'): + continue + + brick_status_dict[brick] = status + + glogger.info(brick_status_dict) + return brick_status_dict + + +def gf_get_volume_config(volume_info): + """Fetch configuration od EC volume from volume info. + + Input: volume_info in the for of list + return: Dictionary of volume config + containing: + {'data': , + 'redundancy': , + 'bricks': , + 'subvols': } + """ + volume_config = volume_info["vol_config"] + glogger.info(volume_config) + return volume_config + + +def gf_get_volume_bricks_list(volume_info): + """Fetch bricks from volume info. + + Input: + volume_info - in the form of list + return: list of bricks in the form of + hostname: + """ + all_brick_list = volume_info["bricks_list"] + glogger.info(all_brick_list) + return all_brick_list + + +def gf_get_node_to_bricks_dict(all_brick_list): + """Get bricks of individual hosts. + + Input: + all_brick_list - list of all the bricks per host + return: dict in the form of + 'hostname':[brick1, brick2] containing all the host - bricks + """ + all_brick_dict = collections.defaultdict(list) + + for line in all_brick_list: + host_brick = line.split(':', 1) + all_brick_dict[host_brick[0]].append(host_brick[1]) + + glogger.info(all_brick_dict) + return all_brick_dict + + +def gf_subvol_host_to_bricks_dict(brick_list, brcount): + """List of subvolumes and its corresponding bricks. + + Input: + brick_list - list of bricks of volume + brcount : brick count for one subvolume (data + redundancy) + + return: list of dicts in the form of + 'hostname':[brick1, brick2] for individual subvolumes + """ + subvol_brick_list = [] + host_brick = collections.defaultdict(list) + i = 0 + + for br in brick_list: + brick = br.split(':', 1) + host_brick[brick[0]].append(brick[1]) + i += 1 + if (i == brcount): + subvol_brick_list.append(host_brick) + host_brick = collections.defaultdict(list) + i = 0 + + glogger.info(f'************List of subvolumes and its corresponding bricks************') + glogger.info(subvol_brick_list) + return subvol_brick_list + + +def gf_subvol_bricks_list(volume_info): + """List of subvolumes and its corresponding bricks. + + Input: volume_info in the form of list + return: list of bricks in the form of + 'hostname': + The length of this list will be equal to the + number of subvolume in this volume + """ + brick_list = gf_get_volume_bricks_list(volume_info) + vlconfig = gf_get_volume_config(volume_info) + brcount = vlconfig['disperseCount'] + glogger.info(f'Bricks per EC subvolume = {brcount}') + return gf_subvol_host_to_bricks_dict(brick_list, brcount) diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..80e00f8 --- /dev/null +++ b/setup.py @@ -0,0 +1,39 @@ +# +# Copyright (c) 2018 Red Hat, Inc. +# +# This file is part of gluster-plus-one-scale project which is a +# subproject of GlusterFS ( www.gluster.org) +# +# This file is licensed to you under your choice of the GNU Lesser +# General Public License, version 3 or any later version (LGPLv3 or +# later), or the GNU General Public License, version 2 (GPLv2), in all +# cases as published by the Free Software Foundation. + +from setuptools import setup + + +setup( + name="gluster-plus-one-scale", + version="0.1", + packages=["scale"], + include_package_data=True, + platforms="linux", + zip_safe=False, + author="Gluster Developers", + author_email="gluster-devel@gluster.org", + description="Gluster Plus One scale API's and Script", + license="GPLv2", + keywords="gluster, tool, health", + url="https://github.com/gluster/gluster-plus-one-scale", + long_description=""" + Gluster Plus One Scale + """, + classifiers=[ + "Development Status :: Beta", + "Topic :: Utilities", + "Environment :: Console", + "License :: OSI Approved :: GNU General Public License v2 (GPLv2)", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python :: 3", + ], +) diff --git a/test_data/__init__.py b/test_data/__init__.py new file mode 100755 index 0000000..e69de29 diff --git a/test_data/gf_plus_one_test_data.py b/test_data/gf_plus_one_test_data.py new file mode 100755 index 0000000..3d576f8 --- /dev/null +++ b/test_data/gf_plus_one_test_data.py @@ -0,0 +1,146 @@ +"""Module to create test data like volume info and new bricks. + +This module just provide functions to create file with volume +infomarion. Also, we can create a file with list of bricks of +new nodes. +""" + + +import collections +from scale.gf_logs import glogger +import scale.gf_plus_one_scale as sc + + +def gf_test_get_new_bricks(new_node, total): + """Generate the new bricks list insode file. + + new_node: Name of a node + Total: Number of brick list to generate + """ + bricks_list = [] + + br = 1 + while br <= total: + bricks_list.append(f'{new_node}:/root/brick-new-{br}') + br += 1 + + return bricks_list + +def gf_get_test_volume_info(subvol=1, data=4, red=2): + """Read the file containing volume info. + + Input: file name + return: list of bricks + """ + vol_config = collections.defaultdict(int) + vol_info = collections.defaultdict() + bricks_list = [] + total = subvol * (data + red) + config = data + red + host = 1 + br = 1 + sv = 0 + + for i in range(1, total + 1): + bricks_list.append(f'node-{host}:/root/brick-{sv}-{br}') + if host == 3: + host = 1 + else: + host += 1 + + if br == 6: + br = 1 + sv += 1 + else: + br += 1 + + vol_config["brickCount"] = total + vol_config["disperseCount"] = data + red + vol_config["redundancyCount"] = red + vol_config["subvolCount"] = subvol + + vol_info["bricks_list"] = bricks_list + vol_info["vol_config"] = vol_config + + return vol_info + + +def create_gluster_v_info(subvol=1, data=4, red=2): + """Create a file with volume info. + + Input: + subvol = Number of subvolume + data = number of data bricks + red = redundancy + + Output: File gluster_v_info.txt with volume infomation + """ + total = subvol * (data + red) + config = data + red + host = 1 + br = 1 + file_name = 'gluster_v_info.txt' + file_out = open(file_name, 'w') + + vinfo = 'Volume Name: vol\n\ +Type: Distributed-Disperse\n\ +Volume ID: ea40eb13-d42c-431c-9c89-0153e834e67e\n\ +Status: Started\n\ +Snapshot Count: 0\n\ +Number of Bricks: 6 x (4 + 2) = 36\n\ +Transport-type: tcp\n\ +Bricks:\n' + sv = 0 + file_out.write(vinfo) + for i in range(1, total + 1): + file_out.write(f'Brick{i}: node-{host}:/root/brick-{sv}-{br}\n') + if host == 3: + host = 1 + else: + host += 1 + + if br == 6: + br = 1 + sv += 1 + else: + br += 1 + + vinfo = 'Options Reconfigured:\n\ +cluster.disperse-self-heal-daemon: disable\n\ +transport.address-family: inet\n\ +nfs.disable: on' + file_out.write(vinfo) + file_out.close() + return file_name + + +def create_new_node_bricks(no_of_bricks=12, node_num=4, file_name='new-node.txt'): + """Create file with new bricks. + + Input: no_of_bricks - No of new bricks on new node + node_num- Number of the node + file_name - name of the file where we want to store this data + + return file_name + """ + with open(file_name, 'w') as file_out: + for i in range(1, no_of_bricks + 1): + file_out.write(f'Brick{i}: node-{node_num}:/root/brick-{i}\n') + + return file_name + + +def gf_test_heal_info_when_glusterd_is_down(): + pass + + +def gf_test_vol_info_when_glusterd_is_down(): + pass + + +def gf_test_status_when_glusterd_is_down(): + pass + + +def gf_test_status_when_glusterd_is_down(): + pass diff --git a/testing.py b/testing.py new file mode 100644 index 0000000..9f8d967 --- /dev/null +++ b/testing.py @@ -0,0 +1,16 @@ +"""This script test the code.""" + + +import test_data.gf_plus_one_test_data as test + + +# Test code to test scripts. +def test(): + pass + +def getmap(): + pass + +if True: + test() + getmap()