├── 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 | 
96 |
97 | XtremIOSnap --e --f --snap= --n= --schedule= --tf=
98 |
99 | 
100 |
101 | XtremIOSnap --e --f --snap= --n= --schedule= --tf=
102 |
103 | 
104 |
105 | XtremIOSnap --e --v --snap= --n= --schedule= --tf=
106 |
107 | 
108 |
109 | XtremIOSnap --e --v --snap= --n= --schedule= --tf=
110 |
111 | 
112 |
113 | Showing the snapshot folder under an existing folder:
114 |
115 | 
116 |
117 | Showing a set of 5, hourly, snapshots:
118 |
119 | 
120 |
121 | Showing the snapshot hierarchy view with a mix of individual volume snaps and folder snaps:
122 |
123 | 
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 |
--------------------------------------------------------------------------------