From ea6c887d6c768d456226ae881bb8b4292bd26058 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Sat, 24 Jan 2015 01:33:53 +0000 Subject: [PATCH 1/7] 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 --- clustering/consul | 463 ++++++++++++++++++++++++++++++++++++++ clustering/consul_acl | 298 ++++++++++++++++++++++++ clustering/consul_kv | 238 ++++++++++++++++++++ clustering/consul_session | 213 ++++++++++++++++++ 4 files changed, 1212 insertions(+) create mode 100644 clustering/consul create mode 100644 clustering/consul_acl create mode 100644 clustering/consul_kv create mode 100644 clustering/consul_session diff --git a/clustering/consul b/clustering/consul new file mode 100644 index 00000000000..fa1e06c3678 --- /dev/null +++ b/clustering/consul @@ -0,0 +1,463 @@ +#!/usr/bin/python +# +# (c) 2015, Steve Gargan +# +# 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 . + +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() diff --git a/clustering/consul_acl b/clustering/consul_acl new file mode 100644 index 00000000000..ae3efe5787f --- /dev/null +++ b/clustering/consul_acl @@ -0,0 +1,298 @@ +#!/usr/bin/python +# +# (c) 2015, Steve Gargan +# +# 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 . + +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() diff --git a/clustering/consul_kv b/clustering/consul_kv new file mode 100644 index 00000000000..6a2b77ea7c6 --- /dev/null +++ b/clustering/consul_kv @@ -0,0 +1,238 @@ +#!/usr/bin/python +# +# (c) 2015, Steve Gargan +# +# 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 . + +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() diff --git a/clustering/consul_session b/clustering/consul_session new file mode 100644 index 00000000000..f11c5447e57 --- /dev/null +++ b/clustering/consul_session @@ -0,0 +1,213 @@ +#!/usr/bin/python +# +# (c) 2015, Steve Gargan +# +# 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 . + +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() From 0d7647d904580351a87824287fc88c25bf817a10 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Sat, 28 Feb 2015 15:23:23 +0000 Subject: [PATCH 2/7] remove debug imports from acl module --- clustering/consul_acl | 3 --- 1 file changed, 3 deletions(-) diff --git a/clustering/consul_acl b/clustering/consul_acl index ae3efe5787f..fc997400ae9 100644 --- a/clustering/consul_acl +++ b/clustering/consul_acl @@ -92,8 +92,6 @@ except ImportError: " see https://pypi.python.org/pypi/pyhcl'" sys.exit(1) -import epdb - def execute(module): @@ -216,7 +214,6 @@ class Rules: 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} From a8584ade957004ad43a5c18e172f5a843569a739 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Tue, 3 Mar 2015 14:18:56 +0000 Subject: [PATCH 3/7] fix logic that tests for change in an existing registered service --- clustering/consul | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clustering/consul b/clustering/consul index fa1e06c3678..8aa2ce1fe4c 100644 --- a/clustering/consul +++ b/clustering/consul @@ -224,7 +224,7 @@ def add_service(module, service): # 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): + if service.has_checks() or not existing or not existing == service: service.register(consul_api) # check that it registered correctly From 0c6d426c40932ec55a70ee96ec24f5131f46e2af Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Tue, 3 Mar 2015 20:03:46 +0000 Subject: [PATCH 4/7] require a valid duration suffix for interval and ttl values --- clustering/consul | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/clustering/consul b/clustering/consul index 8aa2ce1fe4c..24df908c45c 100644 --- a/clustering/consul +++ b/clustering/consul @@ -375,21 +375,21 @@ class ConsulCheck(): 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.interval = self.validate_duration('interval', interval) + self.ttl = self.validate_duration('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 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)): + raise Exception('Invalid %s %s you must specify units (%s)' % + (name, duration, ', '.join(duration_units))) + return duration def register(self, consul_api): consul_api.agent.check.register(self.name, check_id=self.check_id, @@ -434,7 +434,8 @@ def main(): check_id=dict(required=False), check_name=dict(required=False), host=dict(default='localhost'), - interval=dict(required=False, default='1m'), + interval=dict(required=False, type='str'), + ttl=dict(required=False, type='str'), check_node=dict(required=False), check_host=dict(required=False), notes=dict(required=False), From b553f59a54dbf7a0cae58a6d42054cc74d593c55 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Mon, 16 Mar 2015 16:50:53 +0000 Subject: [PATCH 5/7] Properly report exception causes particularly connection exceptions contacting the consul agent --- clustering/consul | 11 ++++++----- clustering/consul_acl | 11 ++++++----- clustering/consul_kv | 12 +++++++----- clustering/consul_session | 11 ++++++----- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/clustering/consul b/clustering/consul index 24df908c45c..15a68f068a2 100644 --- a/clustering/consul +++ b/clustering/consul @@ -143,6 +143,7 @@ except ImportError, e: "see http://python-consul.readthedocs.org/en/latest/#installation'" sys.exit(1) +from requests.exceptions import ConnectionError def register_with_consul(module): @@ -453,11 +454,11 @@ def main(): ) try: register_with_consul(module) - except IOError, e: - error = e.read() - if not error: - error = str(e) - module.fail_json(msg=error) + except ConnectionError, 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, e: + module.fail_json(msg=str(e)) # import module snippets from ansible.module_utils.basic import * diff --git a/clustering/consul_acl b/clustering/consul_acl index fc997400ae9..cd5466c53b1 100644 --- a/clustering/consul_acl +++ b/clustering/consul_acl @@ -92,6 +92,7 @@ except ImportError: " see https://pypi.python.org/pypi/pyhcl'" sys.exit(1) +from requests.exceptions import ConnectionError def execute(module): @@ -284,11 +285,11 @@ def main(): try: execute(module) - except IOError, e: - error = e.read() - if not error: - error = str(e) - module.fail_json(msg=error) + except ConnectionError, 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, e: + module.fail_json(msg=str(e)) # import module snippets from ansible.module_utils.basic import * diff --git a/clustering/consul_kv b/clustering/consul_kv index 6a2b77ea7c6..8999a43319f 100644 --- a/clustering/consul_kv +++ b/clustering/consul_kv @@ -117,6 +117,7 @@ except ImportError, e: see http://python-consul.readthedocs.org/en/latest/#installation'""" sys.exit(1) +from requests.exceptions import ConnectionError def execute(module): @@ -227,11 +228,12 @@ def main(): try: execute(module) - except IOError, e: - error = e.read() - if not error: - error = str(e) - module.fail_json(msg=error) + except ConnectionError, 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, e: + module.fail_json(msg=str(e)) + # import module snippets from ansible.module_utils.basic import * diff --git a/clustering/consul_session b/clustering/consul_session index f11c5447e57..00f4cae7344 100644 --- a/clustering/consul_session +++ b/clustering/consul_session @@ -80,6 +80,7 @@ except ImportError, e: "http://python-consul.readthedocs.org/en/latest/#installation'" sys.exit(1) +from requests.errors import ConnectionError def execute(module): @@ -202,11 +203,11 @@ def main(): try: execute(module) - except IOError, e: - error = e.read() - if not error: - error = str(e) - module.fail_json(msg=error) + except ConnectionError, 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, e: + module.fail_json(msg=str(e)) # import module snippets from ansible.module_utils.basic import * From 3900643352b1da677847f00eb4e0b9f9d5cee9b6 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Sun, 26 Apr 2015 22:27:53 +0100 Subject: [PATCH 6/7] documentation tweaks to fix missing arguments and specification of defaults --- clustering/consul | 99 ++++++++++++++++++++++++++------------- clustering/consul_acl | 13 ++++- clustering/consul_kv | 30 ++++++++---- clustering/consul_session | 38 +++++++++++++++ 4 files changed, 138 insertions(+), 42 deletions(-) diff --git a/clustering/consul b/clustering/consul index 15a68f068a2..fec55726539 100644 --- a/clustering/consul +++ b/clustering/consul @@ -24,19 +24,19 @@ 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 - that will be used to notify the consul cluster of the health of the service. + 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. + 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: @@ -45,71 +45,105 @@ options: - register or deregister the consul service, defaults to present required: true choices: ['present', 'absent'] + 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_id: description: - the ID for the service, must be unique per node, defaults to the - service name + service name if the service name is supplied required: false + default: service_name if supplied host: description: - - host of the consul agent with which to register the service, - defaults to localhost + - 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 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 + default: None service_port: description: - the port on which the service is listening required for - registration of a service. - required: true + registration of a service, i.e. if service_name or service_id is set + required: false 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 + 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 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 + - 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 service definition. + 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 + 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: name: nginx - port: 80 + service_port: 80 - name: register nginx service with curl check consul: name: nginx - port: 80 + service_port: 80 script: "curl http://localhost" interval: 60s - name: register nginx with some service tags consul: name: nginx - port: 80 + service_port: 80 tags: - prod - webservers @@ -432,23 +466,22 @@ class ConsulCheck(): def main(): module = AnsibleModule( argument_spec=dict( + host=dict(default='localhost'), + port=dict(default=8500, type='int'), check_id=dict(required=False), check_name=dict(required=False), - host=dict(default='localhost'), - interval=dict(required=False, type='str'), - ttl=dict(required=False, type='str'), 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']), + interval=dict(required=False, type='str'), + ttl=dict(required=False, type='str'), tags=dict(required=False, type='list'), - token=dict(required=False), - url=dict(default='http://localhost:8500') + token=dict(required=False) ), supports_check_mode=False, ) diff --git a/clustering/consul_acl b/clustering/consul_acl index cd5466c53b1..5e50c54431e 100644 --- a/clustering/consul_acl +++ b/clustering/consul_acl @@ -22,7 +22,8 @@ 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. + 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 version_added: "1.9" author: Steve Gargan (steve.gargan@gmail.com) options: @@ -53,6 +54,16 @@ options: description: - an list of the rules that should be associated with a given key/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 """ EXAMPLES = ''' diff --git a/clustering/consul_kv b/clustering/consul_kv index 8999a43319f..a9132a3d1c2 100644 --- a/clustering/consul_kv +++ b/clustering/consul_kv @@ -42,8 +42,9 @@ options: '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 + required: false choices: ['present', 'absent', 'acquire', 'release'] + default: present key: description: - the key at which the value should be stored. @@ -57,30 +58,43 @@ options: description: - if the key represents a prefix, each entry with the prefix can be retrieved by setting this to true. - required: 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 - url: - description: - - location of the consul agent with which access the keay/value store, - defaults to http://localhost:8500 - 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 """ @@ -214,8 +228,8 @@ def main(): argument_spec = dict( cas=dict(required=False), flags=dict(required=False), - host=dict(default='localhost'), key=dict(required=True), + host=dict(default='localhost'), port=dict(default=8500, type='int'), recurse=dict(required=False, type='bool'), retrieve=dict(required=False, default=True), diff --git a/clustering/consul_session b/clustering/consul_session index 00f4cae7344..7088dc275ba 100644 --- a/clustering/consul_session +++ b/clustering/consul_session @@ -39,35 +39,73 @@ options: 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. default: 15s + 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 """ 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: 20 + +- name: retrieve info about session by id + consul_session: id=session_id state=info +- name: retrieve active sessions + consul_session: state=list ''' import sys From 3a6f57cbc0d1f307e87cdb8cf256452bb30d6cf6 Mon Sep 17 00:00:00 2001 From: Steve Gargan Date: Mon, 27 Apr 2015 13:40:21 +0100 Subject: [PATCH 7/7] use module.fail_json to report import errors. document valid duration units --- clustering/consul | 20 ++++++++++++++------ clustering/consul_acl | 31 +++++++++++++++++++++--------- clustering/consul_kv | 19 ++++++++++++++----- clustering/consul_session | 40 +++++++++++++++++++++++++++------------ 4 files changed, 78 insertions(+), 32 deletions(-) diff --git a/clustering/consul b/clustering/consul index fec55726539..5db79e20c40 100644 --- a/clustering/consul +++ b/clustering/consul @@ -37,6 +37,9 @@ description: 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. +requirements: + - python-consul + - requests version_added: "1.9" author: Steve Gargan (steve.gargan@gmail.com) options: @@ -172,13 +175,11 @@ except ImportError: try: import consul + from requests.exceptions import ConnectionError + python_consul_installed = True 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) - -from requests.exceptions import ConnectionError - + python_consul_installed = False + def register_with_consul(module): state = module.params.get('state') @@ -462,6 +463,10 @@ class ConsulCheck(): 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( @@ -485,6 +490,9 @@ def main(): ), supports_check_mode=False, ) + + test_dependencies(module) + try: register_with_consul(module) except ConnectionError, e: diff --git a/clustering/consul_acl b/clustering/consul_acl index 5e50c54431e..c481b780a64 100644 --- a/clustering/consul_acl +++ b/clustering/consul_acl @@ -23,7 +23,11 @@ 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 + configuring ACLs, see https://www.consul.io/docs/internals/acl.html. +requirements: + - python-consul + - pyhcl + - requests version_added: "1.9" author: Steve Gargan (steve.gargan@gmail.com) options: @@ -91,17 +95,16 @@ import urllib2 try: import consul + from requests.exceptions import ConnectionError + python_consul_installed = True 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) + python_consul_installed = False try: import hcl + pyhcl_installed = True except ImportError: - print "failed=True msg='pyhcl required for this module."\ - " see https://pypi.python.org/pypi/pyhcl'" - sys.exit(1) + pyhcl_installed = False from requests.exceptions import ConnectionError @@ -271,6 +274,7 @@ class Rule: 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') @@ -278,6 +282,14 @@ def get_consul_api(module, token=None): port=module.params.get('port'), 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( @@ -291,9 +303,10 @@ def main(): token_type=dict( required=False, choices=['client', 'management'], default='client') ) + module = AnsibleModule(argument_spec, supports_check_mode=False) - module = AnsibleModule(argument_spec, supports_check_mode=True) - + test_dependencies(module) + try: execute(module) except ConnectionError, e: diff --git a/clustering/consul_kv b/clustering/consul_kv index a9132a3d1c2..e5a010a8c18 100644 --- a/clustering/consul_kv +++ b/clustering/consul_kv @@ -27,6 +27,9 @@ description: 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. +requirements: + - python-consul + - requests version_added: "1.9" author: Steve Gargan (steve.gargan@gmail.com) options: @@ -126,10 +129,10 @@ except ImportError: try: import consul + from requests.exceptions import ConnectionError + python_consul_installed = True 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) + python_consul_installed = False from requests.exceptions import ConnectionError @@ -222,7 +225,11 @@ def get_consul_api(module, token=None): port=module.params.get('port'), 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( @@ -238,8 +245,10 @@ def main(): value=dict(required=False) ) - module = AnsibleModule(argument_spec, supports_check_mode=True) + module = AnsibleModule(argument_spec, supports_check_mode=False) + test_dependencies(module) + try: execute(module) except ConnectionError, e: diff --git a/clustering/consul_session b/clustering/consul_session index 7088dc275ba..8e6516891d2 100644 --- a/clustering/consul_session +++ b/clustering/consul_session @@ -25,6 +25,9 @@ description: 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-consul + - requests version_added: "1.9" author: Steve Gargan (steve.gargan@gmail.com) options: @@ -50,7 +53,8 @@ options: 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. + acquired until this delay has expired. Valid units for delays + include 'ns', 'us', 'ms', 's', 'm', 'h' default: 15s required: false node: @@ -99,7 +103,7 @@ EXAMPLES = ''' - name: register a session with lock_delay consul_session: name: session_with_delay - delay: 20 + delay: 20s - name: retrieve info about session by id consul_session: id=session_id state=info @@ -113,12 +117,10 @@ import urllib2 try: import consul + from requests.exceptions import ConnectionError + python_consul_installed = True 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) - -from requests.errors import ConnectionError + python_consul_installed = False def execute(module): @@ -182,11 +184,11 @@ def update_session(module): changed = True try: - + session = consul.session.create( name=name, node=node, - lock_delay=delay, + lock_delay=validate_duration('delay', delay), dc=datacenter, checks=checks ) @@ -219,15 +221,27 @@ def remove_session(module): module.fail_json(msg="Could not remove session with id '%s' %s" % ( session_id, e)) +def validate_duration(name, duration): + if duration: + duration_units = ['ns', 'us', 'ms', 's', 'm', 'h'] + if not any((duration.endswith(suffix) for suffix in duration_units)): + raise Exception('Invalid %s %s you must specify units (%s)' % + (name, duration, ', '.join(duration_units))) + return duration 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), + delay=dict(required=False,type='str', default='15s'), host=dict(default='localhost'), port=dict(default=8500, type='int'), id=dict(required=False), @@ -237,8 +251,10 @@ def main(): choices=['present', 'absent', 'info', 'node', 'list']) ) - module = AnsibleModule(argument_spec, supports_check_mode=True) - + module = AnsibleModule(argument_spec, supports_check_mode=False) + + test_dependencies(module) + try: execute(module) except ConnectionError, e: