2013-02-15 23:32:31 +01:00
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2013, Romeo Theriault <romeot () hawaii.edu>
#
# 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/uri.yml
import shutil
import tempfile
import base64
2013-04-10 00:14:57 +02:00
import datetime
2013-02-15 23:32:31 +01:00
try :
import json
except ImportError :
import simplejson as json
DOCUMENTATION = '''
- - -
module : uri
short_description : Interacts with webservices
description :
2013-03-30 20:53:28 +01:00
- Interacts with HTTP and HTTPS web services and supports Digest , Basic and WSSE
HTTP authentication mechanisms .
2013-02-15 23:32:31 +01:00
version_added : " 1.1 "
options :
url :
description :
- HTTP or HTTPS URL in the form ( http | https ) : / / host . domain [ : port ] / path
required : true
default : null
aliases : [ ]
dest :
description :
2013-02-19 00:03:19 +01:00
- path of where to download the file to ( if desired ) . If I ( dest ) is a directory , the basename of the file on the remote server will be used .
2013-02-15 23:32:31 +01:00
required : false
default : null
user :
description :
- username for the module to use for Digest , Basic or WSSE authentication .
required : false
default : null
password :
description :
- password for the module to use for Digest , Basic or WSSE authentication .
required : false
default : null
body :
description :
2013-02-19 00:03:19 +01:00
- The body of the http request / response to the web service .
2013-02-15 23:32:31 +01:00
required : false
default : null
method :
description :
- The HTTP method of the request or response .
required : false
2014-02-02 21:53:52 +01:00
choices : [ " GET " , " POST " , " PUT " , " HEAD " , " DELETE " , " OPTIONS " , " PATCH " ]
2013-02-15 23:32:31 +01:00
default : " GET "
return_content :
description :
2013-02-18 14:43:02 +01:00
- Whether or not to return the body of the request as a " content " key in the dictionary result . If the reported Content - type is " application/json " , then the JSON is additionally loaded into a key called C ( json ) in the dictionary results .
2013-02-15 23:32:31 +01:00
required : false
choices : [ " yes " , " no " ]
2013-02-19 00:03:19 +01:00
default : " no "
2013-02-15 23:32:31 +01:00
force_basic_auth :
description :
- httplib2 , the library used by the uri module only sends authentication information when a webservice
responds to an initial request with a 401 status . Since some basic auth services do not properly
send a 401 , logins will fail . This option forces the sending of the Basic authentication header
upon initial request .
required : false
choices : [ " yes " , " no " ]
2013-02-19 00:03:19 +01:00
default : " no "
2013-02-15 23:32:31 +01:00
follow_redirects :
description :
2014-01-30 01:04:53 +01:00
- Whether or not the URI module should follow redirects . C ( all ) will follow all redirects .
C ( safe ) will follow only " safe " redirects , where " safe " means that the client is only
doing a GET or HEAD on the URI to which it is being redirected . C ( none ) will not follow
any redirects . Note that C ( yes ) and C ( no ) choices are accepted for backwards compatibility ,
where C ( yes ) is the equivalent of C ( all ) and C ( no ) is the equivalent of C ( safe ) . C ( yes ) and C ( no )
are deprecated and will be removed in some future version of Ansible .
2013-02-15 23:32:31 +01:00
required : false
2014-01-30 01:04:53 +01:00
choices : [ " all " , " safe " , " none " ]
default : " safe "
2013-02-15 23:32:31 +01:00
creates :
description :
- a filename , when it already exists , this step will not be run .
required : false
removes :
description :
- a filename , when it does not exist , this step will not be run .
required : false
status_code :
description :
2014-03-24 14:51:44 +01:00
- A valid , numeric , HTTP status code that signifies success of the request . Can also be comma separated list of status codes .
2013-02-15 23:32:31 +01:00
required : false
default : 200
2013-03-03 02:27:09 +01:00
timeout :
description :
- The socket level timeout in seconds
required : false
2013-03-18 04:07:15 +01:00
default : 30
2013-02-15 23:32:31 +01:00
HEADER_ :
2013-03-30 20:53:28 +01:00
description :
- Any parameter starting with " HEADER_ " is a sent with your request as a header .
For example , HEADER_Content - Type = " application/json " would send the header
" Content-Type " along with your request with a value of " application/json " .
2013-02-15 23:32:31 +01:00
required : false
default : null
others :
description :
- all arguments accepted by the M ( file ) module also work here
required : false
2013-02-19 00:03:19 +01:00
2013-06-14 11:53:43 +02:00
# informational: requirements for nodes
requirements : [ urlparse , httplib2 ]
author : Romeo Theriault
'''
2013-02-19 00:03:19 +01:00
2013-06-14 11:53:43 +02:00
EXAMPLES = '''
# Check that you can connect (GET) to a page and it returns a status 200
- uri : url = http : / / www . example . com
2013-02-19 00:03:19 +01:00
2013-06-14 11:53:43 +02:00
# Check that a page returns a status 200 and fail if the word AWESOME is not in the page contents.
- action : uri url = http : / / www . example . com return_content = yes
register : webpage
2013-02-19 00:03:19 +01:00
2013-06-17 08:53:46 +02:00
- action : fail
2013-07-19 15:37:19 +02:00
when : ' AWESOME ' not in " {{ webpage.content }} "
2013-02-19 00:03:19 +01:00
2014-04-02 23:32:44 +02:00
# Create a JIRA issue
- uri : url = https : / / your . jira . example . com / rest / api / 2 / issue /
method = POST user = your_username password = your_pass
body = " {{ lookup( ' file ' , ' issue.json ' ) }} " force_basic_auth = yes
status_code = 201 HEADER_Content - Type = " application/json "
2013-06-14 11:53:43 +02:00
# Login to a form based webpage, then use the returned cookie to
2014-04-02 23:32:44 +02:00
# access the app in later tasks
- uri : url = https : / / your . form . based . auth . examle . com / index . php
method = POST body = " name=your_username&password=your_password&enter=Sign %20i n "
status_code = 302 HEADER_Content - Type = " application/x-www-form-urlencoded "
register : login
- uri : url = https : / / your . form . based . auth . example . com / dashboard . php
method = GET return_content = yes HEADER_Cookie = " {{ login.set_cookie}} "
2014-03-31 14:32:07 +02:00
# Queue build of a project in Jenkins:
2014-04-02 23:32:44 +02:00
- uri : url = http : / / { { jenkins . host } } / job / { { jenkins . job } } / build ? token = { { jenkins . token } }
method = GET user = { { jenkins . user } } password = { { jenkins . password } } force_basic_auth = yes status_code = 201
2013-02-15 23:32:31 +01:00
'''
HAS_HTTPLIB2 = True
try :
import httplib2
except ImportError :
HAS_HTTPLIB2 = False
HAS_URLPARSE = True
try :
import urlparse
import socket
except ImportError :
HAS_URLPARSE = False
def write_file ( module , url , dest , content ) :
# create a tempfile with some test content
fd , tmpsrc = tempfile . mkstemp ( )
f = open ( tmpsrc , ' wb ' )
try :
f . write ( content )
except Exception , err :
os . remove ( tmpsrc )
module . fail_json ( msg = " failed to create temporary content file: %s " % str ( err ) )
f . close ( )
md5sum_src = None
md5sum_dest = None
# raise an error if there is no tmpsrc file
if not os . path . exists ( tmpsrc ) :
os . remove ( tmpsrc )
module . fail_json ( msg = " Source %s does not exist " % ( tmpsrc ) )
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 )
2013-04-10 00:14:57 +02:00
module . fail_json ( msg = " Destination dir %s not writable " % ( os . path . dirname ( dest ) ) )
2013-02-15 23:32:31 +01:00
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 ) ) )
os . remove ( tmpsrc )
def url_filename ( url ) :
fn = os . path . basename ( urlparse . urlsplit ( url ) [ 2 ] )
if fn == ' ' :
return ' index.html '
return fn
2013-03-03 02:27:09 +01:00
def uri ( module , url , dest , user , password , body , method , headers , redirects , socket_timeout ) :
2013-02-15 23:32:31 +01:00
# To debug
#httplib2.debug = 4
2014-01-30 01:04:53 +01:00
# Handle Redirects
if redirects == " all " or redirects == " yes " :
follow_redirects = True
follow_all_redirects = True
elif redirects == " none " :
follow_redirects = False
follow_all_redirects = False
else :
follow_redirects = True
follow_all_redirects = False
2013-02-15 23:32:31 +01:00
# Create a Http object and set some default options.
2013-03-03 02:27:09 +01:00
h = httplib2 . Http ( disable_ssl_certificate_validation = True , timeout = socket_timeout )
2014-01-30 01:04:53 +01:00
h . follow_all_redirects = follow_all_redirects
h . follow_redirects = follow_redirects
2013-02-15 23:32:31 +01:00
h . forward_authorization_headers = True
# If they have a username or password verify they have both, then add them to the request
if user is not None and password is None :
module . fail_json ( msg = " Both a username and password need to be set. " )
if password is not None and user is None :
module . fail_json ( msg = " Both a username and password need to be set. " )
if user is not None and password is not None :
h . add_credentials ( user , password )
2013-04-10 00:14:57 +02:00
# is dest is set and is a directory, let's check if we get redirected and
# set the filename from that url
redirected = False
resp_redir = { }
r = { }
if dest is not None :
dest = os . path . expanduser ( dest )
if os . path . isdir ( dest ) :
# first check if we are redirected to a file download
h . follow_redirects = False
# Try the request
try :
resp_redir , content_redir = h . request ( url , method = method , body = body , headers = headers )
# if we are redirected, update the url with the location header,
# and update dest with the new url filename
except :
pass
2013-08-29 15:29:14 +02:00
if ' status ' in resp_redir and resp_redir [ ' status ' ] in [ " 301 " , " 302 " , " 303 " , " 307 " ] :
2013-04-10 00:14:57 +02:00
url = resp_redir [ ' location ' ]
redirected = True
dest = os . path . join ( dest , url_filename ( url ) )
# if destination file already exist, only download if file newer
if os . path . exists ( dest ) :
t = datetime . datetime . utcfromtimestamp ( os . path . getmtime ( dest ) )
tstamp = t . strftime ( ' %a , %d % b % Y % H: % M: % S +0000 ' )
headers [ ' If-Modified-Since ' ] = tstamp
# do safe redirects now, including 307
2014-01-30 01:04:53 +01:00
h . follow_redirects = follow_redirects
2013-04-10 00:14:57 +02:00
2013-02-15 23:32:31 +01:00
# Make the request, or try to :)
try :
resp , content = h . request ( url , method = method , body = body , headers = headers )
2013-04-10 00:14:57 +02:00
r [ ' redirected ' ] = redirected
r . update ( resp_redir )
r . update ( resp )
2014-02-25 06:34:17 +01:00
try :
2014-02-25 20:03:20 +01:00
return r , unicode ( content . decode ( ' unicode_escape ' ) ) , dest
2014-02-25 06:34:17 +01:00
except :
return r , content , dest
2013-02-15 23:32:31 +01:00
except httplib2 . RedirectMissingLocation :
module . fail_json ( msg = " A 3xx redirect response code was provided but no Location: header was provided to point to the new location. " )
except httplib2 . RedirectLimit :
module . fail_json ( msg = " The maximum number of redirections was reached without coming to a final URI. " )
except httplib2 . ServerNotFoundError :
module . fail_json ( msg = " Unable to resolve the host name given. " )
except httplib2 . RelativeURIError :
module . fail_json ( msg = " A relative, as opposed to an absolute URI, was passed in. " )
except httplib2 . FailedToDecompressContent :
module . fail_json ( msg = " The headers claimed that the content of the response was compressed but the decompression algorithm applied to the content failed. " )
except httplib2 . UnimplementedDigestAuthOptionError :
module . fail_json ( msg = " The server requested a type of Digest authentication that we are unfamiliar with. " )
except httplib2 . UnimplementedHmacDigestAuthOptionError :
module . fail_json ( msg = " The server requested a type of HMACDigest authentication that we are unfamiliar with. " )
except httplib2 . UnimplementedHmacDigestAuthOptionError :
module . fail_json ( msg = " The server requested a type of HMACDigest authentication that we are unfamiliar with. " )
except socket . error , e :
module . fail_json ( msg = " Socket error: %s to %s " % ( e , url ) )
def main ( ) :
module = AnsibleModule (
argument_spec = dict (
url = dict ( required = True ) ,
dest = dict ( required = False , default = None ) ,
user = dict ( required = False , default = None ) ,
password = dict ( required = False , default = None ) ,
body = dict ( required = False , default = None ) ,
2014-02-02 21:53:52 +01:00
method = dict ( required = False , default = ' GET ' , choices = [ ' GET ' , ' POST ' , ' PUT ' , ' HEAD ' , ' DELETE ' , ' OPTIONS ' , ' PATCH ' ] ) ,
2013-02-23 22:56:45 +01:00
return_content = dict ( required = False , default = ' no ' , type = ' bool ' ) ,
force_basic_auth = dict ( required = False , default = ' no ' , type = ' bool ' ) ,
2014-01-30 01:04:53 +01:00
follow_redirects = dict ( required = False , default = ' safe ' , choices = [ ' all ' , ' safe ' , ' none ' , ' yes ' , ' no ' ] ) ,
2013-02-15 23:32:31 +01:00
creates = dict ( required = False , default = None ) ,
removes = dict ( required = False , default = None ) ,
2014-03-24 14:51:44 +01:00
status_code = dict ( required = False , default = [ 200 ] , type = ' list ' ) ,
2013-03-18 04:07:15 +01:00
timeout = dict ( required = False , default = 30 , type = ' int ' ) ,
2013-02-15 23:32:31 +01:00
) ,
check_invalid_arguments = False ,
add_file_common_args = True
)
2013-02-16 01:10:21 +01:00
if not HAS_HTTPLIB2 :
module . fail_json ( msg = " httplib2 is not installed " )
if not HAS_URLPARSE :
module . fail_json ( msg = " urlparse is not installed " )
2013-02-15 23:32:31 +01:00
url = module . params [ ' url ' ]
user = module . params [ ' user ' ]
password = module . params [ ' password ' ]
body = module . params [ ' body ' ]
method = module . params [ ' method ' ]
dest = module . params [ ' dest ' ]
2013-02-23 19:59:52 +01:00
return_content = module . params [ ' return_content ' ]
force_basic_auth = module . params [ ' force_basic_auth ' ]
2014-01-30 01:04:53 +01:00
redirects = module . params [ ' follow_redirects ' ]
2013-02-15 23:32:31 +01:00
creates = module . params [ ' creates ' ]
removes = module . params [ ' removes ' ]
2014-03-24 15:23:18 +01:00
status_code = [ int ( x ) for x in list ( module . params [ ' status_code ' ] ) ]
2013-03-03 02:27:09 +01:00
socket_timeout = module . params [ ' timeout ' ]
2013-02-15 23:32:31 +01:00
# Grab all the http headers. Need this hack since passing multi-values is currently a bit ugly. (e.g. headers='{"Content-Type":"application/json"}')
dict_headers = { }
for key , value in module . params . iteritems ( ) :
if key . startswith ( " HEADER_ " ) :
skey = key . replace ( " HEADER_ " , " " )
dict_headers [ skey ] = value
if creates is not None :
# do not run the command if the line contains creates=filename
# and the filename already exists. This allows idempotence
# of uri executions.
creates = os . path . expanduser ( creates )
if os . path . exists ( creates ) :
module . exit_json ( stdout = " skipped, since %s exists " % creates , skipped = True , changed = False , stderr = False , rc = 0 )
if removes is not None :
# do not run the command if the line contains removes=filename
# and the filename do not exists. This allows idempotence
# of uri executions.
v = os . path . expanduser ( removes )
if not os . path . exists ( removes ) :
module . exit_json ( stdout = " skipped, since %s does not exist " % removes , skipped = True , changed = False , stderr = False , rc = 0 )
# httplib2 only sends authentication after the server asks for it with a 401.
# Some 'basic auth' servies fail to send a 401 and require the authentication
# up front. This creates the Basic authentication header and sends it immediately.
if force_basic_auth :
2013-02-18 01:48:02 +01:00
dict_headers [ " Authorization " ] = " Basic {0} " . format ( base64 . b64encode ( " {0} : {1} " . format ( user , password ) ) )
2013-02-15 23:32:31 +01:00
# Make the request
2013-04-10 00:14:57 +02:00
resp , content , dest = uri ( module , url , dest , user , password , body , method , dict_headers , redirects , socket_timeout )
2013-10-12 22:10:40 +02:00
resp [ ' status ' ] = int ( resp [ ' status ' ] )
2013-02-15 23:32:31 +01:00
# Write the file out if requested
if dest is not None :
2013-10-12 22:10:40 +02:00
if resp [ ' status ' ] == 304 :
2013-04-10 00:14:57 +02:00
changed = False
else :
write_file ( module , url , dest , content )
# allow file attribute changes
changed = True
module . params [ ' path ' ] = dest
file_args = module . load_file_common_arguments ( module . params )
file_args [ ' path ' ] = dest
2014-03-19 03:39:45 +01:00
changed = module . set_fs_attributes_if_different ( file_args , changed )
2013-04-10 00:14:57 +02:00
resp [ ' path ' ] = dest
2013-04-29 20:59:06 +02:00
else :
changed = False
2013-02-15 23:32:31 +01:00
# Transmogrify the headers, replacing '-' with '_', since variables dont work with dashes.
uresp = { }
for key , value in resp . iteritems ( ) :
ukey = key . replace ( " - " , " _ " )
uresp [ ukey ] = value
2013-02-18 14:43:02 +01:00
if ' content_type ' in uresp :
if uresp [ ' content_type ' ] . startswith ( ' application/json ' ) :
try :
js = json . loads ( content )
uresp [ ' json ' ] = js
except :
pass
2014-03-24 15:23:18 +01:00
if resp [ ' status ' ] not in status_code :
2013-10-12 22:10:40 +02:00
module . fail_json ( msg = " Status code was not " + str ( status_code ) , content = content , * * uresp )
2013-02-15 23:32:31 +01:00
elif return_content :
2013-04-10 00:14:57 +02:00
module . exit_json ( changed = changed , content = content , * * uresp )
2013-02-15 23:32:31 +01:00
else :
2013-04-10 00:14:57 +02:00
module . exit_json ( changed = changed , * * uresp )
2013-02-15 23:32:31 +01:00
2013-12-02 21:13:49 +01:00
# import module snippets
2013-12-02 21:11:23 +01:00
from ansible . module_utils . basic import *
2013-02-15 23:32:31 +01:00
main ( )