We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Special characters in st2admin account causing st2 key failure
Just upgraded to version 3.4 and my keyvault is having problems. I believe it's due to my st2admin password containing special characters.

```
[root@stackstorm workflows]# st2 key list --scope=all
Traceback (most recent call last):
  File "/bin/st2", line 10, in <module>
    sys.exit(main())
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/shell.py", line 470, in main
    return Shell().run(argv)
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/shell.py", line 385, in run
    config = self._parse_config_file(args=args, validate_config_permissions=False)
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/base.py", line 183, in _parse_config_file
    result = parser.parse()
  File "/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/config_parser.py", line 197, in parse
    value = get_func(section, key)
  File "/usr/lib64/python3.6/configparser.py", line 800, in get
    d)
  File "/usr/lib64/python3.6/configparser.py", line 394, in before_get
    self._interpolate_some(parser, option, L, value, section, defaults, 1)
  File "/usr/lib64/python3.6/configparser.py", line 444, in _interpolate_some
    "found: %r" % (rest,))
configparser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%3C#V~Bvg%3E3t+'
```

This password above is what I used to install stackstorm. Or at least part of it. I've since changed the password via the documented htpasswd method, but the issue persists. Any tips? Left the password in for research purposes.

curl -sSL https://stackstorm.com/packages/install.sh | bash -s -- --user=st2admin --password='q7j/t%3C#V~Bvg%3E3t+'

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `st2client/st2client/config_parser.py`
Content:
```
1 # Copyright 2020 The StackStorm Authors.
2 # Copyright 2019 Extreme Networks, Inc.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15 
16 """
17 Module for parsing CLI config file.
18 """
19 
20 from __future__ import absolute_import
21 
22 import logging
23 import os
24 
25 from collections import defaultdict
26 
27 import io
28 
29 import six
30 from six.moves.configparser import ConfigParser
31 
32 
33 __all__ = [
34     "CLIConfigParser",
35     "ST2_CONFIG_DIRECTORY",
36     "ST2_CONFIG_PATH",
37     "CONFIG_DEFAULT_VALUES",
38 ]
39 
40 ST2_CONFIG_DIRECTORY = "~/.st2"
41 ST2_CONFIG_DIRECTORY = os.path.abspath(os.path.expanduser(ST2_CONFIG_DIRECTORY))
42 
43 ST2_CONFIG_PATH = os.path.abspath(os.path.join(ST2_CONFIG_DIRECTORY, "config"))
44 
45 CONFIG_FILE_OPTIONS = {
46     "general": {
47         "base_url": {"type": "string", "default": None},
48         "api_version": {"type": "string", "default": None},
49         "cacert": {"type": "string", "default": None},
50         "silence_ssl_warnings": {"type": "bool", "default": False},
51         "silence_schema_output": {"type": "bool", "default": True},
52     },
53     "cli": {
54         "debug": {"type": "bool", "default": False},
55         "cache_token": {"type": "boolean", "default": True},
56         "timezone": {"type": "string", "default": "UTC"},
57     },
58     "credentials": {
59         "username": {"type": "string", "default": None},
60         "password": {"type": "string", "default": None},
61         "api_key": {"type": "string", "default": None},
62     },
63     "api": {"url": {"type": "string", "default": None}},
64     "auth": {"url": {"type": "string", "default": None}},
65     "stream": {"url": {"type": "string", "default": None}},
66 }
67 
68 CONFIG_DEFAULT_VALUES = {}
69 
70 for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):
71     CONFIG_DEFAULT_VALUES[section] = {}
72 
73     for key, options in six.iteritems(keys):
74         default_value = options["default"]
75         CONFIG_DEFAULT_VALUES[section][key] = default_value
76 
77 
78 class CLIConfigParser(object):
79     def __init__(
80         self,
81         config_file_path,
82         validate_config_exists=True,
83         validate_config_permissions=True,
84         log=None,
85     ):
86         if validate_config_exists and not os.path.isfile(config_file_path):
87             raise ValueError('Config file "%s" doesn\'t exist')
88 
89         if log is None:
90             log = logging.getLogger(__name__)
91             logging.basicConfig()
92 
93         self.config_file_path = config_file_path
94         self.validate_config_permissions = validate_config_permissions
95         self.LOG = log
96 
97     def parse(self):
98         """
99         Parse the config and return a dict with the parsed values.
100 
101         :rtype: ``dict``
102         """
103         result = defaultdict(dict)
104 
105         if not os.path.isfile(self.config_file_path):
106             # Config doesn't exist, return the default values
107             return CONFIG_DEFAULT_VALUES
108 
109         config_dir_path = os.path.dirname(self.config_file_path)
110 
111         if self.validate_config_permissions:
112             # Make sure the directory permissions == 0o770
113             if bool(os.stat(config_dir_path).st_mode & 0o7):
114                 self.LOG.warn(
115                     "The StackStorm configuration directory permissions are "
116                     "insecure (too permissive): others have access."
117                 )
118 
119             # Make sure the setgid bit is set on the directory
120             if not bool(os.stat(config_dir_path).st_mode & 0o2000):
121                 self.LOG.info(
122                     "The SGID bit is not set on the StackStorm configuration "
123                     "directory."
124                 )
125 
126             # Make sure the file permissions == 0o660
127             if bool(os.stat(self.config_file_path).st_mode & 0o7):
128                 self.LOG.warn(
129                     "The StackStorm configuration file permissions are "
130                     "insecure: others have access."
131                 )
132 
133         config = ConfigParser()
134         with io.open(self.config_file_path, "r", encoding="utf8") as fp:
135             config.readfp(fp)
136 
137         for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):
138             for key, options in six.iteritems(keys):
139                 key_type = options["type"]
140                 key_default_value = options["default"]
141 
142                 if config.has_option(section, key):
143                     if key_type in ["str", "string"]:
144                         get_func = config.get
145                     elif key_type in ["int", "integer"]:
146                         get_func = config.getint
147                     elif key_type in ["float"]:
148                         get_func = config.getfloat
149                     elif key_type in ["bool", "boolean"]:
150                         get_func = config.getboolean
151                     else:
152                         msg = 'Invalid type "%s" for option "%s"' % (key_type, key)
153                         raise ValueError(msg)
154 
155                     value = get_func(section, key)
156                     result[section][key] = value
157                 else:
158                     result[section][key] = key_default_value
159 
160         return dict(result)
161
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/st2client/st2client/config_parser.py b/st2client/st2client/config_parser.py
--- a/st2client/st2client/config_parser.py
+++ b/st2client/st2client/config_parser.py
@@ -152,7 +152,7 @@
                         msg = 'Invalid type "%s" for option "%s"' % (key_type, key)
                         raise ValueError(msg)
 
-                    value = get_func(section, key)
+                    value = get_func(section, key, raw=True)
                     result[section][key] = value
                 else:
                     result[section][key] = key_default_value

{"golden_diff": "diff --git a/st2client/st2client/config_parser.py b/st2client/st2client/config_parser.py\n--- a/st2client/st2client/config_parser.py\n+++ b/st2client/st2client/config_parser.py\n@@ -152,7 +152,7 @@\n                         msg = 'Invalid type \"%s\" for option \"%s\"' % (key_type, key)\n                         raise ValueError(msg)\n \n-                    value = get_func(section, key)\n+                    value = get_func(section, key, raw=True)\n                     result[section][key] = value\n                 else:\n                     result[section][key] = key_default_value\n", "issue": "Special characters in st2admin account causing st2 key failure\nJust upgraded to version 3.4 and my keyvault is having problems. I believe it's due to my st2admin password containing special characters.\r\n\r\n```\r\n[root@stackstorm workflows]# st2 key list --scope=all\r\nTraceback (most recent call last):\r\n  File \"/bin/st2\", line 10, in <module>\r\n    sys.exit(main())\r\n  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/shell.py\", line 470, in main\r\n    return Shell().run(argv)\r\n  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/shell.py\", line 385, in run\r\n    config = self._parse_config_file(args=args, validate_config_permissions=False)\r\n  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/base.py\", line 183, in _parse_config_file\r\n    result = parser.parse()\r\n  File \"/opt/stackstorm/st2/lib/python3.6/site-packages/st2client/config_parser.py\", line 197, in parse\r\n    value = get_func(section, key)\r\n  File \"/usr/lib64/python3.6/configparser.py\", line 800, in get\r\n    d)\r\n  File \"/usr/lib64/python3.6/configparser.py\", line 394, in before_get\r\n    self._interpolate_some(parser, option, L, value, section, defaults, 1)\r\n  File \"/usr/lib64/python3.6/configparser.py\", line 444, in _interpolate_some\r\n    \"found: %r\" % (rest,))\r\nconfigparser.InterpolationSyntaxError: '%' must be followed by '%' or '(', found: '%3C#V~Bvg%3E3t+'\r\n```\r\n\r\nThis password above is what I used to install stackstorm. Or at least part of it. I've since changed the password via the documented htpasswd method, but the issue persists. Any tips? Left the password in for research purposes.\r\n\r\ncurl -sSL https://stackstorm.com/packages/install.sh | bash -s -- --user=st2admin --password='q7j/t%3C#V~Bvg%3E3t+'\n", "before_files": [{"content": "# Copyright 2020 The StackStorm Authors.\n# Copyright 2019 Extreme Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nModule for parsing CLI config file.\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport logging\nimport os\n\nfrom collections import defaultdict\n\nimport io\n\nimport six\nfrom six.moves.configparser import ConfigParser\n\n\n__all__ = [\n    \"CLIConfigParser\",\n    \"ST2_CONFIG_DIRECTORY\",\n    \"ST2_CONFIG_PATH\",\n    \"CONFIG_DEFAULT_VALUES\",\n]\n\nST2_CONFIG_DIRECTORY = \"~/.st2\"\nST2_CONFIG_DIRECTORY = os.path.abspath(os.path.expanduser(ST2_CONFIG_DIRECTORY))\n\nST2_CONFIG_PATH = os.path.abspath(os.path.join(ST2_CONFIG_DIRECTORY, \"config\"))\n\nCONFIG_FILE_OPTIONS = {\n    \"general\": {\n        \"base_url\": {\"type\": \"string\", \"default\": None},\n        \"api_version\": {\"type\": \"string\", \"default\": None},\n        \"cacert\": {\"type\": \"string\", \"default\": None},\n        \"silence_ssl_warnings\": {\"type\": \"bool\", \"default\": False},\n        \"silence_schema_output\": {\"type\": \"bool\", \"default\": True},\n    },\n    \"cli\": {\n        \"debug\": {\"type\": \"bool\", \"default\": False},\n        \"cache_token\": {\"type\": \"boolean\", \"default\": True},\n        \"timezone\": {\"type\": \"string\", \"default\": \"UTC\"},\n    },\n    \"credentials\": {\n        \"username\": {\"type\": \"string\", \"default\": None},\n        \"password\": {\"type\": \"string\", \"default\": None},\n        \"api_key\": {\"type\": \"string\", \"default\": None},\n    },\n    \"api\": {\"url\": {\"type\": \"string\", \"default\": None}},\n    \"auth\": {\"url\": {\"type\": \"string\", \"default\": None}},\n    \"stream\": {\"url\": {\"type\": \"string\", \"default\": None}},\n}\n\nCONFIG_DEFAULT_VALUES = {}\n\nfor section, keys in six.iteritems(CONFIG_FILE_OPTIONS):\n    CONFIG_DEFAULT_VALUES[section] = {}\n\n    for key, options in six.iteritems(keys):\n        default_value = options[\"default\"]\n        CONFIG_DEFAULT_VALUES[section][key] = default_value\n\n\nclass CLIConfigParser(object):\n    def __init__(\n        self,\n        config_file_path,\n        validate_config_exists=True,\n        validate_config_permissions=True,\n        log=None,\n    ):\n        if validate_config_exists and not os.path.isfile(config_file_path):\n            raise ValueError('Config file \"%s\" doesn\\'t exist')\n\n        if log is None:\n            log = logging.getLogger(__name__)\n            logging.basicConfig()\n\n        self.config_file_path = config_file_path\n        self.validate_config_permissions = validate_config_permissions\n        self.LOG = log\n\n    def parse(self):\n        \"\"\"\n        Parse the config and return a dict with the parsed values.\n\n        :rtype: ``dict``\n        \"\"\"\n        result = defaultdict(dict)\n\n        if not os.path.isfile(self.config_file_path):\n            # Config doesn't exist, return the default values\n            return CONFIG_DEFAULT_VALUES\n\n        config_dir_path = os.path.dirname(self.config_file_path)\n\n        if self.validate_config_permissions:\n            # Make sure the directory permissions == 0o770\n            if bool(os.stat(config_dir_path).st_mode & 0o7):\n                self.LOG.warn(\n                    \"The StackStorm configuration directory permissions are \"\n                    \"insecure (too permissive): others have access.\"\n                )\n\n            # Make sure the setgid bit is set on the directory\n            if not bool(os.stat(config_dir_path).st_mode & 0o2000):\n                self.LOG.info(\n                    \"The SGID bit is not set on the StackStorm configuration \"\n                    \"directory.\"\n                )\n\n            # Make sure the file permissions == 0o660\n            if bool(os.stat(self.config_file_path).st_mode & 0o7):\n                self.LOG.warn(\n                    \"The StackStorm configuration file permissions are \"\n                    \"insecure: others have access.\"\n                )\n\n        config = ConfigParser()\n        with io.open(self.config_file_path, \"r\", encoding=\"utf8\") as fp:\n            config.readfp(fp)\n\n        for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):\n            for key, options in six.iteritems(keys):\n                key_type = options[\"type\"]\n                key_default_value = options[\"default\"]\n\n                if config.has_option(section, key):\n                    if key_type in [\"str\", \"string\"]:\n                        get_func = config.get\n                    elif key_type in [\"int\", \"integer\"]:\n                        get_func = config.getint\n                    elif key_type in [\"float\"]:\n                        get_func = config.getfloat\n                    elif key_type in [\"bool\", \"boolean\"]:\n                        get_func = config.getboolean\n                    else:\n                        msg = 'Invalid type \"%s\" for option \"%s\"' % (key_type, key)\n                        raise ValueError(msg)\n\n                    value = get_func(section, key)\n                    result[section][key] = value\n                else:\n                    result[section][key] = key_default_value\n\n        return dict(result)\n", "path": "st2client/st2client/config_parser.py"}], "after_files": [{"content": "# Copyright 2020 The StackStorm Authors.\n# Copyright 2019 Extreme Networks, Inc.\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n#\n#     http://www.apache.org/licenses/LICENSE-2.0\n#\n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\n\"\"\"\nModule for parsing CLI config file.\n\"\"\"\n\nfrom __future__ import absolute_import\n\nimport logging\nimport os\n\nfrom collections import defaultdict\n\nimport io\n\nimport six\nfrom six.moves.configparser import ConfigParser\n\n\n__all__ = [\n    \"CLIConfigParser\",\n    \"ST2_CONFIG_DIRECTORY\",\n    \"ST2_CONFIG_PATH\",\n    \"CONFIG_DEFAULT_VALUES\",\n]\n\nST2_CONFIG_DIRECTORY = \"~/.st2\"\nST2_CONFIG_DIRECTORY = os.path.abspath(os.path.expanduser(ST2_CONFIG_DIRECTORY))\n\nST2_CONFIG_PATH = os.path.abspath(os.path.join(ST2_CONFIG_DIRECTORY, \"config\"))\n\nCONFIG_FILE_OPTIONS = {\n    \"general\": {\n        \"base_url\": {\"type\": \"string\", \"default\": None},\n        \"api_version\": {\"type\": \"string\", \"default\": None},\n        \"cacert\": {\"type\": \"string\", \"default\": None},\n        \"silence_ssl_warnings\": {\"type\": \"bool\", \"default\": False},\n        \"silence_schema_output\": {\"type\": \"bool\", \"default\": True},\n    },\n    \"cli\": {\n        \"debug\": {\"type\": \"bool\", \"default\": False},\n        \"cache_token\": {\"type\": \"boolean\", \"default\": True},\n        \"timezone\": {\"type\": \"string\", \"default\": \"UTC\"},\n    },\n    \"credentials\": {\n        \"username\": {\"type\": \"string\", \"default\": None},\n        \"password\": {\"type\": \"string\", \"default\": None},\n        \"api_key\": {\"type\": \"string\", \"default\": None},\n    },\n    \"api\": {\"url\": {\"type\": \"string\", \"default\": None}},\n    \"auth\": {\"url\": {\"type\": \"string\", \"default\": None}},\n    \"stream\": {\"url\": {\"type\": \"string\", \"default\": None}},\n}\n\nCONFIG_DEFAULT_VALUES = {}\n\nfor section, keys in six.iteritems(CONFIG_FILE_OPTIONS):\n    CONFIG_DEFAULT_VALUES[section] = {}\n\n    for key, options in six.iteritems(keys):\n        default_value = options[\"default\"]\n        CONFIG_DEFAULT_VALUES[section][key] = default_value\n\n\nclass CLIConfigParser(object):\n    def __init__(\n        self,\n        config_file_path,\n        validate_config_exists=True,\n        validate_config_permissions=True,\n        log=None,\n    ):\n        if validate_config_exists and not os.path.isfile(config_file_path):\n            raise ValueError('Config file \"%s\" doesn\\'t exist')\n\n        if log is None:\n            log = logging.getLogger(__name__)\n            logging.basicConfig()\n\n        self.config_file_path = config_file_path\n        self.validate_config_permissions = validate_config_permissions\n        self.LOG = log\n\n    def parse(self):\n        \"\"\"\n        Parse the config and return a dict with the parsed values.\n\n        :rtype: ``dict``\n        \"\"\"\n        result = defaultdict(dict)\n\n        if not os.path.isfile(self.config_file_path):\n            # Config doesn't exist, return the default values\n            return CONFIG_DEFAULT_VALUES\n\n        config_dir_path = os.path.dirname(self.config_file_path)\n\n        if self.validate_config_permissions:\n            # Make sure the directory permissions == 0o770\n            if bool(os.stat(config_dir_path).st_mode & 0o7):\n                self.LOG.warn(\n                    \"The StackStorm configuration directory permissions are \"\n                    \"insecure (too permissive): others have access.\"\n                )\n\n            # Make sure the setgid bit is set on the directory\n            if not bool(os.stat(config_dir_path).st_mode & 0o2000):\n                self.LOG.info(\n                    \"The SGID bit is not set on the StackStorm configuration \"\n                    \"directory.\"\n                )\n\n            # Make sure the file permissions == 0o660\n            if bool(os.stat(self.config_file_path).st_mode & 0o7):\n                self.LOG.warn(\n                    \"The StackStorm configuration file permissions are \"\n                    \"insecure: others have access.\"\n                )\n\n        config = ConfigParser()\n        with io.open(self.config_file_path, \"r\", encoding=\"utf8\") as fp:\n            config.readfp(fp)\n\n        for section, keys in six.iteritems(CONFIG_FILE_OPTIONS):\n            for key, options in six.iteritems(keys):\n                key_type = options[\"type\"]\n                key_default_value = options[\"default\"]\n\n                if config.has_option(section, key):\n                    if key_type in [\"str\", \"string\"]:\n                        get_func = config.get\n                    elif key_type in [\"int\", \"integer\"]:\n                        get_func = config.getint\n                    elif key_type in [\"float\"]:\n                        get_func = config.getfloat\n                    elif key_type in [\"bool\", \"boolean\"]:\n                        get_func = config.getboolean\n                    else:\n                        msg = 'Invalid type \"%s\" for option \"%s\"' % (key_type, key)\n                        raise ValueError(msg)\n\n                    value = get_func(section, key, raw=True)\n                    result[section][key] = value\n                else:\n                    result[section][key] = key_default_value\n\n        return dict(result)\n", "path": "st2client/st2client/config_parser.py"}]}