├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md └── fs2consulkv.py /.gitignore: -------------------------------------------------------------------------------- 1 | test.sh 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 1.0.6 2 | 3 | * added sleep/chunk size options: https://github.com/bitsofinfo/files-to-consul-kv/pull/5 4 | 5 | # 1.0.5 6 | 7 | * [#4 fix possible targetkv name mangling](https://github.com/bitsofinfo/files-to-consul-kv/pull/4) 8 | * add license 9 | # 1.0.4 10 | 11 | exit non-zero on KV push failures 12 | 13 | # 1.0.3 14 | 15 | added `--consul-kv-root-file` 16 | 17 | # 1.0.2 18 | 19 | added `--consul-acl-token-file` 20 | 21 | # 1.0.1 22 | 23 | fix trimming kv root whitespace 24 | 25 | # 1.0.0 26 | 27 | initial release 28 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7.6-alpine 2 | 3 | ARG GIT_TAG=master 4 | 5 | RUN echo GIT_TAG=${GIT_TAG} 6 | 7 | # install under /usr/local/bin 8 | RUN apk update ; \ 9 | apk upgrade ; \ 10 | apk add git ; \ 11 | echo $PATH ; \ 12 | git clone --branch ${GIT_TAG} https://github.com/bitsofinfo/files-to-consul-kv.git ; \ 13 | cd /files-to-consul-kv; git status; rm -rf .git; cd / ; \ 14 | cp /files-to-consul-kv/*.py /usr/local/bin/ ; \ 15 | rm -rf /files-to-consul-kv ; \ 16 | apk del git ; \ 17 | ls -al /usr/local/bin ; \ 18 | chmod +x /usr/local/bin/*.py ; \ 19 | rm -rf /var/cache/apk/* 20 | 21 | # required modules 22 | RUN pip install --upgrade pip python-dateutil requests 23 | 24 | ENV PATH="/usr/local/bin/;$PATH" 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # files-to-consul-kv 2 | 3 | Simple utility for bulk loading sets of [Consul key-value](https://www.consul.io/docs/agent/kv.html) entries via the [transactions API](https://www.consul.io/api/txn.html) where the source of those values exist on disk in a directory structure. 4 | 5 | For example given a simple directory structure like: 6 | ``` 7 | cd mykvs/ 8 | 9 | $ find . -print 10 | . 11 | ./sub 12 | ./sub/key2 13 | ./key1 14 | 15 | $ cat key1 16 | val1 17 | 18 | $ cat sub/key2 19 | val2 20 | ``` 21 | 22 | You could use `fs2consulkv.py` to set all these in Consul under some root path: 23 | ``` 24 | ./fs2consulkv.py \ 25 | --fs-kv-path ./mykvs \ 26 | --consul-url https://[consul-fqdn][:port] \ 27 | --consul-acl-token xxxxxxx \ 28 | --consul-data-center optional-dc \ 29 | --consul-kv-root some/root/path/ 30 | ``` 31 | 32 | Would result in your KVs in consul at: 33 | ``` 34 | https://[consul-fqdn][:port]/ui/mydc/kv/some/root/path/key1 = val1 35 | https://[consul-fqdn][:port]/ui/mydc/kv/some/root/path/sub/key2 = val2 36 | ``` 37 | 38 | ## Docker 39 | 40 | Run via Docker: 41 | https://hub.docker.com/r/bitsofinfo/files-to-consul-kv 42 | 43 | ``` 44 | docker run -i -v `pwd`/mykvs:/kvsource \ 45 | bitsofinfo/files-to-consul-kv fs2consulkv.py \ 46 | --fs-kv-path /kvsource \ 47 | --consul-url https://[consul-fqdn][:port] \ 48 | --consul-acl-token xxxxxxx \ 49 | --consul-data-center optional-dc \ 50 | --consul-kv-root some/root/path/ 51 | ``` 52 | 53 | ## Usage 54 | 55 | ``` 56 | $ ./fs2consulkv.py -h 57 | 58 | usage: fs2consulkv.py [-h] [-p FS_KV_PATH] [-k CONSUL_KV_ROOT] 59 | [-z CONSUL_KV_ROOT_FILE] [-c CONSUL_URL] 60 | [-t CONSUL_ACL_TOKEN] [-f CONSUL_ACL_TOKEN_FILE] 61 | [-d CONSUL_DATA_CENTER] [-x] [-n] [-l LOG_LEVEL] 62 | [-b LOG_FILE] 63 | 64 | optional arguments: 65 | -h, --help show this help message and exit 66 | -p FS_KV_PATH, --fs-kv-path FS_KV_PATH 67 | Full or relative path to filesystem directory 68 | containing the KV structure to send to consul 69 | (default: ./) 70 | -k CONSUL_KV_ROOT, --consul-kv-root CONSUL_KV_ROOT 71 | Root path in Consul KV by which all new keys will be 72 | set, required. i.e. 'some/root/path' (default: None) 73 | -z CONSUL_KV_ROOT_FILE, --consul-kv-root-file CONSUL_KV_ROOT_FILE 74 | Path to a file that contains the consul-kv-root 75 | argument value, optional, can be used instead of 76 | --consul-kv-root i.e. /path/to/consul-kv-root.txt 77 | where the file contents contains the value 78 | 'some/root/path' (default: None) 79 | -c CONSUL_URL, --consul-url CONSUL_URL 80 | Consul url, required. i.e. http[s]://[fqdn][:port] 81 | (default: None) 82 | -t CONSUL_ACL_TOKEN, --consul-acl-token CONSUL_ACL_TOKEN 83 | Consul acl token, required (default: None) 84 | -f CONSUL_ACL_TOKEN_FILE, --consul-acl-token-file CONSUL_ACL_TOKEN_FILE 85 | Consul acl token file, path to a file that contains 86 | the token value, required (default: None) 87 | -d CONSUL_DATA_CENTER, --consul-data-center CONSUL_DATA_CENTER 88 | Consul data-center, optional. (default: None) 89 | -x, --skip-prompt Skip confirmation and prompt (default: False) 90 | -n, --retain-trailing-newlines 91 | Retain trailing newline chars (\n) in values files and 92 | do not strip them. Default behavior is to strip them 93 | (default: False) 94 | -s SLEEP_DELAY, --sleep-delay SLEEP_DELAY 95 | Delay [in seconds] in kv upload loop, to avoid 96 | overwhelming the consul server. Default behavior is 97 | 0.000 seconds (default: 0) 98 | -u CHUNK_SIZE, --chunk-size CHUNK_SIZE 99 | Number of KV pairs uploaded at once. Default is 64, 100 | the maximum allowed. (default: 64) 101 | -l LOG_LEVEL, --log-level LOG_LEVEL 102 | log level, DEBUG, INFO, etc (default: DEBUG) 103 | -b LOG_FILE, --log-file LOG_FILE 104 | Path to log file; default None = STDOUT (default: 105 | None) 106 | ``` 107 | -------------------------------------------------------------------------------- /fs2consulkv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # https://www.consul.io/api/txn.html 4 | # https://github.com/hashicorp/consul/issues/7278 5 | 6 | import os 7 | import base64 8 | import requests 9 | import json 10 | import logging 11 | import sys 12 | import time 13 | import argparse 14 | 15 | 16 | # Yield successive n-sized 17 | # chunks from l. 18 | # https://www.geeksforgeeks.org/break-list-chunks-size-n-python/ 19 | def divide_chunks(l, n): 20 | 21 | # looping till length l 22 | for i in range(0, len(l), n): 23 | yield l[i:i + n] 24 | 25 | 26 | def main(): 27 | parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) 28 | 29 | parser.add_argument('-p', '--fs-kv-path', dest='fs_kv_path', default="./", \ 30 | help="Full or relative path to filesystem directory containing the KV structure to send to consul") 31 | 32 | parser.add_argument('-k', '--consul-kv-root', dest='consul_kv_root', default=None, \ 33 | help="Root path in Consul KV by which all new keys will be set, required. i.e. 'some/root/path'") 34 | 35 | parser.add_argument('-z', '--consul-kv-root-file', dest='consul_kv_root_file', default=None, \ 36 | help="Path to a file that contains the consul-kv-root argument value, optional, can be used instead of --consul-kv-root " + \ 37 | "i.e. /path/to/consul-kv-root.txt where the file contents contains the value 'some/root/path'") 38 | 39 | parser.add_argument('-c', '--consul-url', dest='consul_url', default=None, \ 40 | help="Consul url, required. i.e. http[s]://[fqdn][:port]") 41 | 42 | parser.add_argument('-t', '--consul-acl-token', dest='consul_acl_token', default=None, \ 43 | help="Consul acl token, required") 44 | 45 | parser.add_argument('-f', '--consul-acl-token-file', dest='consul_acl_token_file', default=None, \ 46 | help="Consul acl token file, path to a file that contains the token value, required") 47 | 48 | parser.add_argument('-d', '--consul-data-center', dest='consul_data_center', default=None, \ 49 | help="Consul data-center, optional.") 50 | 51 | parser.add_argument('-x', '--skip-prompt', action='store_true', default=False, \ 52 | help="Skip confirmation and prompt") 53 | 54 | parser.add_argument('-n', '--retain-trailing-newlines', action='store_true', default=False, \ 55 | help="Retain trailing newline chars (\\n) in values files and do not strip them. Default behavior is to strip them") 56 | 57 | parser.add_argument('-s', '--sleep-delay', dest='sleep_delay', default=0, \ 58 | help="Delay [in seconds] in kv upload loop, to avoid overwhelming the consul server. Default behavior is 0.000 seconds") 59 | parser.add_argument('-u', '--chunk-size', dest='chunk_size', default=64, \ 60 | help="Number of KV pairs uploaded at once. Default is 64, the maximum allowed.") 61 | 62 | parser.add_argument('-l', '--log-level', dest='log_level', default="DEBUG", \ 63 | help="log level, DEBUG, INFO, etc") 64 | parser.add_argument('-b', '--log-file', dest='log_file', default=None, \ 65 | help="Path to log file; default None = STDOUT") 66 | 67 | args = parser.parse_args() 68 | 69 | dump_help = False 70 | 71 | if not args.consul_kv_root and not args.consul_kv_root_file: 72 | dump_help = True 73 | 74 | if dump_help: 75 | parser.print_help() 76 | sys.exit(1) 77 | 78 | logging.basicConfig(level=logging.getLevelName(args.log_level), 79 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', 80 | filename=args.log_file,filemode='w') 81 | logging.Formatter.converter = time.gmtime 82 | 83 | url = "{}/v1/txn".format(args.consul_url) 84 | 85 | if args.consul_data_center is not None: 86 | url += "?dc={}".format(args.consul_data_center) 87 | 88 | consul_acl_token = args.consul_acl_token 89 | if args.consul_acl_token_file: 90 | with open (args.consul_acl_token_file, "r") as tokenfile: 91 | value = tokenfile.read() 92 | if value and value.strip() == '': 93 | logging.error("--consul-acl-token-file {} contains nothing!".format(args.consul_acl_token_file)) 94 | sys.exit(1) 95 | else: 96 | consul_acl_token = value 97 | 98 | if not consul_acl_token or consul_acl_token.strip() == '': 99 | logging.error("--consul-acl-token-file or --consul-acl-token is required!") 100 | sys.exit(1) 101 | 102 | 103 | consul_kv_root = args.consul_kv_root 104 | if args.consul_kv_root_file: 105 | with open (args.consul_kv_root_file, "r") as f: 106 | consul_kv_root = f.read() 107 | logging.debug("sourced --consul-kv-root-file at {} with value {}".format(args.consul_kv_root_file,consul_kv_root)) 108 | 109 | 110 | headers = { 111 | 'Content-Type': "application/json", 112 | 'X-Consul-Token': consul_acl_token, 113 | 'User-Agent': "github.com/bitsofinfo/files-to-consul-kv", 114 | 'Accept': "*/*", 115 | 'Cache-Control': "no-cache" 116 | } 117 | 118 | try: 119 | kvs = [] 120 | 121 | if consul_kv_root.strip().endswith('/'): 122 | consul_kv_root = consul_kv_root.strip()[:-1] 123 | 124 | for root, dirs, files in os.walk(args.fs_kv_path): 125 | 126 | for name in files: 127 | filepath = os.path.join(root, name) 128 | 129 | targetkv = filepath.replace(args.fs_kv_path, "", 1) 130 | 131 | with open (filepath, "r") as kvfile: 132 | value = kvfile.read() 133 | 134 | if not args.retain_trailing_newlines: 135 | if value.endswith('\n'): 136 | value = value[:-1] 137 | 138 | if targetkv.startswith('/'): 139 | targetkv = targetkv[1:] 140 | 141 | kvs.append({ 142 | "KV": { 143 | "Verb": "set", 144 | "Key": "{}/{}".format(consul_kv_root,targetkv), 145 | "Value": "{}".format(base64.b64encode(value.encode("utf-8")).decode("utf-8")) 146 | } 147 | }) 148 | 149 | if not args.skip_prompt: 150 | print(json.dumps(kvs,indent=2)) 151 | proceed = input("\n\nYou are about to 'set' the above consul KV paths:\n... that were consumed from {}\n... will be PUT against {}\n... and set relative from {}/\n\nAre you sure?: (y|n):".format(args.fs_kv_path,url,consul_kv_root)).strip() 152 | if proceed.lower() != 'y': 153 | logging.info("Exiting, confirmation prompt input was: " + proceed) 154 | sys.exit(1) 155 | 156 | 157 | # we can only max send 64 per request... 158 | # https://github.com/hashicorp/consul/issues/7278 159 | kv_chunks = list(divide_chunks(kvs, int(args.chunk_size))) 160 | 161 | logging.info("Number of kvs totals: {}, this has to be split up " \ 162 | "into {} {} kv chunks: https://github.com/hashicorp/consul/issues/7278".format(len(kvs),len(kv_chunks),int(args.chunk_size))) 163 | 164 | exit_with_exit_code = 0 165 | 166 | for kvchunk in kv_chunks: 167 | 168 | logging.info("PUTing chunk with {} keys @ {}".format(len(kvchunk),url)) 169 | 170 | response = requests.request("PUT", url, data=json.dumps(kvchunk), headers=headers) 171 | 172 | time.sleep(float(args.sleep_delay)) 173 | 174 | if response.status_code == 200: 175 | logging.debug("KVs 'set' OK: {}".format(response.content)) 176 | else: 177 | logging.error("KVs 'set' http response code: {} FAILED: {} ".format(response.status_code,json.loads(response.content))) 178 | exit_with_exit_code = 1 179 | 180 | sys.exit(exit_with_exit_code) 181 | 182 | except Exception as e: 183 | logging.exception("error PUTing consul KVs via /txn: POST-DATA={} ERROR={} " \ 184 | .format(kvs,str(sys.exc_info()[:2]))) 185 | 186 | if __name__ == '__main__': 187 | main() 188 | --------------------------------------------------------------------------------