634 lines
21 KiB
Python
634 lines
21 KiB
Python
|
#!/usr/bin/python
|
||
|
|
||
|
"""LogicMonitor Ansible module for managing Collectors, Hosts and Hostgroups
|
||
|
Copyright (C) 2015 LogicMonitor
|
||
|
|
||
|
This program 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.
|
||
|
|
||
|
This program 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 this program; if not, write to the Free Software Foundation,
|
||
|
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA"""
|
||
|
|
||
|
|
||
|
import socket
|
||
|
import sys
|
||
|
import types
|
||
|
import urllib
|
||
|
|
||
|
HAS_LIB_JSON = True
|
||
|
try:
|
||
|
import json
|
||
|
# Detect the python-json library which is incompatible
|
||
|
# Look for simplejson if that's the case
|
||
|
try:
|
||
|
if (
|
||
|
not isinstance(json.loads, types.FunctionType) or
|
||
|
not isinstance(json.dumps, types.FunctionType)
|
||
|
):
|
||
|
raise ImportError
|
||
|
except AttributeError:
|
||
|
raise ImportError
|
||
|
except ImportError:
|
||
|
try:
|
||
|
import simplejson as json
|
||
|
except ImportError:
|
||
|
print(
|
||
|
'\n{"msg": "Error: ansible requires the stdlib json or ' +
|
||
|
'simplejson module, neither was found!", "failed": true}'
|
||
|
)
|
||
|
HAS_LIB_JSON = False
|
||
|
except SyntaxError:
|
||
|
print(
|
||
|
'\n{"msg": "SyntaxError: probably due to installed simplejson ' +
|
||
|
'being for a different python version", "failed": true}'
|
||
|
)
|
||
|
HAS_LIB_JSON = False
|
||
|
|
||
|
|
||
|
DOCUMENTATION = '''
|
||
|
---
|
||
|
module: logicmonitor_facts
|
||
|
short_description: Collect facts about LogicMonitor objects
|
||
|
description:
|
||
|
- LogicMonitor is a hosted, full-stack, infrastructure monitoring platform.
|
||
|
- This module collects facts about hosts abd host groups within your LogicMonitor account.
|
||
|
version_added: "2.2"
|
||
|
author: Ethan Culler-Mayeno, Jeff Wozniak
|
||
|
notes:
|
||
|
- You must have an existing LogicMonitor account for this module to function.
|
||
|
requirements: ["An existing LogicMonitor account", "Linux"]
|
||
|
options:
|
||
|
target:
|
||
|
description:
|
||
|
- The LogicMonitor object you wish to manage.
|
||
|
required: true
|
||
|
default: null
|
||
|
choices: ['host', 'hostgroup']
|
||
|
company:
|
||
|
description:
|
||
|
- The LogicMonitor account company name. If you would log in to your account at "superheroes.logicmonitor.com" you would use "superheroes"
|
||
|
required: true
|
||
|
default: null
|
||
|
user:
|
||
|
description:
|
||
|
- A LogicMonitor user name. The module will authenticate and perform actions on behalf of this user
|
||
|
required: true
|
||
|
default: null
|
||
|
password:
|
||
|
description:
|
||
|
- The password for the chosen LogicMonitor User
|
||
|
- If an md5 hash is used, the digest flag must be set to true
|
||
|
required: true
|
||
|
default: null
|
||
|
collector:
|
||
|
description:
|
||
|
- The fully qualified domain name of a collector in your LogicMonitor account.
|
||
|
- This is optional for querying a LogicMonitor host when a displayname is specified
|
||
|
- This is required for querying a LogicMonitor host when a displayname is not specified
|
||
|
required: false
|
||
|
default: null
|
||
|
hostname:
|
||
|
description:
|
||
|
- The hostname of a host in your LogicMonitor account, or the desired hostname of a device to add into monitoring.
|
||
|
- Required for managing hosts (target=host)
|
||
|
required: false
|
||
|
default: 'hostname -f'
|
||
|
displayname:
|
||
|
description:
|
||
|
- The display name of a host in your LogicMonitor account or the desired display name of a device to add into monitoring.
|
||
|
required: false
|
||
|
default: 'hostname -f'
|
||
|
fullpath:
|
||
|
description:
|
||
|
- The fullpath of the hostgroup object you would like to manage
|
||
|
- Recommend running on a single ansible host
|
||
|
- Required for management of LogicMonitor host groups (target=hostgroup)
|
||
|
required: false
|
||
|
default: null
|
||
|
...
|
||
|
'''
|
||
|
|
||
|
EXAMPLES = '''
|
||
|
#example of querying a list of hosts
|
||
|
```
|
||
|
---
|
||
|
- hosts: hosts
|
||
|
user: root
|
||
|
vars:
|
||
|
company: 'yourcompany'
|
||
|
user: 'Luigi'
|
||
|
password: 'ImaLuigi,number1!'
|
||
|
tasks:
|
||
|
- name: query a list of hosts
|
||
|
# All tasks should use local_action
|
||
|
local_action:
|
||
|
logicmonitor_facts:
|
||
|
target: host
|
||
|
company: '{{ company }}'
|
||
|
user: '{{ user }}'
|
||
|
password: '{{ password }}'
|
||
|
```
|
||
|
|
||
|
#example of querying a hostgroup
|
||
|
```
|
||
|
---
|
||
|
- hosts: somemachine.superheroes.com
|
||
|
user: root
|
||
|
vars:
|
||
|
company: 'yourcompany'
|
||
|
user: 'mario'
|
||
|
password: 'itsame.Mario!'
|
||
|
tasks:
|
||
|
- name: query a host group
|
||
|
# All tasks should use local_action
|
||
|
local_action:
|
||
|
logicmonitor_facts:
|
||
|
target: hostgroup
|
||
|
fullpath: '/servers/production'
|
||
|
company: '{{ company }}'
|
||
|
user: '{{ user }}'
|
||
|
password: '{{ password }}'
|
||
|
```
|
||
|
'''
|
||
|
|
||
|
|
||
|
RETURN = '''
|
||
|
---
|
||
|
ansible_facts:
|
||
|
description: LogicMonitor properties set for the specified object
|
||
|
returned: success
|
||
|
type: list of dicts containing name/value pairs
|
||
|
example: >
|
||
|
{
|
||
|
"name": "dc",
|
||
|
"value": "1"
|
||
|
},
|
||
|
{
|
||
|
"name": "type",
|
||
|
"value": "prod"
|
||
|
},
|
||
|
{
|
||
|
"name": "system.categories",
|
||
|
"value": ""
|
||
|
},
|
||
|
{
|
||
|
"name": "snmp.community",
|
||
|
"value": "********"
|
||
|
}
|
||
|
...
|
||
|
'''
|
||
|
|
||
|
|
||
|
class LogicMonitor(object):
|
||
|
|
||
|
def __init__(self, module, **params):
|
||
|
self.__version__ = "1.0-python"
|
||
|
self.module = module
|
||
|
self.module.debug("Instantiating LogicMonitor object")
|
||
|
|
||
|
self.check_mode = False
|
||
|
self.company = params["company"]
|
||
|
self.user = params["user"]
|
||
|
self.password = params["password"]
|
||
|
self.fqdn = socket.getfqdn()
|
||
|
self.lm_url = "logicmonitor.com/santaba"
|
||
|
self.urlopen = open_url # use the ansible provided open_url
|
||
|
self.__version__ = self.__version__ + "-ansible-module"
|
||
|
|
||
|
def rpc(self, action, params):
|
||
|
"""Make a call to the LogicMonitor RPC library
|
||
|
and return the response"""
|
||
|
self.module.debug("Running LogicMonitor.rpc")
|
||
|
|
||
|
param_str = urllib.urlencode(params)
|
||
|
creds = urllib.urlencode(
|
||
|
{"c": self.company,
|
||
|
"u": self.user,
|
||
|
"p": self.password})
|
||
|
|
||
|
if param_str:
|
||
|
param_str = param_str + "&"
|
||
|
|
||
|
param_str = param_str + creds
|
||
|
|
||
|
try:
|
||
|
url = ("https://" + self.company + "." + self.lm_url +
|
||
|
"/rpc/" + action + "?" + param_str)
|
||
|
|
||
|
# Set custom LogicMonitor header with version
|
||
|
headers = {"X-LM-User-Agent": self.__version__}
|
||
|
|
||
|
# Set headers
|
||
|
f = self.urlopen(url, headers=headers)
|
||
|
|
||
|
raw = f.read()
|
||
|
resp = json.loads(raw)
|
||
|
if resp["status"] == 403:
|
||
|
self.module.debug("Authentication failed.")
|
||
|
self.fail(msg="Error: " + resp["errmsg"])
|
||
|
else:
|
||
|
return raw
|
||
|
except IOError:
|
||
|
self.fail(msg="Error: Unknown exception making RPC call")
|
||
|
|
||
|
def get_collectors(self):
|
||
|
"""Returns a JSON object containing a list of
|
||
|
LogicMonitor collectors"""
|
||
|
self.module.debug("Running LogicMonitor.get_collectors...")
|
||
|
|
||
|
self.module.debug("Making RPC call to 'getAgents'")
|
||
|
resp = self.rpc("getAgents", {})
|
||
|
resp_json = json.loads(resp)
|
||
|
|
||
|
if resp_json["status"] is 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
return resp_json["data"]
|
||
|
else:
|
||
|
self.fail(msg=resp)
|
||
|
|
||
|
def get_host_by_hostname(self, hostname, collector):
|
||
|
"""Returns a host object for the host matching the
|
||
|
specified hostname"""
|
||
|
self.module.debug("Running LogicMonitor.get_host_by_hostname...")
|
||
|
|
||
|
self.module.debug("Looking for hostname " + hostname)
|
||
|
self.module.debug("Making RPC call to 'getHosts'")
|
||
|
hostlist_json = json.loads(self.rpc("getHosts", {"hostGroupId": 1}))
|
||
|
|
||
|
if collector:
|
||
|
if hostlist_json["status"] == 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
|
||
|
hosts = hostlist_json["data"]["hosts"]
|
||
|
|
||
|
self.module.debug(
|
||
|
"Looking for host matching: hostname " + hostname +
|
||
|
" and collector " + str(collector["id"]))
|
||
|
|
||
|
for host in hosts:
|
||
|
if (host["hostName"] == hostname and
|
||
|
host["agentId"] == collector["id"]):
|
||
|
|
||
|
self.module.debug("Host match found")
|
||
|
return host
|
||
|
self.module.debug("No host match found")
|
||
|
return None
|
||
|
else:
|
||
|
self.module.debug("RPC call failed")
|
||
|
self.module.debug(hostlist_json)
|
||
|
else:
|
||
|
self.module.debug("No collector specified")
|
||
|
return None
|
||
|
|
||
|
def get_host_by_displayname(self, displayname):
|
||
|
"""Returns a host object for the host matching the
|
||
|
specified display name"""
|
||
|
self.module.debug("Running LogicMonitor.get_host_by_displayname...")
|
||
|
|
||
|
self.module.debug("Looking for displayname " + displayname)
|
||
|
self.module.debug("Making RPC call to 'getHost'")
|
||
|
host_json = (json.loads(self.rpc("getHost",
|
||
|
{"displayName": displayname})))
|
||
|
|
||
|
if host_json["status"] == 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
return host_json["data"]
|
||
|
else:
|
||
|
self.module.debug("RPC call failed")
|
||
|
self.module.debug(host_json)
|
||
|
return None
|
||
|
|
||
|
def get_collector_by_description(self, description):
|
||
|
"""Returns a JSON collector object for the collector
|
||
|
matching the specified FQDN (description)"""
|
||
|
self.module.debug(
|
||
|
"Running LogicMonitor.get_collector_by_description..."
|
||
|
)
|
||
|
|
||
|
collector_list = self.get_collectors()
|
||
|
if collector_list is not None:
|
||
|
self.module.debug("Looking for collector with description " +
|
||
|
description)
|
||
|
for collector in collector_list:
|
||
|
if collector["description"] == description:
|
||
|
self.module.debug("Collector match found")
|
||
|
return collector
|
||
|
self.module.debug("No collector match found")
|
||
|
return None
|
||
|
|
||
|
def get_group(self, fullpath):
|
||
|
"""Returns a JSON group object for the group matching the
|
||
|
specified path"""
|
||
|
self.module.debug("Running LogicMonitor.get_group...")
|
||
|
|
||
|
self.module.debug("Making RPC call to getHostGroups")
|
||
|
resp = json.loads(self.rpc("getHostGroups", {}))
|
||
|
|
||
|
if resp["status"] == 200:
|
||
|
self.module.debug("RPC called succeeded")
|
||
|
groups = resp["data"]
|
||
|
|
||
|
self.module.debug("Looking for group matching " + fullpath)
|
||
|
for group in groups:
|
||
|
if group["fullPath"] == fullpath.lstrip('/'):
|
||
|
self.module.debug("Group match found")
|
||
|
return group
|
||
|
|
||
|
self.module.debug("No group match found")
|
||
|
return None
|
||
|
else:
|
||
|
self.module.debug("RPC call failed")
|
||
|
self.module.debug(resp)
|
||
|
|
||
|
return None
|
||
|
|
||
|
def create_group(self, fullpath):
|
||
|
"""Recursively create a path of host groups.
|
||
|
Returns the id of the newly created hostgroup"""
|
||
|
self.module.debug("Running LogicMonitor.create_group...")
|
||
|
|
||
|
res = self.get_group(fullpath)
|
||
|
if res:
|
||
|
self.module.debug("Group " + fullpath + " exists.")
|
||
|
return res["id"]
|
||
|
|
||
|
if fullpath == "/":
|
||
|
self.module.debug("Specified group is root. Doing nothing.")
|
||
|
return 1
|
||
|
else:
|
||
|
self.module.debug("Creating group named " + fullpath)
|
||
|
self.module.debug("System changed")
|
||
|
self.change = True
|
||
|
|
||
|
if self.check_mode:
|
||
|
self.exit(changed=True)
|
||
|
|
||
|
parentpath, name = fullpath.rsplit('/', 1)
|
||
|
parentgroup = self.get_group(parentpath)
|
||
|
|
||
|
parentid = 1
|
||
|
|
||
|
if parentpath == "":
|
||
|
parentid = 1
|
||
|
elif parentgroup:
|
||
|
parentid = parentgroup["id"]
|
||
|
else:
|
||
|
parentid = self.create_group(parentpath)
|
||
|
|
||
|
h = None
|
||
|
|
||
|
# Determine if we're creating a group from host or hostgroup class
|
||
|
if hasattr(self, '_build_host_group_hash'):
|
||
|
h = self._build_host_group_hash(
|
||
|
fullpath,
|
||
|
self.description,
|
||
|
self.properties,
|
||
|
self.alertenable)
|
||
|
h["name"] = name
|
||
|
h["parentId"] = parentid
|
||
|
else:
|
||
|
h = {"name": name,
|
||
|
"parentId": parentid,
|
||
|
"alertEnable": True,
|
||
|
"description": ""}
|
||
|
|
||
|
self.module.debug("Making RPC call to 'addHostGroup'")
|
||
|
resp = json.loads(
|
||
|
self.rpc("addHostGroup", h))
|
||
|
|
||
|
if resp["status"] == 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
return resp["data"]["id"]
|
||
|
elif resp["errmsg"] == "The record already exists":
|
||
|
self.module.debug("The hostgroup already exists")
|
||
|
group = self.get_group(fullpath)
|
||
|
return group["id"]
|
||
|
else:
|
||
|
self.module.debug("RPC call failed")
|
||
|
self.fail(
|
||
|
msg="Error: unable to create new hostgroup \"" + name +
|
||
|
"\".\n" + resp["errmsg"])
|
||
|
|
||
|
def fail(self, msg):
|
||
|
self.module.fail_json(msg=msg, changed=self.change)
|
||
|
|
||
|
def exit(self, changed):
|
||
|
self.module.debug("Changed: " + changed)
|
||
|
self.module.exit_json(changed=changed)
|
||
|
|
||
|
def output_info(self, info):
|
||
|
self.module.debug("Registering properties as Ansible facts")
|
||
|
self.module.exit_json(changed=False, ansible_facts=info)
|
||
|
|
||
|
|
||
|
class Host(LogicMonitor):
|
||
|
|
||
|
def __init__(self, params, module=None):
|
||
|
"""Initializor for the LogicMonitor host object"""
|
||
|
self.change = False
|
||
|
self.params = params
|
||
|
self.collector = None
|
||
|
|
||
|
LogicMonitor.__init__(self, module, **self.params)
|
||
|
self.module.debug("Instantiating Host object")
|
||
|
|
||
|
if self.params["hostname"]:
|
||
|
self.module.debug("Hostname is " + self.params["hostname"])
|
||
|
self.hostname = self.params['hostname']
|
||
|
else:
|
||
|
self.module.debug("No hostname specified. Using " + self.fqdn)
|
||
|
self.hostname = self.fqdn
|
||
|
|
||
|
if self.params["displayname"]:
|
||
|
self.module.debug("Display name is " + self.params["displayname"])
|
||
|
self.displayname = self.params['displayname']
|
||
|
else:
|
||
|
self.module.debug("No display name specified. Using " + self.fqdn)
|
||
|
self.displayname = self.fqdn
|
||
|
|
||
|
# Attempt to host information via display name of host name
|
||
|
self.module.debug("Attempting to find host by displayname " +
|
||
|
self.displayname)
|
||
|
info = self.get_host_by_displayname(self.displayname)
|
||
|
|
||
|
if info is not None:
|
||
|
self.module.debug("Host found by displayname")
|
||
|
# Used the host information to grab the collector description
|
||
|
# if not provided
|
||
|
if (not hasattr(self.params, "collector") and
|
||
|
"agentDescription" in info):
|
||
|
self.module.debug("Setting collector from host response. " +
|
||
|
"Collector " + info["agentDescription"])
|
||
|
self.params["collector"] = info["agentDescription"]
|
||
|
else:
|
||
|
self.module.debug("Host not found by displayname")
|
||
|
|
||
|
# At this point, a valid collector description is required for success
|
||
|
# Check that the description exists or fail
|
||
|
if self.params["collector"]:
|
||
|
self.module.debug("Collector specified is " +
|
||
|
self.params["collector"])
|
||
|
self.collector = (self.get_collector_by_description(
|
||
|
self.params["collector"]))
|
||
|
else:
|
||
|
self.fail(msg="No collector specified.")
|
||
|
|
||
|
# If the host wasn't found via displayname, attempt by hostname
|
||
|
if info is None:
|
||
|
self.module.debug("Attempting to find host by hostname " +
|
||
|
self.hostname)
|
||
|
info = self.get_host_by_hostname(self.hostname, self.collector)
|
||
|
|
||
|
self.info = info
|
||
|
|
||
|
def get_properties(self):
|
||
|
"""Returns a hash of the properties
|
||
|
associated with this LogicMonitor host"""
|
||
|
self.module.debug("Running Host.get_properties...")
|
||
|
|
||
|
if self.info:
|
||
|
self.module.debug("Making RPC call to 'getHostProperties'")
|
||
|
properties_json = (json.loads(self.rpc("getHostProperties",
|
||
|
{'hostId': self.info["id"],
|
||
|
"filterSystemProperties": True})))
|
||
|
|
||
|
if properties_json["status"] == 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
return properties_json["data"]
|
||
|
else:
|
||
|
self.module.debug("Error: there was an issue retrieving the " +
|
||
|
"host properties")
|
||
|
self.module.debug(properties_json["errmsg"])
|
||
|
|
||
|
self.fail(msg=properties_json["status"])
|
||
|
else:
|
||
|
self.module.debug(
|
||
|
"Unable to find LogicMonitor host which matches " +
|
||
|
self.displayname + " (" + self.hostname + ")"
|
||
|
)
|
||
|
return None
|
||
|
|
||
|
def site_facts(self):
|
||
|
"""Output current properties information for the Host"""
|
||
|
self.module.debug("Running Host.site_facts...")
|
||
|
|
||
|
if self.info:
|
||
|
self.module.debug("Host exists")
|
||
|
props = self.get_properties()
|
||
|
|
||
|
self.output_info(props)
|
||
|
else:
|
||
|
self.fail(msg="Error: Host doesn't exit.")
|
||
|
|
||
|
|
||
|
class Hostgroup(LogicMonitor):
|
||
|
|
||
|
def __init__(self, params, module=None):
|
||
|
"""Initializor for the LogicMonitor host object"""
|
||
|
self.change = False
|
||
|
self.params = params
|
||
|
|
||
|
LogicMonitor.__init__(self, module, **self.params)
|
||
|
self.module.debug("Instantiating Hostgroup object")
|
||
|
|
||
|
self.fullpath = self.params["fullpath"]
|
||
|
self.info = self.get_group(self.fullpath)
|
||
|
|
||
|
def get_properties(self, final=False):
|
||
|
"""Returns a hash of the properties
|
||
|
associated with this LogicMonitor host"""
|
||
|
self.module.debug("Running Hostgroup.get_properties...")
|
||
|
|
||
|
if self.info:
|
||
|
self.module.debug("Group found")
|
||
|
|
||
|
self.module.debug("Making RPC call to 'getHostGroupProperties'")
|
||
|
properties_json = json.loads(self.rpc(
|
||
|
"getHostGroupProperties",
|
||
|
{'hostGroupId': self.info["id"],
|
||
|
"finalResult": final}))
|
||
|
|
||
|
if properties_json["status"] == 200:
|
||
|
self.module.debug("RPC call succeeded")
|
||
|
return properties_json["data"]
|
||
|
else:
|
||
|
self.module.debug("RPC call failed")
|
||
|
self.fail(msg=properties_json["status"])
|
||
|
else:
|
||
|
self.module.debug("Group not found")
|
||
|
return None
|
||
|
|
||
|
def site_facts(self):
|
||
|
"""Output current properties information for the Hostgroup"""
|
||
|
self.module.debug("Running Hostgroup.site_facts...")
|
||
|
|
||
|
if self.info:
|
||
|
self.module.debug("Group exists")
|
||
|
props = self.get_properties(True)
|
||
|
|
||
|
self.output_info(props)
|
||
|
else:
|
||
|
self.fail(msg="Error: Group doesn't exit.")
|
||
|
|
||
|
|
||
|
def selector(module):
|
||
|
"""Figure out which object and which actions
|
||
|
to take given the right parameters"""
|
||
|
|
||
|
if module.params["target"] == "host":
|
||
|
target = Host(module.params, module)
|
||
|
target.site_facts()
|
||
|
elif module.params["target"] == "hostgroup":
|
||
|
# Validate target specific required parameters
|
||
|
if module.params["fullpath"] is not None:
|
||
|
target = Hostgroup(module.params, module)
|
||
|
target.site_facts()
|
||
|
else:
|
||
|
module.fail_json(
|
||
|
msg="Parameter 'fullpath' required for target 'hostgroup'")
|
||
|
else:
|
||
|
module.fail_json(
|
||
|
msg="Error: Unexpected target \"" + module.params["target"] +
|
||
|
"\" was specified.")
|
||
|
|
||
|
|
||
|
def main():
|
||
|
TARGETS = [
|
||
|
"host",
|
||
|
"hostgroup"]
|
||
|
|
||
|
module = AnsibleModule(
|
||
|
argument_spec=dict(
|
||
|
target=dict(required=True, default=None, choices=TARGETS),
|
||
|
company=dict(required=True, default=None),
|
||
|
user=dict(required=True, default=None),
|
||
|
password=dict(required=True, default=None, no_log=True),
|
||
|
|
||
|
collector=dict(require=False, default=None),
|
||
|
hostname=dict(required=False, default=None),
|
||
|
displayname=dict(required=False, default=None),
|
||
|
fullpath=dict(required=False, default=None)
|
||
|
),
|
||
|
supports_check_mode=True
|
||
|
)
|
||
|
|
||
|
if HAS_LIB_JSON is not True:
|
||
|
module.fail_json(msg="Unable to load JSON library")
|
||
|
|
||
|
selector(module)
|
||
|
|
||
|
from ansible.module_utils.basic import *
|
||
|
from ansible.module_utils.urls import *
|
||
|
from ansible.module_utils.urls import open_url
|
||
|
|
||
|
if __name__ == "__main__":
|
||
|
main()
|