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-vagrant
This commit is contained in:
Steve Gargan 2015-01-24 01:33:53 +00:00
parent 21126a4af3
commit ea6c887d6c
4 changed files with 1212 additions and 0 deletions

463
clustering/consul Normal file
View file

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

298
clustering/consul_acl Normal file
View file

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

238
clustering/consul_kv Normal file
View file

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

213
clustering/consul_session Normal file
View file

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