mirror of https://github.com/ansible/ansible.git
Initial commit of Ansible support for the Consul clustering framework (http://consul.io).
Submission includes support for - creating and registering services and checks - reading, writing and lookup for values in consul's kv store - creating and manipulating sessions for distributed locking on values in the kv - creating and manipulating ACLs for restricting access to the kv store - inventory support that reads the Consul catalog and group nodes according to - datacenters - exposed services - service availability - arbitrary groupings from the kv store This submission makes extensive use of the python-consul library and this is required as a dependency and can be installed from pip. The tests were written to target a vagrant cluster which can be setup by following the instructions here http://github.com/sgargan/consul-vagrantreviewable/pr18780/r1
parent
21126a4af3
commit
ea6c887d6c
@ -0,0 +1,463 @@
|
||||
#!/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/>.
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul
|
||||
short_description: "Add, modify & delete services within a consul cluster.
|
||||
See http://conul.io for more details."
|
||||
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
|
||||
that will be used to notify the consul cluster of the health of the service.
|
||||
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.
|
||||
version_added: "1.9"
|
||||
author: Steve Gargan (steve.gargan@gmail.com)
|
||||
options:
|
||||
state:
|
||||
description:
|
||||
- register or deregister the consul service, defaults to present
|
||||
required: true
|
||||
choices: ['present', 'absent']
|
||||
service_id:
|
||||
description:
|
||||
- the ID for the service, must be unique per node, defaults to the
|
||||
service name
|
||||
required: false
|
||||
host:
|
||||
description:
|
||||
- host of the consul agent with which to register the service,
|
||||
defaults to localhost
|
||||
required: false
|
||||
notes:
|
||||
description:
|
||||
- Notes to attach to check when registering it.
|
||||
service_name:
|
||||
desciption:
|
||||
- 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_port:
|
||||
description:
|
||||
- the port on which the service is listening required for
|
||||
registration of a service.
|
||||
required: true
|
||||
tags:
|
||||
description:
|
||||
- a list of tags that will be attached to the service registration.
|
||||
required: false
|
||||
script:
|
||||
description:
|
||||
- the script/command that will be run periodically to check the health
|
||||
of the service
|
||||
required: false
|
||||
interval:
|
||||
description:
|
||||
- the interval at which the service check will be run. This is by
|
||||
convention a number with a s or m to signify the units of seconds
|
||||
or minutes. if none is supplied, m will be appended
|
||||
check_id:
|
||||
description:
|
||||
- an ID for the service check, defaults to the check name, ignored if
|
||||
part of service definition.
|
||||
check_name:
|
||||
description:
|
||||
- a name for the service check, defaults to the check id. required if
|
||||
standalone, ignored if part of service definition.
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
- name: register nginx service with the local consul agent
|
||||
consul:
|
||||
name: nginx
|
||||
port: 80
|
||||
|
||||
- name: register nginx service with curl check
|
||||
consul:
|
||||
name: nginx
|
||||
port: 80
|
||||
script: "curl http://localhost"
|
||||
interval: 60s
|
||||
|
||||
- name: register nginx with some service tags
|
||||
consul:
|
||||
name: nginx
|
||||
port: 80
|
||||
tags:
|
||||
- prod
|
||||
- webservers
|
||||
|
||||
- name: remove nginx service
|
||||
consul:
|
||||
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
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import consul
|
||||
except ImportError, e:
|
||||
print "failed=True msg='python-consul required for this module. "\
|
||||
"see http://python-consul.readthedocs.org/en/latest/#installation'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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:
|
||||
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)
|
||||
|
||||
|
||||
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(consul_api, service.id)
|
||||
|
||||
# there is no way to retreive the details of checks so if a check is present
|
||||
# in the service it must be reregistered
|
||||
if service.has_checks() or not(existing or existing == service):
|
||||
|
||||
service.register(consul_api)
|
||||
# check that it registered correctly
|
||||
registered = get_service_by_id(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=map(lambda x: x.to_dict(), 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(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'),
|
||||
token=module.params.get('token'))
|
||||
|
||||
|
||||
def get_service_by_id(consul_api, service_id):
|
||||
''' 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:
|
||||
return ConsulService(loaded=service)
|
||||
|
||||
|
||||
def parse_check(module):
|
||||
|
||||
if module.params.get('script') and module.params.get('ttl'):
|
||||
module.fail_json(
|
||||
msg='check are either script or ttl driven, supplying both does'\
|
||||
' not make sense')
|
||||
|
||||
if module.params.get('check_id') or module.params.get('script') or module.params.get('ttl'):
|
||||
|
||||
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')
|
||||
)
|
||||
|
||||
|
||||
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_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, port=-1,
|
||||
tags=None, loaded=None):
|
||||
self.id = self.name = name
|
||||
if service_id:
|
||||
self.id = service_id
|
||||
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,
|
||||
port=self.port,
|
||||
tags=self.tags,
|
||||
script=check.script,
|
||||
interval=check.interval,
|
||||
ttl=check.ttl)
|
||||
else:
|
||||
consul_api.agent.service.register(
|
||||
self.name,
|
||||
service_id=self.id,
|
||||
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):
|
||||
self.check_id = self.name = name
|
||||
if check_id:
|
||||
self.check_id = check_id
|
||||
self.script = script
|
||||
self.interval = str(interval)
|
||||
|
||||
if not self.interval.endswith('m') or self.interval.endswith('s'):
|
||||
self.interval += 'm'
|
||||
|
||||
self.ttl = ttl
|
||||
self.notes = notes
|
||||
self.node = node
|
||||
self.host = host
|
||||
|
||||
if interval and interval <= 0:
|
||||
raise Error('check interval must be positive')
|
||||
|
||||
if ttl and ttl <= 0:
|
||||
raise Error('check ttl value must be positive')
|
||||
|
||||
def register(self, consul_api):
|
||||
consul_api.agent.check.register(self.name, check_id=self.check_id,
|
||||
script=self.script,
|
||||
interval=self.interval,
|
||||
ttl=self.ttl, notes=self.notes)
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.check_id == other.check_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')
|
||||
return data
|
||||
|
||||
def _add(self, data, key, attr=None):
|
||||
try:
|
||||
if attr == None:
|
||||
attr = key
|
||||
data[key] = getattr(self, attr)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def main():
|
||||
module = AnsibleModule(
|
||||
argument_spec=dict(
|
||||
check_id=dict(required=False),
|
||||
check_name=dict(required=False),
|
||||
host=dict(default='localhost'),
|
||||
interval=dict(required=False, default='1m'),
|
||||
check_node=dict(required=False),
|
||||
check_host=dict(required=False),
|
||||
notes=dict(required=False),
|
||||
port=dict(default=8500, type='int'),
|
||||
script=dict(required=False),
|
||||
service_id=dict(required=False),
|
||||
service_name=dict(required=False),
|
||||
service_port=dict(required=False, type='int'),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
tags=dict(required=False, type='list'),
|
||||
token=dict(required=False),
|
||||
url=dict(default='http://localhost:8500')
|
||||
),
|
||||
supports_check_mode=False,
|
||||
)
|
||||
try:
|
||||
register_with_consul(module)
|
||||
except IOError, e:
|
||||
error = e.read()
|
||||
if not error:
|
||||
error = str(e)
|
||||
module.fail_json(msg=error)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -0,0 +1,298 @@
|
||||
#!/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/>.
|
||||
|
||||
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.
|
||||
version_added: "1.9"
|
||||
author: Steve Gargan (steve.gargan@gmail.com)
|
||||
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, defaults to present
|
||||
required: false
|
||||
choices: ['present', 'absent']
|
||||
type:
|
||||
description:
|
||||
- the type of token that should be created, either management or
|
||||
client, defaults to client
|
||||
choices: ['client', 'management']
|
||||
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 key/token.
|
||||
required: false
|
||||
"""
|
||||
|
||||
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: remove a token
|
||||
consul_acl:
|
||||
mgmt_token: 'some_management_acl'
|
||||
host: 'consul1.mycluster.io'
|
||||
token: '172bd5c8-9fe9-11e4-b1b0-3c15c2c9fd5e'
|
||||
state: absent
|
||||
'''
|
||||
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
try:
|
||||
import consul
|
||||
except ImportError, e:
|
||||
print "failed=True msg='python-consul required for this module. "\
|
||||
"see http://python-consul.readthedocs.org/en/latest/#installation'"
|
||||
sys.exit(1)
|
||||
|
||||
try:
|
||||
import hcl
|
||||
except ImportError:
|
||||
print "failed=True msg='pyhcl required for this module."\
|
||||
" see https://pypi.python.org/pypi/pyhcl'"
|
||||
sys.exit(1)
|
||||
|
||||
import epdb
|
||||
|
||||
|
||||
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)
|
||||
print existing_rules
|
||||
print supplied_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_json()
|
||||
else:
|
||||
rules = None
|
||||
|
||||
token = consul.acl.create(
|
||||
name=name, type=token_type, rules=rules)
|
||||
changed = True
|
||||
except Exception, 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, 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 = to_ascii(info['Rules'])
|
||||
for rule in hcl.loads(rule_set).values():
|
||||
for key, policy in rule.iteritems():
|
||||
rules.add_rule(Rule(key, policy['policy']))
|
||||
return rules
|
||||
except Exception, 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 not('key' in rule or 'policy' in rule):
|
||||
module.fail_json(msg="a rule requires a key and a policy.")
|
||||
rules.add_rule(Rule(rule['key'], rule['policy']))
|
||||
return rules
|
||||
|
||||
template = '''key "%s" {
|
||||
policy = "%s"
|
||||
}'''
|
||||
|
||||
class Rules:
|
||||
|
||||
def __init__(self):
|
||||
self.rules = {}
|
||||
|
||||
def add_rule(self, rule):
|
||||
self.rules[rule.key] = rule
|
||||
|
||||
def are_rules(self):
|
||||
return len(self.rules) > 0
|
||||
|
||||
def to_json(self):
|
||||
# import epdb; epdb.serve()
|
||||
rules = {}
|
||||
for key, rule in self.rules.iteritems():
|
||||
rules[key] = {'policy': rule.policy}
|
||||
return json.dumps({'keys': rules})
|
||||
|
||||
def to_hcl(self):
|
||||
|
||||
rules = ""
|
||||
for key, rule in self.rules.iteritems():
|
||||
rules += template % (key, rule.policy)
|
||||
|
||||
return to_ascii(rules)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not (other or isinstance(other, self.__class__)
|
||||
or len(other.rules) == len(self.rules)):
|
||||
return False
|
||||
|
||||
for name, other_rule in other.rules.iteritems():
|
||||
if not name in self.rules:
|
||||
return False
|
||||
rule = self.rules[name]
|
||||
|
||||
if not (rule and rule == other_rule):
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
return self.to_hcl()
|
||||
|
||||
class Rule:
|
||||
|
||||
def __init__(self, key, policy):
|
||||
self.key = key
|
||||
self.policy = policy
|
||||
|
||||
def __eq__(self, other):
|
||||
return (isinstance(other, self.__class__)
|
||||
and self.key == other.key
|
||||
and self.policy == other.policy)
|
||||
def __hash__(self):
|
||||
return hash(self.key) ^ hash(self.policy)
|
||||
|
||||
def __str__(self):
|
||||
return '%s %s' % (self.key, self.policy)
|
||||
def get_consul_api(module, token=None):
|
||||
if not token:
|
||||
token = token = module.params.get('token')
|
||||
return consul.Consul(host=module.params.get('host'),
|
||||
port=module.params.get('port'),
|
||||
token=token)
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
mgmt_token=dict(required=True),
|
||||
host=dict(default='localhost'),
|
||||
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),
|
||||
token_type=dict(
|
||||
required=False, choices=['client', 'management'], default='client')
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=True)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except IOError, e:
|
||||
error = e.read()
|
||||
if not error:
|
||||
error = str(e)
|
||||
module.fail_json(msg=error)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -0,0 +1,238 @@
|
||||
#!/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/>.
|
||||
|
||||
DOCUMENTATION = """
|
||||
module: consul_kv
|
||||
short_description: "manipulate entries in the key/value store of a consul
|
||||
cluster. See http://www.consul.io/docs/agent/http.html#kv for more details."
|
||||
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.
|
||||
version_added: "1.9"
|
||||
author: Steve Gargan (steve.gargan@gmail.com)
|
||||
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: true
|
||||
choices: ['present', 'absent', 'acquire', 'release']
|
||||
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: true
|
||||
session:
|
||||
description:
|
||||
- the session that should be used to acquire or release a lock
|
||||
associated with a key/value pair
|
||||
token:
|
||||
description:
|
||||
- the token key indentifying an ACL rule set that controls access to
|
||||
the key value pair
|
||||
required: false
|
||||
url:
|
||||
description:
|
||||
- location of the consul agent with which access the keay/value store,
|
||||
defaults to http://localhost:8500
|
||||
required: false
|
||||
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.
|
||||
flags:
|
||||
description:
|
||||
- opaque integer value that can be passed when setting a value.
|
||||
"""
|
||||
|
||||
|
||||
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'
|
||||
'''
|
||||
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
try:
|
||||
import json
|
||||
except ImportError:
|
||||
import simplejson as json
|
||||
|
||||
try:
|
||||
import consul
|
||||
except ImportError, e:
|
||||
print """failed=True msg='python-consul required for this module. \
|
||||
see http://python-consul.readthedocs.org/en/latest/#installation'"""
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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):
|
||||
|
||||
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))
|
||||
|
||||
if state == 'acquire':
|
||||
successful = consul_api.kv.put(key, value,
|
||||
cas=module.params.get('cas'),
|
||||
acquire=session,
|
||||
flags=module.params.get('flags'))
|
||||
else:
|
||||
successful = consul_api.kv.put(key, value,
|
||||
cas=module.params.get('cas'),
|
||||
release=session,
|
||||
flags=module.params.get('flags'))
|
||||
|
||||
module.exit_json(changed=successful,
|
||||
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'),
|
||||
token=module.params.get('token'))
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = dict(
|
||||
cas=dict(required=False),
|
||||
flags=dict(required=False),
|
||||
host=dict(default='localhost'),
|
||||
key=dict(required=True),
|
||||
port=dict(default=8500, type='int'),
|
||||
recurse=dict(required=False, type='bool'),
|
||||
retrieve=dict(required=False, default=True),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
token=dict(required=False, default='anonymous'),
|
||||
value=dict(required=False)
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=True)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except IOError, e:
|
||||
error = e.read()
|
||||
if not error:
|
||||
error = str(e)
|
||||
module.fail_json(msg=error)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
@ -0,0 +1,213 @@
|
||||
#!/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/>.
|
||||
|
||||
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
|
||||
version_added: "1.9"
|
||||
author: Steve Gargan (steve.gargan@gmail.com)
|
||||
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']
|
||||
name:
|
||||
description:
|
||||
- the name that should be associated with the session. This is opaque
|
||||
to Consul and not required.
|
||||
required: false
|
||||
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.
|
||||
default: 15s
|
||||
node:
|
||||
description:
|
||||
- the name of the node that with which the session will be associated.
|
||||
by default this is the name of the agent.
|
||||
datacenter:
|
||||
description:
|
||||
- name of the datacenter in which the session exists or should be
|
||||
created.
|
||||
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.
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
|
||||
'''
|
||||
|
||||
import sys
|
||||
import urllib2
|
||||
|
||||
try:
|
||||
import consul
|
||||
except ImportError, e:
|
||||
print "failed=True msg='python-consul required for this module. see "\
|
||||
"http://python-consul.readthedocs.org/en/latest/#installation'"
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
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 = get_consul_api(module)
|
||||
try:
|
||||
if state == 'list':
|
||||
sessions_list = consul.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.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.session.info(session_id, dc=datacenter)
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id,
|
||||
sessions=session_by_id)
|
||||
|
||||
except Exception, e:
|
||||
module.fail_json(msg="Could not retrieve session info %s" % e)
|
||||
|
||||
|
||||
def update_session(module):
|
||||
|
||||
name = module.params.get('name')
|
||||
session_id = module.params.get('id')
|
||||
delay = module.params.get('delay')
|
||||
checks = module.params.get('checks')
|
||||
datacenter = module.params.get('datacenter')
|
||||
node = module.params.get('node')
|
||||
|
||||
consul = get_consul_api(module)
|
||||
changed = True
|
||||
|
||||
try:
|
||||
|
||||
session = consul.session.create(
|
||||
name=name,
|
||||
node=node,
|
||||
lock_delay=delay,
|
||||
dc=datacenter,
|
||||
checks=checks
|
||||
)
|
||||
module.exit_json(changed=True,
|
||||
session_id=session,
|
||||
name=name,
|
||||
delay=delay,
|
||||
checks=checks,
|
||||
node=node)
|
||||
except Exception, 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 = get_consul_api(module)
|
||||
changed = False
|
||||
|
||||
try:
|
||||
session = consul.session.destroy(session_id)
|
||||
|
||||
module.exit_json(changed=True,
|
||||
session_id=session_id)
|
||||
except Exception, 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 main():
|
||||
argument_spec = dict(
|
||||
checks=dict(default=None, required=False, type='list'),
|
||||
delay=dict(required=False,type='int', default=15),
|
||||
host=dict(default='localhost'),
|
||||
port=dict(default=8500, type='int'),
|
||||
id=dict(required=False),
|
||||
name=dict(required=False),
|
||||
node=dict(required=False),
|
||||
state=dict(default='present',
|
||||
choices=['present', 'absent', 'info', 'node', 'list'])
|
||||
)
|
||||
|
||||
module = AnsibleModule(argument_spec, supports_check_mode=True)
|
||||
|
||||
try:
|
||||
execute(module)
|
||||
except IOError, e:
|
||||
error = e.read()
|
||||
if not error:
|
||||
error = str(e)
|
||||
module.fail_json(msg=error)
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import *
|
||||
main()
|
Loading…
Reference in New Issue