How to import an external library into an Ansible module? - python

I would like to add Ansible module locally. The module should use external Python library. I have added just the following line:
from ansible.module_utils.foo import Bar
to the Ansible new module template making it look like below:
my_test.py
#!/usr/bin/python
# Copyright: (c) 2018, Terry Jones <terry.jones#example.org>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: my_test
short_description: This is my test module
version_added: "2.4"
description:
- "This is my longer description explaining my test module"
options:
name:
description:
- This is the message to send to the test module
required: true
new:
description:
- Control to demo if the result of this module is changed or not
required: false
extends_documentation_fragment:
- azure
author:
- Your Name (#yourhandle)
'''
EXAMPLES = '''
# Pass in a message
- name: Test with a message
my_test:
name: hello world
# pass in a message and have changed true
- name: Test with a message and changed output
my_test:
name: hello world
new: true
# fail the module
- name: Test failure of the module
my_test:
name: fail me
'''
RETURN = '''
original_message:
description: The original name param that was passed in
type: str
returned: always
message:
description: The output message that the test module generates
type: str
returned: always
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.foo import Bar
def run_module():
# define available arguments/parameters a user can pass to the module
module_args = dict(
name=dict(type='str', required=True),
new=dict(type='bool', required=False, default=False)
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
original_message='',
message=''
)
# the AnsibleModule object will be our abstraction working with Ansible
# this includes instantiation, a couple of common attr would be the
# args/params passed to the execution, as well as if the module
# supports check mode
module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True
)
# if the user is working with this module in only check mode we do not
# want to make any changes to the environment, just return the current
# state with no modifications
if module.check_mode:
module.exit_json(**result)
# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
result['original_message'] = module.params['name']
result['message'] = 'goodbye'
# use whatever logic you need to determine whether or not this module
# made any modifications to your target
if module.params['new']:
result['changed'] = True
# during the execution of the module, if there is an exception or a
# conditional state that effectively causes a failure, run
# AnsibleModule.fail_json() to pass in the message and the result
if module.params['name'] == 'fail me':
module.fail_json(msg='You requested this to fail', **result)
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()
I haven't introduced any changes in Ansible playbook template. I paste playbook here for neatness:
testmod.yml
- name: test my new module
hosts: localhost
tasks:
- name: run the new module
my_test:
name: 'hello'
new: true
register: testout
- name: dump test output
debug:
msg: '{{ testout }}'
I have put module my_test.py in the following localization:
~/.ansible/plugins/modules
I have extended ANSIBLE_MODULE_UTILS environment variable making foo library visible. Generally, leaving aside Ansible, parts of foo library may be imported to the Python script in the following way:
from foo import Bar
I have tested, that when e.g. foo is a Python script and Bar is a class inside that script, the testmod.yml playbook may be run correctly. The problem is that foo is a directory, there is not foo.py file, nor Bar.py. In my case, when I run testmod.yml, I receive traceback:
ImportError: No module named foo.config
Could you tell me what should I do to be able to use foo external library in my local Ansible module?

Related

Custom Ansible module is giving param extra params error

I am trying to implement hostname like module and my target machine in an amazon-ec2. But When I am running the script its giving me below error:
[ansible-user#ansible-master ~]$ ansible node1 -m edit_hostname.py -a node2
ERROR! this task 'edit_hostname.py' has extra params, which is only allowed in the following modules: meta, group_by, add_host, include_tasks, import_role, raw, set_fact, command, win_shell, import_tasks, script, shell, include_vars, include_role, include, win_command
My module is like this:
#!/usr/bin/python
from ansible.module_utils.basic import *
try:
import json
except ImportError:
import simplejson as json
def write_to_file(module, hostname, hostname_file):
try:
with open(hostname_file, 'w+') as f:
try:
f.write("%s\n" %hostname)
finally:
f.close()
except Exception:
err = get_exception()
module.fail_json(msg="failed to write to the /etc/hostname file")
def main():
hostname_file = '/etc/hostname'
module = AnsibleModule(argument_spec=dict(name=dict(required=True, type=str)))
name = module.params['name']
write_to _file(module, name, hostname_file)
module.exit_json(changed=True, meta=name)
if __name__ == "__main__":
main()
I don't know where I am making the mistake. Any help will be greatly appreciated. Thank you.
When developing a new module, I would recommend to use the boilerplate described in the documentation. This also shows that you'll need to use AnsibleModule to define your arguments.
In your main, you should add something like the following:
def main():
# define available arguments/parameters a user can pass to the module
module_args = dict(
name=dict(type='str', required=True)
)
# seed the result dict in the object
# we primarily care about changed and state
# change is if this module effectively modified the target
# state will include any data that you want your module to pass back
# for consumption, for example, in a subsequent task
result = dict(
changed=False,
original_hostname='',
hostname=''
)
module = AnsibleModule(
argument_spec=module_args
supports_check_mode=False
)
# manipulate or modify the state as needed (this is going to be the
# part where your module will do what it needs to do)
result['original_hostname'] = module.params['name']
result['hostname'] = 'goodbye'
# use whatever logic you need to determine whether or not this module
# made any modifications to your target
result['changed'] = True
# in the event of a successful module execution, you will want to
# simple AnsibleModule.exit_json(), passing the key/value results
module.exit_json(**result)
Then, you can call the module like so:
ansible node1 -m mymodule.py -a "name=myname"
ERROR! this task 'edit_hostname.py' has extra params, which is only allowed in the following modules: meta, group_by, add_host, include_tasks, import_role, raw, set_fact, command, win_shell, import_tasks, script, shell, include_vars, include_role, include, win_command
As explained by your error message, an anonymous default parameter is only supported by a limited number of modules. In your custom module, the paramter you created is called name. Moreover, you should not include the .py extension in the module name. You have to call your module like so as an ad-hoc command:
$ ansible node1 -m edit_hostname -a name=node2
I did not test your module code so you may have further errors to fix.
Meanwhile, I still strongly suggest you use the default boilerplate from the ansible documentation as proposed in #Simon's answer.

Ansible error of var type when passing to my custom module

I'm looking to make an ansible role and module abled to list all packages on a Linux System actually installed and register them to a var.
Then upgrade all of them and put the second list in an other var.
My module is here to make a diff of the two dictionaries (yum_packages1 and yum_packages2) and return it at the end of my role
When i'm trying to pass thoses two dictonaries into my modules and start my treatment i have a very strange error.
fatal: [centos7_]: FAILED! => {"changed": false, "msg": "argument yum_packages2 is of type and we were unable to convert to dict: cannot be converted to a dict"}
Ansible role task
---
# tasks file for ansible_yum_update_linux
- name: Listing Linux packages already installed
yum:
list: installed
register: yum_packages1
- name: Upgrade paquets Linux
yum:
name: '*'
state: latest
exclude: "{{ packages_exclude }}"
- name: Listing Linux packages installed and updated
yum:
list: installed
register: yum_packages2
- debug:
var: yum_packages1
- debug:
var: yum_packages2
- name: file compare
filecompare:
yum_packages1: "{{ yum_packages1.results }}"
yum_packages2: "{{ yum_packages2.results }}"
register: result
- debug:
var: result
Custome ansible module
#!/usr/bin/python
import json
from ansible.module_utils.basic import AnsibleModule
def diff_update(f1,f2):
#f3 = set(f1.keys()) == set(f2.keys())
upd1 = set(f1.values())
upd2 = set(f2.values())
f3 = (upd1.difference(upd2))
return f3
def main():
module = AnsibleModule(
argument_spec = dict(
yum_packages1 = dict(required=True, type='dict'),
yum_packages2 = dict(required=True, type='dict')
)
)
f3 = diff_update(module.params['yum_packages1'],module.params['yum_packages2'])
module.exit_json(changed=False, diff=f3)
if __name__ == '__main__':
main()
Do you have any idea why i get this error ?
Do you have any idea why i get this error ?
Because set is not json serializable:
import json
print(json.dumps(set("hello", "kaboom")))
cheerfully produces:
TypeError: set(['kaboom', 'hello']) is not JSON serializable
That's actually bitten me a couple of times in actual ansible modules, which is why I knew it was a thing; if you insist on using your custom module, you'll want to convert the set back to a list before returning it:
module.exit_json(changed=False, diff=list(f3))

How do i register the python variables in a Ansible task

i'm trying to extract variables from a python script response, basically i have a task that executes a python script and i need to get the variables of that response, this python script parse a json and put the response in diferent variables
((python script)
import json
with open('dd.json') as f:
data = json.load(f)
for item in data['service-nat-pool-information'][0]['sfw-per-service-set-nat-pool']:
ifname = [b['data'] for b in item['interface-name']]
for q in item['service-nat-pool']:
name = [a['data'] for a in q['pool-name']]
rang = [n['data'] for n in q['pool-address-range-list'][0]['pool-address-range']] #linea agregada de stack
# ports = item['pool-port-range'][0]['data']
# use= item['pool-ports-in-use'][0]['data']
block= [j['data'] for j in q['effective-port-blocks']]
mblock= [m['data'] for m in q['effective-ports']]
maxp =[d['data'] for d in q['port-block-efficiency']]
print("|if-name",ifname,"|name:",name,"|ip-range:",rang,"|Effective-port-blocks:",block[0],"|Effective-port:",mblock[0],"|Port-Block-Efficiency:",maxp[0])
ansible playbook
---
- name: Parse
hosts: localhost
connection: local
vars:
pool: "{{ lookup('file','/etc/ansible/playbook/dd.json') | from_json }}"
tasks:
- name: Execute Script
command: python3.7 parsetry.py
i expected a task in ansible that gets the variables in the python script and store them in ansible variables
You have to use register. If you modify your script to output json that might ease your work a little bit.
- name: Execute Script
command: python3.7 parsetry.py
register: script_run
- name: Degug output
debug:
msg: "{{ script_run.stdout | from_json }}"
If you want to keep full python power under your fingers, you might as well consider turning your script in a custom module or a custom filter if it ever makes sense.

Robotframework treats variables passed from python class as 'None'

i have a python class Wiresharking.py
from subprocess import Popen, CREATE_NEW_CONSOLE,PIPE,STDOUT
import time
import subprocess
import datetime
import os
#import envSI
class Wiresharking:
"""Wireshark Server subclass"""
def __init__(self,**kwargs):
self.filters=''
self.window_ip = kwargs.get('ip')
print type(self.window_ip)
self.window_user= kwargs.get('username')
self.window_password= kwargs.get('password')
self.dest_path= kwargs.get('Target_path')
self.interface= kwargs.get('interface')
self.terminal='cmd'
self.home=kwargs.get('Home_path')
def test(self):
print 'hi'
return self.window_ip
i can call it from another python file (env.py) like below
SERVER_01 = Wiresharking(
name='WIRESHARK_ENV91',
ip='192.168.1.16',
username=r'INTRA\pmmm', #always prepend r , before giving your username and password
password='jan#2018',
prompt='$ ',
autostart=False,
unzip_capture=True,
filter='',
#interface=['ens2f0'],
interface='Ethernet',
Target_path=r'D:\Users\pankaj-m\Desktop\Test'
)
print SERVER_01.test()
output :
<type 'str'>
hi
192.168.1.16
however , the problem arises when i use env.py file as --variable file with robotframework
command
pybot -V env.py Check.robot
Check.robot file is below
*** Settings ***
Library Wiresharking.py
*** Test Cases ***
Test
check
*** Keywords ***
check
${abc} = test
log ${abc}
the output i here getting is 'None'
16:13:37.279 INFO None
can anyone point out what wrong i am doing here.
Your env.py defines a single variable named ${SERVER_01}. However, Check.robot never uses that variable.
Check.robot imports Wiresharking.py without passing any arguments. That causes its self.window_ip to be None, and thus the keyword returns None.
If you want to see the values from env.py, you need to look at the ${SERVER_01} variable. For example:
log ${SERVER_01.window_ip}
This is the way i was able to pass **kwargs and able to resolve error .
I am still looking for a cleaner way to pass **kwargs
*** Settings ***
Library Wiresharking.py ip=${SERVER_01.window_ip} username=${SERVER_01.window_user} password=${SERVER_01.window_password} Target_path=${SERVER_01.dest_path} interface=${SERVER_01.interface} Home_path=${SERVER_01.home} WITH NAME Mylib
*** Variables ***
${window_ip }
#&{names} = Create Dictionary 0=First Name 2=Email
*** Test Cases ***
Test
check
*** Keywords ***
check
${abc} = test
log ${abc}
Output
INFO 192.168.1.16

Using Ansible variables in testinfra

Using TestInfra with Ansible backend for testing purposes. Everything goes fine except using Ansible itself while running tests
test.py
import pytest
def test_zabbix_agent_package(host):
package = host.package("zabbix-agent")
assert package.is_installed
package_version = host.ansible("debug", "msg={{ zabbix_agent_version }}")["msg"]
(...)
where zabbix_agent_version is an Ansible variable from group_vars. It can be obtained by running this playbook
- hosts: all
become: true
tasks:
- name: debug
debug: msg={{ zabbix_agent_version }}
command executing tests
pytest --connection=ansible --ansible-inventory=inventory --hosts=$hosts -v test.py
ansible.cfg
[defaults]
timeout = 10
host_key_checking = False
library=library/
retry_files_enabled = False
roles_path=roles/
pipelining=true
ConnectTimeout=60
remote_user=deploy
private_key_file=/opt/jenkins/.ssh/deploy
the output I get is
self = <ansible>, module_name = 'debug', module_args = 'msg={{ zabbix_agent_version }}', check = True, kwargs = {}
result = {'failed': True, 'msg': "the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'zabbix_agent_version' is undefined"}
def __call__(self, module_name, module_args=None, check=True, **kwargs):
if not self._host.backend.HAS_RUN_ANSIBLE:
raise RuntimeError((
"Ansible module is only available with ansible "
"connection backend"))
result = self._host.backend.run_ansible(
module_name, module_args, check=check, **kwargs)
if result.get("failed", False) is True:
> raise AnsibleException(result)
E AnsibleException: Unexpected error: {'failed': True,
E 'msg': u"the field 'args' has an invalid value, which appears to include a variable that is undefined. The error was: 'zabbix_agent_version' is undefined"}
/usr/lib/python2.7/site-packages/testinfra/modules/ansible.py:70: AnsibleException
Any idea why Ansible can't see this variable when running testinfra's Ansible module while it can see it while running Ansible alone?
If zabbix_agent_version is a variable set using group_vars, then it seems as if you should be accessing it using host.ansible.get_variables() rather than running debug task. In any case, both should work. If I have, in my current directory:
test_myvar.py
group_vars/
all.yml
And in group_vars/all.yml I have:
myvar: value
And in test_myvar.py I have:
def test_myvar_using_get_variables(host):
all_variables = host.ansible.get_variables()
assert 'myvar' in all_variables
assert all_variables['myvar'] == 'myvalue'
def test_myvar_using_debug_var(host):
result = host.ansible("debug", "var=myvar")
assert 'myvar' in result
assert result['myvar'] == 'myvalue'
def test_myvar_using_debug_msg(host):
result = host.ansible("debug", "msg={{ myvar }}")
assert 'msg' in result
assert result['msg'] == 'myvalue'
Then all tests pass:
$ py.test --connection=ansible --ansible-inventory=hosts -v
test_myvar.py
============================= test session starts ==============================
platform linux2 -- Python 2.7.13, pytest-3.2.3, py-1.4.34, pluggy-0.4.0 -- /home/lars/env/common/bin/python2
cachedir: .cache
rootdir: /home/lars/tmp/testinfra, inifile:
plugins: testinfra-1.8.1.dev2
collected 3 items
test_myvar.py::test_myvar_using_get_variables[ansible://localhost] PASSED
test_myvar.py::test_myvar_using_debug_var[ansible://localhost] PASSED
test_myvar.py::test_myvar_using_debug_msg[ansible://localhost] PASSED
=========================== 3 passed in 1.77 seconds ===========================
Can you confirm that the layout of our files (in particular, the location of your group_vars directory relative to the your tests) matches what I've shown here?
I chased an answer to this for days. Here's what finally worked for me. Essentially you are using testinfra's Ansible module to access the include_vars function of Ansible.
import pytest
#pytest.fixture()
def AnsibleVars(host):
ansible_vars = host.ansible(
"include_vars", "file=./group_vars/all/vars.yml")
return ansible_vars["ansible_facts"]
Then in my tests, I included the function as a parameter:
def test_something(host, AnsibleVars):
This solution was taken partially from https://github.com/metacloud/molecule/issues/151
I had an interesting issue where I was trying to include the variables from my main playbook and I was receiving an error of "must be stored as a dictionary/hash" when including the playbook.yml file. Separating the variables out into the group_vars/all/vars.yml file resolved that error.

Categories