Merge "Remove color formatting from the output of rabbitmqctl cluster_status"
diff --git a/_modules/rabbitmq_custom.py b/_modules/rabbitmq_custom.py
new file mode 100644
index 0000000..182ca49
--- /dev/null
+++ b/_modules/rabbitmq_custom.py
@@ -0,0 +1,190 @@
+# -*- coding: utf-8 -*-
+
+# This file is copy of original /usr/lib/python2.7/dist-packages/salt/modules/rabbitmq.py
+# with fix applied for PROD-35125 - rabbitmq module incompatible with rabbitmq-server 3.8+
+# It fix native check_password function by override it here and call it from
+# _states/rabbitmq_user_common.py which also introduced in this patch.
+# Fix source: https://github.com/saltstack/salt/commit/d0e0ed6b60f4de6d2b2551cad2fc7a553efe0274
+
+from __future__ import absolute_import
+
+# Import python libs
+import json
+import re
+import logging
+import os
+import os.path
+import random
+import string
+
+# Import salt libs
+import salt.utils
+import salt.utils.itertools
+import salt.ext.six as six
+from salt.exceptions import SaltInvocationError
+from salt.ext.six.moves import range
+from salt.exceptions import CommandExecutionError
+
+log = logging.getLogger(__name__)
+
+RABBITMQCTL = None
+RABBITMQ_PLUGINS = None
+
+
+def __virtual__():
+    '''
+    Verify RabbitMQ is installed.
+    '''
+    global RABBITMQCTL
+    global RABBITMQ_PLUGINS
+
+    if salt.utils.is_windows():
+        from salt.ext.six.moves import winreg
+        key = None
+        try:
+            key = winreg.OpenKeyEx(
+                winreg.HKEY_LOCAL_MACHINE,
+                'SOFTWARE\\VMware, Inc.\\RabbitMQ Server',
+                0,
+                winreg.KEY_READ | winreg.KEY_WOW64_32KEY
+            )
+            (dir_path, value_type) = winreg.QueryValueEx(
+                key,
+                'Install_Dir'
+            )
+            if value_type != winreg.REG_SZ:
+                raise TypeError('Invalid RabbitMQ Server directory type: {0}'.format(value_type))
+            if not os.path.isdir(dir_path):
+                raise IOError('RabbitMQ directory not found: {0}'.format(dir_path))
+            subdir_match = ''
+            for name in os.listdir(dir_path):
+                if name.startswith('rabbitmq_server-'):
+                    subdir_path = os.path.join(dir_path, name)
+                    # Get the matching entry that is last in ASCII order.
+                    if os.path.isdir(subdir_path) and subdir_path > subdir_match:
+                        subdir_match = subdir_path
+            if not subdir_match:
+                raise IOError('"rabbitmq_server-*" subdirectory not found in: {0}'.format(dir_path))
+            RABBITMQCTL = os.path.join(subdir_match, 'sbin', 'rabbitmqctl.bat')
+            RABBITMQ_PLUGINS = os.path.join(subdir_match, 'sbin', 'rabbitmq-plugins.bat')
+        except Exception:
+            pass
+        finally:
+            if key is not None:
+                winreg.CloseKey(key)
+    else:
+        RABBITMQCTL = salt.utils.which('rabbitmqctl')
+        RABBITMQ_PLUGINS = salt.utils.which('rabbitmq-plugins')
+
+    if not RABBITMQCTL:
+        return (False, 'Module rabbitmq: module only works when RabbitMQ is installed')
+    return True
+
+
+def _format_response(response, msg):
+    if isinstance(response, dict):
+        if response['retcode'] != 0 or response['stderr']:
+            raise CommandExecutionError(
+                'RabbitMQ command failed: {0}'.format(response['stderr'])
+            )
+        else:
+            response = response['stdout']
+    else:
+        if 'Error' in response:
+            raise CommandExecutionError(
+                'RabbitMQ command failed: {0}'.format(response)
+            )
+    return {
+        msg: response
+    }
+
+
+def check_password(name, password, runas=None):
+    '''
+    .. versionadded:: 2016.3.0
+
+    Checks if a user's password is valid.
+
+    CLI Example:
+
+    .. code-block:: bash
+
+        salt '*' rabbitmq.check_password rabbit_user password
+    '''
+    # try to get the rabbitmq-version - adapted from _get_rabbitmq_plugin
+
+    if runas is None and not salt.utils.is_windows():
+        runas = salt.utils.get_user()
+
+    try:
+        res = __salt__['cmd.run']([RABBITMQCTL, 'status'], reset_system_locale=False, runas=runas, python_shell=False)
+
+        # Fix for PROD-35125 - rabbitmq module incompatible with rabbitmq-server 3.8+
+        # https://github.com/saltstack/salt/commit/d0e0ed6b60f4de6d2b2551cad2fc7a553efe0274
+
+        # Check regex against older RabbitMQ version status output
+        old_server_version = re.search(r'\{rabbit,"RabbitMQ","(.+)"\}', res)
+        # Check regex against newer RabbitMQ version status output
+        server_version = re.search(r"RabbitMQ version:\s*(.+)", res)
+
+        if server_version is None and old_server_version is None:
+            raise ValueError
+
+        if old_server_version:
+            server_version = old_server_version
+        server_version = server_version.group(1).split("-")[0]
+        version = [int(i) for i in server_version.split(".")]
+
+    except ValueError:
+        version = (0, 0, 0)
+    if len(version) < 3:
+        version = (0, 0, 0)
+
+    # rabbitmq introduced a native api to check a username and password in version 3.5.7.
+    if tuple(version) >= (3, 5, 7):
+        if salt.utils.is_windows():
+            # On Windows, if the password contains a special character
+            # such as '|', normal execution will fail. For example:
+            # cmd: rabbitmq.add_user abc "asdf|def"
+            # stderr: 'def' is not recognized as an internal or external
+            #         command,\r\noperable program or batch file.
+            # Work around this by using a shell and a quoted command.
+            python_shell = True
+            cmd = '"{0}" authenticate_user "{1}" "{2}"'.format(
+                RABBITMQCTL, name, password
+            )
+        else:
+            python_shell = False
+            cmd = [RABBITMQCTL, 'authenticate_user', name, password]
+
+        res = __salt__['cmd.run_all'](
+            cmd,
+            reset_system_locale=False,
+            runas=runas,
+            output_loglevel='quiet',
+            python_shell=python_shell)
+
+        if res['retcode'] != 0 or res['stderr']:
+            return False
+        return True
+
+    cmd = ('rabbit_auth_backend_internal:check_user_login'
+        '(<<"{0}">>, [{{password, <<"{1}">>}}]).').format(
+        name.replace('"', '\\"'),
+        password.replace('"', '\\"'))
+
+    res = __salt__['cmd.run_all'](
+        [RABBITMQCTL, 'eval', cmd],
+        reset_system_locale=False,
+        runas=runas,
+        output_loglevel='quiet',
+        python_shell=False)
+    msg = 'password-check'
+
+    _response = _format_response(res, msg)
+    _key = _response.keys()[0]
+
+    if 'invalid credentials' in _response[_key]:
+        return False
+
+    return True
diff --git a/_states/rabbitmq_user_custom.py b/_states/rabbitmq_user_custom.py
new file mode 100644
index 0000000..49b6622
--- /dev/null
+++ b/_states/rabbitmq_user_custom.py
@@ -0,0 +1,236 @@
+# -*- coding: utf-8 -*-
+'''
+Manage RabbitMQ Users
+=====================
+
+Example:
+
+.. code-block:: yaml
+
+    rabbit_user:
+      rabbitmq_user.present:
+        - password: password
+        - force: True
+        - tags:
+          - monitoring
+          - user
+        - perms:
+          - '/':
+            - '.*'
+            - '.*'
+            - '.*'
+        - runas: rabbitmq
+'''
+
+# This file is copy of original /usr/lib/python2.7/dist-packages/salt/states/rabbitmq_user.py
+# with fix applied for PROD-35125 - rabbitmq module incompatible with rabbitmq-server 3.8+
+# It change module for check_password function.
+# Now it uses rabbitmq_custom module instead of rabbitmq for check_password function.
+
+# Import python libs
+from __future__ import absolute_import
+import logging
+
+# Import salt libs
+import salt.utils
+import salt.ext.six as six
+from salt.exceptions import CommandExecutionError
+
+log = logging.getLogger(__name__)
+
+
+def __virtual__():
+    '''
+    Only load if RabbitMQ is installed.
+    '''
+    return salt.utils.which('rabbitmqctl') is not None
+
+
+def _check_perms_changes(name, newperms, runas=None, existing=None):
+    '''
+    Check whether Rabbitmq user's permissions need to be changed.
+    '''
+    if not newperms:
+        return False
+
+    if existing is None:
+        try:
+            existing = __salt__['rabbitmq.list_user_permissions'](name, runas=runas)
+        except CommandExecutionError as err:
+            log.error('Error: {0}'.format(err))
+            return False
+
+    perm_need_change = False
+    for vhost_perms in newperms:
+        for vhost, perms in six.iteritems(vhost_perms):
+            if vhost in existing:
+                existing_vhost = existing[vhost]
+                if perms != existing_vhost:
+                    # This checks for setting permissions to nothing in the state,
+                    # when previous state runs have already set permissions to
+                    # nothing. We don't want to report a change in this case.
+                    if existing_vhost == '' and perms == ['', '', '']:
+                        continue
+                    perm_need_change = True
+            else:
+                perm_need_change = True
+
+    return perm_need_change
+
+
+def _get_current_tags(name, runas=None):
+    '''
+    Whether Rabbitmq user's tags need to be changed
+    '''
+    try:
+        return list(__salt__['rabbitmq.list_users'](runas=runas)[name])
+    except CommandExecutionError as err:
+        log.error('Error: {0}'.format(err))
+        return []
+
+
+def present(name,
+            password=None,
+            force=False,
+            tags=None,
+            perms=(),
+            runas=None):
+    '''
+    Ensure the RabbitMQ user exists.
+
+    name
+        User name
+    password
+        User's password, if one needs to be set
+    force
+        If user exists, forcibly change the password
+    tags
+        Optional list of tags for the user
+    perms
+        A list of dicts with vhost keys and 3-tuple values
+    runas
+        Name of the user to run the command
+    '''
+    ret = {'name': name, 'result': False, 'comment': '', 'changes': {}}
+
+    try:
+        user = __salt__['rabbitmq.user_exists'](name, runas=runas)
+    except CommandExecutionError as err:
+        ret['comment'] = 'Error: {0}'.format(err)
+        return ret
+
+    passwd_reqs_update = False
+    if user and password is not None:
+        try:
+            # Fix for PROD-35125
+            # Use rabbitmq_custom module instead of rabbitmq for check_password function
+            if not __salt__['rabbitmq_custom.check_password'](name,
+                password, runas=runas):
+                passwd_reqs_update = True
+                log.debug('RabbitMQ user %s password update required', name)
+        except CommandExecutionError as err:
+            ret['comment'] = 'Error: {0}'.format(err)
+            return ret
+
+    if user and not any((force, perms, tags, passwd_reqs_update)):
+        log.debug(('RabbitMQ user \'%s\' exists, password is up to'
+                   ' date and force is not set.'), name)
+        ret['comment'] = 'User \'{0}\' is already present.'.format(name)
+        ret['result'] = True
+        return ret
+
+    if not user:
+        ret['changes'].update({'user':
+                              {'old': '',
+                               'new': name}})
+        if __opts__['test']:
+            ret['result'] = None
+            ret['comment'] = 'User \'{0}\' is set to be created.'.format(name)
+            return ret
+
+        log.debug(
+            'RabbitMQ user \'{0}\' doesn\'t exist - Creating.'.format(name))
+        try:
+            __salt__['rabbitmq.add_user'](name, password, runas=runas)
+        except CommandExecutionError as err:
+            ret['comment'] = 'Error: {0}'.format(err)
+            return ret
+    else:
+        log.debug('RabbitMQ user \'{0}\' exists'.format(name))
+        if force or passwd_reqs_update:
+            if password is not None:
+                if not __opts__['test']:
+                    try:
+                        __salt__['rabbitmq.change_password'](name, password, runas=runas)
+                    except CommandExecutionError as err:
+                        ret['comment'] = 'Error: {0}'.format(err)
+                        return ret
+                ret['changes'].update({'password':
+                                      {'old': '',
+                                       'new': 'Set password.'}})
+            else:
+                if not __opts__['test']:
+                    log.debug('Password for {0} is not set - Clearing password.'.format(name))
+                    try:
+                        __salt__['rabbitmq.clear_password'](name, runas=runas)
+                    except CommandExecutionError as err:
+                        ret['comment'] = 'Error: {0}'.format(err)
+                        return ret
+                ret['changes'].update({'password':
+                                      {'old': 'Removed password.',
+                                       'new': ''}})
+
+    if tags is not None:
+        current_tags = _get_current_tags(name, runas=runas)
+        if isinstance(tags, str):
+            tags = tags.split()
+        # Diff the tags sets. Symmetric difference operator ^ will give us
+        # any element in one set, but not both
+        if set(tags) ^ set(current_tags):
+            if not __opts__['test']:
+                try:
+                    __salt__['rabbitmq.set_user_tags'](name, tags, runas=runas)
+                except CommandExecutionError as err:
+                    ret['comment'] = 'Error: {0}'.format(err)
+                    return ret
+            ret['changes'].update({'tags':
+                                  {'old': current_tags,
+                                   'new': tags}})
+    try:
+        existing_perms = __salt__['rabbitmq.list_user_permissions'](name, runas=runas)
+    except CommandExecutionError as err:
+        ret['comment'] = 'Error: {0}'.format(err)
+        return ret
+
+    if _check_perms_changes(name, perms, runas=runas, existing=existing_perms):
+        for vhost_perm in perms:
+            for vhost, perm in six.iteritems(vhost_perm):
+                if not __opts__['test']:
+                    try:
+                        __salt__['rabbitmq.set_permissions'](
+                            vhost, name, perm[0], perm[1], perm[2], runas=runas
+                        )
+                    except CommandExecutionError as err:
+                        ret['comment'] = 'Error: {0}'.format(err)
+                        return ret
+                new_perms = {vhost: perm}
+                if existing_perms != new_perms:
+                    if ret['changes'].get('perms') is None:
+                        ret['changes'].update({'perms':
+                                              {'old': {},
+                                               'new': {}}})
+                    ret['changes']['perms']['old'].update(existing_perms)
+                    ret['changes']['perms']['new'].update(new_perms)
+
+    ret['result'] = True
+    if ret['changes'] == {}:
+        ret['comment'] = '\'{0}\' is already in the desired state.'.format(name)
+        return ret
+
+    if __opts__['test']:
+        ret['result'] = None
+        ret['comment'] = 'Configuration for \'{0}\' will change.'.format(name)
+        return ret
+
+    ret['comment'] = '\'{0}\' was configured.'.format(name)
+    return ret
diff --git a/rabbitmq/server/user.sls b/rabbitmq/server/user.sls
index 600e3e7..9f71963 100644
--- a/rabbitmq/server/user.sls
+++ b/rabbitmq/server/user.sls
@@ -7,7 +7,7 @@
 {%- if server.admin is defined %}
 
 rabbit_user_admin_present:
-  rabbitmq_user.present:
+  rabbitmq_user_custom.present:
   - name: {{ server.admin.name }}
   - password: {{ server.admin.password }}
   - force: True
diff --git a/rabbitmq/server/vhost.sls b/rabbitmq/server/vhost.sls
index f53019c..8e71f3e 100644
--- a/rabbitmq/server/vhost.sls
+++ b/rabbitmq/server/vhost.sls
@@ -16,7 +16,7 @@
 {%- endif %}
 
 rabbitmq_user_{{ host.user }}:
-  rabbitmq_user.present:
+  rabbitmq_user_custom.present:
   - name: {{ host.user }}
   - password: {{ host.password }}
   - force: true
diff --git a/rabbitmq/upgrade/verify/_service.sls b/rabbitmq/upgrade/verify/_service.sls
index e212b2a..9bf9221 100644
--- a/rabbitmq/upgrade/verify/_service.sls
+++ b/rabbitmq/upgrade/verify/_service.sls
@@ -6,10 +6,11 @@
 
 {%- if server.get('enabled') %}
 {% set host_id = salt['network.get_hostname']() %}
+{% set rmq_version = salt['pkg.version']('rabbitmq-server') %}
 
 rabbitmq_status:
   cmd.run:
-    - name: rabbitmqctl cluster_status |grep -w running_nodes |grep -w {{ host_id }}
+    - name: rabbitmqctl cluster_status{%- if salt['pkg.version_cmp'](rmq_version,'3.8') >= 0 %} --formatter erlang {%- endif %}|grep -w running_nodes |grep -w {{ host_id }}
   {%- if grains.get('noservices') %}
     - onlyif: /bin/false
   {%- endif %}