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.
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/AUTHORS +1 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/CHANGELOG.md +21 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/PKG-INFO +1 -1
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/__init__.py +1 -1
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/bundle.py +4 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/apply.py +2 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/parser.py +13 -2
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/itemqueue.py +4 -7
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/directories.py +14 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/kubernetes.py +10 -5
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemd.py +5 -5
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/node.py +54 -27
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/repo.py +23 -8
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/secrets.py +34 -9
- bundlewrap-5.2.0/bundlewrap/utils/node_lambda.py +34 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/remote.py +1 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/PKG-INFO +1 -1
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/setup.py +1 -1
- bundlewrap-5.0.3/bundlewrap/utils/node_lambda.py +0 -55
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/LICENSE +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/MANIFEST.in +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/README.md +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/__init__.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/debug.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/diff.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/generate_completions.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/groups.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/hash.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/ipmi.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/items.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/lock.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/metadata.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/nodes.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/plot.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/pw.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/repo.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/run.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/stats.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/test.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/verify.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/cmdline/zen.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/concurrency.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/deps.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/exceptions.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/group.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/__init__.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/actions.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/dconf.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/files.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/git_deploy.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/groups.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apk.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_apt.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_dnf.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_freebsd.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_openbsd.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_opkg.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pacman.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pamac.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_pip.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_snap.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_yum.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/pkg_zypper.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/postgres_dbs.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/postgres_roles.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/routeros.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_freebsd.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_openbsd.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_openrc.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/svc_systemv.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/symlinks.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/users.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/zfs_dataset.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/items/zfs_pool.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/lock.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/metadata.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/metagen.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/operations.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/__init__.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/cmdline.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/crypto.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/dicts.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/magic_strings.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/metastack.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/plot.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/scm.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/table.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/testing.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/text.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap/utils/ui.py +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/SOURCES.txt +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/dependency_links.txt +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/entry_points.txt +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/not-zip-safe +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/requires.txt +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/bundlewrap.egg-info/top_level.txt +0 -0
- {bundlewrap-5.0.3 → bundlewrap-5.2.0}/setup.cfg +0 -0
|
@@ -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,2 +1,2 @@
|
|
|
1
|
-
VERSION = (5,
|
|
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] = {}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
from argparse import ArgumentParser,
|
|
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=
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
711
|
+
targets_for_name.update(self.nodes_matching_lambda(lambda_str, lambda_workers))
|
|
703
712
|
else:
|
|
704
713
|
try:
|
|
705
|
-
|
|
714
|
+
targets_for_name.add(self.get_node(name))
|
|
706
715
|
except NoSuchNode:
|
|
707
716
|
try:
|
|
708
717
|
group = self.get_group(name)
|
|
709
|
-
|
|
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,
|
|
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
|
|
141
|
+
if key_name is None:
|
|
124
142
|
if key_from_text is not None:
|
|
125
|
-
|
|
143
|
+
key_name = key_from_text
|
|
126
144
|
else:
|
|
127
|
-
|
|
145
|
+
key_name = 'encrypt'
|
|
128
146
|
|
|
147
|
+
key = None
|
|
129
148
|
try:
|
|
130
|
-
key = self.keys[
|
|
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())
|
|
@@ -2,7 +2,7 @@ from setuptools import find_packages, setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="bundlewrap",
|
|
5
|
-
version="5.0
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|