1# 2# Copyright (C) 2010-2017 Samuel Abels 3# The MIT License (MIT) 4# 5# Permission is hereby granted, free of charge, to any person obtaining 6# a copy of this software and associated documentation files 7# (the "Software"), to deal in the Software without restriction, 8# including without limitation the rights to use, copy, modify, merge, 9# publish, distribute, sublicense, and/or sell copies of the Software, 10# and to permit persons to whom the Software is furnished to do so, 11# subject to the following conditions: 12# 13# The above copyright notice and this permission notice shall be 14# included in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19# IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20# CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21# TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23""" 24Utilities for reading data from files. 25""" 26from __future__ import print_function, absolute_import 27from builtins import str 28from future import standard_library 29standard_library.install_aliases() 30import sys 31import re 32import os 33import base64 34import codecs 35import imp # Py2 36import importlib # Py3 37from .. import Account 38from .cast import to_host 39 40 41def get_accounts_from_file(filename): 42 """ 43 Reads a list of user/password combinations from the given file 44 and returns a list of Account instances. The file content 45 has the following format:: 46 47 [account-pool] 48 user1 = cGFzc3dvcmQ= 49 user2 = cGFzc3dvcmQ= 50 51 Note that "cGFzc3dvcmQ=" is a base64 encoded password. 52 If the input file contains extra config sections other than 53 "account-pool", they are ignored. 54 Each password needs to be base64 encrypted. To encrypt a password, 55 you may use the following command:: 56 57 python -c 'import base64; print(base64.b64encode("thepassword"))' 58 59 :type filename: string 60 :param filename: The name of the file containing the list of accounts. 61 :rtype: list[Account] 62 :return: The newly created account instances. 63 """ 64 accounts = [] 65 cfgparser = __import__('configparser', {}, {}, ['']) 66 parser = cfgparser.RawConfigParser() 67 parser.optionxform = str 68 parser.read(filename) 69 for user, password in parser.items('account-pool'): 70 password = base64.decodebytes(password.encode('latin1')) 71 accounts.append(Account(user, password.decode('latin1'))) 72 return accounts 73 74 75def get_hosts_from_file(filename, 76 default_protocol='telnet', 77 default_domain='', 78 remove_duplicates=False, 79 encoding='utf-8'): 80 """ 81 Reads a list of hostnames from the file with the given name. 82 83 :type filename: string 84 :param filename: A full filename. 85 :type default_protocol: str 86 :param default_protocol: Passed to the Host constructor. 87 :type default_domain: str 88 :param default_domain: Appended to each hostname that has no domain. 89 :type remove_duplicates: bool 90 :param remove_duplicates: Whether duplicates are removed. 91 :type encoding: str 92 :param encoding: The encoding of the file. 93 :rtype: list[Host] 94 :return: The newly created host instances. 95 """ 96 # Open the file. 97 if not os.path.exists(filename): 98 raise IOError('No such file: %s' % filename) 99 100 # Read the hostnames. 101 have = set() 102 hosts = [] 103 with codecs.open(filename, 'r', encoding) as file_handle: 104 for line in file_handle: 105 hostname = line.split('#')[0].strip() 106 if hostname == '': 107 continue 108 if remove_duplicates and hostname in have: 109 continue 110 have.add(hostname) 111 hosts.append(to_host(hostname, default_protocol, default_domain)) 112 113 return hosts 114 115 116def get_hosts_from_csv(filename, 117 default_protocol='telnet', 118 default_domain='', 119 encoding='utf-8'): 120 """ 121 Reads a list of hostnames and variables from the tab-separated .csv file 122 with the given name. The first line of the file must contain the column 123 names, e.g.:: 124 125 address testvar1 testvar2 126 10.0.0.1 value1 othervalue 127 10.0.0.1 value2 othervalue2 128 10.0.0.2 foo bar 129 130 For the above example, the function returns *two* host objects, where 131 the 'testvar1' variable of the first host holds a list containing two 132 entries ('value1' and 'value2'), and the 'testvar1' variable of the 133 second host contains a list with a single entry ('foo'). 134 135 Both, the address and the hostname of each host are set to the address 136 given in the first column. If you want the hostname set to another value, 137 you may add a second column containing the hostname:: 138 139 address hostname testvar 140 10.0.0.1 myhost value 141 10.0.0.2 otherhost othervalue 142 143 :type filename: string 144 :param filename: A full filename. 145 :type default_protocol: str 146 :param default_protocol: Passed to the Host constructor. 147 :type default_domain: str 148 :param default_domain: Appended to each hostname that has no domain. 149 :type encoding: str 150 :param encoding: The encoding of the file. 151 :rtype: list[Host] 152 :return: The newly created host instances. 153 """ 154 # Open the file. 155 if not os.path.exists(filename): 156 raise IOError('No such file: %s' % filename) 157 158 with codecs.open(filename, 'r', encoding) as file_handle: 159 # Read and check the header. 160 header = file_handle.readline().rstrip() 161 if re.search(r'^(?:hostname|address)\b', header) is None: 162 msg = 'Syntax error in CSV file header:' 163 msg += ' File does not start with "hostname" or "address".' 164 raise Exception(msg) 165 if re.search(r'^(?:hostname|address)(?:\t[^\t]+)*$', header) is None: 166 msg = 'Syntax error in CSV file header:' 167 msg += ' Make sure to separate columns by tabs.' 168 raise Exception(msg) 169 varnames = [str(v) for v in header.split('\t')] 170 varnames.pop(0) 171 172 # Walk through all lines and create a map that maps hostname to 173 # definitions. 174 last_uri = '' 175 line_re = re.compile(r'[\r\n]*$') 176 hosts = [] 177 for line in file_handle: 178 if line.strip() == '': 179 continue 180 181 line = line_re.sub('', line) 182 values = line.split('\t') 183 uri = values.pop(0).strip() 184 185 # Add the hostname to our list. 186 if uri != last_uri: 187 # print "Reading hostname", hostname_url, "from csv." 188 host = to_host(uri, default_protocol, default_domain) 189 last_uri = uri 190 hosts.append(host) 191 192 # Define variables according to the definition. 193 for i, varname in enumerate(varnames): 194 try: 195 value = values[i] 196 except IndexError: 197 value = '' 198 if varname == 'hostname': 199 host.set_name(value) 200 else: 201 host.append(varname, value) 202 203 return hosts 204 205 206def load_lib(filename): 207 """ 208 Loads a Python file containing functions, and returns the 209 content of the __lib__ variable. The __lib__ variable must contain 210 a dictionary mapping function names to callables. 211 212 Returns a dictionary mapping the namespaced function names to 213 callables. The namespace is the basename of the file, without file 214 extension. 215 216 The result of this function can later be passed to run_template:: 217 218 functions = load_lib('my_library.py') 219 run_template(conn, 'foo.exscript', **functions) 220 221 :type filename: string 222 :param filename: A full filename. 223 :rtype: dict[string->object] 224 :return: The loaded functions. 225 """ 226 # Open the file. 227 if not os.path.exists(filename): 228 raise IOError('No such file: %s' % filename) 229 230 name = os.path.splitext(os.path.basename(filename))[0] 231 if sys.version_info[0] < 3: 232 module = imp.load_source(name, filename) 233 else: 234 module = importlib.machinery.SourceFileLoader(name, filename).load_module() 235 236 return dict((name + '.' + k, v) for (k, v) in list(module.__lib__.items())) 237