Skip to content

Instantly share code, notes, and snippets.

@vlinevych
Last active July 18, 2023 03:54
Show Gist options
  • Save vlinevych/7174d7d491004bbcb83d99bb7f91b70f to your computer and use it in GitHub Desktop.
Save vlinevych/7174d7d491004bbcb83d99bb7f91b70f to your computer and use it in GitHub Desktop.
Import/export tool for consul key-value storage
#!/usr/bin/env python3
"""
Import/Export tool for Consul Key-Value storage.
If your server has ACL enabled,
be sure to export CONSUL_HTTP_TOKEN env variable or use -t to provide a token
Usage:
Export KV storage decoding values
./consul-kv-tool.py export > file.json
Export a certain path, decoding values
./consul-kv-tool.py export --path "vault/staging" > file.json
Dump KV storage "as is", with base64 encoded values
./consul-kv-tool.py dump > file.json
Decode base64 in existing dump/backup:
./consul-kv-tool.py decode-dump -f file.json
Import KV from file
./consul-kv-tool.py import -f file.json
Import some part of a file
./consul-kv-tool.py import -f file.json --path "vault/staging"
"""
import json
import requests
import base64
import sys
import argparse
import os
def download(server, token, consul_path=""):
url = server + "/v1/kv/" + consul_path
params = {"token": token, "recurse": 1}
try:
response = requests.get(url, params=params)
if response.status_code != 200:
raise Exception("HTTP code %s on GET %s" % (response.status_code, url))
return response.json()
except Exception as e:
print("ERROR during download:", e, file=sys.stderr)
exit(1)
def process(downloaded_data):
#
# Some typical kv data probems are fixed here:
#
# - sometimes a 'directory' appears as a key with trailing "/" - we should just skip these keys,
# since 'directory' will be created once we put the keys in it.
#
# - sometimes a non-ascii chars like "eé" appear in values instrad of Null.
# These values are replaced with None.
#
result_obj = []
for obj in downloaded_data:
try:
# Skip 'directory creation' keys
if obj['Key'][-1] == "/":
continue
# Decode only non-empty values
if obj['Value']:
obj['Value'] = base64.b64decode(obj['Value'])
obj['Value'] = str(obj['Value'].decode('utf-8'))
result_obj.append(obj)
# Replace values with bad chars with None (aka null) and continue
except UnicodeDecodeError as e:
print("WARNING: bad chars in", obj['Key'], ": ", e, file=sys.stderr)
obj['Value'] = None
result_obj.append(obj)
except Exception as e:
print ("ERROR during processing:", e, file=sys.stderr)
exit(1)
return result_obj
def read_from_file(file_path, consul_path=None):
try:
f = open(file_path, "r")
data = json.load(f)
f.close()
if consul_path:
result_obj = []
for obj in data:
if consul_path in obj['Key']:
result_obj.append(obj)
data = result_obj
except Exception as e:
print ("ERROR reading file:", e, file=sys.stderr)
exit(1)
return data
def output(data):
print(json.dumps(data, sort_keys=True, indent=4, separators=(',', ': ')))
def upload(server, token, data):
params = {"token": token}
for obj in data:
try:
payload = obj['Value']
url = server + "/v1/kv/" + obj['Key']
response = requests.put(url, data=payload, params=params)
if response.status_code != 200:
raise Exception("HTTP code %s on PUT %s" % (response.status_code, url))
except Exception as e:
print ("ERROR uploading:", e, file=sys.stderr)
exit(1)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("action", type=str, choices=["export", "import", "dump", "decode-dump"], action="store", help="export or import")
parser.add_argument("--file", "-f", type=str, help="json file to read", metavar='')
parser.add_argument("--path", "-p", type=str, help="Consul kv path: 'vault/master/something/key'", default="", metavar='')
parser.add_argument("--server", "-s", type=str, help="Consul server, proto://url:port", default="http://localhost:8500", metavar='')
parser.add_argument("--token", "-t", type=str, help="Consul ACL token", metavar='')
args = parser.parse_args()
if args.token:
token = args.token
elif 'CONSUL_HTTP_TOKEN' in os.environ:
token = os.environ['CONSUL_HTTP_TOKEN']
else:
print("Warning: no Consul token provided.", file=sys.stderr)
if args.action in ['import', 'decode-dump'] and not args.file:
print ("Provide a filename using -f parameter.", file=sys.stderr)
exit(1)
if args.action == 'export':
data = download(args.server, token, args.path)
data = process(data)
output(data)
elif args.action == 'import':
data = read_from_file(args.file, args.path)
upload(args.server, token, data)
elif args.action == 'dump':
data = download(args.server, token, args.path)
output(data)
elif args.action == 'decode-dump':
data = read_from_file(args.file, args.path)
data = process(data)
output(data)
@vlinevych
Copy link
Author

The script only works with key-value API. ACL is not supported.

Please note that recent consul releases have both kv import/export and ACL command-line commands:
https://www.consul.io/docs/commands/kv.html
https://www.consul.io/docs/commands/acl.html

.. so it does not make much sense to support this script in future.

Copy link

ghost commented Feb 26, 2020

Thank you for the information and your help!!

Copy link

ghost commented Mar 9, 2020

Hi @vlinevich

I am getting some error while importing the KV as one of my value contains a special character ( ' ) and that's creating the issue. Can you please help to fix the issue?

Error:
ERROR uploading: 'latin-1' codec can't encode character '\u2019' in position 5735: Body ('â') is not valid Latin-1. Use body.encode('utf-8') if you want to send it encoded in UTF-8.

@vlinevych
Copy link
Author

Hi,
Sorry for the late response.
The script expects your input file to be saved in UTF-8 encoding. Look up how to save it this way in your editor.

Copy link

ghost commented Mar 12, 2020

Hi @vlinevich
Thanks for your response. I managed to do that with following change into upload function.
payload = obj['Value'].encode('utf-8')

@ankujuniyal
Copy link

@vlinevych This helps lots, thanks

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment