mirror of https://github.com/ansible/ansible.git
Include more test support plugins. (#68015)
* Include more test support plugins. Also add missing module_utils `__init__.py` files. * Update sanity ignores.pull/68021/head
parent
a51266ba85
commit
4fb7e62003
@ -0,0 +1,364 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# This code is part of Ansible, but is an independent component.
|
||||
# This particular file snippet, and this file snippet only, is licensed under the
|
||||
# Modified BSD License. Modules you write using this snippet, which is embedded
|
||||
# dynamically by Ansible, still belong to the author of the module, and may assign
|
||||
# their own license to the complete work.
|
||||
#
|
||||
# Copyright (c), Entrust Datacard Corporation, 2019
|
||||
# Simplified BSD License (see licenses/simplified_bsd.txt or https://opensource.org/licenses/BSD-2-Clause)
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without modification,
|
||||
# are permitted provided that the following conditions are met:
|
||||
# 1. Redistributions of source code must retain the above copyright notice,
|
||||
# this list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
||||
# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
||||
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
||||
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
|
||||
__metaclass__ = type
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from ansible.module_utils._text import to_text, to_native
|
||||
from ansible.module_utils.basic import missing_required_lib
|
||||
from ansible.module_utils.six.moves.urllib.parse import urlencode
|
||||
from ansible.module_utils.six.moves.urllib.error import HTTPError
|
||||
from ansible.module_utils.urls import Request
|
||||
|
||||
YAML_IMP_ERR = None
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
YAML_FOUND = False
|
||||
YAML_IMP_ERR = traceback.format_exc()
|
||||
else:
|
||||
YAML_FOUND = True
|
||||
|
||||
valid_file_format = re.compile(r".*(\.)(yml|yaml|json)$")
|
||||
|
||||
|
||||
def ecs_client_argument_spec():
|
||||
return dict(
|
||||
entrust_api_user=dict(type='str', required=True),
|
||||
entrust_api_key=dict(type='str', required=True, no_log=True),
|
||||
entrust_api_client_cert_path=dict(type='path', required=True),
|
||||
entrust_api_client_cert_key_path=dict(type='path', required=True, no_log=True),
|
||||
entrust_api_specification_path=dict(type='path', default='https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml'),
|
||||
)
|
||||
|
||||
|
||||
class SessionConfigurationException(Exception):
|
||||
""" Raised if we cannot configure a session with the API """
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class RestOperationException(Exception):
|
||||
""" Encapsulate a REST API error """
|
||||
|
||||
def __init__(self, error):
|
||||
self.status = to_native(error.get("status", None))
|
||||
self.errors = [to_native(err.get("message")) for err in error.get("errors", {})]
|
||||
self.message = to_native(" ".join(self.errors))
|
||||
|
||||
|
||||
def generate_docstring(operation_spec):
|
||||
"""Generate a docstring for an operation defined in operation_spec (swagger)"""
|
||||
# Description of the operation
|
||||
docs = operation_spec.get("description", "No Description")
|
||||
docs += "\n\n"
|
||||
|
||||
# Parameters of the operation
|
||||
parameters = operation_spec.get("parameters", [])
|
||||
if len(parameters) != 0:
|
||||
docs += "\tArguments:\n\n"
|
||||
for parameter in parameters:
|
||||
docs += "{0} ({1}:{2}): {3}\n".format(
|
||||
parameter.get("name"),
|
||||
parameter.get("type", "No Type"),
|
||||
"Required" if parameter.get("required", False) else "Not Required",
|
||||
parameter.get("description"),
|
||||
)
|
||||
|
||||
return docs
|
||||
|
||||
|
||||
def bind(instance, method, operation_spec):
|
||||
def binding_scope_fn(*args, **kwargs):
|
||||
return method(instance, *args, **kwargs)
|
||||
|
||||
# Make sure we don't confuse users; add the proper name and documentation to the function.
|
||||
# Users can use !help(<function>) to get help on the function from interactive python or pdb
|
||||
operation_name = operation_spec.get("operationId").split("Using")[0]
|
||||
binding_scope_fn.__name__ = str(operation_name)
|
||||
binding_scope_fn.__doc__ = generate_docstring(operation_spec)
|
||||
|
||||
return binding_scope_fn
|
||||
|
||||
|
||||
class RestOperation(object):
|
||||
def __init__(self, session, uri, method, parameters=None):
|
||||
self.session = session
|
||||
self.method = method
|
||||
if parameters is None:
|
||||
self.parameters = {}
|
||||
else:
|
||||
self.parameters = parameters
|
||||
self.url = "{scheme}://{host}{base_path}{uri}".format(scheme="https", host=session._spec.get("host"), base_path=session._spec.get("basePath"), uri=uri)
|
||||
|
||||
def restmethod(self, *args, **kwargs):
|
||||
"""Do the hard work of making the request here"""
|
||||
|
||||
# gather named path parameters and do substitution on the URL
|
||||
if self.parameters:
|
||||
path_parameters = {}
|
||||
body_parameters = {}
|
||||
query_parameters = {}
|
||||
for x in self.parameters:
|
||||
expected_location = x.get("in")
|
||||
key_name = x.get("name", None)
|
||||
key_value = kwargs.get(key_name, None)
|
||||
if expected_location == "path" and key_name and key_value:
|
||||
path_parameters.update({key_name: key_value})
|
||||
elif expected_location == "body" and key_name and key_value:
|
||||
body_parameters.update({key_name: key_value})
|
||||
elif expected_location == "query" and key_name and key_value:
|
||||
query_parameters.update({key_name: key_value})
|
||||
|
||||
if len(body_parameters.keys()) >= 1:
|
||||
body_parameters = body_parameters.get(list(body_parameters.keys())[0])
|
||||
else:
|
||||
body_parameters = None
|
||||
else:
|
||||
path_parameters = {}
|
||||
query_parameters = {}
|
||||
body_parameters = None
|
||||
|
||||
# This will fail if we have not set path parameters with a KeyError
|
||||
url = self.url.format(**path_parameters)
|
||||
if query_parameters:
|
||||
# modify the URL to add path parameters
|
||||
url = url + "?" + urlencode(query_parameters)
|
||||
|
||||
try:
|
||||
if body_parameters:
|
||||
body_parameters_json = json.dumps(body_parameters)
|
||||
response = self.session.request.open(method=self.method, url=url, data=body_parameters_json)
|
||||
else:
|
||||
response = self.session.request.open(method=self.method, url=url)
|
||||
request_error = False
|
||||
except HTTPError as e:
|
||||
# An HTTPError has the same methods available as a valid response from request.open
|
||||
response = e
|
||||
request_error = True
|
||||
|
||||
# Return the result if JSON and success ({} for empty responses)
|
||||
# Raise an exception if there was a failure.
|
||||
try:
|
||||
result_code = response.getcode()
|
||||
result = json.loads(response.read())
|
||||
except ValueError:
|
||||
result = {}
|
||||
|
||||
if result or result == {}:
|
||||
if result_code and result_code < 400:
|
||||
return result
|
||||
else:
|
||||
raise RestOperationException(result)
|
||||
|
||||
# Raise a generic RestOperationException if this fails
|
||||
raise RestOperationException({"status": result_code, "errors": [{"message": "REST Operation Failed"}]})
|
||||
|
||||
|
||||
class Resource(object):
|
||||
""" Implement basic CRUD operations against a path. """
|
||||
|
||||
def __init__(self, session):
|
||||
self.session = session
|
||||
self.parameters = {}
|
||||
|
||||
for url in session._spec.get("paths").keys():
|
||||
methods = session._spec.get("paths").get(url)
|
||||
for method in methods.keys():
|
||||
operation_spec = methods.get(method)
|
||||
operation_name = operation_spec.get("operationId", None)
|
||||
parameters = operation_spec.get("parameters")
|
||||
|
||||
if not operation_name:
|
||||
if method.lower() == "post":
|
||||
operation_name = "Create"
|
||||
elif method.lower() == "get":
|
||||
operation_name = "Get"
|
||||
elif method.lower() == "put":
|
||||
operation_name = "Update"
|
||||
elif method.lower() == "delete":
|
||||
operation_name = "Delete"
|
||||
elif method.lower() == "patch":
|
||||
operation_name = "Patch"
|
||||
else:
|
||||
raise SessionConfigurationException(to_native("Invalid REST method type {0}".format(method)))
|
||||
|
||||
# Get the non-parameter parts of the URL and append to the operation name
|
||||
# e.g /application/version -> GetApplicationVersion
|
||||
# e.g. /application/{id} -> GetApplication
|
||||
# This may lead to duplicates, which we must prevent.
|
||||
operation_name += re.sub(r"{(.*)}", "", url).replace("/", " ").title().replace(" ", "")
|
||||
operation_spec["operationId"] = operation_name
|
||||
|
||||
op = RestOperation(session, url, method, parameters)
|
||||
setattr(self, operation_name, bind(self, op.restmethod, operation_spec))
|
||||
|
||||
|
||||
# Session to encapsulate the connection parameters of the module_utils Request object, the api spec, etc
|
||||
class ECSSession(object):
|
||||
def __init__(self, name, **kwargs):
|
||||
"""
|
||||
Initialize our session
|
||||
"""
|
||||
|
||||
self._set_config(name, **kwargs)
|
||||
|
||||
def client(self):
|
||||
resource = Resource(self)
|
||||
return resource
|
||||
|
||||
def _set_config(self, name, **kwargs):
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"Connection": "keep-alive",
|
||||
}
|
||||
self.request = Request(headers=headers, timeout=60)
|
||||
|
||||
configurators = [self._read_config_vars]
|
||||
for configurator in configurators:
|
||||
self._config = configurator(name, **kwargs)
|
||||
if self._config:
|
||||
break
|
||||
if self._config is None:
|
||||
raise SessionConfigurationException(to_native("No Configuration Found."))
|
||||
|
||||
# set up auth if passed
|
||||
entrust_api_user = self.get_config("entrust_api_user")
|
||||
entrust_api_key = self.get_config("entrust_api_key")
|
||||
if entrust_api_user and entrust_api_key:
|
||||
self.request.url_username = entrust_api_user
|
||||
self.request.url_password = entrust_api_key
|
||||
else:
|
||||
raise SessionConfigurationException(to_native("User and key must be provided."))
|
||||
|
||||
# set up client certificate if passed (support all-in one or cert + key)
|
||||
entrust_api_cert = self.get_config("entrust_api_cert")
|
||||
entrust_api_cert_key = self.get_config("entrust_api_cert_key")
|
||||
if entrust_api_cert:
|
||||
self.request.client_cert = entrust_api_cert
|
||||
if entrust_api_cert_key:
|
||||
self.request.client_key = entrust_api_cert_key
|
||||
else:
|
||||
raise SessionConfigurationException(to_native("Client certificate for authentication to the API must be provided."))
|
||||
|
||||
# set up the spec
|
||||
entrust_api_specification_path = self.get_config("entrust_api_specification_path")
|
||||
|
||||
if not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path):
|
||||
raise SessionConfigurationException(to_native("OpenAPI specification was not found at location {0}.".format(entrust_api_specification_path)))
|
||||
if not valid_file_format.match(entrust_api_specification_path):
|
||||
raise SessionConfigurationException(to_native("OpenAPI specification filename must end in .json, .yml or .yaml"))
|
||||
|
||||
self.verify = True
|
||||
|
||||
if entrust_api_specification_path.startswith("http"):
|
||||
try:
|
||||
http_response = Request().open(method="GET", url=entrust_api_specification_path)
|
||||
http_response_contents = http_response.read()
|
||||
if entrust_api_specification_path.endswith(".json"):
|
||||
self._spec = json.load(http_response_contents)
|
||||
elif entrust_api_specification_path.endswith(".yml") or entrust_api_specification_path.endswith(".yaml"):
|
||||
self._spec = yaml.safe_load(http_response_contents)
|
||||
except HTTPError as e:
|
||||
raise SessionConfigurationException(to_native("Error downloading specification from address '{0}', received error code '{1}'".format(
|
||||
entrust_api_specification_path, e.getcode())))
|
||||
else:
|
||||
with open(entrust_api_specification_path) as f:
|
||||
if ".json" in entrust_api_specification_path:
|
||||
self._spec = json.load(f)
|
||||
elif ".yml" in entrust_api_specification_path or ".yaml" in entrust_api_specification_path:
|
||||
self._spec = yaml.safe_load(f)
|
||||
|
||||
def get_config(self, item):
|
||||
return self._config.get(item, None)
|
||||
|
||||
def _read_config_vars(self, name, **kwargs):
|
||||
""" Read configuration from variables passed to the module. """
|
||||
config = {}
|
||||
|
||||
entrust_api_specification_path = kwargs.get("entrust_api_specification_path")
|
||||
if not entrust_api_specification_path or (not entrust_api_specification_path.startswith("http") and not os.path.isfile(entrust_api_specification_path)):
|
||||
raise SessionConfigurationException(
|
||||
to_native(
|
||||
"Parameter provided for entrust_api_specification_path of value '{0}' was not a valid file path or HTTPS address.".format(
|
||||
entrust_api_specification_path
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
for required_file in ["entrust_api_cert", "entrust_api_cert_key"]:
|
||||
file_path = kwargs.get(required_file)
|
||||
if not file_path or not os.path.isfile(file_path):
|
||||
raise SessionConfigurationException(
|
||||
to_native("Parameter provided for {0} of value '{1}' was not a valid file path.".format(required_file, file_path))
|
||||
)
|
||||
|
||||
for required_var in ["entrust_api_user", "entrust_api_key"]:
|
||||
if not kwargs.get(required_var):
|
||||
raise SessionConfigurationException(to_native("Parameter provided for {0} was missing.".format(required_var)))
|
||||
|
||||
config["entrust_api_cert"] = kwargs.get("entrust_api_cert")
|
||||
config["entrust_api_cert_key"] = kwargs.get("entrust_api_cert_key")
|
||||
config["entrust_api_specification_path"] = kwargs.get("entrust_api_specification_path")
|
||||
config["entrust_api_user"] = kwargs.get("entrust_api_user")
|
||||
config["entrust_api_key"] = kwargs.get("entrust_api_key")
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def ECSClient(entrust_api_user=None, entrust_api_key=None, entrust_api_cert=None, entrust_api_cert_key=None, entrust_api_specification_path=None):
|
||||
"""Create an ECS client"""
|
||||
|
||||
if not YAML_FOUND:
|
||||
raise SessionConfigurationException(missing_required_lib("PyYAML"), exception=YAML_IMP_ERR)
|
||||
|
||||
if entrust_api_specification_path is None:
|
||||
entrust_api_specification_path = "https://cloud.entrust.net/EntrustCloud/documentation/cms-api-2.1.0.yaml"
|
||||
|
||||
# Not functionally necessary with current uses of this module_util, but better to be explicit for future use cases
|
||||
entrust_api_user = to_text(entrust_api_user)
|
||||
entrust_api_key = to_text(entrust_api_key)
|
||||
entrust_api_cert_key = to_text(entrust_api_cert_key)
|
||||
entrust_api_specification_path = to_text(entrust_api_specification_path)
|
||||
|
||||
return ECSSession(
|
||||
"ecs",
|
||||
entrust_api_user=entrust_api_user,
|
||||
entrust_api_key=entrust_api_key,
|
||||
entrust_api_cert=entrust_api_cert,
|
||||
entrust_api_cert_key=entrust_api_cert_key,
|
||||
entrust_api_specification_path=entrust_api_specification_path,
|
||||
).client()
|
@ -0,0 +1,275 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# (c) 2013, Nimbis Services, Inc.
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
from __future__ import absolute_import, division, print_function
|
||||
__metaclass__ = type
|
||||
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: htpasswd
|
||||
version_added: "1.3"
|
||||
short_description: manage user files for basic authentication
|
||||
description:
|
||||
- Add and remove username/password entries in a password file using htpasswd.
|
||||
- This is used by web servers such as Apache and Nginx for basic authentication.
|
||||
options:
|
||||
path:
|
||||
required: true
|
||||
aliases: [ dest, destfile ]
|
||||
description:
|
||||
- Path to the file that contains the usernames and passwords
|
||||
name:
|
||||
required: true
|
||||
aliases: [ username ]
|
||||
description:
|
||||
- User name to add or remove
|
||||
password:
|
||||
required: false
|
||||
description:
|
||||
- Password associated with user.
|
||||
- Must be specified if user does not exist yet.
|
||||
crypt_scheme:
|
||||
required: false
|
||||
choices: ["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext"]
|
||||
default: "apr_md5_crypt"
|
||||
description:
|
||||
- Encryption scheme to be used. As well as the four choices listed
|
||||
here, you can also use any other hash supported by passlib, such as
|
||||
md5_crypt and sha256_crypt, which are linux passwd hashes. If you
|
||||
do so the password file will not be compatible with Apache or Nginx
|
||||
state:
|
||||
required: false
|
||||
choices: [ present, absent ]
|
||||
default: "present"
|
||||
description:
|
||||
- Whether the user entry should be present or not
|
||||
create:
|
||||
required: false
|
||||
type: bool
|
||||
default: "yes"
|
||||
description:
|
||||
- Used with C(state=present). If specified, the file will be created
|
||||
if it does not already exist. If set to "no", will fail if the
|
||||
file does not exist
|
||||
notes:
|
||||
- "This module depends on the I(passlib) Python library, which needs to be installed on all target systems."
|
||||
- "On Debian, Ubuntu, or Fedora: install I(python-passlib)."
|
||||
- "On RHEL or CentOS: Enable EPEL, then install I(python-passlib)."
|
||||
requirements: [ passlib>=1.6 ]
|
||||
author: "Ansible Core Team"
|
||||
extends_documentation_fragment: files
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Add a user to a password file and ensure permissions are set
|
||||
- htpasswd:
|
||||
path: /etc/nginx/passwdfile
|
||||
name: janedoe
|
||||
password: '9s36?;fyNp'
|
||||
owner: root
|
||||
group: www-data
|
||||
mode: 0640
|
||||
|
||||
# Remove a user from a password file
|
||||
- htpasswd:
|
||||
path: /etc/apache2/passwdfile
|
||||
name: foobar
|
||||
state: absent
|
||||
|
||||
# Add a user to a password file suitable for use by libpam-pwdfile
|
||||
- htpasswd:
|
||||
path: /etc/mail/passwords
|
||||
name: alex
|
||||
password: oedu2eGh
|
||||
crypt_scheme: md5_crypt
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import traceback
|
||||
from distutils.version import LooseVersion
|
||||
from ansible.module_utils.basic import AnsibleModule, missing_required_lib
|
||||
from ansible.module_utils._text import to_native
|
||||
|
||||
PASSLIB_IMP_ERR = None
|
||||
try:
|
||||
from passlib.apache import HtpasswdFile, htpasswd_context
|
||||
from passlib.context import CryptContext
|
||||
import passlib
|
||||
except ImportError:
|
||||
PASSLIB_IMP_ERR = traceback.format_exc()
|
||||
passlib_installed = False
|
||||
else:
|
||||
passlib_installed = True
|
||||
|
||||
apache_hashes = ["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext"]
|
||||
|
||||
|
||||
def create_missing_directories(dest):
|
||||
destpath = os.path.dirname(dest)
|
||||
if not os.path.exists(destpath):
|
||||
os.makedirs(destpath)
|
||||
|
||||
|
||||
def present(dest, username, password, crypt_scheme, create, check_mode):
|
||||
""" Ensures user is present
|
||||
|
||||
Returns (msg, changed) """
|
||||
if crypt_scheme in apache_hashes:
|
||||
context = htpasswd_context
|
||||
else:
|
||||
context = CryptContext(schemes=[crypt_scheme] + apache_hashes)
|
||||
if not os.path.exists(dest):
|
||||
if not create:
|
||||
raise ValueError('Destination %s does not exist' % dest)
|
||||
if check_mode:
|
||||
return ("Create %s" % dest, True)
|
||||
create_missing_directories(dest)
|
||||
if LooseVersion(passlib.__version__) >= LooseVersion('1.6'):
|
||||
ht = HtpasswdFile(dest, new=True, default_scheme=crypt_scheme, context=context)
|
||||
else:
|
||||
ht = HtpasswdFile(dest, autoload=False, default=crypt_scheme, context=context)
|
||||
if getattr(ht, 'set_password', None):
|
||||
ht.set_password(username, password)
|
||||
else:
|
||||
ht.update(username, password)
|
||||
ht.save()
|
||||
return ("Created %s and added %s" % (dest, username), True)
|
||||
else:
|
||||
if LooseVersion(passlib.__version__) >= LooseVersion('1.6'):
|
||||
ht = HtpasswdFile(dest, new=False, default_scheme=crypt_scheme, context=context)
|
||||
else:
|
||||
ht = HtpasswdFile(dest, default=crypt_scheme, context=context)
|
||||
|
||||
found = None
|
||||
if getattr(ht, 'check_password', None):
|
||||
found = ht.check_password(username, password)
|
||||
else:
|
||||
found = ht.verify(username, password)
|
||||
|
||||
if found:
|
||||
return ("%s already present" % username, False)
|
||||
else:
|
||||
if not check_mode:
|
||||
if getattr(ht, 'set_password', None):
|
||||
ht.set_password(username, password)
|
||||
else:
|
||||
ht.update(username, password)
|
||||
ht.save()
|
||||
return ("Add/update %s" % username, True)
|
||||
|
||||
|
||||
def absent(dest, username, check_mode):
|
||||
""" Ensures user is absent
|
||||
|
||||
Returns (msg, changed) """
|
||||
if LooseVersion(passlib.__version__) >= LooseVersion('1.6'):
|
||||
ht = HtpasswdFile(dest, new=False)
|
||||
else:
|
||||
ht = HtpasswdFile(dest)
|
||||
|
||||
if username not in ht.users():
|
||||
return ("%s not present" % username, False)
|
||||
else:
|
||||
if not check_mode:
|
||||
ht.delete(username)
|
||||
ht.save()
|
||||
return ("Remove %s" % username, True)
|
||||
|
||||
|
||||
def check_file_attrs(module, changed, message):
|
||||
|
||||
file_args = module.load_file_common_arguments(module.params)
|
||||
if module.set_fs_attributes_if_different(file_args, False):
|
||||
|
||||
if changed:
|
||||
message += " and "
|
||||
changed = True
|
||||
message += "ownership, perms or SE linux context changed"
|
||||
|
||||
return message, changed
|
||||
|
||||
|
||||
def main():
|
||||
arg_spec = dict(
|
||||
path=dict(required=True, aliases=["dest", "destfile"]),
|
||||
name=dict(required=True, aliases=["username"]),
|
||||
password=dict(required=False, default=None, no_log=True),
|
||||
crypt_scheme=dict(required=False, default="apr_md5_crypt"),
|
||||
state=dict(required=False, default="present"),
|
||||
create=dict(type='bool', default='yes'),
|
||||
|
||||
)
|
||||
module = AnsibleModule(argument_spec=arg_spec,
|
||||
add_file_common_args=True,
|
||||
supports_check_mode=True)
|
||||
|
||||
path = module.params['path']
|
||||
username = module.params['name']
|
||||
password = module.params['password']
|
||||
crypt_scheme = module.params['crypt_scheme']
|
||||
state = module.params['state']
|
||||
create = module.params['create']
|
||||
check_mode = module.check_mode
|
||||
|
||||
if not passlib_installed:
|
||||
module.fail_json(msg=missing_required_lib("passlib"), exception=PASSLIB_IMP_ERR)
|
||||
|
||||
# Check file for blank lines in effort to avoid "need more than 1 value to unpack" error.
|
||||
try:
|
||||
f = open(path, "r")
|
||||
except IOError:
|
||||
# No preexisting file to remove blank lines from
|
||||
f = None
|
||||
else:
|
||||
try:
|
||||
lines = f.readlines()
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
# If the file gets edited, it returns true, so only edit the file if it has blank lines
|
||||
strip = False
|
||||
for line in lines:
|
||||
if not line.strip():
|
||||
strip = True
|
||||
break
|
||||
|
||||
if strip:
|
||||
# If check mode, create a temporary file
|
||||
if check_mode:
|
||||
temp = tempfile.NamedTemporaryFile()
|
||||
path = temp.name
|
||||
f = open(path, "w")
|
||||
try:
|
||||
[f.write(line) for line in lines if line.strip()]
|
||||
finally:
|
||||
f.close()
|
||||
|
||||
try:
|
||||
if state == 'present':
|
||||
(msg, changed) = present(path, username, password, crypt_scheme, create, check_mode)
|
||||
elif state == 'absent':
|
||||
if not os.path.exists(path):
|
||||
module.exit_json(msg="%s not present" % username,
|
||||
warnings="%s does not exist" % path, changed=False)
|
||||
(msg, changed) = absent(path, username, check_mode)
|
||||
else:
|
||||
module.fail_json(msg="Invalid state: %s" % state)
|
||||
|
||||
check_file_attrs(module, changed, msg)
|
||||
module.exit_json(msg=msg, changed=changed)
|
||||
except Exception as e:
|
||||
module.fail_json(msg=to_native(e))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
@ -0,0 +1,260 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#AnsibleRequires -CSharpUtil Ansible.Basic
|
||||
|
||||
$store_name_values = ([System.Security.Cryptography.X509Certificates.StoreName]).GetEnumValues() | ForEach-Object { $_.ToString() }
|
||||
$store_location_values = ([System.Security.Cryptography.X509Certificates.StoreLocation]).GetEnumValues() | ForEach-Object { $_.ToString() }
|
||||
|
||||
$spec = @{
|
||||
options = @{
|
||||
state = @{ type = "str"; default = "present"; choices = "absent", "exported", "present" }
|
||||
path = @{ type = "path" }
|
||||
thumbprint = @{ type = "str" }
|
||||
store_name = @{ type = "str"; default = "My"; choices = $store_name_values }
|
||||
store_location = @{ type = "str"; default = "LocalMachine"; choices = $store_location_values }
|
||||
password = @{ type = "str"; no_log = $true }
|
||||
key_exportable = @{ type = "bool"; default = $true }
|
||||
key_storage = @{ type = "str"; default = "default"; choices = "default", "machine", "user" }
|
||||
file_type = @{ type = "str"; default = "der"; choices = "der", "pem", "pkcs12" }
|
||||
}
|
||||
required_if = @(
|
||||
@("state", "absent", @("path", "thumbprint"), $true),
|
||||
@("state", "exported", @("path", "thumbprint")),
|
||||
@("state", "present", @("path"))
|
||||
)
|
||||
supports_check_mode = $true
|
||||
}
|
||||
$module = [Ansible.Basic.AnsibleModule]::Create($args, $spec)
|
||||
|
||||
Function Get-CertFile($module, $path, $password, $key_exportable, $key_storage) {
|
||||
# parses a certificate file and returns X509Certificate2Collection
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) {
|
||||
$module.FailJson("File at '$path' either does not exist or is not a file")
|
||||
}
|
||||
|
||||
# must set at least the PersistKeySet flag so that the PrivateKey
|
||||
# is stored in a permanent container and not deleted once the handle
|
||||
# is gone.
|
||||
$store_flags = [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::PersistKeySet
|
||||
|
||||
$key_storage = $key_storage.substring(0,1).ToUpper() + $key_storage.substring(1).ToLower()
|
||||
$store_flags = $store_flags -bor [Enum]::Parse([System.Security.Cryptography.X509Certificates.X509KeyStorageFlags], "$($key_storage)KeySet")
|
||||
if ($key_exportable) {
|
||||
$store_flags = $store_flags -bor [System.Security.Cryptography.X509Certificates.X509KeyStorageFlags]::Exportable
|
||||
}
|
||||
|
||||
# TODO: If I'm feeling adventurours, write code to parse PKCS#12 PEM encoded
|
||||
# file as .NET does not have an easy way to import this
|
||||
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
|
||||
try {
|
||||
$certs.Import($path, $password, $store_flags)
|
||||
} catch {
|
||||
$module.FailJson("Failed to load cert from file: $($_.Exception.Message)", $_)
|
||||
}
|
||||
|
||||
return $certs
|
||||
}
|
||||
|
||||
Function New-CertFile($module, $cert, $path, $type, $password) {
|
||||
$content_type = switch ($type) {
|
||||
"pem" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||
"der" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Cert }
|
||||
"pkcs12" { [System.Security.Cryptography.X509Certificates.X509ContentType]::Pkcs12 }
|
||||
}
|
||||
if ($type -eq "pkcs12") {
|
||||
$missing_key = $false
|
||||
if ($null -eq $cert.PrivateKey) {
|
||||
$missing_key = $true
|
||||
} elseif ($cert.PrivateKey.CspKeyContainerInfo.Exportable -eq $false) {
|
||||
$missing_key = $true
|
||||
}
|
||||
if ($missing_key) {
|
||||
$module.FailJson("Cannot export cert with key as PKCS12 when the key is not marked as exportable or not accessible by the current user")
|
||||
}
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $path) {
|
||||
Remove-Item -LiteralPath $path -Force
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
try {
|
||||
$cert_bytes = $cert.Export($content_type, $password)
|
||||
} catch {
|
||||
$module.FailJson("Failed to export certificate as bytes: $($_.Exception.Message)", $_)
|
||||
}
|
||||
|
||||
# Need to manually handle a PEM file
|
||||
if ($type -eq "pem") {
|
||||
$cert_content = "-----BEGIN CERTIFICATE-----`r`n"
|
||||
$base64_string = [System.Convert]::ToBase64String($cert_bytes, [System.Base64FormattingOptions]::InsertLineBreaks)
|
||||
$cert_content += $base64_string
|
||||
$cert_content += "`r`n-----END CERTIFICATE-----"
|
||||
$file_encoding = [System.Text.Encoding]::ASCII
|
||||
$cert_bytes = $file_encoding.GetBytes($cert_content)
|
||||
} elseif ($type -eq "pkcs12") {
|
||||
$module.Result.key_exported = $false
|
||||
if ($null -ne $cert.PrivateKey) {
|
||||
$module.Result.key_exportable = $cert.PrivateKey.CspKeyContainerInfo.Exportable
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $module.CheckMode) {
|
||||
try {
|
||||
[System.IO.File]::WriteAllBytes($path, $cert_bytes)
|
||||
} catch [System.ArgumentNullException] {
|
||||
$module.FailJson("Failed to write cert to file, cert was null: $($_.Exception.Message)", $_)
|
||||
} catch [System.IO.IOException] {
|
||||
$module.FailJson("Failed to write cert to file due to IO Exception: $($_.Exception.Message)", $_)
|
||||
} catch [System.UnauthorizedAccessException] {
|
||||
$module.FailJson("Failed to write cert to file due to permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Failed to write cert to file: $($_.Exception.Message)", $_)
|
||||
}
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
|
||||
Function Get-CertFileType($path, $password) {
|
||||
$certs = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Certificate2Collection
|
||||
try {
|
||||
$certs.Import($path, $password, 0)
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
# the file is a pkcs12 we just had the wrong password
|
||||
return "pkcs12"
|
||||
} catch {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
$file_contents = Get-Content -LiteralPath $path -Raw
|
||||
if ($file_contents.StartsWith("-----BEGIN CERTIFICATE-----")) {
|
||||
return "pem"
|
||||
} elseif ($file_contents.StartsWith("-----BEGIN PKCS7-----")) {
|
||||
return "pkcs7-ascii"
|
||||
} elseif ($certs.Count -gt 1) {
|
||||
# multiple certs must be pkcs7
|
||||
return "pkcs7-binary"
|
||||
} elseif ($certs[0].HasPrivateKey) {
|
||||
return "pkcs12"
|
||||
} elseif ($path.EndsWith(".pfx") -or $path.EndsWith(".p12")) {
|
||||
# no way to differenciate a pfx with a der file so we must rely on the
|
||||
# extension
|
||||
return "pkcs12"
|
||||
} else {
|
||||
return "der"
|
||||
}
|
||||
}
|
||||
|
||||
$state = $module.Params.state
|
||||
$path = $module.Params.path
|
||||
$thumbprint = $module.Params.thumbprint
|
||||
$store_name = [System.Security.Cryptography.X509Certificates.StoreName]"$($module.Params.store_name)"
|
||||
$store_location = [System.Security.Cryptography.X509Certificates.Storelocation]"$($module.Params.store_location)"
|
||||
$password = $module.Params.password
|
||||
$key_exportable = $module.Params.key_exportable
|
||||
$key_storage = $module.Params.key_storage
|
||||
$file_type = $module.Params.file_type
|
||||
|
||||
$module.Result.thumbprints = @()
|
||||
|
||||
$store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList $store_name, $store_location
|
||||
try {
|
||||
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
$module.FailJson("Unable to open the store as it is not readable: $($_.Exception.Message)", $_)
|
||||
} catch [System.Security.SecurityException] {
|
||||
$module.FailJson("Unable to open the store with the current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to open the store: $($_.Exception.Message)", $_)
|
||||
}
|
||||
$store_certificates = $store.Certificates
|
||||
|
||||
try {
|
||||
if ($state -eq "absent") {
|
||||
$cert_thumbprints = @()
|
||||
|
||||
if ($null -ne $path) {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
foreach ($cert in $certs) {
|
||||
$cert_thumbprints += $cert.Thumbprint
|
||||
}
|
||||
} elseif ($null -ne $thumbprint) {
|
||||
$cert_thumbprints += $thumbprint
|
||||
}
|
||||
|
||||
foreach ($cert_thumbprint in $cert_thumbprints) {
|
||||
$module.Result.thumbprints += $cert_thumbprint
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert_thumbprint, $false)
|
||||
if ($found_certs.Count -gt 0) {
|
||||
foreach ($found_cert in $found_certs) {
|
||||
try {
|
||||
if (-not $module.CheckMode) {
|
||||
$store.Remove($found_cert)
|
||||
}
|
||||
} catch [System.Security.SecurityException] {
|
||||
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint' with current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to remove cert with thumbprint '$cert_thumbprint': $($_.Exception.Message)", $_)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($state -eq "exported") {
|
||||
# TODO: Add support for PKCS7 and exporting a cert chain
|
||||
$module.Result.thumbprints += $thumbprint
|
||||
$export = $true
|
||||
if (Test-Path -LiteralPath $path -PathType Container) {
|
||||
$module.FailJson("Cannot export cert to path '$path' as it is a directory")
|
||||
} elseif (Test-Path -LiteralPath $path -PathType Leaf) {
|
||||
$actual_cert_type = Get-CertFileType -path $path -password $password
|
||||
if ($actual_cert_type -eq $file_type) {
|
||||
try {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
} catch {
|
||||
# failed to load the file so we set the thumbprint to something
|
||||
# that will fail validation
|
||||
$certs = @{Thumbprint = $null}
|
||||
}
|
||||
|
||||
if ($certs.Thumbprint -eq $thumbprint) {
|
||||
$export = $false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($export) {
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $thumbprint, $false)
|
||||
if ($found_certs.Count -ne 1) {
|
||||
$module.FailJson("Found $($found_certs.Count) certs when only expecting 1")
|
||||
}
|
||||
|
||||
New-CertFile -module $module -cert $found_certs -path $path -type $file_type -password $password
|
||||
}
|
||||
} else {
|
||||
$certs = Get-CertFile -module $module -path $path -password $password -key_exportable $key_exportable -key_storage $key_storage
|
||||
foreach ($cert in $certs) {
|
||||
$module.Result.thumbprints += $cert.Thumbprint
|
||||
$found_certs = $store_certificates.Find([System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $cert.Thumbprint, $false)
|
||||
if ($found_certs.Count -eq 0) {
|
||||
try {
|
||||
if (-not $module.CheckMode) {
|
||||
$store.Add($cert)
|
||||
}
|
||||
} catch [System.Security.Cryptography.CryptographicException] {
|
||||
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)' with the current permissions: $($_.Exception.Message)", $_)
|
||||
} catch {
|
||||
$module.FailJson("Unable to import certificate with thumbprint '$($cert.Thumbprint)': $($_.Exception.Message)", $_)
|
||||
}
|
||||
$module.Result.changed = $true
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
$store.Close()
|
||||
}
|
||||
|
||||
$module.ExitJson()
|
@ -0,0 +1,208 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# 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 = r'''
|
||||
---
|
||||
module: win_certificate_store
|
||||
version_added: '2.5'
|
||||
short_description: Manages the certificate store
|
||||
description:
|
||||
- Used to import/export and remove certificates and keys from the local
|
||||
certificate store.
|
||||
- This module is not used to create certificates and will only manage existing
|
||||
certs as a file or in the store.
|
||||
- It can be used to import PEM, DER, P7B, PKCS12 (PFX) certificates and export
|
||||
PEM, DER and PKCS12 certificates.
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- If C(present), will ensure that the certificate at I(path) is imported
|
||||
into the certificate store specified.
|
||||
- If C(absent), will ensure that the certificate specified by I(thumbprint)
|
||||
or the thumbprint of the cert at I(path) is removed from the store
|
||||
specified.
|
||||
- If C(exported), will ensure the file at I(path) is a certificate
|
||||
specified by I(thumbprint).
|
||||
- When exporting a certificate, if I(path) is a directory then the module
|
||||
will fail, otherwise the file will be replaced if needed.
|
||||
type: str
|
||||
choices: [ absent, exported, present ]
|
||||
default: present
|
||||
path:
|
||||
description:
|
||||
- The path to a certificate file.
|
||||
- This is required when I(state) is C(present) or C(exported).
|
||||
- When I(state) is C(absent) and I(thumbprint) is not specified, the
|
||||
thumbprint is derived from the certificate at this path.
|
||||
type: path
|
||||
thumbprint:
|
||||
description:
|
||||
- The thumbprint as a hex string to either export or remove.
|
||||
- See the examples for how to specify the thumbprint.
|
||||
type: str
|
||||
store_name:
|
||||
description:
|
||||
- The store name to use when importing a certificate or searching for a
|
||||
certificate.
|
||||
- "C(AddressBook): The X.509 certificate store for other users"
|
||||
- "C(AuthRoot): The X.509 certificate store for third-party certificate authorities (CAs)"
|
||||
- "C(CertificateAuthority): The X.509 certificate store for intermediate certificate authorities (CAs)"
|
||||
- "C(Disallowed): The X.509 certificate store for revoked certificates"
|
||||
- "C(My): The X.509 certificate store for personal certificates"
|
||||
- "C(Root): The X.509 certificate store for trusted root certificate authorities (CAs)"
|
||||
- "C(TrustedPeople): The X.509 certificate store for directly trusted people and resources"
|
||||
- "C(TrustedPublisher): The X.509 certificate store for directly trusted publishers"
|
||||
type: str
|
||||
choices:
|
||||
- AddressBook
|
||||
- AuthRoot
|
||||
- CertificateAuthority
|
||||
- Disallowed
|
||||
- My
|
||||
- Root
|
||||
- TrustedPeople
|
||||
- TrustedPublisher
|
||||
default: My
|
||||
store_location:
|
||||
description:
|
||||
- The store location to use when importing a certificate or searching for a
|
||||
certificate.
|
||||
choices: [ CurrentUser, LocalMachine ]
|
||||
default: LocalMachine
|
||||
password:
|
||||
description:
|
||||
- The password of the pkcs12 certificate key.
|
||||
- This is used when reading a pkcs12 certificate file or the password to
|
||||
set when C(state=exported) and C(file_type=pkcs12).
|
||||
- If the pkcs12 file has no password set or no password should be set on
|
||||
the exported file, do not set this option.
|
||||
type: str
|
||||
key_exportable:
|
||||
description:
|
||||
- Whether to allow the private key to be exported.
|
||||
- If C(no), then this module and other process will only be able to export
|
||||
the certificate and the private key cannot be exported.
|
||||
- Used when C(state=present) only.
|
||||
type: bool
|
||||
default: yes
|
||||
key_storage:
|
||||
description:
|
||||
- Specifies where Windows will store the private key when it is imported.
|
||||
- When set to C(default), the default option as set by Windows is used, typically C(user).
|
||||
- When set to C(machine), the key is stored in a path accessible by various
|
||||
users.
|
||||
- When set to C(user), the key is stored in a path only accessible by the
|
||||
current user.
|
||||
- Used when C(state=present) only and cannot be changed once imported.
|
||||
- See U(https://msdn.microsoft.com/en-us/library/system.security.cryptography.x509certificates.x509keystorageflags.aspx)
|
||||
for more details.
|
||||
type: str
|
||||
choices: [ default, machine, user ]
|
||||
default: default
|
||||
file_type:
|
||||
description:
|
||||
- The file type to export the certificate as when C(state=exported).
|
||||
- C(der) is a binary ASN.1 encoded file.
|
||||
- C(pem) is a base64 encoded file of a der file in the OpenSSL form.
|
||||
- C(pkcs12) (also known as pfx) is a binary container that contains both
|
||||
the certificate and private key unlike the other options.
|
||||
- When C(pkcs12) is set and the private key is not exportable or accessible
|
||||
by the current user, it will throw an exception.
|
||||
type: str
|
||||
choices: [ der, pem, pkcs12 ]
|
||||
default: der
|
||||
notes:
|
||||
- Some actions on PKCS12 certificates and keys may fail with the error
|
||||
C(the specified network password is not correct), either use CredSSP or
|
||||
Kerberos with credential delegation, or use C(become) to bypass these
|
||||
restrictions.
|
||||
- The certificates must be located on the Windows host to be set with I(path).
|
||||
- When importing a certificate for usage in IIS, it is generally required
|
||||
to use the C(machine) key_storage option, as both C(default) and C(user)
|
||||
will make the private key unreadable to IIS APPPOOL identities and prevent
|
||||
binding the certificate to the https endpoint.
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Import a certificate
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pem
|
||||
state: present
|
||||
|
||||
- name: Import pfx certificate that is password protected
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: present
|
||||
password: VeryStrongPasswordHere!
|
||||
become: yes
|
||||
become_method: runas
|
||||
|
||||
- name: Import pfx certificate without password and set private key as un-exportable
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: present
|
||||
key_exportable: no
|
||||
# usually you don't set this here but it is for illustrative purposes
|
||||
vars:
|
||||
ansible_winrm_transport: credssp
|
||||
|
||||
- name: Remove a certificate based on file thumbprint
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pem
|
||||
state: absent
|
||||
|
||||
- name: Remove a certificate based on thumbprint
|
||||
win_certificate_store:
|
||||
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
|
||||
state: absent
|
||||
|
||||
- name: Remove certificate based on thumbprint is CurrentUser/TrustedPublishers store
|
||||
win_certificate_store:
|
||||
thumbprint: BD7AF104CF1872BDB518D95C9534EA941665FD27
|
||||
state: absent
|
||||
store_location: CurrentUser
|
||||
store_name: TrustedPublisher
|
||||
|
||||
- name: Export certificate as der encoded file
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.cer
|
||||
state: exported
|
||||
file_type: der
|
||||
|
||||
- name: Export certificate and key as pfx encoded file
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
state: exported
|
||||
file_type: pkcs12
|
||||
password: AnotherStrongPass!
|
||||
become: yes
|
||||
become_method: runas
|
||||
become_user: SYSTEM
|
||||
|
||||
- name: Import certificate be used by IIS
|
||||
win_certificate_store:
|
||||
path: C:\Temp\cert.pfx
|
||||
file_type: pkcs12
|
||||
password: StrongPassword!
|
||||
store_location: LocalMachine
|
||||
key_storage: machine
|
||||
state: present
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
thumbprints:
|
||||
description: A list of certificate thumbprints that were touched by the
|
||||
module.
|
||||
returned: success
|
||||
type: list
|
||||
sample: ["BC05633694E675449136679A658281F17A191087"]
|
||||
'''
|
@ -0,0 +1,259 @@
|
||||
#!powershell
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
#Requires -Module Ansible.ModuleUtils.Legacy
|
||||
#Requires -Module Ansible.ModuleUtils.FileUtil
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$params = Parse-Args -arguments $args -supports_check_mode $true
|
||||
|
||||
$connect_timeout = Get-AnsibleParam -obj $params -name "connect_timeout" -type "int" -default 5
|
||||
$delay = Get-AnsibleParam -obj $params -name "delay" -type "int"
|
||||
$exclude_hosts = Get-AnsibleParam -obj $params -name "exclude_hosts" -type "list"
|
||||
$hostname = Get-AnsibleParam -obj $params -name "host" -type "str" -default "127.0.0.1"
|
||||
$path = Get-AnsibleParam -obj $params -name "path" -type "path"
|
||||
$port = Get-AnsibleParam -obj $params -name "port" -type "int"
|
||||
$regex = Get-AnsibleParam -obj $params -name "regex" -type "str" -aliases "search_regex","regexp"
|
||||
$sleep = Get-AnsibleParam -obj $params -name "sleep" -type "int" -default 1
|
||||
$state = Get-AnsibleParam -obj $params -name "state" -type "str" -default "started" -validateset "present","started","stopped","absent","drained"
|
||||
$timeout = Get-AnsibleParam -obj $params -name "timeout" -type "int" -default 300
|
||||
|
||||
$result = @{
|
||||
changed = $false
|
||||
elapsed = 0
|
||||
}
|
||||
|
||||
# validate the input with the various options
|
||||
if ($null -ne $port -and $null -ne $path) {
|
||||
Fail-Json $result "port and path parameter can not both be passed to win_wait_for"
|
||||
}
|
||||
if ($null -ne $exclude_hosts -and $state -ne "drained") {
|
||||
Fail-Json $result "exclude_hosts should only be with state=drained"
|
||||
}
|
||||
if ($null -ne $path) {
|
||||
if ($state -in @("stopped","drained")) {
|
||||
Fail-Json $result "state=$state should only be used for checking a port in the win_wait_for module"
|
||||
}
|
||||
|
||||
if ($null -ne $exclude_hosts) {
|
||||
Fail-Json $result "exclude_hosts should only be used when checking a port and state=drained in the win_wait_for module"
|
||||
}
|
||||
}
|
||||
|
||||
if ($null -ne $port) {
|
||||
if ($null -ne $regex) {
|
||||
Fail-Json $result "regex should by used when checking a string in a file in the win_wait_for module"
|
||||
}
|
||||
|
||||
if ($null -ne $exclude_hosts -and $state -ne "drained") {
|
||||
Fail-Json $result "exclude_hosts should be used when state=drained in the win_wait_for module"
|
||||
}
|
||||
}
|
||||
|
||||
Function Test-Port($hostname, $port) {
|
||||
$timeout = $connect_timeout * 1000
|
||||
$socket = New-Object -TypeName System.Net.Sockets.TcpClient
|
||||
$connect = $socket.BeginConnect($hostname, $port, $null, $null)
|
||||
$wait = $connect.AsyncWaitHandle.WaitOne($timeout, $false)
|
||||
|
||||
if ($wait) {
|
||||
try {
|
||||
$socket.EndConnect($connect) | Out-Null
|
||||
$valid = $true
|
||||
} catch {
|
||||
$valid = $false
|
||||
}
|
||||
} else {
|
||||
$valid = $false
|
||||
}
|
||||
|
||||
$socket.Close()
|
||||
$socket.Dispose()
|
||||
|
||||
$valid
|
||||
}
|
||||
|
||||
Function Get-PortConnections($hostname, $port) {
|
||||
$connections = @()
|
||||
|
||||
$conn_info = [Net.NetworkInformation.IPGlobalProperties]::GetIPGlobalProperties()
|
||||
if ($hostname -eq "0.0.0.0") {
|
||||
$active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Port -eq $port }
|
||||
} else {
|
||||
$active_connections = $conn_info.GetActiveTcpConnections() | Where-Object { $_.LocalEndPoint.Address -eq $hostname -and $_.LocalEndPoint.Port -eq $port }
|
||||
}
|
||||
|
||||
if ($null -ne $active_connections) {
|
||||
foreach ($active_connection in $active_connections) {
|
||||
$connections += $active_connection.RemoteEndPoint.Address
|
||||
}
|
||||
}
|
||||
|
||||
$connections
|
||||
}
|
||||
|
||||
$module_start = Get-Date
|
||||
|
||||
if ($null -ne $delay) {
|
||||
Start-Sleep -Seconds $delay
|
||||
}
|
||||
|
||||
$attempts = 0
|
||||
if ($null -eq $path -and $null -eq $port -and $state -ne "drained") {
|
||||
Start-Sleep -Seconds $timeout
|
||||
} elseif ($null -ne $path) {
|
||||
if ($state -in @("present", "started")) {
|
||||
# check if the file exists or string exists in file
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
if (Test-AnsiblePath -Path $path) {
|
||||
if ($null -eq $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
} else {
|
||||
$file_contents = Get-Content -Path $path -Raw
|
||||
if ($file_contents -match $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
if ($null -eq $regex) {
|
||||
Fail-Json $result "timeout while waiting for file $path to be present"
|
||||
} else {
|
||||
Fail-Json $result "timeout while waiting for string regex $regex in file $path to match"
|
||||
}
|
||||
}
|
||||
} elseif ($state -in @("absent")) {
|
||||
# check if the file is deleted or string doesn't exist in file
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
if (Test-AnsiblePath -Path $path) {
|
||||
if ($null -ne $regex) {
|
||||
$file_contents = Get-Content -Path $path -Raw
|
||||
if ($file_contents -notmatch $regex) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
if ($null -eq $regex) {
|
||||
Fail-Json $result "timeout while waiting for file $path to be absent"
|
||||
} else {
|
||||
Fail-Json $result "timeout while waiting for string regex $regex in file $path to not match"
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($null -ne $port) {
|
||||
if ($state -in @("started","present")) {
|
||||
# check that the port is online and is listening
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$port_result = Test-Port -hostname $hostname -port $port
|
||||
if ($port_result -eq $true) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to start listening"
|
||||
}
|
||||
} elseif ($state -in @("stopped","absent")) {
|
||||
# check that the port is offline and is not listening
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$port_result = Test-Port -hostname $hostname -port $port
|
||||
if ($port_result -eq $false) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to stop listening"
|
||||
}
|
||||
} elseif ($state -eq "drained") {
|
||||
# check that the local port is online but has no active connections
|
||||
$start_time = Get-Date
|
||||
$complete = $false
|
||||
while (((Get-Date) - $start_time).TotalSeconds -lt $timeout) {
|
||||
$attempts += 1
|
||||
$active_connections = Get-PortConnections -hostname $hostname -port $port
|
||||
if ($null -eq $active_connections) {
|
||||
$complete = $true
|
||||
break
|
||||
} elseif ($active_connections.Count -eq 0) {
|
||||
# no connections on port
|
||||
$complete = $true
|
||||
break
|
||||
} else {
|
||||
# there are listeners, check if we should ignore any hosts
|
||||
if ($null -ne $exclude_hosts) {
|
||||
$connection_info = $active_connections
|
||||
foreach ($exclude_host in $exclude_hosts) {
|
||||
try {
|
||||
$exclude_ips = [System.Net.Dns]::GetHostAddresses($exclude_host) | ForEach-Object { Write-Output $_.IPAddressToString }
|
||||
$connection_info = $connection_info | Where-Object { $_ -notin $exclude_ips }
|
||||
} catch { # ignore invalid hostnames
|
||||
Add-Warning -obj $result -message "Invalid hostname specified $exclude_host"
|
||||
}
|
||||
}
|
||||
|
||||
if ($connection_info.Count -eq 0) {
|
||||
$complete = $true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Start-Sleep -Seconds $sleep
|
||||
}
|
||||
|
||||
if ($complete -eq $false) {
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
Fail-Json $result "timeout while waiting for $($hostname):$port to drain"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$result.elapsed = ((Get-Date) - $module_start).TotalSeconds
|
||||
$result.wait_attempts = $attempts
|
||||
|
||||
Exit-Json $result
|
@ -0,0 +1,155 @@
|
||||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
# Copyright: (c) 2017, Ansible Project
|
||||
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
||||
|
||||
# this is a windows documentation stub, actual code lives in the .ps1
|
||||
# file of the same name
|
||||
|
||||
ANSIBLE_METADATA = {'metadata_version': '1.1',
|
||||
'status': ['preview'],
|
||||
'supported_by': 'community'}
|
||||
|
||||
DOCUMENTATION = r'''
|
||||
---
|
||||
module: win_wait_for
|
||||
version_added: '2.4'
|
||||
short_description: Waits for a condition before continuing
|
||||
description:
|
||||
- You can wait for a set amount of time C(timeout), this is the default if
|
||||
nothing is specified.
|
||||
- Waiting for a port to become available is useful for when services are not
|
||||
immediately available after their init scripts return which is true of
|
||||
certain Java application servers.
|
||||
- You can wait for a file to exist or not exist on the filesystem.
|
||||
- This module can also be used to wait for a regex match string to be present
|
||||
in a file.
|
||||
- You can wait for active connections to be closed before continuing on a
|
||||
local port.
|
||||
options:
|
||||
connect_timeout:
|
||||
description:
|
||||
- The maximum number of seconds to wait for a connection to happen before
|
||||
closing and retrying.
|
||||
type: int
|
||||
default: 5
|
||||
delay:
|
||||
description:
|
||||
- The number of seconds to wait before starting to poll.
|
||||
type: int
|
||||
exclude_hosts:
|
||||
description:
|
||||
- The list of hosts or IPs to ignore when looking for active TCP
|
||||
connections when C(state=drained).
|
||||
type: list
|
||||
host:
|
||||
description:
|
||||
- A resolvable hostname or IP address to wait for.
|
||||
- If C(state=drained) then it will only check for connections on the IP
|
||||
specified, you can use '0.0.0.0' to use all host IPs.
|
||||
type: str
|
||||
default: '127.0.0.1'
|
||||
path:
|
||||
description:
|
||||
- The path to a file on the filesystem to check.
|
||||
- If C(state) is present or started then it will wait until the file
|
||||
exists.
|
||||
- If C(state) is absent then it will wait until the file does not exist.
|
||||
type: path
|
||||
port:
|
||||
description:
|
||||
- The port number to poll on C(host).
|
||||
type: int
|
||||
regex:
|
||||
description:
|
||||
- Can be used to match a string in a file.
|
||||
- If C(state) is present or started then it will wait until the regex
|
||||
matches.
|
||||
- If C(state) is absent then it will wait until the regex does not match.
|
||||
- Defaults to a multiline regex.
|
||||
type: str
|
||||
aliases: [ "search_regex", "regexp" ]
|
||||
sleep:
|
||||
description:
|
||||
- Number of seconds to sleep between checks.
|
||||
type: int
|
||||
default: 1
|
||||
state:
|
||||
description:
|
||||
- When checking a port, C(started) will ensure the port is open, C(stopped)
|
||||
will check that is it closed and C(drained) will check for active
|
||||
connections.
|
||||
- When checking for a file or a search string C(present) or C(started) will
|
||||
ensure that the file or string is present, C(absent) will check that the
|
||||
file or search string is absent or removed.
|
||||
type: str
|
||||
choices: [ absent, drained, present, started, stopped ]
|
||||
default: started
|
||||
timeout:
|
||||
description:
|
||||
- The maximum number of seconds to wait for.
|
||||
type: int
|
||||
default: 300
|
||||
seealso:
|
||||
- module: wait_for
|
||||
- module: win_wait_for_process
|
||||
author:
|
||||
- Jordan Borean (@jborean93)
|
||||
'''
|
||||
|
||||
EXAMPLES = r'''
|
||||
- name: Wait 300 seconds for port 8000 to become open on the host, don't start checking for 10 seconds
|
||||
win_wait_for:
|
||||
port: 8000
|
||||
delay: 10
|
||||
|
||||
- name: Wait 150 seconds for port 8000 of any IP to close active connections
|
||||
win_wait_for:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
state: drained
|
||||
timeout: 150
|
||||
|
||||
- name: Wait for port 8000 of any IP to close active connection, ignoring certain hosts
|
||||
win_wait_for:
|
||||
host: 0.0.0.0
|
||||
port: 8000
|
||||
state: drained
|
||||
exclude_hosts: ['10.2.1.2', '10.2.1.3']
|
||||
|
||||
- name: Wait for file C:\temp\log.txt to exist before continuing
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
|
||||
- name: Wait until process complete is in the file before continuing
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
regex: process complete
|
||||
|
||||
- name: Wait until file is removed
|
||||
win_wait_for:
|
||||
path: C:\temp\log.txt
|
||||
state: absent
|
||||
|
||||
- name: Wait until port 1234 is offline but try every 10 seconds
|
||||
win_wait_for:
|
||||
port: 1234
|
||||
state: absent
|
||||
sleep: 10
|
||||
'''
|
||||
|
||||
RETURN = r'''
|
||||
wait_attempts:
|
||||
description: The number of attempts to poll the file or port before module
|
||||
finishes.
|
||||
returned: always
|
||||
type: int
|
||||
sample: 1
|
||||
elapsed:
|
||||
description: The elapsed seconds between the start of poll and the end of the
|
||||
module. This includes the delay if the option is set.
|
||||
returned: always
|
||||
type: float
|
||||
sample: 2.1406487
|
||||
'''
|
Loading…
Reference in New Issue