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