Remove modules which have ended their deprecation cycle

* Remove code but leave the metadata so that they can be listed as
  removed in documentation.
* Remove removed modules from validate-modules ignore
* Remove unittests for the removed nodules
* Remove links to removed modules and add list of removed moduels to the
  2.9 porting guide
This commit is contained in:
Toshio Kuratomi 2019-04-11 17:39:13 -07:00
parent e5a31e81b6
commit a1c8fc37e8
29 changed files with 135 additions and 9130 deletions

View file

@ -7,422 +7,12 @@ from __future__ import absolute_import, division, print_function
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['deprecated'],
'status': ['removed'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: kubernetes
version_added: "2.1"
deprecated:
removed_in: "2.9"
why: This module used the oc command line tool, where as M(k8s_raw) goes over the REST API.
alternative: Use M(k8s_raw) instead.
short_description: Manage Kubernetes resources
description:
- This module can manage Kubernetes resources on an existing cluster using
the Kubernetes server API. Users can specify in-line API data, or
specify an existing Kubernetes YAML file.
- Currently, this module
(1) Only supports HTTP Basic Auth
(2) Only supports 'strategic merge' for update, http://goo.gl/fCPYxT
SSL certs are not working, use C(validate_certs=off) to disable.
options:
api_endpoint:
description:
- The IPv4 API endpoint of the Kubernetes cluster.
required: true
aliases: [ endpoint ]
inline_data:
description:
- The Kubernetes YAML data to send to the API I(endpoint). This option is
mutually exclusive with C('file_reference').
required: true
file_reference:
description:
- Specify full path to a Kubernets YAML file to send to API I(endpoint).
This option is mutually exclusive with C('inline_data').
patch_operation:
description:
- Specify patch operation for Kubernetes resource update.
- For details, see the description of PATCH operations at
U(https://github.com/kubernetes/kubernetes/blob/release-1.5/docs/devel/api-conventions.md#patch-operations).
default: Strategic Merge Patch
choices: [ JSON Patch, Merge Patch, Strategic Merge Patch ]
aliases: [ patch_strategy ]
version_added: 2.4
certificate_authority_data:
description:
- Certificate Authority data for Kubernetes server. Should be in either
standard PEM format or base64 encoded PEM data. Note that certificate
verification is broken until ansible supports a version of
'match_hostname' that can match the IP address against the CA data.
state:
description:
- The desired action to take on the Kubernetes data.
required: true
choices: [ absent, present, replace, update ]
default: present
url_password:
description:
- The HTTP Basic Auth password for the API I(endpoint). This should be set
unless using the C('insecure') option.
aliases: [ password ]
url_username:
description:
- The HTTP Basic Auth username for the API I(endpoint). This should be set
unless using the C('insecure') option.
default: admin
aliases: [ username ]
insecure:
description:
- Reverts the connection to using HTTP instead of HTTPS. This option should
only be used when execuing the M('kubernetes') module local to the Kubernetes
cluster using the insecure local port (locahost:8080 by default).
validate_certs:
description:
- Enable/disable certificate validation. Note that this is set to
C(false) until Ansible can support IP address based certificate
hostname matching (exists in >= python3.5.0).
type: bool
default: 'no'
author:
- Eric Johnson (@erjohnso) <erjohnso@google.com>
'''
EXAMPLES = '''
# Create a new namespace with in-line YAML.
- name: Create a kubernetes namespace
kubernetes:
api_endpoint: 123.45.67.89
url_username: admin
url_password: redacted
inline_data:
kind: Namespace
apiVersion: v1
metadata:
name: ansible-test
labels:
label_env: production
label_ver: latest
annotations:
a1: value1
a2: value2
state: present
# Create a new namespace from a YAML file.
- name: Create a kubernetes namespace
kubernetes:
api_endpoint: 123.45.67.89
url_username: admin
url_password: redacted
file_reference: /path/to/create_namespace.yaml
state: present
# Do the same thing, but using the insecure localhost port
- name: Create a kubernetes namespace
kubernetes:
api_endpoint: 123.45.67.89
insecure: true
file_reference: /path/to/create_namespace.yaml
state: present
'''
RETURN = '''
# Example response from creating a Kubernetes Namespace.
api_response:
description: Raw response from Kubernetes API, content varies with API.
returned: success
type: complex
contains:
apiVersion: "v1"
kind: "Namespace"
metadata:
creationTimestamp: "2016-01-04T21:16:32Z"
name: "test-namespace"
resourceVersion: "509635"
selfLink: "/api/v1/namespaces/test-namespace"
uid: "6dbd394e-b328-11e5-9a02-42010af0013a"
spec:
finalizers:
- kubernetes
status:
phase: "Active"
'''
import base64
import json
import traceback
YAML_IMP_ERR = None
try:
import yaml
HAS_LIB_YAML = True
except ImportError:
YAML_IMP_ERR = traceback.format_exc()
HAS_LIB_YAML = False
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
from ansible.module_utils.urls import fetch_url
############################################################################
############################################################################
# For API coverage, this Anislbe module provides capability to operate on
# all Kubernetes objects that support a "create" call (except for 'Events').
# In order to obtain a valid list of Kubernetes objects, the v1 spec file
# was referenced and the below python script was used to parse the JSON
# spec file, extract only the objects with a description starting with
# 'create a'. The script then iterates over all of these base objects
# to get the endpoint URL and was used to generate the KIND_URL map.
#
# import json
# from urllib2 import urlopen
#
# r = urlopen("https://raw.githubusercontent.com/kubernetes"
# "/kubernetes/master/api/swagger-spec/v1.json")
# v1 = json.load(r)
#
# apis = {}
# for a in v1['apis']:
# p = a['path']
# for o in a['operations']:
# if o["summary"].startswith("create a") and o["type"] != "v1.Event":
# apis[o["type"]] = p
#
# def print_kind_url_map():
# results = []
# for a in apis.keys():
# results.append('"%s": "%s"' % (a[3:].lower(), apis[a]))
# results.sort()
# print("KIND_URL = {")
# print(",\n".join(results))
# print("}")
#
# if __name__ == '__main__':
# print_kind_url_map()
############################################################################
############################################################################
KIND_URL = {
"binding": "/api/v1/namespaces/{namespace}/bindings",
"configmap": "/api/v1/namespaces/{namespace}/configmaps",
"endpoints": "/api/v1/namespaces/{namespace}/endpoints",
"limitrange": "/api/v1/namespaces/{namespace}/limitranges",
"namespace": "/api/v1/namespaces",
"node": "/api/v1/nodes",
"persistentvolume": "/api/v1/persistentvolumes",
"persistentvolumeclaim": "/api/v1/namespaces/{namespace}/persistentvolumeclaims", # NOQA
"pod": "/api/v1/namespaces/{namespace}/pods",
"podtemplate": "/api/v1/namespaces/{namespace}/podtemplates",
"replicationcontroller": "/api/v1/namespaces/{namespace}/replicationcontrollers", # NOQA
"resourcequota": "/api/v1/namespaces/{namespace}/resourcequotas",
"secret": "/api/v1/namespaces/{namespace}/secrets",
"service": "/api/v1/namespaces/{namespace}/services",
"serviceaccount": "/api/v1/namespaces/{namespace}/serviceaccounts",
"daemonset": "/apis/extensions/v1beta1/namespaces/{namespace}/daemonsets",
"deployment": "/apis/extensions/v1beta1/namespaces/{namespace}/deployments",
"horizontalpodautoscaler": "/apis/extensions/v1beta1/namespaces/{namespace}/horizontalpodautoscalers", # NOQA
"ingress": "/apis/extensions/v1beta1/namespaces/{namespace}/ingresses",
"job": "/apis/extensions/v1beta1/namespaces/{namespace}/jobs",
}
USER_AGENT = "ansible-k8s-module/0.0.1"
# TODO(erjohnso): SSL Certificate validation is currently unsupported.
# It can be made to work when the following are true:
# - Ansible consistently uses a "match_hostname" that supports IP Address
# matching. This is now true in >= python3.5.0. Currently, this feature
# is not yet available in backports.ssl_match_hostname (still 3.4).
# - Ansible allows passing in the self-signed CA cert that is created with
# a kubernetes master. The lib/ansible/module_utils/urls.py method,
# SSLValidationHandler.get_ca_certs() needs a way for the Kubernetes
# CA cert to be passed in and included in the generated bundle file.
# When this is fixed, the following changes can be made to this module,
# - Remove the 'return' statement in line 254 below
# - Set 'required=true' for certificate_authority_data and ensure that
# ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert
# - Set 'required=true' for the validate_certs param.
def decode_cert_data(module):
return
# pylint: disable=unreachable
d = module.params.get("certificate_authority_data")
if d and not d.startswith("-----BEGIN"):
module.params["certificate_authority_data"] = base64.b64decode(d)
def api_request(module, url, method="GET", headers=None, data=None):
body = None
if data:
data = json.dumps(data)
response, info = fetch_url(module, url, method=method, headers=headers, data=data)
if int(info['status']) == -1:
module.fail_json(msg="Failed to execute the API request: %s" % info['msg'], url=url, method=method, headers=headers)
if response is not None:
body = json.loads(response.read())
return info, body
def k8s_create_resource(module, url, data):
info, body = api_request(module, url, method="POST", data=data, headers={"Content-Type": "application/json"})
if info['status'] == 409:
name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name)
return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to create the resource: %s" % info['msg'], url=url)
return True, body
def k8s_delete_resource(module, url, data):
name = data.get('metadata', {}).get('name')
if name is None:
module.fail_json(msg="Missing a named resource in object metadata when trying to remove a resource")
url = url + '/' + name
info, body = api_request(module, url, method="DELETE")
if info['status'] == 404:
return False, "Resource name '%s' already absent" % name
elif info['status'] >= 400:
module.fail_json(msg="failed to delete the resource '%s': %s" % (name, info['msg']), url=url)
return True, "Successfully deleted resource name '%s'" % name
def k8s_replace_resource(module, url, data):
name = data.get('metadata', {}).get('name')
if name is None:
module.fail_json(msg="Missing a named resource in object metadata when trying to replace a resource")
headers = {"Content-Type": "application/json"}
url = url + '/' + name
info, body = api_request(module, url, method="PUT", data=data, headers=headers)
if info['status'] == 409:
name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name)
return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to replace the resource '%s': %s" % (name, info['msg']), url=url)
return True, body
def k8s_update_resource(module, url, data, patch_operation):
# PATCH operations are explained in details at:
# https://github.com/kubernetes/kubernetes/blob/release-1.5/docs/devel/api-conventions.md#patch-operations
PATCH_OPERATIONS_MAP = {
'JSON Patch': 'application/json-patch+json',
'Merge Patch': 'application/merge-patch+json',
'Strategic Merge Patch': 'application/strategic-merge-patch+json',
}
name = data.get('metadata', {}).get('name')
if name is None:
module.fail_json(msg="Missing a named resource in object metadata when trying to update a resource")
headers = {"Content-Type": PATCH_OPERATIONS_MAP[patch_operation]}
url = url + '/' + name
info, body = api_request(module, url, method="PATCH", data=data, headers=headers)
if info['status'] == 409:
name = data["metadata"].get("name", None)
info, body = api_request(module, url + "/" + name)
return False, body
elif info['status'] >= 400:
module.fail_json(msg="failed to update the resource '%s': %s" % (name, info['msg']), url=url)
return True, body
def main():
module = AnsibleModule(
argument_spec=dict(
http_agent=dict(type='str', default=USER_AGENT),
url_username=dict(type='str', default='admin', aliases=['username']),
url_password=dict(type='str', default='', no_log=True, aliases=['password']),
force_basic_auth=dict(type='bool', default=True),
validate_certs=dict(type='bool', default=False),
certificate_authority_data=dict(type='str'),
insecure=dict(type='bool', default=False),
api_endpoint=dict(type='str', required=True),
patch_operation=dict(type='str', default='Strategic Merge Patch', aliases=['patch_strategy'],
choices=['JSON Patch', 'Merge Patch', 'Strategic Merge Patch']),
file_reference=dict(type='str'),
inline_data=dict(type='str'),
state=dict(type='str', default='present', choices=['absent', 'present', 'replace', 'update'])
),
mutually_exclusive=(('file_reference', 'inline_data'),
('url_username', 'insecure'),
('url_password', 'insecure')),
required_one_of=(('file_reference', 'inline_data'),),
)
if not HAS_LIB_YAML:
module.fail_json(msg=missing_required_lib('PyYAML'), exception=YAML_IMP_ERR)
decode_cert_data(module)
api_endpoint = module.params.get('api_endpoint')
state = module.params.get('state')
insecure = module.params.get('insecure')
inline_data = module.params.get('inline_data')
file_reference = module.params.get('file_reference')
patch_operation = module.params.get('patch_operation')
if inline_data:
if not isinstance(inline_data, dict) and not isinstance(inline_data, list):
data = yaml.safe_load(inline_data)
else:
data = inline_data
else:
try:
f = open(file_reference, "r")
data = [x for x in yaml.safe_load_all(f)]
f.close()
if not data:
module.fail_json(msg="No valid data could be found.")
except Exception:
module.fail_json(msg="The file '%s' was not found or contained invalid YAML/JSON data" % file_reference)
# set the transport type and build the target endpoint url
transport = 'https'
if insecure:
transport = 'http'
target_endpoint = "%s://%s" % (transport, api_endpoint)
body = []
changed = False
# make sure the data is a list
if not isinstance(data, list):
data = [data]
for item in data:
namespace = "default"
if item and 'metadata' in item:
namespace = item.get('metadata', {}).get('namespace', "default")
kind = item.get('kind', '').lower()
try:
url = target_endpoint + KIND_URL[kind]
except KeyError:
module.fail_json(msg="invalid resource kind specified in the data: '%s'" % kind)
url = url.replace("{namespace}", namespace)
else:
url = target_endpoint
if state == 'present':
item_changed, item_body = k8s_create_resource(module, url, item)
elif state == 'absent':
item_changed, item_body = k8s_delete_resource(module, url, item)
elif state == 'replace':
item_changed, item_body = k8s_replace_resource(module, url, item)
elif state == 'update':
item_changed, item_body = k8s_update_resource(module, url, item, patch_operation)
changed |= item_changed
body.append(item_body)
module.exit_json(changed=changed, api_response=body)
from ansible.module_utils.common.removed import removed_module
if __name__ == '__main__':
main()
removed_module(removed_in='2.9')

View file

@ -9,457 +9,13 @@ __metaclass__ = type
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['deprecated'],
'status': ['removed'],
'supported_by': 'community'
}
DOCUMENTATION = """
author:
- "Kenneth D. Evensen (@kevensen)"
deprecated:
removed_in: "2.9"
why: This module used the oc command line tool, where as M(openshift_raw) goes over the REST API.
alternative: Use M(openshift_raw) instead.
description:
- This module allows management of resources in an OpenShift cluster. The
inventory host can be any host with network connectivity to the OpenShift
cluster; the default port being 8443/TCP.
- This module relies on a token to authenticate to OpenShift. This can either
be a user or a service account.
module: oc
options:
host:
description:
- "Hostname or address of the OpenShift API endpoint. By default, this is expected to be the current inventory host."
required: false
default: 127.0.0.1
port:
description:
- "The port number of the API endpoint."
required: false
default: 8443
inline:
description:
- "The inline definition of the resource. This is mutually exclusive with name, namespace and kind."
required: false
aliases: ['def', 'definition']
kind:
description: The kind of the resource upon which to take action.
required: true
name:
description:
- "The name of the resource on which to take action."
required: false
namespace:
description:
- "The namespace of the resource upon which to take action."
required: false
token:
description:
- "The token with which to authenticate against the OpenShift cluster."
required: true
validate_certs:
description:
- If C(no), SSL certificates for the target url will not be validated.
This should only be used on personally controlled sites using
self-signed certificates.
type: bool
default: yes
state:
choices:
- present
- absent
description:
- "If the state is present, and the resource doesn't exist, it shall be created using the inline definition. If the state is present and the
resource exists, the definition will be updated, again using an inline definition. If the state is absent, the resource will be deleted if it exists."
required: true
short_description: Manage OpenShift Resources
version_added: 2.4
"""
EXAMPLES = """
- name: Create project
oc:
state: present
inline:
kind: ProjectRequest
metadata:
name: ansibletestproject
displayName: Ansible Test Project
description: This project was created using Ansible
token: << redacted >>
- name: Delete a service
oc:
state: absent
name: myservice
namespace: mynamespace
kind: Service
token: << redacted >>
- name: Add project role Admin to a user
oc:
state: present
inline:
kind: RoleBinding
metadata:
name: admin
namespace: mynamespace
roleRef:
name: admin
userNames:
- "myuser"
token: << redacted >>
- name: Obtain an object definition
oc:
state: present
name: myroute
namespace: mynamespace
kind: Route
token: << redacted >>
"""
RETURN = '''
result:
description:
The resource that was created, changed, or otherwise determined to be present.
In the case of a deletion, this is the response from the delete request.
returned: success
type: str
url:
description: The URL to the requested resource.
returned: success
type: str
method:
description: The HTTP method that was used to take action upon the resource
returned: success
type: str
...
'''
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils import urls
class ApiEndpoint(object):
def __init__(self, host, port, api, version):
self.host = host
self.port = port
self.api = api
self.version = version
def __str__(self):
url = "https://"
url += self.host
url += ":"
url += str(self.port)
url += "/"
url += self.api
url += "/"
url += self.version
return url
class ResourceEndpoint(ApiEndpoint):
def __init__(self, name, namespaced, api_endpoint):
super(ResourceEndpoint, self).__init__(api_endpoint.host,
api_endpoint.port,
api_endpoint.api,
api_endpoint.version)
self.name = name
self.namespaced = namespaced
class NamedResource(object):
def __init__(self, module, definition, resource_endpoint):
self.module = module
self.set_definition(definition)
self.resource_endpoint = resource_endpoint
def name(self):
if 'name' in self.definition['metadata'].keys():
return self.definition['metadata']['name']
return None
def namespace(self):
if 'namespace' in self.definition['metadata'].keys():
return self.definition['metadata']['namespace']
return None
def set_definition(self, definition):
if isinstance(definition, str):
self.definition = self.module.from_json(definition)
else:
self.definition = definition
def url(self, create=False):
url = str(self.resource_endpoint)
url += '/'
if self.resource_endpoint.namespaced:
url += 'namespaces/'
url += self.namespace()
url += '/'
url += self.resource_endpoint.name
if not create:
url += '/'
url += self.name()
return url
def __dict__(self):
return self.definition
def __str__(self):
return self.module.jsonify(self.definition)
class OC(object):
def __init__(self, module, token, host, port,
apis=None):
apis = ['api', 'oapi'] if apis is None else apis
self.apis = apis
self.version = 'v1'
self.token = token
self.module = module
self.host = host
self.port = port
self.kinds = {}
self.bearer = "Bearer " + self.token
self.headers = {"Authorization": self.bearer,
"Content-type": "application/json"}
# Build Endpoints
for api in self.apis:
endpoint = ApiEndpoint(self.host,
self.port,
api,
self.version)
# Create resource facts
response, code = self.connect(str(endpoint), "get")
if code < 300:
self.build_kinds(response['resources'], endpoint)
def build_kinds(self, resources, endpoint):
for resource in resources:
if 'generated' not in resource['name']:
self.kinds[resource['kind']] = \
ResourceEndpoint(resource['name'].split('/')[0],
resource['namespaced'],
endpoint)
def get(self, named_resource):
changed = False
response, code = self.connect(named_resource.url(), 'get')
return response, changed
def exists(self, named_resource):
x, code = self.connect(named_resource.url(), 'get')
if code == 200:
return True
return False
def delete(self, named_resource):
changed = False
response, code = self.connect(named_resource.url(), 'delete')
if code == 404:
return None, changed
elif code >= 300:
self.module.fail_json(msg='Failed to delete resource %s in \
namespace %s with msg %s'
% (named_resource.name(),
named_resource.namespace(),
response))
changed = True
return response, changed
def create(self, named_resource):
changed = False
response, code = self.connect(named_resource.url(create=True),
'post',
data=str(named_resource))
if code == 404:
return None, changed
elif code == 409:
return self.get(named_resource)
elif code >= 300:
self.module.fail_json(
msg='Failed to create resource %s in \
namespace %s with msg %s' % (named_resource.name(),
named_resource.namespace(),
response))
changed = True
return response, changed
def replace(self, named_resource, check_mode):
changed = False
existing_definition, x = self.get(named_resource)
new_definition, changed = self.merge(named_resource.definition,
existing_definition,
changed)
if changed and not check_mode:
named_resource.set_definition(new_definition)
response, code = self.connect(named_resource.url(),
'put',
data=str(named_resource))
return response, changed
return existing_definition, changed
def connect(self, url, method, data=None):
body = None
json_body = ""
if data is not None:
self.module.log(msg="Payload is %s" % data)
response, info = urls.fetch_url(module=self.module,
url=url,
headers=self.headers,
method=method,
data=data)
if response is not None:
body = response.read()
if info['status'] >= 300:
body = info['body']
message = "The URL, method, and code for connect is %s, %s, %d." % (url, method, info['status'])
if info['status'] == 401:
self.module.fail_json(msg=message + " Unauthorized. Check that you have a valid serivce account and token.")
self.module.log(msg=message)
try:
json_body = self.module.from_json(body)
except TypeError:
self.module.fail_json(msg="Response from %s expected to be a " +
"expected string or buffer." % url)
except ValueError:
return body, info['status']
return json_body, info['status']
def get_resource_endpoint(self, kind):
return self.kinds[kind]
# Attempts to 'kindly' merge the dictionaries into a new object definition
def merge(self, source, destination, changed):
for key, value in source.items():
if isinstance(value, dict):
# get node or create one
try:
node = destination.setdefault(key, {})
except AttributeError:
node = {}
finally:
x, changed = self.merge(value, node, changed)
elif isinstance(value, list) and key in destination.keys():
if destination[key] != source[key]:
destination[key] = source[key]
changed = True
elif (key not in destination.keys() or
destination[key] != source[key]):
destination[key] = value
changed = True
return destination, changed
def main():
module = AnsibleModule(
argument_spec=dict(
host=dict(type='str', default='127.0.0.1'),
port=dict(type='int', default=8443),
definition=dict(aliases=['def', 'inline'],
type='dict'),
kind=dict(type='str'),
name=dict(type='str'),
namespace=dict(type='str'),
token=dict(required=True, type='str', no_log=True),
state=dict(required=True,
choices=['present', 'absent']),
validate_certs=dict(type='bool', default='yes')
),
mutually_exclusive=(['kind', 'definition'],
['name', 'definition'],
['namespace', 'definition']),
required_if=([['state', 'absent', ['kind']]]),
required_one_of=([['kind', 'definition']]),
no_log=False,
supports_check_mode=True
)
kind = None
definition = None
name = None
namespace = None
host = module.params['host']
port = module.params['port']
definition = module.params['definition']
state = module.params['state']
kind = module.params['kind']
name = module.params['name']
namespace = module.params['namespace']
token = module.params['token']
if definition is None:
definition = {}
definition['metadata'] = {}
definition['metadata']['name'] = name
definition['metadata']['namespace'] = namespace
if "apiVersion" not in definition.keys():
definition['apiVersion'] = 'v1'
if "kind" not in definition.keys():
definition['kind'] = kind
result = None
oc = OC(module, token, host, port)
resource = NamedResource(module,
definition,
oc.get_resource_endpoint(definition['kind']))
changed = False
method = ''
exists = oc.exists(resource)
module.log(msg="URL %s" % resource.url())
if state == 'present' and exists:
method = 'put'
result, changed = oc.replace(resource, module.check_mode)
elif state == 'present' and not exists and definition is not None:
method = 'create'
if not module.check_mode:
result, changed = oc.create(resource)
else:
changed = True
result = definition
elif state == 'absent' and exists:
method = 'delete'
if not module.check_mode:
result, changed = oc.delete(resource)
else:
changed = True
result = definition
facts = {}
if result is not None and "items" in result:
result['item_list'] = result.pop('items')
elif result is None and state == 'present':
result = 'Resource not present and no inline provided.'
facts['oc'] = {'definition': result,
'url': resource.url(),
'method': method}
module.exit_json(changed=changed, ansible_facts=facts)
from ansible.module_utils.common.removed import removed_module
if __name__ == '__main__':
main()
removed_module(removed_in='2.9')