As we know, to interact with webservices, there are multiple ways to do in Ansible realm.
Shell Module
The most obvious and straight-foward way to request http resources, some like:
curl -k -u 'admin:admin' https://my.example.com/path/to/myresource.html
Seem to be simple enough, but definitely, it’s not elegant way I wish. Things could go to very messy soon enough if the url is getting longer or there are many variable in the curl command. You will find yourself at lost while reading the long string.
One big problem of doing this way: it may dramatically increase the risk of exposing your password during the execution.
URI Module
URI module is the official recommended module for almost every http request. And, it has been evolved sophisticatedly enough to handle most of the use case: details can be found on here
One example from the offical
- name: Login to a form based webpage using a list of tuples
uri:
url: https://your.form.based.auth.example.com/index.php
method: POST
body_format: form-urlencoded
body:
- [ name, your_username ]
- [ password, your_password ]
- [ enter, Sign in ]
status_code: 302
register: login
GET_URI Module
Get_uri module is similar to uri module, but its main purpose is to download file from http/https/ftp, etc.
Example:
- name: Download file with custom HTTP headers
get_url:
url: http://example.com/path/file.conf
dest: /etc/foo.conf
headers:
key1: one
key2: two
Custom Module
It would be more interesting to take a look at the custom module approach. As we know, ansible module is actually a executable file written in all kinds of program languages, python, shell, or even GO. But, most of the built-in modules come in with python, and ansible are basically python based. So, we would like to discuss python code module.
When it comes to python code, there is no doubt the famouse requests
library will come in the first place
Python Requests Library
requests library is one of the best python library I ever worked with, with its simple elegant syntax, powerful feature. I think that is why it gain such wide-spread popularity in python’s world.
The only one problem with requests
is that it’s needed to be installed on that specific host before it can be refered to by ansible module running on that host, or you will be seeing ugly and familiar message, No module named 'requests'
.
# Need to install on host in order for ansible module to import
pip install requests
It sound like quite simple to pip install this library, but for some rare case, with some highly secure dmz zone, which have no access to any external or internal pip source, although it is not totally impossible, but definitely a great headache to set it up.
Ansible Built-in fetch_url Methods
Another possible option is to use ansible built-in methods, fetch_url
.
One of great thing about fetch_url
is that it is available everywhere as long as ansible is installed, no need to install anything on target host except python.
But, I believe there is no Linux host coming without python at all. ^^.
However, it’s not well documented too much on the internet.
The best way to understand it is to download the source code from here
From lib/ansible/module_utils/urls.py
, we can see some simple document on how to use that.
Basically, it’s quite similar to its counterpart requests
lib.
def fetch_url(module, url, data=None, headers=None, method=None,
use_proxy=True, force=False, last_mod_time=None, timeout=10,
use_gssapi=False, unix_socket=None, ca_path=None, cookies=None, unredirected_headers=None):
"""Sends a request via HTTP(S) or FTP (needs the module as parameter)
:arg module: The AnsibleModule (used to get username, password etc. (s.b.).
:arg url: The url to use.
:kwarg data: The data to be sent (in case of POST/PUT).
:kwarg headers: A dict with the request headers.
:kwarg method: "POST", "PUT", etc.
:kwarg boolean use_proxy: Default: True
:kwarg boolean force: If True: Do not get a cached copy (Default: False)
:kwarg last_mod_time: Default: None
:kwarg int timeout: Default: 10
:kwarg boolean use_gssapi: Default: False
:kwarg unix_socket: (optional) String of file system path to unix socket file to use when establishing
connection to the provided url
:kwarg ca_path: (optional) String of file system path to CA cert bundle to use
:kwarg cookies: (optional) CookieJar object to send with the request
:kwarg unredirected_headers: (optional) A list of headers to not attach on a redirected request
:returns: A tuple of (**response**, **info**). Use ``response.read()`` to read the data.
The **info** contains the 'status' and other meta data. When a HttpError (status >= 400)
occurred then ``info['body']`` contains the error response data::
Example::
data={...}
resp, info = fetch_url(module,
"http://example.com",
data=module.jsonify(data),
headers={'Content-type': 'application/json'},
method="POST")
status_code = info["status"]
body = resp.read()
if status_code >= 400 :
body = info['body']
"""
But, as matter of fact, this simple document doesn’t serve much help to write working code. So, I will try to show some example below.
How to import fetch_url
from ansible.module_utils.urls import fetch_url
How to supply username/password to fetch_url
For example, for your custom module
- name: My Demo Custom Module
my_memo_module:
username: admin
password: admin
state: present
validate_certs: false
basic_auth: true
password/username MUST be passed on to url_username
/url_password
to module params.
And, then fetch_url
will get this info from module object by passing in module as parameter.
username = module.params['username']
password = module.params['password']
module.params['url_username'] = username
module.params['url_password'] = password
And, if you hope to enforce basic auth, pass on that as well
force_basic_auth = module.params['basic_auth']
module.params['force_basic_auth'] = force_basic_auth
Same case with validate_certs
/follow_redirects
/http_agent
.
How to do simple GET with fetch_url
(resp, info) = fetch_url(
module=module,
url=url,
headers=headers,
method="GET")
if info['status'] == 200:
module.exit_json(changed=True)
else:
module.fail_json(msg="unable to call url: %s"% url)
How to do simple POST with fetch_url
data = {}
(resp, info) = fetch_url(
module=module,
url=url,
data=urllib.parse.urlencode(data),
headers=headers,
method="POST")
if info['status'] == 200:
module.exit_json(changed=True)
else:
module.fail_json(msg="unable to post url: %s"% url)
In my case, urllib.parse.urlencode(data)
work better than module.jsonify(data)
How to POST binary file with mutipart protocal
There is no much magic about fetch_url
, except that there is no built-in support for uploading binary file along with mutipart
protocal according to this
I do some deep research about that, finally be able to do that with some hack work.
Create local module_utils dir
Create module_utils
to host shared lib file to fetch_url
cd <your playbook dir>
mkdir -pv library/module_utils
Add two lines to your ansible.cfg
, [default]
section
[defaults]
library = ./library
module_utils = ./library/module_utils
Download requests toolbelt lib
download toolbelt
lib with pip in your local machine
pip3 install requests-toolbelt -t ./
cp -Rv requests_toolbelt/multipart/ <playbook_dir>/library/module_utils
Copy over necessary file, fields.py
cp -Rv urllib3/fields.py <playbook_dir>/library/module_utils/multipart/
Now, it should look like this
library/
├── module_utils
│ └── multipart
│ ├── decoder.py
│ ├── encoder.py
│ ├── fields.py
│ ├── __init__.py
Update library/module_utils/multipart/encoder.py
Update this part
import requests
from .._compat import fields
to
# import requests
from .fields import RequestField
#self.session = session or requests.Session()
#This line become
self.session = session
#....
#field = fields.RequestField(name=k, data=file_pointer,
#This line become
field = RequestField(name=k, data=file_pointer,
Import the multipart
module utils you bake
Import the utils
try:
from ansible.module_utils.multipart import MultipartEncoder
except ImportError:
from module_utils.multipart import MultipartEncoder
Use the same way as normal toolbelt lib
m = MultipartEncoder(
fields={'field0': 'value', 'field1': 'value',
'field2': ('filename', open('file.zip', 'rb'), 'application/octet-stream')}
)
headers = {}
headers['Content-Type'] = m.content_type
(resp, info) = fetch_url(
module=module,
url=url,
data=m,
headers=headers,
method="POST")
Cheers !