summaryrefslogtreecommitdiff
path: root/library/get_url
blob: 057a8c662b611405b18e2c897ff6ef2282c834f0 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
#!/usr/bin/python

# (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/>.
#
# see examples/playbooks/get_url.yml

import shutil
import datetime
import tempfile

HAS_URLLIB2=True
try:
    import urllib2
except ImportError: 
    HAS_URLLIB2=False
HAS_URLPARSE=True

try:
    import urlparse
    import socket
except ImportError: 
    HAS_URLPARSE=False

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

def url_filename(url):
    fn = os.path.basename(urlparse.urlsplit(url)[2])
    if fn == '':
        return 'index.html'
    return fn

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
    """

    USERAGENT = 'ansible-httpget'
    info = dict(url=url)
    r = None
    actualdest = None

    if os.path.isdir(dest):
        urlfilename = url_filename(url)
        actualdest = "%s/%s" % (dest, urlfilename)
        module.params['path'] = actualdest
    else:
         actualdest = dest
    info['daisychain_args'] = module.params
    info['actualdest'] = actualdest

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

    if os.path.exists(actualdest):
        t = datetime.datetime.utcfromtimestamp(os.path.getmtime(actualdest))
        tstamp = t.strftime('%a, %d %b %Y %H:%M:%S +0000')
        request.add_header('If-Modified-Since', tstamp)

    try:
        r = urllib2.urlopen(request)
        info.update(r.info())
        info.update(dict(msg="OK (%s bytes)" % r.headers.get('Content-Length', 'unknown'), status=200))
    except urllib2.HTTPError, e:
        # Must not fail_json() here so caller can handle HTTP 304 unmodified
        info.update(dict(msg=str(e), status=e.code))
        return r, info
    except urllib2.URLError, e:
        code = getattr(e, 'code', -1)
        module.fail_json(msg="Request failed: %s" % str(e), status_code=code)

    return r, info

def url_get(module, url, dest):
    """
    Download url and store at dest. 
    If dest is a directory, determine filename from url.
    Return (tempfile, info about the request)
    """

    req, info = url_do_get(module, url, dest)

    # TODO: should really handle 304, but how? src file could exist (and be newer) but empty
    if info['status'] == 304:
        module.exit_json(url=url, dest=info.get('actualdest', dest), changed=False, msg=info.get('msg', ''))

    # create a temporary file and copy content to do md5-based replacement
    if info['status'] != 200:
        module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url)
    actualdest = info['actualdest']

    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

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

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

    module = AnsibleModule(
        # not checking because of daisy chain to file module
        check_invalid_arguments = False,
        argument_spec = dict(
            url = dict(required=True),
            dest = dict(required=True),
        )
    )
    
    url  = module.params['url']
    dest = os.path.expanduser(module.params['dest'])
    
    # download to tmpsrc
    tmpsrc, info = url_get(module, url, dest)
    md5sum_src   = None
    md5sum_dest  = None
    dest         = info['actualdest']
    
    # 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))
    md5sum_src = module.md5(tmpsrc)
     
    # 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))
        md5sum_dest = module.md5(dest)
    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)))
    
    if md5sum_src != md5sum_dest:
        try:
            shutil.copyfile(tmpsrc, dest)
        except Exception, err:
            os.remove(tmpsrc)
            module.fail_json(msg="failed to copy %s to %s: %s" % (tmpsrc, dest, str(err))) 
        changed = True
    else:
        changed = False
    
    os.remove(tmpsrc)

    # Mission complete
    module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum_src, 
        changed=changed, msg=info.get('msg',''), 
        daisychain="file", daisychain_args=info.get('daisychain_args',''))

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