source: anuga_core/source/anuga/utilities/system_tools.py @ 7276

Last change on this file since 7276 was 7276, checked in by ole, 15 years ago

Merged numpy branch back into the trunk.

In ~/sandpit/anuga/anuga_core/source
svn merge -r 6246:HEAD ../../branches/numpy .

In ~/sandpit/anuga/anuga_validation
svn merge -r 6417:HEAD ../branches/numpy_anuga_validation .

In ~/sandpit/anuga/misc
svn merge -r 6809:HEAD ../branches/numpy_misc .

For all merges, I used numpy version where conflicts existed

The suites test_all.py (in source/anuga) and validate_all.py passed using Python2.5 with numpy on my Ubuntu Linux box.

File size: 19.1 KB
Line 
1"""Implementation of tools to do with system administration made as platform independent as possible.
2
3
4"""
5
6import sys
7import os
8import string
9import urllib
10import urllib2
11import getpass
12import tarfile
13try:
14    import hashlib
15except ImportError:
16    import md5 as hashlib
17
18
19def log_to_file(filename, s, verbose=False):
20    """Log string to file name
21    """
22
23    fid = open(filename, 'a')
24    if verbose: print s
25    fid.write(s + '\n')
26    fid.close()
27
28
29def get_user_name():
30    """Get user name provide by operating system
31    """
32
33    if sys.platform == 'win32':
34        #user = os.getenv('USERPROFILE')
35        user = os.getenv('USERNAME')
36    else:
37        user = os.getenv('LOGNAME')
38
39
40    return user   
41
42def get_host_name():
43    """Get host name provide by operating system
44    """
45
46    if sys.platform == 'win32':
47        host = os.getenv('COMPUTERNAME')
48    else:
49        host = os.uname()[1]
50
51
52    return host   
53
54def get_revision_number():
55    """Get the version number of this repository copy.
56
57    Try getting data from stored_version_info.py first, otherwise
58    try using SubWCRev.exe (Windows) or svnversion (linux), otherwise
59    try reading file .svn/entries for version information, otherwise
60    throw an exception.
61
62    NOTE: This requires that the command svn is on the system PATH
63    (simply aliasing svn to the binary will not work)
64    """
65
66    def get_revision_from_svn_entries():
67        '''Get a subversion revision number from the .svn/entires file.'''
68
69        msg = '''
70No version info stored and command 'svn' is not recognised on the system PATH.
71
72If ANUGA has been installed from a distribution e.g. as obtained from SourceForge,
73the version info should be available in the automatically generated file
74'stored_version_info.py' in the anuga root directory.
75
76If run from a Subversion sandpit, ANUGA will try to obtain the version info by
77using the command 'svn info'.  In this case, make sure the command line client
78'svn' is accessible on the system path.  Simply aliasing 'svn' to the binary will
79not work.
80
81If you are using Windows, you have to install the file svn.exe which can be
82obtained from http://www.collab.net/downloads/subversion.
83
84Good luck!
85'''
86
87        try:
88            fd = open(os.path.join('.svn', 'entries'))
89        except:
90            raise Exception, msg
91
92        line = fd.readlines()[3]
93        fd.close()
94        try:
95            revision_number = int(line)
96        except:
97            msg = ".svn/entries, line 4 was '%s'?" % line.strip()
98            raise Exception, msg
99
100        return revision_number
101
102    def get_revision_from_svn_client():
103        '''Get a subversion revision number from an svn client.'''
104
105        if sys.platform[0:3] == 'win':
106            try:
107                fid = os.popen(r'C:\Program Files\TortoiseSVN\bin\SubWCRev.exe')
108            except:
109                return get_revision_from_svn_entries()
110            else:
111                version_info = fid.read()
112                if version_info == '':
113                    return get_revision_from_svn_entries()
114
115            # split revision number from data
116            for line in version_info.split('\n'):
117                if line.startswith('Updated to revision '):
118                    break
119
120            fields = line.split(' ')
121            msg = 'Keyword "Revision" was not found anywhere in text: %s' % version_info
122            assert fields[0].startswith('Updated'), msg
123
124            try:
125                revision_number = int(fields[3])
126            except:
127                msg = ("Revision number must be an integer. I got '%s' from "
128                       "'SubWCRev.exe'." % fields[3])
129                raise Exception, msg
130        else:                   # assume Linux
131            try:
132                fid = os.popen('svnversion -n . 2>/dev/null')
133            except:
134                return get_revision_from_svn_entries()
135            else:
136                version_info = fid.read()
137                if version_info == '':
138                    return get_revision_from_svn_entries()
139
140            # split revision number from data
141            if ':' in version_info:
142                (_, revision_number) =  version_info.split(':')
143            elif version_info.endswith('M'):
144                revision_number = version_info[:-1]
145            else:
146                revision_number = version_info
147
148            try:
149                revision_number = int(revision_number)
150            except:
151                msg = ("Revision number must be an integer. I got '%s' from "
152                       "'svn'." % version_info)
153                raise Exception, msg
154
155        return revision_number
156
157    # try to get revision information from stored_version_info.py
158    try:
159        from anuga.stored_version_info import version_info
160    except:
161        return get_revision_from_svn_client()
162
163    # split revision number from data
164    for line in version_info.split('\n'):
165        if line.startswith('Revision:'):
166            break
167
168    fields = line.split(':')
169    msg = 'Keyword "Revision" was not found anywhere in text: %s' % version_info
170    assert fields[0].startswith('Revision'), msg
171
172    try:
173        revision_number = int(fields[1])
174    except:
175        msg = ("Revision number must be an integer. I got '%s'.\n"
176               'Check that the command svn is on the system path.'
177               % fields[1])
178        raise Exception, msg
179
180    return revision_number
181
182
183def store_version_info(destination_path='.', verbose=False):
184    """Obtain current version from Subversion and store it.
185   
186    Title: store_version_info()
187
188    Author: Ole Nielsen (Ole.Nielsen@ga.gov.au)
189
190    CreationDate: January 2006
191
192    Description:
193        This function obtains current version from Subversion and stores it
194        is a Python file named 'stored_version_info.py' for use with
195        get_version_info()
196
197        If svn is not available on the system PATH, an Exception is thrown
198    """
199
200    # Note (Ole): This function should not be unit tested as it will only
201    # work when running out of the sandpit. End users downloading the
202    # ANUGA distribution would see a failure.
203    #
204    # FIXME: This function should really only be used by developers (
205    # (e.g. for creating new ANUGA releases), so maybe it should move
206    # to somewhere else.
207   
208    import config
209
210    try:
211        fid = os.popen('svn info')
212    except:
213        msg = 'Command "svn" is not recognised on the system PATH'
214        raise Exception(msg)
215    else:   
216        txt = fid.read()
217        fid.close()
218
219
220        # Determine absolute filename
221        if destination_path[-1] != os.sep:
222            destination_path += os.sep
223           
224        filename = destination_path + config.version_filename
225
226        fid = open(filename, 'w')
227
228        docstring = 'Stored version info.\n\n'
229        docstring += 'This file provides the version for distributions '
230        docstring += 'that are not accessing Subversion directly.\n'
231        docstring += 'The file is automatically generated and should not '
232        docstring += 'be modified manually.\n'
233        fid.write('"""%s"""\n\n' %docstring)
234       
235        fid.write('version_info = """\n%s"""' %txt)
236        fid.close()
237
238
239        if verbose is True:
240            print 'Version info stored to %s' %filename
241
242
243def safe_crc(string):
244    """64 bit safe crc computation.
245
246    See http://docs.python.org/library/zlib.html#zlib.crc32:
247
248        To generate the same numeric value across all Python versions
249        and platforms use crc32(data) & 0xffffffff.
250    """
251
252    from zlib import crc32
253
254    return crc32(string) & 0xffffffff
255
256
257def compute_checksum(filename, max_length=2**20):
258    """Compute the CRC32 checksum for specified file
259
260    Optional parameter max_length sets the maximum number
261    of bytes used to limit time used with large files.
262    Default = 2**20 (1MB)
263    """
264
265    fid = open(filename, 'rb') # Use binary for portability
266    crcval = safe_crc(fid.read(max_length))
267    fid.close()
268
269    return crcval
270
271def get_pathname_from_package(package):
272    """Get pathname of given package (provided as string)
273
274    This is useful for reading files residing in the same directory as
275    a particular module. Typically, this is required in unit tests depending
276    on external files.
277
278    The given module must start from a directory on the pythonpath
279    and be importable using the import statement.
280
281    Example
282    path = get_pathname_from_package('anuga.utilities')
283
284    """
285
286    exec('import %s as x' %package)
287
288    path = x.__path__[0]
289   
290    return path
291
292    # Alternative approach that has been used at times
293    #try:
294    #    # When unit test is run from current dir
295    #    p1 = read_polygon('mainland_only.csv')
296    #except:
297    #    # When unit test is run from ANUGA root dir
298    #    from os.path import join, split
299    #    dir, tail = split(__file__)
300    #    path = join(dir, 'mainland_only.csv')
301    #    p1 = read_polygon(path)
302       
303   
304##
305# @brief Split a string into 'clean' fields.
306# @param str The string to process.
307# @param delimiter The delimiter string to split 'line' with.
308# @return A list of 'cleaned' field strings.
309# @note Any fields that were initially zero length will be removed.
310# @note If a field contains '\n' it isn't zero length.
311def clean_line(str, delimiter):
312    """Split string on given delimiter, remove whitespace from each field."""
313
314    return [x.strip() for x in str.strip().split(delimiter) if x != '']
315
316
317################################################################################
318# The following two functions are used to get around a problem with numpy and
319# NetCDF files.  Previously, using Numeric, we could take a list of strings and
320# convert to a Numeric array resulting in this:
321#     Numeric.array(['abc', 'xy']) -> [['a', 'b', 'c'],
322#                                      ['x', 'y', ' ']]
323#
324# However, under numpy we get:
325#     numpy.array(['abc', 'xy']) -> ['abc',
326#                                    'xy']
327#
328# And writing *strings* to a NetCDF file is problematic.
329#
330# The solution is to use these two routines to convert a 1-D list of strings
331# to the 2-D list of chars form and back.  The 2-D form can be written to a
332# NetCDF file as before.
333#
334# The other option, of inverting a list of tag strings into a dictionary with
335# keys being the unique tag strings and the key value a list of indices of where
336# the tag string was in the original list was rejected because:
337#    1. It's a lot of work
338#    2. We'd have to rewite the I/O code a bit (extra variables instead of one)
339#    3. The code below is fast enough in an I/O scenario
340################################################################################
341
342##
343# @brief Convert 1-D list of strings to 2-D list of chars.
344# @param l 1-dimensional list of strings.
345# @return A 2-D list of 'characters' (1 char strings).
346# @note No checking that we supply a 1-D list.
347def string_to_char(l):
348    '''Convert 1-D list of strings to 2-D list of chars.'''
349
350    if not l:
351        return []
352
353    if l == ['']:
354        l = [' ']
355
356    maxlen = reduce(max, map(len, l))
357    ll = [x.ljust(maxlen) for x in l]
358    result = []
359    for s in ll:
360        result.append([x for x in s])
361    return result
362
363
364##
365# @brief Convert 2-D list of chars to 1-D list of strings.
366# @param ll 2-dimensional list of 'characters' (1 char strings).
367# @return A 1-dimensional list of strings.
368# @note Each string has had right-end spaces removed.
369def char_to_string(ll):
370    '''Convert 2-D list of chars to 1-D list of strings.'''
371
372    return map(string.rstrip, [''.join(x) for x in ll])
373
374################################################################################
375
376##
377# @brief Get list of variable names in a python expression string.
378# @param source A string containing a python expression.
379# @return A list of variable name strings.
380# @note Throws SyntaxError exception if not a valid expression.
381def get_vars_in_expression(source):
382    '''Get list of variable names in a python expression.'''
383
384    import compiler
385    from compiler.ast import Node
386
387    ##
388    # @brief Internal recursive function.
389    # @param node An AST parse Node.
390    # @param var_list Input list of variables.
391    # @return An updated list of variables.
392    def get_vars_body(node, var_list=[]):
393        if isinstance(node, Node):
394            if node.__class__.__name__ == 'Name':
395                for child in node.getChildren():
396                    if child not in var_list:
397                        var_list.append(child)
398            for child in node.getChildren():
399                if isinstance(child, Node):
400                    for child in node.getChildren():
401                        var_list = get_vars_body(child, var_list)
402                    break
403
404        return var_list
405
406    return get_vars_body(compiler.parse(source))
407
408
409##
410# @brief Get a file from the web.
411# @param file_url URL of the file to fetch.
412# @param file_name Path to file to create in the filesystem.
413# @param auth Auth tuple (httpproxy, proxyuser, proxypass).
414# @param blocksize Read file in this block size.
415# @return (True, auth) if successful, else (False, auth).
416# @note If 'auth' not supplied, will prompt user.
417# @note Will try using environment variable HTTP_PROXY for proxy server.
418# @note Will try using environment variable PROXY_USERNAME for proxy username.
419# @note Will try using environment variable PROXY_PASSWORD for proxy password.
420def get_web_file(file_url, file_name, auth=None, blocksize=1024*1024):
421    '''Get a file from the web (HTTP).
422
423    file_url:  The URL of the file to get
424    file_name: Local path to save loaded file in
425    auth:      A tuple (httpproxy, proxyuser, proxypass)
426    blocksize: Block size of file reads
427   
428    Will try simple load through urllib first.  Drop down to urllib2
429    if there is a proxy and it requires authentication.
430
431    Environment variable HTTP_PROXY can be used to supply proxy information.
432    PROXY_USERNAME is used to supply the authentication username.
433    PROXY_PASSWORD supplies the password, if you dare!
434    '''
435
436    # Simple fetch, if fails, check for proxy error
437    try:
438        urllib.urlretrieve(file_url, file_name)
439        return (True, auth)     # no proxy, no auth required
440    except IOError, e:
441        if e[1] == 407:     # proxy error
442            pass
443        elif e[1][0] == 113:  # no route to host
444            print 'No route to host for %s' % file_url
445            return (False, auth)    # return False
446        else:
447            print 'Unknown connection error to %s' % file_url
448            return (False, auth)
449
450    # We get here if there was a proxy error, get file through the proxy
451    # unpack auth info
452    try:
453        (httpproxy, proxyuser, proxypass) = auth
454    except:
455        (httpproxy, proxyuser, proxypass) = (None, None, None)
456
457    # fill in any gaps from the environment
458    if httpproxy is None:
459        httpproxy = os.getenv('HTTP_PROXY')
460    if proxyuser is None:
461        proxyuser = os.getenv('PROXY_USERNAME')
462    if proxypass is None:
463        proxypass = os.getenv('PROXY_PASSWORD')
464
465    # Get auth info from user if still not supplied
466    if httpproxy is None or proxyuser is None or proxypass is None:
467        print '-'*72
468        print ('You need to supply proxy authentication information.')
469        if httpproxy is None:
470            httpproxy = raw_input('                    proxy server: ')
471        else:
472            print '         HTTP proxy was supplied: %s' % httpproxy
473        if proxyuser is None:
474            proxyuser = raw_input('                  proxy username: ') 
475        else:
476            print 'HTTP proxy username was supplied: %s' % proxyuser
477        if proxypass is None:
478            proxypass = getpass.getpass('                  proxy password: ')
479        else:
480            print 'HTTP proxy password was supplied: %s' % '*'*len(proxyuser)
481        print '-'*72
482
483    # the proxy URL cannot start with 'http://', we add that later
484    httpproxy = httpproxy.lower()
485    if httpproxy.startswith('http://'):
486        httpproxy = httpproxy.replace('http://', '', 1)
487
488    # open remote file
489    proxy = urllib2.ProxyHandler({'http': 'http://' + proxyuser
490                                              + ':' + proxypass
491                                              + '@' + httpproxy})
492    authinfo = urllib2.HTTPBasicAuthHandler()
493    opener = urllib2.build_opener(proxy, authinfo, urllib2.HTTPHandler)
494    urllib2.install_opener(opener)
495    try:
496        webget = urllib2.urlopen(file_url)
497    except urllib2.HTTPError, e:
498        print 'Error received from proxy:\n%s' % str(e)
499        print 'Possibly the user/password is wrong.'
500        return (False, (httpproxy, proxyuser, proxypass))
501
502    # transfer file to local filesystem
503    fd = open(file_name, 'wb')
504    while True:
505        data = webget.read(blocksize)
506        if len(data) == 0:
507            break
508        fd.write(data)
509    fd.close
510    webget.close()
511
512    # return successful auth info
513    return (True, (httpproxy, proxyuser, proxypass))
514
515
516##
517# @brief Tar a file (or directory) into a tarfile.
518# @param files A list of files (or directories) to tar.
519# @param tarfile The created tarfile name.
520# @note 'files' may be a string (single file) or a list of strings.
521# @note We use gzip compression.
522def tar_file(files, tarname):
523    '''Compress a file or directory into a tar file.'''
524
525    if isinstance(files, basestring):
526        files = [files]
527
528    o = tarfile.open(tarname, 'w:gz')
529    for file in files:
530        o.add(file)
531    o.close()
532
533
534##
535# @brief Untar a file into an optional target directory.
536# @param tarname Name of the file to untar.
537# @param target_dir Directory to untar into.
538def untar_file(tarname, target_dir='.'):
539    '''Uncompress a tar file.'''
540
541    o = tarfile.open(tarname, 'r:gz')
542    members = o.getmembers()
543    for member in members:
544        o.extract(member, target_dir)
545    o.close()
546
547
548##
549# @brief Return a hex digest (MD5) of a given file.
550# @param filename Path to the file of interest.
551# @param blocksize Size of data blocks to read.
552# @return A hex digest string (16 bytes).
553# @note Uses MD5 digest if hashlib not available.
554def get_file_hexdigest(filename, blocksize=1024*1024*10):
555    '''Get a hex digest of a file.'''
556
557    if hashlib.__name__ == 'hashlib':
558        m = hashlib.md5()       # new - 'hashlib' module
559    else:
560        m = hashlib.new()       # old - 'md5' module - remove once py2.4 gone
561    fd = open(filename, 'r')
562           
563    while True:
564        data = fd.read(blocksize)
565        if len(data) == 0:
566            break
567        m.update(data)
568                                                               
569    fd.close()
570    return m.hexdigest()
571
572
573##
574# @brief Create a file containing a hexdigest string of a data file.
575# @param data_file Path to the file to get the hexdigest from.
576# @param digest_file Path to hexdigest file to create.
577# @note Uses MD5 digest.
578def make_digest_file(data_file, digest_file):
579    '''Create a file containing the hex digest string of a data file.'''
580   
581    hexdigest = get_file_hexdigest(data_file)
582    fd = open(digest_file, 'w')
583    fd.write(hexdigest)
584    fd.close()
585
586
587##
588# @brief Function to return the length of a file.
589# @param in_file Path to file to get length of.
590# @return Number of lines in file.
591# @note Doesn't count '\n' characters.
592# @note Zero byte file, returns 0.
593# @note No \n in file at all, but >0 chars, returns 1.
594def file_length(in_file):
595    '''Function to return the length of a file.'''
596
597    fid = open(in_file)
598    data = fid.readlines()
599    fid.close()
600    return len(data)
601
602
Note: See TracBrowser for help on using the repository browser.