mirror of
https://github.com/ansible-collections/community.general.git
synced 2025-08-22 14:01:42 -07:00
Relocating extras into lib/ansible/modules/ after merge
This commit is contained in:
parent
c65ba07d2c
commit
011ea55a8f
596 changed files with 0 additions and 266 deletions
0
lib/ansible/modules/clustering/__init__.py
Normal file
0
lib/ansible/modules/clustering/__init__.py
Normal file
589
lib/ansible/modules/clustering/consul.py
Normal file
589
lib/ansible/modules/clustering/consul.py
Normal file
|
@ -0,0 +1,589 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul
|
||||
short_description: "Add, modify & delete services within a consul cluster."
|
||||
description:
|
||||
- Registers services and checks for an agent with a consul cluster.
|
||||
A service is some process running on the agent node that should be advertised by
|
||||
consul's discovery mechanism. It may optionally supply a check definition,
|
||||
a periodic service test to notify the consul cluster of service's health.
|
||||
- "Checks may also be registered per node e.g. disk usage, or cpu usage and
|
||||
notify the health of the entire node to the cluster.
|
||||
Service level checks do not require a check name or id as these are derived
|
||||
by Consul from the Service name and id respectively by appending 'service:'
|
||||
Node level checks require a check_name and optionally a check_id."
|
||||
- Currently, there is no complete way to retrieve the script, interval or ttl
|
||||
metadata for a registered check. Without this metadata it is not possible to
|
||||
tell if the data supplied with ansible represents a change to a check. As a
|
||||
result this does not attempt to determine changes and will always report a
|
||||
changed occurred. An api method is planned to supply this metadata so at that
|
||||
stage change management will be added.
|
||||
- "See http://consul.io for more details."
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- python-consul
|
||||
- requests
|
||||
version_added: "2.0"
|
||||
author: "Steve Gargan (@sgargan)"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the consul service, defaults to present
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
service_name:
|
||||
description:
|
||||
- Unique name for the service on a node, must be unique per node,
|
||||
required if registering a service. May be ommitted if registering
|
||||
a node level check
|
||||
required: false
|
||||
service_id:
|
||||
description:
|
||||
- the ID for the service, must be unique per node, defaults to the
|
||||
service name if the service name is supplied
|
||||
required: false
|
||||
default: service_name if supplied
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent defaults to localhost
|
||||
required: false
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- the port on which the consul agent is running
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- the protocol scheme on which the consul agent is running
|
||||
required: false
|
||||
default: http
|
||||
version_added: "2.1"
|
||||
validate_certs:
|
||||
description:
|
||||
- whether to verify the tls certificate of the consul agent
|
||||
required: false
|
||||
default: True
|
||||
version_added: "2.1"
|
||||
notes:
|
||||
description:
|
||||
- Notes to attach to check when registering it.
|
||||
required: false
|
||||
default: None
|
||||
service_port:
|
||||
description:
|
||||
- the port on which the service is listening required for
|
||||
registration of a service, i.e. if service_name or service_id is set
|
||||
required: false
|
||||
service_address:
|
||||
description:
|
||||
- the address to advertise that the service will be listening on.
|
||||
This value will be passed as the I(Address) parameter to Consul's
|
||||
U(/v1/agent/service/register) API method, so refer to the Consul API
|
||||
documentation for further details.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.1"
|
||||
tags:
|
||||
description:
|
||||
- a list of tags that will be attached to the service registration.
|
||||
required: false
|
||||
default: None
|
||||
script:
|
||||
description:
|
||||
- the script/command that will be run periodically to check the health
|
||||
of the service. Scripts require an interval and vise versa
|
||||
required: false
|
||||
default: None
|
||||
interval:
|
||||
description:
|
||||
- the interval at which the service check will be run. This is a number
|
||||
with a s or m suffix to signify the units of seconds or minutes e.g
|
||||
15s or 1m. If no suffix is supplied, m will be used by default e.g.
|
||||
1 will be 1m. Required if the script param is specified.
|
||||
required: false
|
||||
default: None
|
||||
check_id:
|
||||
description:
|
||||
- an ID for the service check, defaults to the check name, ignored if
|
||||
part of a service definition.
|
||||
required: false
|
||||
default: None
|
||||
check_name:
|
||||
description:
|
||||
- a name for the service check, defaults to the check id. required if
|
||||
standalone, ignored if part of service definition.
|
||||
required: false
|
||||
default: None
|
||||
ttl:
|
||||
description:
|
||||
- checks can be registered with a ttl instead of a script and interval
|
||||
this means that the service will check in with the agent before the
|
||||
ttl expires. If it doesn't the check will be considered failed.
|
||||
Required if registering a check and the script an interval are missing
|
||||
Similar to the interval this is a number with a s or m suffix to
|
||||
signify the units of seconds or minutes e.g 15s or 1m. If no suffix
|
||||
is supplied, m will be used by default e.g. 1 will be 1m
|
||||
required: false
|
||||
default: None
|
||||
http:
|
||||
description:
|
||||
- checks can be registered with an http endpoint. This means that consul
|
||||
will check that the http endpoint returns a successful http status.
|
||||
Interval must also be provided with this option.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.0"
|
||||
timeout:
|
||||
description:
|
||||
- A custom HTTP check timeout. The consul default is 10 seconds.
|
||||
Similar to the interval this is a number with a s or m suffix to
|
||||
signify the units of seconds or minutes, e.g. 15s or 1m.
|
||||
required: false
|
||||
default: None
|
||||
version_added: "2.0"
|
||||
token:
|
||||
description:
|
||||
- the token key indentifying an ACL rule set. May be required to register services.
|
||||
required: false
|
||||
default: None
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: register nginx service with the local consul agent
|
||||
consul:
|
||||
service_name: nginx
|
||||
service_port: 80
|
||||
|
||||
- name: register nginx service with curl check
|
||||
consul:
|
||||
service_name: nginx
|
||||
service_port: 80
|
||||
script: "curl http://localhost"
|
||||
interval: 60s
|
||||
|
||||
- name: register nginx with an http check
|
||||
consul:
|
||||
service_name: nginx
|
||||
service_port: 80
|
||||
interval: 60s
|
||||
http: "http://localhost:80/status"
|
||||
|
||||
- name: register external service nginx available at 10.1.5.23
|
||||
consul:
|
||||
service_name: nginx
|
||||
service_port: 80
|
||||
service_address: 10.1.5.23
|
||||
|
||||
- name: register nginx with some service tags
|
||||
consul:
|
||||
service_name: nginx
|
||||
service_port: 80
|
||||
tags:
|
||||
- prod
|
||||
- webservers
|
||||
|
||||
- name: remove nginx service
|
||||
consul:
|
||||
service_name: nginx
|
||||
state: absent
|
||||
|
||||
- name: create a node level check to test disk usage
|
||||
consul:
|
||||
check_name: Disk usage
|
||||
check_id: disk_usage
|
||||
script: "/opt/disk_usage.py"
|
||||
interval: 5m
|
||||
|
||||
- name: register an http check against a service that's already registered
|
||||
consul:
|
||||
check_name: nginx-check2
|
||||
check_id: nginx-check2
|
||||
service_id: nginx
|
||||
interval: 60s
|
||||
http: "http://localhost:80/morestatus"
|
||||
|
||||
'''
|
||||
|
||||
try:
|
||||
import consul
|
||||
from requests.exceptions import ConnectionError
|
||||
python_consul_installed = True
|
||||
except ImportError:
|
||||
python_consul_installed = False
|
||||
|
||||
def register_with_consul(module):
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state == 'present':
|
||||
add(module)
|
||||
else:
|
||||
remove(module)
|
||||
|
||||
|
||||
def add(module):
|
||||
''' adds a service or a check depending on supplied configuration'''
|
||||
check = parse_check(module)
|
||||
service = parse_service(module)
|
||||
|
||||
if not service and not check:
|
||||
module.fail_json(msg='a name and port are required to register a service')
|
||||
|
||||
if service:
|
||||
if check:
|
||||
service.add_check(check)
|
||||
add_service(module, service)
|
||||
elif check:
|
||||
add_check(module, check)
|
||||
|
||||
|
||||
def remove(module):
|
||||
''' removes a service or a check '''
|
||||
service_id = module.params.get('service_id') or module.params.get('service_name')
|
||||
check_id = module.params.get('check_id') or module.params.get('check_name')
|
||||
if not (service_id or check_id):
|
||||
module.fail_json(msg='services and checks are removed by id or name. please supply a service id/name or a check id/name')
|
||||
if service_id:
|
||||
remove_service(module, service_id)
|
||||
else:
|
||||
remove_check(module, check_id)
|
||||
|
||||
|
||||
def add_check(module, check):
|
||||
''' registers a check with the given agent. currently there is no way
|
||||
retrieve the full metadata of an existing check through the consul api.
|
||||
Without this we can't compare to the supplied check and so we must assume
|
||||
a change. '''
|
||||
if not check.name and not service_id:
|
||||
module.fail_json(msg='a check name is required for a node level check, one not attached to a service')
|
||||
|
||||
consul_api = get_consul_api(module)
|
||||
check.register(consul_api)
|
||||
|
||||
module.exit_json(changed=True,
|
||||
check_id=check.check_id,
|
||||
check_name=check.name,
|
||||
script=check.script,
|
||||
interval=check.interval,
|
||||
ttl=check.ttl,
|
||||
http=check.http,
|
||||
timeout=check.timeout,
|
||||
service_id=check.service_id)
|
||||
|
||||
|
||||
def remove_check(module, check_id):
|
||||
''' removes a check using its id '''
|
||||
consul_api = get_consul_api(module)
|
||||
|
||||
if check_id in consul_api.agent.checks():
|
||||
consul_api.agent.check.deregister(check_id)
|
||||
module.exit_json(changed=True, id=check_id)
|
||||
|
||||
module.exit_json(changed=False, id=check_id)
|
||||
|
||||
|
||||
def add_service(module, service):
|
||||
''' registers a service with the the current agent '''
|
||||
result = service
|
||||
changed = False
|
||||
|
||||
consul_api = get_consul_api(module)
|
||||
existing = get_service_by_id_or_name(consul_api, service.id)
|
||||
|
||||
# there is no way to retrieve the details of checks so if a check is present
|
||||
# in the service it must be re-registered
|
||||
if service.has_checks() or not existing or not existing == service:
|
||||
|
||||
service.register(consul_api)
|
||||
# check that it registered correctly
|
||||
registered = get_service_by_id_or_name(consul_api, service.id)
|
||||
if registered:
|
||||
result = registered
|
||||
changed = True
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
service_id=result.id,
|
||||
service_name=result.name,
|
||||
service_port=result.port,
|
||||
checks=[check.to_dict() for check in service.checks],
|
||||
tags=result.tags)
|
||||
|
||||
|
||||
def remove_service(module, service_id):
|
||||
''' deregister a service from the given agent using its service id '''
|
||||
consul_api = get_consul_api(module)
|
||||
service = get_service_by_id_or_name(consul_api, service_id)
|
||||
if service:
|
||||
consul_api.agent.service.deregister(service_id)
|
||||
module.exit_json(changed=True, id=service_id)
|
||||
|
||||
module.exit_json(changed=False, id=service_id)
|
||||
|
||||
|
||||
def get_consul_api(module, token=None):
|
||||
return consul.Consul(host=module.params.get('host'),
|
||||
port=module.params.get('port'),
|
||||
scheme=module.params.get('scheme'),
|
||||
verify=module.params.get('validate_certs'),
|
||||
token=module.params.get('token'))
|
||||
|
||||
|
||||
def get_service_by_id_or_name(consul_api, service_id_or_name):
|
||||
''' iterate the registered services and find one with the given id '''
|
||||
for name, service in consul_api.agent.services().iteritems():
|
||||
if service['ID'] == service_id_or_name or service['Service'] == service_id_or_name:
|
||||
return ConsulService(loaded=service)
|
||||
|
||||
|
||||
def parse_check(module):
|
||||
|
||||
if len(filter(None, [module.params.get('script'), module.params.get('ttl'), module.params.get('http')])) > 1:
|
||||
module.fail_json(
|
||||
msg='check are either script, http or ttl driven, supplying more than one does not make sense')
|
||||
|
||||
if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl') or module.params.get('http'):
|
||||
|
||||
return ConsulCheck(
|
||||
module.params.get('check_id'),
|
||||
module.params.get('check_name'),
|
||||
module.params.get('check_node'),
|
||||
module.params.get('check_host'),
|
||||
module.params.get('script'),
|
||||
module.params.get('interval'),
|
||||
module.params.get('ttl'),
|
||||
module.params.get('notes'),
|
||||
module.params.get('http'),
|
||||
module.params.get('timeout'),
|
||||
module.params.get('service_id'),
|
||||
)
|
||||
|
||||
|
||||
def parse_service(module):
|
||||
|
||||
if module.params.get('service_name') and module.params.get('service_port'):
|
||||
return ConsulService(
|
||||
module.params.get('service_id'),
|
||||
module.params.get('service_name'),
|
||||
module.params.get('service_address'),
|
||||
module.params.get('service_port'),
|
||||
module.params.get('tags'),
|
||||
)
|
||||
elif module.params.get('service_name') and not module.params.get('service_port'):
|
||||
|
||||
module.fail_json( msg="service_name supplied but no service_port, a port is required to configure a service. Did you configure the 'port' argument meaning 'service_port'?")
|
||||
|
||||
|
||||
class ConsulService():
|
||||
|
||||
def __init__(self, service_id=None, name=None, address=None, port=-1,
|
||||
tags=None, loaded=None):
|
||||
self.id = self.name = name
|
||||
if service_id:
|
||||
self.id = service_id
|
||||
self.address = address
|
||||
self.port = port
|
||||
self.tags = tags
|
||||
self.checks = []
|
||||
if loaded:
|
||||
self.id = loaded['ID']
|
||||
self.name = loaded['Service']
|
||||
self.port = loaded['Port']
|
||||
self.tags = loaded['Tags']
|
||||
|
||||
def register(self, consul_api):
|
||||
if len(self.checks) > 0:
|
||||
check = self.checks[0]
|
||||
|
||||
consul_api.agent.service.register(
|
||||
self.name,
|
||||
service_id=self.id,
|
||||
address=self.address,
|
||||
port=self.port,
|
||||
tags=self.tags,
|
||||
check=check.check)
|
||||
else:
|
||||
consul_api.agent.service.register(
|
||||
self.name,
|
||||
service_id=self.id,
|
||||
address=self.address,
|
||||
port=self.port,
|
||||
tags=self.tags)
|
||||
|
||||
def add_check(self, check):
|
||||
self.checks.append(check)
|
||||
|
||||
def checks(self):
|
||||
return self.checks
|
||||
|
||||
def has_checks(self):
|
||||
return len(self.checks) > 0
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.id == other.id
|
||||
and self.name == other.name
|
||||
and self.port == other.port
|
||||
and self.tags == other.tags)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def to_dict(self):
|
||||
data = {'id': self.id, "name": self.name}
|
||||
if self.port:
|
||||
data['port'] = self.port
|
||||
if self.tags and len(self.tags) > 0:
|
||||
data['tags'] = self.tags
|
||||
if len(self.checks) > 0:
|
||||
data['check'] = self.checks[0].to_dict()
|
||||
return data
|
||||
|
||||
|
||||
class ConsulCheck():
|
||||
|
||||
def __init__(self, check_id, name, node=None, host='localhost',
|
||||
script=None, interval=None, ttl=None, notes=None, http=None, timeout=None, service_id=None):
|
||||
self.check_id = self.name = name
|
||||
if check_id:
|
||||
self.check_id = check_id
|
||||
self.service_id = service_id
|
||||
self.notes = notes
|
||||
self.node = node
|
||||
self.host = host
|
||||
|
||||
self.interval = self.validate_duration('interval', interval)
|
||||
self.ttl = self.validate_duration('ttl', ttl)
|
||||
self.script = script
|
||||
self.http = http
|
||||
self.timeout = self.validate_duration('timeout', timeout)
|
||||
|
||||
self.check = None
|
||||
|
||||
if script:
|
||||
self.check = consul.Check.script(script, self.interval)
|
||||
|
||||
if ttl:
|
||||
self.check = consul.Check.ttl(self.ttl)
|
||||
|
||||
if http:
|
||||
if interval is None:
|
||||
raise Exception('http check must specify interval')
|
||||
|
||||
self.check = consul.Check.http(http, self.interval, self.timeout)
|
||||
|
||||
|
||||
def validate_duration(self, name, duration):
|
||||
if duration:
|
||||
duration_units = ['ns', 'us', 'ms', 's', 'm', 'h']
|
||||
if not any((duration.endswith(suffix) for suffix in duration_units)):
|
||||
duration = "{}s".format(duration)
|
||||
return duration
|
||||
|
||||
def register(self, consul_api):
|
||||
consul_api.agent.check.register(self.name, check_id=self.check_id, service_id=self.service_id,
|
||||
notes=self.notes,
|
||||
check=self.check)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.check_id == other.check_id
|
||||
and self.service_id == other.service_id
|
||||
and self.name == other.name
|
||||
and self.script == script
|
||||
and self.interval == interval)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def to_dict(self):
|
||||
data = {}
|
||||
self._add(data, 'id', attr='check_id')
|
||||
self._add(data, 'name', attr='check_name')
|
||||
self._add(data, 'script')
|
||||
self._add(data, 'node')
|
||||
self._add(data, 'notes')
|
||||
self._add(data, 'host')
|
||||
self._add(data, 'interval')
|
||||
self._add(data, 'ttl')
|
||||
self._add(data, 'http')
|
||||
self._add(data, 'timeout')
|
||||
self._add(data, 'service_id')
|
||||
return data
|
||||
|
||||
def _add(self, data, key, attr=None):
|
||||
try:
|
||||
if attr is None:
|
||||
attr = key
|
||||
data[key] = getattr(self, attr)
|
||||
except:
|
||||
pass
|
||||
|
||||
def test_dependencies(module):
|
||||
if not python_consul_installed:
|
||||
module.fail_json(msg="python-consul required for this module. see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
host=dict(default='localhost'),
|
||||
port=dict(default=8500, type='int'),
|
||||
scheme=dict(required=False, default='http'),
|
||||
validate_certs=dict(required=False, default=True, type='bool'),
|
||||
check_id=dict(required=False),
|
||||
check_name=dict(required=False),
|
||||
check_node=dict(required=False),
|
||||
check_host=dict(required=False),
|
||||
notes=dict(required=False),
|
||||
script=dict(required=False),
|
||||
service_id=dict(required=False),
|
||||
service_name=dict(required=False),
|
||||
service_address=dict(required=False, type='str', default=None),
|
||||
service_port=dict(required=False, type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
interval=dict(required=False, type='str'),
|
||||
ttl=dict(required=False, type='str'),
|
||||
http=dict(required=False, type='str'),
|
||||
timeout=dict(required=False, type='str'),
|
||||
tags=dict(required=False, type='list'),
|
||||
token=dict(required=False, no_log=True)
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
|
||||
test_dependencies(module)
|
||||
|
||||
try:
|
||||
register_with_consul(module)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
module.params.get('host'), module.params.get('port'), str(e)))
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
367
lib/ansible/modules/clustering/consul_acl.py
Normal file
367
lib/ansible/modules/clustering/consul_acl.py
Normal file
|
@ -0,0 +1,367 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul_acl
|
||||
short_description: "manipulate consul acl keys and rules"
|
||||
description:
|
||||
- allows the addition, modification and deletion of ACL keys and associated
|
||||
rules in a consul cluster via the agent. For more details on using and
|
||||
configuring ACLs, see https://www.consul.io/docs/internals/acl.html.
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- python-consul
|
||||
- pyhcl
|
||||
- requests
|
||||
version_added: "2.0"
|
||||
author: "Steve Gargan (@sgargan)"
|
||||
options:
|
||||
mgmt_token:
|
||||
description:
|
||||
- a management token is required to manipulate the acl lists
|
||||
state:
|
||||
description:
|
||||
- whether the ACL pair should be present or absent
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
token_type:
|
||||
description:
|
||||
- the type of token that should be created, either management or
|
||||
client
|
||||
choices: ['client', 'management']
|
||||
default: client
|
||||
name:
|
||||
description:
|
||||
- the name that should be associated with the acl key, this is opaque
|
||||
to Consul
|
||||
required: false
|
||||
token:
|
||||
description:
|
||||
- the token key indentifying an ACL rule set. If generated by consul
|
||||
this will be a UUID.
|
||||
required: false
|
||||
rules:
|
||||
description:
|
||||
- an list of the rules that should be associated with a given token.
|
||||
required: false
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent defaults to localhost
|
||||
required: false
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- the port on which the consul agent is running
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- the protocol scheme on which the consul agent is running
|
||||
required: false
|
||||
default: http
|
||||
version_added: "2.1"
|
||||
validate_certs:
|
||||
description:
|
||||
- whether to verify the tls certificate of the consul agent
|
||||
required: false
|
||||
default: True
|
||||
version_added: "2.1"
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: create an acl token with rules
|
||||
consul_acl:
|
||||
mgmt_token: 'some_management_acl'
|
||||
host: 'consul1.mycluster.io'
|
||||
name: 'Foo access'
|
||||
rules:
|
||||
- key: 'foo'
|
||||
policy: read
|
||||
- key: 'private/foo'
|
||||
policy: deny
|
||||
|
||||
- name: create an acl with specific token with both key and serivce rules
|
||||
consul_acl:
|
||||
mgmt_token: 'some_management_acl'
|
||||
name: 'Foo access'
|
||||
token: 'some_client_token'
|
||||
rules:
|
||||
- key: 'foo'
|
||||
policy: read
|
||||
- service: ''
|
||||
policy: write
|
||||
- service: 'secret-'
|
||||
policy: deny
|
||||
|
||||
- name: remove a token
|
||||
consul_acl:
|
||||
mgmt_token: 'some_management_acl'
|
||||
host: 'consul1.mycluster.io'
|
||||
token: '172bd5c8-9fe9-11e4-b1b0-3c15c2c9fd5e'
|
||||
state: absent
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import consul
|
||||
from requests.exceptions import ConnectionError
|
||||
python_consul_installed = True
|
||||
except ImportError:
|
||||
python_consul_installed = False
|
||||
|
||||
try:
|
||||
import hcl
|
||||
pyhcl_installed = True
|
||||
except ImportError:
|
||||
pyhcl_installed = False
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
def execute(module):
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state == 'present':
|
||||
update_acl(module)
|
||||
else:
|
||||
remove_acl(module)
|
||||
|
||||
|
||||
def update_acl(module):
|
||||
|
||||
rules = module.params.get('rules')
|
||||
state = module.params.get('state')
|
||||
token = module.params.get('token')
|
||||
token_type = module.params.get('token_type')
|
||||
mgmt = module.params.get('mgmt_token')
|
||||
name = module.params.get('name')
|
||||
consul = get_consul_api(module, mgmt)
|
||||
changed = False
|
||||
|
||||
try:
|
||||
|
||||
if token:
|
||||
existing_rules = load_rules_for_token(module, consul, token)
|
||||
supplied_rules = yml_to_rules(module, rules)
|
||||
changed = not existing_rules == supplied_rules
|
||||
if changed:
|
||||
y = supplied_rules.to_hcl()
|
||||
token = consul.acl.update(
|
||||
token,
|
||||
name=name,
|
||||
type=token_type,
|
||||
rules=supplied_rules.to_hcl())
|
||||
else:
|
||||
try:
|
||||
rules = yml_to_rules(module, rules)
|
||||
if rules.are_rules():
|
||||
rules = rules.to_hcl()
|
||||
else:
|
||||
rules = None
|
||||
|
||||
token = consul.acl.create(
|
||||
name=name, type=token_type, rules=rules)
|
||||
changed = True
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="No token returned, check your managment key and that \
|
||||
the host is in the acl datacenter %s" % e)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Could not create/update acl %s" % e)
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
token=token,
|
||||
rules=rules,
|
||||
name=name,
|
||||
type=token_type)
|
||||
|
||||
|
||||
def remove_acl(module):
|
||||
state = module.params.get('state')
|
||||
token = module.params.get('token')
|
||||
mgmt = module.params.get('mgmt_token')
|
||||
|
||||
consul = get_consul_api(module, token=mgmt)
|
||||
changed = token and consul.acl.info(token)
|
||||
if changed:
|
||||
token = consul.acl.destroy(token)
|
||||
|
||||
module.exit_json(changed=changed, token=token)
|
||||
|
||||
def load_rules_for_token(module, consul_api, token):
|
||||
try:
|
||||
rules = Rules()
|
||||
info = consul_api.acl.info(token)
|
||||
if info and info['Rules']:
|
||||
rule_set = hcl.loads(to_ascii(info['Rules']))
|
||||
for rule_type in rule_set:
|
||||
for pattern, policy in rule_set[rule_type].iteritems():
|
||||
rules.add_rule(rule_type, Rule(pattern, policy['policy']))
|
||||
return rules
|
||||
except Exception as e:
|
||||
module.fail_json(
|
||||
msg="Could not load rule list from retrieved rule data %s, %s" % (
|
||||
token, e))
|
||||
|
||||
return json_to_rules(module, loaded)
|
||||
|
||||
def to_ascii(unicode_string):
|
||||
if isinstance(unicode_string, unicode):
|
||||
return unicode_string.encode('ascii', 'ignore')
|
||||
return unicode_string
|
||||
|
||||
def yml_to_rules(module, yml_rules):
|
||||
rules = Rules()
|
||||
if yml_rules:
|
||||
for rule in yml_rules:
|
||||
if ('key' in rule and 'policy' in rule):
|
||||
rules.add_rule('key', Rule(rule['key'], rule['policy']))
|
||||
elif ('service' in rule and 'policy' in rule):
|
||||
rules.add_rule('service', Rule(rule['service'], rule['policy']))
|
||||
elif ('event' in rule and 'policy' in rule):
|
||||
rules.add_rule('event', Rule(rule['event'], rule['policy']))
|
||||
elif ('query' in rule and 'policy' in rule):
|
||||
rules.add_rule('query', Rule(rule['query'], rule['policy']))
|
||||
else:
|
||||
module.fail_json(msg="a rule requires a key/service/event or query and a policy.")
|
||||
return rules
|
||||
|
||||
template = '''%s "%s" {
|
||||
policy = "%s"
|
||||
}
|
||||
'''
|
||||
|
||||
RULE_TYPES = ['key', 'service', 'event', 'query']
|
||||
|
||||
class Rules:
|
||||
|
||||
def __init__(self):
|
||||
self.rules = {}
|
||||
for rule_type in RULE_TYPES:
|
||||
self.rules[rule_type] = {}
|
||||
|
||||
def add_rule(self, rule_type, rule):
|
||||
self.rules[rule_type][rule.pattern] = rule
|
||||
|
||||
def are_rules(self):
|
||||
return len(self) > 0
|
||||
|
||||
def to_hcl(self):
|
||||
|
||||
rules = ""
|
||||
for rule_type in RULE_TYPES:
|
||||
for pattern, rule in self.rules[rule_type].iteritems():
|
||||
rules += template % (rule_type, pattern, rule.policy)
|
||||
return to_ascii(rules)
|
||||
|
||||
def __len__(self):
|
||||
count = 0
|
||||
for rule_type in RULE_TYPES:
|
||||
count += len(self.rules[rule_type])
|
||||
return count
|
||||
|
||||
def __eq__(self, other):
|
||||
if not (other or isinstance(other, self.__class__)
|
||||
or len(other) == len(self)):
|
||||
return False
|
||||
|
||||
for rule_type in RULE_TYPES:
|
||||
for name, other_rule in other.rules[rule_type].iteritems():
|
||||
if not name in self.rules[rule_type]:
|
||||
return False
|
||||
rule = self.rules[rule_type][name]
|
||||
|
||||
if not (rule and rule == other_rule):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return self.to_hcl()
|
||||
|
||||
class Rule:
|
||||
|
||||
def __init__(self, pattern, policy):
|
||||
self.pattern = pattern
|
||||
self.policy = policy
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.pattern == other.pattern
|
||||
and self.policy == other.policy)
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.pattern) ^ hash(self.policy)
|
||||
|
||||
def __str__(self):
|
||||
return '%s %s' % (self.pattern, self.policy)
|
||||
|
||||
def get_consul_api(module, token=None):
|
||||
if not token:
|
||||
token = module.params.get('token')
|
||||
return consul.Consul(host=module.params.get('host'),
|
||||
port=module.params.get('port'),
|
||||
scheme=module.params.get('scheme'),
|
||||
verify=module.params.get('validate_certs'),
|
||||
token=token)
|
||||
|
||||
def test_dependencies(module):
|
||||
if not python_consul_installed:
|
||||
module.fail_json(msg="python-consul required for this module. "\
|
||||
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||
|
||||
if not pyhcl_installed:
|
||||
module.fail_json( msg="pyhcl required for this module."\
|
||||
" see https://pypi.python.org/pypi/pyhcl")
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
mgmt_token=dict(required=True, no_log=True),
|
||||
host=dict(default='localhost'),
|
||||
scheme=dict(required=False, default='http'),
|
||||
validate_certs=dict(required=False, type='bool', default=True),
|
||||
name=dict(required=False),
|
||||
port=dict(default=8500, type='int'),
|
||||
rules=dict(default=None, required=False, type='list'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
token=dict(required=False, no_log=True),
|
||||
token_type=dict(
|
||||
required=False, choices=['client', 'management'], default='client')
|
||||
)
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||
|
||||
test_dependencies(module)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
module.params.get('host'), module.params.get('port'), str(e)))
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
293
lib/ansible/modules/clustering/consul_kv.py
Normal file
293
lib/ansible/modules/clustering/consul_kv.py
Normal file
|
@ -0,0 +1,293 @@
|
|||
#!/usr/bin/python
|
||||
#
|
||||
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul_kv
|
||||
short_description: Manipulate entries in the key/value store of a consul cluster.
|
||||
description:
|
||||
- Allows the addition, modification and deletion of key/value entries in a
|
||||
consul cluster via the agent. The entire contents of the record, including
|
||||
the indices, flags and session are returned as 'value'.
|
||||
- If the key represents a prefix then Note that when a value is removed, the existing
|
||||
value if any is returned as part of the results.
|
||||
- "See http://www.consul.io/docs/agent/http.html#kv for more details."
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- python-consul
|
||||
- requests
|
||||
version_added: "2.0"
|
||||
author: "Steve Gargan (@sgargan)"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- the action to take with the supplied key and value. If the state is
|
||||
'present', the key contents will be set to the value supplied,
|
||||
'changed' will be set to true only if the value was different to the
|
||||
current contents. The state 'absent' will remove the key/value pair,
|
||||
again 'changed' will be set to true only if the key actually existed
|
||||
prior to the removal. An attempt can be made to obtain or free the
|
||||
lock associated with a key/value pair with the states 'acquire' or
|
||||
'release' respectively. a valid session must be supplied to make the
|
||||
attempt changed will be true if the attempt is successful, false
|
||||
otherwise.
|
||||
required: false
|
||||
choices: ['present', 'absent', 'acquire', 'release']
|
||||
default: present
|
||||
key:
|
||||
description:
|
||||
- the key at which the value should be stored.
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- the value should be associated with the given key, required if state
|
||||
is present
|
||||
required: true
|
||||
recurse:
|
||||
description:
|
||||
- if the key represents a prefix, each entry with the prefix can be
|
||||
retrieved by setting this to true.
|
||||
required: false
|
||||
default: false
|
||||
session:
|
||||
description:
|
||||
- the session that should be used to acquire or release a lock
|
||||
associated with a key/value pair
|
||||
required: false
|
||||
default: None
|
||||
token:
|
||||
description:
|
||||
- the token key indentifying an ACL rule set that controls access to
|
||||
the key value pair
|
||||
required: false
|
||||
default: None
|
||||
cas:
|
||||
description:
|
||||
- used when acquiring a lock with a session. If the cas is 0, then
|
||||
Consul will only put the key if it does not already exist. If the
|
||||
cas value is non-zero, then the key is only set if the index matches
|
||||
the ModifyIndex of that key.
|
||||
required: false
|
||||
default: None
|
||||
flags:
|
||||
description:
|
||||
- opaque integer value that can be passed when setting a value.
|
||||
required: false
|
||||
default: None
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent defaults to localhost
|
||||
required: false
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- the port on which the consul agent is running
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- the protocol scheme on which the consul agent is running
|
||||
required: false
|
||||
default: http
|
||||
version_added: "2.1"
|
||||
validate_certs:
|
||||
description:
|
||||
- whether to verify the tls certificate of the consul agent
|
||||
required: false
|
||||
default: True
|
||||
version_added: "2.1"
|
||||
"""
|
||||
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
- name: add or update the value associated with a key in the key/value store
|
||||
consul_kv:
|
||||
key: somekey
|
||||
value: somevalue
|
||||
|
||||
- name: remove a key from the store
|
||||
consul_kv:
|
||||
key: somekey
|
||||
state: absent
|
||||
|
||||
- name: add a node to an arbitrary group via consul inventory (see consul.ini)
|
||||
consul_kv:
|
||||
key: ansible/groups/dc1/somenode
|
||||
value: 'top_secret'
|
||||
|
||||
- name: Register a key/value pair with an associated session
|
||||
consul_kv:
|
||||
key: stg/node/server_birthday
|
||||
value: 20160509
|
||||
session: "{{ sessionid }}"
|
||||
state: acquire
|
||||
'''
|
||||
|
||||
import sys
|
||||
|
||||
try:
|
||||
import consul
|
||||
from requests.exceptions import ConnectionError
|
||||
python_consul_installed = True
|
||||
except ImportError:
|
||||
python_consul_installed = False
|
||||
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
def execute(module):
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state == 'acquire' or state == 'release':
|
||||
lock(module, state)
|
||||
if state == 'present':
|
||||
add_value(module)
|
||||
else:
|
||||
remove_value(module)
|
||||
|
||||
|
||||
def lock(module, state):
|
||||
|
||||
consul_api = get_consul_api(module)
|
||||
|
||||
session = module.params.get('session')
|
||||
key = module.params.get('key')
|
||||
value = module.params.get('value')
|
||||
|
||||
if not session:
|
||||
module.fail(
|
||||
msg='%s of lock for %s requested but no session supplied' %
|
||||
(state, key))
|
||||
|
||||
index, existing = consul_api.kv.get(key)
|
||||
|
||||
changed = not existing or (existing and existing['Value'] != value)
|
||||
if changed and not module.check_mode:
|
||||
if state == 'acquire':
|
||||
changed = consul_api.kv.put(key, value,
|
||||
cas=module.params.get('cas'),
|
||||
acquire=session,
|
||||
flags=module.params.get('flags'))
|
||||
else:
|
||||
changed = consul_api.kv.put(key, value,
|
||||
cas=module.params.get('cas'),
|
||||
release=session,
|
||||
flags=module.params.get('flags'))
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
index=index,
|
||||
key=key)
|
||||
|
||||
|
||||
def add_value(module):
|
||||
|
||||
consul_api = get_consul_api(module)
|
||||
|
||||
key = module.params.get('key')
|
||||
value = module.params.get('value')
|
||||
|
||||
index, existing = consul_api.kv.get(key)
|
||||
|
||||
changed = not existing or (existing and existing['Value'] != value)
|
||||
if changed and not module.check_mode:
|
||||
changed = consul_api.kv.put(key, value,
|
||||
cas=module.params.get('cas'),
|
||||
flags=module.params.get('flags'))
|
||||
|
||||
if module.params.get('retrieve'):
|
||||
index, stored = consul_api.kv.get(key)
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
index=index,
|
||||
key=key,
|
||||
data=stored)
|
||||
|
||||
|
||||
def remove_value(module):
|
||||
''' remove the value associated with the given key. if the recurse parameter
|
||||
is set then any key prefixed with the given key will be removed. '''
|
||||
consul_api = get_consul_api(module)
|
||||
|
||||
key = module.params.get('key')
|
||||
value = module.params.get('value')
|
||||
|
||||
index, existing = consul_api.kv.get(
|
||||
key, recurse=module.params.get('recurse'))
|
||||
|
||||
changed = existing != None
|
||||
if changed and not module.check_mode:
|
||||
consul_api.kv.delete(key, module.params.get('recurse'))
|
||||
|
||||
module.exit_json(changed=changed,
|
||||
index=index,
|
||||
key=key,
|
||||
data=existing)
|
||||
|
||||
|
||||
def get_consul_api(module, token=None):
|
||||
return consul.Consul(host=module.params.get('host'),
|
||||
port=module.params.get('port'),
|
||||
scheme=module.params.get('scheme'),
|
||||
verify=module.params.get('validate_certs'),
|
||||
token=module.params.get('token'))
|
||||
|
||||
def test_dependencies(module):
|
||||
if not python_consul_installed:
|
||||
module.fail_json(msg="python-consul required for this module. "\
|
||||
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = dict(
|
||||
cas=dict(required=False),
|
||||
flags=dict(required=False),
|
||||
key=dict(required=True),
|
||||
host=dict(default='localhost'),
|
||||
scheme=dict(required=False, default='http'),
|
||||
validate_certs=dict(required=False, type='bool', default=True),
|
||||
port=dict(default=8500, type='int'),
|
||||
recurse=dict(required=False, type='bool'),
|
||||
retrieve=dict(required=False, type='bool', default=True),
|
||||
state=dict(default='present', choices=['present', 'absent', 'acquire', 'release']),
|
||||
token=dict(required=False, no_log=True),
|
||||
value=dict(required=False),
|
||||
session=dict(required=False)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||
|
||||
test_dependencies(module)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
module.params.get('host'), module.params.get('port'), str(e)))
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
286
lib/ansible/modules/clustering/consul_session.py
Normal file
286
lib/ansible/modules/clustering/consul_session.py
Normal file
|
@ -0,0 +1,286 @@
|
|||
#!/usr/bin/python
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# (c) 2015, Steve Gargan <steve.gargan@gmail.com>
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul_session
|
||||
short_description: "manipulate consul sessions"
|
||||
description:
|
||||
- allows the addition, modification and deletion of sessions in a consul
|
||||
cluster. These sessions can then be used in conjunction with key value pairs
|
||||
to implement distributed locks. In depth documentation for working with
|
||||
sessions can be found here http://www.consul.io/docs/internals/sessions.html
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- python-consul
|
||||
- requests
|
||||
version_added: "2.0"
|
||||
author: "Steve Gargan @sgargan"
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- whether the session should be present i.e. created if it doesn't
|
||||
exist, or absent, removed if present. If created, the ID for the
|
||||
session is returned in the output. If absent, the name or ID is
|
||||
required to remove the session. Info for a single session, all the
|
||||
sessions for a node or all available sessions can be retrieved by
|
||||
specifying info, node or list for the state; for node or info, the
|
||||
node name or session id is required as parameter.
|
||||
required: false
|
||||
choices: ['present', 'absent', 'info', 'node', 'list']
|
||||
default: present
|
||||
name:
|
||||
description:
|
||||
- the name that should be associated with the session. This is opaque
|
||||
to Consul and not required.
|
||||
required: false
|
||||
default: None
|
||||
delay:
|
||||
description:
|
||||
- the optional lock delay that can be attached to the session when it
|
||||
is created. Locks for invalidated sessions ar blocked from being
|
||||
acquired until this delay has expired. Durations are in seconds
|
||||
default: 15
|
||||
required: false
|
||||
node:
|
||||
description:
|
||||
- the name of the node that with which the session will be associated.
|
||||
by default this is the name of the agent.
|
||||
required: false
|
||||
default: None
|
||||
datacenter:
|
||||
description:
|
||||
- name of the datacenter in which the session exists or should be
|
||||
created.
|
||||
required: false
|
||||
default: None
|
||||
checks:
|
||||
description:
|
||||
- a list of checks that will be used to verify the session health. If
|
||||
all the checks fail, the session will be invalidated and any locks
|
||||
associated with the session will be release and can be acquired once
|
||||
the associated lock delay has expired.
|
||||
required: false
|
||||
default: None
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent defaults to localhost
|
||||
required: false
|
||||
default: localhost
|
||||
port:
|
||||
description:
|
||||
- the port on which the consul agent is running
|
||||
required: false
|
||||
default: 8500
|
||||
scheme:
|
||||
description:
|
||||
- the protocol scheme on which the consul agent is running
|
||||
required: false
|
||||
default: http
|
||||
version_added: "2.1"
|
||||
validate_certs:
|
||||
description:
|
||||
- whether to verify the tls certificate of the consul agent
|
||||
required: false
|
||||
default: True
|
||||
version_added: "2.1"
|
||||
behavior:
|
||||
description:
|
||||
- the optional behavior that can be attached to the session when it
|
||||
is created. This can be set to either ‘release’ or ‘delete’. This
|
||||
controls the behavior when a session is invalidated.
|
||||
default: release
|
||||
required: false
|
||||
version_added: "2.2"
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: register basic session with consul
|
||||
consul_session:
|
||||
name: session1
|
||||
|
||||
- name: register a session with an existing check
|
||||
consul_session:
|
||||
name: session_with_check
|
||||
checks:
|
||||
- existing_check_name
|
||||
|
||||
- name: register a session with lock_delay
|
||||
consul_session:
|
||||
name: session_with_delay
|
||||
delay: 20s
|
||||
|
||||
- name: retrieve info about session by id
|
||||
consul_session: id=session_id state=info
|
||||
|
||||
- name: retrieve active sessions
|
||||
consul_session: state=list
|
||||
'''
|
||||
|
||||
try:
|
||||
import consul
|
||||
from requests.exceptions import ConnectionError
|
||||
python_consul_installed = True
|
||||
except ImportError:
|
||||
python_consul_installed = False
|
||||
|
||||
def execute(module):
|
||||
|
||||
state = module.params.get('state')
|
||||
|
||||
if state in ['info', 'list', 'node']:
|
||||
lookup_sessions(module)
|
||||
elif state == 'present':
|
||||
update_session(module)
|
||||
else:
|
||||
remove_session(module)
|
||||
|
||||
def lookup_sessions(module):
|
||||
|
||||
datacenter = module.params.get('datacenter')
|
||||
|
||||
state = module.params.get('state')
|
||||
consul_client = get_consul_api(module)
|
||||
try:
|
||||
if state == 'list':
|
||||
sessions_list = consul_client.session.list(dc=datacenter)
|
||||
#ditch the index, this can be grabbed from the results
|
||||
if sessions_list and sessions_list[1]:
|
||||
sessions_list = sessions_list[1]
|
||||
module.exit_json(changed=True,
|
||||
sessions=sessions_list)
|
||||
elif state == 'node':
|
||||
node = module.params.get('node')
|
||||
if not node:
|
||||
module.fail_json(
|
||||
msg="node name is required to retrieve sessions for node")
|
||||
sessions = consul_client.session.node(node, dc=datacenter)
|
||||
module.exit_json(changed=True,
|
||||
node=node,
|
||||
sessions=sessions)
|
||||
elif state == 'info':
|
||||
session_id = module.params.get('id')
|
||||
if not session_id:
|
||||
module.fail_json(
|
||||
msg="session_id is required to retrieve indvidual session info")
|
||||
|
||||
session_by_id = consul_client.session.info(session_id, dc=datacenter)
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id,
|
||||
sessions=session_by_id)
|
||||
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Could not retrieve session info %s" % e)
|
||||
|
||||
|
||||
def update_session(module):
|
||||
|
||||
name = module.params.get('name')
|
||||
delay = module.params.get('delay')
|
||||
checks = module.params.get('checks')
|
||||
datacenter = module.params.get('datacenter')
|
||||
node = module.params.get('node')
|
||||
behavior = module.params.get('behavior')
|
||||
|
||||
consul_client = get_consul_api(module)
|
||||
|
||||
try:
|
||||
session = consul_client.session.create(
|
||||
name=name,
|
||||
behavior=behavior,
|
||||
node=node,
|
||||
lock_delay=delay,
|
||||
dc=datacenter,
|
||||
checks=checks
|
||||
)
|
||||
module.exit_json(changed=True,
|
||||
session_id=session,
|
||||
name=name,
|
||||
behavior=behavior,
|
||||
delay=delay,
|
||||
checks=checks,
|
||||
node=node)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Could not create/update session %s" % e)
|
||||
|
||||
|
||||
def remove_session(module):
|
||||
session_id = module.params.get('id')
|
||||
if not session_id:
|
||||
module.fail_json(msg="""A session id must be supplied in order to
|
||||
remove a session.""")
|
||||
|
||||
consul_client = get_consul_api(module)
|
||||
|
||||
try:
|
||||
consul_client.session.destroy(session_id)
|
||||
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id)
|
||||
except Exception as e:
|
||||
module.fail_json(msg="Could not remove session with id '%s' %s" % (
|
||||
session_id, e))
|
||||
|
||||
def get_consul_api(module):
|
||||
return consul.Consul(host=module.params.get('host'),
|
||||
port=module.params.get('port'))
|
||||
|
||||
def test_dependencies(module):
|
||||
if not python_consul_installed:
|
||||
module.fail_json(msg="python-consul required for this module. "\
|
||||
"see http://python-consul.readthedocs.org/en/latest/#installation")
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
checks=dict(default=None, required=False, type='list'),
|
||||
delay=dict(required=False,type='int', default='15'),
|
||||
behavior=dict(required=False,type='str', default='release',
|
||||
choices=['release', 'delete']),
|
||||
host=dict(default='localhost'),
|
||||
port=dict(default=8500, type='int'),
|
||||
scheme=dict(required=False, default='http'),
|
||||
validate_certs=dict(required=False, default=True),
|
||||
id=dict(required=False),
|
||||
name=dict(required=False),
|
||||
node=dict(required=False),
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent', 'info', 'node', 'list']),
|
||||
datacenter=dict(required=False)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=False)
|
||||
|
||||
test_dependencies(module)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except ConnectionError as e:
|
||||
module.fail_json(msg='Could not connect to consul agent at %s:%s, error was %s' % (
|
||||
module.params.get('host'), module.params.get('port'), str(e)))
|
||||
except Exception as e:
|
||||
module.fail_json(msg=str(e))
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
if __name__ == '__main__':
|
||||
main()
|
411
lib/ansible/modules/clustering/kubernetes.py
Normal file
411
lib/ansible/modules/clustering/kubernetes.py
Normal file
|
@ -0,0 +1,411 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = '''
|
||||
---
|
||||
module: kubernetes
|
||||
version_added: "2.1"
|
||||
short_description: Manage Kubernetes resources.
|
||||
description:
|
||||
- This module can manage Kubernetes resources on an existing cluster using
|
||||
the Kubernetes server API. Users can specify in-line API data, or
|
||||
specify an existing Kubernetes YAML file. Currently, this module,
|
||||
Only supports HTTP Basic Auth
|
||||
Only supports 'strategic merge' for update, http://goo.gl/fCPYxT
|
||||
SSL certs are not working, use 'validate_certs=off' to disable
|
||||
options:
|
||||
api_endpoint:
|
||||
description:
|
||||
- The IPv4 API endpoint of the Kubernetes cluster.
|
||||
required: true
|
||||
default: null
|
||||
aliases: ["endpoint"]
|
||||
inline_data:
|
||||
description:
|
||||
- The Kubernetes YAML data to send to the API I(endpoint). This option is
|
||||
mutually exclusive with C('file_reference').
|
||||
required: true
|
||||
default: null
|
||||
file_reference:
|
||||
description:
|
||||
- Specify full path to a Kubernets YAML file to send to API I(endpoint).
|
||||
This option is mutually exclusive with C('inline_data').
|
||||
required: false
|
||||
default: null
|
||||
certificate_authority_data:
|
||||
description:
|
||||
- Certificate Authority data for Kubernetes server. Should be in either
|
||||
standard PEM format or base64 encoded PEM data. Note that certificate
|
||||
verification is broken until ansible supports a version of
|
||||
'match_hostname' that can match the IP address against the CA data.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- The desired action to take on the Kubernetes data.
|
||||
required: true
|
||||
default: "present"
|
||||
choices: ["present", "absent", "update", "replace"]
|
||||
url_password:
|
||||
description:
|
||||
- The HTTP Basic Auth password for the API I(endpoint). This should be set
|
||||
unless using the C('insecure') option.
|
||||
default: null
|
||||
aliases: ["password"]
|
||||
url_username:
|
||||
description:
|
||||
- The HTTP Basic Auth username for the API I(endpoint). This should be set
|
||||
unless using the C('insecure') option.
|
||||
default: "admin"
|
||||
aliases: ["username"]
|
||||
insecure:
|
||||
description:
|
||||
- "Reverts the connection to using HTTP instead of HTTPS. This option should
|
||||
only be used when execuing the M('kubernetes') module local to the Kubernetes
|
||||
cluster using the insecure local port (locahost:8080 by default)."
|
||||
validate_certs:
|
||||
description:
|
||||
- Enable/disable certificate validation. Note that this is set to
|
||||
C(false) until Ansible can support IP address based certificate
|
||||
hostname matching (exists in >= python3.5.0).
|
||||
required: false
|
||||
default: false
|
||||
|
||||
author: "Eric Johnson (@erjohnso) <erjohnso@google.com>"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
# Create a new namespace with in-line YAML.
|
||||
- name: Create a kubernetes namespace
|
||||
kubernetes:
|
||||
api_endpoint: 123.45.67.89
|
||||
url_username: admin
|
||||
url_password: redacted
|
||||
inline_data:
|
||||
kind: Namespace
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: ansible-test
|
||||
labels:
|
||||
label_env: production
|
||||
label_ver: latest
|
||||
annotations:
|
||||
a1: value1
|
||||
a2: value2
|
||||
state: present
|
||||
|
||||
# Create a new namespace from a YAML file.
|
||||
- name: Create a kubernetes namespace
|
||||
kubernetes:
|
||||
api_endpoint: 123.45.67.89
|
||||
url_username: admin
|
||||
url_password: redacted
|
||||
file_reference: /path/to/create_namespace.yaml
|
||||
state: present
|
||||
|
||||
# Do the same thing, but using the insecure localhost port
|
||||
- name: Create a kubernetes namespace
|
||||
kubernetes:
|
||||
api_endpoint: 123.45.67.89
|
||||
insecure: true
|
||||
file_reference: /path/to/create_namespace.yaml
|
||||
state: present
|
||||
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
# Example response from creating a Kubernetes Namespace.
|
||||
api_response:
|
||||
description: Raw response from Kubernetes API, content varies with API.
|
||||
returned: success
|
||||
type: dictionary
|
||||
contains:
|
||||
apiVersion: "v1"
|
||||
kind: "Namespace"
|
||||
metadata:
|
||||
creationTimestamp: "2016-01-04T21:16:32Z"
|
||||
name: "test-namespace"
|
||||
resourceVersion: "509635"
|
||||
selfLink: "/api/v1/namespaces/test-namespace"
|
||||
uid: "6dbd394e-b328-11e5-9a02-42010af0013a"
|
||||
spec:
|
||||
finalizers:
|
||||
- kubernetes
|
||||
status:
|
||||
phase: "Active"
|
||||
'''
|
||||
|
||||
import base64
|
||||
|
||||
try:
|
||||
import yaml
|
||||
has_lib_yaml = True
|
||||
except ImportError:
|
||||
has_lib_yaml = False
|
||||
|
||||
############################################################################
|
||||
############################################################################
|
||||
# For API coverage, this Anislbe module provides capability to operate on
|
||||
# all Kubernetes objects that support a "create" call (except for 'Events').
|
||||
# In order to obtain a valid list of Kubernetes objects, the v1 spec file
|
||||
# was referenced and the below python script was used to parse the JSON
|
||||
# spec file, extract only the objects with a description starting with
|
||||
# 'create a'. The script then iterates over all of these base objects
|
||||
# to get the endpoint URL and was used to generate the KIND_URL map.
|
||||
#
|
||||
# import json
|
||||
# from urllib2 import urlopen
|
||||
#
|
||||
# r = urlopen("https://raw.githubusercontent.com/kubernetes"
|
||||
# "/kubernetes/master/api/swagger-spec/v1.json")
|
||||
# v1 = json.load(r)
|
||||
#
|
||||
# apis = {}
|
||||
# for a in v1['apis']:
|
||||
# p = a['path']
|
||||
# for o in a['operations']:
|
||||
# if o["summary"].startswith("create a") and o["type"] != "v1.Event":
|
||||
# apis[o["type"]] = p
|
||||
#
|
||||
# def print_kind_url_map():
|
||||
# results = []
|
||||
# for a in apis.keys():
|
||||
# results.append('"%s": "%s"' % (a[3:].lower(), apis[a]))
|
||||
# results.sort()
|
||||
# print "KIND_URL = {"
|
||||
# print ",\n".join(results)
|
||||
# print "}"
|
||||
#
|
||||
# if __name__ == '__main__':
|
||||
# print_kind_url_map()
|
||||
############################################################################
|
||||
############################################################################
|
||||
|
||||
KIND_URL = {
|
||||
"binding": "/api/v1/namespaces/{namespace}/bindings",
|
||||
"endpoints": "/api/v1/namespaces/{namespace}/endpoints",
|
||||
"limitrange": "/api/v1/namespaces/{namespace}/limitranges",
|
||||
"namespace": "/api/v1/namespaces",
|
||||
"node": "/api/v1/nodes",
|
||||
"persistentvolume": "/api/v1/persistentvolumes",
|
||||
"persistentvolumeclaim": "/api/v1/namespaces/{namespace}/persistentvolumeclaims", # NOQA
|
||||
"pod": "/api/v1/namespaces/{namespace}/pods",
|
||||
"podtemplate": "/api/v1/namespaces/{namespace}/podtemplates",
|
||||
"replicationcontroller": "/api/v1/namespaces/{namespace}/replicationcontrollers", # NOQA
|
||||
"resourcequota": "/api/v1/namespaces/{namespace}/resourcequotas",
|
||||
"secret": "/api/v1/namespaces/{namespace}/secrets",
|
||||
"service": "/api/v1/namespaces/{namespace}/services",
|
||||
"serviceaccount": "/api/v1/namespaces/{namespace}/serviceaccounts"
|
||||
}
|
||||
USER_AGENT = "ansible-k8s-module/0.0.1"
|
||||
|
||||
|
||||
# TODO(erjohnso): SSL Certificate validation is currently unsupported.
|
||||
# It can be made to work when the following are true:
|
||||
# - Ansible consistently uses a "match_hostname" that supports IP Address
|
||||
# matching. This is now true in >= python3.5.0. Currently, this feature
|
||||
# is not yet available in backports.ssl_match_hostname (still 3.4).
|
||||
# - Ansible allows passing in the self-signed CA cert that is created with
|
||||
# a kubernetes master. The lib/ansible/module_utils/urls.py method,
|
||||
# SSLValidationHandler.get_ca_certs() needs a way for the Kubernetes
|
||||
# CA cert to be passed in and included in the generated bundle file.
|
||||
# When this is fixed, the following changes can be made to this module,
|
||||
# - Remove the 'return' statement in line 254 below
|
||||
# - Set 'required=true' for certificate_authority_data and ensure that
|
||||
# ansible's SSLValidationHandler.get_ca_certs() can pick up this CA cert
|
||||
# - Set 'required=true' for the validate_certs param.
|
||||
|
||||
def decode_cert_data(module):
|
||||
return
|
||||
d = module.params.get("certificate_authority_data")
|
||||
if d and not d.startswith("-----BEGIN"):
|
||||
module.params["certificate_authority_data"] = base64.b64decode(d)
|
||||
|
||||
|
||||
def api_request(module, url, method="GET", headers=None, data=None):
|
||||
body = None
|
||||
if data:
|
||||
data = json.dumps(data)
|
||||
response, info = fetch_url(module, url, method=method, headers=headers, data=data)
|
||||
if int(info['status']) == -1:
|
||||
module.fail_json(msg="Failed to execute the API request: %s" % info['msg'], url=url, method=method, headers=headers)
|
||||
if response is not None:
|
||||
body = json.loads(response.read())
|
||||
return info, body
|
||||
|
||||
|
||||
def k8s_create_resource(module, url, data):
|
||||
info, body = api_request(module, url, method="POST", data=data, headers={"Content-Type": "application/json"})
|
||||
if info['status'] == 409:
|
||||
name = data["metadata"].get("name", None)
|
||||
info, body = api_request(module, url + "/" + name)
|
||||
return False, body
|
||||
elif info['status'] >= 400:
|
||||
module.fail_json(msg="failed to create the resource: %s" % info['msg'], url=url)
|
||||
return True, body
|
||||
|
||||
|
||||
def k8s_delete_resource(module, url, data):
|
||||
name = data.get('metadata', {}).get('name')
|
||||
if name is None:
|
||||
module.fail_json(msg="Missing a named resource in object metadata when trying to remove a resource")
|
||||
|
||||
url = url + '/' + name
|
||||
info, body = api_request(module, url, method="DELETE")
|
||||
if info['status'] == 404:
|
||||
return False, "Resource name '%s' already absent" % name
|
||||
elif info['status'] >= 400:
|
||||
module.fail_json(msg="failed to delete the resource '%s': %s" % (name, info['msg']), url=url)
|
||||
return True, "Successfully deleted resource name '%s'" % name
|
||||
|
||||
|
||||
def k8s_replace_resource(module, url, data):
|
||||
name = data.get('metadata', {}).get('name')
|
||||
if name is None:
|
||||
module.fail_json(msg="Missing a named resource in object metadata when trying to replace a resource")
|
||||
|
||||
headers = {"Content-Type": "application/json"}
|
||||
url = url + '/' + name
|
||||
info, body = api_request(module, url, method="PUT", data=data, headers=headers)
|
||||
if info['status'] == 409:
|
||||
name = data["metadata"].get("name", None)
|
||||
info, body = api_request(module, url + "/" + name)
|
||||
return False, body
|
||||
elif info['status'] >= 400:
|
||||
module.fail_json(msg="failed to replace the resource '%s': %s" % (name, info['msg']), url=url)
|
||||
return True, body
|
||||
|
||||
|
||||
def k8s_update_resource(module, url, data):
|
||||
name = data.get('metadata', {}).get('name')
|
||||
if name is None:
|
||||
module.fail_json(msg="Missing a named resource in object metadata when trying to update a resource")
|
||||
|
||||
headers = {"Content-Type": "application/strategic-merge-patch+json"}
|
||||
url = url + '/' + name
|
||||
info, body = api_request(module, url, method="PATCH", data=data, headers=headers)
|
||||
if info['status'] == 409:
|
||||
name = data["metadata"].get("name", None)
|
||||
info, body = api_request(module, url + "/" + name)
|
||||
return False, body
|
||||
elif info['status'] >= 400:
|
||||
module.fail_json(msg="failed to update the resource '%s': %s" % (name, info['msg']), url=url)
|
||||
return True, body
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
http_agent=dict(default=USER_AGENT),
|
||||
|
||||
url_username=dict(default="admin", aliases=["username"]),
|
||||
url_password=dict(default="", no_log=True, aliases=["password"]),
|
||||
force_basic_auth=dict(default="yes"),
|
||||
validate_certs=dict(default=False, type='bool'),
|
||||
certificate_authority_data=dict(required=False),
|
||||
insecure=dict(default=False, type='bool'),
|
||||
api_endpoint=dict(required=True),
|
||||
file_reference=dict(required=False),
|
||||
inline_data=dict(required=False),
|
||||
state=dict(default="present", choices=["present", "absent", "update", "replace"])
|
||||
),
|
||||
mutually_exclusive = (('file_reference', 'inline_data'),
|
||||
('url_username', 'insecure'),
|
||||
('url_password', 'insecure')),
|
||||
required_one_of = (('file_reference', 'inline_data'),),
|
||||
)
|
||||
|
||||
if not has_lib_yaml:
|
||||
module.fail_json(msg="missing python library: yaml")
|
||||
|
||||
decode_cert_data(module)
|
||||
|
||||
api_endpoint = module.params.get('api_endpoint')
|
||||
state = module.params.get('state')
|
||||
insecure = module.params.get('insecure')
|
||||
inline_data = module.params.get('inline_data')
|
||||
file_reference = module.params.get('file_reference')
|
||||
|
||||
if inline_data:
|
||||
if not isinstance(inline_data, dict) and not isinstance(inline_data, list):
|
||||
data = yaml.load(inline_data)
|
||||
else:
|
||||
data = inline_data
|
||||
else:
|
||||
try:
|
||||
f = open(file_reference, "r")
|
||||
data = [x for x in yaml.load_all(f)]
|
||||
f.close()
|
||||
if not data:
|
||||
module.fail_json(msg="No valid data could be found.")
|
||||
except:
|
||||
module.fail_json(msg="The file '%s' was not found or contained invalid YAML/JSON data" % file_reference)
|
||||
|
||||
# set the transport type and build the target endpoint url
|
||||
transport = 'https'
|
||||
if insecure:
|
||||
transport = 'http'
|
||||
|
||||
target_endpoint = "%s://%s" % (transport, api_endpoint)
|
||||
|
||||
body = []
|
||||
changed = False
|
||||
|
||||
# make sure the data is a list
|
||||
if not isinstance(data, list):
|
||||
data = [ data ]
|
||||
|
||||
for item in data:
|
||||
namespace = "default"
|
||||
if item and 'metadata' in item:
|
||||
namespace = item.get('metadata', {}).get('namespace', "default")
|
||||
kind = item.get('kind', '').lower()
|
||||
try:
|
||||
url = target_endpoint + KIND_URL[kind]
|
||||
except KeyError:
|
||||
module.fail_json(msg="invalid resource kind specified in the data: '%s'" % kind)
|
||||
url = url.replace("{namespace}", namespace)
|
||||
else:
|
||||
url = target_endpoint
|
||||
|
||||
if state == 'present':
|
||||
item_changed, item_body = k8s_create_resource(module, url, item)
|
||||
elif state == 'absent':
|
||||
item_changed, item_body = k8s_delete_resource(module, url, item)
|
||||
elif state == 'replace':
|
||||
item_changed, item_body = k8s_replace_resource(module, url, item)
|
||||
elif state == 'update':
|
||||
item_changed, item_body = k8s_update_resource(module, url, item)
|
||||
|
||||
changed |= item_changed
|
||||
body.append(item_body)
|
||||
|
||||
module.exit_json(changed=changed, api_response=body)
|
||||
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import * # NOQA
|
||||
from ansible.module_utils.urls import * # NOQA
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
256
lib/ansible/modules/clustering/znode.py
Normal file
256
lib/ansible/modules/clustering/znode.py
Normal file
|
@ -0,0 +1,256 @@
|
|||
#!/usr/bin/python
|
||||
# Copyright 2015 WP Engine, Inc. All rights reserved.
|
||||
#
|
||||
# This file is part of Ansible
|
||||
#
|
||||
# Ansible is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# Ansible is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
'supported_by': 'community',
|
||||
'version': '1.0'}
|
||||
|
||||
DOCUMENTATION = """
|
||||
---
|
||||
module: znode
|
||||
version_added: "2.0"
|
||||
short_description: Create, delete, retrieve, and update znodes using ZooKeeper.
|
||||
options:
|
||||
hosts:
|
||||
description:
|
||||
- A list of ZooKeeper servers (format '[server]:[port]').
|
||||
required: true
|
||||
name:
|
||||
description:
|
||||
- The path of the znode.
|
||||
required: true
|
||||
value:
|
||||
description:
|
||||
- The value assigned to the znode.
|
||||
default: None
|
||||
required: false
|
||||
op:
|
||||
description:
|
||||
- An operation to perform. Mutually exclusive with state.
|
||||
default: None
|
||||
required: false
|
||||
state:
|
||||
description:
|
||||
- The state to enforce. Mutually exclusive with op.
|
||||
default: None
|
||||
required: false
|
||||
timeout:
|
||||
description:
|
||||
- The amount of time to wait for a node to appear.
|
||||
default: 300
|
||||
required: false
|
||||
recursive:
|
||||
description:
|
||||
- Recursively delete node and all its children.
|
||||
default: False
|
||||
required: false
|
||||
version_added: "2.1"
|
||||
requirements:
|
||||
- kazoo >= 2.1
|
||||
- python >= 2.6
|
||||
author: "Trey Perry (@treyperry)"
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
# Creating or updating a znode with a given value
|
||||
- znode:
|
||||
hosts: 'localhost:2181'
|
||||
name: /mypath
|
||||
value: myvalue
|
||||
state: present
|
||||
|
||||
# Getting the value and stat structure for a znode
|
||||
- znode:
|
||||
hosts: 'localhost:2181'
|
||||
name: /mypath
|
||||
op: get
|
||||
|
||||
# Listing a particular znode's children
|
||||
- znode:
|
||||
hosts: 'localhost:2181'
|
||||
name: /zookeeper
|
||||
op: list
|
||||
|
||||
# Waiting 20 seconds for a znode to appear at path /mypath
|
||||
- znode:
|
||||
hosts: 'localhost:2181'
|
||||
name: /mypath
|
||||
op: wait
|
||||
timeout: 20
|
||||
|
||||
# Deleting a znode at path /mypath
|
||||
- znode:
|
||||
hosts: 'localhost:2181'
|
||||
name: /mypath
|
||||
state: absent
|
||||
"""
|
||||
|
||||
try:
|
||||
from kazoo.client import KazooClient
|
||||
from kazoo.exceptions import NoNodeError, ZookeeperError
|
||||
from kazoo.handlers.threading import KazooTimeoutError
|
||||
KAZOO_INSTALLED = True
|
||||
except ImportError:
|
||||
KAZOO_INSTALLED = False
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
hosts=dict(required=True, type='str'),
|
||||
name=dict(required=True, type='str'),
|
||||
value=dict(required=False, default=None, type='str'),
|
||||
op=dict(required=False, default=None, choices=['get', 'wait', 'list']),
|
||||
state=dict(choices=['present', 'absent']),
|
||||
timeout=dict(required=False, default=300, type='int'),
|
||||
recursive=dict(required=False, default=False, type='bool')
|
||||
),
|
||||
supports_check_mode=False
|
||||
)
|
||||
|
||||
if not KAZOO_INSTALLED:
|
||||
module.fail_json(msg='kazoo >= 2.1 is required to use this module. Use pip to install it.')
|
||||
|
||||
check = check_params(module.params)
|
||||
if not check['success']:
|
||||
module.fail_json(msg=check['msg'])
|
||||
|
||||
zoo = KazooCommandProxy(module)
|
||||
try:
|
||||
zoo.start()
|
||||
except KazooTimeoutError:
|
||||
module.fail_json(msg='The connection to the ZooKeeper ensemble timed out.')
|
||||
|
||||
command_dict = {
|
||||
'op': {
|
||||
'get': zoo.get,
|
||||
'list': zoo.list,
|
||||
'wait': zoo.wait
|
||||
},
|
||||
'state': {
|
||||
'present': zoo.present,
|
||||
'absent': zoo.absent
|
||||
}
|
||||
}
|
||||
|
||||
command_type = 'op' if 'op' in module.params and module.params['op'] is not None else 'state'
|
||||
method = module.params[command_type]
|
||||
result, result_dict = command_dict[command_type][method]()
|
||||
zoo.shutdown()
|
||||
|
||||
if result:
|
||||
module.exit_json(**result_dict)
|
||||
else:
|
||||
module.fail_json(**result_dict)
|
||||
|
||||
|
||||
def check_params(params):
|
||||
if not params['state'] and not params['op']:
|
||||
return {'success': False, 'msg': 'Please define an operation (op) or a state.'}
|
||||
|
||||
if params['state'] and params['op']:
|
||||
return {'success': False, 'msg': 'Please choose an operation (op) or a state, but not both.'}
|
||||
|
||||
return {'success': True}
|
||||
|
||||
|
||||
class KazooCommandProxy():
|
||||
def __init__(self, module):
|
||||
self.module = module
|
||||
self.zk = KazooClient(module.params['hosts'])
|
||||
|
||||
def absent(self):
|
||||
return self._absent(self.module.params['name'])
|
||||
|
||||
def exists(self, znode):
|
||||
return self.zk.exists(znode)
|
||||
|
||||
def list(self):
|
||||
children = self.zk.get_children(self.module.params['name'])
|
||||
return True, {'count': len(children), 'items': children, 'msg': 'Retrieved znodes in path.',
|
||||
'znode': self.module.params['name']}
|
||||
|
||||
def present(self):
|
||||
return self._present(self.module.params['name'], self.module.params['value'])
|
||||
|
||||
def get(self):
|
||||
return self._get(self.module.params['name'])
|
||||
|
||||
def shutdown(self):
|
||||
self.zk.stop()
|
||||
self.zk.close()
|
||||
|
||||
def start(self):
|
||||
self.zk.start()
|
||||
|
||||
def wait(self):
|
||||
return self._wait(self.module.params['name'], self.module.params['timeout'])
|
||||
|
||||
def _absent(self, znode):
|
||||
if self.exists(znode):
|
||||
self.zk.delete(znode, recursive=self.module.params['recursive'])
|
||||
return True, {'changed': True, 'msg': 'The znode was deleted.'}
|
||||
else:
|
||||
return True, {'changed': False, 'msg': 'The znode does not exist.'}
|
||||
|
||||
def _get(self, path):
|
||||
if self.exists(path):
|
||||
value, zstat = self.zk.get(path)
|
||||
stat_dict = {}
|
||||
for i in dir(zstat):
|
||||
if not i.startswith('_'):
|
||||
attr = getattr(zstat, i)
|
||||
if isinstance(attr, (int, str)):
|
||||
stat_dict[i] = attr
|
||||
result = True, {'msg': 'The node was retrieved.', 'znode': path, 'value': value,
|
||||
'stat': stat_dict}
|
||||
else:
|
||||
result = False, {'msg': 'The requested node does not exist.'}
|
||||
|
||||
return result
|
||||
|
||||
def _present(self, path, value):
|
||||
if self.exists(path):
|
||||
(current_value, zstat) = self.zk.get(path)
|
||||
if value != current_value:
|
||||
self.zk.set(path, value)
|
||||
return True, {'changed': True, 'msg': 'Updated the znode value.', 'znode': path,
|
||||
'value': value}
|
||||
else:
|
||||
return True, {'changed': False, 'msg': 'No changes were necessary.', 'znode': path, 'value': value}
|
||||
else:
|
||||
self.zk.create(path, value, makepath=True)
|
||||
return True, {'changed': True, 'msg': 'Created a new znode.', 'znode': path, 'value': value}
|
||||
|
||||
def _wait(self, path, timeout, interval=5):
|
||||
lim = time.time() + timeout
|
||||
|
||||
while time.time() < lim:
|
||||
if self.exists(path):
|
||||
return True, {'msg': 'The node appeared before the configured timeout.',
|
||||
'znode': path, 'timeout': timeout}
|
||||
else:
|
||||
time.sleep(interval)
|
||||
|
||||
return False, {'msg': 'The node did not appear before the operation timed out.', 'timeout': timeout,
|
||||
'znode': path}
|
||||
|
||||
from ansible.module_utils.basic import *
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
Loading…
Add table
Add a link
Reference in a new issue