bundlewrap 5.0.3__tar.gz → 5.2.0__tar.gz

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.
Files changed (98) hide show
  1. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/AUTHORS +1 -0
  2. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/CHANGELOG.md +21 -0
  3. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/PKG-INFO +1 -1
  4. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/__init__.py +1 -1
  5. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/bundle.py +4 -0
  6. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/apply.py +2 -0
  7. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/parser.py +13 -2
  8. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/itemqueue.py +4 -7
  9. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/directories.py +14 -0
  10. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/kubernetes.py +10 -5
  11. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemd.py +5 -5
  12. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/node.py +54 -27
  13. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/repo.py +23 -8
  14. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/secrets.py +34 -9
  15. bundlewrap-5.2.0/bundlewrap/utils/node_lambda.py +34 -0
  16. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/remote.py +1 -0
  17. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/PKG-INFO +1 -1
  18. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/setup.py +1 -1
  19. bundlewrap-5.0.3/bundlewrap/utils/node_lambda.py +0 -55
  20. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/LICENSE +0 -0
  21. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/MANIFEST.in +0 -0
  22. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/README.md +0 -0
  23. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/__init__.py +0 -0
  24. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/debug.py +0 -0
  25. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/diff.py +0 -0
  26. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/generate_completions.py +0 -0
  27. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/groups.py +0 -0
  28. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/hash.py +0 -0
  29. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/ipmi.py +0 -0
  30. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/items.py +0 -0
  31. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/lock.py +0 -0
  32. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/metadata.py +0 -0
  33. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/nodes.py +0 -0
  34. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/plot.py +0 -0
  35. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/pw.py +0 -0
  36. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/repo.py +0 -0
  37. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/run.py +0 -0
  38. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/stats.py +0 -0
  39. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/test.py +0 -0
  40. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/verify.py +0 -0
  41. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/zen.py +0 -0
  42. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/concurrency.py +0 -0
  43. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/deps.py +0 -0
  44. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/exceptions.py +0 -0
  45. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/group.py +0 -0
  46. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/__init__.py +0 -0
  47. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/actions.py +0 -0
  48. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/dconf.py +0 -0
  49. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/files.py +0 -0
  50. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/git_deploy.py +0 -0
  51. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/groups.py +0 -0
  52. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg.py +0 -0
  53. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apk.py +0 -0
  54. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apt.py +0 -0
  55. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_dnf.py +0 -0
  56. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_freebsd.py +0 -0
  57. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_openbsd.py +0 -0
  58. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_opkg.py +0 -0
  59. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pacman.py +0 -0
  60. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pamac.py +0 -0
  61. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pip.py +0 -0
  62. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_snap.py +0 -0
  63. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_yum.py +0 -0
  64. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_zypper.py +0 -0
  65. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/postgres_dbs.py +0 -0
  66. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/postgres_roles.py +0 -0
  67. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/routeros.py +0 -0
  68. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_freebsd.py +0 -0
  69. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_openbsd.py +0 -0
  70. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_openrc.py +0 -0
  71. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemv.py +0 -0
  72. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/symlinks.py +0 -0
  73. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/users.py +0 -0
  74. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/zfs_dataset.py +0 -0
  75. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/zfs_pool.py +0 -0
  76. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/lock.py +0 -0
  77. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/metadata.py +0 -0
  78. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/metagen.py +0 -0
  79. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/operations.py +0 -0
  80. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/__init__.py +0 -0
  81. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/cmdline.py +0 -0
  82. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/crypto.py +0 -0
  83. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/dicts.py +0 -0
  84. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/magic_strings.py +0 -0
  85. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/metastack.py +0 -0
  86. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/plot.py +0 -0
  87. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/scm.py +0 -0
  88. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/table.py +0 -0
  89. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/testing.py +0 -0
  90. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/text.py +0 -0
  91. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/ui.py +0 -0
  92. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/SOURCES.txt +0 -0
  93. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/dependency_links.txt +0 -0
  94. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/entry_points.txt +0 -0
  95. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/not-zip-safe +0 -0
  96. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/requires.txt +0 -0
  97. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/top_level.txt +0 -0
  98. {bundlewrap-5.0.3 → bundlewrap-5.2.0}/setup.cfg +0 -0
@@ -13,3 +13,4 @@ Simon Brielbeck <simon.brielbeck@seibert.group>
13
13
  Peter Körner <git@mazdermind.de>
14
14
  Manuel Domke <manuel-domke@gmx.org>
15
15
  Sophie Schiller <sophie.schiller@seibert.group>
16
+ Max Jäger <xje4@xje4.eu>
@@ -1,3 +1,24 @@
1
+ # 5.2.0
2
+
3
+ 2026-10-07
4
+
5
+ * added `$BW_LOCK_ADD_SKIP_ITEM_VERIFICATION`: This sets a default for the `--skip-item-verification` option of `bw lock`
6
+ * `bw verify` will skip items when Faults are unavailable (instead of showing long stack traces), now matches the behavior of `bw apply`
7
+ * added `pre_purge_command` to `directory` items
8
+ * fixed `repo.nodes_matching()`: No longer swallows errors and now raises `NoSuchTarget` as documented
9
+
10
+ # 5.1.0
11
+
12
+ 2026-06-18
13
+
14
+ * added [new hooks](https://docs.bundlewrap.org/repo/hooks/) `repo_init` and `secret_key_use`
15
+ * added `bw apply --add-lock-for-fixed-items` to automatically lock all items that were changed during an apply
16
+ * `BUNDLE_DIR` and `BUNDLE_DATA_DIR` are now available as variables inside bundles
17
+ * fixed triggers, items can now trigger `tag:foo` or any other valid [item selector](https://docs.bundlewrap.org/guide/selectors/)
18
+ * fixed `svc_systemd` items so masking happens only after disabling (and unmasking before enabling); the previous order left orphaned `.wants` symlinks and broke units with `Also=` dependencies such as `NetworkManager`
19
+ * fixed `kubernetes` items so `delete: True` works on its own without requiring `manifest` or `apiVersion`
20
+ * fixed `PathInfo` so `OpenSSH ECDSA public key` is also recognized as text
21
+
1
22
  # 5.0.3
2
23
 
3
24
  2026-03-17
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bundlewrap
3
- Version: 5.0.3
3
+ Version: 5.2.0
4
4
  Summary: Config management with Python
5
5
  Home-page: http://bundlewrap.org
6
6
  Author: Torsten Rehn
@@ -1,2 +1,2 @@
1
- VERSION = (5, 0, 3)
1
+ VERSION = (5, 2, 0)
2
2
  VERSION_STRING = ".".join([str(v) for v in VERSION])
@@ -86,6 +86,8 @@ class Bundle:
86
86
  base_env = {
87
87
  'node': self.node,
88
88
  'repo': self.repo,
89
+ 'BUNDLE_DIR': self.bundle_dir,
90
+ 'BUNDLE_DATA_DIR': self.bundle_data_dir,
89
91
  }
90
92
 
91
93
  # TODO prevent access to node.metadata
@@ -103,6 +105,8 @@ class Bundle:
103
105
  base_env = {
104
106
  'node': self.node,
105
107
  'repo': self.repo,
108
+ 'BUNDLE_DIR': self.bundle_dir,
109
+ 'BUNDLE_DATA_DIR': self.bundle_data_dir,
106
110
  }
107
111
  for item_class in self.repo.item_classes:
108
112
  base_env[item_class.BUNDLE_ATTRIBUTE_NAME] = {}
@@ -72,6 +72,8 @@ def bw_apply(repo, args):
72
72
  'show_skipped_items': args['show_skipped_items'],
73
73
  'skip_list': skip_list,
74
74
  'workers': args['item_workers'],
75
+ 'add_lock_for_fixed_items': bool(args['expiry']),
76
+ 'lock_expiry': args['expiry'],
75
77
  },
76
78
  }
77
79
 
@@ -1,4 +1,5 @@
1
- from argparse import ArgumentParser, RawTextHelpFormatter, SUPPRESS
1
+ from argparse import (ArgumentParser, BooleanOptionalAction,
2
+ RawTextHelpFormatter, SUPPRESS)
2
3
  from os import environ, getcwd
3
4
  from os.path import join
4
5
 
@@ -221,6 +222,15 @@ bundle:my_bundle # items in this bundle
221
222
  metavar=_("PATH"),
222
223
  type=str,
223
224
  )
225
+ parser_apply.add_argument(
226
+ "--add-lock-for-fixed-items",
227
+ default="",
228
+ const=DEFAULT_softlock_expiry,
229
+ nargs='?',
230
+ dest='expiry',
231
+ help=HELP_softlock_expiry,
232
+ type=str,
233
+ )
224
234
 
225
235
  # bw debug
226
236
  help_debug = _("Start an interactive Python shell for this repository")
@@ -572,7 +582,8 @@ bundle:my_bundle # items in this bundle
572
582
  )
573
583
  parser_lock_add.add_argument(
574
584
  "--skip-item-verification",
575
- action='store_true',
585
+ action=BooleanOptionalAction,
586
+ default=bool(int(environ.get('BW_LOCK_ADD_SKIP_ITEM_VERIFICATION', 0))),
576
587
  dest='skip_item_verification',
577
588
  help=_("disable verification of item selector (-i)"),
578
589
  )
@@ -1,10 +1,10 @@
1
1
  from collections import defaultdict
2
2
 
3
3
  from .deps import (
4
- find_item,
5
4
  prepare_dependencies,
6
5
  remove_item_dependents,
7
6
  remove_dep_from_items,
7
+ resolve_selector,
8
8
  split_items_without_deps,
9
9
  )
10
10
  from .exceptions import NoSuchItem, MetadataUnavailable
@@ -142,12 +142,9 @@ class ItemQueue(BaseQueue):
142
142
  def _fire_triggers_for_item(self, item):
143
143
  for triggered_item_id in item.triggers:
144
144
  try:
145
- triggered_item = find_item(
146
- triggered_item_id,
147
- self.all_items,
148
- )
149
- triggered_item.has_been_triggered = True
150
- except NoSuchItem:
145
+ for triggered_item in resolve_selector(triggered_item_id, self.all_items):
146
+ triggered_item.has_been_triggered = True
147
+ except (NoSuchItem, ValueError):
151
148
  io.debug(_(
152
149
  "{item} tried to trigger {triggered_item}, "
153
150
  "but it wasn't available. It must have been skipped previously."
@@ -49,6 +49,7 @@ class Directory(Item):
49
49
  'mode': "0755",
50
50
  'owner': "root",
51
51
  'purge': False,
52
+ 'pre_purge_command': None,
52
53
  }
53
54
  ITEM_TYPE_NAME = "directory"
54
55
 
@@ -98,6 +99,11 @@ class Directory(Item):
98
99
  return
99
100
 
100
101
  for path in status.actual_state.get('paths_to_purge', set()):
102
+ if self.attributes['pre_purge_command']:
103
+ self.run(self.attributes['pre_purge_command'].format(
104
+ path=quote(path),
105
+ file=quote(path.split('/')[-1]),
106
+ ))
101
107
  self.run("rm -rf -- {}".format(quote(path)))
102
108
 
103
109
  for fix_type in ('mode', 'owner', 'group'):
@@ -280,6 +286,14 @@ class Directory(Item):
280
286
  for key, value in attributes.items():
281
287
  ATTRIBUTE_VALIDATORS[key](item_id, value)
282
288
 
289
+ if attributes.get('pre_purge_command') and not attributes.get('purge'):
290
+ raise BundleError(_(
291
+ "{b} {i} cannot use pre_purge_command if not purging"
292
+ ).format(
293
+ n=bundle.name,
294
+ i=item_id,
295
+ ))
296
+
283
297
  @classmethod
284
298
  def validate_name(cls, bundle, name):
285
299
  if normpath(name) != name:
@@ -206,11 +206,16 @@ class KubernetesItem(Item, metaclass=ABCMeta):
206
206
  def validate_attributes(cls, bundle, item_id, attributes):
207
207
  if attributes.get('delete', False):
208
208
  for attr in attributes.keys():
209
- if attr not in ['delete'] + list(BUILTIN_ITEM_ATTRIBUTES.keys()):
210
- raise BundleError(_(
211
- "{item} from bundle '{bundle}' cannot have other "
212
- "attributes besides 'delete'"
213
- ).format(item=item_id, bundle=bundle.name))
209
+ if attr in ['delete'] + list(BUILTIN_ITEM_ATTRIBUTES.keys()):
210
+ continue
211
+
212
+ if attr == 'manifest' and set(attributes['manifest'].keys()) == {'apiVersion'}:
213
+ continue
214
+
215
+ raise BundleError(_(
216
+ "{item} from bundle '{bundle}' cannot have other "
217
+ "attributes besides 'delete' and 'manifest/apiVersion'"
218
+ ).format(item=item_id, bundle=bundle.name))
214
219
  if attributes.get('manifest') and attributes.get('manifest_file'):
215
220
  raise BundleError(_(
216
221
  "{item} from bundle '{bundle}' cannot have both 'manifest' and 'manifest_file'"
@@ -87,11 +87,8 @@ class SvcSystemd(Item):
87
87
  return state
88
88
 
89
89
  def fix(self, status):
90
- if 'masked' in status.keys_to_fix:
91
- if self.attributes['masked']:
92
- svc_mask(self.node, self.name)
93
- else:
94
- svc_unmask(self.node, self.name)
90
+ if 'masked' in status.keys_to_fix and not self.attributes['masked']:
91
+ svc_unmask(self.node, self.name)
95
92
 
96
93
  if 'enabled' in status.keys_to_fix:
97
94
  if self.attributes['enabled']:
@@ -105,6 +102,9 @@ class SvcSystemd(Item):
105
102
  else:
106
103
  svc_stop(self.node, self.name)
107
104
 
105
+ if 'masked' in status.keys_to_fix and self.attributes['masked']:
106
+ svc_mask(self.node, self.name)
107
+
108
108
  def get_canned_actions(self):
109
109
  return {
110
110
  'stop': {
@@ -13,6 +13,7 @@ from .concurrency import WorkerPool
13
13
  from .deps import find_item, ItemDependencyLoop
14
14
  from .exceptions import (
15
15
  BundleError,
16
+ FaultUnavailable,
16
17
  GracefulApplyException,
17
18
  ItemSkipped,
18
19
  NodeLockedException,
@@ -26,7 +27,7 @@ from .exceptions import (
26
27
  from .group import GROUP_ATTR_DEFAULTS, GROUP_ATTR_TYPES, GROUP_ATTR_TYPES_ENFORCED
27
28
  from .itemqueue import ItemQueue
28
29
  from .items import Item
29
- from .lock import NodeLock
30
+ from .lock import NodeLock, softlock_add
30
31
  from .metadata import hash_metadata
31
32
  from .utils import (
32
33
  cached_property,
@@ -55,6 +56,7 @@ from .utils.text import (
55
56
  green,
56
57
  mark_for_translation as _,
57
58
  prefix_lines,
59
+ randstr,
58
60
  red,
59
61
  toml_clean,
60
62
  validate_name,
@@ -739,6 +741,8 @@ class Node:
739
741
  show_skipped_items=True,
740
742
  skip_list=(),
741
743
  workers=4,
744
+ add_lock_for_fixed_items=False,
745
+ lock_expiry="8h",
742
746
  ):
743
747
  if not list(self.items):
744
748
  io.stdout(_("{x} {node} has no items").format(
@@ -801,6 +805,23 @@ class Node:
801
805
  show_diff=show_diff,
802
806
  show_skipped_items=show_skipped_items,
803
807
  )
808
+
809
+ if add_lock_for_fixed_items:
810
+ items_to_lock = set()
811
+
812
+ for item in item_results:
813
+ item_id, status_code, _duration = item
814
+ if status_code in (Item.STATUS_FIXED, Item.STATUS_ACTION_SUCCEEDED):
815
+ items_to_lock.add(item_id)
816
+
817
+ if items_to_lock:
818
+ lock_id = "APPLY_" + randstr(length=4).upper()
819
+ softlock_add(
820
+ self,
821
+ lock_id,
822
+ item_selectors=sorted(items_to_lock),
823
+ expiry=lock_expiry,
824
+ )
804
825
  except NodeLockedException as e:
805
826
  if not interactive:
806
827
  io.stderr(_(
@@ -1125,38 +1146,44 @@ def verify_items(
1125
1146
  return bool(items)
1126
1147
 
1127
1148
  def next_task():
1128
- while True:
1129
- try:
1130
- item = items.pop()
1131
- except IndexError:
1132
- return None
1133
- if item._faults_missing_for_attributes:
1134
- if item.error_on_missing_fault:
1135
- item._raise_for_faults()
1136
- else:
1137
- io.progress_advance()
1138
- io.stdout(_("{x} {node} {bundle} {item} ({msg})").format(
1139
- bundle=bold(item.bundle.name),
1140
- item=item.id,
1141
- msg=yellow(_("Fault unavailable")),
1142
- node=bold(node.name),
1143
- x=yellow("»"),
1144
- ))
1145
- else:
1146
- return {
1147
- 'task_id': node.name + ":" + item.bundle.name + ":" + item.id,
1148
- 'target': item.verify,
1149
- 'kwargs': {
1150
- 'autoskip_selector': autoskip_selector,
1151
- 'autoonly_selector': autoonly_selector,
1152
- },
1153
- }
1149
+ try:
1150
+ item = items.pop()
1151
+ except IndexError:
1152
+ return None
1153
+ return {
1154
+ 'task_id': node.name + ":" + item.bundle.name + ":" + item.id,
1155
+ 'target': item.verify,
1156
+ 'kwargs': {
1157
+ 'autoskip_selector': autoskip_selector,
1158
+ 'autoonly_selector': autoonly_selector,
1159
+ },
1160
+ }
1154
1161
 
1155
1162
  def handle_exception(task_id, exception, traceback):
1156
1163
  node_name, bundle_name, item_id = task_id.split(":", 2)
1157
1164
  io.progress_advance()
1158
1165
  if isinstance(exception, (ItemSkipped, NotImplementedError)):
1159
1166
  pass
1167
+ elif isinstance(exception, FaultUnavailable):
1168
+ item = node.get_item(item_id)
1169
+ if item.error_on_missing_fault:
1170
+ io.stderr(_("{x} {node} {bundle} {item} ({msg})").format(
1171
+ bundle=bold(bundle_name),
1172
+ item=item_id,
1173
+ msg=red(_("Fault unavailable")),
1174
+ node=bold(node_name),
1175
+ x=red("!"),
1176
+ ))
1177
+ return False
1178
+ else:
1179
+ io.stdout(_("{x} {node} {bundle} {item} ({msg})").format(
1180
+ bundle=bold(bundle_name),
1181
+ item=item_id,
1182
+ msg=yellow(_("Fault unavailable")),
1183
+ node=bold(node_name),
1184
+ x=yellow("»"),
1185
+ ))
1186
+ return None
1160
1187
  else:
1161
1188
  # Unlike with `bw apply`, it is OK for `bw verify` to encounter
1162
1189
  # exceptions when getting an item's status. `bw verify` doesn't
@@ -17,7 +17,7 @@ if version_info >= VERSION_NEW_PACKAGING:
17
17
  else:
18
18
  from pkg_resources import DistributionNotFound, require, VersionConflict # needs setuptools
19
19
 
20
- from . import items, VERSION_STRING
20
+ from . import items, VERSION, VERSION_STRING
21
21
  from .bundle import FILENAME_ITEMS
22
22
  from .exceptions import (
23
23
  NoSuchGroup,
@@ -68,8 +68,10 @@ HOOK_EVENTS = (
68
68
  'node_apply_start',
69
69
  'node_run_end',
70
70
  'node_run_start',
71
+ 'repo_init',
71
72
  'run_end',
72
73
  'run_start',
74
+ 'secret_key_use',
73
75
  'test',
74
76
  'test_node',
75
77
  )
@@ -178,7 +180,7 @@ class HooksProxy:
178
180
 
179
181
  def __getattr__(self, attrname):
180
182
  if attrname not in HOOK_EVENTS:
181
- raise AttributeError
183
+ raise AttributeError(attrname)
182
184
 
183
185
  if self.__registered_hooks is None:
184
186
  self._register_hooks()
@@ -289,6 +291,12 @@ class Repository(MetadataGenerator):
289
291
  else:
290
292
  self.item_classes = list(self.items_from_dir(items.__path__[0]))
291
293
 
294
+ self.hooks.repo_init(
295
+ repo=self,
296
+ version_string=VERSION_STRING,
297
+ version_tuple=VERSION,
298
+ )
299
+
292
300
  def __eq__(self, other):
293
301
  if self.path == "/dev/null":
294
302
  # in-memory repos are never equal
@@ -688,28 +696,35 @@ class Repository(MetadataGenerator):
688
696
  targets = set()
689
697
  for name in target_strings:
690
698
  name = name.strip()
699
+ targets_for_name = set()
691
700
  if name.startswith("bundle:"):
692
701
  bundle_name = name.split(":", 1)[1]
693
- targets.update(self.nodes_with_bundle(bundle_name))
702
+ targets_for_name.update(self.nodes_with_bundle(bundle_name))
694
703
  elif name.startswith("!bundle:"):
695
704
  bundle_name = name.split(":", 1)[1]
696
- targets.update(self.nodes_without_bundle(bundle_name))
705
+ targets_for_name.update(self.nodes_without_bundle(bundle_name))
697
706
  elif name.startswith("!group:"):
698
707
  group_name = name.split(":", 1)[1]
699
- targets.update(self.nodes_not_in_group(group_name))
708
+ targets_for_name.update(self.nodes_not_in_group(group_name))
700
709
  elif name.startswith("lambda:"):
701
710
  lambda_str = name.split(":", 1)[1]
702
- targets.update(self.nodes_matching_lambda(lambda_str, lambda_workers))
711
+ targets_for_name.update(self.nodes_matching_lambda(lambda_str, lambda_workers))
703
712
  else:
704
713
  try:
705
- targets.add(self.get_node(name))
714
+ targets_for_name.add(self.get_node(name))
706
715
  except NoSuchNode:
707
716
  try:
708
717
  group = self.get_group(name)
709
- targets.update(group.nodes)
718
+ targets_for_name.update(group.nodes)
710
719
  except NoSuchGroup:
711
720
  raise NoSuchTarget(name)
712
721
 
722
+ # Puplic API docs demand this.
723
+ if not targets_for_name:
724
+ raise NoSuchTarget(name)
725
+
726
+ targets |= targets_for_name
727
+
713
728
  return list(targets)
714
729
 
715
730
  def metadata_hash(self):
@@ -6,6 +6,7 @@ from os import environ
6
6
  from os.path import join
7
7
  from string import ascii_letters, punctuation, digits
8
8
  from subprocess import PIPE, run
9
+ from threading import Lock
9
10
 
10
11
  from cryptography.fernet import Fernet
11
12
 
@@ -70,6 +71,20 @@ class SecretProxy:
70
71
  def __init__(self, repo):
71
72
  self.repo = repo
72
73
  self.keys = self._load_keys()
74
+ self.key_hook_lock = Lock()
75
+ self.key_hook_in_use = {}
76
+
77
+ def __hook(self, key):
78
+ with self.key_hook_lock:
79
+ if key not in self.key_hook_in_use:
80
+ self.key_hook_in_use[key] = Lock()
81
+ if self.key_hook_in_use[key].locked():
82
+ return
83
+ with self.key_hook_in_use[key]:
84
+ self.repo.hooks.secret_key_use(
85
+ repo=self.repo,
86
+ key=key,
87
+ )
73
88
 
74
89
  def _decrypt(self, cryptotext=None, key=None):
75
90
  """
@@ -78,7 +93,8 @@ class SecretProxy:
78
93
  if environ.get("BW_VAULT_DUMMY_MODE", "0") != "0":
79
94
  return "decrypted text"
80
95
 
81
- key, cryptotext = self._determine_key_to_use(cryptotext.encode('utf-8'), key, cryptotext)
96
+ key, key_name, cryptotext = self._determine_key_to_use(cryptotext.encode('utf-8'), key, cryptotext)
97
+ self.__hook(key_name)
82
98
  return Fernet(key).decrypt(cryptotext).decode('utf-8')
83
99
 
84
100
  def _decrypt_file(self, source_path=None, binary=False, key=None):
@@ -90,7 +106,8 @@ class SecretProxy:
90
106
  return "decrypted file"
91
107
 
92
108
  cryptotext = get_file_contents(join(self.repo.data_dir, source_path))
93
- key, cryptotext = self._determine_key_to_use(cryptotext, key, source_path)
109
+ key, key_name, cryptotext = self._determine_key_to_use(cryptotext, key, source_path)
110
+ self.__hook(key_name)
94
111
 
95
112
  f = Fernet(key)
96
113
  if binary:
@@ -107,12 +124,13 @@ class SecretProxy:
107
124
  return b64encode(b"decrypted file as base64").decode('utf-8')
108
125
 
109
126
  cryptotext = get_file_contents(join(self.repo.data_dir, source_path))
110
- key, cryptotext = self._determine_key_to_use(cryptotext, key, source_path)
127
+ key, key_name, cryptotext = self._determine_key_to_use(cryptotext, key, source_path)
128
+ self.__hook(key_name)
111
129
 
112
130
  f = Fernet(key)
113
131
  return b64encode(f.decrypt(cryptotext)).decode('utf-8')
114
132
 
115
- def _determine_key_to_use(self, cryptotext, key, entity_description):
133
+ def _determine_key_to_use(self, cryptotext, key_name, entity_description):
116
134
  key_delim = cryptotext.find(b'$')
117
135
  if key_delim > -1:
118
136
  key_from_text = cryptotext[:key_delim].decode('utf-8')
@@ -120,14 +138,15 @@ class SecretProxy:
120
138
  else:
121
139
  key_from_text = None
122
140
 
123
- if key is None:
141
+ if key_name is None:
124
142
  if key_from_text is not None:
125
- key = key_from_text
143
+ key_name = key_from_text
126
144
  else:
127
- key = 'encrypt'
145
+ key_name = 'encrypt'
128
146
 
147
+ key = None
129
148
  try:
130
- key = self.keys[key]
149
+ key = self.keys[key_name]
131
150
  except KeyError:
132
151
  raise FaultUnavailable(_(
133
152
  "Key '{key}' not available for decryption of the following entity, "
@@ -138,7 +157,7 @@ class SecretProxy:
138
157
  entity_description=entity_description,
139
158
  ))
140
159
 
141
- return key, cryptotext
160
+ return key, key_name, cryptotext
142
161
 
143
162
  def _generate_human_password(
144
163
  self, identifier=None, digits=2, key='generate', per_word=3, words=4,
@@ -159,6 +178,8 @@ class SecretProxy:
159
178
  if environ.get("BW_VAULT_DUMMY_MODE", "0") != "0":
160
179
  return "generatedpassword"
161
180
 
181
+ self.__hook(key)
182
+
162
183
  prng = self._get_prng(identifier, key)
163
184
 
164
185
  pwd = ""
@@ -208,6 +229,8 @@ class SecretProxy:
208
229
  if environ.get("BW_VAULT_DUMMY_MODE", "0") != "0":
209
230
  return ("generatedpassword"*length)[:length]
210
231
 
232
+ self.__hook(key)
233
+
211
234
  prng = self._get_prng(identifier, key)
212
235
 
213
236
  alphabet = ascii_letters + digits
@@ -220,6 +243,8 @@ class SecretProxy:
220
243
  if environ.get("BW_VAULT_DUMMY_MODE", "0") != "0":
221
244
  return b64encode(bytearray([ord("a") for i in range(length)])).decode()
222
245
 
246
+ self.__hook(key)
247
+
223
248
  prng = self._get_prng(identifier, key)
224
249
  return b64encode(bytearray([next(prng) for i in range(length)])).decode()
225
250
 
@@ -0,0 +1,34 @@
1
+ from bundlewrap.concurrency import WorkerPool
2
+
3
+
4
+ def parallel_node_eval(
5
+ nodes,
6
+ expression,
7
+ workers,
8
+ ):
9
+ nodes = set(nodes)
10
+
11
+ def tasks_available():
12
+ return bool(nodes)
13
+
14
+ def next_task():
15
+ node = nodes.pop()
16
+
17
+ def get_values():
18
+ return eval("lambda node: " + expression)(node)
19
+
20
+ return {
21
+ 'task_id': node.name,
22
+ 'target': get_values,
23
+ }
24
+
25
+ def handle_result(task_id, result, _duration):
26
+ return task_id, result
27
+
28
+ worker_pool = WorkerPool(
29
+ tasks_available,
30
+ next_task,
31
+ handle_result=handle_result,
32
+ workers=workers,
33
+ )
34
+ return dict(worker_pool.run())
@@ -82,6 +82,7 @@ class PathInfo:
82
82
  self.desc in (
83
83
  "empty",
84
84
  "JSON data",
85
+ "OpenSSH ECDSA public key",
85
86
  "OpenSSH ED25519 public key",
86
87
  "OpenSSH RSA public key",
87
88
  "OpenSSH DSA public key",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bundlewrap
3
- Version: 5.0.3
3
+ Version: 5.2.0
4
4
  Summary: Config management with Python
5
5
  Home-page: http://bundlewrap.org
6
6
  Author: Torsten Rehn
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
2
2
 
3
3
  setup(
4
4
  name="bundlewrap",
5
- version="5.0.3",
5
+ version="5.2.0",
6
6
  description="Config management with Python",
7
7
  long_description=(
8
8
  "By allowing for easy and low-overhead config management, BundleWrap fills the gap between complex deployments using Chef or Puppet and old school system administration over SSH.\n"
@@ -1,55 +0,0 @@
1
- from traceback import format_exc
2
-
3
- from bundlewrap.concurrency import WorkerPool
4
- from bundlewrap.exceptions import RepositoryError
5
- from bundlewrap.utils.text import red, bold, prefix_lines, _
6
- from bundlewrap.utils.ui import io
7
-
8
-
9
- def parallel_node_eval(
10
- nodes,
11
- expression,
12
- workers,
13
- ):
14
- nodes = set(nodes)
15
-
16
- def tasks_available():
17
- return bool(nodes)
18
-
19
- def next_task():
20
- node = nodes.pop()
21
-
22
- def get_values():
23
- try:
24
- return eval("lambda node: " + expression)(node)
25
- except RepositoryError:
26
- raise
27
- except Exception:
28
- traceback = format_exc()
29
- io.stderr(_(
30
- "{x} {node} Exception while evaluating `{expression}`, returning as None:\n{traceback}"
31
- ).format(
32
- x=red("✘"),
33
- node=bold(node),
34
- expression=expression,
35
- traceback=prefix_lines("\n" + traceback, f"{red('│')} ") + red("╵"),
36
- ))
37
- # Returning None here is kinda meh. But it's the only alternative
38
- # to failing hard by re-raising, which would be very annoying.
39
- return None
40
-
41
- return {
42
- 'task_id': node.name,
43
- 'target': get_values,
44
- }
45
-
46
- def handle_result(task_id, result, _duration):
47
- return task_id, result
48
-
49
- worker_pool = WorkerPool(
50
- tasks_available,
51
- next_task,
52
- handle_result=handle_result,
53
- workers=workers,
54
- )
55
- return dict(worker_pool.run())
File without changes
File without changes
File without changes
File without changes