From f61878fa805649c0d2a89f8174024277672a7ecc Mon Sep 17 00:00:00 2001 From: Gerrit Germis Date: Tue, 31 May 2016 18:26:25 +0200 Subject: [PATCH] fail when backend host is not found (#1385) --- lib/ansible/modules/extras/network/haproxy.py | 217 ++++++++---------- 1 file changed, 100 insertions(+), 117 deletions(-) diff --git a/lib/ansible/modules/extras/network/haproxy.py b/lib/ansible/modules/extras/network/haproxy.py index 4cc1c1c618b..f9b7661243d 100644 --- a/lib/ansible/modules/extras/network/haproxy.py +++ b/lib/ansible/modules/extras/network/haproxy.py @@ -60,6 +60,12 @@ options: required: true default: null choices: [ "enabled", "disabled" ] + fail_on_not_found: + description: + - Fail whenever trying to enable/disable a backend host that does not exist + required: false + default: false + version_added: "2.2" wait: description: - Wait until the server reports a status of 'UP' when `state=enabled`, or @@ -105,6 +111,9 @@ EXAMPLES = ''' # disable backend server in 'www' backend pool and drop open sessions to it - haproxy: state=disabled host={{ inventory_hostname }} backend=www socket=/var/run/haproxy.sock shutdown_sessions=true +# disable server without backend pool name (apply to all available backend pool) but fail when the backend host is not found +- haproxy: state=disabled host={{ inventory_hostname }} fail_on_not_found=yes + # enable server in 'www' backend pool - haproxy: state=enabled host={{ inventory_hostname }} backend=www @@ -123,6 +132,7 @@ author: "Ravi Bhure (@ravibhure)" import socket import csv import time +from string import Template DEFAULT_SOCKET_LOCATION="/var/run/haproxy.sock" @@ -156,23 +166,17 @@ class HAProxy(object): self.weight = self.module.params['weight'] self.socket = self.module.params['socket'] self.shutdown_sessions = self.module.params['shutdown_sessions'] + self.fail_on_not_found = self.module.params['fail_on_not_found'] self.wait = self.module.params['wait'] self.wait_retries = self.module.params['wait_retries'] self.wait_interval = self.module.params['wait_interval'] - self.command_results = [] - self.status_servers = [] - self.status_weights = [] - self.previous_weights = [] - self.previous_states = [] - self.current_states = [] - self.current_weights = [] + self.command_results = {} def execute(self, cmd, timeout=200, capture_output=True): """ Executes a HAProxy command by sending a message to a HAProxy's local UNIX socket and waiting up to 'timeout' milliseconds for the response. """ - self.client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self.client.connect(self.socket) self.client.sendall('%s\n' % cmd) @@ -183,10 +187,67 @@ class HAProxy(object): result += buf buf = self.client.recv(RECV_SIZE) if capture_output: - self.command_results = result.strip() + self.capture_command_output(cmd, result.strip()) self.client.close() return result + + def capture_command_output(self, cmd, output): + """ + Capture the output for a command + """ + if not 'command' in self.command_results.keys(): + self.command_results['command'] = [] + self.command_results['command'].append(cmd) + if not 'output' in self.command_results.keys(): + self.command_results['output'] = [] + self.command_results['output'].append(output) + + + def discover_all_backends(self): + """ + Discover all entries with svname = 'BACKEND' and return a list of their corresponding + pxnames + """ + data = self.execute('show stat', 200, False).lstrip('# ') + r = csv.DictReader(data.splitlines()) + return map(lambda d: d['pxname'], filter(lambda d: d['svname'] == 'BACKEND', r)) + + + def execute_for_backends(self, cmd, pxname, svname, wait_for_status = None): + """ + Run some command on the specified backends. If no backends are provided they will + be discovered automatically (all backends) + """ + # Discover backends if none are given + if pxname is None: + backends = self.discover_all_backends() + else: + backends = [pxname] + + # Run the command for each requested backend + for backend in backends: + # Fail when backends were not found + state = self.get_state_for(backend, svname) + if (self.fail_on_not_found or self.wait) and state is None: + self.module.fail_json(msg="The specified backend '%s/%s' was not found!" % (backend, svname)) + + self.execute(Template(cmd).substitute(pxname = backend, svname = svname)) + if self.wait: + self.wait_until_status(backend, svname, wait_for_status) + + + def get_state_for(self, pxname, svname): + """ + Find the state of specific services. When pxname is not set, get all backends for a specific host. + Returns a list of dictionaries containing the status and weight for those services. + """ + data = self.execute('show stat', 200, False).lstrip('# ') + r = csv.DictReader(data.splitlines()) + state = map(lambda d: { 'status': d['status'], 'weight': d['weight'] }, filter(lambda d: (pxname is None or d['pxname'] == pxname) and d['svname'] == svname, r)) + return state or None + + def wait_until_status(self, pxname, svname, status): """ Wait for a service to reach the specified status. Try RETRIES times @@ -195,49 +256,16 @@ class HAProxy(object): not found, the module will fail. """ for i in range(1, self.wait_retries): - data = self.execute('show stat', 200, False).lstrip('# ') - r = csv.DictReader(data.splitlines()) - found = False - for row in r: - if row['pxname'] == pxname and row['svname'] == svname: - found = True - if row['status'] == status: - return True; - else: - time.sleep(self.wait_interval) - - if not found: - self.module.fail_json(msg="unable to find server %s/%s" % (pxname, svname)) + state = self.get_state_for(pxname, svname) + + # We can assume there will only be 1 element in state because both svname and pxname are always set when we get here + if state[0]['status'] == status: + return True + else: + time.sleep(self.wait_interval) self.module.fail_json(msg="server %s/%s not status '%s' after %d retries. Aborting." % (pxname, svname, status, self.wait_retries)) - def get_current_state(self, host, backend): - """ - Gets the each original state value from show stat. - Runs before and after to determine if values are changed. - This relies on weight always being the next element after - status in "show stat" as well as status states remaining - as indicated in status_states and haproxy documentation. - """ - - output = self.execute('show stat') - output = output.lstrip('# ').strip() - output = output.split(',') - result = output - status_states = [ 'UP','DOWN','DRAIN','NOLB','MAINT' ] - self.status_server = [] - status_weight_pos = [] - self.status_weight = [] - - for check, status in enumerate(result): - if status in status_states: - self.status_server.append(status) - status_weight_pos.append(check + 1) - - for weight in status_weight_pos: - self.status_weight.append(result[weight]) - - return{'self.status_server':self.status_server, 'self.status_weight':self.status_weight} def enabled(self, host, backend, weight): """ @@ -245,33 +273,11 @@ class HAProxy(object): also supports to get current weight for server (default) and set the weight for haproxy backend server when provides. """ - svname = host - if self.backend is None: - output = self.execute('show stat') - #sanitize and make a list of lines - output = output.lstrip('# ').strip() - output = output.split('\n') - result = output - - for line in result: - if 'BACKEND' in line: - result = line.split(',')[0] - pxname = result - cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname) - if weight: - cmd += "; set weight %s/%s %s" % (pxname, svname, weight) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'UP') + cmd = "get weight $pxname/$svname; enable server $pxname/$svname" + if weight: + cmd += "; set weight $pxname/$svname %s" % weight + self.execute_for_backends(cmd, backend, host, 'UP') - else: - pxname = backend - cmd = "get weight %s/%s ; enable server %s/%s" % (pxname, svname, pxname, svname) - if weight: - cmd += "; set weight %s/%s %s" % (pxname, svname, weight) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'UP') def disabled(self, host, backend, shutdown_sessions): """ @@ -279,64 +285,40 @@ class HAProxy(object): performed on the server until it leaves maintenance, also it shutdown sessions while disabling backend host server. """ - svname = host - if self.backend is None: - output = self.execute('show stat') - #sanitize and make a list of lines - output = output.lstrip('# ').strip() - output = output.split('\n') - result = output - - for line in result: - if 'BACKEND' in line: - result = line.split(',')[0] - pxname = result - cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname) - if shutdown_sessions: - cmd += "; shutdown sessions server %s/%s" % (pxname, svname) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'MAINT') + cmd = "get weight $pxname/$svname; disable server $pxname/$svname" + if shutdown_sessions: + cmd += "; shutdown sessions server $pxname/$svname" + self.execute_for_backends(cmd, backend, host, 'MAINT') - else: - pxname = backend - cmd = "get weight %s/%s ; disable server %s/%s" % (pxname, svname, pxname, svname) - if shutdown_sessions: - cmd += "; shutdown sessions server %s/%s" % (pxname, svname) - self.execute(cmd) - if self.wait: - self.wait_until_status(pxname, svname, 'MAINT') def act(self): """ Figure out what you want to do from ansible, and then do it. """ - - self.get_current_state(self.host, self.backend) - self.previous_states = ','.join(self.status_server) - self.previous_weights = ','.join(self.status_weight) + # Get the state before the run + state_before = self.get_state_for(self.backend, self.host) + self.command_results['state_before'] = state_before # toggle enable/disbale server if self.state == 'enabled': self.enabled(self.host, self.backend, self.weight) - elif self.state == 'disabled': self.disabled(self.host, self.backend, self.shutdown_sessions) - else: self.module.fail_json(msg="unknown state specified: '%s'" % self.state) - self.get_current_state(self.host, self.backend) - self.current_states = ','.join(self.status_server) - self.current_weights = ','.join(self.status_weight) - + # Get the state after the run + state_after = self.get_state_for(self.backend, self.host) + self.command_results['state_after'] = state_after - if self.current_weights != self.previous_weights: - self.module.exit_json(stdout=self.command_results, changed=True) - elif self.current_states != self.previous_states: - self.module.exit_json(stdout=self.command_results, changed=True) + # Report change status + if state_before != state_after: + self.command_results['changed'] = True + self.module.exit_json(**self.command_results) else: - self.module.exit_json(stdout=self.command_results, changed=False) + self.command_results['changed'] = False + self.module.exit_json(**self.command_results) + def main(): @@ -349,11 +331,11 @@ def main(): weight=dict(required=False, default=None), socket = dict(required=False, default=DEFAULT_SOCKET_LOCATION), shutdown_sessions=dict(required=False, default=False), + fail_on_not_found=dict(required=False, default=False, type='bool'), wait=dict(required=False, default=False, type='bool'), wait_retries=dict(required=False, default=WAIT_RETRIES, type='int'), wait_interval=dict(required=False, default=WAIT_INTERVAL, type='int'), ), - ) if not socket: @@ -366,3 +348,4 @@ def main(): from ansible.module_utils.basic import * main() +