get_url 7.65 KB
Newer Older
Jan-Piet Mens committed
1
#!/usr/bin/python
2
# -*- coding: utf-8 -*-
Jan-Piet Mens committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

# (c) 2012, Jan-Piet Mens <jpmens () gmail.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
#
21
# see examples/playbooks/get_url.yml
Jan-Piet Mens committed
22 23 24 25 26

import shutil
import datetime
import tempfile

27 28 29 30
DOCUMENTATION = '''
---
module: get_url
short_description: Downloads files from HTTP, HTTPS, or FTP to node
31 32 33
description:
     - Downloads files from HTTP, HTTPS, or FTP to the remote server. The remote
       server must have direct access to the remote resource.
34 35
version_added: "0.6"
options:
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
  url:
    description:
      - HTTP, HTTPS, or FTP URL
    required: true
    default: null
    aliases: []
  dest:
    description:
      - absolute path of where to download the file to.
      - If I(dest) is a directory, the basename of the file on the remote server will be used. If a directory, I(thirsty=yes) must also be set.
    required: true
    default: null
  thirsty:
    description:
      - if C(yes), will download the file every time and replace the
        file if the contents change. if C(no), the file will only be downloaded if
        the destination does not exist. Generally should be C(yes) only for small
        local files. prior to 0.6, acts if C(yes) by default.
    version_added: "0.7"
    required: false
    choices: [ "yes", "no" ]
    default: "no"
  others:
    description:
      - all arguments accepted by the M(file) module also work here
    required: false
62
examples:
63
   - code: "get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf mode=0440"
64
     description: "Example from Ansible Playbooks"
65
notes:
66
    - This module doesn't yet support configuration for proxies or passwords.
67 68
# informational: requirements for nodes
requirements: [ urllib2, urlparse ]
69
author: Jan-Piet Mens
70 71
'''

Jan-Piet Mens committed
72 73 74
HAS_URLLIB2=True
try:
    import urllib2
75
except ImportError:
Jan-Piet Mens committed
76 77
    HAS_URLLIB2=False
HAS_URLPARSE=True
78

Jan-Piet Mens committed
79 80 81
try:
    import urlparse
    import socket
82
except ImportError:
Jan-Piet Mens committed
83 84 85 86 87 88
    HAS_URLPARSE=False

# ==============================================================
# url handling

def url_filename(url):
89 90 91 92
    fn = os.path.basename(urlparse.urlsplit(url)[2])
    if fn == '':
        return 'index.html'
    return fn
Jan-Piet Mens committed
93

94 95 96 97
def url_do_get(module, url, dest):
    """
    Get url and return request and info
    Credits: http://stackoverflow.com/questions/7006574/how-to-download-file-from-ftp
Jan-Piet Mens committed
98
    """
99

Jan-Piet Mens committed
100
    USERAGENT = 'ansible-httpget'
101
    info = dict(url=url, dest=dest)
Jan-Piet Mens committed
102 103 104 105 106
    r = None

    request = urllib2.Request(url)
    request.add_header('User-agent', USERAGENT)

107 108
    if os.path.exists(dest):
        t = datetime.datetime.utcfromtimestamp(os.path.getmtime(dest))
Jan-Piet Mens committed
109 110 111 112 113
        tstamp = t.strftime('%a, %d %b %Y %H:%M:%S +0000')
        request.add_header('If-Modified-Since', tstamp)

    try:
        r = urllib2.urlopen(request)
114 115
        info.update(r.info())
        info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200))
116
    except urllib2.HTTPError, e:
Jan-Piet Mens committed
117
        # Must not fail_json() here so caller can handle HTTP 304 unmodified
118
        info.update(dict(msg=str(e), status=e.code))
Jan-Piet Mens committed
119
        return r, info
120
    except urllib2.URLError, e:
121 122
        code = getattr(e, 'code', -1)
        module.fail_json(msg="Request failed: %s" % str(e), status_code=code)
Jan-Piet Mens committed
123 124 125

    return r, info

126 127
def url_get(module, url, dest):
    """
128
    Download url and store at dest.
129 130
    If dest is a directory, determine filename from url.
    Return (tempfile, info about the request)
Jan-Piet Mens committed
131 132
    """

133
    req, info = url_do_get(module, url, dest)
Jan-Piet Mens committed
134

135
    # TODO: should really handle 304, but how? src file could exist (and be newer) but empty
Jan-Piet Mens committed
136
    if info['status'] == 304:
137
        module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', ''))
Jan-Piet Mens committed
138

139 140
    # create a temporary file and copy content to do md5-based replacement
    if info['status'] != 200:
141
        module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url, dest=dest)
142 143 144 145 146 147 148 149 150 151 152

    fd, tempname = tempfile.mkstemp()
    f = os.fdopen(fd, 'wb')
    try:
        shutil.copyfileobj(req, f)
    except Exception, err:
        os.remove(tempname)
        module.fail_json(msg="failed to create temporary content file: %s" % str(err))
    f.close()
    req.close()
    return tempname, info
Jan-Piet Mens committed
153 154 155 156 157

# ==============================================================
# main

def main():
158 159

    # does this really happen on non-ancient python?
160 161 162 163 164
    if not HAS_URLLIB2:
        module.fail_json(msg="urllib2 is not installed")
    if not HAS_URLPARSE:
        module.fail_json(msg="urlparse is not installed")

Jan-Piet Mens committed
165
    module = AnsibleModule(
166
        # not checking because of daisy chain to file module
Jan-Piet Mens committed
167 168 169
        argument_spec = dict(
            url = dict(required=True),
            dest = dict(required=True),
170
            thirsty = dict(default='no', choices=BOOLEANS)
171 172
        ),
        add_file_common_args=True
Jan-Piet Mens committed
173
    )
174

175 176
    url  = module.params['url']
    dest = os.path.expanduser(module.params['dest'])
177 178
    thirsty = module.boolean(module.params['thirsty'])

179 180 181
    if os.path.isdir(dest):
        dest = os.path.join(dest, url_filename(url))

182
    if not thirsty:
183
        if os.path.exists(dest):
184
            module.exit_json(msg="file already exists", dest=dest, url=url, changed=False)
185

186 187 188 189
    # download to tmpsrc
    tmpsrc, info = url_get(module, url, dest)
    md5sum_src   = None
    md5sum_dest  = None
190

Jan-Piet Mens committed
191 192 193 194 195 196 197
    # raise an error if there is no tmpsrc file
    if not os.path.exists(tmpsrc):
        os.remove(tmpsrc)
        module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'])
    if not os.access(tmpsrc, os.R_OK):
        os.remove(tmpsrc)
        module.fail_json( msg="Source %s not readable" % (tmpsrc))
198
    md5sum_src = module.md5(tmpsrc)
199

Jan-Piet Mens committed
200 201 202 203 204 205 206 207 208
    # check if there is no dest file
    if os.path.exists(dest):
        # raise an error if copy has no permission on dest
        if not os.access(dest, os.W_OK):
            os.remove(tmpsrc)
            module.fail_json( msg="Destination %s not writable" % (dest))
        if not os.access(dest, os.R_OK):
            os.remove(tmpsrc)
            module.fail_json( msg="Destination %s not readable" % (dest))
209
        md5sum_dest = module.md5(dest)
Jan-Piet Mens committed
210 211 212 213
    else:
        if not os.access(os.path.dirname(dest), os.W_OK):
            os.remove(tmpsrc)
            module.fail_json( msg="Destination %s not writable" % (os.path.dirname(dest)))
214

Jan-Piet Mens committed
215 216 217 218 219
    if md5sum_src != md5sum_dest:
        try:
            shutil.copyfile(tmpsrc, dest)
        except Exception, err:
            os.remove(tmpsrc)
220
            module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err)))
Jan-Piet Mens committed
221 222 223
        changed = True
    else:
        changed = False
224

Jan-Piet Mens committed
225
    os.remove(tmpsrc)
226

227 228 229 230 231 232
    # allow file attribute changes
    module.params['path'] = dest
    file_args = module.load_file_common_arguments(module.params)
    file_args['path'] = dest
    changed = module.set_file_attributes_if_different(file_args, changed)

233
    # Mission complete
234 235
    module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src,
        changed=changed, msg=info.get('msg',''),
236
        daisychain="file", daisychain_args=info.get('daisychain_args',''))
Jan-Piet Mens committed
237 238 239 240

# this is magic, see lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>
main()