ansible-vars 1.0.6__py3-none-any.whl → 1.0.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ansible_vars/cli.py +165 -45
- ansible_vars/errors.py +8 -0
- ansible_vars/util.py +3 -3
- ansible_vars/vault.py +27 -12
- ansible_vars/vault_crypt.py +13 -9
- {ansible_vars-1.0.6.dist-info → ansible_vars-1.0.8.dist-info}/METADATA +15 -6
- ansible_vars-1.0.8.dist-info/RECORD +12 -0
- ansible_vars-1.0.6.dist-info/RECORD +0 -12
- {ansible_vars-1.0.6.dist-info → ansible_vars-1.0.8.dist-info}/WHEEL +0 -0
- {ansible_vars-1.0.6.dist-info → ansible_vars-1.0.8.dist-info}/entry_points.txt +0 -0
- {ansible_vars-1.0.6.dist-info → ansible_vars-1.0.8.dist-info}/licenses/LICENSE +0 -0
ansible_vars/cli.py
CHANGED
@@ -39,7 +39,7 @@ from .vault import VaultFile, EncryptedVar, ProtoEncryptedVar
|
|
39
39
|
from .vault_crypt import VaultKey, VaultKeyring
|
40
40
|
from .util import DiffFileLogger, VaultDaemon
|
41
41
|
from .constants import Unset, MatchLocation, SENTINEL_KEY
|
42
|
-
from .errors import YAMLFormatError
|
42
|
+
from .errors import YAMLFormatError, UnsupportedGenericFileOperation
|
43
43
|
|
44
44
|
## CLI argument parsing
|
45
45
|
|
@@ -84,6 +84,10 @@ Specify vault keys to load for en-/decryption. Not required for vars files with
|
|
84
84
|
A key is a combination of an identifier (can be a vault ID or anything else, ideally unique) and a passphrase.
|
85
85
|
By default, available keys are auto-detected if your current directory contains an Ansible config and appended to the ones you supplied.
|
86
86
|
If no explicit encryption key is specified, the first supplied/available key is used.
|
87
|
+
|
88
|
+
[!] When editing a hybrid vault file, changing even a single variable will change all variables' ciphers, as salts are generated randomly.
|
89
|
+
You can prevent this by passing a fixed salt via `-S <salt>`, which will result in any plaintext always resolving to the same cipher.
|
90
|
+
For Ansible's AES-256, a length of at least 32 characters is recommended.
|
87
91
|
''',
|
88
92
|
'log_args': '''
|
89
93
|
Log a diff of any vault changes performed with this program to an encrypted or plain logfile, creating it if necessary.
|
@@ -198,6 +202,7 @@ DEFAULT_EDITOR: str = os.environ.get('EDITOR', 'notepad.exe' if os.name == 'nt'
|
|
198
202
|
DEFAULT_COLOR_MODE: str = os.environ.get('AV_COLOR_MODE', '256' if sys.stdout.isatty() else 'none')
|
199
203
|
DEFAULT_TEMP_DIR: str = os.environ.get('AV_TEMP_DIR', gettempdir())
|
200
204
|
DEFAULT_CREATE_PLAIN: bool = os.environ.get('AV_CREATE_PLAIN', 'no').lower() in [ 'yes', 'y', 'true', 't', '1' ]
|
205
|
+
DEFAULT_SALT: str | None = os.environ.get('AV_SALT', None)
|
201
206
|
|
202
207
|
args: ArgumentParser = ArgumentParser(
|
203
208
|
prog = 'ansible-vars',
|
@@ -243,6 +248,10 @@ key_args.add_argument(
|
|
243
248
|
)
|
244
249
|
key_args.add_argument('--no-detect-keys', '-D', action='store_false', dest='detect_keys', help='disable automatic key detection')
|
245
250
|
key_args.add_argument('--encryption-key', '-K', type=str, metavar='<identifier>', help='which of the loaded keys to use for encryption')
|
251
|
+
key_args.add_argument(
|
252
|
+
'--fixed-salt', '-S', type=str, metavar='<salt>', default=DEFAULT_SALT,
|
253
|
+
help='a fixed salt to use for encryption (should be 32+ chars!)'
|
254
|
+
)
|
246
255
|
|
247
256
|
log_args = args.add_argument_group('logging vault changes', description=HELP['log_args'])
|
248
257
|
log_mutex = log_args.add_mutually_exclusive_group()
|
@@ -253,11 +262,17 @@ log_args.add_argument('--logging-key', '-Q', type=str, metavar='<identifier>', h
|
|
253
262
|
# Commands
|
254
263
|
commands = args.add_subparsers(dest='command', metavar='<command>', required=True)
|
255
264
|
|
256
|
-
cmd_keyring = commands.add_parser(
|
265
|
+
cmd_keyring = commands.add_parser(
|
266
|
+
'keyring', help='show available vault keys and their passphrases', description=HELP['cmd_keyring'],
|
267
|
+
formatter_class=RawDescriptionHelpFormatter
|
268
|
+
)
|
257
269
|
cmd_keyring.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the vault keys as JSON and nothing else')
|
258
270
|
cmd_keyring.add_argument('--keys-only', '-o', action='store_false', dest='show_passphrases', help='show only the vault keys, not the passphrases')
|
259
271
|
|
260
|
-
cmd_create = commands.add_parser(
|
272
|
+
cmd_create = commands.add_parser(
|
273
|
+
'create', help=f"create a new vault ({ 'hybrid/plain' if DEFAULT_CREATE_PLAIN else 'fully encrypted' } by default)",
|
274
|
+
description=HELP['cmd_create'], formatter_class=RawDescriptionHelpFormatter
|
275
|
+
)
|
261
276
|
cmd_create.add_argument('vault_path', type=str, metavar='<vault path>', help='path to create a new vault at') \
|
262
277
|
.completer = _prefixed_path_completer # type: ignore
|
263
278
|
# Invert flag if the user wants plain mode by default
|
@@ -266,50 +281,74 @@ if DEFAULT_CREATE_PLAIN:
|
|
266
281
|
else:
|
267
282
|
cmd_create.add_argument('--plain', '-p', action='store_false', dest='encrypt_vault', help='create without full file encryption')
|
268
283
|
cmd_create.add_argument('--make-parents', '-m', action='store_true', help='create all directories in the given path')
|
269
|
-
|
270
|
-
|
271
|
-
|
284
|
+
create_mutex = cmd_create.add_mutually_exclusive_group()
|
285
|
+
create_mutex.add_argument('--no-edit', '-n', action='store_false', dest='open_edit_mode', help='just create the file, don\'t open it for editing')
|
286
|
+
create_mutex.add_argument(
|
272
287
|
'--edit-command', '-e', type=str, default=DEFAULT_EDITOR, help=f"editor command to use (runs as <command> <some path>) (default: { DEFAULT_EDITOR })"
|
273
288
|
)
|
274
289
|
|
275
|
-
cmd_edit = commands.add_parser(
|
290
|
+
cmd_edit = commands.add_parser(
|
291
|
+
'edit', help='edit a vault', description=HELP['cmd_edit'], formatter_class=RawDescriptionHelpFormatter
|
292
|
+
)
|
276
293
|
cmd_edit.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to edit') \
|
277
294
|
.completer = _prefixed_path_completer # type: ignore
|
278
295
|
cmd_edit.add_argument(
|
279
296
|
'--edit-command', '-e', type=str, default=DEFAULT_EDITOR, help=f"editor command to use (runs as <command> <some path>) (default: { DEFAULT_EDITOR })"
|
280
297
|
)
|
281
298
|
|
282
|
-
cmd_view = commands.add_parser(
|
299
|
+
cmd_view = commands.add_parser(
|
300
|
+
'view', help='show the decrypted contents of a vault', formatter_class=RawDescriptionHelpFormatter
|
301
|
+
)
|
283
302
|
cmd_view.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to dump') \
|
284
303
|
.completer = _prefixed_path_completer # type: ignore
|
285
304
|
cmd_view.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the vault data as JSON and nothing else')
|
286
305
|
|
287
|
-
cmd_info = commands.add_parser(
|
306
|
+
cmd_info = commands.add_parser(
|
307
|
+
'info', help='show information about a vault\'s variables', description=HELP['cmd_info'],
|
308
|
+
formatter_class=RawDescriptionHelpFormatter
|
309
|
+
)
|
288
310
|
cmd_info.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to analyze') \
|
289
311
|
.completer = _prefixed_path_completer # type: ignore
|
290
312
|
cmd_info.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the information as JSON and nothing else')
|
291
313
|
|
292
|
-
cmd_encrypt = commands.add_parser(
|
314
|
+
cmd_encrypt = commands.add_parser(
|
315
|
+
'encrypt', help='encrypt a file in-place or a string with the encryption key', description=HELP['cmd_encrypt'],
|
316
|
+
formatter_class=RawDescriptionHelpFormatter
|
317
|
+
)
|
293
318
|
cmd_encrypt.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
294
319
|
cmd_encrypt.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to encrypt') \
|
295
320
|
.completer = _prefixed_path_completer # type: ignore
|
321
|
+
cmd_encrypt.add_argument('--quiet', '-q', action='store_true', help='only output the encrypted value (ignored in file mode)')
|
296
322
|
|
297
|
-
cmd_decrypt = commands.add_parser(
|
323
|
+
cmd_decrypt = commands.add_parser(
|
324
|
+
'decrypt', help='decrypt a file in-place or a string', description=HELP['cmd_decrypt'],
|
325
|
+
formatter_class=RawDescriptionHelpFormatter
|
326
|
+
)
|
298
327
|
cmd_decrypt.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
299
328
|
cmd_decrypt.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to decrypt') \
|
300
329
|
.completer = _prefixed_path_completer # type: ignore
|
330
|
+
cmd_decrypt.add_argument('--quiet', '-q', action='store_true', help='only output the decrypted value (ignored in file mode)')
|
301
331
|
|
302
|
-
cmd_is_enc = commands.add_parser(
|
332
|
+
cmd_is_enc = commands.add_parser(
|
333
|
+
'is-encrypted', help='check if a file or string is vault-encrypted', description=HELP['cmd_is_enc'],
|
334
|
+
formatter_class=RawDescriptionHelpFormatter
|
335
|
+
)
|
303
336
|
cmd_is_enc.add_argument('target_type', type=str, choices=['file', 'string'], help='select if target is a file path or a string')
|
304
337
|
cmd_is_enc.add_argument('target', type=str, metavar='<vault path | string>', help='path of vault or string value to check') \
|
305
338
|
.completer = _prefixed_path_completer # type: ignore
|
306
339
|
cmd_is_enc.add_argument('--quiet', '-q', action='store_true', help='no output, only set the rc to 0 if encrypted or 100 if unencrypted')
|
307
340
|
|
308
|
-
cmd_convert = commands.add_parser(
|
341
|
+
cmd_convert = commands.add_parser(
|
342
|
+
'convert', help='switch vault between outer (file) and inner (vars) encryption', description=HELP['cmd_convert'],
|
343
|
+
formatter_class=RawDescriptionHelpFormatter
|
344
|
+
)
|
309
345
|
cmd_convert.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to convert') \
|
310
346
|
.completer = _prefixed_path_completer # type: ignore
|
311
347
|
|
312
|
-
cmd_grep = commands.add_parser(
|
348
|
+
cmd_grep = commands.add_parser(
|
349
|
+
'grep', help='search a file or folder for a pattern', description=HELP['cmd_grep'],
|
350
|
+
formatter_class=RawDescriptionHelpFormatter
|
351
|
+
)
|
313
352
|
cmd_grep.add_argument('query', type=str, metavar='<pattern>', help='regex query to match with targets')
|
314
353
|
cmd_grep.add_argument('targets', type=str, nargs='+', metavar='[<target> ...]', help='file(s) or folder(s) to search recursively') \
|
315
354
|
.completer = _prefixed_path_completer # type: ignore
|
@@ -324,21 +363,30 @@ grep_mutex_type.add_argument('--multiline', '-m', action='store_true', help='mak
|
|
324
363
|
cmd_grep.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the matches as JSON and nothing else')
|
325
364
|
cmd_grep.add_argument('--quiet', '-q', action='store_true', help='no output, only set the rc to 0 if any matches found or 100 if none found')
|
326
365
|
|
327
|
-
cmd_diff = commands.add_parser(
|
366
|
+
cmd_diff = commands.add_parser(
|
367
|
+
'diff', help='show line differences between two vaults', description=HELP['cmd_diff'],
|
368
|
+
formatter_class=RawDescriptionHelpFormatter
|
369
|
+
)
|
328
370
|
cmd_diff.add_argument('old_vault', type=str, metavar='<old vault vault path>', help='path of "old"/base vault') \
|
329
371
|
.completer = _prefixed_path_completer # type: ignore
|
330
372
|
cmd_diff.add_argument('new_vault', type=str, metavar='<new vault vault path>', help='path of "new"/changed vault') \
|
331
373
|
.completer = _prefixed_path_completer # type: ignore
|
332
374
|
cmd_diff.add_argument('--context-lines', '-c', type=int, metavar='<amount>', default=3, help='show <amount> lines of context around changed lines (default: 3)')
|
333
375
|
|
334
|
-
cmd_changes = commands.add_parser(
|
376
|
+
cmd_changes = commands.add_parser(
|
377
|
+
'changes', help='show var changes between vaults', description=HELP['cmd_changes'],
|
378
|
+
formatter_class=RawDescriptionHelpFormatter
|
379
|
+
)
|
335
380
|
cmd_changes.add_argument('old_vault', type=str, metavar='<old vault vault path>', help='path of "old"/base vault') \
|
336
381
|
.completer = _prefixed_path_completer # type: ignore
|
337
382
|
cmd_changes.add_argument('new_vault', type=str, metavar='<new vault vault path>', help='path of "new"/changed vault') \
|
338
383
|
.completer = _prefixed_path_completer # type: ignore
|
339
384
|
cmd_changes.add_argument('--json', '-j', action='store_true', dest='as_json', help='print added/changed/removed/decrypted vars as JSON and nothing else')
|
340
385
|
|
341
|
-
cmd_daemon = commands.add_parser(
|
386
|
+
cmd_daemon = commands.add_parser(
|
387
|
+
'file-daemon', help='sync decrypted vault copies into a folder', description=HELP['cmd_daemon'],
|
388
|
+
formatter_class=RawDescriptionHelpFormatter
|
389
|
+
)
|
342
390
|
cmd_daemon.add_argument('target_root', type=str, metavar='<target root path>', help='root folder the decrypted files and folders should be synced into (non-existent or empty)') \
|
343
391
|
.completer = _prefixed_path_completer # type: ignore
|
344
392
|
# This arg can be repeated (results in [ [source, rel_target], ... ])
|
@@ -350,7 +398,10 @@ cmd_daemon.add_argument('--no-recurse', '-n', action='store_false', dest='recurs
|
|
350
398
|
cmd_daemon.add_argument('--no-default-dirs', '-N', action='store_false', dest='include_default_dirs', help='don\'t include default sync sources')
|
351
399
|
cmd_daemon.add_argument('--force', '-f', action='store_true', help='if the target root already exists and is not empty, delete its contents')
|
352
400
|
|
353
|
-
cmd_get = commands.add_parser(
|
401
|
+
cmd_get = commands.add_parser(
|
402
|
+
'get', help='get a key\'s (decrypted) value if it exists', description=HELP['cmd_get'],
|
403
|
+
formatter_class=RawDescriptionHelpFormatter
|
404
|
+
)
|
354
405
|
cmd_get.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to get value from') \
|
355
406
|
.completer = _prefixed_path_completer # type: ignore
|
356
407
|
cmd_get.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
@@ -359,14 +410,20 @@ get_mutex_format = cmd_get.add_mutually_exclusive_group()
|
|
359
410
|
get_mutex_format.add_argument('--quiet', '-q', action='store_true', help='only output the raw YAML value or set the rc to 100 if the key doesn\'t exist')
|
360
411
|
get_mutex_format.add_argument('--json', '-j', action='store_true', dest='as_json', help='print the value as JSON or set the rc to 100 if the key doesn\'t exist')
|
361
412
|
|
362
|
-
cmd_set = commands.add_parser(
|
413
|
+
cmd_set = commands.add_parser(
|
414
|
+
'set', help='update a key\'s value or add a new key (experimental!)', description=HELP['cmd_set'],
|
415
|
+
formatter_class=RawDescriptionHelpFormatter
|
416
|
+
)
|
363
417
|
cmd_set.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to set value in') \
|
364
418
|
.completer = _prefixed_path_completer # type: ignore
|
365
419
|
cmd_set.add_argument('value', type=str, metavar='<value>', help='value to set (will be loaded as YAML)')
|
366
420
|
cmd_set.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
367
421
|
cmd_set.add_argument('--encrypt', '-e', action='store_true', dest='encrypt_value', help='encrypt the value if it is\'t encrypted yet')
|
368
422
|
|
369
|
-
cmd_del = commands.add_parser(
|
423
|
+
cmd_del = commands.add_parser(
|
424
|
+
'del', help='delete a key and its value if they exist (experimental!)', description=HELP['cmd_del'],
|
425
|
+
formatter_class=RawDescriptionHelpFormatter
|
426
|
+
)
|
370
427
|
cmd_del.add_argument('vault_path', type=str, metavar='<vault path>', help='path of vault to delete key from') \
|
371
428
|
.completer = _prefixed_path_completer # type: ignore
|
372
429
|
cmd_del.add_argument('key_segments', type=str, nargs='+', metavar='<key segment> [<key segment> ...]', help='segment(s) of the key to look up (`[<num>]` for numbers)')
|
@@ -517,7 +574,7 @@ sys.excepthook = _exc_hook
|
|
517
574
|
# Load vault keys
|
518
575
|
|
519
576
|
_explicit_keys: list[VaultKey] = [ VaultKey(passphrase, vault_id=id) for id, passphrase in config.keys ]
|
520
|
-
keyring = VaultKeyring(_explicit_keys.copy(), detect_available_keys=config.detect_keys)
|
577
|
+
keyring = VaultKeyring(_explicit_keys.copy(), detect_available_keys=config.detect_keys, default_salt=config.fixed_salt)
|
521
578
|
|
522
579
|
if config.encryption_key:
|
523
580
|
keyring.default_encryption_key = keyring.key_by_id(config.encryption_key)
|
@@ -527,6 +584,8 @@ try:
|
|
527
584
|
debug(f"Encryption key: { keyring.encryption_key.id }")
|
528
585
|
except:
|
529
586
|
debug('Encryption key: unavailable')
|
587
|
+
if config.fixed_salt:
|
588
|
+
debug(f"Using fixed encryption salt: { config.fixed_salt }")
|
530
589
|
|
531
590
|
# Set up logging
|
532
591
|
|
@@ -582,7 +641,30 @@ if config.command in [ 'create', 'edit' ]:
|
|
582
641
|
vault = VaultFile.create(vault_path, full_encryption=config.encrypt_vault, permissions=0o600, keyring=keyring)
|
583
642
|
print(f"Created { 'encrypted' if vault.full_encryption else 'plain' } vault at { vault_path }", Color.GOOD)
|
584
643
|
else:
|
585
|
-
|
644
|
+
try:
|
645
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
646
|
+
except YAMLFormatError:
|
647
|
+
print('Invalid vault format, will be treated as a generic file', Color.MEH)
|
648
|
+
with NamedTemporaryFile(mode='w+', dir=config.temp_dir, prefix='vaultlike_') as edit_file:
|
649
|
+
with open(vault_path, 'r+') as file:
|
650
|
+
# Load and decrypt file
|
651
|
+
content: str = file.read()
|
652
|
+
if (is_enc := VaultKey.is_encrypted(content)):
|
653
|
+
content = keyring.decrypt(content)
|
654
|
+
# Let user edit the content in a temporary file
|
655
|
+
edit_file.write(content)
|
656
|
+
edit_file.flush()
|
657
|
+
sys_command(f"{ config.edit_command } { edit_file.name }", shell=True)
|
658
|
+
edit_file.seek(0)
|
659
|
+
# Encrypt the new content and write it back
|
660
|
+
new_content: str = edit_file.read()
|
661
|
+
if is_enc:
|
662
|
+
new_content = keyring.encrypt(new_content)
|
663
|
+
file.seek(0)
|
664
|
+
file.truncate()
|
665
|
+
file.write(new_content)
|
666
|
+
print(f"Saved changes!", Color.GOOD)
|
667
|
+
exit()
|
586
668
|
# Open vault for edit mode
|
587
669
|
if getattr(config, 'open_edit_mode', True):
|
588
670
|
print(f"Editing vault at { vault_path }")
|
@@ -591,6 +673,7 @@ if config.command in [ 'create', 'edit' ]:
|
|
591
673
|
# Write vault contents to temp file
|
592
674
|
editable: str = vault.as_editable()
|
593
675
|
edit_file.write(editable)
|
676
|
+
edit_file.flush()
|
594
677
|
while True:
|
595
678
|
# Open editor and wait for it to close
|
596
679
|
edit_file.seek(0)
|
@@ -613,18 +696,22 @@ if config.command in [ 'create', 'edit' ]:
|
|
613
696
|
break
|
614
697
|
new_vault.save()
|
615
698
|
print(f"Saved changes!", Color.GOOD)
|
616
|
-
|
617
|
-
if (changes := new_vault.changes(vault))[0]:
|
618
|
-
print(f"\n[!] The following vars have been decrypted in this edit:", Color.MEH)
|
619
|
-
print('\n'.join([ f"- { format_key_path(path) }" for path in changes[0] ]))
|
620
|
-
# Inform about new plain leaf variables
|
699
|
+
decrypted_vars: list[tuple[Hashable, ...]] = []
|
621
700
|
new_plain_leaves: list[tuple[Hashable, ...]] = []
|
622
701
|
def _find_new_plain_vars(path: tuple[Hashable, ...], value: Any) -> Any:
|
623
702
|
if path != ( SENTINEL_KEY, ) and type(value) is not EncryptedVar:
|
624
|
-
if vault.get(path, default=Unset) != value:
|
625
|
-
|
703
|
+
if (old_value := vault.get(path, default=Unset)) != value:
|
704
|
+
if type(old_value) is EncryptedVar:
|
705
|
+
decrypted_vars.append(path)
|
706
|
+
else:
|
707
|
+
new_plain_leaves.append(path)
|
626
708
|
return value
|
627
709
|
vault._transform_leaves(new_vault._data, _find_new_plain_vars, tuple())
|
710
|
+
# Warn about decrypted variables
|
711
|
+
if decrypted_vars:
|
712
|
+
print(f"\n[!] The following vars have been decrypted in this edit:", Color.MEH)
|
713
|
+
print('\n'.join([ f"- { format_key_path(path) }" for path in decrypted_vars ]))
|
714
|
+
# Warn about new plain leaf variables
|
628
715
|
if not new_vault.full_encryption and new_plain_leaves:
|
629
716
|
print(f"\n[!] The following plain vars have been added in this edit:", Color.MEH)
|
630
717
|
print('\n'.join([ f"- { format_key_path(path) }" for path in new_plain_leaves ]))
|
@@ -639,11 +726,18 @@ if config.command in [ 'create', 'edit' ]:
|
|
639
726
|
|
640
727
|
if config.command == 'view':
|
641
728
|
vault_path: str = resolve_vault_path(config.vault_path)
|
642
|
-
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
729
|
+
try:
|
730
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
731
|
+
if config.as_json:
|
732
|
+
print_json(vault.as_json())
|
733
|
+
else:
|
734
|
+
print_yaml(vault.as_plain())
|
735
|
+
except YAMLFormatError:
|
736
|
+
if config.as_json:
|
737
|
+
raise UnsupportedGenericFileOperation(operation='--json')
|
738
|
+
with open(vault_path) as file:
|
739
|
+
content: str = file.read()
|
740
|
+
print(keyring.decrypt(content) if VaultKey.is_encrypted(content) else content)
|
647
741
|
|
648
742
|
# Info command
|
649
743
|
|
@@ -684,27 +778,53 @@ if config.command in [ 'encrypt', 'decrypt', 'is-encrypted' ]:
|
|
684
778
|
# File target
|
685
779
|
if config.target_type == 'file':
|
686
780
|
vault_path: str = resolve_vault_path(config.target)
|
687
|
-
|
781
|
+
is_generic: bool = False
|
782
|
+
try:
|
783
|
+
vault = VaultFile(vault_path, keyring=keyring)
|
784
|
+
is_enc: bool = vault.full_encryption
|
785
|
+
except YAMLFormatError as e:
|
786
|
+
print('Invalid vault format, will be treated as a generic file', Color.MEH)
|
787
|
+
with open(vault_path) as file:
|
788
|
+
is_enc = VaultKey.is_encrypted(file.read())
|
789
|
+
is_generic = True
|
688
790
|
if config.command in [ 'encrypt', 'decrypt' ]:
|
689
|
-
if
|
690
|
-
print(f"Vault is already { 'en' if
|
791
|
+
if is_enc == (config.command == 'encrypt'):
|
792
|
+
print(f"Vault is already { 'en' if is_enc else 'de' }crypted.", Color.GOOD)
|
691
793
|
else:
|
692
|
-
|
693
|
-
|
694
|
-
|
794
|
+
is_enc = (config.command == 'encrypt')
|
795
|
+
# Generic file
|
796
|
+
if is_generic:
|
797
|
+
with open(vault_path, 'r+') as file:
|
798
|
+
content: str = file.read()
|
799
|
+
file.seek(0)
|
800
|
+
file.truncate()
|
801
|
+
file.write(keyring.encrypt(content) if is_enc else keyring.decrypt(content))
|
802
|
+
# Vault file
|
803
|
+
else:
|
804
|
+
vault.full_encryption = is_enc # type: ignore
|
805
|
+
vault.save() # type: ignore
|
806
|
+
print(f"Vault { 'en' if is_enc else 'de' }crypted.", Color.GOOD)
|
695
807
|
else:
|
696
808
|
if config.quiet:
|
697
|
-
exit(0 if
|
809
|
+
exit(0 if is_enc else 100)
|
698
810
|
else:
|
699
|
-
print(f"Vault is { 'encrypted' if
|
811
|
+
print(f"Vault is { 'fully encrypted' if is_enc else 'plain or hybrid' }.", Color.GOOD if is_enc else Color.MEH)
|
700
812
|
# String target
|
701
813
|
else:
|
702
814
|
is_encrypted: bool = VaultKey.is_encrypted(config.target)
|
815
|
+
# The key may not be passed properly, in which case we auto-convert literal '\n' to newlines
|
816
|
+
# We can assume an encrypted value should not contain any literal backslashes
|
817
|
+
if is_encrypted:
|
818
|
+
config.target = config.target.replace('\\n', '\n')
|
703
819
|
if config.command in [ 'encrypt', 'decrypt' ]:
|
704
820
|
if is_encrypted == (config.command == 'encrypt'):
|
705
|
-
|
821
|
+
if not config.quiet:
|
822
|
+
print(f"Value is already { 'en' if is_encrypted else 'de' }crypted.", Color.GOOD)
|
823
|
+
else:
|
824
|
+
print(config.target)
|
706
825
|
else:
|
707
|
-
|
826
|
+
if not config.quiet:
|
827
|
+
print(f"{ 'En' if not is_encrypted else 'De' }crypted value:", Color.GOOD)
|
708
828
|
print(keyring.encrypt(config.target) if (config.command == 'encrypt') else keyring.decrypt(config.target))
|
709
829
|
else:
|
710
830
|
if config.quiet:
|
@@ -732,7 +852,7 @@ if config.command == 'convert':
|
|
732
852
|
vault.save()
|
733
853
|
print(f"Vault converted to { 'outer' if vault.full_encryption else 'inner' } encryption.", Color.GOOD)
|
734
854
|
if not vault.full_encryption:
|
735
|
-
print('Please check the vault to make sure all secrets have been encrypted', Color.MEH)
|
855
|
+
print('Please check the vault to make sure all secrets have been encrypted!', Color.MEH)
|
736
856
|
_convert()
|
737
857
|
|
738
858
|
# Grep command
|
ansible_vars/errors.py
CHANGED
@@ -2,6 +2,14 @@
|
|
2
2
|
|
3
3
|
# YAML parsing
|
4
4
|
|
5
|
+
class UnsupportedGenericFileOperation(Exception):
|
6
|
+
'''The requested operation or flag is not available for generic (i.e. non-YAML-dictionary) files.'''
|
7
|
+
|
8
|
+
def __init__(self, *args: object, operation: str | None = None) -> None:
|
9
|
+
super().__init__(
|
10
|
+
f"The requested operation or flag is not available for generic files{ f': { operation }' * bool(operation) }", *args
|
11
|
+
)
|
12
|
+
|
5
13
|
class YAMLFormatError(Exception):
|
6
14
|
'''The supplied content is not a valid Ansible YAML file. Supports passing the triggering parent exception.'''
|
7
15
|
|
ansible_vars/util.py
CHANGED
@@ -282,7 +282,7 @@ class VaultDaemon(FileSystemEventHandler):
|
|
282
282
|
content: str = src.read()
|
283
283
|
with open(target_path, 'w') as tgt:
|
284
284
|
try:
|
285
|
-
tgt.write(Vault(content, keyring=self.keyring).
|
285
|
+
tgt.write(Vault(content, keyring=self.keyring).as_plain())
|
286
286
|
self.debug(f"Created/Updated file { target_path } from vault contents")
|
287
287
|
except:
|
288
288
|
tgt.write(content)
|
@@ -338,7 +338,7 @@ class VaultDaemon(FileSystemEventHandler):
|
|
338
338
|
content: str = src.read()
|
339
339
|
with open(self.target_file, 'w') as tgt:
|
340
340
|
try:
|
341
|
-
tgt.write(Vault(content, keyring=self.keyring).
|
341
|
+
tgt.write(Vault(content, keyring=self.keyring).as_plain())
|
342
342
|
self.debug(f"Created/Updated file { self.target_file } from vault contents")
|
343
343
|
except:
|
344
344
|
tgt.write(content)
|
@@ -373,7 +373,7 @@ class VaultDaemon(FileSystemEventHandler):
|
|
373
373
|
content: str = src.read()
|
374
374
|
with open(target_path, 'w') as tgt:
|
375
375
|
try:
|
376
|
-
tgt.write(Vault(content, keyring=self.keyring).
|
376
|
+
tgt.write(Vault(content, keyring=self.keyring).as_plain())
|
377
377
|
self.debug(f"Created/Updated file { target_path } from vault contents")
|
378
378
|
except:
|
379
379
|
tgt.write(content)
|
ansible_vars/vault.py
CHANGED
@@ -6,16 +6,14 @@ from io import StringIO
|
|
6
6
|
from functools import reduce
|
7
7
|
from typing import Type, Hashable, Callable, Any
|
8
8
|
from types import MappingProxyType
|
9
|
-
from collections import OrderedDict
|
10
9
|
from difflib import unified_diff
|
11
10
|
|
12
11
|
# External library imports
|
13
12
|
from ruamel.yaml import YAML
|
14
13
|
from ruamel.yaml.nodes import ScalarNode
|
15
|
-
from ruamel.yaml.parser import CommentToken
|
16
14
|
from ruamel.yaml.representer import Representer
|
17
15
|
from ruamel.yaml.constructor import Constructor
|
18
|
-
from ruamel.yaml.comments import CommentedMap
|
16
|
+
from ruamel.yaml.comments import CommentedMap
|
19
17
|
|
20
18
|
# Internal module imports
|
21
19
|
from .constants import ThrowError, octal, Indexable, ChangeList, MatchLocation, SENTINEL_KEY, EDIT_MODE_HEADER, ENCRYPTED_VAR_TAG
|
@@ -53,7 +51,7 @@ class EncryptedVar():
|
|
53
51
|
cipher: Any = constructor.construct_scalar(node)
|
54
52
|
if not isinstance(cipher, str):
|
55
53
|
raise TypeError(f"Expected encrypted value to be a str, but got { type(cipher) }")
|
56
|
-
return EncryptedVar(
|
54
|
+
return EncryptedVar(cipher, name=node.id)
|
57
55
|
|
58
56
|
class ProtoEncryptedVar():
|
59
57
|
'''A variable marked to be encrypted in a `Vault` editable.'''
|
@@ -77,14 +75,14 @@ class ProtoEncryptedVar():
|
|
77
75
|
|
78
76
|
@classmethod
|
79
77
|
def to_yaml(ProtoEncryptedVar: Type['ProtoEncryptedVar'], representer: Representer, var: 'ProtoEncryptedVar') -> Any:
|
80
|
-
return representer.represent_scalar(ENCRYPTED_VAR_TAG, var.plaintext, style='\'')
|
78
|
+
return representer.represent_scalar(ENCRYPTED_VAR_TAG, var.plaintext, style=('|' if '\n' in var.plaintext else ''))
|
81
79
|
|
82
80
|
@classmethod
|
83
81
|
def from_yaml(ProtoEncryptedVar: Type['ProtoEncryptedVar'], constructor: Constructor, node: ScalarNode) -> 'ProtoEncryptedVar':
|
84
82
|
plaintext: Any = constructor.construct_scalar(node)
|
85
83
|
if not isinstance(plaintext, str):
|
86
84
|
raise TypeError(f"Expected decrypted value to be a str, but got { type(plaintext) }")
|
87
|
-
return ProtoEncryptedVar(
|
85
|
+
return ProtoEncryptedVar(plaintext.rstrip('\n'), name=node.id)
|
88
86
|
|
89
87
|
class Vault():
|
90
88
|
'''
|
@@ -205,7 +203,7 @@ class Vault():
|
|
205
203
|
A read-only dictionary of the vault's variables, with any EncryptedVars already decrypted.
|
206
204
|
Note that the state is frozen whenever you access this property, and not updated when the vault changes.
|
207
205
|
'''
|
208
|
-
copy: dict = self._data
|
206
|
+
copy: dict = Vault._copy_data(self._data)
|
209
207
|
copy.pop(SENTINEL_KEY, None)
|
210
208
|
def _traverse_and_decrypt(root: Any) -> Any:
|
211
209
|
if isinstance(root, dict):
|
@@ -495,7 +493,7 @@ class Vault():
|
|
495
493
|
|
496
494
|
def as_plain(self) -> str:
|
497
495
|
'''Returns the vault in fully decrypted form as Jinja2 YAML code with the original metadata.'''
|
498
|
-
copy: CommentedMap = self._data
|
496
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
499
497
|
#copy.pop(SENTINEL_KEY, None) # <-- would break a file containing only metadata and the sentinel key
|
500
498
|
def _decrypt_leaf(_: tuple[Hashable, ...], value: Any) -> Any:
|
501
499
|
'''Transforms EncryptedVar leaves into decrypted strings.'''
|
@@ -510,7 +508,7 @@ class Vault():
|
|
510
508
|
Returns the vault as Jinja2 YAML code with the original metadata.
|
511
509
|
It is prepared for editing and later re-encryption with YAML tags and a static explanatory header.
|
512
510
|
'''
|
513
|
-
copy: CommentedMap = self._data
|
511
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
514
512
|
def _convert_to_proto(path: tuple[Hashable, ...], value: Any) -> Any:
|
515
513
|
'''Marks encrypted leaves for encryption after editing.'''
|
516
514
|
if type(value) is not EncryptedVar:
|
@@ -526,7 +524,7 @@ class Vault():
|
|
526
524
|
|
527
525
|
def as_encrypted(self) -> str:
|
528
526
|
'''Returns the vault as Jinja2 YAML code with the original metadata, with encrypted variables and full encryption if enabled.'''
|
529
|
-
copy: CommentedMap = self._data
|
527
|
+
copy: CommentedMap = Vault._copy_data(self._data)
|
530
528
|
yaml_content: str = self._dump_to_str(copy)
|
531
529
|
yaml_content = Vault._remove_sentinel(yaml_content)
|
532
530
|
if self.full_encryption:
|
@@ -567,6 +565,23 @@ class Vault():
|
|
567
565
|
self._parser.dump(data, builder)
|
568
566
|
return builder.getvalue().strip('\n') + '\n'
|
569
567
|
|
568
|
+
@staticmethod
|
569
|
+
def _copy_data(data: Any) -> Any:
|
570
|
+
'''
|
571
|
+
Create a deep copy of any data, using the object's `copy()` method for dicts and lists.
|
572
|
+
Parser dicts and lists contain special data, and their copy function will preserve that.
|
573
|
+
The built-in `copy.deepcopy()` would require a `__deepcopy__` method for this to work.
|
574
|
+
Copying works for dicts and lists. Everything else is referenced normally.
|
575
|
+
Not thread-safe.
|
576
|
+
'''
|
577
|
+
if (is_dict := isinstance(data, dict)) or isinstance(data, list):
|
578
|
+
copy = data.copy()
|
579
|
+
keys = copy.keys() if is_dict else range(len(copy)) # type: ignore
|
580
|
+
for key in keys:
|
581
|
+
copy[key] = Vault._copy_data(copy[key])
|
582
|
+
return copy
|
583
|
+
return data
|
584
|
+
|
570
585
|
# Comparing to older versions of this vault
|
571
586
|
|
572
587
|
def diff(self, prev_vault: 'Vault', context_lines: int = 3, show_filenames: bool = True) -> str:
|
@@ -653,7 +668,7 @@ class Vault():
|
|
653
668
|
def copy(self) -> 'Vault':
|
654
669
|
'''Create a copy of this `Vault` instance.'''
|
655
670
|
copy = Vault('', self.keyring)
|
656
|
-
copy._data = self._data
|
671
|
+
copy._data = Vault._copy_data(self._data)
|
657
672
|
copy.full_encryption = self.full_encryption
|
658
673
|
copy.keyring = self.keyring
|
659
674
|
copy._parser = self._parser
|
@@ -804,7 +819,7 @@ class VaultFile(Vault):
|
|
804
819
|
def copy(self) -> 'VaultFile':
|
805
820
|
'''Create a copy of this `VaultFile` instance.'''
|
806
821
|
copy: VaultFile = self.from_editable(self, '')
|
807
|
-
copy._data = self._data
|
822
|
+
copy._data = VaultFile._copy_data(self._data)
|
808
823
|
copy.full_encryption = self.full_encryption
|
809
824
|
copy.keyring = self.keyring
|
810
825
|
copy._parser = self._parser
|
ansible_vars/vault_crypt.py
CHANGED
@@ -54,12 +54,12 @@ class VaultKey():
|
|
54
54
|
'''
|
55
55
|
return VaultLib.is_encrypted(VaultKey._strip_vault_tag(test_me))
|
56
56
|
|
57
|
-
def encrypt(self, plain: str) -> str:
|
58
|
-
'''Encrypts a string using this `VaultKey`'s secret.'''
|
57
|
+
def encrypt(self, plain: str, salt: str | None = None) -> str:
|
58
|
+
'''Encrypts a string using this `VaultKey`'s secret. You can specify an optional fixed salt (ideally 32+ chars).'''
|
59
59
|
# Pass our secret directly to the encrypt call to skip expensive secret matching
|
60
60
|
# Beware: the encrypt function takes a `secret`, but means just the VaultSecret and not a tuple of (vault_id, VaultSecret)
|
61
61
|
# In other calls, `secret` or `secrets` may refer to the tuple(s)
|
62
|
-
return self._vaultlib.encrypt(plain, secret=self.secret, vault_id=self.id).decode('utf-8').strip()
|
62
|
+
return self._vaultlib.encrypt(plain, secret=self.secret, vault_id=self.id, salt=salt).decode('utf-8').strip()
|
63
63
|
|
64
64
|
def decrypt(self, vault_cipher: str) -> str:
|
65
65
|
'''
|
@@ -70,7 +70,7 @@ class VaultKey():
|
|
70
70
|
vault_cipher = VaultKey._strip_vault_tag(vault_cipher)
|
71
71
|
try:
|
72
72
|
decrypted: bytes = self._vaultlib.decrypt(vault_cipher)
|
73
|
-
return decrypted.decode('utf-8')
|
73
|
+
return decrypted.decode('utf-8')
|
74
74
|
except AnsibleVaultError as e:
|
75
75
|
if e.message.startswith('Decryption failed (no vault secrets were found that could decrypt)'):
|
76
76
|
raise VaultKeyMatchError(f"Could not match cipher with { self }")
|
@@ -96,6 +96,7 @@ class VaultKeyring():
|
|
96
96
|
self,
|
97
97
|
keys: list[VaultKey] | None = None,
|
98
98
|
default_encryption_key: VaultKey | None = None,
|
99
|
+
default_salt: str | None = None,
|
99
100
|
detect_available_keys: bool = True
|
100
101
|
) -> None:
|
101
102
|
'''
|
@@ -107,9 +108,13 @@ class VaultKeyring():
|
|
107
108
|
|
108
109
|
When encrypting data, you can specify an explicit `VaultKey` to use. If none is specified, `default_encryption_key` is used.
|
109
110
|
If no explicit or default keys are available, the first key of the `keys` parameter is used.
|
111
|
+
|
112
|
+
You can also set a fixed salt to use for encryption tasks, either passing it to the `encrypt` function directly
|
113
|
+
or as a `default_salt` here. A length of at least 32 chars is recommended for Ansible's AES-256.
|
110
114
|
'''
|
111
115
|
self.keys: list[VaultKey] = keys or []
|
112
116
|
self.default_encryption_key: VaultKey | None = default_encryption_key
|
117
|
+
self.default_salt: str | None = default_salt
|
113
118
|
if detect_available_keys:
|
114
119
|
self.keys.extend(VaultKeyring.load_cli_secrets())
|
115
120
|
|
@@ -124,16 +129,15 @@ class VaultKeyring():
|
|
124
129
|
raise NoVaultKeysError('No vault keys available for encryption')
|
125
130
|
return self.default_encryption_key or self.keys[0]
|
126
131
|
|
127
|
-
def encrypt(self, plain: str, key: VaultKey | None = None) -> str:
|
132
|
+
def encrypt(self, plain: str, key: VaultKey | None = None, salt: str | None = None) -> str:
|
128
133
|
'''
|
129
134
|
Encrypts the given vault data using the supplied `VaultKey`.
|
130
135
|
If no key is supplied, the `VaultKeyring`'s `default_encryption_key` is used.
|
131
136
|
If that key is also unset, the first key of the `VaultKeyring`'s `keys` is used.
|
137
|
+
You can specify an optional fixed salt for encrypting (ideally 32+ chars).
|
132
138
|
'''
|
133
139
|
# If no key is provided, use the default encryption key or first key in `keys`
|
134
|
-
|
135
|
-
key = self.encryption_key
|
136
|
-
return key.encrypt(plain)
|
140
|
+
return (key or self.encryption_key).encrypt(plain, salt=(salt or self.default_salt))
|
137
141
|
|
138
142
|
def decrypt(self, vault_cipher: str, key: VaultKey | None = None) -> str:
|
139
143
|
'''
|
@@ -143,7 +147,7 @@ class VaultKeyring():
|
|
143
147
|
|
144
148
|
Expects a cipher with optional YAML tag preamble (`!vault | $ANSIBLE_VAULT;<options>\\n<cipher>`).
|
145
149
|
'''
|
146
|
-
if not key
|
150
|
+
if not (key or self.keys):
|
147
151
|
raise NoVaultKeysError('No vault keys available for decryption')
|
148
152
|
if key:
|
149
153
|
return key.decrypt(vault_cipher)
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ansible-vars
|
3
|
-
Version: 1.0.
|
3
|
+
Version: 1.0.8
|
4
4
|
Summary: Manage vaults and variable files for Ansible
|
5
5
|
Project-URL: Homepage, https://github.com/xorwow/ansible-vars
|
6
6
|
Project-URL: Issues, https://github.com/xorwow/ansible-vars/issues
|
@@ -105,6 +105,10 @@ By default, the first loaded key is used for all encryption tasks. Note that aut
|
|
105
105
|
|
106
106
|
You can disable automatic key detection by flagging `--no-encrypt-keys|-D`. Use `ansible-vars keyring` to view all available keys.
|
107
107
|
|
108
|
+
#### Encryption salts
|
109
|
+
|
110
|
+
Each time you edit a vault or otherwise encrypt a value, a randomly generated salt is used so even identical plain values will result in unique ciphers. However, this also means that each time a single encrypted variable is edited, all other ciphers in the vault will change as well. You can avoid this by passing a fixed salt via `--fixed-salt|-S <salt>`. Note that it should be at least 32 characters long and sufficiently random for Ansible's AES-256 encryption, and that you won't benefit from unique ciphers for identical plaintexts anymore.
|
111
|
+
|
108
112
|
### Diff logging
|
109
113
|
|
110
114
|
**TL;DR:** You can use `-l <log directory>` to log changes to edited vaults to a vault-encrypted log file.
|
@@ -220,6 +224,10 @@ Set the tempfile/staging root as you would with `-T <path>`.
|
|
220
224
|
|
221
225
|
Invert the default creation mode for files: If unset or `no`, files are created with full encryption unless specified otherwise via the `--plain|-p` flag. This behavior mirrors that of `ansible-vault`. When set to `yes`, the behavior and flag are inverted as files are created without encryption by default unless specified otherwise via the `--no-plain|-P` flag.
|
222
226
|
|
227
|
+
#### AV_SALT
|
228
|
+
|
229
|
+
Set a fixed salt as you would with `-S <salt>`.
|
230
|
+
|
223
231
|
### Python library
|
224
232
|
|
225
233
|
When using `ansible-vars` as a library, import any of these modules from the `ansible_vars` module.
|
@@ -254,21 +262,22 @@ When editing a file or creating a daemon, decrypted vaults are written to disk t
|
|
254
262
|
|
255
263
|
## Known issues and limitations
|
256
264
|
|
257
|
-
- YAML round-trip parser
|
265
|
+
- YAML round-trip parser:
|
258
266
|
- Trailing comments and Jinja2 blocks may be misaligned and a trailing newline may be inserted/removed when switching between folded (`|`, `>`) and non-foldes values.
|
259
267
|
- The `set` and `del` commands may remove trailing comments and Jinja2 blocks.
|
260
268
|
- Explicit start/end markers (`---`, `...`) are not preserved.
|
261
269
|
- Supports lists, dictionaries, and scalar values.
|
262
270
|
- Does not support custom YAML tags (`!tag`).
|
263
|
-
- Ansible
|
271
|
+
- Ansible:
|
264
272
|
- Ansible only directly supports encrypted string values (although you can work around this with the `from_yaml` filter).
|
265
273
|
- Ansible-encrypted strings must include a newline between the envelope and the cipher.
|
266
274
|
- Ansible vault and variable file roots must be a dictionary.
|
267
|
-
- `grep` command
|
275
|
+
- `grep` command:
|
268
276
|
- Will ignore files which cannot be parsed as an Ansible YAML file.
|
269
|
-
- `file-daemon` command
|
277
|
+
- `file-daemon` command:
|
270
278
|
- Changes to file metadata (permissions, ...) are not mirrored.
|
271
|
-
- `ansible-vars`
|
279
|
+
- `ansible-vars` does not support files which are not (Jinja2) YAML dictionaries, except for limited support in these commands:
|
280
|
+
- `edit`, `view` (without `--json` support), `encrypt`, `decrypt`, `is-encrypted`
|
272
281
|
|
273
282
|
## Extension plans
|
274
283
|
|
@@ -0,0 +1,12 @@
|
|
1
|
+
ansible_vars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
+
ansible_vars/cli.py,sha256=TFPSuUmOTWjX6FF5aAd2PnJkyftSur9DfccYwzjQe6A,63959
|
3
|
+
ansible_vars/constants.py,sha256=Nd3sIuSoOvyfUfHfnsnJBDGMW7eNzbMm1NAvEQio9hE,1624
|
4
|
+
ansible_vars/errors.py,sha256=6dzyksPKWira9O2-Ir3MIOwr4XjN9MSBiRp5e6siY6Q,1256
|
5
|
+
ansible_vars/util.py,sha256=UwGPBT19pee7lBpWuBzLPAvcrHUBAn6i1MrJvzM9OQ4,21265
|
6
|
+
ansible_vars/vault.py,sha256=cMvFdtc3bw6yf-aChUEP34k2yafWS2UuubFO84De_rA,46383
|
7
|
+
ansible_vars/vault_crypt.py,sha256=nh2k686nTI3yERIp-qzx5iDE1kZKg10YG019QeZDnLM,10019
|
8
|
+
ansible_vars-1.0.8.dist-info/METADATA,sha256=KSO8y8E4DZeGzYjXKrnuj-aW8ni3TEAh1upcwGPdlL0,17967
|
9
|
+
ansible_vars-1.0.8.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
+
ansible_vars-1.0.8.dist-info/entry_points.txt,sha256=RrhkEH0MbfRzflguVrfYfthsFC5V2fkFnizUG3uHMtQ,55
|
11
|
+
ansible_vars-1.0.8.dist-info/licenses/LICENSE,sha256=ocyJHLG5wD12qB4uam2pqWTHIJmzloiyNyTex6Q2DKo,1062
|
12
|
+
ansible_vars-1.0.8.dist-info/RECORD,,
|
@@ -1,12 +0,0 @@
|
|
1
|
-
ansible_vars/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
2
|
-
ansible_vars/cli.py,sha256=Mp-J_1Tz6vz2uPhTQ_OWdWeZp_Zfc5qDHk2SnIjOo2U,59045
|
3
|
-
ansible_vars/constants.py,sha256=Nd3sIuSoOvyfUfHfnsnJBDGMW7eNzbMm1NAvEQio9hE,1624
|
4
|
-
ansible_vars/errors.py,sha256=VnViEBMR3rIeioMj460DgdBA5S5FYiDObaDDhG2FMBs,857
|
5
|
-
ansible_vars/util.py,sha256=BzS7n3UzaKqVZ3W78HVkJtdVCYofprqQDtU8wYH1d0Q,21325
|
6
|
-
ansible_vars/vault.py,sha256=_dp1K5_UAFGgcg6iO4on4-L_BaJO2cHP6My3EU6enA4,45593
|
7
|
-
ansible_vars/vault_crypt.py,sha256=JcFc6dTZ6EqhKXv_C5ofggTpBK8hWG3ZwrBrDNYcEIg,9501
|
8
|
-
ansible_vars-1.0.6.dist-info/METADATA,sha256=G4q4OqsOTJ6R9cUyoh9odAn5ZmWR59i5knWHPalQROs,17198
|
9
|
-
ansible_vars-1.0.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
10
|
-
ansible_vars-1.0.6.dist-info/entry_points.txt,sha256=RrhkEH0MbfRzflguVrfYfthsFC5V2fkFnizUG3uHMtQ,55
|
11
|
-
ansible_vars-1.0.6.dist-info/licenses/LICENSE,sha256=ocyJHLG5wD12qB4uam2pqWTHIJmzloiyNyTex6Q2DKo,1062
|
12
|
-
ansible_vars-1.0.6.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|