├── .gitignore ├── LICENSE ├── README.md ├── dozones.py ├── library ├── cisco_funcs.py ├── na_funcs.py └── utils.py ├── singleinitiatorzone ├── README.md ├── checkwwpnigroupfcaliases.py ├── genzonesfromexistingfcaliases.py └── initiatorscheck.py └── smartzone ├── README.md ├── gen_smartzones.py └── zone.conf /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .DS_Store -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Python scripts to automate MDS fibre channel zoning 2 | 3 | Fibre channel zoning is a challenge for storage admins and a time sink and, 4 | automation helps reduce the potential for human error. This scripts will help 5 | generate and push out zoning changes. 6 | 7 | > **IMPORTANT**: The multidue of objects, lengthy names and WWPNs require standardization. 8 | 9 | ## Requirements 10 | 11 | - [Python 2.7+](https://www.python.org/download/releases/2.7/) 12 | - [NetrMiko](https://github.com/ktbyers/netmiko) and it's dependencies 13 | - [CiscoConfParse](https://github.com/mpenning/ciscoconfparse) and it's dependencies 14 | 15 | ## Documentation 16 | 17 | - [Single Initiator](singleinitiatorzone/README.md) 18 | - [SmartZone](smartzone/README.md) 19 | -------------------------------------------------------------------------------- /dozones.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # dozones.py - Scott Harney 3 | # 4 | # from generated zoning list, create zones on Cisco MDS switches 5 | # requires netmiko from https://github.com/ktbyers/netmiko/tree/master/netmiko 6 | 7 | 8 | import sys 9 | import argparse 10 | import os 11 | import getpass 12 | import time 13 | sys.path.append("./library") 14 | from na_funcs import * 15 | from cisco_funcs import * 16 | debug = False 17 | 18 | # parse command line arguments and optional environment variables 19 | arguments = argparse.ArgumentParser( 20 | description='push zoning configuration to MDS switch from generated command file. Zoneset name and VSAN are derived form the input file.') 21 | arguments.add_argument( 22 | '--hostname', required=True, type=str, 23 | help='MDS switch fqdn or IP.') 24 | arguments.add_argument( 25 | '--filename', required=True, type=str, 26 | help='Generated file with zoning commands to push to the mds switch.') 27 | arguments.add_argument( 28 | '--username', required=False, type=str, 29 | help='optional username to ssh into mds switch. Alternate: set environment variable MDS_USERNAME. If neither exists, defaults to current OS username') 30 | arguments.add_argument( 31 | '--password', required=False, type=str, 32 | help='optional password to ssh into mds switch. Alternate: set environment variable MDS_PASSWORD. If unset use_keys defaults to True.') 33 | arguments.add_argument( 34 | '--use_keys', action='store_true', 35 | help='use ssh keys to log into switch') 36 | arguments.add_argument( 37 | '--dry_run', default=False, action='store_true', 38 | help='don\'t do anything. just print some debug output') 39 | arguments.add_argument( 40 | '--key_file', required=False, type=str, 41 | help='filename for ssh key file') 42 | arguments.add_argument( 43 | '--activate_zoneset', action='store_true', 44 | help='add the \'zoneset activate\' command to activate the updated zoneset') 45 | args = arguments.parse_args() 46 | 47 | if args.password : 48 | use_keys = False 49 | password = args.password 50 | elif os.getenv('MDS_PASSWORD') : 51 | use_keys = False 52 | password = os.getenv('MDS_PASSWORD') 53 | else : 54 | use_keys = True 55 | password = '' 56 | 57 | if args.username : 58 | username = args.username 59 | elif os.getenv('MDS_USERNAME') : 60 | username = os.getenv('MDS_USERNAME') 61 | else: 62 | username = getpass.getuser() 63 | 64 | if args.activate_zoneset : 65 | activate = True 66 | else : 67 | activate = False 68 | 69 | mds = args.hostname 70 | zone_commands_filename = args.filename 71 | keyfile = args.key_file 72 | dry_run = args.dry_run 73 | 74 | if not has_netmiko : 75 | print "netmiko is required to use this script, download installation from:" 76 | print "https://github.com/ktbyers/netmiko/tree/master/netmiko" 77 | exit(1) 78 | 79 | # call nonblank_lines to clean up input and load command set into list variable. 80 | commands = [] 81 | with open(zone_commands_filename) as f_in: 82 | for line in nonblank_lines(f_in) : 83 | if "zoneset name ZS_" in line : # populating zoneset & vsan based on simple pattern matching. this is a hack. 84 | zoneset_line = line.split() 85 | zoneset = zoneset_line[2] 86 | vsan = zoneset_line[4] 87 | else: 88 | commands.append(line) 89 | 90 | # connect to mds 91 | mds = { 92 | 'device_type': 'cisco_nxos', 93 | 'ip': mds, 94 | 'verbose': False, 95 | 'username': username, 96 | 'password': password, 97 | 'use_keys': use_keys 98 | } 99 | 100 | 101 | net_connect = ConnectHandler(**mds) 102 | 103 | if dry_run : 104 | output = net_connect.find_prompt() 105 | print "DRY RUN: prompt = %s" % output 106 | 107 | #uncomment lines below to actually do this 108 | if not dry_run : 109 | output = net_connect.send_config_set(commands) 110 | print output 111 | else : 112 | print "DRY RUN: command list = %r" % commands 113 | 114 | zoneset_command = "zoneset activate name %s vsan %s\n" % (zoneset,vsan) 115 | if activate and not dry_run : 116 | net_connect.config_mode() 117 | output = handle_mds_continue(net_connect, cmd=zoneset_command) 118 | print output 119 | net_connect.exit_config_mode() 120 | net_connect.send_command('copy run start') 121 | elif dry_run and not activate : 122 | print "DRY RUN: no activate command" 123 | else : 124 | print "DRY RUN: activate command = %r" % zoneset_command 125 | 126 | net_connect.disconnect() 127 | -------------------------------------------------------------------------------- /library/cisco_funcs.py: -------------------------------------------------------------------------------- 1 | # cisco_funcs.py 2 | # 3 | # helper functions for mds switches 4 | # uses netmiko and ciscoconfparse 5 | 6 | import re 7 | 8 | try: 9 | from netmiko import ConnectHandler 10 | has_netmiko = True 11 | except: 12 | has_netmiko = False 13 | try: 14 | from ciscoconfparse import CiscoConfParse 15 | has_ciscoconfparse = True 16 | except: 17 | has_ciscoconfparse = False 18 | 19 | if not has_netmiko : 20 | print "netmiko is required to use this script, download installation from:" 21 | print "https://github.com/ktbyers/netmiko/tree/master/netmiko" 22 | exit(1) 23 | 24 | if not has_ciscoconfparse : 25 | print "The ciscoconfparse module is needed. Download " 26 | print "installation from: https://github.com/mpenning/ciscoconfparse" 27 | exit(1) 28 | 29 | def parsefcaliases(cisco_cfg) : 30 | """ parse fcalias data from cisco show running-config into list of dictionaries """ 31 | fcalias_dict = {} 32 | fcaliases = cisco_cfg.find_objects(r"^fcalias name") 33 | for fcalias_line in fcaliases : 34 | fcalias_line_list = fcalias_line.text.strip().split() 35 | curfcalias = fcalias_line_list[2] 36 | fcalias_dict[curfcalias] = {} 37 | curfcalias_dict = {} 38 | curfcalias_dict['name'] = curfcalias 39 | curfcalias_dict['vsan'] = fcalias_line_list[4] 40 | pwwn_list = [] 41 | for members_line in fcalias_line.children : 42 | members_line_list = members_line.text.strip().split() 43 | pwwn_list.append(members_line_list[2]) 44 | curfcalias_dict['pwwns'] = pwwn_list 45 | if pwwn_list == [] : 46 | curfcalias_dict['pwwns'] = [] 47 | fcalias_dict[curfcalias] = curfcalias_dict 48 | 49 | return(fcalias_dict) 50 | 51 | def nonblank_lines(f): 52 | for l in f: 53 | line = l.strip() 54 | if line: 55 | yield line 56 | 57 | def handle_mds_continue(net_connect, cmd): 58 | net_connect.remote_conn.sendall(cmd) 59 | time.sleep(1) 60 | output = net_connect.remote_conn.recv(65535).decode('utf-8') 61 | if 'want to continue' in output: 62 | net_connect.remote_conn.sendall('y\n') 63 | output += net_connect.remote_conn.recv(65535).decode('utf-8') 64 | return output 65 | 66 | def getzones(sh_zones) : 67 | ''' get a list of zone dictionies and their memberchildren in lists of dictionaries''' 68 | zones = sh_zones.find_objects(r"zone name") 69 | zones_list = [] 70 | for zone in zones : 71 | zone_list = [] 72 | zoneline_dict = {} 73 | zoneline = zone.text.strip().split() 74 | zoneline_dict['vsan'] = zoneline[4] 75 | zoneline_dict['name'] = zoneline[2] 76 | zone_list.append(zoneline_dict) 77 | for fcalias in zone.children : 78 | fcaliases = [] 79 | fcaliasline = fcalias.text.strip().split() 80 | fcaliasline_dict = {} 81 | try: 82 | fcaliasline_dict['fcalias'] = fcaliasline[2] 83 | except: 84 | fcaliasline_dict['fcalias'] = None 85 | fcaliases.append(fcaliasline_dict) 86 | for member in fcalias.children: 87 | members = [] 88 | memberline = member.text.strip().split() 89 | memberline_dict = {} 90 | memberline_dict['pwwn'] = memberline[1] 91 | members.append(memberline_dict) 92 | fcaliases.append(members) 93 | zone_list.append(fcaliases) 94 | zones_list.append(zone_list) 95 | 96 | return(zones_list) 97 | 98 | def zone_exists(mds, zone_name, vsan_id): 99 | """ Check if a specifc zone name exists on MDS switch opening a connection with MDS switch 100 | and execute \"show zoneset brief\" commands receiving vsan id as a param """ 101 | 102 | # Instantiate a netmiko connection with MDS and, 103 | # if anything goes wrong a wxception will raise 104 | try: 105 | conn = ConnectHandler(**mds) 106 | except Exception, e: 107 | raise e 108 | 109 | # Receive VSAN ID param and define the zoneset command, 110 | # send to MDS, store the result into a variable 111 | command = 'show zoneset brief vsan %s' % vsan_id 112 | zoneset_brief = conn.send_command(command) 113 | 114 | # Close the connection after execute command 115 | conn.disconnect() 116 | 117 | # Compile a regex with the received zone name 118 | # make a search into the zoneset resukt variable, 119 | # return True if the zone name was found or False if doesn't 120 | regex = re.compile(zone_name) 121 | 122 | if regex.search(zoneset_brief): 123 | return True 124 | else: 125 | return False 126 | 127 | def zoneset_exists(mds, zoneset_name, vsan_id): 128 | """ Check if a specifc zoneset name exists on MDS switch opening a connection with MDS switch 129 | and execute \"show zoneset brief\" commands receiving vsan id as a param """ 130 | 131 | # Instantiate a netmiko connection with MDS and, 132 | # if anything goes wrong a wxception will raise 133 | try: 134 | conn = ConnectHandler(**mds) 135 | except Exception, e: 136 | raise e 137 | 138 | # Receive VSAN ID param and define the zoneset command, 139 | # send to MDS, store the result into a variable 140 | command = 'show zoneset brief vsan %s' % vsan_id 141 | zoneset_brief = conn.send_command(command) 142 | 143 | # Close the connection after execute command 144 | conn.disconnect() 145 | 146 | # Compile a regex with the received zoneset name 147 | # make a search into the zoneset result variable, 148 | # return True if the zoneset name and VSAN ID was found or False if doesn't 149 | regex = re.compile("^zoneset.*(%s).vsan.*(%s)" % (zoneset_name,vsan_id)) 150 | 151 | if regex.search(zoneset_brief): 152 | return True 153 | else: 154 | return False 155 | 156 | def count_smartzone_members(mds, zone_name): 157 | """ Count the total members existent on a specifc smartzone, including initiator and target members """ 158 | 159 | # Instantiate a netmiko connection with MDS and, 160 | # if anything goes wrong a wxception will raise 161 | try: 162 | conn = ConnectHandler(**mds) 163 | except Exception, e: 164 | raise e 165 | 166 | # Receive zone name as a param and define the command to be 167 | # send to MDS, store the result into a variable 168 | command = 'show zone name %s' % zone_name 169 | zone_members = conn.send_command(command) 170 | 171 | # Close the connection after execute command 172 | conn.disconnect() 173 | 174 | # Compile a regex with the received zone name 175 | # make a search into the zone result variable, 176 | # and count the total number of matches 177 | regex = re.compile('pwwn.*(init|target)') 178 | 179 | members = 0 180 | 181 | for member in zone_members.split('\n'): 182 | if regex.search(member): 183 | members += 1 184 | 185 | return members 186 | 187 | def device_alias_exists(mds, pwwn): 188 | """ Count the total members existent on a specifc smartzone, including initiator and target members """ 189 | 190 | # Instantiate a netmiko connection with MDS and, 191 | # if anything goes wrong a wxception will raise 192 | try: 193 | conn = ConnectHandler(**mds) 194 | except Exception, e: 195 | raise e 196 | 197 | # Receive zone name as a param and define the command to be 198 | # send to MDS, store the result into a variable 199 | command = 'show device-alias database' 200 | device_alias_db = conn.send_command(command) 201 | 202 | # Close the connection after execute command 203 | conn.disconnect() 204 | 205 | # Compile a regex with the received zone name 206 | # make a search into the zone result variable, 207 | # and count the total number of matches 208 | regex = re.compile('device-alias\sname\s(.*)pwwn\s(%s)' % pwwn) 209 | 210 | for device_alias in device_alias_db.split('\n'): 211 | if regex.search(device_alias): 212 | return device_alias.split()[2] -------------------------------------------------------------------------------- /library/na_funcs.py: -------------------------------------------------------------------------------- 1 | # na_funcs.py 2 | # 3 | # helper functions for netapp cdot filers 4 | # uses netapp managability sdk 5 | 6 | import sys 7 | sys.path.append("../NetApp/netapp-manageability-sdk-5.6/lib/python/NetApp/") 8 | try: 9 | from NaServer import * 10 | has_nasdk = True 11 | except: 12 | has_nasdk = False 13 | 14 | 15 | if not has_nasdk : 16 | print "NetApp managability SDK 5.2 or higher is required to use this script. download" 17 | print "installation from: https://support.netapp.com" 18 | exit(1) 19 | 20 | def cdotconnect(filer, username, password) : 21 | """ return a filer connection handle """ 22 | s = NaServer(filer, 1 , 31) 23 | s.set_server_type("FILER") 24 | s.set_transport_type("HTTP") # would like to use HTTPS but I get ssl cert errors on Ubuntu 15.x 25 | s.set_port(80) 26 | s.set_style("LOGIN") 27 | s.set_admin_user(username, password) 28 | return s 29 | 30 | def getigroupwwpns(igroupname, filerconnection) : 31 | """ given an igroup name and connection handle, return normalized 32 | list of initiators in that igroup from cDOT filer 33 | """ 34 | api = NaElement("igroup-get-iter") 35 | query = NaElement("query") 36 | api.child_add(query) 37 | 38 | initiator_group_info = NaElement("initiator-group-info") 39 | query.child_add(initiator_group_info) 40 | 41 | initiator_group_info.child_add_string("initiator-group-name",igroupname) 42 | 43 | xo = filerconnection.invoke_elem(api) 44 | if (xo.results_status() == "failed") : 45 | print ("Error:\n") 46 | print (xo.sprintf()) 47 | sys.exit (1) 48 | 49 | attributes = xo.child_get("attributes-list") 50 | initiator_group_info = attributes.child_get("initiator-group-info") 51 | initiators = initiator_group_info.child_get("initiators").children_get() 52 | 53 | items = [] 54 | for igroup in initiators : 55 | item = igroup.child_get_string("initiator-name") 56 | items.append(item) 57 | 58 | return items 59 | 60 | def getigrouplist(NaElement) : 61 | ''' return a list of igroup dictionaries''' 62 | igroup_list = [] 63 | 64 | for igroups in NaElement.children_get() : 65 | initiator_dict = {} 66 | igroup = '' 67 | igroup = igroups.child_get_string('initiator-group-name') 68 | initiator_dict['igroup'] = igroup 69 | igroup_list.append(initiator_dict) 70 | 71 | return (igroup_list) 72 | 73 | def getfcpconnectedinitiators(NaElement) : 74 | ''' return a list of connected fcp initiators''' 75 | 76 | connected_initiators_list = [] 77 | 78 | for getitem in NaElement.children_get() : 79 | initiator_dict = {} 80 | try: 81 | igroup_list = getigrouplist(getitem.child_get('initiator-group-list')) 82 | except: 83 | igroup_list = [{'igroup': None}] 84 | initiator_dict['igroups'] = igroup_list 85 | initiator_dict['wwpn'] = getitem.child_get_string('port-name') 86 | connected_initiators_list.append(initiator_dict) 87 | 88 | return (connected_initiators_list) 89 | 90 | def getfcpinitiators(NaElement) : 91 | ''' return a list of dicts containing connected fcp initiator information ''' 92 | 93 | initiators_list =[] 94 | 95 | for getitem in NaElement.child_get('attributes-list').children_get() : 96 | vserver_adapter_dict = {} 97 | connected_initiators_list = [] 98 | vserver_adapter_dict['vserver'] = getitem.child_get_string('vserver') 99 | vserver_adapter_dict['adapter'] = getitem.child_get_string('adapter') 100 | initiators_list.append(vserver_adapter_dict) 101 | connected_initiators_list = getfcpconnectedinitiators(getitem.child_get('fcp-connected-initiators')) 102 | initiators_list.append(connected_initiators_list) 103 | 104 | return (initiators_list) -------------------------------------------------------------------------------- /library/utils.py: -------------------------------------------------------------------------------- 1 | # utils.py 2 | # 3 | # General utilities functions 4 | 5 | class bcolors: 6 | OKGREEN = '\033[92m' # Green 7 | WARNING = '\033[93m' # Yellow 8 | FAIL = '\033[91m' # Red 9 | OKBLUE = '\033[94m' # Blue 10 | HEADER = '\033[95m' # Pink 11 | BOLD = '\033[1m' # Bold text 12 | UNDERLINE = '\033[4m' # Underline text 13 | ENDC = '\033[0m' # EOL 14 | 15 | def confirm(prompt=None, resp=False): 16 | """ Prompts yes or no response to user user. 17 | Returns True for yes and False for no """ 18 | 19 | # Set a default message if no param was received 20 | if prompt is None: 21 | prompt = 'Confirm?' 22 | 23 | # Check if 'resp' was set to True the default answer will be set to Y (yes), 24 | # if varible wasn't set the default answer will be N (No) 25 | if resp: 26 | prompt = '%s [%s/%s]: ' % (prompt, 'Y', 'n') 27 | else: 28 | prompt = '%s [%s/%s]: ' % (prompt, 'y', 'N') 29 | 30 | # Create a while loop to read the answer from terminal. 31 | # If no answer, the print will be returned 32 | while True: 33 | answer = raw_input(prompt) 34 | if not answer: 35 | return resp 36 | if answer not in ['y', 'Y', 'n', 'N']: 37 | print 'Please enter y or n.\n' 38 | continue 39 | if answer == 'y' or answer == 'Y': 40 | return True 41 | if answer == 'n' or answer == 'N': 42 | return False 43 | -------------------------------------------------------------------------------- /singleinitiatorzone/README.md: -------------------------------------------------------------------------------- 1 | # Single Initiator Zone 2 | 3 | The first script generates zoning commands suitable for Cisco MDS switches 4 | running NX-OS by examining existing fcalias entries and using pattern 5 | matching aginst a provided list of hostnames. The use case here is a SAN 6 | migration to a new target array, in my particular initial case a NetApp 7 | cDOT cluster. This may work on other Cisco NX-OS devices for fiber channel 8 | with minimal modifications. The reliance here is existing fcalias entries 9 | that contain server hostnames and therefore can be matched against with 10 | case-insenstive regexes. 11 | 12 | ## List of Scripts 13 | 14 | - [genzonesfromexistingfcaliases.py](.genzonesfromexistingfcaliases.py) 15 | - [checkwwpnigroupfcaliases.py](.checkwwpnigroupfcaliases.py) 16 | - [initiatorscheck.py](.initiatorscheck.py) 17 | 18 | ## genzonesfromexistingfcaliases.py 19 | 20 | ``` 21 | usage: genzonesfromexistingfcaliases.py [-h] --hostname HOSTNAME 22 | --hosts_filename HOSTS_FILENAME --vsan 23 | VSAN --zoneset ZONESET 24 | [--fcalias_filename FCALIAS_FILENAME] 25 | [--target_fcalias TARGET_FCALIAS] 26 | [--username USERNAME] 27 | [--get_from_switch] 28 | [--password PASSWORD] [--use_keys] 29 | [--backout] [--key_file KEY_FILE] 30 | 31 | Generate zoning commands from input file listing of short hostnames one per 32 | line. Will match against switch fcalias entries by hostname pattern. print to 33 | STDOUT. redirect with > filename.txt 34 | 35 | optional arguments: 36 | -h, --help show this help message and exit 37 | --hostname HOSTNAME MDS switch fqdn or IP. 38 | --hosts_filename HOSTS_FILENAME 39 | list of hosts to match against. one per line 40 | --vsan VSAN VSAN for fcaliases/zones 41 | --zoneset ZONESET zoneset name 42 | --fcalias_filename FCALIAS_FILENAME 43 | generated fcaliases output from 'ssh 44 | username@switchname show fcalias > 45 | switch_fcaliases.txt' 46 | --target_fcalias TARGET_FCALIAS 47 | optional fcalias name of cDOT cluster on switch. 48 | default ='NAC1' 49 | --username USERNAME optional username to ssh into mds switch. Alternate: 50 | set environment variable MDS_USERNAME. If neither 51 | exists, defaults to current OS username 52 | --get_from_switch get fcaliases directly from switch instead of file. 53 | NOTE: not yet implemented 54 | --password PASSWORD optional password to ssh into mds switch. Alternate: 55 | set environment variable MDS_PASSWORD. If unset 56 | use_keys defaults to True. 57 | --use_keys use ssh keys to log into switch 58 | --backout generate backout commands 59 | --key_file KEY_FILE filename for ssh key file 60 | ``` 61 | 62 | > **note**: This script currently relies on the `--fcalias_filename` argument 63 | and that the functionality to pull fcalias data directly from the switch is 64 | not yet implemented as of 2015-12-17. The output file save via redirection eg. 65 | `> output_zoning.txt` could simply be reviewed and cut and pasted into the 66 | relevant switch. 67 | 68 | ## initiatorscheck.py | checkwwpnigroupfcaliases.py 69 | These scripts perform checks that zoning items are as 70 | expected. Note that they also require the NetApp Manageability SDK which is 71 | for now behind support.netapp.com and requires a netapp login. The first 72 | script is a comparison of NetApp igroup member WWPNs and fcaliases on the 73 | switch. It's a post-check for some of the work here. The second script is 74 | just a sanity check against a filer only for initiators that are logged in 75 | (zoned) but have no igroup membership. 76 | -------------------------------------------------------------------------------- /singleinitiatorzone/checkwwpnigroupfcaliases.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # checkwwpngroupfcaliases.py 3 | # 4 | # interrogate a NetApp cDOT cluster to retrive an igroup list of WWPNs and compare that against 5 | # a cisco mds switch list of fcaliases. 6 | 7 | 8 | import sys 9 | import argparse 10 | import os 11 | import getpass 12 | import time 13 | import pprint 14 | sys.path.append("./library") 15 | from na_funcs import * 16 | from cisco_funcs import * 17 | debug = False 18 | 19 | # parse command line arguments and optional environment variables 20 | 21 | arguments = argparse.ArgumentParser( 22 | description='Provide an igroup and filer and check the wwpns in that igroup are defined as fcaliases on an mds switch') 23 | arguments.add_argument( 24 | '--switch_hostname', required=True, type=str, 25 | help='MDS switch fqdn or IP.') 26 | arguments.add_argument( 27 | '--filer_hostname', required=True, type=str, 28 | help='filer fqdn or IP.') 29 | arguments.add_argument( 30 | '--igroup', required=True, type=str, 31 | help='name of igroup on NetApp cDOT system to check') 32 | arguments.add_argument( 33 | '--switch_username', required=False, type=str, 34 | help='optional username to ssh into mds switch. Alternate: set environment variable MDS_USERNAME. If neither exists, defaults to current OS username') 35 | arguments.add_argument( 36 | '--switch_password', required=False, type=str, 37 | help='optional password to ssh into mds switch. Alternate: set environment variable MDS_PASSWORD. If unset use_keys defaults to True.') 38 | arguments.add_argument( 39 | '--switch_use_keys', action='store_true', 40 | help='use ssh keys to log into switch') 41 | arguments.add_argument( 42 | '--switch_key_file', required=False, type=str, 43 | help='filename for ssh key file') 44 | arguments.add_argument( 45 | '--filer_username', required=False, type=str, 46 | help='optional username to ssh into mds switch. Alternate: set environment variable FILER_USERNAME. If neither exists, defaults to admin') 47 | arguments.add_argument( 48 | '--filer_password', required=False, type=str, 49 | help='optional password to ssh into mds switch. Alternate: set environment variable filer_PASSWORD. If unset use_keys defaults to True.') 50 | 51 | args = arguments.parse_args() 52 | 53 | if args.switch_password : 54 | use_keys = False 55 | switch_password = args.switch_password 56 | elif os.getenv('MDS_PASSWORD') : 57 | use_keys = False 58 | switch_password = os.getenv('MDS_PASSWORD') 59 | else : 60 | switch_use_keys = True 61 | switch_password = '' 62 | 63 | if args.switch_username : 64 | switch_username = args.switch_username 65 | elif os.getenv('MDS_USERNAME') : 66 | switch_username = os.getenv('MDS_USERNAME') 67 | else: 68 | switch_username = getpass.getuser() 69 | 70 | if args.filer_password : 71 | filer_password = args.filer_password 72 | elif os.getenv('FILER_PASSWORD') : 73 | filer_password = os.getenv('FILER_PASSWORD') 74 | else : 75 | filer_password = '' 76 | 77 | if args.filer_username : 78 | filer_username = args.filer_username 79 | elif os.getenv('FILER_USERNAME') : 80 | filer_username = os.getenv('FILER_USERNAME') 81 | else: 82 | filer_username = 'admin' 83 | 84 | 85 | switch_hostname = args.switch_hostname 86 | filer_hostname = args.filer_hostname 87 | igroup = args.igroup 88 | 89 | # main loop 90 | 91 | mds = { 92 | 'device_type': 'cisco_nxos', 93 | 'ip': switch_hostname, 94 | 'verbose': False, 95 | 'username': switch_username, 96 | 'password': switch_password, 97 | 'use_keys': switch_use_keys 98 | } 99 | 100 | #igroup = 'JDC-Prod-01' 101 | #filerusername = 'admin' 102 | #filerpassword = 'netapp123' 103 | #filerhostname = 'jdc-nac1.prod.entergy.com' 104 | 105 | filerconnect = cdotconnect(filer_hostname,filer_username,filer_password) 106 | wwpns = getigroupwwpns(igroup, filerconnect) 107 | 108 | net_connect = ConnectHandler(**mds) 109 | show_run_str = net_connect.send_command("show run") 110 | show_run = show_run_str.splitlines() 111 | cisco_cfg = CiscoConfParse(show_run) 112 | fcalias_dict = parsefcaliases(cisco_cfg) 113 | net_connect.disconnect() 114 | 115 | 116 | if debug : 117 | print "DEBUG START: dump grabbed igroup WWPNs and switch fcaliases" 118 | pp = pprint.PrettyPrinter(indent=4) 119 | pp.pprint(wwpns) 120 | pp.pprint(fcalias_dict) 121 | print "DEBUG END: " 122 | 123 | for check_wwpn in wwpns : 124 | for key in fcalias_dict.keys() : 125 | if check_wwpn in fcalias_dict[key]['pwwns'] : 126 | print "%s in igroup %s matches fcalias name %s on mds %s vsan %s" % (check_wwpn, igroup, fcalias_dict[key]['name'], switch_hostname, fcalias_dict[key]['vsan']) 127 | 128 | 129 | -------------------------------------------------------------------------------- /singleinitiatorzone/genzonesfromexistingfcaliases.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # genzonesfromexistingfcaliases.py - Scott Harney 3 | # 4 | # generate zoning commands given output of "show fcalias" from switch 5 | # assumes zones have lowercase entries and single initiator zoning is used 6 | # generates zoning commands to add new array. List of hostnames should 7 | # also be provided one per line in a file. 8 | # populate relevant variables below 9 | 10 | import sys 11 | import argparse 12 | import os 13 | import getpass 14 | import re 15 | sys.path.append("./library") 16 | from na_funcs import * 17 | from cisco_funcs import * 18 | debug = True 19 | 20 | # parse command line arguments and optional environment variables 21 | 22 | arguments = argparse.ArgumentParser( 23 | description='Generate zoning commands from input file listing of short hostnames one per line. Will match against switch fcalias entries by hostname pattern. print to STDOUT. redirect with > filename.txt') 24 | arguments.add_argument( 25 | '--hostname', required=True, type=str, 26 | help='MDS switch fqdn or IP.') 27 | arguments.add_argument( 28 | '--hosts_filename', required=True, type=str, 29 | help='list of hosts to match against. one per line') 30 | arguments.add_argument( 31 | '--vsan', required=True, type=str, 32 | help='VSAN for fcaliases/zones') 33 | arguments.add_argument( 34 | '--zoneset', required=True, type=str, 35 | help='zoneset name') 36 | arguments.add_argument( 37 | '--fcalias_filename', required=True, type=str, 38 | help='generated fcaliases output from \'ssh username@switchname show fcalias > switch_fcaliases.txt\'') 39 | arguments.add_argument( 40 | '--target_fcalias', required=False, type=str, 41 | default='NAC1', help='optional fcalias name of cDOT cluster on switch. default =\'NAC1\'') 42 | arguments.add_argument( 43 | '--username', required=False, type=str, 44 | help='optional username to ssh into mds switch. Alternate: set environment variable MDS_USERNAME. If neither exists, defaults to current OS username') 45 | arguments.add_argument( 46 | '--get_from_switch', default=False, action='store_true', 47 | help='get fcaliases directly from switch instead of file. NOTE: not yet implemented') 48 | arguments.add_argument( 49 | '--password', required=False, type=str, 50 | help='optional password to ssh into mds switch. Alternate: set environment variable MDS_PASSWORD. If unset use_keys defaults to True.') 51 | arguments.add_argument( 52 | '--use_keys', action='store_true', 53 | help='use ssh keys to log into switch') 54 | arguments.add_argument( 55 | '--backout', default=False, action='store_true', 56 | help='generate backout commands') 57 | arguments.add_argument( 58 | '--key_file', required=False, type=str, 59 | help='filename for ssh key file') 60 | 61 | args = arguments.parse_args() 62 | 63 | if args.password : 64 | use_keys = False 65 | password = args.password 66 | elif os.getenv('MDS_PASSWORD') : 67 | use_keys = False 68 | password = os.getenv('MDS_PASSWORD') 69 | else : 70 | use_keys = True 71 | password = '' 72 | 73 | if args.username : 74 | username = args.username 75 | elif os.getenv('MDS_USERNAME') : 76 | username = os.getenv('MDS_USERNAME') 77 | else: 78 | username = getpass.getuser() 79 | 80 | if args.fcalias_filename and args.get_from_switch : 81 | print "You can get from the switch directly OR use an input file of fcaliases. pick one." 82 | exit (1) 83 | 84 | switch_hostname = args.hostname 85 | hostname_filename = args.hosts_filename 86 | if args.fcalias_filename: 87 | fcalias_filename = args.fcalias_filename 88 | 89 | NAfcalias = args.target_fcalias 90 | keyfile = args.key_file 91 | vsan = args.vsan 92 | zoneset = args.zoneset 93 | backout = args.backout 94 | 95 | hostnames = open(hostname_filename, "r").readlines() 96 | 97 | if args.get_from_switch : 98 | # connect to mds 99 | mds = { 100 | 'device_type': 'cisco_nxos', 101 | 'ip': switch_hostname, 102 | 'verbose': False, 103 | 'username': username, 104 | 'password': password, 105 | 'use_keys': use_keys 106 | } 107 | net_connect = ConnectHandler(**mds) 108 | show_fcaliases = net_connect.send_command("show fcalias") 109 | fcaliases = show_fcaliases.splitlines() 110 | else: 111 | fcaliases = open(fcalias_filename, "r").readlines() 112 | 113 | esxfcaliases = [] 114 | zones = [] 115 | for host in hostnames : 116 | hostpattern = ".*%s.*" % host.strip() 117 | checkhost = re.compile(hostpattern, re.IGNORECASE) 118 | for fcalias in fcaliases: 119 | if checkhost.search(fcalias): 120 | esxhost = fcalias.split() 121 | esxfcaliases.append(esxhost[2]) 122 | zones.append("%s_%s" % (esxhost[2], NAfcalias)) 123 | 124 | if not backout: 125 | for esxfcalias in esxfcaliases : 126 | print "zone name %s_%s vsan %s" % (esxfcalias, NAfcalias, vsan) 127 | print " member fcalias %s" % NAfcalias 128 | print " member fcalias %s" % esxfcalias 129 | print "\n" 130 | 131 | print "zoneset name %s vsan %s" % (zoneset, vsan) 132 | for zone in zones : 133 | print " member %s" % zone 134 | else : 135 | print "\n" 136 | print "zoneset name %s vsan %s" % (zoneset, vsan) 137 | for zone in zones : 138 | print "no member %s" % zone 139 | print "\n" 140 | for esxfcalias in esxfcaliases : 141 | print "no zone name %s_%s vsan %s" % (esxfcalias, NAfcalias, vsan) 142 | 143 | -------------------------------------------------------------------------------- /singleinitiatorzone/initiatorscheck.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # initiatorscheck.py 3 | # 4 | # check fcp initatior show on a filer for logged in initiators with no 5 | # igroup members or multiple igroup members 6 | 7 | import sys 8 | import argparse 9 | import json 10 | import os 11 | import getpass 12 | import time 13 | import pprint 14 | sys.path.append("./library") 15 | from na_funcs import * 16 | from cisco_funcs import * 17 | debug = False 18 | 19 | # parse command line arguments and optional environment variables 20 | 21 | arguments = argparse.ArgumentParser( 22 | description='Get connected fcp initiators from cDOT filer and check that they are mapped to an igroup and also not mapped to multiple igroups ') 23 | arguments.add_argument( 24 | '--filer_hostname', required=True, type=str, 25 | help='filer fqdn or IP') 26 | arguments.add_argument( 27 | '--filer_username', required=False, type=str, 28 | help='optional username to ssh into mds switch. Alternate: set environment variable FILER_USERNAME. If neither exists, defaults to admin') 29 | arguments.add_argument( 30 | '--filer_password', required=False, type=str, 31 | help='optional password to ssh into mds switch. Alternate: set environment variable filer_PASSWORD. If unset use_keys defaults to True.') 32 | arguments.add_argument( 33 | '--svm', '--vserver', required=False, type=str, 34 | help='limit "fcp initiator show" query named netapp vserver') 35 | arguments.add_argument( 36 | '--lif', '--adapter', required=False, type=str, 37 | help='limit "fcp initiator show" query to named netapp vserver lif') 38 | 39 | args = arguments.parse_args() 40 | 41 | if args.filer_password : 42 | filer_password = args.filer_password 43 | elif os.getenv('FILER_PASSWORD') : 44 | filer_password = os.getenv('FILER_PASSWORD') 45 | else : 46 | filer_password = '' 47 | 48 | if args.filer_username : 49 | filer_username = args.filer_username 50 | elif os.getenv('FILER_USERNAME') : 51 | filer_username = os.getenv('FILER_USERNAME') 52 | else: 53 | filer_username = 'admin' 54 | 55 | if args.svm : 56 | svm = args.svm 57 | else : 58 | svm = False 59 | 60 | if args.lif : 61 | lif = args.lif 62 | else: 63 | lif = False 64 | 65 | filer_hostname = args.filer_hostname 66 | 67 | # main loop 68 | pp = pprint.PrettyPrinter(indent=4) 69 | 70 | filerconnect = cdotconnect(filer_hostname,filer_username,filer_password) 71 | 72 | api = NaElement("fcp-initiator-get-iter") 73 | ## can stack elements on to the query to limit output 74 | xi = NaElement("query") 75 | api.child_add(xi) 76 | xi1 = NaElement("fcp-adapter-initiators-info") 77 | xi.child_add(xi1) 78 | if svm : 79 | xi1.child_add_string("vserver",svm) 80 | if lif : 81 | xi1.child_add_string("adapter",lif) 82 | 83 | xo = filerconnect.invoke_elem(api) 84 | if (xo.results_status() == "failed") : 85 | print ("Error:\n") 86 | print (xo.sprintf()) 87 | sys.exit (1) 88 | 89 | if debug : 90 | print xo.sprintf() #debugging 91 | 92 | initiators_list = [] 93 | 94 | initiators_list = getfcpinitiators(xo) 95 | i = 1 96 | for item in range(len(initiators_list)) : 97 | if i > len(initiators_list) : 98 | break # leave the loop when iterator > length of our list 99 | x = 0 100 | for initiators in range(len(initiators_list[i])) : 101 | if len(initiators_list[i][x]['igroups']) > 1 : 102 | print "[ { 'results' : 'multiple igroups found'}," 103 | print "[ %s," % initiators_list[i-1] 104 | print " [ %s ] ]" % initiators_list[i][x] 105 | elif initiators_list[i][x]['igroups'][0]['igroup'] == None : 106 | print "[ { 'results' : 'no igroup found'}," 107 | print "[ %s," % initiators_list[i-1] 108 | print " [ %s ] ] ]" % initiators_list[i][x] 109 | x = x+1 110 | i = i+2 111 | 112 | 113 | 114 | if debug : 115 | print(json.dumps(initiators_list, indent=2, sort_keys=True)) 116 | 117 | 118 | -------------------------------------------------------------------------------- /smartzone/README.md: -------------------------------------------------------------------------------- 1 | # SmartZone 2 | 3 | The script generates zoning commands suitable for Cisco MDS switches 4 | running NX-OS by examining existing params entries and using pattern 5 | matching aginst a provided configuration file containing hostnames, 6 | pwwns and SmartZones. 7 | 8 | ## gen_smartzones.py 9 | 10 | ``` 11 | usage: gen_smartzones.py [-h] -c CONFIG_HOSTS --vsan VSAN --zoneset ZONESET -f 12 | {impar,par} [--check] [-s SWITCH] [-u USERNAME] 13 | [-p PASSWORD] [--use_keys] [--key_file KEY_FILE] 14 | gen_smartzones.py: error: argument -c/--config_hosts is required 15 | esquizophrenia:python-mdszoning italosantos$ ./smartzone/gen_smartzones.py -h 16 | usage: gen_smartzones.py [-h] -c CONFIG_HOSTS --vsan VSAN --zoneset ZONESET -f 17 | {impar,par} [--check] [-s SWITCH] [-u USERNAME] 18 | [-p PASSWORD] [--use_keys] [--key_file KEY_FILE] 19 | 20 | Generate SmartZone commands from input config file listing of short hostnames, 21 | pwwns and zones which each host will belongs. 22 | 23 | optional arguments: 24 | -h, --help show this help message and exit 25 | -c CONFIG_HOSTS, --config_hosts CONFIG_HOSTS 26 | Configuration file with hosts, pwwns and zones 27 | --vsan VSAN VSAN ID 28 | --zoneset ZONESET ZoneSet name 29 | -f {impar,par}, --fabric {impar,par} 30 | Fabric side 31 | --check [optional] Start a validation process by connection on 32 | MDS switch of all params 33 | -s SWITCH, --switch SWITCH 34 | MDS switch fqdn or IP 35 | -u USERNAME, --username USERNAME 36 | [optional] Username to ssh into mds switch. Alternate: 37 | set environment variable MDS_USERNAME. If neither 38 | exists, defaults to current OS username 39 | -p PASSWORD, --password PASSWORD 40 | [optional] Password to ssh into mds switch. Alternate: 41 | set environment variable MDS_PASSWORD. If unset 42 | use_keys defaults to True. 43 | --use_keys [optional] Use ssh keys to log into switch. If set key 44 | file will need be pass as param 45 | --key_file KEY_FILE [optional] filename for ssh key file 46 | ``` 47 | 48 | > **note**: This script currently read the params from the `--config_hosts` argument 49 | and validate all into the MDS if the argument `--check` was passed. The output file 50 | should be saved via redirection eg. `> output_zoning.txt` could simply be reviewed 51 | and cut and pasted into the relevant switch. -------------------------------------------------------------------------------- /smartzone/gen_smartzones.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # gen_smartzones.py - Italo Santos 3 | 4 | import os 5 | import sys 6 | import time 7 | import getpass 8 | sys.path.append("./library") 9 | import argparse 10 | import ConfigParser 11 | from utils import * 12 | from na_funcs import * 13 | from cisco_funcs import * 14 | 15 | def check_on_switch(mds, zoneset, pwwns, zones, vsan, fabric, switch): 16 | 17 | non_existent_zones = [] 18 | alias_exists = {} 19 | zoneset_existent = False 20 | 21 | # http://www.cisco.com/c/en/us/td/docs/switches/datacenter/mds9000/sw/6_2/configuration/guides/config_limits/b_mds_9000_configuration_limits_6_2.html 22 | # Table 2 Fabric-level Fibre Channel Configuration Limits 23 | # Note: The preferred number of members per zone is 2, and the maximum recommended limit is 50. 24 | smartzone_members_limit = 50 25 | 26 | print bcolors.OKGREEN + "Initiate validations ...\n" + bcolors.ENDC 27 | print bcolors.BOLD + "Validating ZoneSet %s and VSAN ID %s on MDS..." % (zoneset, vsan) + bcolors.ENDC 28 | if zoneset_exists(mds, zoneset, vsan) is not False: 29 | zoneset_existent = True 30 | 31 | for pwwn in pwwns: 32 | print bcolors.BOLD + "Validating if device-alias exists with pwwn %s on MDS..." % pwwn + bcolors.ENDC 33 | alias = device_alias_exists(mds, pwwn) 34 | if alias: 35 | alias_exists[pwwn] = alias 36 | 37 | for zone_name in zones.keys(): 38 | if len(zone_name) > 1: 39 | print bcolors.BOLD + "Validating %s on MDS..." % zone_name.strip() + bcolors.ENDC 40 | if zone_exists(mds, zone_name, vsan) is False: 41 | non_existent_zones.append(zone_name) 42 | 43 | print bcolors.BOLD + "Validating number of members of %s on MDS..." % zone_name.strip() + bcolors.ENDC 44 | members = count_smartzone_members(mds, zone_name) 45 | 46 | if alias_exists: 47 | print bcolors.OKBLUE + "\n### INFO! Some device-alias already exists ... ###\n" + bcolors.ENDC 48 | for pwwn, alias in alias_exists.iteritems(): 49 | print bcolors.BOLD + "device-alias %s already exists for %s" % (alias, pwwn) + bcolors.ENDC 50 | 51 | raw_input('\nPress ' + bcolors.BOLD + '[enter]' + bcolors.ENDC + ' to continue ...') 52 | 53 | if zoneset_existent is False or len(non_existent_zones) > 0 or members >= smartzone_members_limit: 54 | print bcolors.WARNING + "\n### ATENTION! Validation found some errors ... ###\n" + bcolors.ENDC 55 | 56 | if zoneset_existent is False: 57 | print bcolors.FAIL + "ZoneSet \"%s\" and/or VSAN ID %s doesn't exists!\n" % (zoneset, vsan) + bcolors.ENDC 58 | 59 | if len(non_existent_zones) > 0: 60 | for zone in non_existent_zones: 61 | print bcolors.FAIL + "Zone \"%s\" doesn't exists!" % zone.strip() + bcolors.ENDC 62 | 63 | if members >= smartzone_members_limit: 64 | print bcolors.FAIL + "Zone \"%s\" has more then 50 members\n" % zone_name.strip() + bcolors.ENDC 65 | 66 | if confirm("Are you sure you want to continue?"): 67 | generate_smartzones(config_file, zoneset, vsan, fabric, switch) 68 | else: 69 | print bcolors.OKGREEN + "\nValidation successfully!" + bcolors.ENDC 70 | generate_smartzones(config_file, zoneset, vsan, fabric, switch) 71 | 72 | def generate_smartzones(config_file, zoneset, vsan, fabric, switch=None, check=False, mds=None): 73 | 74 | try: 75 | config = ConfigParser.ConfigParser() 76 | config.read(config_file) 77 | except Exception, e: 78 | print bcolors.FAIL + "Error reading config file!" + bcolors.ENDC 79 | print bcolors.BOLD + "Exception:" + bcolors.ENDC + "\n%s" % e 80 | exit(1) 81 | 82 | hosts_per_zone = {} 83 | pwwns = [] 84 | 85 | for host in config.sections(): 86 | pwwns.append(config.get(host, fabric)) 87 | 88 | for host in config.sections(): 89 | for zone in config.get(host, 'zones').split(','): 90 | hosts_per_zone[zone] = [] 91 | 92 | for host in config.sections(): 93 | for zone in config.get(host, 'zones').split(','): 94 | hosts_per_zone[zone].append(host) 95 | 96 | if check: 97 | check_on_switch(mds, zoneset, pwwns, hosts_per_zone, vsan, fabric, switch) 98 | else: 99 | if switch: 100 | print bcolors.OKGREEN + "\nGenerating commands to switch %s ... \n" % switch + bcolors.ENDC 101 | else: 102 | print bcolors.OKGREEN + "\nGenerating commands to FABRIC %s ... \n" % fabric + bcolors.ENDC 103 | time.sleep(3) 104 | print "config t" 105 | print "device-alias database" 106 | for host in config.sections(): 107 | print " device-alias name %s pwwn %s" % (host.strip(), config.get(host, fabric)) 108 | print "device-alias commit\n" 109 | 110 | for zone, hosts in hosts_per_zone.iteritems(): 111 | if len(zone) > 1: 112 | print "zone name %s vsan %s" % (zone.strip(), vsan) 113 | for host in hosts: 114 | print " member device-alias %s initiator" % host.strip() 115 | print "exit\n" 116 | 117 | print "zoneset activate name %s vsan %s\n" % (zoneset, vsan) 118 | 119 | print "copy running-config startup-config\n" 120 | 121 | if __name__ == "__main__": 122 | 123 | arguments = argparse.ArgumentParser( 124 | description='Generate SmartZone commands from input config file listing of short hostnames, pwwns and zones which each host will belongs.') 125 | arguments.add_argument( 126 | '-c','--config_hosts', required=True, type=str, 127 | help='Configuration file with hosts, pwwns and zones') 128 | arguments.add_argument( 129 | '--vsan', required=True, type=str, 130 | help='VSAN ID') 131 | arguments.add_argument( 132 | '--zoneset', required=True, type=str, 133 | help='ZoneSet name') 134 | arguments.add_argument( 135 | '-f','--fabric', required=True, type=str, choices=['impar', 'par'], 136 | help='Fabric side') 137 | arguments.add_argument( 138 | '--check',default=False, action='store_true', 139 | help='[optional] Start a validation process by connection on MDS switch of all params') 140 | arguments.add_argument( 141 | '-s','--switch', required=False, type=str, 142 | help='MDS switch fqdn or IP') 143 | arguments.add_argument( 144 | '-u','--username', required=False, type=str, 145 | help='[optional] Username to ssh into mds switch. Alternate: set environment variable MDS_USERNAME. If neither exists, defaults to current OS username') 146 | arguments.add_argument( 147 | '-p','--password', required=False, type=str, 148 | help='[optional] Password to ssh into mds switch. Alternate: set environment variable MDS_PASSWORD. If unset use_keys defaults to True.') 149 | arguments.add_argument( 150 | '--use_keys', required=False, action='store_true', 151 | help='[optional] Use ssh keys to log into switch. If set key file will need be pass as param') 152 | arguments.add_argument( 153 | '--key_file', required=False, type=str, 154 | help='[optional] filename for ssh key file') 155 | 156 | args = arguments.parse_args() 157 | 158 | config_file = args.config_hosts 159 | 160 | if not os.path.exists(config_file): 161 | print bcolors.FAIL + "%s: No such file or directory!" % config_file + bcolors.ENDC 162 | exit(1) 163 | 164 | vsan = args.vsan 165 | zoneset = args.zoneset 166 | fabric = args.fabric 167 | switch = None 168 | 169 | if args.check: 170 | if args.password : 171 | use_keys = False 172 | password = args.password 173 | elif os.getenv('MDS_PASSWORD') : 174 | use_keys = False 175 | password = os.getenv('MDS_PASSWORD') 176 | else : 177 | use_keys = True 178 | password = '' 179 | 180 | if args.username : 181 | username = args.username 182 | elif os.getenv('MDS_USERNAME') : 183 | username = os.getenv('MDS_USERNAME') 184 | else: 185 | username = getpass.getuser() 186 | 187 | switch = args.switch 188 | 189 | # Params to connect on MDS 190 | mds = { 191 | 'device_type': 'cisco_nxos', 192 | 'ip': switch, 193 | 'verbose': False, 194 | 'username': username, 195 | 'password': password, 196 | 'use_keys': use_keys 197 | } 198 | generate_smartzones(config_file, zoneset, vsan, fabric, switch=switch, check=True, mds=mds) 199 | else: 200 | generate_smartzones(config_file, zoneset, vsan, fabric) 201 | 202 | -------------------------------------------------------------------------------- /smartzone/zone.conf: -------------------------------------------------------------------------------- 1 | [HOSTNAME] 2 | impar = 20:00:00:00:b0:00:a0:00 3 | par = 20:00:00:00:b0:00:b0:00 4 | zones = MDS_SmartZone_Name_Zone_1 5 | 6 | [HOSTNAME] 7 | impar = 10:00:00:00:b0:00:a0:00 8 | par = 10:00:00:00:b0:00:b0:00 9 | zones = MDS_SmartZone_Name_Zone_1, MDS_SmartZone_Name_Zone_2 --------------------------------------------------------------------------------