Include more test support plugins. (#68015)

* Include more test support plugins.

Also add missing module_utils `__init__.py` files.

* Update sanity ignores.
This commit is contained in:
Matt Clay 2020-03-04 12:20:02 -08:00 committed by GitHub
parent a51266ba85
commit 4fb7e62003
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 1522 additions and 0 deletions

View file

@ -8293,6 +8293,7 @@ test/support/windows-integration/plugins/modules/win_lineinfile.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_regedit.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_security_policy.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_shell.ps1 pslint!skip
test/support/windows-integration/plugins/modules/win_wait_for.ps1 pslint!skip
test/units/config/manager/test_find_ini_config_file.py future-import-boilerplate
test/units/contrib/inventory/test_vmware_inventory.py future-import-boilerplate
test/units/contrib/inventory/test_vmware_inventory.py metaclass-boilerplate

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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"]
'''

View file

@ -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

View file

@ -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
'''