Skip to content

Commit 579f3f6

Browse files
authored
Global config object (#633)
* Add global `pyinfra.config` pseudo module. * Implement `__setattr__` for the pseudo modules. * Move config defaults into a dictionary rather than class attributes. * Simplify config extraction from files, update for new defaults. * Log a warning when loading config variables directly from files. * Implement config state locking. This provides two guarantees: + When executing multiple deploy files via the CLI, any config changes in them does not affect the others (this has not existed previously). + When including files, any config changes in them are only held during the execution of the file, and removed after. This matches the current include config logic. * Linting fixes.
1 parent 8c7a7c0 commit 579f3f6

File tree

7 files changed

+119
-91
lines changed

7 files changed

+119
-91
lines changed

pyinfra/api/config.py

+50-29
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,87 @@
11
import six
22

33

4-
class Config(object):
5-
'''
6-
The default/base configuration options for a pyinfra deploy.
7-
'''
8-
9-
state = None
10-
4+
config_defaults = {
115
# % of hosts which have to fail for all operations to stop
12-
FAIL_PERCENT = None
6+
'FAIL_PERCENT': None,
137

148
# Seconds to timeout SSH connections
15-
CONNECT_TIMEOUT = 10
9+
'CONNECT_TIMEOUT': 10,
1610

1711
# Temporary directory (on the remote side) to use for caching any files/downloads
18-
TEMP_DIR = '/tmp'
12+
'TEMP_DIR': '/tmp',
1913

2014
# Gevent pool size (defaults to #of target hosts)
21-
PARALLEL = None
15+
'PARALLEL': None,
2216

2317
# Specify the required pyinfra version (using PEP 440 setuptools specifier)
24-
REQUIRE_PYINFRA_VERSION = None
18+
'REQUIRE_PYINFRA_VERSION': None,
2519
# Specify any required packages (either using PEP 440 or a requirements file)
2620
# Note: this can also include pyinfra, potentially replacing REQUIRE_PYINFRA_VERSION
27-
REQUIRE_PACKAGES = None
21+
'REQUIRE_PACKAGES': None,
2822

2923
# COMPAT w/<1.1
3024
# TODO: remove this in favour of above at v2
3125
# Specify a minimum required pyinfra version for a deploy
32-
MIN_PYINFRA_VERSION = None
26+
'MIN_PYINFRA_VERSION': None,
3327

3428
# All these can be overridden inside individual operation calls:
3529

3630
# Switch to this user (from ssh_user) using su before executing operations
37-
SU_USER = None
38-
USE_SU_LOGIN = False
39-
SU_SHELL = None
40-
PRESERVE_SU_ENV = False
31+
'SU_USER': None,
32+
'USE_SU_LOGIN': False,
33+
'SU_SHELL': None,
34+
'PRESERVE_SU_ENV': False,
4135

4236
# Use sudo and optional user
43-
SUDO = False
44-
SUDO_USER = None
45-
PRESERVE_SUDO_ENV = False
46-
USE_SUDO_LOGIN = False
47-
USE_SUDO_PASSWORD = False
37+
'SUDO': False,
38+
'SUDO_USER': None,
39+
'PRESERVE_SUDO_ENV': False,
40+
'USE_SUDO_LOGIN': False,
41+
'USE_SUDO_PASSWORD': False,
4842

4943
# Use doas and optional user
50-
DOAS = False
51-
DOAS_USER = None
44+
'DOAS': False,
45+
'DOAS_USER': None,
5246

5347
# Only show errors, but don't count as failure
54-
IGNORE_ERRORS = False
48+
'IGNORE_ERRORS': False,
5549

5650
# Shell to use to execute commands
57-
SHELL = None
51+
'SHELL': None,
52+
}
53+
54+
55+
class Config(object):
56+
'''
57+
The default/base configuration options for a pyinfra deploy.
58+
'''
59+
60+
state = None
5861

5962
def __init__(self, **kwargs):
6063
# Always apply some env
6164
env = kwargs.pop('ENV', {})
6265
self.ENV = env
6366

64-
# Apply kwargs
65-
for key, value in six.iteritems(kwargs):
67+
config = config_defaults.copy()
68+
config.update(kwargs)
69+
70+
for key, value in six.iteritems(config):
6671
setattr(self, key, value)
72+
73+
def get_current_state(self):
74+
return [
75+
(key, getattr(self, key))
76+
for key in config_defaults.keys()
77+
]
78+
79+
def set_current_state(self, config_state):
80+
for key, value in config_state:
81+
setattr(self, key, value)
82+
83+
def lock_current_sate(self):
84+
self._locked_config = self.get_current_state()
85+
86+
def reset_locked_state(self):
87+
self.set_current_state(self._locked_config)

pyinfra/api/connectors/util.py

+13-13
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from six.moves import shlex_quote
1313

1414
from pyinfra import logger
15-
from pyinfra.api import Config, MaskString, QuoteString, StringCommand
15+
from pyinfra.api import MaskString, QuoteString, StringCommand
1616
from pyinfra.api.util import memoize
1717

1818
SUDO_ASKPASS_ENV_VAR = 'PYINFRA_SUDO_PASSWORD'
@@ -214,21 +214,21 @@ def make_unix_command(
214214
command,
215215
env=None,
216216
chdir=None,
217-
shell_executable=Config.SHELL,
217+
shell_executable=None,
218218
# Su config
219-
su_user=Config.SU_USER,
220-
use_su_login=Config.USE_SU_LOGIN,
221-
su_shell=Config.SU_SHELL,
222-
preserve_su_env=Config.PRESERVE_SU_ENV,
219+
su_user=None,
220+
use_su_login=False,
221+
su_shell=None,
222+
preserve_su_env=False,
223223
# Sudo config
224-
sudo=Config.SUDO,
225-
sudo_user=Config.SUDO_USER,
226-
use_sudo_login=Config.USE_SUDO_LOGIN,
227-
use_sudo_password=Config.USE_SUDO_PASSWORD,
228-
preserve_sudo_env=Config.PRESERVE_SUDO_ENV,
224+
sudo=False,
225+
sudo_user=None,
226+
use_sudo_login=False,
227+
use_sudo_password=False,
228+
preserve_sudo_env=False,
229229
# Doas config
230-
doas=Config.DOAS,
231-
doas_user=Config.DOAS_USER,
230+
doas=False,
231+
doas_user=None,
232232
# Optional state object, used to decide if we print invalid auth arg warnings
233233
state=None,
234234
):

pyinfra/api/connectors/winrm.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import click
77

88
from pyinfra import logger
9-
from pyinfra.api import Config
109
from pyinfra.api.exceptions import ConnectError, PyinfraError
1110
from pyinfra.api.util import get_file_io, memoize, sha1_hash
1211

@@ -108,7 +107,7 @@ def run_shell_command(
108107
print_output=False,
109108
print_input=False,
110109
return_combined_output=False,
111-
shell_executable=Config.SHELL,
110+
shell_executable=None,
112111
**ignored_command_kwargs
113112
):
114113
'''

pyinfra/local.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
import pyinfra
99

10-
from . import logger, pseudo_state
10+
from . import logger, pseudo_config, pseudo_state
1111
from .api.connectors.util import run_local_process, split_combined_output
1212
from .api.exceptions import PyinfraError
1313

@@ -28,6 +28,8 @@ def include(filename):
2828

2929
logger.debug('Including local file: {0}'.format(filename))
3030

31+
config_state = pseudo_config.get_current_state()
32+
3133
try:
3234
# Fixes a circular import because `pyinfra.local` is really a CLI
3335
# only thing (so should be `pyinfra_cli.local`). It is kept here
@@ -38,6 +40,7 @@ def include(filename):
3840
from pyinfra_cli.util import exec_file
3941

4042
# Load any config defined in the file and setup like a @deploy
43+
# TODO: remove this in v2
4144
config_data = extract_file_config(filename)
4245
kwargs = {
4346
key.lower(): value
@@ -58,6 +61,9 @@ def include(filename):
5861
'Could not include local file: {0}:\n{1}'.format(filename, e),
5962
)
6063

64+
finally:
65+
pseudo_config.set_current_state(config_state)
66+
6167

6268
def shell(commands, splitlines=False, ignore_errors=False, print_output=False, print_input=False):
6369
'''

pyinfra/pseudo_modules.py

+17-1
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,15 @@ def __getattr__(self, key):
3535
return getattr(self._base_module, key)
3636
return getattr(self._module, key)
3737

38+
def __setattr__(self, key, value):
39+
if key in ('_module', '_base_module'):
40+
return super(PseudoModule, self).__setattr__(key, value)
41+
42+
if self._module is None:
43+
raise TypeError('Cannot assign to pseudo base module')
44+
45+
return setattr(self._module, key, value)
46+
3847
def __iter__(self):
3948
return iter(self._module)
4049

@@ -63,6 +72,12 @@ def isset(self):
6372
pyinfra.pseudo_state = pyinfra.state = \
6473
PseudoModule()
6574

75+
# The current deploy config
76+
pseudo_config = \
77+
sys.modules['pyinfra.pseudo_config'] = sys.modules['pyinfra.config'] = \
78+
pyinfra.pseudo_config = pyinfra.config = \
79+
PseudoModule()
80+
6681
# The current deploy inventory
6782
pseudo_inventory = \
6883
sys.modules['pyinfra.pseudo_inventory'] = sys.modules['pyinfra.inventory'] = \
@@ -77,8 +92,9 @@ def isset(self):
7792

7893

7994
def init_base_classes():
80-
from pyinfra.api import Host, Inventory, State
95+
from pyinfra.api import Config, Host, Inventory, State
8196

97+
pseudo_config.set_base(Config)
8298
pseudo_host.set_base(Host)
8399
pseudo_inventory.set_base(Inventory)
84100
pseudo_state.set_base(State)

pyinfra_cli/config.py

+9-40
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
import ast
22

3-
from os import path
4-
53
import six
64

7-
from pyinfra.api import Config
8-
9-
from .util import exec_file
5+
from pyinfra import logger
6+
from pyinfra.api.config import config_defaults
107

118

129
def extract_file_config(filename, config=None):
@@ -48,7 +45,13 @@ def extract_file_config(filename, config=None):
4845
# If one of the assignments matches a config variable (e.g. SUDO = True)
4946
# then assign it to the config object!
5047
for target in node.targets:
51-
if target.id.isupper() and hasattr(Config, target.id):
48+
if not isinstance(target, ast.Name):
49+
continue
50+
if target.id.isupper() and target.id in config_defaults:
51+
logger.warning((
52+
'file: {0}\n\tDefining config variables directly is deprecated, '
53+
'please use `config.{1} = {2}`.'
54+
).format(filename, target.id, repr(value)))
5255
config_data[target.id] = value
5356

5457
# If we have a config, update and exit
@@ -58,37 +61,3 @@ def extract_file_config(filename, config=None):
5861
return
5962

6063
return config_data
61-
62-
63-
def load_config(deploy_dir):
64-
'''
65-
Loads any local config.py file.
66-
'''
67-
68-
config = Config()
69-
config_filename = path.join(deploy_dir, 'config.py')
70-
71-
if path.exists(config_filename):
72-
extract_file_config(config_filename, config)
73-
74-
# Now execute the file to trigger loading of any hooks
75-
exec_file(config_filename)
76-
77-
return config
78-
79-
80-
def load_deploy_config(deploy_filename, config=None):
81-
'''
82-
Loads any local config overrides in the deploy file.
83-
'''
84-
85-
if not config:
86-
config = Config()
87-
88-
if not deploy_filename:
89-
return
90-
91-
if path.exists(deploy_filename):
92-
extract_file_config(deploy_filename, config)
93-
94-
return config

pyinfra_cli/main.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,11 @@
1212
from pyinfra import (
1313
__version__,
1414
logger,
15+
pseudo_config,
1516
pseudo_inventory,
1617
pseudo_state,
1718
)
18-
from pyinfra.api import State
19+
from pyinfra.api import Config, State
1920
from pyinfra.api.connect import connect_all, disconnect_all
2021
from pyinfra.api.exceptions import NoGroupError, PyinfraError
2122
from pyinfra.api.facts import (
@@ -29,7 +30,7 @@
2930
from pyinfra.api.util import get_kwargs_str
3031
from pyinfra.operations import server
3132

32-
from .config import load_config, load_deploy_config
33+
from .config import extract_file_config
3334
from .exceptions import (
3435
CliError,
3536
UnexpectedExternalError,
@@ -49,6 +50,7 @@
4950
print_support_info,
5051
)
5152
from .util import (
53+
exec_file,
5254
get_facts_and_args,
5355
get_operation_and_args,
5456
list_dirs_above_file,
@@ -346,8 +348,16 @@ def _main(
346348
if not quiet:
347349
click.echo('--> Loading config...', err=True)
348350

351+
config = Config()
352+
pseudo_config.set(config)
353+
349354
# Load up any config.py from the filesystem
350-
config = load_config(deploy_dir)
355+
config_filename = path.join(deploy_dir, 'config.py')
356+
if path.exists(config_filename):
357+
extract_file_config(config_filename, config) # TODO: remove this
358+
exec_file(config_filename)
359+
360+
# TODO: lock the config here, moving up from below when possible (v2)
351361

352362
# Make a copy before we overwrite
353363
original_operations = operations
@@ -409,9 +419,14 @@ def _main(
409419
pyinfra INVENTORY exec -- echo "hello world"
410420
pyinfra INVENTORY fact os [users]...'''.format(operations))
411421

412-
# Load any hooks/config from the deploy file
422+
# TODO: remove this - legacy load of any config variables from the top of
423+
# the first deploy file.
413424
if command == 'deploy':
414-
load_deploy_config(operations[0], config)
425+
extract_file_config(operations[0], config)
426+
427+
# Lock the current config, this allows us to restore this version after
428+
# executing deploy files that may alter them.
429+
config.lock_current_sate()
415430

416431
# Arg based config overrides
417432
if sudo:
@@ -577,6 +592,8 @@ def _main(
577592
for i, filename in enumerate(operations):
578593
logger.info('Loading: {0}'.format(click.style(filename, bold=True)))
579594
load_deploy_file(state, filename)
595+
# Remove any config changes introduced by the deploy file & any includes
596+
config.reset_locked_state()
580597

581598
# Operation w/optional args
582599
elif command == 'op':

0 commit comments

Comments
 (0)