├── modules ├── __init__.py ├── logger.py ├── encoder.py ├── options.py ├── rest.py └── colorer.py ├── XtremIOSnap.exe ├── images ├── encode.png ├── vol_snap.png ├── XMS_Folder.png ├── folder_snap.png ├── XMS_hourly_snaps.png ├── volsnap_with_delete.png ├── XMS_Snapshot_Hierarchy.png └── folder_snap_with_delete.png ├── .gitignore ├── README.md └── XtremIOSnap.py /modules/__init__.py: -------------------------------------------------------------------------------- 1 | __author__ = 'ebattle' 2 | -------------------------------------------------------------------------------- /XtremIOSnap.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/XtremIOSnap.exe -------------------------------------------------------------------------------- /images/encode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/encode.png -------------------------------------------------------------------------------- /images/vol_snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/vol_snap.png -------------------------------------------------------------------------------- /images/XMS_Folder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/XMS_Folder.png -------------------------------------------------------------------------------- /images/folder_snap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/folder_snap.png -------------------------------------------------------------------------------- /images/XMS_hourly_snaps.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/XMS_hourly_snaps.png -------------------------------------------------------------------------------- /images/volsnap_with_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/volsnap_with_delete.png -------------------------------------------------------------------------------- /images/XMS_Snapshot_Hierarchy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/XMS_Snapshot_Hierarchy.png -------------------------------------------------------------------------------- /images/folder_snap_with_delete.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanbattle/XtremIOSnap/HEAD/images/folder_snap_with_delete.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | XtremIOSnap_old.zip 2 | XtremIOSnap_old_0.zip 3 | XtremIOSnap_old_1.zip 4 | setup.py 5 | modules/encoder.pyc 6 | *.pyc 7 | *.log 8 | requirements.txt -------------------------------------------------------------------------------- /modules/logger.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap - Logger Module 3 | # Purpose: 4 | # 5 | # Author: Evan R. Battle 6 | # Sr. Systems Engineer, EMC 7 | # evan.battle@emc.com 8 | # 9 | # Version: 3.1 10 | # 11 | # Created: 11/22/2014 12 | # 13 | # Licence: Open to distribute and modify. This example code is unsupported 14 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 15 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 16 | # RESOLVING ISSUES. USE CODE AS IS! 17 | # 18 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 19 | #------------------------------------------------------------------------------- 20 | 21 | ##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 22 | ## def_FuncLogger 23 | ## 24 | ## this is the default logging function I use in all my python code 25 | ##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 26 | import inspect 27 | import logging 28 | import colorer 29 | 30 | def Logger(logfile,file_level,console_level=None): 31 | 32 | function_name = inspect.stack()[1][3] 33 | logger = logging.getLogger(function_name) 34 | logger.setLevel(logging.DEBUG) 35 | 36 | if logger.handlers: 37 | logger.handlers = [] 38 | 39 | if console_level != None: 40 | ch = colorer.ColorizingStreamHandler() 41 | ch.setLevel(console_level) 42 | ch_format = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') 43 | ch.setFormatter(ch_format) 44 | logger.addHandler(ch) 45 | try: 46 | fh = logging.FileHandler(logfile.format(function_name)) 47 | except IOError as e: 48 | print 'Logging IOError: ['+str(e.errno)+'] - '+e.strerror 49 | print 'Use the -l LogPath option' 50 | sys.exit(1) 51 | 52 | fh.setLevel(file_level) 53 | fh_format = logging.Formatter('%(asctime)s - %(lineno)d - %(levelname)8s - %(message)s') 54 | fh.setFormatter(fh_format) 55 | logger.addHandler(fh) 56 | 57 | return logger 58 | -------------------------------------------------------------------------------- /modules/encoder.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap - Encoder module 3 | # Purpose: Encodes and Decodes the user ID and password so they don't have 4 | # to be stored in a batch file or task scheduler in plain text. 5 | # 6 | # Author: Evan R. Battle 7 | # Sr. Systems Engineer, EMC 8 | # evan.battle@emc.com 9 | # 10 | # Version: 3.0 11 | # 12 | # Created: 11/20/2014 13 | # 14 | # Licence: Open to distribute and modify. This example code is unsupported 15 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 16 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 17 | # RESOLVING ISSUES. USE CODE AS IS! 18 | # 19 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 20 | #------------------------------------------------------------------------------- 21 | 22 | import logging 23 | import base64 24 | from modules.logger import Logger 25 | 26 | class Encode: 27 | 28 | def __init__(self,logfile,debugmode): 29 | 30 | global encode_logger 31 | if debugmode == True: 32 | encode_logger = Logger(logfile,logging.DEBUG,logging.INFO) 33 | else: 34 | encode_logger = Logger(logfile,logging.INFO,logging.INFO) 35 | encode_logger.debug('Loading Encode Module') 36 | 37 | def _encodeuser(self,user): 38 | 39 | encode_logger.debug('_encodeuser - Encoding User ID') 40 | encoded_user = base64.b64encode(user) 41 | return encoded_user 42 | 43 | def _encodepass(self,password): 44 | 45 | encode_logger.debug('_encodepass - Encoding Password') 46 | encoded_pass = base64.b64encode(password) 47 | return encoded_pass 48 | 49 | def _decodeuser(self,user): 50 | 51 | encode_logger.debug('_decodeuser - Decoding User') 52 | decoded_user = base64.b64decode(user) 53 | return decoded_user 54 | 55 | def _decodepass(self,password): 56 | 57 | encode_logger.debug('_decodepass - Decoding Password') 58 | decoded_pass = base64.b64decode(password) 59 | return decoded_pass 60 | -------------------------------------------------------------------------------- /modules/options.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap - Options Module 3 | # Purpose: 4 | # 5 | # Author: Evan R. Battle 6 | # Sr. Systems Engineer, EMC 7 | # evan.battle@emc.com 8 | # 9 | # Version: 3.0 10 | # 11 | # Created: 11/20/2014 12 | # 13 | # Licence: Open to distribute and modify. This example code is unsupported 14 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 15 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 16 | # RESOLVING ISSUES. USE CODE AS IS! 17 | # 18 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 19 | #------------------------------------------------------------------------------- 20 | 21 | import logging 22 | import sys 23 | from modules.logger import Logger 24 | from modules.encoder import Encode 25 | 26 | class Options: 27 | def __init__(self,logfile,options): 28 | 29 | global options_logger 30 | if options['--debug'] == True: 31 | options_logger = Logger(logfile,logging.DEBUG,logging.INFO) 32 | else: 33 | options_logger = Logger(logfile,logging.INFO,logging.INFO) 34 | 35 | options_logger.debug('Loading Options Module') 36 | encoder = Encode(options['--l'],options['--debug']) 37 | 38 | if options['--encode'] is True: 39 | options_logger.info('Encoding user id and password') 40 | encode_user = encoder._encodeuser(options['XMS_USER']) 41 | encode_pass = encoder._encodepass(options['XMS_PASS']) 42 | print '' 43 | print 'Encoded User ID = ' + encode_user 44 | print 'Encoded Password = ' + encode_pass 45 | print '' 46 | options_logger.info('Use the above, encoded, user id and password with the --e option') 47 | options_logger.info('to execute the tool without using the plain text usename and password') 48 | sys.exit(0) 49 | 50 | elif options['--e']: 51 | options_logger.debug('Using an encoded username and password') 52 | XMS_USER = encoder._decodeuser(options['XMS_USER']) 53 | XMS_PASS = encoder._decodepass(options['XMS_PASS']) 54 | 55 | else: 56 | options_logger.debug('Username and password are not encoded') 57 | XMS_USER = options['XMS_USER'] 58 | XMS_PASS = options['XMS_PASS'] 59 | 60 | self.XMS_USER = XMS_USER 61 | self.XMS_PASS = XMS_PASS 62 | 63 | if options['--schedule'] == 'hourly': 64 | options_logger.debug('Using the hourly schedule') 65 | self.schedule = 'hourly' 66 | 67 | elif options['--schedule'] == 'daily': 68 | options_logger.debug('Using the daily schedule') 69 | self.schedule = 'daily' 70 | 71 | elif options['--schedule'] == 'weekly': 72 | options_logger.debug('Using the weekly schedule') 73 | self.schedule = 'weekly' 74 | 75 | else: 76 | options_logger.critical('No schedule, or incorrect option Specified. Exiting...') 77 | sys.exit(1) 78 | 79 | SnapFolder = '_Snapshots' 80 | 81 | self.var_snap_tgt_folder = options['--tf'] ## this should be an optional variable, if the customer wants to organize snapshots under a folder hierarchy 82 | 83 | if self.var_snap_tgt_folder == None: 84 | options_logger.debug('No snapshot target folder specified') 85 | self.var_snap_tgt_folder ='' 86 | self.snap_tgt_folder = SnapFolder 87 | options_logger.debug('Using '+self.snap_tgt_folder+' as the snapshot target') 88 | else: 89 | self.snap_tgt_folder = self.var_snap_tgt_folder+'/'+SnapFolder 90 | options_logger.debug('Using '+self.snap_tgt_folder+' as the snapshot target') 91 | 92 | ##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 93 | ## exit_gracefully 94 | ## 95 | ## gracefully exits if the script is interrupted 96 | ##%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 97 | 98 | def exit_gracefully(signum, frame): 99 | signal.signal(signal.SIGINT, original_sigint) 100 | 101 | try: 102 | if raw_input("\nDo you really want to quit? (y/n)> ").lower().startswith('y'): 103 | sys.exit(1) 104 | 105 | except KeyboardInterrupt: 106 | print("Ok, Exiting....") 107 | sys.exit(1) 108 | 109 | signal.signal(signal.SIGINT, exit_gracefully) 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /modules/rest.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap - Rest Module 3 | # Purpose: 4 | # 5 | # Author: Evan R. Battle 6 | # Sr. Systems Engineer, EMC 7 | # evan.battle@emc.com 8 | # 9 | # Version: 3.0 10 | # 11 | # Created: 11/20/2014 12 | # 13 | # Licence: Open to distribute and modify. This example code is unsupported 14 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 15 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 16 | # RESOLVING ISSUES. USE CODE AS IS! 17 | # 18 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 19 | #------------------------------------------------------------------------------- 20 | import logging 21 | import sys 22 | import requests 23 | from requests.auth import HTTPBasicAuth 24 | import json 25 | from modules.logger import Logger 26 | requests.packages.urllib3.disable_warnings() 27 | 28 | class Restful: 29 | 30 | def __init__(self,logfile,debugmode,global_XMS_IP,global_XMS_USER,global_XMS_PASS): 31 | 32 | global rest_logger 33 | global XMS_IP 34 | global XMS_USERID 35 | global XMS_PASS 36 | if debugmode == True: 37 | rest_logger = Logger(logfile,logging.DEBUG,logging.INFO) 38 | else: 39 | rest_logger = Logger(logfile,logging.INFO,logging.INFO) 40 | 41 | rest_logger.debug('Loading Restful Module') 42 | XMS_IP = global_XMS_IP 43 | XMS_USERID = global_XMS_USER 44 | XMS_PASS = global_XMS_PASS 45 | 46 | 47 | def _get(self,XMS_URL): 48 | rest_logger.debug('Starting _get module') 49 | 50 | try: 51 | rest_logger.debug('_get - https://'+XMS_IP+XMS_URL) 52 | resp = requests.get( 53 | 'https://'+XMS_IP+XMS_URL, 54 | auth=HTTPBasicAuth(XMS_USERID,XMS_PASS), 55 | verify=False 56 | ) 57 | except requests.exceptions.RequestException as e: 58 | rest_logger.error(e) 59 | sys.exit(1) 60 | 61 | if resp.status_code == 200: 62 | rest_logger.debug('_get - Get Request Status: <'+str(resp.status_code)+'>') 63 | rest_logger.debug('_get - '+resp.text) 64 | else: 65 | rest_logger.error('_get - Get Request Status: <'+str(resp.status_code)+'>') 66 | rest_logger.error(resp.text) 67 | sys.exit(1) 68 | 69 | return resp 70 | 71 | def _post(self,XMS_URL,PAYLOAD): 72 | rest_logger.debug('Starting _post module') 73 | 74 | j=json.loads(PAYLOAD) 75 | 76 | try: 77 | rest_logger.debug('_post - https://'+XMS_IP+XMS_URL) 78 | rest_logger.debug('_post - '+PAYLOAD) 79 | resp = requests.post( 80 | 'https://'+XMS_IP+XMS_URL, 81 | auth=HTTPBasicAuth(XMS_USERID,XMS_PASS), 82 | verify=False, 83 | json=j 84 | ) 85 | except requests.exceptions.RequestException as e: 86 | rest_logger.error(e) 87 | sys.exit(1) 88 | 89 | if resp.status_code == 201: 90 | rest_logger.debug('_post - Post Request Status: <'+str(resp.status_code)+'>') 91 | rest_logger.debug(resp.text) 92 | else: 93 | rest_logger.critical('_post - Post Request Status: <'+str(resp.status_code)+'>') 94 | rest_logger.critical(resp.text) 95 | sys.exit(1) 96 | 97 | return resp 98 | 99 | def _put(self,XMS_URL,PAYLOAD): 100 | rest_logger.debug('Starting _put module') 101 | 102 | j=json.loads(PAYLOAD) 103 | 104 | try: 105 | rest_logger.debug('_put - https://'+XMS_IP+XMS_URL) 106 | rest_logger.debug('_put - '+PAYLOAD) 107 | resp = requests.put( 108 | 'https://'+XMS_IP+XMS_URL, 109 | auth=HTTPBasicAuth(XMS_USERID,XMS_PASS), 110 | verify=False, 111 | json=j 112 | ) 113 | except requests.exceptions.RequestException as e: 114 | rest_logger.error(e) 115 | sys.exit(1) 116 | 117 | if resp.status_code == 200: 118 | rest_logger.debug('_put - Put Request Status: <'+str(resp.status_code)+'>') 119 | rest_logger.debug(resp.text) 120 | else: 121 | rest_logger.critical('_put - Put Request Status: <'+str(resp.status_code)+'>') 122 | rest_logger.critical(resp.text) 123 | sys.exit(1) 124 | 125 | return resp 126 | 127 | def _delete(self,XMS_URL): 128 | rest_logger.debug('Starting _delete module') 129 | 130 | try: 131 | rest_logger.debug('_delete - https://'+XMS_IP+XMS_URL) 132 | resp = requests.delete( 133 | 'https://'+XMS_IP+XMS_URL, 134 | auth=HTTPBasicAuth(XMS_USERID,XMS_PASS), 135 | verify=False 136 | ) 137 | except requests.exceptions.RequestException as e: 138 | rest_logger.error(e) 139 | sys.exit(1) 140 | 141 | if resp.status_code == 200: 142 | rest_logger.debug('_delete - Delete Request Status: <'+str(resp.status_code)+'>') 143 | rest_logger.debug(resp.text) 144 | else: 145 | rest_logger.critical('_delete - Delete Request Status: <'+str(resp.status_code)+'>') 146 | rest_logger.critical(resp.text) 147 | sys.exit(1) 148 | 149 | return resp 150 | 151 | -------------------------------------------------------------------------------- /modules/colorer.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap - colorer Module 3 | # Purpose: Add a little color to the output 4 | # 5 | # Author: Evan R. Battle 6 | # Sr. Systems Engineer, EMC 7 | # evan.battle@emc.com 8 | # 9 | # Version: 1.0 10 | # 11 | # Created: 11/22/2014 12 | # 13 | # Licence: Open to distribute and modify. This example code is unsupported 14 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 15 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 16 | # RESOLVING ISSUES. USE CODE AS IS! 17 | # 18 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 19 | #------------------------------------------------------------------------------- 20 | 21 | import ctypes 22 | import logging 23 | import os 24 | 25 | class ColorizingStreamHandler(logging.StreamHandler): 26 | # color names to indices 27 | color_map = { 28 | 'black': 0, 29 | 'red': 1, 30 | 'green': 2, 31 | 'yellow': 3, 32 | 'blue': 4, 33 | 'magenta': 5, 34 | 'cyan': 6, 35 | 'white': 7, 36 | } 37 | 38 | #levels to (background, foreground, bold/intense) 39 | if os.name == 'nt': 40 | level_map = { 41 | logging.DEBUG: (None, 'blue', True), 42 | logging.INFO: (None, 'green', False), 43 | logging.WARNING: (None, 'yellow', True), 44 | logging.ERROR: (None, 'red', True), 45 | logging.CRITICAL: ('red', 'white', True), 46 | } 47 | else: 48 | level_map = { 49 | logging.DEBUG: (None, 'blue', False), 50 | logging.INFO: (None, 'green', False), 51 | logging.WARNING: (None, 'yellow', False), 52 | logging.ERROR: (None, 'red', False), 53 | logging.CRITICAL: ('red', 'white', True), 54 | } 55 | csi = '\x1b[' 56 | reset = '\x1b[0m' 57 | 58 | @property 59 | def is_tty(self): 60 | isatty = getattr(self.stream, 'isatty', None) 61 | return isatty and isatty() 62 | 63 | def emit(self, record): 64 | try: 65 | message = self.format(record) 66 | stream = self.stream 67 | if not self.is_tty: 68 | stream.write(message) 69 | else: 70 | self.output_colorized(message) 71 | stream.write(getattr(self, 'terminator', '\n')) 72 | self.flush() 73 | except (KeyboardInterrupt, SystemExit): 74 | raise 75 | except: 76 | self.handleError(record) 77 | 78 | if os.name != 'nt': 79 | def output_colorized(self, message): 80 | self.stream.write(message) 81 | else: 82 | import re 83 | ansi_esc = re.compile(r'\x1b\[((?:\d+)(?:;(?:\d+))*)m') 84 | 85 | nt_color_map = { 86 | 0: 0x00, # black 87 | 1: 0x04, # red 88 | 2: 0x02, # green 89 | 3: 0x06, # yellow 90 | 4: 0x01, # blue 91 | 5: 0x05, # magenta 92 | 6: 0x03, # cyan 93 | 7: 0x07, # white 94 | } 95 | 96 | def output_colorized(self, message): 97 | parts = self.ansi_esc.split(message) 98 | write = self.stream.write 99 | h = None 100 | fd = getattr(self.stream, 'fileno', None) 101 | if fd is not None: 102 | fd = fd() 103 | if fd in (1, 2): # stdout or stderr 104 | h = ctypes.windll.kernel32.GetStdHandle(-10 - fd) 105 | while parts: 106 | text = parts.pop(0) 107 | if text: 108 | write(text) 109 | if parts: 110 | params = parts.pop(0) 111 | if h is not None: 112 | params = [int(p) for p in params.split(';')] 113 | color = 0 114 | for p in params: 115 | if 40 <= p <= 47: 116 | color |= self.nt_color_map[p - 40] << 4 117 | elif 30 <= p <= 37: 118 | color |= self.nt_color_map[p - 30] 119 | elif p == 1: 120 | color |= 0x08 # foreground intensity on 121 | elif p == 0: # reset to default color 122 | color = 0x07 123 | else: 124 | pass # error condition ignored 125 | ctypes.windll.kernel32.SetConsoleTextAttribute(h, color) 126 | 127 | def colorize(self, message, record): 128 | if record.levelno in self.level_map: 129 | bg, fg, bold = self.level_map[record.levelno] 130 | params = [] 131 | if bg in self.color_map: 132 | params.append(str(self.color_map[bg] + 40)) 133 | if fg in self.color_map: 134 | params.append(str(self.color_map[fg] + 30)) 135 | if bold: 136 | params.append('1') 137 | if params: 138 | message = ''.join((self.csi, ';'.join(params), 139 | 'm', message, self.reset)) 140 | return message 141 | 142 | def format(self, record): 143 | message = logging.StreamHandler.format(self, record) 144 | if self.is_tty: 145 | # Don't colorize any traceback 146 | parts = message.split('\n', 1) 147 | parts[0] = self.colorize(parts[0], record) 148 | message = '\n'.join(parts) 149 | return message 150 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | XtremIOSnap 2 | =========== 3 | ------------------------------------------------------------------------ 4 | Version 3.0 5 | 6 | Summary 7 | ------- 8 | 9 | Snapshots can provide a number of benefits, from being used as backups in the event a recovery is needed at some point in the future, to making copies of production datasets available for non-production use (test/dev, reporting, etc). Historically, snapshot use has been limited due to performance constraints introduced by COFW mechanisms or limitations on the underlying block filesystem (redirect on write). 10 | 11 | With XtremIO, snapshots can be easilly created and mapped to hosts to create full-performance, space-efficient, writeable copies of production data for a variety of uses without the legacy caveats that have become commonplace with legacy storage arrays. However, in the current version, 3.0, a built in snapshot scheduler does not exist, making it challenging to automate snapshots for operational uses. 12 | 13 | As a stop-gap, XtremIOSnap has been designed to bridge the gap that exists in the scheduling of snapshots until a scheduler is included in XMS. Additionlly, XtremIOSnap provides a working example of using the XtremIO REST API to interact and perform actions on the XtremIO array. 14 | 15 |
16 | Install 17 | ------- 18 | Create and maintain snapshots on an XtremIO array utilizing the REST API interface. Designed and tested for v3.0. 19 | 20 | If you are running this in python, it was written against Python 2.7.8 and will require the requests (2.4.3 or newer) package and the docopt package to be installed using: 21 | 22 | pip install requests 23 | pip install requests --upgrade (if using an older version of requests) 24 | pip install docopt 25 | 26 | The script has been tested back to python 2.6 on both Linux (Ubuntu 14.04) and Mac OSX Mavericks. 27 | 28 | If you are running in Windows without Python, use the compiled executable XtremIOSnap.exe. Visual C++ 2008 Redistributable package from MS (http://www.microsoft.com/en-us/download/details.aspx?id=29 ) is required for the compiled Windows executable. 29 |
30 | Usage 31 | ------- 32 | 33 | XtremIOSnap -h | --help 34 | XtremIOSnap (--encode) XMS_USER XMS_PASS [--l=] [--debug] 35 | XtremIOSnap XMS_IP XMS_USER XMS_PASS [--e] [(--f --snap=)] [--n=] [--schedule=] [--tf=] [--l=] [--debug] 36 | XtremIOSnap XMS_IP XMS_USER XMS_PASS [--e] [(--v --snap=)] [--n=] [--schedule=] [--tf=] [--l=] [--debug] 37 | 38 | Arguments 39 | --------- 40 | 41 | XMS_IP IP or Hostname of XMS (required) 42 | XMS_USER Username for XMS 43 | XMS_PASS Password for XMS 44 | 45 | Options 46 | ------- 47 | 48 | -h --help Show this help screen 49 | 50 | --encode Use this option with the XMS_USER and XMS_PASS 51 | arguments to generate an encoded Username and Password 52 | so the user and password don't need to be saved in 53 | clear text when using in a script or task scheduler. 54 | 55 | --e If specified, will use the encoded User and Password 56 | generated by the --encode option. 57 | 58 | --f Specify to signify the object to snap is a folder. 59 | 60 | --v Specify to signify the object to snap is a volume. 61 | 62 | --snap= Object to snap, either a volume or folder 63 | 64 | --n= Number of snapshots to retain [default: 5] 65 | 66 | --schedule= [hourly | daily | weekly] Used in naming the snapsots 67 | based on how they are scheduled [default: hourly] 68 | 69 | --tf= When specified, a _Snapshot subfolder will be created 70 | in this folder. If not used, snapshots will be saved 71 | in a _Snapshot folder at the root. 72 | 73 | --l= [default: """+var_cwd+"""/XtremIOSnap.log] 74 | 75 | --debug 76 | 77 | 78 | 79 | --schedule=hourly will append the suffix .hourly.0 allong with a timestamp to the newest snapshot. The suffix will be shifted as new snaps are taken, up to the specified number of snapshots (hourly.0 will become hourly.1, hourly.1 will become hourly.2, etc.). By default we will keep 5 hourly snapshots. 80 | 81 | --schedule=daily will append the suffix .daily.0 allong with a timestamp to the newest snapshot. The suffix will be shifted as new snaps are taken, up to the specified number of snapshots (daily.0 will become daily.1, daily.1 will become daily.2, etc.). By default we will keep 5 daily snapshots. 82 | 83 | --schedule=weekly will append the suffix .weekly.0 allong with a timestamp to the newest snapshot. The suffix will be shifted as new snaps are taken, up to the specified number of snapshots (weekly.0 will become weekly.1, weekly.1 will become weekly.2, etc.). By default we will keep 5 weekly snapshot. 84 | 85 | If the --n= option is not used, the script will maintain a maximum of 5 snapshots, deleting snaps on a FIFO basis, depending on the --schedule=hourly/daily/weekly option. 86 | 87 | The --f= switch is optional, if not specified, all snapshots will be placed into the /_Snapshots folder. This is not really necessary for anything other than aesthetics. You can select the "Show as Snapshot Hierarchy" to view snaps with their source LUN. 88 | 89 | Running XtremIOSnap 90 | ----------- 91 | XtremIOSnap includes the ability to encode the username and password for use in scripts and task schedulers so that the plain text username and password does not need to be stored in clear text. 92 | 93 | XtremIOSnap --encode admin Xtrem10 94 | 95 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/encode.png) 96 | 97 | XtremIOSnap --e --f --snap= --n= --schedule= --tf= 98 | 99 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/folder_snap.png) 100 | 101 | XtremIOSnap --e --f --snap= --n= --schedule= --tf= 102 | 103 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/folder_snap_with_delete.png) 104 | 105 | XtremIOSnap --e --v --snap= --n= --schedule= --tf= 106 | 107 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/vol_snap.png) 108 | 109 | XtremIOSnap --e --v --snap= --n= --schedule= --tf= 110 | 111 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/volsnap_with_delete.png) 112 | 113 | Showing the snapshot folder under an existing folder: 114 | 115 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/XMS_Folder.png) 116 | 117 | Showing a set of 5, hourly, snapshots: 118 | 119 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/XMS_hourly_snaps.png) 120 | 121 | Showing the snapshot hierarchy view with a mix of individual volume snaps and folder snaps: 122 | 123 | ![alt tag](https://github.com/evanbattle/XtremIOSnap/blob/master/images/XMS_Snapshot_Hierarchy.png) 124 | 125 | Contributing 126 | ----------- 127 | Please contribute in any way to the project. Specifically, normalizing differnet image sizes, locations, and intance types would be easy adds to enhance the usefulness of the project. 128 | 129 | 130 | Licensing 131 | --------- 132 | Licensed under the Apache License, Version 2.0 (the “License”); you may not use this file except in compliance with the License. You may obtain a copy of the License at 133 | 134 | Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 135 | 136 | Support 137 | ------- 138 | Please file bugs and issues at the Github issues page. For more general discussions you can contact the EMC Code team at Google Groups. The code and documentation are released with no warranties or SLAs and are intended to be supported through a community driven process. 139 | -------------------------------------------------------------------------------- /XtremIOSnap.py: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------- 2 | # Name: XtremIOSnap 3 | # Purpose: Create and delete snapshots on an XtremIO array using the RESTful 4 | # Services. 5 | # 6 | # Author: Evan R. Battle 7 | # Sr. Systems Engineer, EMC 8 | # evan.battle@emc.com 9 | # 10 | # Version: 3.1 11 | # 12 | # Created: 11/22/2014 13 | # 14 | # Licence: Open to distribute and modify. This example code is unsupported 15 | # by both EMC and the author. IF YOU HAVE PROBLEMS WITH THIS 16 | # SOFTWARE, THERE IS NO ONE PROVIDING TECHNICAL SUPPORT FOR 17 | # RESOLVING ISSUES. USE CODE AS IS! 18 | # 19 | # THIS CODE IS NOT AFFILIATED WITH EMC CORPORATION. 20 | #------------------------------------------------------------------------------- 21 | import sys 22 | import os 23 | import signal 24 | import logging 25 | import datetime 26 | from docopt import docopt 27 | from modules.logger import Logger 28 | from modules.encoder import Encode 29 | from modules.options import Options 30 | from modules.options import exit_gracefully 31 | from modules.rest import Restful 32 | 33 | var_cwd = os.getcwd() 34 | 35 | __doc__ = """ 36 | XtremIOSnap 37 | 38 | Version 3.0 39 | 40 | Usage: 41 | XtremIOSnap -h | --help 42 | XtremIOSnap (--encode) XMS_USER XMS_PASS [--l=] [--debug] 43 | XtremIOSnap XMS_IP XMS_USER XMS_PASS [--e] [(--f --snap=)] [--n=] [--schedule=] [--tf=] [--l=] [--debug] 44 | XtremIOSnap XMS_IP XMS_USER XMS_PASS [--e] [(--v --snap=)] [--n=] [--schedule=] [--tf=] [--l=] [--debug] 45 | 46 | Create and maintain snapshots of both volumes and folders on an XtremIO array 47 | utilizing the REST API interface. Designed and tested for XtremIO v3.0+. 48 | 49 | Arguments: 50 | XMS_IP IP or Hostname of XMS (required) 51 | XMS_USER Username for XMS 52 | XMS_PASS Password for XMS 53 | 54 | Options: 55 | -h --help Show this help screen 56 | 57 | --encode Use this option with the XMS_USER and XMS_PASS 58 | arguments to generate an encoded Username and Password 59 | so the user and password don't need to be saved in 60 | clear text when using in a script or task scheduler. 61 | 62 | --e If specified, will use the encoded User and Password 63 | generated by the --encode option. 64 | 65 | --f Specify to signify the object to snap is a folder. 66 | 67 | --v Specify to signify the object to snap is a volume. 68 | 69 | --snap= Object to snap, either a volume or folder 70 | 71 | --n= Number of snapshots to retain [default: 5] 72 | 73 | --schedule= [hourly | daily | weekly] Used in naming the snapsots 74 | based on how they are scheduled [default: hourly] 75 | 76 | --tf= When specified, a _Snapshot subfolder will be created 77 | in this folder. If not used, snapshots will be saved 78 | in a _Snapshot folder at the root. 79 | 80 | --l= [default: """+var_cwd+"""/XtremIOSnap.log] 81 | 82 | --debug 83 | 84 | """ 85 | 86 | def main(): 87 | log = options['--l'] 88 | 89 | if options['--debug'] == True: 90 | main_logger = Logger( 91 | options['--l'], 92 | logging.DEBUG, 93 | logging.INFO 94 | ) 95 | else: 96 | main_logger = Logger( 97 | options['--l'], 98 | logging.INFO, 99 | logging.INFO 100 | ) 101 | 102 | main_logger.info( 103 | 'Running XtremIO Snap Script' 104 | ) 105 | main_options = Options( 106 | log, 107 | options 108 | ) 109 | XMS_IP = options['XMS_IP'] 110 | XMS_USER = main_options.XMS_USER 111 | XMS_PASS = main_options.XMS_PASS 112 | snap_tgt_folder = main_options.snap_tgt_folder 113 | parent_folder_id = main_options.var_snap_tgt_folder 114 | num_snaps = options['--n'] 115 | snap_src = options['--snap'] 116 | SnapFolder = '_Snapshots' 117 | bool_create_folder = True ## will change to False if the /_Snapshots folder already exists 118 | rest = Restful( 119 | log, 120 | options['--debug'], 121 | XMS_IP, 122 | XMS_USER, 123 | XMS_PASS 124 | ) 125 | folder_list = rest._get( 126 | '/api/json/types/volume-folders' 127 | ).json()['folders'] 128 | for folder_list_rs in folder_list: 129 | if folder_list_rs['name'] == '/'+ snap_tgt_folder: 130 | bool_create_folder = False 131 | main_logger.info( 132 | 'The target snapshot folder, '+folder_list_rs['name']+' already exists.' 133 | ) 134 | if bool_create_folder is True: 135 | cf_payload = '{\ 136 | "caption": \"'+SnapFolder+'\" , \ 137 | "parent-folder-id": \"/'+main_options.var_snap_tgt_folder+'\" \ 138 | }' 139 | cf_resp = rest._post( 140 | '/api/json/types/volume-folders', 141 | cf_payload 142 | ) 143 | if cf_resp.status_code == 201: 144 | main_logger.warn( 145 | 'Created folder: '+main_options.snap_tgt_folder 146 | ) 147 | if options['--f'] ==True: 148 | newsnapsuffix = '.folder.'+main_options.schedule+'.' 149 | folder_vol_list = rest._get( 150 | '/api/json/types/volume-folders/?name=/'+snap_src 151 | ).json()['content']['direct-list'] 152 | arr_folder_vol_list_component = [] 153 | for folder_vol_list_rs in folder_vol_list: 154 | if '/_Snapshots' in folder_vol_list_rs[1]: 155 | pass 156 | else: 157 | arr_folder_vol_list_component.append(folder_vol_list_rs[1]) 158 | main_logger.info( 159 | 'Will retain '+num_snaps+' snapshots for volume: '+folder_vol_list_rs [1] 160 | ) 161 | vol_snap_list = rest._get( 162 | '/api/json/types/volumes/?name='+folder_vol_list_rs [1] 163 | ).json()['content']['dest-snap-list']##<--Initial list of snapshots 164 | arr_vol_snap_list_component = [] 165 | for vol_snap_list_rs in vol_snap_list: 166 | if newsnapsuffix in vol_snap_list_rs[1]: 167 | arr_vol_snap_list_component.append(vol_snap_list_rs[1]) 168 | arr_vol_snap_list_component.sort(reverse=True) 169 | for y in range(len(arr_vol_snap_list_component)): ##<--shifting the suffix of each matchin snap by 1 170 | if newsnapsuffix in arr_vol_snap_list_component[y]: 171 | list_snapname = [] 172 | try: 173 | list_snapname = arr_vol_snap_list_component[y].split('.',3) 174 | except: 175 | pass 176 | rename_to =list_snapname[0]+newsnapsuffix+str(y+1) 177 | rename_payload = '{"vol-name": \"'+rename_to+'\"}' 178 | rename_resp = rest._put( 179 | '/api/json/types/volumes/?name='+arr_vol_snap_list_component[y], 180 | rename_payload 181 | ) 182 | if rename_resp.status_code == 200: 183 | main_logger.info( 184 | 'Snapshot: '+arr_vol_snap_list_component[y]+' was renamed to '+rename_to 185 | ) 186 | timestamp = datetime.datetime.now() 187 | timestamp = timestamp.isoformat() 188 | arr_timestamp = timestamp.split('.',2) ##<--stripping the microseconds from the timestamp for aesthetics 189 | fullsuffix = '_'+arr_timestamp[0]+newsnapsuffix+'0' 190 | fs_payload = '{\ 191 | "source-folder-id": \"/'+snap_src+'\" , \ 192 | "suffix": \"'+fullsuffix+'\" ,\ 193 | "folder-id": \"/'+snap_tgt_folder+'\" \ 194 | }' 195 | vol_snap = rest._post( 196 | '/api/json/types/snapshots', 197 | fs_payload 198 | ) 199 | folder_vol_list = rest._get( 200 | '/api/json/types/volume-folders/?name=/'+snap_src 201 | ).json()['content']['direct-list'] 202 | arr_folder_vol_list_component = [] 203 | for folder_vol_list_rs in folder_vol_list: 204 | if '/_Snapshots' in folder_vol_list_rs[1]: 205 | pass 206 | else: 207 | vol_snap_list = rest._get( 208 | '/api/json/types/volumes/?name='+folder_vol_list_rs [1] 209 | ).json()['content']['dest-snap-list']##<--Refresh the snap list 210 | arr_vol_snap_list_component = [] 211 | for vol_snap_list_rs in vol_snap_list: 212 | if newsnapsuffix in vol_snap_list_rs[1]: 213 | arr_vol_snap_list_component.append(vol_snap_list_rs[1]) 214 | arr_vol_snap_list_component.sort(reverse=False) 215 | for x in xrange(len(arr_vol_snap_list_component)-(int(num_snaps))): 216 | if newsnapsuffix in arr_vol_snap_list_component[x]: 217 | main_logger.debug( 218 | str(x)+': '+ arr_vol_snap_list_component[x] 219 | ) 220 | get_snap_details = rest._get( 221 | '/api/json/types/snapshots/?name='+arr_vol_snap_list_component[x] 222 | ) 223 | arr_ancestor_vol_id = get_snap_details.json()['content']['ancestor-vol-id'] 224 | snap_parent_name = arr_ancestor_vol_id[1] 225 | snap_creation_time = get_snap_details.json()['content']['creation-time'] 226 | snap_space_consumed = get_snap_details.json()['content']['logical-space-in-use'] 227 | arr_snap_lun_mapping = get_snap_details.json()['content']['lun-mapping-list'] 228 | main_logger.warn( 229 | 'Parent Volume of '+arr_vol_snap_list_component[x]+' = '+snap_parent_name 230 | ) 231 | main_logger.warn( 232 | 'Snapshot: ' +arr_vol_snap_list_component[x] 233 | ) 234 | main_logger.warn( 235 | ' Snap was created on '+snap_creation_time 236 | ) 237 | main_logger.warn( 238 | ' Snap is using '+ str((float(snap_space_consumed)/1024)/1024)+' GB' 239 | ) 240 | arr_lun_mapping_component = [] 241 | if len(arr_snap_lun_mapping) > 0:##<--checking to see if an active LUN mapping exists 242 | for rs in arr_snap_lun_mapping: 243 | arr_lun_mapping_component = [[y] for y in rs[0]] 244 | arr_lun_mapping_component =str(arr_lun_mapping_component[1]) 245 | arr_lun_mapping_component = arr_lun_mapping_component.replace('[u\'','') 246 | arr_lun_mapping_component = arr_lun_mapping_component.replace('\']','') 247 | main_logger.critical( 248 | 'Snapshot: '+arr_vol_snap_list_component[x]+' is currently mapped to '+arr_lun_mapping_component+', it will not be deleted.' 249 | ) 250 | else: 251 | main_logger.warn( 252 | ' No hosts mapped to '+arr_vol_snap_list_component[x]+', it will be deleted.' 253 | ) 254 | delete_status = rest._delete( 255 | '/api/json/types/volumes/?name='+arr_vol_snap_list_component[x] 256 | ) 257 | elif options['--v'] ==True: 258 | main_logger.info( 259 | 'Will retain '+num_snaps+' snapshots for volume: '+snap_src 260 | ) 261 | newsnapsuffix = '.'+main_options.schedule+'.' 262 | vol_snap_list = rest._get( 263 | '/api/json/types/volumes/?name='+snap_src 264 | ).json()['content']['dest-snap-list'] 265 | arr_vol_snap_list_component = [] 266 | for vol_snap_list_rs in vol_snap_list: 267 | if newsnapsuffix in vol_snap_list_rs[1]: 268 | if '.folder.' in vol_snap_list_rs[1]: 269 | pass 270 | else: 271 | arr_vol_snap_list_component.append(vol_snap_list_rs[1]) 272 | arr_vol_snap_list_component.sort(reverse=True) 273 | for y in range(len(arr_vol_snap_list_component)): ##<--shifting the suffix of each matchin snap by 1 274 | if newsnapsuffix in arr_vol_snap_list_component[y]: 275 | list_snapname = [] 276 | try: 277 | list_snapname = arr_vol_snap_list_component[y].split('.',3) 278 | except: 279 | pass 280 | rename_to =list_snapname[0]+newsnapsuffix+str(y+1) 281 | rename_payload = '{"vol-name": \"'+rename_to+'\"}' 282 | rename_resp = rest._put( 283 | '/api/json/types/volumes/?name='+arr_vol_snap_list_component[y], 284 | rename_payload 285 | ) 286 | if rename_resp.status_code == 200: 287 | main_logger.info( 288 | 'Snapshot: '+arr_vol_snap_list_component[y]+' was renamed to '+rename_to 289 | ) 290 | timestamp = datetime.datetime.now() 291 | timestamp = timestamp.isoformat() 292 | arr_timestamp = timestamp.split('.',2) ##<--stripping the microseconds from the timestamp for aesthetics 293 | fullsuffix = '_'+arr_timestamp[0]+newsnapsuffix 294 | newsnap = snap_src+fullsuffix+'0'##<--sets the newly created snapshot to always be .0 295 | vol_snap_payload = '{\ 296 | "ancestor-vol-id": \"'+snap_src+'\" , \ 297 | "snap-vol-name": \"'+newsnap+'\" ,\ 298 | "folder-id": \"/'+snap_tgt_folder+'\" \ 299 | }' 300 | vol_snap_resp = rest._post( 301 | '/api/json/types/snapshots', 302 | vol_snap_payload 303 | ) 304 | vol_snap_list = rest._get( 305 | '/api/json/types/volumes/?name='+snap_src 306 | ).json()['content']['dest-snap-list'] 307 | arr_vol_snap_list_component = [] 308 | for vol_snap_list_rs in vol_snap_list: 309 | if newsnapsuffix in vol_snap_list_rs[1]: 310 | if '.folder.' in vol_snap_list_rs[1]: 311 | pass 312 | else: 313 | arr_vol_snap_list_component.append(vol_snap_list_rs[1]) 314 | arr_vol_snap_list_component.sort(reverse=False) 315 | for x in xrange(len(arr_vol_snap_list_component)-(int(num_snaps))): 316 | if newsnapsuffix in arr_vol_snap_list_component[x]: 317 | main_logger.debug(str(x)+': '+ arr_vol_snap_list_component[x]) 318 | get_snap_details = rest._get( 319 | '/api/json/types/snapshots/?name='+arr_vol_snap_list_component[x] 320 | ) 321 | arr_ancestor_vol_id = get_snap_details.json()['content']['ancestor-vol-id'] 322 | snap_parent_name = arr_ancestor_vol_id[1] 323 | snap_creation_time = get_snap_details.json()['content']['creation-time'] 324 | snap_space_consumed = get_snap_details.json()['content']['logical-space-in-use'] 325 | arr_snap_lun_mapping = get_snap_details.json()['content']['lun-mapping-list'] 326 | main_logger.warn( 327 | 'Parent Volume of '+arr_vol_snap_list_component[x]+' = '+snap_parent_name 328 | ) 329 | main_logger.warn( 330 | 'Snapshot: '+arr_vol_snap_list_component[x] 331 | ) 332 | main_logger.warn( 333 | ' Snap was created on '+snap_creation_time 334 | ) 335 | main_logger.warn( 336 | ' Snap is using '+ str((float(snap_space_consumed)/1024)/1024)+' GB' 337 | ) 338 | arr_lun_mapping_component = [] 339 | if len(arr_snap_lun_mapping) > 0:##<--checking to see if an active LUN mapping exists 340 | for rs in arr_snap_lun_mapping: 341 | arr_lun_mapping_component = [[y] for y in rs[0]] 342 | arr_lun_mapping_component =str(arr_lun_mapping_component[1]) 343 | arr_lun_mapping_component = arr_lun_mapping_component.replace('[u\'','') 344 | arr_lun_mapping_component = arr_lun_mapping_component.replace('\']','') 345 | main_logger.critical( 346 | 'Snapshot '+arr_vol_snap_list_component[x]+' is currently mapped to '+arr_lun_mapping_component+', it will not be deleted.' 347 | ) 348 | else: 349 | main_logger.warn( 350 | ' No hosts mapped to '+arr_vol_snap_list_component[x]+', it will be deleted.' 351 | ) 352 | delete_status = rest._delete( 353 | '/api/json/types/volumes/?name='+arr_vol_snap_list_component[x] 354 | ) 355 | else: 356 | print 'NO FOLDER OR VOLUME OPTION SPECIFIED' 357 | sys.exit(1) 358 | 359 | main_logger.info('Complete!') 360 | sys.exit(0) 361 | 362 | if __name__ == '__main__': 363 | original_sigint = signal.getsignal(signal.SIGINT) 364 | signal.signal(signal.SIGINT, exit_gracefully) 365 | options = docopt( 366 | __doc__, 367 | argv=None, 368 | help=True, 369 | version=None, 370 | options_first=False 371 | ) 372 | main() 373 | 374 | 375 | --------------------------------------------------------------------------------