S3/Config.py
ec50b5a7
 ## Amazon S3 manager
 ## Author: Michal Ludvig <michal@logix.cz>
 ##         http://www.logix.cz/michal
 ## License: GPL Version 2
afd51b6c
 ## Copyright: TGRMN Software and contributors
ec50b5a7
 
b6e1cada
 import logging
f06cf98f
 from logging import debug, info, warning, error
b6e1cada
 import re
a495865c
 import os
dc590d62
 import sys
2d7ceec9
 import Progress
fa664913
 from SortedDict import SortedDict
dc590d62
 import httplib
a122d976
 try:
     import json
 except ImportError, e:
     pass
b6e1cada
 
b008e471
 class Config(object):
d439efb4
     _instance = None
     _parsed_files = []
     _doc = {}
     access_key = ""
     secret_key = ""
dc590d62
     access_token = ""
d439efb4
     host_base = "s3.amazonaws.com"
     host_bucket = "%(bucket)s.s3.amazonaws.com"
     simpledb_host = "sdb.amazonaws.com"
     cloudfront_host = "cloudfront.amazonaws.com"
     verbosity = logging.WARNING
     progress_meter = True
     progress_class = Progress.ProgressCR
     send_chunk = 4096
     recv_chunk = 4096
     list_md5 = False
     human_readable_sizes = False
     extra_headers = SortedDict(ignore_case = True)
     force = False
754f575d
     server_side_encryption = False
d439efb4
     enable = None
     get_continue = False
dc071cc1
     put_continue = False
     upload_id = None
d439efb4
     skip_existing = False
     recursive = False
40deabb4
     restore_days = 1
d439efb4
     acl_public = None
     acl_grants = []
     acl_revokes = []
     proxy_host = ""
     proxy_port = 3128
     encrypt = False
     dry_run = False
833f07bb
     add_encoding_exts = ""
d439efb4
     preserve_attrs = True
     preserve_attrs_list = [
         'uname',    # Verbose owner Name (e.g. 'root')
         'uid',      # Numeric user ID (e.g. 0)
         'gname',    # Group name (e.g. 'users')
         'gid',      # Numeric group ID (e.g. 100)
         'atime',    # Last access timestamp
         'mtime',    # Modification timestamp
         'ctime',    # Creation timestamp
         'mode',     # File mode (e.g. rwxr-xr-x = 755)
1703df70
         'md5',      # File MD5 (if known)
d439efb4
         #'acl',     # Full ACL (not yet supported)
     ]
     delete_removed = False
255d96b8
     delete_after = False
552df705
     delete_after_fetch = False
f230f799
     max_delete = -1
d439efb4
     _doc['delete_removed'] = "[sync] Remove remote S3 objects when local file has been deleted"
c3deb6a8
     delay_updates = False
d439efb4
     gpg_passphrase = ""
     gpg_command = ""
     gpg_encrypt = "%(gpg_command)s -c --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
     gpg_decrypt = "%(gpg_command)s -d --verbose --no-use-agent --batch --yes --passphrase-fd %(passphrase_fd)s -o %(output_file)s %(input_file)s"
     use_https = False
     bucket_location = "US"
     default_mime_type = "binary/octet-stream"
0d477b9c
     guess_mime_type = True
b4207d9c
     use_mime_magic = True
35612e61
     mime_type = ""
880e0dec
     enable_multipart = True
80310166
     multipart_chunk_size_mb = 15    # MB
d439efb4
     # List of checks to be performed for 'sync'
     sync_checks = ['size', 'md5']   # 'weak-timestamp'
     # List of compiled REGEXPs
     exclude = []
     include = []
     # Dict mapping compiled REGEXPs back to their textual form
     debug_exclude = {}
     debug_include = {}
     encoding = "utf-8"
     urlencoding_mode = "normal"
     log_target_prefix = ""
     reduced_redundancy = False
     follow_symlinks = False
     socket_timeout = 300
     invalidate_on_cf = False
c0a81434
     # joseprio: new flags for default index invalidation
     invalidate_default_index_on_cf = False
     invalidate_default_index_root_on_cf = True
d439efb4
     website_index = "index.html"
     website_error = ""
     website_endpoint = "http://%(bucket)s.s3-website-%(location)s.amazonaws.com/"
07c9e2de
     additional_destinations = []
3ce5e989
     files_from = []
488c9565
     cache_file = ""
50a42333
     add_headers = ""
91464838
     ignore_failed_copy = False
d439efb4
 
     ## Creating a singleton
     def __new__(self, configfile = None):
         if self._instance is None:
             self._instance = object.__new__(self)
         return self._instance
 
     def __init__(self, configfile = None):
         if configfile:
dc590d62
             try:
                 self.read_config_file(configfile)
             except IOError, e:
                 if 'AWS_CREDENTIAL_FILE' in os.environ:
                     self.env_config()
             if len(self.access_key)==0:
98692c12
                 self.role_config()
dc590d62
 
     def role_config(self):
a122d976
         if sys.version_info[0] * 10 + sys.version_info[1] < 26:
             error("IAM authentication requires Python 2.6 or newer")
             raise
         if not 'json' in sys.modules:
             error("IAM authentication not available -- missing module json")
             raise
dc590d62
         try:
a122d976
             conn = httplib.HTTPConnection(host='169.254.169.254', timeout = 2)
dc590d62
             conn.request('GET', "/latest/meta-data/iam/security-credentials/")
             resp = conn.getresponse()
             files = resp.read()
98692c12
             if resp.status == 200 and len(files)>1:
dc590d62
                 conn.request('GET', "/latest/meta-data/iam/security-credentials/%s"%files)
                 resp=conn.getresponse()
                 if resp.status == 200:
                     creds=json.load(resp)
                     Config().update_option('access_key', creds['AccessKeyId'].encode('ascii'))
                     Config().update_option('secret_key', creds['SecretAccessKey'].encode('ascii'))
                     Config().update_option('access_token', creds['Token'].encode('ascii'))
                 else:
                     raise IOError
             else:
                 raise IOError
         except:
             raise
 
     def role_refresh(self):
         try:
             self.role_config()
         except:
             warning("Could not refresh role")
 
     def env_config(self):
         cred_content = ""
         try:
             cred_file = open(os.environ['AWS_CREDENTIAL_FILE'],'r')
             cred_content = cred_file.read()
         except IOError, e:
             debug("Error %d accessing credentials file %s" % (e.errno,os.environ['AWS_CREDENTIAL_FILE']))
         r_data = re.compile("^\s*(?P<orig_key>\w+)\s*=\s*(?P<value>.*)")
         r_quotes = re.compile("^\"(.*)\"\s*$")
         if len(cred_content)>0:
             for line in cred_content.splitlines():
                 is_data = r_data.match(line)
                 is_data = r_data.match(line)
                 if is_data:
                     data = is_data.groupdict()
                     if r_quotes.match(data["value"]):
                         data["value"] = data["value"][1:-1]
                     if data["orig_key"]=="AWSAccessKeyId":
                         data["key"] = "access_key"
                     elif data["orig_key"]=="AWSSecretKey":
                         data["key"] = "secret_key"
                     else:
98692c12
                         del data["key"]
dc590d62
                     if "key" in data:
                         Config().update_option(data["key"], data["value"])
                         if data["key"] in ("access_key", "secret_key", "gpg_passphrase"):
2c41f7e1
                             print_value = ("%s...%d_chars...%s") % (data["value"][:2], len(data["value"]) - 3, data["value"][-1:])
dc590d62
                         else:
                             print_value = data["value"]
                         debug("env_Config: %s->%s" % (data["key"], print_value))
d439efb4
 
     def option_list(self):
         retval = []
         for option in dir(self):
             ## Skip attributes that start with underscore or are not string, int or bool
             option_type = type(getattr(Config, option))
             if option.startswith("_") or \
                not (option_type in (
                     type("string"), # str
                         type(42),   # int
                     type(True))):   # bool
                 continue
             retval.append(option)
         return retval
 
     def read_config_file(self, configfile):
         cp = ConfigParser(configfile)
         for option in self.option_list():
             self.update_option(option, cp.get(option))
50a42333
 
         if cp.get('add_headers'):
             for option in cp.get('add_headers').split(","):
                 (key, value) = option.split(':')
                 self.extra_headers[key.replace('_', '-').strip()] = value.strip()
 
d439efb4
         self._parsed_files.append(configfile)
 
     def dump_config(self, stream):
         ConfigDumper(stream).dump("default", self)
 
     def update_option(self, option, value):
         if value is None:
             return
7d1ea1d7
 
a495865c
         #### Handle environment reference
         if str(value).startswith("$"):
             return self.update_option(option, os.getenv(str(value)[1:]))
7d1ea1d7
 
d439efb4
         #### Special treatment of some options
         ## verbosity must be known to "logging" module
         if option == "verbosity":
7d1ea1d7
             # support integer verboisities
d439efb4
             try:
7d1ea1d7
                 value = int(value)
             except ValueError, e:
                 try:
                     # otherwise it must be a key known to the logging module
                     value = logging._levelNames[value]
                 except KeyError:
                     error("Config: verbosity level '%s' is not valid" % value)
                     return
 
d439efb4
         ## allow yes/no, true/false, on/off and 1/0 for boolean options
         elif type(getattr(Config, option)) is type(True):   # bool
             if str(value).lower() in ("true", "yes", "on", "1"):
7d1ea1d7
                 value = True
d439efb4
             elif str(value).lower() in ("false", "no", "off", "0"):
7d1ea1d7
                 value = False
d439efb4
             else:
                 error("Config: value of option '%s' must be Yes or No, not '%s'" % (option, value))
7d1ea1d7
                 return
 
d439efb4
         elif type(getattr(Config, option)) is type(42):     # int
             try:
7d1ea1d7
                 value = int(value)
d439efb4
             except ValueError, e:
                 error("Config: value of option '%s' must be an integer, not '%s'" % (option, value))
7d1ea1d7
                 return
 
         setattr(Config, option, value)
b008e471
 
5a736f08
 class ConfigParser(object):
d439efb4
     def __init__(self, file, sections = []):
         self.cfg = {}
         self.parse_file(file, sections)
 
     def parse_file(self, file, sections = []):
         debug("ConfigParser: Reading file '%s'" % file)
         if type(sections) != type([]):
             sections = [sections]
         in_our_section = True
         f = open(file, "r")
         r_comment = re.compile("^\s*#.*")
         r_empty = re.compile("^\s*$")
         r_section = re.compile("^\[([^\]]+)\]")
         r_data = re.compile("^\s*(?P<key>\w+)\s*=\s*(?P<value>.*)")
         r_quotes = re.compile("^\"(.*)\"\s*$")
         for line in f:
             if r_comment.match(line) or r_empty.match(line):
                 continue
             is_section = r_section.match(line)
             if is_section:
                 section = is_section.groups()[0]
                 in_our_section = (section in sections) or (len(sections) == 0)
                 continue
             is_data = r_data.match(line)
             if is_data and in_our_section:
                 data = is_data.groupdict()
                 if r_quotes.match(data["value"]):
                     data["value"] = data["value"][1:-1]
                 self.__setitem__(data["key"], data["value"])
                 if data["key"] in ("access_key", "secret_key", "gpg_passphrase"):
2c41f7e1
                     print_value = ("%s...%d_chars...%s") % (data["value"][:2], len(data["value"]) - 3, data["value"][-1:])
d439efb4
                 else:
                     print_value = data["value"]
                 debug("ConfigParser: %s->%s" % (data["key"], print_value))
                 continue
             warning("Ignoring invalid line in '%s': %s" % (file, line))
 
     def __getitem__(self, name):
         return self.cfg[name]
 
     def __setitem__(self, name, value):
         self.cfg[name] = value
 
     def get(self, name, default = None):
         if self.cfg.has_key(name):
             return self.cfg[name]
         return default
5a736f08
 
 class ConfigDumper(object):
d439efb4
     def __init__(self, stream):
         self.stream = stream
5a736f08
 
d439efb4
     def dump(self, section, config):
         self.stream.write("[%s]\n" % section)
         for option in config.option_list():
7d1ea1d7
             value = getattr(config, option)
             if option == "verbosity":
                 # we turn level numbers back into strings if possible
                 if isinstance(value,int) and value in logging._levelNames:
                     value = logging._levelNames[value]
 
             self.stream.write("%s = %s\n" % (option, value))
5a736f08
 
344cadc8
 # vim:et:ts=4:sts=4:ai