From 14aba241116950e264c3c53dfc589666ea899dfc Mon Sep 17 00:00:00 2001 From: angeloudy Date: Wed, 3 Jan 2018 14:49:04 +1100 Subject: [PATCH 001/117] fix bytes-object required error in python 3 this fixes the following error for http.query ``` An exception occurred in this state: Traceback (most recent call last): File "/usr/local/lib/python3.6/site-packages/salt/state.py", line 1843, in call **cdata['kwargs']) File "/usr/local/lib/python3.6/site-packages/salt/loader.py", line 1795, in wrapper return f(*args, **kwargs) File "/usr/local/lib/python3.6/site-packages/salt/states/http.py", line 95, in query if match in data.get('text', ''): TypeError: a bytes-like object is required, not 'str' ``` --- salt/utils/http.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/utils/http.py b/salt/utils/http.py index 5e7bb531aa..63c4094340 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -531,6 +531,8 @@ def query(url, 'charset' in res_params and \ not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) + if isinstance(result_text, bytes): + result_text = result_text.decode('utf-8') ret['body'] = result_text if 'Set-Cookie' in result_headers.keys() and cookies is not None: result_cookies = parse_cookie_header(result_headers['Set-Cookie']) From 51ba3c135bdb71091f8957b49e603bb4261fbf7c Mon Sep 17 00:00:00 2001 From: angeloudy Date: Mon, 15 Jan 2018 12:54:34 +1100 Subject: [PATCH 002/117] Update http.py add python3 check --- salt/utils/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 63c4094340..08fa376a90 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -532,7 +532,8 @@ def query(url, not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) if isinstance(result_text, bytes): - result_text = result_text.decode('utf-8') + if six.PY3 and isinstance(result_text, bytes): + result_text = result_text.decode('utf-8') ret['body'] = result_text if 'Set-Cookie' in result_headers.keys() and cookies is not None: result_cookies = parse_cookie_header(result_headers['Set-Cookie']) From 908c040ac36c43d2e898a60595cf9108dc35db3b Mon Sep 17 00:00:00 2001 From: angeloudy Date: Mon, 15 Jan 2018 12:56:21 +1100 Subject: [PATCH 003/117] Update http.py add python3 checks --- salt/utils/http.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 08fa376a90..14b3c60ec4 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -430,7 +430,8 @@ def query(url, not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) if isinstance(result_text, bytes): - result_text = result.body.decode('utf-8') + if six.PY3 and isinstance(result_text, bytes): + result_text = result.body.decode('utf-8') ret['body'] = result_text else: # Tornado From 269514683fd6c1f192b46afd89610f816fe584cf Mon Sep 17 00:00:00 2001 From: angeloudy Date: Mon, 15 Jan 2018 13:06:33 +1100 Subject: [PATCH 004/117] Update http.py remove duplicate checks --- salt/utils/http.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 14b3c60ec4..954fdd9785 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -429,8 +429,7 @@ def query(url, 'charset' in res_params and \ not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) - if isinstance(result_text, bytes): - if six.PY3 and isinstance(result_text, bytes): + if six.PY3 and isinstance(result_text, bytes): result_text = result.body.decode('utf-8') ret['body'] = result_text else: @@ -532,8 +531,7 @@ def query(url, 'charset' in res_params and \ not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) - if isinstance(result_text, bytes): - if six.PY3 and isinstance(result_text, bytes): + if six.PY3 and isinstance(result_text, bytes): result_text = result_text.decode('utf-8') ret['body'] = result_text if 'Set-Cookie' in result_headers.keys() and cookies is not None: From 485d777ac0726d6c7b7100bb33d71cf6ac2c42c6 Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 23 Feb 2018 09:58:31 -0500 Subject: [PATCH 005/117] Add team-suse to CODEOWNERS file for zypper files This addition will enable automatic reviews from team-suse for PRs that touch the zypper files. --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 29288c6efe..2f5b391028 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -52,6 +52,9 @@ salt/**/thin.py @saltstack/team-ssh # Team State salt/state.py @saltstack/team-state +# Team SUSE +salt/**/*zypper* @saltstack/team-suse + # Team Transport salt/transport/ @saltstack/team-transport salt/utils/zeromq.py @saltstack/team-transport From 35c7b7b0d30148e3f6c6169878169e27514971a9 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 27 Feb 2018 13:53:43 -0500 Subject: [PATCH 006/117] Add btrfs, xfs, yumpkg, and kubernetes file to team-suse --- .github/CODEOWNERS | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2f5b391028..a62d27d7c1 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -53,7 +53,11 @@ salt/**/thin.py @saltstack/team-ssh salt/state.py @saltstack/team-state # Team SUSE -salt/**/*zypper* @saltstack/team-suse +salt/**/*btrfs* @saltstack/team-suse +salt/**/*kubernetes* @saltstack/team-suse +salt/**/*xfs* @saltstack/team-suse +salt/**/*yumpkg* @saltstack/team-suse +salt/**/*zypper* @saltstack/team-suse # Team Transport salt/transport/ @saltstack/team-transport From 13a295a3b79a9ed64b4034b4b35c4af44b8e4f54 Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 28 Feb 2018 09:58:46 -0500 Subject: [PATCH 007/117] Add *pkg* and *snapper* to team-suse Since *pkg* was specified, we don't specifially need *yumpkg* anymore. --- .github/CODEOWNERS | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a62d27d7c1..124054ded2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -55,8 +55,9 @@ salt/state.py @saltstack/team-state # Team SUSE salt/**/*btrfs* @saltstack/team-suse salt/**/*kubernetes* @saltstack/team-suse +salt/**/*pkg* @saltstack/team-suse +salt/**/*snapper* @saltstack/team-suse salt/**/*xfs* @saltstack/team-suse -salt/**/*yumpkg* @saltstack/team-suse salt/**/*zypper* @saltstack/team-suse # Team Transport From bcf8b19566f3e71b1fde4d3c2d797f781c41f724 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 2 Mar 2018 13:04:48 -0700 Subject: [PATCH 008/117] Update the installer build Fixes some issues with Py3 not building Brings the 2 build_env files more in line with each other Fixes some typos in the build_env scripts Removes the need for seperate req files for Py2 and Py3 Setup.py uses the windows req file on Windows Removes the need to install pycrypto and pyyaml in setup.py. They are now handled in the req.txt Uses pypi resources instead of downloading .whl files and .exes for dependencies Updates nssm version to address the issue in #46192 Removes nssm from buildenv and downloads 32 and 64 bit versions of nssm in build_pkg.bat Removes the AppNoConsole=1 setting from the nsis script since it is not needed with the new version of nssm --- pkg/windows/build_env_2.ps1 | 50 +++------ pkg/windows/build_env_3.ps1 | 28 ++--- pkg/windows/build_pkg.bat | 17 +++ pkg/windows/buildenv/nssm.exe | Bin 294912 -> 0 bytes pkg/windows/installer/Salt-Minion-Setup.nsi | 1 - pkg/windows/{req_base.txt => req.txt} | 4 + pkg/windows/req_2.txt | 4 - pkg/windows/req_3.txt | 5 - setup.py | 109 ++------------------ 9 files changed, 49 insertions(+), 169 deletions(-) delete mode 100644 pkg/windows/buildenv/nssm.exe rename pkg/windows/{req_base.txt => req.txt} (91%) delete mode 100644 pkg/windows/req_2.txt delete mode 100644 pkg/windows/req_3.txt diff --git a/pkg/windows/build_env_2.ps1 b/pkg/windows/build_env_2.ps1 index b186517812..42f8305e0e 100644 --- a/pkg/windows/build_env_2.ps1 +++ b/pkg/windows/build_env_2.ps1 @@ -206,7 +206,7 @@ if ( ! [bool]$Env:SALT_PIP_LOCAL_CACHE) { Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip download --dest $Env:SALT_PIP_LOCAL_CACHE -r $($script_path)\req_pip.txt" "pip download" } Write-Output " reading from local pip cache $Env:SALT_PIP_LOCAL_CACHE" - Write-Output " If a (new) ressource is missing, please delete all files in this cache, go online and repeat" + Write-Output " If a (new) resource is missing, please delete all files in this cache, go online and repeat" Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_PIP_LOCAL_CACHE -r $($script_path)\req_pip.txt" "pip install" } @@ -218,21 +218,26 @@ Write-Output " ----------------------------------------------------------------" Write-Output " - $script_name :: Installing pypi resources using pip . . ." Write-Output " ----------------------------------------------------------------" if ( ! [bool]$Env:SALT_REQ_LOCAL_CACHE) { - Start_Process_and_test_exitcode "$($ini['Settings']['Scripts2Dir'])\pip.exe" "--no-cache-dir install -r $($script_path)\req_2.txt" "pip install" + Start_Process_and_test_exitcode "$($ini['Settings']['Scripts2Dir'])\pip.exe" "--no-cache-dir install -r $($script_path)\req.txt" "pip install" } else { if ( (Get-ChildItem $Env:SALT_REQ_LOCAL_CACHE | Measure-Object).Count -eq 0 ) { # folder empty - Write-Output " pip download from req_2.txt into empty local cache SALT_REQ $Env:SALT_REQ_LOCAL_CACHE" - Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip download --dest $Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req_2.txt" "pip download" + Write-Output " pip download from req.txt into empty local cache SALT_REQ $Env:SALT_REQ_LOCAL_CACHE" + Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip download --dest $Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req.txt" "pip download" } Write-Output " reading from local pip cache $Env:SALT_REQ_LOCAL_CACHE" - Write-Output " If a (new) ressource is missing, please delete all files in this cache, go online and repeat" - Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req_2.txt" "pip install" + Write-Output " If a (new) resource is missing, please delete all files in this cache, go online and repeat" + Start_Process_and_test_exitcode "$($ini['Settings']['Python2Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req.txt" "pip install" } #============================================================================== -# Move PyWin32 DLL's to site-packages\win32 +# Cleaning Up PyWin32 #============================================================================== +Write-Output " ----------------------------------------------------------------" +Write-Output " - $script_name :: Cleaning Up PyWin32 . . ." +Write-Output " ----------------------------------------------------------------" + +# Move DLL's to Python Root Write-Output " - $script_name :: Moving PyWin32 DLLs . . ." Move-Item "$($ini['Settings']['SitePkgs2Dir'])\pywin32_system32\*.dll" "$($ini['Settings']['SitePkgs2Dir'])\win32" -Force @@ -248,37 +253,6 @@ Remove-Item "$($ini['Settings']['SitePkgs2Dir'])\pythonwin" -Force -Recurse Write-Output " - $script_name :: Removing PyWin32 scripts . . ." Remove-Item "$($ini['Settings']['Scripts2Dir'])\pywin32_*" -Force -Recurse -#============================================================================== -# Install PyYAML with CLoader -# This has to be a compiled binary to get the CLoader -#============================================================================== -Write-Output " ----------------------------------------------------------------" -Write-Output " - $script_name :: Installing PyYAML . . ." -Write-Output " ----------------------------------------------------------------" -# Download -$file = "$($ini[$bitPrograms]['PyYAML2'])" -$url = "$($ini['Settings']['SaltRepo'])/$bitFolder/$file" -$file = "$($ini['Settings']['DownloadDir'])\$bitFolder\$file" -DownloadFileWithProgress $url $file - -# Install -Start_Process_and_test_exitcode "$($ini['Settings']['Scripts2Dir'])\easy_install.exe" "-Z $file " "easy_install PyYAML" - -#============================================================================== -# Install PyCrypto from wheel file -#============================================================================== -Write-Output " ----------------------------------------------------------------" -Write-Output " - $script_name :: Installing PyCrypto . . ." -Write-Output " ----------------------------------------------------------------" -# Download -$file = "$($ini[$bitPrograms]['PyCrypto2'])" -$url = "$($ini['Settings']['SaltRepo'])/$bitFolder/$file" -$file = "$($ini['Settings']['DownloadDir'])\$bitFolder\$file" -DownloadFileWithProgress $url $file - -# Install -Start_Process_and_test_exitcode "$($ini['Settings']['Scripts2Dir'])\pip.exe" "install --no-index --find-links=$($ini['Settings']['DownloadDir']) $file " "pip install PyCrypto" - #============================================================================== # Copy DLLs to Python Directory #============================================================================== diff --git a/pkg/windows/build_env_3.ps1 b/pkg/windows/build_env_3.ps1 index 0dcbafd996..2c11e2d524 100644 --- a/pkg/windows/build_env_3.ps1 +++ b/pkg/windows/build_env_3.ps1 @@ -191,7 +191,7 @@ If (!($Path.ToLower().Contains("$($ini['Settings']['Scripts3Dir'])".ToLower()))) #============================================================================== # Update PIP and SetupTools -# caching depends on environmant variable SALT_PIP_LOCAL_CACHE +# caching depends on environment variable SALT_PIP_LOCAL_CACHE #============================================================================== Write-Output " ----------------------------------------------------------------" Write-Output " - $script_name :: Updating PIP and SetupTools . . ." @@ -206,44 +206,36 @@ if ( ! [bool]$Env:SALT_PIP_LOCAL_CACHE) { Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip download --dest $Env:SALT_PIP_LOCAL_CACHE -r $($script_path)\req_pip.txt" "pip download" } Write-Output " reading from local pip cache $Env:SALT_PIP_LOCAL_CACHE" - Write-Output " If a (new) ressource is missing, please delete all files in this cache, go online and repeat" + Write-Output " If a (new) resource is missing, please delete all files in this cache, go online and repeat" Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_PIP_LOCAL_CACHE -r $($script_path)\req_pip.txt" "pip install" } #============================================================================== # Install pypi resources using pip -# caching depends on environmant variable SALT_REQ_LOCAL_CACHE +# caching depends on environment variable SALT_REQ_LOCAL_CACHE #============================================================================== Write-Output " ----------------------------------------------------------------" Write-Output " - $script_name :: Installing pypi resources using pip . . ." Write-Output " ----------------------------------------------------------------" if ( ! [bool]$Env:SALT_REQ_LOCAL_CACHE) { - Start_Process_and_test_exitcode "$($ini['Settings']['Scripts3Dir'])\pip.exe" "--no-cache-dir install -r $($script_path)\req_3.txt" "pip install" + Start_Process_and_test_exitcode "$($ini['Settings']['Scripts3Dir'])\pip.exe" "--no-cache-dir install -r $($script_path)\req.txt" "pip install" } else { if ( (Get-ChildItem $Env:SALT_REQ_LOCAL_CACHE | Measure-Object).Count -eq 0 ) { # folder empty - Write-Output " pip download from req_3.txt into empty local cache SALT_REQ $Env:SALT_REQ_LOCAL_CACHE" - Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip download --dest $Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req_3.txt" "pip download" + Write-Output " pip download from req.txt into empty local cache SALT_REQ $Env:SALT_REQ_LOCAL_CACHE" + Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip download --dest $Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req.txt" "pip download" } Write-Output " reading from local pip cache $Env:SALT_REQ_LOCAL_CACHE" - Write-Output " If a (new) ressource is missing, please delete all files in this cache, go online and repeat" - Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req_3.txt" "pip install" + Write-Output " If a (new) resource is missing, please delete all files in this cache, go online and repeat" + Start_Process_and_test_exitcode "$($ini['Settings']['Python3Dir'])\python.exe" "-m pip install --no-index --find-links=$Env:SALT_REQ_LOCAL_CACHE -r $($script_path)\req.txt" "pip install" } #============================================================================== -# Install PyWin32 from wheel file +# Cleaning Up PyWin32 #============================================================================== Write-Output " ----------------------------------------------------------------" -Write-Output " - $script_name :: Installing PyWin32 . . ." +Write-Output " - $script_name :: Cleaning Up PyWin32 . . ." Write-Output " ----------------------------------------------------------------" -# Download -$file = "$($ini[$bitPrograms]['PyWin323'])" -$url = "$($ini['Settings']['SaltRepo'])/$bitFolder/$file" -$file = "$($ini['Settings']['DownloadDir'])\$bitFolder\$file" -DownloadFileWithProgress $url $file - -# Install -Start_Process_and_test_exitcode "$($ini['Settings']['Scripts3Dir'])\pip.exe" "install --no-index --find-links=$($ini['Settings']['DownloadDir']) $file " "pip install PyWin32" # Move DLL's to Python Root Write-Output " - $script_name :: Moving PyWin32 DLLs . . ." diff --git a/pkg/windows/build_pkg.bat b/pkg/windows/build_pkg.bat index 2aa33c0b7d..99976760d6 100644 --- a/pkg/windows/build_pkg.bat +++ b/pkg/windows/build_pkg.bat @@ -113,6 +113,23 @@ xcopy /Q /Y "%SrcDir%\conf\master" "%CnfDir%\" xcopy /Q /Y "%SrcDir%\conf\minion" "%CnfDir%\" @echo. +@echo Copying NSSM to buildenv +@echo ---------------------------------------------------------------------- +:: Make sure the "prereq" directory exists +If NOT Exist "%PreDir%" mkdir "%PreDir%" + +:: Set the location of the nssm to download +Set Url64="https://repo.saltstack.com/windows/dependencies/64/nssm-2.24-101-g897c7ad.exe" +Set Url32="https://repo.saltstack.com/windows/dependencies/32/nssm-2.24-101-g897c7ad.exe" + +:: Check for 64 bit by finding the Program Files (x86) directory +If Defined ProgramFiles(x86) ( + powershell -ExecutionPolicy RemoteSigned -File download_url_file.ps1 -url "%Url64%" -file "%BldDir%\nssm.exe" +) Else ( + powershell -ExecutionPolicy RemoteSigned -File download_url_file.ps1 -url "%Url32%" -file "%BldDir%\nssm.exe" +) +@echo. + @echo Copying VCRedist to Prerequisites @echo ---------------------------------------------------------------------- :: Make sure the "prereq" directory exists diff --git a/pkg/windows/buildenv/nssm.exe b/pkg/windows/buildenv/nssm.exe deleted file mode 100644 index 8faee45b7a619de41f6463ecf170391467deb199..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 294912 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 Date: Fri, 2 Mar 2018 18:33:56 -0700 Subject: [PATCH 009/117] Fix issues with the DSC module Adds the `ignore_retcode` option to the _pshell helper function The _pshell helper function will either return a dict or raise an error When powershell returns an empty string, makes it an empty dict Adds some `log.info` commands for easier debugging Fixes some erroneous CLI Examples Handles situations where _shell returns an empty dict Handles situations where a powershell command completed successfully, but returned invalid JSON `dsc.get_config` returns a cleaner dict Adds a `dsc.remove_config` function with the option to reset the dsc environment entirely Adds a `dsc.restore_config` function to rollback to a previous config --- salt/modules/win_dsc.py | 244 ++++++++++++++++++++++++++++++++-------- 1 file changed, 198 insertions(+), 46 deletions(-) diff --git a/salt/modules/win_dsc.py b/salt/modules/win_dsc.py index 90aef68630..eec2eed952 100644 --- a/salt/modules/win_dsc.py +++ b/salt/modules/win_dsc.py @@ -39,33 +39,35 @@ def __virtual__(): ''' # Verify Windows if not salt.utils.is_windows(): - log.debug('Module DSC: Only available on Windows systems') - return False, 'Module DSC: Only available on Windows systems' + log.debug('DSC: Only available on Windows systems') + return False, 'DSC: Only available on Windows systems' # Verify PowerShell powershell_info = __salt__['cmd.shell_info']('powershell') if not powershell_info['installed']: - log.debug('Module DSC: Requires PowerShell') - return False, 'Module DSC: Requires PowerShell' + log.debug('DSC: Requires PowerShell') + return False, 'DSC: Requires PowerShell' # Verify PowerShell 5.0 or greater if salt.utils.compare_versions(powershell_info['version'], '<', '5.0'): - log.debug('Module DSC: Requires PowerShell 5 or later') - return False, 'Module DSC: Requires PowerShell 5 or later' + log.debug('DSC: Requires PowerShell 5 or later') + return False, 'DSC: Requires PowerShell 5 or later' return __virtualname__ -def _pshell(cmd, cwd=None, json_depth=2): +def _pshell(cmd, cwd=None, json_depth=2, ignore_retcode=False): ''' Execute the desired PowerShell command and ensure that it returns data - in json format and load that into python + in json format and load that into python. Either return a dict or raise a + CommandExecutionError. ''' if 'convertto-json' not in cmd.lower(): cmd = '{0} | ConvertTo-Json -Depth {1}'.format(cmd, json_depth) log.debug('DSC: {0}'.format(cmd)) results = __salt__['cmd.run_all']( - cmd, shell='powershell', cwd=cwd, python_shell=True) + cmd, shell='powershell', cwd=cwd, python_shell=True, + ignore_retcode=ignore_retcode) if 'pid' in results: del results['pid'] @@ -75,12 +77,17 @@ def _pshell(cmd, cwd=None, json_depth=2): raise CommandExecutionError( 'Issue executing PowerShell {0}'.format(cmd), info=results) + # Sometimes Powershell returns an empty string, which isn't valid JSON + if results['stdout'] == '': + results['stdout'] = '{}' + try: ret = json.loads(results['stdout'], strict=False) except ValueError: raise CommandExecutionError( 'No JSON results from PowerShell', info=results) + log.info('DSC: Returning "{0}"'.format(ret)) return ret @@ -98,8 +105,8 @@ def run_config(path, script, the desired configuration can be applied by passing the name in the ``config`` option. - This command would be the equivalent of running ``dsc.compile_config`` and - ``dsc.apply_config`` separately. + This command would be the equivalent of running ``dsc.compile_config`` + followed by ``dsc.apply_config``. Args: @@ -141,7 +148,7 @@ def run_config(path, Default is 'base' Returns: - bool: True if successfully compiled and applied, False if not + bool: True if successfully compiled and applied, otherwise False CLI Example: @@ -149,13 +156,13 @@ def run_config(path, .. code-block:: bash - salt '*' dsc.compile_apply_config C:\\DSC\\WebsiteConfig.ps1 + salt '*' dsc.run_config C:\\DSC\\WebsiteConfig.ps1 To cache a config script to the system from the master and compile it: .. code-block:: bash - salt '*' dsc.compile_apply_config C:\\DSC\\WebsiteConfig.ps1 salt://dsc/configs/WebsiteConfig.ps1 + salt '*' dsc.run_config C:\\DSC\\WebsiteConfig.ps1 salt://dsc/configs/WebsiteConfig.ps1 ''' ret = compile_config(path=path, source=source, @@ -240,31 +247,31 @@ def compile_config(path, salt '*' dsc.compile_config C:\\DSC\\WebsiteConfig.ps1 salt://dsc/configs/WebsiteConfig.ps1 ''' if source: - log.info('Caching {0}'.format(source)) + log.info('DSC: Caching {0}'.format(source)) cached_files = __salt__['cp.get_file'](path=source, dest=path, saltenv=salt_env, makedirs=True) if not cached_files: error = 'Failed to cache {0}'.format(source) - log.error(error) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) if config_data_source: - log.info('Caching {0}'.format(config_data_source)) + log.info('DSC: Caching {0}'.format(config_data_source)) cached_files = __salt__['cp.get_file'](path=config_data_source, dest=config_data, saltenv=salt_env, makedirs=True) if not cached_files: error = 'Failed to cache {0}'.format(config_data_source) - log.error(error) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) # Make sure the path exists if not os.path.exists(path): - error = '"{0} not found.'.format(path) - log.error(error) + error = '"{0}" not found'.format(path) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) if config_name is None: @@ -290,10 +297,11 @@ def compile_config(path, if ret: # Script compiled, return results if ret.get('Exists'): - log.info('DSC Compile Config: {0}'.format(ret)) + log.info('DSC: Compile Config: {0}'.format(ret)) return ret - # Run the script and run the compile command + # If you get to this point, the script did not contain a compile command + # dot source the script to compile the state and generate the mof file cmd = ['.', path] if script_parameters: cmd.append(script_parameters) @@ -311,12 +319,12 @@ def compile_config(path, if ret: # Script compiled, return results if ret.get('Exists'): - log.info('DSC Compile Config: {0}'.format(ret)) + log.info('DSC: Compile Config: {0}'.format(ret)) return ret error = 'Failed to compile config: {0}'.format(path) error += '\nReturned: {0}'.format(ret) - log.error('DSC Compile Config: {0}'.format(error)) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) @@ -348,13 +356,13 @@ def apply_config(path, source=None, salt_env='base'): .. code-block:: bash - salt '*' dsc.run_config C:\\DSC\\WebSiteConfiguration + salt '*' dsc.apply_config C:\\DSC\\WebSiteConfiguration To cache a configuration from the master and apply it: .. code-block:: bash - salt '*' dsc.run_config C:\\DSC\\WebSiteConfiguration salt://dsc/configs/WebSiteConfiguration + salt '*' dsc.apply_config C:\\DSC\\WebSiteConfiguration salt://dsc/configs/WebSiteConfiguration ''' # If you're getting an error along the lines of "The client cannot connect @@ -368,38 +376,35 @@ def apply_config(path, source=None, salt_env='base'): if path_name.lower() != source_name.lower(): # Append the Source name to the Path path = '{0}\\{1}'.format(path, source_name) - log.debug('{0} appended to the path.'.format(source_name)) + log.debug('DSC: {0} appended to the path'.format(source_name)) # Destination path minus the basename dest_path = os.path.dirname(os.path.normpath(path)) - log.info('Caching {0}'.format(source)) + log.info('DSC: Caching {0}'.format(source)) cached_files = __salt__['cp.get_dir'](source, dest_path, salt_env) if not cached_files: error = 'Failed to copy {0}'.format(source) - log.error(error) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) else: config = os.path.dirname(cached_files[0]) # Make sure the path exists if not os.path.exists(config): - error = '{0} not found.'.format(config) - log.error(error) + error = '{0} not found'.format(config) + log.error('DSC: {0}'.format(error)) raise CommandExecutionError(error) # Run the DSC Configuration # Putting quotes around the parameter protects against command injection cmd = 'Start-DscConfiguration -Path "{0}" -Wait -Force'.format(config) - ret = _pshell(cmd) - - if ret is False: - raise CommandExecutionError('Apply Config Failed: {0}'.format(path)) + _pshell(cmd) cmd = '$status = Get-DscConfigurationStatus; $status.Status' ret = _pshell(cmd) - log.info('DSC Apply Config: {0}'.format(ret)) + log.info('DSC: Apply Config: {0}'.format(ret)) - return ret == 'Success' + return ret == 'Success' or ret == {} def get_config(): @@ -409,15 +414,153 @@ def get_config(): Returns: dict: A dictionary representing the DSC Configuration on the machine + Raises: + CommandExecutionError: On failure + CLI Example: .. code-block:: bash salt '*' dsc.get_config ''' - cmd = 'Get-DscConfiguration | ' \ - 'Select-Object * -ExcludeProperty Cim*' - return _pshell(cmd) + cmd = 'Get-DscConfiguration | Select-Object * -ExcludeProperty Cim*' + + try: + raw_config = _pshell(cmd, ignore_retcode=True) + except CommandExecutionError as exc: + if 'Current configuration does not exist' in exc.info['stderr']: + raise CommandExecutionError('Not Configured') + raise + + config = dict() + if raw_config: + # Get DSC Configuration Name + if 'ConfigurationName' in raw_config[0]: + config[raw_config[0]['ConfigurationName']] = {} + # Add all DSC Configurations by ResourceId + for item in raw_config: + config[item['ConfigurationName']][item['ResourceId']] = {} + for key in item: + if key not in ['ConfigurationName', 'ResourceId']: + config[item['ConfigurationName']][item['ResourceId']][key] = item[key] + + return config + + +def remove_config(reset=False): + ''' + Remove the current DSC Configuration. Removes current, pending, and previous + dsc configurations. + + .. versionadded:: 2017.7.5 + + Args: + reset (bool): + Attempts to reset the DSC configuration by removing the following + from ``C:\\Windows\\System32\\Configuration``: + + - File: DSCStatusHistory.mof + - File: DSCEngineCache.mof + - Dir: ConfigurationStatus + + Default is False + + .. warning:: + ``remove_config`` may fail to reset the DSC environment if any + of the files in the ``ConfigurationStatus`` directory. If you + wait a few minutes and run again, it may complete successfully. + + Returns: + bool: True if successful + + Raises: + CommandExecutionError: On failure + + CLI Example: + + .. code-block:: bash + + salt '*' dsc.remove_config True + ''' + # Stopping a running config (not likely to occur) + cmd = 'Stop-DscConfiguration' + log.info('DSC: Stopping Running Configuration') + try: + _pshell(cmd) + except CommandExecutionError as exc: + if exc.info['retcode'] != 0: + raise CommandExecutionError('Failed to Stop DSC Configuration', + info=exc.info) + log.info('DSC: {0}'.format(exc.info['stdout'])) + + # Remove configuration files + cmd = 'Remove-DscConfigurationDocument -Stage Current, Pending, Previous ' \ + '-Force' + log.info('DSC: Removing Configuration') + try: + _pshell(cmd) + except CommandExecutionError as exc: + if exc.info['retcode'] != 0: + raise CommandExecutionError('Failed to remove DSC Configuration', + info=exc.info) + log.info('DSC: {0}'.format(exc.info['stdout'])) + + if not reset: + return True + + def _remove_fs_obj(path): + if os.path.exists(path): + log.info('DSC: Removing {0}'.format(path)) + if not __salt__['file.remove'](path): + error = 'Failed to remove {0}'.format(path) + log.error('DSC: {0}'.format(error)) + raise CommandExecutionError(error) + + dsc_config_dir = '{0}\\System32\\Configuration' \ + ''.format(os.getenv('SystemRoot', 'C:\\Windows')) + + # Remove History + _remove_fs_obj('{0}\\DSCStatusHistory.mof'.format(dsc_config_dir)) + + # Remove Engine Cache + _remove_fs_obj('{0}\\DSCEngineCache.mof'.format(dsc_config_dir)) + + # Remove Status Directory + _remove_fs_obj('{0}\\ConfigurationStatus'.format(dsc_config_dir)) + + return True + + +def restore_config(): + ''' + Reapplies the previous configuration. + + .. versionadded:: 2017.7.5 + + .. note:: + The current configuration will be come the previous configuration. If + run a second time back-to-back it is like toggling between two configs. + + Returns: + bool: True if successfully restored + + Raises: + CommandExecutionError: On failure + + CLI Example: + + .. code-block:: bash + + salt '*' dsc.restore_config + ''' + cmd = 'Restore-DscConfiguration' + try: + _pshell(cmd, ignore_retcode=True) + except CommandExecutionError as exc: + if 'A previous configuration does not exist' in exc.info['stderr']: + raise CommandExecutionError('Previous Configuration Not Found') + raise + return True def test_config(): @@ -433,9 +576,13 @@ def test_config(): salt '*' dsc.test_config ''' - cmd = 'Test-DscConfiguration *>&1' - ret = _pshell(cmd) - return ret == 'True' + cmd = 'Test-DscConfiguration' + try: + _pshell(cmd, ignore_retcode=True) + except CommandExecutionError as exc: + if 'Current configuration does not exist' in exc.info['stderr']: + raise CommandExecutionError('Not Configured') + raise def get_config_status(): @@ -456,7 +603,12 @@ def get_config_status(): 'Select-Object -Property HostName, Status, MetaData, ' \ '@{Name="StartDate";Expression={Get-Date ($_.StartDate) -Format g}}, ' \ 'Type, Mode, RebootRequested, NumberofResources' - return _pshell(cmd) + try: + return _pshell(cmd, ignore_retcode=True) + except CommandExecutionError as exc: + if 'No status information available' in exc.info['stderr']: + raise CommandExecutionError('Not Configured') + raise def get_lcm_config(): @@ -638,8 +790,8 @@ def set_lcm_config(config_mode=None, ret = __salt__['cmd.run_all'](cmd, shell='powershell', python_shell=True) __salt__['file.remove'](r'{0}\SaltConfig'.format(temp_dir)) if not ret['retcode']: - log.info('LCM config applied successfully') + log.info('DSC: LCM config applied successfully') return True else: - log.error('Failed to apply LCM config. Error {0}'.format(ret)) + log.error('DSC: Failed to apply LCM config. Error {0}'.format(ret)) return False From c493ced4154c07460d9130784ef6493784f8fa84 Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Sat, 3 Mar 2018 13:41:56 +0100 Subject: [PATCH 010/117] add warning about future config option change --- salt/modules/kubernetes.py | 7 +++++++ salt/states/kubernetes.py | 4 ++++ 2 files changed, 11 insertions(+) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 12d965bb4d..850f4f18b9 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -31,6 +31,13 @@ In case both are provided the `file` entry is prefered. salt '*' kubernetes.nodes api_url=http://k8s-api-server:port api_user=myuser api_password=pass .. versionadded: 2017.7.0 + +.. warning:: + + Configuration options will change in Flourine. All options above will be replaced by: + + - kubernetes.kubeconfig or kubernetes.kubeconfig-data + - kubernetes.context ''' # Import Python Futures diff --git a/salt/states/kubernetes.py b/salt/states/kubernetes.py index bc62da1ab7..f14061695a 100644 --- a/salt/states/kubernetes.py +++ b/salt/states/kubernetes.py @@ -6,6 +6,10 @@ Manage kubernetes resources as salt states NOTE: This module requires the proper pillar values set. See salt.modules.kubernetes for more information. +.. warning:: + + Configuration options will change in Flourine. + The kubernetes module is used to manage different kubernetes resources. From 5008c53c4484241057c7931fa13df451a4bd16b1 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Mon, 5 Mar 2018 14:16:40 +0100 Subject: [PATCH 011/117] Fix ValueError for template in AppsV1beta1DeploymentSpec Instantiating AppsV1beta1DeploymentSpec without specifying a template will raise: ValueError: Invalid value for `template`, must not be `None` Therefore directly specifying the template when instantiating AppsV1beta1DeploymentSpec and use '' as default template. This fixes the test_create_deployments test case (fixes #46329). Signed-off-by: Benjamin Drung --- salt/modules/kubernetes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 12d965bb4d..bd2221863b 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -1461,7 +1461,7 @@ def __dict_to_deployment_spec(spec): ''' Converts a dictionary into kubernetes AppsV1beta1DeploymentSpec instance. ''' - spec_obj = AppsV1beta1DeploymentSpec() + spec_obj = AppsV1beta1DeploymentSpec(template=spec.get('template', '')) for key, value in iteritems(spec): if hasattr(spec_obj, key): setattr(spec_obj, key, value) From 7161f4d4df72d0e0c51914425210aff538ebf185 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Mon, 5 Mar 2018 11:11:46 -0700 Subject: [PATCH 012/117] fix listen to be able to handle names Before this, multiple names would have the same stateid and module, so we need the actual name to distinguish them ``` test: pkg.latest: - names: - nginx - cronie file.touch: - name: /tmp/test service.running: - names: - nginx - crond - listen: - file: test ``` Only one, nginx or crond would be restarted (should be the last one in the list), because of how the ordered dict did the lookup. --- salt/state.py | 53 +++++++++++++++++++++++++++------------------------ 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/salt/state.py b/salt/state.py index 3bf6927a11..231898b6cc 100644 --- a/salt/state.py +++ b/salt/state.py @@ -2489,14 +2489,13 @@ class State(object): listeners = [] crefs = {} for chunk in chunks: - crefs[(chunk['state'], chunk['name'])] = chunk - crefs[(chunk['state'], chunk['__id__'])] = chunk + crefs[(chunk['state'], chunk['__id__'], chunk['name'])] = chunk if 'listen' in chunk: - listeners.append({(chunk['state'], chunk['__id__']): chunk['listen']}) + listeners.append({(chunk['state'], chunk['__id__'], chunk['name']): chunk['listen']}) if 'listen_in' in chunk: for l_in in chunk['listen_in']: for key, val in six.iteritems(l_in): - listeners.append({(key, val): [{chunk['state']: chunk['__id__']}]}) + listeners.append({(key, val, 'lookup'): [{chunk['state']: chunk['__id__']}]}) mod_watchers = [] errors = {} for l_dict in listeners: @@ -2505,7 +2504,7 @@ class State(object): if not isinstance(listen_to, dict): continue for lkey, lval in six.iteritems(listen_to): - if (lkey, lval) not in crefs: + if not any(lkey == cref[0] and lval in cref for cref in crefs): rerror = {_l_tag(lkey, lval): { 'comment': 'Referenced state {0}: {1} does not exist'.format(lkey, lval), @@ -2515,27 +2514,31 @@ class State(object): }} errors.update(rerror) continue - to_tag = _gen_tag(crefs[(lkey, lval)]) - if to_tag not in running: - continue - if running[to_tag]['changes']: - if key not in crefs: - rerror = {_l_tag(key[0], key[1]): - {'comment': 'Referenced state {0}: {1} does not exist'.format(key[0], key[1]), - 'name': 'listen_{0}:{1}'.format(key[0], key[1]), - 'result': False, - 'changes': {}}} - errors.update(rerror) + to_tags = [ + _gen_tag(data) for cref, data in six.iteritems(crefs) if lkey == cref[0] and lval in cref + ] + for to_tag in to_tags: + if to_tag not in running: continue - chunk = crefs[key] - low = chunk.copy() - low['sfun'] = chunk['fun'] - low['fun'] = 'mod_watch' - low['__id__'] = 'listener_{0}'.format(low['__id__']) - for req in STATE_REQUISITE_KEYWORDS: - if req in low: - low.pop(req) - mod_watchers.append(low) + if running[to_tag]['changes']: + if key not in crefs: + rerror = {_l_tag(key[0], key[1]): + {'comment': 'Referenced state {0}: {1} does not exist'.format(key[0], key[1]), + 'name': 'listen_{0}:{1}'.format(key[0], key[1]), + 'result': False, + 'changes': {}}} + errors.update(rerror) + continue + chunks = [data for cref, data in six.iteritems(crefs) if key == cref] + for chunk in chunks: + low = chunk.copy() + low['sfun'] = chunk['fun'] + low['fun'] = 'mod_watch' + low['__id__'] = 'listener_{0}'.format(low['__id__']) + for req in STATE_REQUISITE_KEYWORDS: + if req in low: + low.pop(req) + mod_watchers.append(low) ret = self.call_chunks(mod_watchers) running.update(ret) for err in errors: From 3f8e0db572eb0710f49729290798a35b79501581 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Mon, 5 Mar 2018 11:31:32 -0700 Subject: [PATCH 013/117] let listen_in work with names --- salt/state.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/salt/state.py b/salt/state.py index 231898b6cc..5f350de325 100644 --- a/salt/state.py +++ b/salt/state.py @@ -2521,7 +2521,7 @@ class State(object): if to_tag not in running: continue if running[to_tag]['changes']: - if key not in crefs: + if not any(key[0] == cref[0] and key[1] in cref for cref in crefs): rerror = {_l_tag(key[0], key[1]): {'comment': 'Referenced state {0}: {1} does not exist'.format(key[0], key[1]), 'name': 'listen_{0}:{1}'.format(key[0], key[1]), @@ -2529,8 +2529,9 @@ class State(object): 'changes': {}}} errors.update(rerror) continue - chunks = [data for cref, data in six.iteritems(crefs) if key == cref] - for chunk in chunks: + + new_chunks = [data for cref, data in six.iteritems(crefs) if key[0] == cref[0] and key[1] in cref] + for chunk in new_chunks: low = chunk.copy() low['sfun'] = chunk['fun'] low['fun'] = 'mod_watch' From d6d9e363597558c5adfbf56ee80a3d025011c12e Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Mon, 5 Mar 2018 11:36:38 -0700 Subject: [PATCH 014/117] add tests for names and listen/listen_in Closes #30115 --- .../file/base/requisites/listen_in_names.sls | 11 ++++++++++ .../file/base/requisites/listen_names.sls | 11 ++++++++++ tests/integration/modules/test_state.py | 22 +++++++++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 tests/integration/files/file/base/requisites/listen_in_names.sls create mode 100644 tests/integration/files/file/base/requisites/listen_names.sls diff --git a/tests/integration/files/file/base/requisites/listen_in_names.sls b/tests/integration/files/file/base/requisites/listen_in_names.sls new file mode 100644 index 0000000000..de9ed3efac --- /dev/null +++ b/tests/integration/files/file/base/requisites/listen_in_names.sls @@ -0,0 +1,11 @@ +test: + test.succeed_with_changes: + - name: test + - listen_in: + - test: service + +service: + test.succeed_without_changes: + - names: + - nginx + - crond diff --git a/tests/integration/files/file/base/requisites/listen_names.sls b/tests/integration/files/file/base/requisites/listen_names.sls new file mode 100644 index 0000000000..87e7e2be65 --- /dev/null +++ b/tests/integration/files/file/base/requisites/listen_names.sls @@ -0,0 +1,11 @@ +test: + test.succeed_with_changes: + - name: test + +service: + test.succeed_without_changes: + - names: + - nginx + - crond + - listen: + - test: test diff --git a/tests/integration/modules/test_state.py b/tests/integration/modules/test_state.py index 2d8a494d8b..6206ef1054 100644 --- a/tests/integration/modules/test_state.py +++ b/tests/integration/modules/test_state.py @@ -1156,6 +1156,28 @@ class StateModuleTest(ModuleCase, SaltReturnAssertsMixin): listener_state = 'cmd_|-listener_test_listening_resolution_two_|-echo "Successful listen resolution"_|-mod_watch' self.assertIn(listener_state, state_run) + def test_listen_in_requisite_resolution_names(self): + ''' + Verify listen_in requisite lookups use ID declaration to check for changes + and resolves magic names state variable + ''' + + # Only run the state once and keep the return data + state_run = self.run_function('state.sls', mods='requisites.listen_in_names') + self.assertIn('test_|-listener_service_|-nginx_|-mod_watch', state_run) + self.assertIn('test_|-listener_service_|-crond_|-mod_watch', state_run) + + def test_listen_requisite_resolution_names(self): + ''' + Verify listen requisite lookups use ID declaration to check for changes + and resolves magic names state variable + ''' + + # Only run the state once and keep the return data + state_run = self.run_function('state.sls', mods='requisites.listen_names') + self.assertIn('test_|-listener_service_|-nginx_|-mod_watch', state_run) + self.assertIn('test_|-listener_service_|-crond_|-mod_watch', state_run) + def test_issue_30820_requisite_in_match_by_name(self): ''' This tests the case where a requisite_in matches by name instead of ID From 4e2e62d5085fc1c39b5a5d3e7670c010f947e737 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Mon, 19 Feb 2018 04:06:52 -0800 Subject: [PATCH 015/117] salt.utils.dns parse scope param for ipv6 servers Recent versions of resolv.conf append the device name to IPv6 nameserver IP address entries as a scoping parameter for that nameserver. See for example, https://bugzilla.redhat.com/show_bug.cgi?id=1093294#c4. --- salt/utils/dns.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/salt/utils/dns.py b/salt/utils/dns.py index 593fc732ce..eb23c8bdf6 100644 --- a/salt/utils/dns.py +++ b/salt/utils/dns.py @@ -797,6 +797,8 @@ def parse_resolv(src='/etc/resolv.conf'): ''' nameservers = [] + ip4_nameservers = [] + ip6_nameservers = [] search = [] sortlist = [] domain = '' @@ -815,10 +817,20 @@ def parse_resolv(src='/etc/resolv.conf'): lambda x: x[0] not in ('#', ';'), arg)) if directive == 'nameserver': + # Split the scope (interface) if it is present + addr, scope = arg[0].split('%', 1) if '%' in arg[0] else (arg[0], '') try: - ip_addr = ipaddress.ip_address(arg[0]) + ip_addr = ipaddress.ip_address(addr) + version = ip_addr.version + # Rejoin scope after address validation + if scope: + ip_addr = '%'.join((str(ip_addr), scope)) if ip_addr not in nameservers: nameservers.append(ip_addr) + if version == 4 and ip_addr not in ip4_nameservers: + ip4_nameservers.append(ip_addr) + elif version == 6 and ip_addr not in ip6_nameservers: + ip6_nameservers.append(ip_addr) except ValueError as exc: log.error('{0}: {1}'.format(src, exc)) elif directive == 'domain': @@ -870,8 +882,8 @@ def parse_resolv(src='/etc/resolv.conf'): return { 'nameservers': nameservers, - 'ip4_nameservers': [ip for ip in nameservers if ip.version == 4], - 'ip6_nameservers': [ip for ip in nameservers if ip.version == 6], + 'ip4_nameservers': ip4_nameservers, + 'ip6_nameservers': ip6_nameservers, 'sortlist': [ip.with_netmask for ip in sortlist], 'domain': domain, 'search': search, From d5561bedafdf8551ef18bd3dc0afd76c98c93bf2 Mon Sep 17 00:00:00 2001 From: Justin Findlay Date: Mon, 19 Feb 2018 04:14:45 -0800 Subject: [PATCH 016/117] tests.unit.grains.core add scoped IPv6 nameserver --- tests/unit/grains/test_core.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py index 7632bf77aa..d445392e1c 100644 --- a/tests/unit/grains/test_core.py +++ b/tests/unit/grains/test_core.py @@ -39,6 +39,7 @@ IP4_ADD2 = '10.0.0.2' IP6_LOCAL = '::1' IP6_ADD1 = '2001:4860:4860::8844' IP6_ADD2 = '2001:4860:4860::8888' +IP6_ADD_SCOPE = 'fe80::6238:e0ff:fe06:3f6b%enp2s0' OS_RELEASE_DIR = os.path.join(os.path.dirname(__file__), "os-releases") @@ -673,15 +674,18 @@ PATCHLEVEL = 3 ''' resolv_mock = {'domain': '', 'sortlist': [], 'nameservers': [ipaddress.IPv4Address(IP4_ADD1), - ipaddress.IPv6Address(IP6_ADD1)], 'ip4_nameservers': + ipaddress.IPv6Address(IP6_ADD1), + IP6_ADD_SCOPE], 'ip4_nameservers': [ipaddress.IPv4Address(IP4_ADD1)], 'search': ['test.saltstack.com'], 'ip6_nameservers': - [ipaddress.IPv6Address(IP6_ADD1)], 'options': []} + [ipaddress.IPv6Address(IP6_ADD1), + IP6_ADD_SCOPE], 'options': []} ret = {'dns': {'domain': '', 'sortlist': [], 'nameservers': - [IP4_ADD1, IP6_ADD1], 'ip4_nameservers': + [IP4_ADD1, IP6_ADD1, + IP6_ADD_SCOPE], 'ip4_nameservers': [IP4_ADD1], 'search': ['test.saltstack.com'], - 'ip6_nameservers': [IP6_ADD1], 'options': - []}} + 'ip6_nameservers': [IP6_ADD1, IP6_ADD_SCOPE], + 'options': []}} self._run_dns_test(resolv_mock, ret) def _run_dns_test(self, resolv_mock, ret): From 5219f7d2bab0dfc66cf3af2adf80df48b4761dfb Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 6 Mar 2018 14:53:11 -0700 Subject: [PATCH 017/117] fix minion log path --- .kitchen.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.kitchen.yml b/.kitchen.yml index 1a6ecf8b9a..10c212eff7 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -202,6 +202,6 @@ verifier: save: /tmp/xml-unittests-output: artifacts/ /tmp/coverage.xml: artifacts/coverage/coverage.xml - /var/log/salt/minion: artifacts/logs/minion + /tmp/kitchen/var/log/salt/minion: artifacts/logs/minion /tmp/salt-runtests.log: artifacts/logs/salt-runtests.log <% end %> From 3620611b5b2ad54143f32e1dac83602c0eed6290 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 6 Mar 2018 14:54:40 -0700 Subject: [PATCH 018/117] fix unhold package for debian --- tests/integration/modules/test_pkg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/integration/modules/test_pkg.py b/tests/integration/modules/test_pkg.py index 20fcdbb952..742db38b3a 100644 --- a/tests/integration/modules/test_pkg.py +++ b/tests/integration/modules/test_pkg.py @@ -174,6 +174,7 @@ class PkgModuleTest(ModuleCase, SaltReturnAssertsMixin): available = self.run_function('sys.doc', ['pkg.hold']) if available: + self.run_function('pkg.install', [self.pkg]) if os_family == 'RedHat': lock_pkg = 'yum-versionlock' if os_major_release == '5' else 'yum-plugin-versionlock' versionlock = self.run_function('pkg.version', [lock_pkg]) @@ -186,11 +187,12 @@ class PkgModuleTest(ModuleCase, SaltReturnAssertsMixin): unhold_ret = self.run_function('pkg.unhold', [self.pkg]) self.assertIn(self.pkg, unhold_ret) - self.assertTrue(hold_ret[self.pkg]['result']) + self.assertTrue(unhold_ret[self.pkg]['result']) if os_family == 'RedHat': if not versionlock: self.run_function('pkg.remove', [lock_pkg]) + self.run_function('pkg.remove', [self.pkg]) else: os_grain = self.run_function('grains.item', ['os'])['os'] From c56baa95a80f2e2c6f71c71dbb6a10b250ff603d Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 6 Mar 2018 15:36:35 -0700 Subject: [PATCH 019/117] clone .git for the version tests --- .kitchen.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.kitchen.yml b/.kitchen.yml index 10c212eff7..30d72c426c 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -41,8 +41,6 @@ provisioner: testingdir: /testing salt_copy_filter: - .bundle - - .git - - .gitignore - .kitchen - .kitchen.yml - Gemfile From d971e0c08b5c91db631758e65198b73e32a02b68 Mon Sep 17 00:00:00 2001 From: angeloudy Date: Wed, 7 Mar 2018 12:22:18 +1100 Subject: [PATCH 020/117] Fix indent --- salt/utils/http.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/utils/http.py b/salt/utils/http.py index 954fdd9785..7f32bb2de1 100644 --- a/salt/utils/http.py +++ b/salt/utils/http.py @@ -430,7 +430,7 @@ def query(url, not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) if six.PY3 and isinstance(result_text, bytes): - result_text = result.body.decode('utf-8') + result_text = result.body.decode('utf-8') ret['body'] = result_text else: # Tornado @@ -532,7 +532,7 @@ def query(url, not isinstance(result_text, six.text_type): result_text = result_text.decode(res_params['charset']) if six.PY3 and isinstance(result_text, bytes): - result_text = result_text.decode('utf-8') + result_text = result_text.decode('utf-8') ret['body'] = result_text if 'Set-Cookie' in result_headers.keys() and cookies is not None: result_cookies = parse_cookie_header(result_headers['Set-Cookie']) From 726ca3044d63817cdcc6cc13bf594479b7a56a61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Wed, 7 Mar 2018 09:42:46 +0000 Subject: [PATCH 021/117] Explore 'module.run' response to catch the 'result' in depth --- salt/states/module.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/salt/states/module.py b/salt/states/module.py index 062e44ff6f..fbbc0001ad 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -530,7 +530,20 @@ def _get_result(func_ret, changes): res = changes_ret.get('result', {}) elif changes_ret.get('retcode', 0) != 0: res = False + # Explore dict in depth to determine if there is a + # 'result' key set to False which sets the global + # state result. + else: + res = _get_result_in_depth(changes_ret) return res +def _get_result_in_depth(node): + for key, val in node.iteritems(): + if key == 'result' and val is False: + return False + elif isinstance(val, dict): + return _get_result_in_depth(val) + return True + mod_watch = salt.utils.alias_function(run, 'mod_watch') From 1476ace558a1c9510e24adcd9b04a73d602368c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Wed, 7 Mar 2018 11:20:44 +0000 Subject: [PATCH 022/117] Fix Python3 and pylint issue --- salt/states/module.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/states/module.py b/salt/states/module.py index fbbc0001ad..49dcd8ccd5 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -538,8 +538,9 @@ def _get_result(func_ret, changes): return res + def _get_result_in_depth(node): - for key, val in node.iteritems(): + for key, val in six.iteritems(node): if key == 'result' and val is False: return False elif isinstance(val, dict): From 9f19ad5264660bc1467f3ce4b3e7405dc6546b3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Wed, 7 Mar 2018 13:41:39 +0000 Subject: [PATCH 023/117] Rename and fix recursive method --- salt/states/module.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/salt/states/module.py b/salt/states/module.py index 49dcd8ccd5..f0d5ab4226 100644 --- a/salt/states/module.py +++ b/salt/states/module.py @@ -534,17 +534,21 @@ def _get_result(func_ret, changes): # 'result' key set to False which sets the global # state result. else: - res = _get_result_in_depth(changes_ret) + res = _get_dict_result(changes_ret) return res -def _get_result_in_depth(node): +def _get_dict_result(node): + ret = True for key, val in six.iteritems(node): if key == 'result' and val is False: - return False + ret = False + break elif isinstance(val, dict): - return _get_result_in_depth(val) - return True + ret = _get_dict_result(val) + if ret is False: + break + return ret mod_watch = salt.utils.alias_function(run, 'mod_watch') From e48c13d9e0766124d68c3c3ec6edc8ff34196543 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Wed, 7 Mar 2018 15:10:03 +0100 Subject: [PATCH 024/117] Fix SSH client exception if SSH is not found When no SSH client is installed, salt will create an empty SaltSystemExit exception: Traceback (most recent call last): File "tests/unit/ssh/test_ssh.py", line 42, in test_password_failure client = ssh.SSH(opts) File "salt/client/ssh/__init__.py", line 226, in __init__ raise salt.exceptions.SaltSystemExit('No ssh binary found in path -- ssh must be ' salt.exceptions.SaltSystemExit: None The SaltSystemExit exception takes two parameters: code and msg. Signed-off-by: Benjamin Drung --- salt/client/ssh/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/client/ssh/__init__.py b/salt/client/ssh/__init__.py index 0beeb554ad..287ebe7e81 100644 --- a/salt/client/ssh/__init__.py +++ b/salt/client/ssh/__init__.py @@ -220,7 +220,8 @@ class SSH(object): if self.opts['regen_thin']: self.opts['ssh_wipe'] = True if not salt.utils.which('ssh'): - raise salt.exceptions.SaltSystemExit('No ssh binary found in path -- ssh must be installed for salt-ssh to run. Exiting.') + raise salt.exceptions.SaltSystemExit(code=-1, + msg='No ssh binary found in path -- ssh must be installed for salt-ssh to run. Exiting.') self.opts['_ssh_version'] = ssh_version() self.tgt_type = self.opts['selected_target_option'] \ if self.opts['selected_target_option'] else 'glob' From 91db2e0782ee823202da4ca7333c3f9fde11e22b Mon Sep 17 00:00:00 2001 From: Graham Clinch Date: Fri, 2 Mar 2018 16:38:08 +0000 Subject: [PATCH 025/117] Python 3 support --- salt/modules/debconfmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index 98fd1aaaa4..d464fff24b 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -127,7 +127,7 @@ def set_(package, question, type, value, *extra): fd_, fname = salt.utils.files.mkstemp(prefix="salt-", close_fd=False) line = "{0} {1} {2} {3}".format(package, question, type, value) - os.write(fd_, line) + os.write(fd_, salt.utils.stringutils.to_bytes(line)) os.close(fd_) _set_file(fname) From 829dfde8e8c65ee943b02366077ffcf4cf5ce66b Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 7 Mar 2018 10:12:49 -0500 Subject: [PATCH 026/117] Change stringutils path to old utils path for 2017.7 --- salt/modules/debconfmod.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index d464fff24b..2acdeaaa44 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -127,7 +127,7 @@ def set_(package, question, type, value, *extra): fd_, fname = salt.utils.files.mkstemp(prefix="salt-", close_fd=False) line = "{0} {1} {2} {3}".format(package, question, type, value) - os.write(fd_, salt.utils.stringutils.to_bytes(line)) + os.write(fd_, salt.utils.to_bytes(line)) os.close(fd_) _set_file(fname) From 885751634e42c965df0c994d9d5bad7074af3dd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20Su=C3=A1rez=20Hern=C3=A1ndez?= Date: Wed, 7 Mar 2018 15:43:52 +0000 Subject: [PATCH 027/117] Add new unit test to check state.apply within module.run --- tests/unit/states/test_module.py | 62 ++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/unit/states/test_module.py b/tests/unit/states/test_module.py index 2d082fed2a..a2af8e7324 100644 --- a/tests/unit/states/test_module.py +++ b/tests/unit/states/test_module.py @@ -25,6 +25,57 @@ log = logging.getLogger(__name__) CMD = 'foo.bar' +STATE_APPLY_RET = { + 'module_|-test2_|-state.apply_|-run': { + 'comment': 'Module function state.apply executed', + 'name': 'state.apply', + 'start_time': '16:11:48.818932', + 'result': False, + 'duration': 179.439, + '__run_num__': 0, + 'changes': { + 'ret': { + 'module_|-test3_|-state.apply_|-run': { + 'comment': 'Module function state.apply executed', + 'name': 'state.apply', + 'start_time': '16:11:48.904796', + 'result': True, + 'duration': 89.522, + '__run_num__': 0, + 'changes': { + 'ret': { + 'module_|-test4_|-cmd.run_|-run': { + 'comment': 'Module function cmd.run executed', + 'name': 'cmd.run', + 'start_time': '16:11:48.988574', + 'result': True, + 'duration': 4.543, + '__run_num__': 0, + 'changes': { + 'ret': 'Wed Mar 7 16:11:48 CET 2018' + }, + '__id__': 'test4' + } + } + }, + '__id__': 'test3' + }, + 'module_|-test3_fail_|-test3_fail_|-run': { + 'comment': 'Module function test3_fail is not available', + 'name': 'test3_fail', + 'start_time': '16:11:48.994607', + 'result': False, + 'duration': 0.466, + '__run_num__': 1, + 'changes': {}, + '__id__': 'test3_fail' + } + } + }, + '__id__': 'test2' + } +} + def _mocked_func_named(name, names=('Fred', 'Swen',)): ''' @@ -143,6 +194,17 @@ class ModuleStateTest(TestCase, LoaderModuleMockMixin): if ret['comment'] != '{0}: Success'.format(CMD) or not ret['result']: self.fail('module.run failed: {0}'.format(ret)) + def test_run_state_apply_result_false(self): + ''' + Tests the 'result' of module.run that calls state.apply execution module + :return: + ''' + with patch.dict(module.__salt__, {"state.apply": MagicMock(return_value=STATE_APPLY_RET)}): + with patch.dict(module.__opts__, {'use_deprecated': ['module.run']}): + ret = module.run(**{"name": "state.apply", 'mods': 'test2'}) + if ret['result']: + self.fail('module.run did not report false result: {0}'.format(ret)) + def test_run_unexpected_keywords(self): with patch.dict(module.__salt__, {CMD: _mocked_func_args}): with patch.dict(module.__opts__, {'use_superseded': ['module.run']}): From cc67e5c2efe525f28f9d4a6f70986b0dc46b6772 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 7 Mar 2018 11:52:17 -0700 Subject: [PATCH 028/117] Set six to 1.11.0 CherryPy has a requisite of six>=1.11.0 and salt had a requisite of six==1.10.0 which was causing a problem in the dev environment --- pkg/windows/req.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/windows/req.txt b/pkg/windows/req.txt index 0e16938f19..dd07054720 100644 --- a/pkg/windows/req.txt +++ b/pkg/windows/req.txt @@ -31,7 +31,7 @@ PyYAML==3.12 pyzmq==16.0.2 requests==2.13.0 singledispatch==3.4.0.3 -six==1.10.0 +six==1.11.0 smmap==0.9.0 timelib==0.2.4 tornado==4.5.1 From e7ab97cc17e432529b4c0e250b9ea4b495b19623 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 7 Mar 2018 12:26:37 -0700 Subject: [PATCH 029/117] Remove six as a hard dep for Salt --- pkg/windows/req.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/windows/req.txt b/pkg/windows/req.txt index dd07054720..1881f4c99b 100644 --- a/pkg/windows/req.txt +++ b/pkg/windows/req.txt @@ -31,7 +31,6 @@ PyYAML==3.12 pyzmq==16.0.2 requests==2.13.0 singledispatch==3.4.0.3 -six==1.11.0 smmap==0.9.0 timelib==0.2.4 tornado==4.5.1 From 8995a9b8de436d2e8c22812e65713a4038c946a9 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 7 Mar 2018 13:48:19 -0700 Subject: [PATCH 030/117] Fix problem with __virtual__ in win_snmp Exposes the `_key_exists` function in the reg.py module as `key_exists` Fixes an issue where `key_exists` was throwing an error when the key didn't exist Fixes a problem with the `broadcast_change` function in the `win_functions` salt util. It was using `unicode_literals` and those aren't implemented until 2018.3. The ctypes.WinDLL function is expecting a string value, not unicode Have the __virtual__ function for win_snmp use the `key_exists` function to detect the presence of the SNMP key in the registry. This will fail gracefully if it doesn't exist --- salt/modules/reg.py | 11 ++++++++--- salt/modules/win_snmp.py | 2 +- salt/utils/win_functions.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/salt/modules/reg.py b/salt/modules/reg.py index 3221b58d05..7bb22638a6 100644 --- a/salt/modules/reg.py +++ b/salt/modules/reg.py @@ -155,9 +155,10 @@ class Registry(object): # pylint: disable=R0903 raise CommandExecutionError(msg.format(k, hkeys)) -def _key_exists(hive, key, use_32bit_registry=False): +def key_exists(hive, key, use_32bit_registry=False): ''' - Check that the key is found in the registry + Check that the key is found in the registry. This refers to keys and not + value/data pairs. :param str hive: The hive to connect to. :param str key: The key to check @@ -179,6 +180,10 @@ def _key_exists(hive, key, use_32bit_registry=False): return True except WindowsError: # pylint: disable=E0602 return False + except pywintypes.error as exc: + if exc.winerror == 2: + return False + raise def broadcast_change(): @@ -603,7 +608,7 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): key_path = local_key access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS - if not _key_exists(local_hive, local_key, use_32bit_registry): + if not key_exists(local_hive, local_key, use_32bit_registry): return False if (len(key) > 1) and (key.count('\\', 1) < registry.subkey_slash_check[hkey]): diff --git a/salt/modules/win_snmp.py b/salt/modules/win_snmp.py index 6fd3bd515f..baa119e388 100644 --- a/salt/modules/win_snmp.py +++ b/salt/modules/win_snmp.py @@ -45,7 +45,7 @@ def __virtual__(): if not salt.utils.is_windows(): return False, 'Module win_snmp: Requires Windows' - if not __salt__['reg.read_value'](_HKEY, _SNMP_KEY)['success']: + if not __salt__['reg.key_exists'](_HKEY, _SNMP_KEY): return False, 'Module win_snmp: SNMP not installed' return __virtualname__ diff --git a/salt/utils/win_functions.py b/salt/utils/win_functions.py index db883566b8..fb1feb8901 100644 --- a/salt/utils/win_functions.py +++ b/salt/utils/win_functions.py @@ -3,7 +3,7 @@ Various functions to be used by windows during start up and to monkey patch missing functions in other modules ''' -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import import platform import re import ctypes From f9f187e915fae629110717250e23ab2a59da3f50 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 7 Mar 2018 15:45:38 -0600 Subject: [PATCH 031/117] Improve reliability/idempotence of file.blockreplace state This makes the following changes: - The `append_newline` argument to the `file.blockreplace` remote-execution function has been modified so that if its value is `None`, it only appends a newline when the content block does not end in one. - A couple of fixes were made to newline handling. The existing code normalized the newlines in the content block, replacing them with os.linesep. However, when the file contains newlines that don't match the OS (i.e. POSIX newlines in a file on a Windows box, or Windows newlines on a Linux/Mac/BSD/etc. box), then we would still end up with mixed newlines. The line separator is now detected when we read in the original file, and the detected line separator is used when writing the content block. Additionally, the same newline mismatch was possible when appending/prepending the content block. This has been fixed by using a common function for appending, prepending, and replacing the content block. - Support for the `append_newline` argument has been added to the `file.blockreplace` state. The default value for the state is `None`. A `versionchanged` has been added to the remote execution function to let users know that the Fluorine release will change the default value of that variable. - 20 new integration tests have been written to test the `file.blockreplace` state. --- salt/modules/file.py | 209 +++-- salt/states/file.py | 98 +- tests/integration/states/test_file.py | 1216 ++++++++++++++++++++++++- tests/support/helpers.py | 21 +- tests/unit/modules/test_file.py | 89 +- 5 files changed, 1473 insertions(+), 160 deletions(-) diff --git a/salt/modules/file.py b/salt/modules/file.py index b8abbb01e8..1e91741304 100644 --- a/salt/modules/file.py +++ b/salt/modules/file.py @@ -2270,8 +2270,7 @@ def blockreplace(path, backup='.bak', dry_run=False, show_changes=True, - append_newline=False, - ): + append_newline=False): ''' .. versionadded:: 2014.1.0 @@ -2318,18 +2317,30 @@ def blockreplace(path, The file extension to use for a backup of the file if any edit is made. Set to ``False`` to skip making a backup. - dry_run - Don't make any edits to the file. + dry_run : False + If ``True``, do not make any edits to the file and simply return the + changes that *would* be made. - show_changes - Output a unified diff of the old file and the new file. If ``False``, - return a boolean if any changes were made. + show_changes : True + Controls how changes are presented. If ``True``, this function will + return a unified diff of the changes made. If False, then it will + return a boolean (``True`` if any changes were made, otherwise + ``False``). - append_newline: - Append a newline to the content block. For more information see: - https://github.com/saltstack/salt/issues/33686 + append_newline : False + Controls whether or not a newline is appended to the content block. If + the value of this argument is ``True`` then a newline will be added to + the content block. If it is ``False``, then a newline will *not* be + added to the content block. If it is ``None`` then a newline will only + be added to the content block if it does not already end in a newline. .. versionadded:: 2016.3.4 + .. versionchanged:: 2017.7.5,2018.3.1 + New behavior added when value is ``None``. + .. versionchanged:: Fluorine + The default value of this argument will change to ``None`` to match + the behavior of the :py:func:`file.blockreplace state + ` CLI Example: @@ -2339,87 +2350,137 @@ def blockreplace(path, '#-- end managed zone foobar --' $'10.0.1.1 foo.foobar\\n10.0.1.2 bar.foobar' True ''' - path = os.path.expanduser(path) - - if not os.path.exists(path): - raise SaltInvocationError('File not found: {0}'.format(path)) - if append_if_not_found and prepend_if_not_found: raise SaltInvocationError( 'Only one of append and prepend_if_not_found is permitted' ) + path = os.path.expanduser(path) + + if not os.path.exists(path): + raise SaltInvocationError('File not found: {0}'.format(path)) + if not salt.utils.istextfile(path): raise SaltInvocationError( 'Cannot perform string replacements on a binary file: {0}' .format(path) ) - # Search the file; track if any changes have been made for the return val + if append_newline is None and not content.endswith((os.linesep, '\n')): + append_newline = True + + # Split the content into a list of lines, removing newline characters. To + # ensure that we handle both Windows and POSIX newlines, first split on + # Windows newlines, and then split on POSIX newlines. + split_content = [] + for win_line in content.split('\r\n'): + for content_line in win_line.split('\n'): + split_content.append(content_line) + + line_count = len(split_content) + has_changes = False orig_file = [] new_file = [] in_block = False - old_content = '' - done = False - # we do not use in_place editing to avoid file attrs modifications when + block_found = False + linesep = None + + def _add_content(linesep, lines=None, include_marker_start=True, + end_line=None): + if lines is None: + lines = [] + include_marker_start = True + + if end_line is None: + end_line = marker_end + end_line = end_line.rstrip('\r\n') + linesep + + if include_marker_start: + lines.append(marker_start + linesep) + + if split_content: + for index, content_line in enumerate(split_content, 1): + if index != line_count: + lines.append(content_line + linesep) + else: + # We're on the last line of the content block + if append_newline: + lines.append(content_line + linesep) + lines.append(end_line) + else: + lines.append(content_line + end_line) + else: + lines.append(end_line) + + return lines + + # We do not use in-place editing to avoid file attrs modifications when # no changes are required and to avoid any file access on a partially # written file. - # we could also use salt.utils.filebuffer.BufferedReader + # + # We could also use salt.utils.filebuffer.BufferedReader try: - fi_file = fileinput.input(path, - inplace=False, backup=False, - bufsize=1, mode='rb') - for line in fi_file: + fi_file = fileinput.input( + path, + inplace=False, + backup=False, + bufsize=1, + mode='rb') + for line in fi_file: line = salt.utils.to_str(line) - result = line + write_line_to_new_file = True + + if linesep is None: + # Auto-detect line separator + if line.endswith('\r\n'): + linesep = '\r\n' + elif line.endswith('\n'): + linesep = '\n' + else: + # No newline(s) in file, fall back to system's linesep + linesep = os.linesep if marker_start in line: - # managed block start found, start recording + # We've entered the content block in_block = True - else: if in_block: - if marker_end in line: - # end of block detected + # We're not going to write the lines from the old file to + # the new file until we have exited the block. + write_line_to_new_file = False + + marker_end_pos = line.find(marker_end) + if marker_end_pos != -1: + # End of block detected in_block = False + # We've found and exited the block + block_found = True - # Handle situations where there may be multiple types - # of line endings in the same file. Separate the content - # into lines. Account for Windows-style line endings - # using os.linesep, then by linux-style line endings - # using '\n' - split_content = [] - for linesep_line in content.split(os.linesep): - for content_line in linesep_line.split('\n'): - split_content.append(content_line) - - # Trim any trailing new lines to avoid unwanted - # additional new lines - while not split_content[-1]: - split_content.pop() - - # push new block content in file - for content_line in split_content: - new_file.append(content_line + os.linesep) - - done = True - - else: - # remove old content, but keep a trace - old_content += line - result = None - # else: we are not in the marked block, keep saving things + _add_content(linesep, lines=new_file, + include_marker_start=False, + end_line=line[marker_end_pos:]) + # Save the line from the original file orig_file.append(line) - if result is not None: - new_file.append(result) - # end for. If we are here without block management we maybe have some problems, - # or we need to initialise the marked block + if write_line_to_new_file: + new_file.append(line) + except (IOError, OSError) as exc: + raise CommandExecutionError( + 'Failed to read from {0}: {1}'.format(path, exc) + ) finally: - fi_file.close() + if linesep is None: + # If the file was empty, we will not have set linesep yet. Assume + # the system's line separator. This is needed for when we + # prepend/append later on. + linesep = os.linesep + try: + fi_file.close() + except Exception: + pass if in_block: # unterminated block => bad, always fail @@ -2427,35 +2488,27 @@ def blockreplace(path, 'Unterminated marked block. End of file reached before marker_end.' ) - if not done: + if not block_found: if prepend_if_not_found: # add the markers and content at the beginning of file - new_file.insert(0, marker_end + os.linesep) - if append_newline is True: - new_file.insert(0, content + os.linesep) - else: - new_file.insert(0, content) - new_file.insert(0, marker_start + os.linesep) - done = True + prepended_content = _add_content(linesep) + prepended_content.extend(new_file) + new_file = prepended_content + block_found = True elif append_if_not_found: # Make sure we have a newline at the end of the file if 0 != len(new_file): - if not new_file[-1].endswith(os.linesep): - new_file[-1] += os.linesep + if not new_file[-1].endswith(linesep): + new_file[-1] += linesep # add the markers and content at the end of file - new_file.append(marker_start + os.linesep) - if append_newline is True: - new_file.append(content + os.linesep) - else: - new_file.append(content) - new_file.append(marker_end + os.linesep) - done = True + _add_content(linesep, lines=new_file) + block_found = True else: raise CommandExecutionError( 'Cannot edit marked block. Markers were not found in file.' ) - if done: + if block_found: diff = ''.join(difflib.unified_diff(orig_file, new_file)) has_changes = diff is not '' if has_changes and not dry_run: diff --git a/salt/states/file.py b/salt/states/file.py index 6c11dc3b0a..3c1e226692 100644 --- a/salt/states/file.py +++ b/salt/states/file.py @@ -4023,11 +4023,20 @@ def blockreplace( append_if_not_found=False, prepend_if_not_found=False, backup='.bak', - show_changes=True): + show_changes=True, + append_newline=None): ''' Maintain an edit in a file in a zone delimited by two line markers .. versionadded:: 2014.1.0 + .. versionchanged:: 2017.7.5,2018.3.1 + ``append_newline`` argument added. Additionally, to improve + idempotence, if the string represented by ``marker_end`` is found in + the middle of the line, the content preceding the marker will be + removed when the block is replaced. This allows one to remove + ``append_newline: False`` from the SLS and have the block properly + replaced if the end of the content block is immediately followed by the + ``marker_end`` (i.e. no newline before the marker). A block of content delimited by comments can help you manage several lines entries without worrying about old entries removal. This can help you @@ -4112,41 +4121,54 @@ def blockreplace( See the ``source_hash`` parameter description for :mod:`file.managed ` function for more details and examples. - template - The named templating engine will be used to render the downloaded file. - Defaults to ``jinja``. The following templates are supported: + template : jinja + Templating engine to be used to render the downloaded file. The + following engines are supported: - - :mod:`cheetah` - - :mod:`genshi` - - :mod:`jinja` - - :mod:`mako` - - :mod:`py` - - :mod:`wempy` + - :mod:`cheetah ` + - :mod:`genshi ` + - :mod:`jinja ` + - :mod:`mako ` + - :mod:`py ` + - :mod:`wempy ` context - Overrides default context variables passed to the template. + Overrides default context variables passed to the template defaults - Default context passed to the template. + Default context passed to the template - append_if_not_found - If markers are not found and set to True then the markers and content - will be appended to the file. Default is ``False`` + append_if_not_found : False + If markers are not found and this option is set to ``True``, the + content block will be appended to the file. - prepend_if_not_found - If markers are not found and set to True then the markers and content - will be prepended to the file. Default is ``False`` + prepend_if_not_found : False + If markers are not found and this option is set to ``True``, the + content block will be prepended to the file. backup The file extension to use for a backup of the file if any edit is made. Set this to ``False`` to skip making a backup. - dry_run - Don't make any edits to the file + dry_run : False + If ``True``, do not make any edits to the file and simply return the + changes that *would* be made. - show_changes - Output a unified diff of the old file and the new file. If ``False`` - return a boolean if any changes were made + show_changes : True + Controls how changes are presented. If ``True``, the ``Changes`` + section of the state return will contain a unified diff of the changes + made. If False, then it will contain a boolean (``True`` if any changes + were made, otherwise ``False``). + + append_newline + Controls whether or not a newline is appended to the content block. If + the value of this argument is ``True`` then a newline will be added to + the content block. If it is ``False``, then a newline will *not* be + added to the content block. If it is unspecified, then a newline will + only be added to the content block if it does not already end in a + newline. + + .. versionadded:: 2017.7.5,2018.3.1 Example of usage with an accumulator and with a variable: @@ -4248,17 +4270,25 @@ def blockreplace( for index, item in enumerate(text): content += str(item) - changes = __salt__['file.blockreplace']( - name, - marker_start, - marker_end, - content=content, - append_if_not_found=append_if_not_found, - prepend_if_not_found=prepend_if_not_found, - backup=backup, - dry_run=__opts__['test'], - show_changes=show_changes - ) + try: + changes = __salt__['file.blockreplace']( + name, + marker_start, + marker_end, + content=content, + append_if_not_found=append_if_not_found, + prepend_if_not_found=prepend_if_not_found, + backup=backup, + dry_run=__opts__['test'], + show_changes=show_changes, + append_newline=append_newline) + except Exception as exc: + log.exception('Encountered error managing block') + ret['comment'] = ( + 'Encountered error managing block: {0}. ' + 'See the log for details.'.format(exc) + ) + return ret if changes: ret['pchanges'] = {'diff': changes} diff --git a/tests/integration/states/test_file.py b/tests/integration/states/test_file.py index e84795aa2c..d0ed2cbf23 100644 --- a/tests/integration/states/test_file.py +++ b/tests/integration/states/test_file.py @@ -27,6 +27,7 @@ from tests.support.paths import FILES, TMP, TMP_STATE_TREE from tests.support.helpers import ( skip_if_not_root, with_system_user_and_group, + with_tempfile, Webserver, ) from tests.support.mixins import SaltReturnAssertsMixin @@ -132,6 +133,16 @@ class FileTest(ModuleCase, SaltReturnAssertsMixin): ''' Validate the file state ''' + def tearDown(self): + ''' + remove files created in previous tests + ''' + for path in (FILEPILLAR, FILEPILLARDEF, FILEPILLARGIT): + try: + os.remove(path) + except OSError as exc: + if exc.errno != os.errno.ENOENT: + log.error('Failed to remove %s: %s', path, exc) def test_symlink(self): ''' @@ -2477,15 +2488,1206 @@ class FileTest(ModuleCase, SaltReturnAssertsMixin): ret = self.run_function('state.sls', mods=state_file) self.assertSaltTrueReturn(ret) - def tearDown(self): + +class BlockreplaceTest(ModuleCase, SaltReturnAssertsMixin): + marker_start = '# start' + marker_end = '# end' + content = textwrap.dedent('''\ + Line 1 of block + Line 2 of block + ''') + without_block = textwrap.dedent('''\ + Hello world! + + # comment here + ''') + with_non_matching_block = textwrap.dedent('''\ + Hello world! + + # start + No match here + # end + # comment here + ''') + with_non_matching_block_and_marker_end_not_after_newline = textwrap.dedent('''\ + Hello world! + + # start + No match here# end + # comment here + ''') + with_matching_block = textwrap.dedent('''\ + Hello world! + + # start + Line 1 of block + Line 2 of block + # end + # comment here + ''') + with_matching_block_and_extra_newline = textwrap.dedent('''\ + Hello world! + + # start + Line 1 of block + Line 2 of block + + # end + # comment here + ''') + with_matching_block_and_marker_end_not_after_newline = textwrap.dedent('''\ + Hello world! + + # start + Line 1 of block + Line 2 of block# end + # comment here + ''') + content_explicit_posix_newlines = ('Line 1 of block\n' + 'Line 2 of block\n') + content_explicit_windows_newlines = ('Line 1 of block\r\n' + 'Line 2 of block\r\n') + without_block_explicit_posix_newlines = ('Hello world!\n\n' + '# comment here\n') + without_block_explicit_windows_newlines = ('Hello world!\r\n\r\n' + '# comment here\r\n') + with_block_prepended_explicit_posix_newlines = ('# start\n' + 'Line 1 of block\n' + 'Line 2 of block\n' + '# end\n' + 'Hello world!\n\n' + '# comment here\n') + with_block_prepended_explicit_windows_newlines = ('# start\r\n' + 'Line 1 of block\r\n' + 'Line 2 of block\r\n' + '# end\r\n' + 'Hello world!\r\n\r\n' + '# comment here\r\n') + with_block_appended_explicit_posix_newlines = ('Hello world!\n\n' + '# comment here\n' + '# start\n' + 'Line 1 of block\n' + 'Line 2 of block\n' + '# end\n') + with_block_appended_explicit_windows_newlines = ('Hello world!\r\n\r\n' + '# comment here\r\n' + '# start\r\n' + 'Line 1 of block\r\n' + 'Line 2 of block\r\n' + '# end\r\n') + + @staticmethod + def _write(dest, content): + with salt.utils.fopen(dest, 'wb') as fp_: + fp_.write(salt.utils.to_bytes(content)) + + @staticmethod + def _read(src): + with salt.utils.fopen(src, 'rb') as fp_: + return salt.utils.to_unicode(fp_.read()) + + @with_tempfile + def test_prepend(self, name): ''' - remove files created in previous tests + Test blockreplace when prepend_if_not_found=True and block doesn't + exist in file. ''' - all_files = [FILEPILLAR, FILEPILLARDEF, FILEPILLARGIT] - for file in all_files: - check_file = self.run_function('file.file_exists', [file]) - if check_file: - self.run_function('file.remove', [file]) + expected = self.marker_start + os.linesep + self.content + \ + self.marker_end + os.linesep + self.without_block + + # Pass 1: content ends in newline + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_prepend_append_newline(self, name): + ''' + Test blockreplace when prepend_if_not_found=True and block doesn't + exist in file. Test with append_newline explicitly set to True. + ''' + # Pass 1: content ends in newline + expected = self.marker_start + os.linesep + self.content + \ + os.linesep + self.marker_end + os.linesep + self.without_block + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + expected = self.marker_start + os.linesep + self.content + \ + self.marker_end + os.linesep + self.without_block + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_prepend_no_append_newline(self, name): + ''' + Test blockreplace when prepend_if_not_found=True and block doesn't + exist in file. Test with append_newline explicitly set to False. + ''' + # Pass 1: content ends in newline + expected = self.marker_start + os.linesep + self.content + \ + self.marker_end + os.linesep + self.without_block + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + expected = self.marker_start + os.linesep + \ + self.content.rstrip('\r\n') + self.marker_end + os.linesep + \ + self.without_block + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_append(self, name): + ''' + Test blockreplace when append_if_not_found=True and block doesn't + exist in file. + ''' + expected = self.without_block + self.marker_start + os.linesep + \ + self.content + self.marker_end + os.linesep + + # Pass 1: content ends in newline + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_append_append_newline(self, name): + ''' + Test blockreplace when append_if_not_found=True and block doesn't + exist in file. Test with append_newline explicitly set to True. + ''' + # Pass 1: content ends in newline + expected = self.without_block + self.marker_start + os.linesep + \ + self.content + os.linesep + self.marker_end + os.linesep + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + expected = self.without_block + self.marker_start + os.linesep + \ + self.content + self.marker_end + os.linesep + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_append_no_append_newline(self, name): + ''' + Test blockreplace when append_if_not_found=True and block doesn't + exist in file. Test with append_newline explicitly set to False. + ''' + # Pass 1: content ends in newline + expected = self.without_block + self.marker_start + os.linesep + \ + self.content + self.marker_end + os.linesep + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + # Pass 2: content does not end in newline + expected = self.without_block + self.marker_start + os.linesep + \ + self.content.rstrip('\r\n') + self.marker_end + os.linesep + self._write(name, self.without_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), expected) + + @with_tempfile + def test_prepend_auto_line_separator(self, name): + ''' + This tests the line separator auto-detection when prepending the block + ''' + # POSIX newlines to Windows newlines + self._write(name, self.without_block_explicit_windows_newlines) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_posix_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_prepended_explicit_windows_newlines) + # Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_posix_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_prepended_explicit_windows_newlines) + + # Windows newlines to POSIX newlines + self._write(name, self.without_block_explicit_posix_newlines) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_windows_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_prepended_explicit_posix_newlines) + # Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_windows_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + prepend_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_prepended_explicit_posix_newlines) + + @with_tempfile + def test_append_auto_line_separator(self, name): + ''' + This tests the line separator auto-detection when appending the block + ''' + # POSIX newlines to Windows newlines + self._write(name, self.without_block_explicit_windows_newlines) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_posix_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_appended_explicit_windows_newlines) + # Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_posix_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_appended_explicit_windows_newlines) + + # Windows newlines to POSIX newlines + self._write(name, self.without_block_explicit_posix_newlines) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_windows_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_appended_explicit_posix_newlines) + # Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content_explicit_windows_newlines, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_if_not_found=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_block_appended_explicit_posix_newlines) + + @with_tempfile + def test_non_matching_block(self, name): + ''' + Test blockreplace when block exists but its contents are not a + match. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_non_matching_block_append_newline(self, name): + ''' + Test blockreplace when block exists but its contents are not a + match. Test with append_newline explicitly set to True. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + + # Pass 2: content does not end in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_non_matching_block_no_append_newline(self, name): + ''' + Test blockreplace when block exists but its contents are not a + match. Test with append_newline explicitly set to False. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write(name, self.with_non_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + + @with_tempfile + def test_non_matching_block_and_marker_not_after_newline(self, name): + ''' + Test blockreplace when block exists but its contents are not a + match, and the marker_end is not directly preceded by a newline. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_non_matching_block_and_marker_not_after_newline_append_newline(self, name): + ''' + Test blockreplace when block exists but its contents are not a match, + and the marker_end is not directly preceded by a newline. Test with + append_newline explicitly set to True. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_non_matching_block_and_marker_not_after_newline_no_append_newline(self, name): + ''' + Test blockreplace when block exists but its contents are not a match, + and the marker_end is not directly preceded by a newline. Test with + append_newline explicitly set to False. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_non_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + + @with_tempfile + def test_matching_block(self, name): + ''' + Test blockreplace when block exists and its contents are a match. No + changes should be made. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_matching_block_append_newline(self, name): + ''' + Test blockreplace when block exists and its contents are a match. Test + with append_newline explicitly set to True. This will result in an + extra newline when the content ends in a newline, and will not when the + content does not end in a newline. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + + # Pass 2: content does not end in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_matching_block_no_append_newline(self, name): + ''' + Test blockreplace when block exists and its contents are a match. Test + with append_newline explicitly set to False. This will result in the + marker_end not being directly preceded by a newline when the content + does not end in a newline. + ''' + # Pass 1: content ends in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write(name, self.with_matching_block) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + + @with_tempfile + def test_matching_block_and_marker_not_after_newline(self, name): + ''' + Test blockreplace when block exists and its contents are a match, but + the marker_end is not directly preceded by a newline. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_matching_block_and_marker_not_after_newline_append_newline(self, name): + ''' + Test blockreplace when block exists and its contents are a match, but + the marker_end is not directly preceded by a newline. Test with + append_newline explicitly set to True. This will result in an extra + newline when the content ends in a newline, and will not when the + content does not end in a newline. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_extra_newline) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=True) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + @with_tempfile + def test_matching_block_and_marker_not_after_newline_no_append_newline(self, name): + ''' + Test blockreplace when block exists and its contents are a match, but + the marker_end is not directly preceded by a newline. Test with + append_newline explicitly set to False. + ''' + # Pass 1: content ends in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertTrue(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + # Pass 1a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content, + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual(self._read(name), self.with_matching_block) + + # Pass 2: content does not end in newline + self._write( + name, + self.with_matching_block_and_marker_end_not_after_newline) + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) + # Pass 2a: Re-run state, no changes should be made + ret = self.run_state('file.blockreplace', + name=name, + content=self.content.rstrip('\r\n'), + marker_start=self.marker_start, + marker_end=self.marker_end, + append_newline=False) + self.assertSaltTrueReturn(ret) + self.assertFalse(ret[next(iter(ret))]['changes']) + self.assertEqual( + self._read(name), + self.with_matching_block_and_marker_end_not_after_newline) class RemoteFileTest(ModuleCase, SaltReturnAssertsMixin): diff --git a/tests/support/helpers.py b/tests/support/helpers.py index 48b8afb35e..fcc5fc3cd9 100644 --- a/tests/support/helpers.py +++ b/tests/support/helpers.py @@ -22,6 +22,7 @@ import os import signal import socket import sys +import tempfile import threading import time import tornado.ioloop @@ -48,7 +49,7 @@ except ImportError: # Import Salt Tests Support libs from tests.support.unit import skip, _id from tests.support.mock import patch -from tests.support.paths import FILES +from tests.support.paths import FILES, TMP log = logging.getLogger(__name__) @@ -947,6 +948,24 @@ def with_system_user_and_group(username, group, return decorator +def with_tempfile(func): + ''' + Generates a tempfile and cleans it up when test completes. + ''' + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + fd_, name = tempfile.mkstemp(prefix='__salt.test.', dir=TMP) + os.close(fd_) + del fd_ + ret = func(self, name, *args, **kwargs) + try: + os.remove(name) + except Exception: + pass + return ret + return wrapper + + def requires_system_grains(func): ''' Function decorator which loads and passes the system's grains to the test diff --git a/tests/unit/modules/test_file.py b/tests/unit/modules/test_file.py index e10accd3e5..1b4c46a811 100644 --- a/tests/unit/modules/test_file.py +++ b/tests/unit/modules/test_file.py @@ -265,10 +265,11 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): "We shall say 'Ni' again to you, if you do not appease us." ]) filemod.blockreplace(self.tfile.name, - '#-- START BLOCK 1', - '#-- END BLOCK 1', - new_multiline_content, - backup=False) + marker_start='#-- START BLOCK 1', + marker_end='#-- END BLOCK 1', + content=new_multiline_content, + backup=False, + append_newline=None) with salt.utils.fopen(self.tfile.name, 'rb') as fp: filecontent = fp.read() @@ -286,9 +287,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): CommandExecutionError, filemod.blockreplace, self.tfile.name, - '#-- START BLOCK 2', - '#-- END BLOCK 2', - new_content, + marker_start='#-- START BLOCK 2', + marker_end='#-- END BLOCK 2', + content=new_content, append_if_not_found=False, backup=False ) @@ -298,9 +299,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): + '#-- END BLOCK 2', fp.read()) filemod.blockreplace(self.tfile.name, - '#-- START BLOCK 2', - '#-- END BLOCK 2', - new_content, + marker_start='#-- START BLOCK 2', + marker_end='#-- END BLOCK 2', + content=new_content, backup=False, append_if_not_found=True) @@ -358,9 +359,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): CommandExecutionError, filemod.blockreplace, self.tfile.name, - '#-- START BLOCK 2', - '#-- END BLOCK 2', - new_content, + marker_start='#-- START BLOCK 2', + marker_end='#-- END BLOCK 2', + content=new_content, prepend_if_not_found=False, backup=False ) @@ -372,8 +373,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): fp.read()) filemod.blockreplace(self.tfile.name, - '#-- START BLOCK 2', '#-- END BLOCK 2', - new_content, + marker_start='#-- START BLOCK 2', + marker_end='#-- END BLOCK 2', + content=new_content, backup=False, prepend_if_not_found=True) @@ -386,9 +388,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): def test_replace_partial_marked_lines(self): filemod.blockreplace(self.tfile.name, - '// START BLOCK', - '// END BLOCK', - 'new content 1', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 1', backup=False) with salt.utils.fopen(self.tfile.name, 'r') as fp: @@ -396,7 +398,7 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): self.assertIn('new content 1', filecontent) self.assertNotIn('to be removed', filecontent) self.assertIn('first part of start line', filecontent) - self.assertIn('first part of end line', filecontent) + self.assertNotIn('first part of end line', filecontent) self.assertIn('part of start line not removed', filecontent) self.assertIn('part of end line not removed', filecontent) @@ -406,7 +408,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): filemod.blockreplace( self.tfile.name, - '// START BLOCK', '// END BLOCK', 'new content 2', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 2', backup=fext) self.assertTrue(os.path.exists(bak_file)) @@ -417,22 +421,27 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): bak_file = '{0}{1}'.format(self.tfile.name, fext) filemod.blockreplace(self.tfile.name, - '// START BLOCK', '// END BLOCK', 'new content 3', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 3', backup=False) self.assertFalse(os.path.exists(bak_file)) def test_no_modifications(self): filemod.blockreplace(self.tfile.name, - '// START BLOCK', '// END BLOCK', - 'new content 4', - backup=False) + marker_start='#-- START BLOCK 1', + marker_end='#-- END BLOCK 1', + content='new content 4', + backup=False, + append_newline=None) before_ctime = os.stat(self.tfile.name).st_mtime filemod.blockreplace(self.tfile.name, - '// START BLOCK', - '// END BLOCK', - 'new content 4', - backup=False) + marker_start='#-- START BLOCK 1', + marker_end='#-- END BLOCK 1', + content='new content 4', + backup=False, + append_newline=None) after_ctime = os.stat(self.tfile.name).st_mtime self.assertEqual(before_ctime, after_ctime) @@ -440,9 +449,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): def test_dry_run(self): before_ctime = os.stat(self.tfile.name).st_mtime filemod.blockreplace(self.tfile.name, - '// START BLOCK', - '// END BLOCK', - 'new content 5', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 5', dry_run=True) after_ctime = os.stat(self.tfile.name).st_mtime @@ -450,18 +459,18 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): def test_show_changes(self): ret = filemod.blockreplace(self.tfile.name, - '// START BLOCK', - '// END BLOCK', - 'new content 6', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 6', backup=False, show_changes=True) self.assertTrue(ret.startswith('---')) # looks like a diff ret = filemod.blockreplace(self.tfile.name, - '// START BLOCK', - '// END BLOCK', - 'new content 7', + marker_start='// START BLOCK', + marker_end='// END BLOCK', + content='new content 7', backup=False, show_changes=False) @@ -472,9 +481,9 @@ class FileBlockReplaceTestCase(TestCase, LoaderModuleMockMixin): CommandExecutionError, filemod.blockreplace, self.tfile.name, - '#-- START BLOCK UNFINISHED', - '#-- END BLOCK UNFINISHED', - 'foobar', + marker_start='#-- START BLOCK UNFINISHED', + marker_end='#-- END BLOCK UNFINISHED', + content='foobar', backup=False ) From 6bfdc0f0dc2af159a931e25faf3d9bc3e6e74626 Mon Sep 17 00:00:00 2001 From: denza Date: Thu, 8 Mar 2018 05:50:17 +0100 Subject: [PATCH 032/117] Support for a volume password --- doc/topics/cloud/profitbricks.rst | 8 ++++++++ salt/cloud/clouds/profitbricks.py | 19 +++++++++++++------ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/doc/topics/cloud/profitbricks.rst b/doc/topics/cloud/profitbricks.rst index 4b08087656..17beb06e31 100644 --- a/doc/topics/cloud/profitbricks.rst +++ b/doc/topics/cloud/profitbricks.rst @@ -104,6 +104,7 @@ Here is an example of a profile: profitbricks_production: provider: my-profitbricks-config image: Ubuntu-15.10-server-2016-05-01 + image_password: MyPassword1 disk_type: SSD disk_size: 40 cores: 8 @@ -185,6 +186,13 @@ disk_type This option allow the disk type to be set to HDD or SSD. The default is HDD. +image_password + A password is set on the image for the "root" or "Administrator" account. + This field may only be set during volume creation. Only valid with + ProfitBricks supplied HDD (not ISO) images. The password must contain at + least 8 and no more than 50 characters. Only these characters are + allowed: [a-z][A-Z][0-9] + cores This option allows you to override the number of CPU cores as defined by the size. diff --git a/salt/cloud/clouds/profitbricks.py b/salt/cloud/clouds/profitbricks.py index 25bcfd723c..998dac647a 100644 --- a/salt/cloud/clouds/profitbricks.py +++ b/salt/cloud/clouds/profitbricks.py @@ -348,7 +348,8 @@ def get_size(vm_): return sizes['Small Instance'] for size in sizes: - if vm_size and six.text_type(vm_size) in (six.text_type(sizes[size]['id']), six.text_type(size)): + combinations = (six.text_type(sizes[size]['id']), six.text_type(size)) + if vm_size and six.text_type(vm_size) in combinations: return sizes[size] raise SaltCloudNotFound( 'The specified size, \'{0}\', could not be found.'.format(vm_size) @@ -568,7 +569,8 @@ def list_nodes(conn=None, call=None): try: nodes = conn.list_servers(datacenter_id=datacenter_id) except PBNotFoundError: - log.error('Failed to get nodes list from datacenter: %s', datacenter_id) + log.error('Failed to get nodes list ' + 'from datacenter: %s', datacenter_id) raise for item in nodes['items']: @@ -1112,8 +1114,6 @@ def _get_system_volume(vm_): ''' Construct VM system volume list from cloud profile config ''' - # Retrieve list of SSH public keys - ssh_keys = get_public_keys(vm_) # Override system volume size if 'disk_size' is defined in cloud profile disk_size = get_size(vm_)['disk'] @@ -1124,10 +1124,17 @@ def _get_system_volume(vm_): volume = Volume( name='{0} Storage'.format(vm_['name']), size=disk_size, - disk_type=get_disk_type(vm_), - ssh_keys=ssh_keys + disk_type=get_disk_type(vm_) ) + if 'image_password' in vm_: + image_password = vm_['image_password'] + volume.image_password = image_password + else: + # Retrieve list of SSH public keys + ssh_keys = get_public_keys(vm_) + volume.ssh_keys = ssh_keys + if 'image_alias' in vm_.keys(): volume.image_alias = vm_['image_alias'] else: From 584b451fd15a35319d451ab5efad5102bd7e136a Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Fri, 2 Mar 2018 11:14:35 +0100 Subject: [PATCH 033/117] Support dynamic pillar_root environment Allow users to specify a __env__ pillar_root directory that applies equally to all environments that are not explicitly specified. fixes #20581 Signed-off-by: Benjamin Drung --- doc/topics/pillar/index.rst | 22 ++++++++++++++++++++++ salt/pillar/__init__.py | 9 +++++++++ tests/unit/test_pillar.py | 27 +++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) diff --git a/doc/topics/pillar/index.rst b/doc/topics/pillar/index.rst index b76e698ca5..f649d0bf40 100644 --- a/doc/topics/pillar/index.rst +++ b/doc/topics/pillar/index.rst @@ -168,6 +168,28 @@ And the actual pillar file at '/srv/pillar/common_pillar.sls': context. +Dynamic Pillar Environments +=========================== + +If environment ``__env__`` is specified in :conf_master:`pillar_roots`, all +environments that are not explicitly specified in :conf_master:`pillar_roots` +will map to the directories from ``__env__``. This allows one to use dynamic +git branch based environments for state/pillar files with the same file-based +pillar applying to all environments. For example: + +.. code-block:: yaml + + pillar_roots: + __env__: + - /srv/pillar + + ext_pillar: + - git: + - __env__ https://example.com/git-pillar.git + +.. versionadded:: 2017.7.5,2018.3.1 + + Pillar Namespace Flattening =========================== diff --git a/salt/pillar/__init__.py b/salt/pillar/__init__.py index 1c43c4efb7..7741661cae 100644 --- a/salt/pillar/__init__.py +++ b/salt/pillar/__init__.py @@ -374,6 +374,15 @@ class Pillar(object): opts['ext_pillar'].append(self.ext) else: opts['ext_pillar'] = [self.ext] + if '__env__' in opts['file_roots']: + env = opts.get('pillarenv') or opts.get('saltenv') or 'base' + if env not in opts['file_roots']: + log.debug("pillar environment '%s' maps to __env__ pillar_roots directory", env) + opts['file_roots'][env] = opts['file_roots'].pop('__env__') + else: + log.debug("pillar_roots __env__ ignored (environment '%s' found in pillar_roots)", + env) + opts['file_roots'].pop('__env__') return opts def _get_envs(self): diff --git a/tests/unit/test_pillar.py b/tests/unit/test_pillar.py index b4365e8fb2..40c7c0af39 100644 --- a/tests/unit/test_pillar.py +++ b/tests/unit/test_pillar.py @@ -49,6 +49,33 @@ class PillarTestCase(TestCase): self.assertEqual(pillar.opts['environment'], 'dev') self.assertEqual(pillar.opts['pillarenv'], 'dev') + def test_dynamic_pillarenv(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': {'__env__': '/srv/pillar/__env__', 'base': '/srv/pillar/base'}, + 'file_roots': {'base': '/srv/salt/base', 'dev': '/svr/salt/dev'}, + 'extension_modules': '', + } + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='dev') + self.assertEqual(pillar.opts['file_roots'], + {'base': '/srv/pillar/base', 'dev': '/srv/pillar/__env__'}) + + def test_ignored_dynamic_pillarenv(self): + opts = { + 'renderer': 'json', + 'renderer_blacklist': [], + 'renderer_whitelist': [], + 'state_top': '', + 'pillar_roots': {'__env__': '/srv/pillar/__env__', 'base': '/srv/pillar/base'}, + 'file_roots': {'base': '/srv/salt/base', 'dev': '/svr/salt/dev'}, + 'extension_modules': '', + } + pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='base') + self.assertEqual(pillar.opts['file_roots'], {'base': '/srv/pillar/base'}) + def test_malformed_pillar_sls(self): with patch('salt.pillar.compile_template') as compile_template: opts = { From a3c54b50f6f72badfc3adfd5505a8341beb55421 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Thu, 8 Mar 2018 10:42:02 +0100 Subject: [PATCH 034/117] Fix various spelling mistakes lintian found various spelling mistakes. Signed-off-by: Benjamin Drung --- doc/ref/configuration/master.rst | 2 +- doc/ref/configuration/proxy.rst | 4 +-- doc/ref/internals/fileserver-and-client.rst | 2 +- doc/ref/states/parallel.rst | 2 +- doc/topics/cloud/aws.rst | 2 +- doc/topics/cloud/cloudstack.rst | 2 +- doc/topics/development/tests/index.rst | 2 +- doc/topics/development/tests/unit.rst | 2 +- doc/topics/installation/solaris.rst | 2 +- doc/topics/jobs/external_cache.rst | 2 +- doc/topics/reactor/index.rst | 8 +++--- doc/topics/releases/2015.5.3.rst | 4 +-- doc/topics/releases/2016.11.4.rst | 2 +- doc/topics/releases/2016.11.6.rst | 6 ++-- doc/topics/releases/2016.11.8.rst | 10 +++---- doc/topics/releases/2016.11.9.rst | 12 ++++---- doc/topics/releases/2017.7.0.rst | 10 +++---- doc/topics/releases/2017.7.2.rst | 14 ++++----- doc/topics/releases/2017.7.3.rst | 32 ++++++++++----------- doc/topics/spm/spm_formula.rst | 2 +- pkg/suse/salt.changes | 2 +- salt/beacons/diskusage.py | 2 +- salt/beacons/load.py | 2 +- salt/cache/redis_cache.py | 2 +- salt/client/ssh/ssh_py_shim.py | 2 +- salt/engines/hipchat.py | 4 +-- salt/fileserver/__init__.py | 2 +- salt/grains/napalm.py | 2 +- salt/modules/bcache.py | 2 +- salt/modules/boto_vpc.py | 2 +- salt/modules/capirca_acl.py | 2 +- salt/modules/dockermod.py | 2 +- salt/modules/etcd_mod.py | 2 +- salt/modules/glusterfs.py | 2 +- salt/modules/junos.py | 2 +- salt/modules/kubernetes.py | 4 +-- salt/modules/logmod.py | 2 +- salt/modules/lxc.py | 2 +- salt/modules/mssql.py | 2 +- salt/modules/napalm_acl.py | 6 ++-- salt/modules/napalm_network.py | 8 +++--- salt/modules/napalm_route.py | 2 +- salt/modules/napalm_yang_mod.py | 2 +- salt/modules/pdbedit.py | 2 +- salt/modules/selinux.py | 2 +- salt/modules/sensehat.py | 2 +- salt/modules/smartos_vmadm.py | 2 +- salt/modules/solrcloud.py | 2 +- salt/modules/win_groupadd.py | 4 +-- salt/modules/win_system.py | 2 +- salt/netapi/rest_cherrypy/app.py | 4 +-- salt/renderers/pyobjects.py | 2 +- salt/returners/elasticsearch_return.py | 2 +- salt/runners/bgp.py | 2 +- salt/runners/manage.py | 2 +- salt/state.py | 2 +- salt/states/docker_container.py | 2 +- salt/states/elasticsearch.py | 6 ++-- salt/states/grafana4_datasource.py | 2 +- salt/states/heat.py | 2 +- salt/states/http.py | 2 +- salt/states/junos.py | 4 +-- salt/states/netacl.py | 14 ++++----- salt/states/netconfig.py | 10 +++---- salt/states/netyang.py | 6 ++-- salt/states/pdbedit.py | 2 +- salt/states/pip_state.py | 2 +- salt/states/pkg.py | 4 +-- salt/states/rabbitmq_user.py | 2 +- salt/states/smartos.py | 2 +- salt/states/win_lgpo.py | 2 +- salt/states/zfs.py | 2 +- salt/states/zone.py | 6 ++-- salt/utils/docker/__init__.py | 2 +- salt/utils/napalm.py | 6 ++-- salt/utils/schema.py | 2 +- salt/version.py | 2 +- tests/integration/runners/test_state.py | 2 +- tests/unit/cloud/clouds/__init__.py | 2 +- tests/unit/states/test_boto_apigateway.py | 2 +- tests/unit/states/test_elasticsearch.py | 6 ++-- tests/unit/utils/test_cache.py | 2 +- 82 files changed, 154 insertions(+), 154 deletions(-) diff --git a/doc/ref/configuration/master.rst b/doc/ref/configuration/master.rst index 01c5fbbcc3..d1a0eaa0b6 100644 --- a/doc/ref/configuration/master.rst +++ b/doc/ref/configuration/master.rst @@ -519,7 +519,7 @@ Default: ``0`` Memcache is an additional cache layer that keeps a limited amount of data fetched from the minion data cache for a limited period of time in memory that -makes cache operations faster. It doesn't make much sence for the ``localfs`` +makes cache operations faster. It doesn't make much sense for the ``localfs`` cache driver but helps for more complex drivers like ``consul``. This option sets the memcache items expiration time. By default is set to ``0`` diff --git a/doc/ref/configuration/proxy.rst b/doc/ref/configuration/proxy.rst index e55f3fc01b..5e871a3aa5 100644 --- a/doc/ref/configuration/proxy.rst +++ b/doc/ref/configuration/proxy.rst @@ -108,7 +108,7 @@ The frequency of keepalive checks, in minutes. It requires the Default: ``True`` -Wheter the proxy should maintain the connection with the remote +Whether the proxy should maintain the connection with the remote device. Similarly to :conf_proxy:`proxy_keep_alive`, this option is very specific to the design of the proxy module. When :conf_proxy:`proxy_always_alive` is set to ``False``, @@ -126,7 +126,7 @@ has to be closed after every command. Default: ``False``. -Wheter the pillar data to be merged into the proxy configuration options. +Whether the pillar data to be merged into the proxy configuration options. As multiple proxies can run on the same server, we may need different configuration options for each, while there's one single configuration file. The solution is merging the pillar data of each proxy minion into the opts. diff --git a/doc/ref/internals/fileserver-and-client.rst b/doc/ref/internals/fileserver-and-client.rst index 938ad9f2bf..da6e3e3bd2 100644 --- a/doc/ref/internals/fileserver-and-client.rst +++ b/doc/ref/internals/fileserver-and-client.rst @@ -6,7 +6,7 @@ The Salt Fileserver and Client Introduction ------------ -Salt has a modular fileserver, and mulitple client classes which are used to +Salt has a modular fileserver, and multiple client classes which are used to interact with it. This page serves as a developer's reference, to help explain how the fileserver and clients both work. diff --git a/doc/ref/states/parallel.rst b/doc/ref/states/parallel.rst index 9edf1750e4..dfe21cc58a 100644 --- a/doc/ref/states/parallel.rst +++ b/doc/ref/states/parallel.rst @@ -12,7 +12,7 @@ option to your state declaration: service.running: - parallel: True -Now ``nginx`` will be started in a seperate process from the normal state run +Now ``nginx`` will be started in a separate process from the normal state run and will therefore not block additional states. Parallel States and Requisites diff --git a/doc/topics/cloud/aws.rst b/doc/topics/cloud/aws.rst index 26b06c976a..7f0ec98793 100644 --- a/doc/topics/cloud/aws.rst +++ b/doc/topics/cloud/aws.rst @@ -1019,7 +1019,7 @@ so:- - AndThirdSecurityGroup Note that 'subnetid' takes precedence over 'subnetname', but 'securitygroupid' -and 'securitygroupname' are merged toghether to generate a single list for +and 'securitygroupname' are merged together to generate a single list for SecurityGroups of instances. Specifying interface properties diff --git a/doc/topics/cloud/cloudstack.rst b/doc/topics/cloud/cloudstack.rst index 28d87eb32a..0e8325ac1f 100644 --- a/doc/topics/cloud/cloudstack.rst +++ b/doc/topics/cloud/cloudstack.rst @@ -156,7 +156,7 @@ security_group ~~~~~~~~~~~~~~ .. versionadded:: next-release -You can specifiy a list of security groups (by name or id) that should be +You can specify a list of security groups (by name or id) that should be assigned to the VM. .. code-block:: yaml diff --git a/doc/topics/development/tests/index.rst b/doc/topics/development/tests/index.rst index 2679ebf0fd..a260cc6e12 100644 --- a/doc/topics/development/tests/index.rst +++ b/doc/topics/development/tests/index.rst @@ -436,7 +436,7 @@ external resource, like a cloud virtual machine. This decorator is not normally used by developers outside of the Salt core team. `@destructiveTest` -- Marks a test as potentially destructive. It will not be run -by the test runner unles the ``-run-destructive`` test is expressly passed. +by the test runner unless the ``-run-destructive`` test is expressly passed. `@requires_network` -- Requires a network connection for the test to operate successfully. If a network connection is not detected, the test will not run. diff --git a/doc/topics/development/tests/unit.rst b/doc/topics/development/tests/unit.rst index cf8b2349e0..4f56f5348d 100644 --- a/doc/topics/development/tests/unit.rst +++ b/doc/topics/development/tests/unit.rst @@ -98,7 +98,7 @@ Mocking Loader Modules Salt loader modules use a series of globally available dunder variables, ``__salt__``, ``__opts__``, ``__pillar__``, etc. To facilitate testing these modules a mixin class was created, ``LoaderModuleMockMixin`` which can be found -in ``tests/support/mixins.py``. The reason for the existance of this class is +in ``tests/support/mixins.py``. The reason for the existence of this class is because historiclly and because it was easier, one would add these dunder variables directly on the imported module. This however, introduces unexpected behavior when running the full test suite since those attributes would not be diff --git a/doc/topics/installation/solaris.rst b/doc/topics/installation/solaris.rst index 549f053fb2..1179dea287 100644 --- a/doc/topics/installation/solaris.rst +++ b/doc/topics/installation/solaris.rst @@ -17,4 +17,4 @@ For example, to install the develop version of salt: .. note:: - SaltStack does offer commerical support for Solaris which includes packages. + SaltStack does offer commercial support for Solaris which includes packages. diff --git a/doc/topics/jobs/external_cache.rst b/doc/topics/jobs/external_cache.rst index 7e3bbb8549..c3b7431b7f 100644 --- a/doc/topics/jobs/external_cache.rst +++ b/doc/topics/jobs/external_cache.rst @@ -18,7 +18,7 @@ and others): The major difference between these two mechanism is from where results are returned (from the Salt Master or Salt Minion). Configuring either of these options will also make the :py:mod:`Jobs Runner functions ` -to automatically query the remote stores for infomation. +to automatically query the remote stores for information. External Job Cache - Minion-Side Returner ----------------------------------------- diff --git a/doc/topics/reactor/index.rst b/doc/topics/reactor/index.rst index de5df946ac..f7515d3f50 100644 --- a/doc/topics/reactor/index.rst +++ b/doc/topics/reactor/index.rst @@ -68,7 +68,7 @@ and each event tag has a list of reactor SLS files to be run. Reactor SLS files are similar to State and Pillar SLS files. They are by default YAML + Jinja templates and are passed familiar context variables. Click :ref:`here ` for more detailed information on the -variables availble in Jinja templating. +variables available in Jinja templating. Here is the SLS for a simple reaction: @@ -179,7 +179,7 @@ The below two examples are equivalent: | | fromrepo: updates | +---------------------------------+-----------------------------+ -This reaction would be equvalent to running the following Salt command: +This reaction would be equivalent to running the following Salt command: .. code-block:: bash @@ -230,7 +230,7 @@ The below two examples are equivalent: +-------------------------------------------------+-------------------------------------------------+ Assuming that the event tag is ``foo``, and the data passed to the event is -``{'bar': 'baz'}``, then this reaction is equvalent to running the following +``{'bar': 'baz'}``, then this reaction is equivalent to running the following Salt command: .. code-block:: bash @@ -295,7 +295,7 @@ The below two examples are equivalent: | - name: /tmp/foo | - /tmp/foo | +---------------------------------+---------------------------+ -This reaction is equvalent to running the following Salt command: +This reaction is equivalent to running the following Salt command: .. code-block:: bash diff --git a/doc/topics/releases/2015.5.3.rst b/doc/topics/releases/2015.5.3.rst index 6de48e07bb..8bce6e38c1 100644 --- a/doc/topics/releases/2015.5.3.rst +++ b/doc/topics/releases/2015.5.3.rst @@ -1251,12 +1251,12 @@ Changes: - **PR** `#24456`_: (*rallytime*) Back-port `#24441`_ to 2015.5 @ *2015-06-05T22:32:25Z* - - **PR** `#24441`_: (*arthurlogilab*) [doc] Alignement fix on external_auth documentation + - **PR** `#24441`_: (*arthurlogilab*) [doc] Alignment fix on external_auth documentation | refs: `#24456`_ * ced558a Merge pull request `#24456`_ from rallytime/`bp-24441`_ * 7002855 yaml indentations should be 2 spaces - * 21b51ab [doc] Alignement fix on external_auth documentation + * 21b51ab [doc] Alignment fix on external_auth documentation - **PR** `#24398`_: (*kiorky*) VirtualName for states.apt | refs: `#24399`_ diff --git a/doc/topics/releases/2016.11.4.rst b/doc/topics/releases/2016.11.4.rst index 6a3411994f..f29ddea03f 100644 --- a/doc/topics/releases/2016.11.4.rst +++ b/doc/topics/releases/2016.11.4.rst @@ -21,7 +21,7 @@ Minion Data Cache Fixes Added Memcache booster for the minion data cache. Memcache is an additional cache layer that keeps a limited amount of data fetched from the minion data cache for a limited period of time in memory that -makes cache operations faster. It doesn't make much sence for the ``localfs`` +makes cache operations faster. It doesn't make much sense for the ``localfs`` cache driver but helps for more complex drivers like ``consul``. For more details see ``memcache_expire_seconds`` and other ``memcache_*`` options in the master config reverence. diff --git a/doc/topics/releases/2016.11.6.rst b/doc/topics/releases/2016.11.6.rst index dda24ba5c7..b48286f4a7 100644 --- a/doc/topics/releases/2016.11.6.rst +++ b/doc/topics/releases/2016.11.6.rst @@ -518,7 +518,7 @@ Changes: * ef8e3ef569 Update win_pki.py -- **PR** `#41557`_: (*dmurphy18*) Add symbolic link for salt-proxy service similar to other serivce files +- **PR** `#41557`_: (*dmurphy18*) Add symbolic link for salt-proxy service similar to other service files @ *2017-06-06T17:13:52Z* * 3335fcbc7d Merge pull request `#41557`_ from dmurphy18/fix-proxy-service @@ -753,7 +753,7 @@ Changes: * 66ab1e5184 Re-adding neutron dependency check - * cce07eefc2 Updating Neutron module to suport KeystoneAuth + * cce07eefc2 Updating Neutron module to support KeystoneAuth - **PR** `#41409`_: (*garethgreenaway*) Fixes to ipc transport @ *2017-05-25T21:06:27Z* @@ -926,7 +926,7 @@ Changes: - **ISSUE** `#41306`_: (*lomeroe*) win_lgpo does not properly pack group policy version number in gpt.ini | refs: `#41319`_ `#41307`_ - - **PR** `#41307`_: (*lomeroe*) properly pack/unpack the verison numbers into a number + - **PR** `#41307`_: (*lomeroe*) properly pack/unpack the version numbers into a number | refs: `#41319`_ * 140b0427e1 Merge pull request `#41319`_ from lomeroe/bp_41307 * 4f0aa577a5 backport 41307 to 2016.11, properly pack version numbers into single number diff --git a/doc/topics/releases/2016.11.8.rst b/doc/topics/releases/2016.11.8.rst index c726d61ee9..bfcf75e040 100644 --- a/doc/topics/releases/2016.11.8.rst +++ b/doc/topics/releases/2016.11.8.rst @@ -632,7 +632,7 @@ Changes: * 3072576 Merge pull request `#42629`_ from xiaoanyunfei/tornadoapi * 1e13383 tornado api -- **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy +- **PR** `#42655`_: (*whiteinge*) Re-enable cpstats for rest_cherrypy @ *2017-08-03T20:44:10Z* - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug @@ -640,7 +640,7 @@ Changes: * f0f00fc Merge pull request `#42655`_ from whiteinge/rest_cherrypy-reenable-stats * deb6316 Fix lint errors - * 6bd91c8 Reenable cpstats for rest_cherrypy + * 6bd91c8 Re-enable cpstats for rest_cherrypy - **PR** `#42693`_: (*gilbsgilbs*) Fix RabbitMQ tags not properly set. @ *2017-08-03T20:23:08Z* @@ -847,11 +847,11 @@ Changes: * 42bb1a6 Merge pull request `#42350`_ from twangboy/win_fix_ver_grains_2016.11 * 8c04840 Detect Server OS with a desktop release name -- **PR** `#42356`_: (*meaksh*) Allow to check whether a function is available on the AliasesLoader wrapper +- **PR** `#42356`_: (*meaksh*) Allow checking whether a function is available on the AliasesLoader wrapper @ *2017-07-19T16:56:41Z* * 0a72e56 Merge pull request `#42356`_ from meaksh/2016.11-AliasesLoader-wrapper-fix - * 915d942 Allow to check whether a function is available on the AliasesLoader wrapper + * 915d942 Allow checking whether a function is available on the AliasesLoader wrapper - **PR** `#42368`_: (*twangboy*) Remove build and dist directories before install (2016.11) @ *2017-07-19T16:47:28Z* @@ -1392,7 +1392,7 @@ Changes: * 7f69613 test and lint fixes - * 8ee4843 Suppress output of crypt context and be more specifc with whitespace vs. serial + * 8ee4843 Suppress output of crypt context and be more specific with whitespace vs. serial * 61f817d Match serials based on output position (fix for non-English languages) diff --git a/doc/topics/releases/2016.11.9.rst b/doc/topics/releases/2016.11.9.rst index 69c261c437..08480b4c25 100644 --- a/doc/topics/releases/2016.11.9.rst +++ b/doc/topics/releases/2016.11.9.rst @@ -29,7 +29,7 @@ Significate changes (PR #43708 & #45390, damon-atkins) have been made to the pkg - ``pkg.install`` without a ``version`` parameter no longer upgrades software if the software is already installed. Use ``pkg.install version=latest`` or in a state use ``pkg.latest`` to get the old behavior. - ``pkg.list_pkgs`` now returns multiple versions if software installed more than once. - ``pkg.list_pkgs`` now returns 'Not Found' when the version is not found instead of '(value not set)' which matches the contents of the sls definitions. -- ``pkg.remove()`` will wait upto 3 seconds (normally about a second) to detect changes in the registry after removing software, improving reporting of version changes. +- ``pkg.remove()`` will wait up to 3 seconds (normally about a second) to detect changes in the registry after removing software, improving reporting of version changes. - ``pkg.remove()`` can remove ``latest`` software, if ``latest`` is defined in sls definition. - Documentation was update for the execution module to match the style in new versions, some corrections as well. - All install/remove commands are prefix with cmd.exe shell and cmdmod is called with a command line string instead of a list. Some sls files in saltstack/salt-winrepo-ng expected the commands to be prefixed with cmd.exe (i.e. the use of ``&``). @@ -407,7 +407,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' @ *2017-11-28T21:50:19Z* * 998d714ee7 Merge pull request `#44517`_ from whytewolf/publish_port_doc_missing - * 4b5855283a missed one place where i didnt chanbge master_port from my copy to publish_port + * 4b5855283a missed one place where i didn't change master_port from my copy to publish_port * e4610baea5 update doc to have publish port @@ -598,7 +598,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' @ *2017-10-31T17:56:34Z* * cab54e34b5 Merge pull request `#44173`_ from twangboy/win_system_docs - * 8e111b413d Fix some of the wording and grammer errors + * 8e111b413d Fix some of the wording and grammar errors * a12bc5ae41 Use google style docstrings @@ -831,7 +831,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' - **ISSUE** `#43581`_: (*jcourington*) cherrypy stats issue | refs: `#44021`_ - - **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + - **PR** `#42655`_: (*whiteinge*) Re-enable cpstats for rest_cherrypy | refs: `#44021`_ - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug | refs: `#42655`_ @@ -1001,13 +1001,13 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * ea8d273c2b Merge pull request `#43768`_ from vutny/fix-pylint-deprecation-warnings * f8b3fa9da1 Merge branch '2016.11' into fix-pylint-deprecation-warnings -- **PR** `#43772`_: (*gtmanfred*) dont print Minion not responding with quiet +- **PR** `#43772`_: (*gtmanfred*) don't print Minion not responding with quiet @ *2017-09-27T15:39:18Z* - **ISSUE** `#40311`_: (*cralston0*) --hide-timeout used with --output json --static produces unparseable JSON | refs: `#43772`_ * 1a8cc60bb4 Merge pull request `#43772`_ from gtmanfred/2016.11 - * 0194c60960 dont print Minion not responding with quiet + * 0194c60960 don't print Minion not responding with quiet - **PR** `#43747`_: (*rallytime*) Add GPG Verification section to Contributing Docs @ *2017-09-26T21:25:37Z* diff --git a/doc/topics/releases/2017.7.0.rst b/doc/topics/releases/2017.7.0.rst index 1681101522..85c1c9fe00 100644 --- a/doc/topics/releases/2017.7.0.rst +++ b/doc/topics/releases/2017.7.0.rst @@ -41,7 +41,7 @@ Salt's policy has always been that when upgrading, the minion should never be on a newer version than the master. Specifically with this update, because of changes in the fileclient, the 2017.7 minion requires a 2017.7 master. -Backwards compatiblity is still maintained, so older minions can still be used. +Backwards compatibility is still maintained, so older minions can still be used. More information can be found in the :ref:`Salt FAQ` @@ -54,7 +54,7 @@ The :py:func:`service.masked ` and added to allow Salt to manage masking of systemd units. Additionally, the following functions in the :mod:`systemd -` execution module have changed to accomodate the fact +` execution module have changed to accommodate the fact that indefinite and runtime masks can co-exist for the same unit: - :py:func:`service.masked ` - The return from @@ -152,7 +152,7 @@ State Module Changes In a rare case that you have a function that needs to be called several times but with the different parameters, an additional feature of "tagging" is to the - rescue. In order to tag a function, use a colon delimeter. For example: + rescue. In order to tag a function, use a colon delimiter. For example: .. code-block:: yaml @@ -281,7 +281,7 @@ Minion Configuration Additions salt-api Changes ================ -The ``rest_cherrypy`` netapi module has recieved a few minor improvements: +The ``rest_cherrypy`` netapi module has received a few minor improvements: * A CORS bugfix. * A new ``/token`` convenience endpoint to generate Salt eauth tokens. @@ -557,7 +557,7 @@ of objects (users, databases, roles, etc.). .. note:: With the `Moby announcement`_ coming at this year's DockerCon_, Salt's :mod:`docker ` execution module (as well as the - state modules) work interchangably when **docker** is replaced with + state modules) work interchangeably when **docker** is replaced with **moby** (e.g. :py:func:`moby_container.running `, :py:func:`moby_image.present `, :py:func:`moby.inspect_container diff --git a/doc/topics/releases/2017.7.2.rst b/doc/topics/releases/2017.7.2.rst index 65ced85367..97b55226fc 100644 --- a/doc/topics/releases/2017.7.2.rst +++ b/doc/topics/releases/2017.7.2.rst @@ -962,7 +962,7 @@ Changes - **PR** `#42884`_: (*Giandom*) Convert to dict type the pillar string value passed from slack @ *2017-08-16T22:30:43Z* - - **ISSUE** `#42842`_: (*Giandom*) retreive kwargs passed with slack engine + - **ISSUE** `#42842`_: (*Giandom*) retrieve kwargs passed with slack engine | refs: `#42884`_ * 82be9dceb6 Merge pull request `#42884`_ from Giandom/2017.7.1-fix-slack-engine-pillar-args * 80fd733c99 Update slack.py @@ -1235,13 +1235,13 @@ Changes * 4ce96eb1a1 Merge pull request `#42778`_ from gtmanfred/spm * 7ef691e8da make sure to use the correct out_file -- **PR** `#42857`_: (*gtmanfred*) use older name if _create_unverified_context is unvailable +- **PR** `#42857`_: (*gtmanfred*) use older name if _create_unverified_context is unavailable @ *2017-08-11T13:37:59Z* - **ISSUE** `#480`_: (*zyluo*) PEP8 types clean-up | refs: `#42857`_ * 3d05d89e09 Merge pull request `#42857`_ from gtmanfred/vmware - * c1f673eca4 use older name if _create_unverified_context is unvailable + * c1f673eca4 use older name if _create_unverified_context is unavailable - **PR** `#42866`_: (*twangboy*) Change to GitPython version 2.1.1 @ *2017-08-11T13:23:52Z* @@ -1448,7 +1448,7 @@ Changes | refs: `#42574`_ - **PR** `#42693`_: (*gilbsgilbs*) Fix RabbitMQ tags not properly set. - **PR** `#42669`_: (*garethgreenaway*) [2016.11] Fixes to augeas module - - **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + - **PR** `#42655`_: (*whiteinge*) Re-enable cpstats for rest_cherrypy - **PR** `#42629`_: (*xiaoanyunfei*) tornado api - **PR** `#42623`_: (*terminalmage*) Fix unicode constructor in custom YAML loader - **PR** `#42574`_: (*sbojarski*) Fixed error reporting in "boto_cfn.present" function. @@ -1469,7 +1469,7 @@ Changes * deb6316d67 Fix lint errors - * 6bd91c8b03 Reenable cpstats for rest_cherrypy + * 6bd91c8b03 Re-enable cpstats for rest_cherrypy * 21cf15f9c3 Merge pull request `#42693`_ from gilbsgilbs/fix-rabbitmq-tags @@ -2031,7 +2031,7 @@ Changes - **PR** `#42368`_: (*twangboy*) Remove build and dist directories before install (2016.11) - **PR** `#42360`_: (*Ch3LL*) [2016.11] Update version numbers in doc config for 2017.7.0 release - **PR** `#42359`_: (*Ch3LL*) [2016.3] Update version numbers in doc config for 2017.7.0 release - - **PR** `#42356`_: (*meaksh*) Allow to check whether a function is available on the AliasesLoader wrapper + - **PR** `#42356`_: (*meaksh*) Allow checking whether a function is available on the AliasesLoader wrapper - **PR** `#42352`_: (*CorvinM*) Multiple documentation fixes - **PR** `#42350`_: (*twangboy*) Fixes problem with Version and OS Release related grains on certain versions of Python (2016.11) - **PR** `#42319`_: (*rallytime*) Add more documentation for config options that are missing from master/minion docs @@ -2046,7 +2046,7 @@ Changes * 0a72e56f6b Merge pull request `#42356`_ from meaksh/2016.11-AliasesLoader-wrapper-fix - * 915d94219e Allow to check whether a function is available on the AliasesLoader wrapper + * 915d94219e Allow checking whether a function is available on the AliasesLoader wrapper * 10eb7b7a79 Merge pull request `#42368`_ from twangboy/win_fix_build_2016.11 diff --git a/doc/topics/releases/2017.7.3.rst b/doc/topics/releases/2017.7.3.rst index d5ca3f8071..21205a2ddc 100644 --- a/doc/topics/releases/2017.7.3.rst +++ b/doc/topics/releases/2017.7.3.rst @@ -30,7 +30,7 @@ Significate changes (PR #43708 & #45390, damon-atkins) have been made to the pkg - ``pkg.install`` without a ``version`` parameter no longer upgrades software if the software is already installed. Use ``pkg.install version=latest`` or in a state use ``pkg.latest`` to get the old behavior. - ``pkg.list_pkgs`` now returns multiple versions if software installed more than once. - ``pkg.list_pkgs`` now returns 'Not Found' when the version is not found instead of '(value not set)' which matches the contents of the sls definitions. -- ``pkg.remove()`` will wait upto 3 seconds (normally about a second) to detect changes in the registry after removing software, improving reporting of version changes. +- ``pkg.remove()`` will wait up to 3 seconds (normally about a second) to detect changes in the registry after removing software, improving reporting of version changes. - ``pkg.remove()`` can remove ``latest`` software, if ``latest`` is defined in sls definition. - Documentation was update for the execution module to match the style in new versions, some corrections as well. - All install/remove commands are prefix with cmd.exe shell and cmdmod is called with a command line string instead of a list. Some sls files in saltstack/salt-winrepo-ng expected the commands to be prefixed with cmd.exe (i.e. the use of ``&``). @@ -69,7 +69,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' - **PR** `#45664`_: (*rallytime*) Back-port `#45452`_ to 2017.7.3 @ *2018-01-24T15:33:13Z* - - **PR** `#45452`_: (*adelcast*) opkg.py: make owner fuction return value, instead of iterator + - **PR** `#45452`_: (*adelcast*) opkg.py: make owner function return value, instead of iterator | refs: `#45664`_ * 0717f7a578 Merge pull request `#45664`_ from rallytime/`bp-45452`_ * 369720677b opkg.py: make owner function return value, instead of iterator @@ -359,7 +359,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 66da9b47bc Merge pull request `#45299`_ from garethgreenaway/config_gate_auth_events * 9a15ec3430 Updating versionadded string. Fixing typo. - * edfc3dc078 Adding in documention for `auth_events` configuration option + * edfc3dc078 Adding in documentation for `auth_events` configuration option * 3ee4eabffd Fixing small typo @@ -1007,7 +1007,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 4b60b1ec84 Merge remote branch 'refs/remotes/upstream/2017.7' into 2017.7_replace_with_newer_2016.11_win_pkg - * b46f818a57 Raise a PR to fix 2016 issues commited here, fixed issues with merge. + * b46f818a57 Raise a PR to fix 2016 issues committed here, fixed issues with merge. * 32ef1e12ae Merge branch '2017.7' into 2017.7_replace_with_newer_2016.11_win_pkg @@ -1362,7 +1362,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 998d714ee7 Merge pull request `#44517`_ from whytewolf/publish_port_doc_missing - * 4b5855283a missed one place where i didnt chanbge master_port from my copy to publish_port + * 4b5855283a missed one place where i didn't change master_port from my copy to publish_port * e4610baea5 update doc to have publish port @@ -1569,9 +1569,9 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 3bb385b44e removing debugging logging - * 7f0ff5a8b0 When passing IDs on the command line convert them all the strings for later comparision. + * 7f0ff5a8b0 When passing IDs on the command line convert them all the strings for later comparison. - * 99e436add4 When looking for job ids to remove based on the tag_name the comparision was comparing an INT to a STR, so the correct job id was not being returned. + * 99e436add4 When looking for job ids to remove based on the tag_name the comparison was comparing an INT to a STR, so the correct job id was not being returned. - **PR** `#44695`_: (*gtmanfred*) pop None for runas and runas_password @ *2017-12-01T14:35:01Z* @@ -1714,7 +1714,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 88ef9f18fc ignore lint error on import - * 25427d845e convert key iterator to list as python 3 wont index an iterator + * 25427d845e convert key iterator to list as python 3 won't index an iterator * bce50154e5 Merge branch '2017.7' into improve-net-load @@ -1773,13 +1773,13 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * c6733ac1ee pop None -- **PR** `#44616`_: (*Ch3LL*) Add Non Base Environement salt:// source integration test +- **PR** `#44616`_: (*Ch3LL*) Add Non Base Environment salt:// source integration test @ *2017-11-22T16:13:54Z* * d6ccf4bb30 Merge pull request `#44616`_ from Ch3LL/nonbase_test * 80b71652e3 Merge branch '2017.7' into nonbase_test - * c9ba33432e Add Non Base Environement salt:// source integration test + * c9ba33432e Add Non Base Environment salt:// source integration test - **PR** `#44617`_: (*Ch3LL*) Add ssh thin_dir integration test @ *2017-11-22T16:12:51Z* @@ -1896,7 +1896,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 1643bb7fd4 Merge pull request `#44551`_ from cloudflare/annoying-tmpnam * ce1882943d Use salt.utils.files.mkstemp() instead - * 6689bd3b2d Dont use dangerous os.tmpnam + * 6689bd3b2d Don't use dangerous os.tmpnam * 2d6176b0bc Fx2 proxy minion: clean return, like all the other modules @@ -2151,7 +2151,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * cab54e34b5 Merge pull request `#44173`_ from twangboy/win_system_docs - * 8e111b413d Fix some of the wording and grammer errors + * 8e111b413d Fix some of the wording and grammar errors * a12bc5ae41 Use google style docstrings @@ -2728,7 +2728,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' @ *2017-10-17T15:24:19Z* * 6252f82f58 Merge pull request `#44133`_ from cachedout/fix_paralell_docs - * 8d1c1e21f0 Fix typos in paralell states docs + * 8d1c1e21f0 Fix typos in parallel states docs - **PR** `#44135`_: (*timfreund*) Insert missing verb in gitfs walkthrough @ *2017-10-17T14:32:13Z* @@ -2814,7 +2814,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' - **PR** `#44021`_: (*whiteinge*) Also catch cpstats AttributeError for bad CherryPy release ~5.6.0 - **PR** `#44010`_: (*Ch3LL*) Security Fixes for 2016.3.8 - **PR** `#43977`_: (*Ch3LL*) Add Security Notes to 2016.3.8 Release Notes - - **PR** `#42655`_: (*whiteinge*) Reenable cpstats for rest_cherrypy + - **PR** `#42655`_: (*whiteinge*) Re-enable cpstats for rest_cherrypy | refs: `#44021`_ - **PR** `#33806`_: (*cachedout*) Work around upstream cherrypy bug | refs: `#42655`_ @@ -3286,7 +3286,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' - **ISSUE** `#40311`_: (*cralston0*) --hide-timeout used with --output json --static produces unparseable JSON | refs: `#43772`_ - - **PR** `#43772`_: (*gtmanfred*) dont print Minion not responding with quiet + - **PR** `#43772`_: (*gtmanfred*) don't print Minion not responding with quiet - **PR** `#43747`_: (*rallytime*) Add GPG Verification section to Contributing Docs * 9615ca32d5 Merge pull request `#43773`_ from rallytime/merge-2017.7 * f7035ed7da Merge branch '2017.7' into merge-2017.7 @@ -3295,7 +3295,7 @@ Windows cmdmod forcing cmd to be a list (issue #43522) resolved by "cmdmod: Don' * 1a8cc60bb4 Merge pull request `#43772`_ from gtmanfred/2016.11 - * 0194c60960 dont print Minion not responding with quiet + * 0194c60960 don't print Minion not responding with quiet * 9dee896fb9 Merge pull request `#43747`_ from rallytime/gpg-verification diff --git a/doc/topics/spm/spm_formula.rst b/doc/topics/spm/spm_formula.rst index 47ed5e0c60..fe133a72ed 100644 --- a/doc/topics/spm/spm_formula.rst +++ b/doc/topics/spm/spm_formula.rst @@ -178,7 +178,7 @@ that the text following it can be evaluated properly. local States ~~~~~~~~~~~~ -``local`` states are evaluated locally; this is analagous to issuing a state +``local`` states are evaluated locally; this is analogous to issuing a state run using a ``salt-call --local`` command. These commands will be issued on the local machine running the ``spm`` command, whether that machine is a master or a minion. diff --git a/pkg/suse/salt.changes b/pkg/suse/salt.changes index 7e6ac7ee29..292a55b9d8 100644 --- a/pkg/suse/salt.changes +++ b/pkg/suse/salt.changes @@ -1042,7 +1042,7 @@ Thu Sep 19 17:18:06 UTC 2013 - aboe76@gmail.com * salt-ssh requires sshpass * salt-syndic requires salt-master Minor features: - - 0.17.0 release wil be last release for 0.XX.X numbering system + - 0.17.0 release will be last release for 0.XX.X numbering system Next release will be .. ------------------------------------------------------------------- diff --git a/salt/beacons/diskusage.py b/salt/beacons/diskusage.py index 635f13954b..2b1aa0eacb 100644 --- a/salt/beacons/diskusage.py +++ b/salt/beacons/diskusage.py @@ -79,7 +79,7 @@ def beacon(config): The second one will match disks from A:\ to Z:\ on a Windows system Note that if a regular expression are evaluated after static mount points, - which means that if a regular expression matches an other defined mount point, + which means that if a regular expression matches another defined mount point, it will override the previously defined threshold. ''' diff --git a/salt/beacons/load.py b/salt/beacons/load.py index a3cba14fcb..64a3f43436 100644 --- a/salt/beacons/load.py +++ b/salt/beacons/load.py @@ -113,7 +113,7 @@ def beacon(config): for k in ['1m', '5m', '15m']: LAST_STATUS[k] = avg_dict[k] if not config['emitatstartup']: - log.debug('Dont emit because emitatstartup is False') + log.debug("Don't emit because emitatstartup is False") return ret send_beacon = False diff --git a/salt/cache/redis_cache.py b/salt/cache/redis_cache.py index 35bce55198..6d006234d4 100644 --- a/salt/cache/redis_cache.py +++ b/salt/cache/redis_cache.py @@ -333,7 +333,7 @@ def flush(bank, key=None): An improvement for this would be loading a custom Lua script in the Redis instance of the user (using the ``register_script`` feature) and call it whenever we flush. This script would only need to build this sub-tree causing problems. It can be added later and the behaviour - should not change as the user needs to explicitely allow Salt inject scripts in their Redis instance. + should not change as the user needs to explicitly allow Salt inject scripts in their Redis instance. ''' redis_server = _get_redis_server() redis_pipe = redis_server.pipeline() diff --git a/salt/client/ssh/ssh_py_shim.py b/salt/client/ssh/ssh_py_shim.py index 4208ac7890..d3d3c8ea3f 100644 --- a/salt/client/ssh/ssh_py_shim.py +++ b/salt/client/ssh/ssh_py_shim.py @@ -101,7 +101,7 @@ def is_windows(): def need_deployment(): """ Salt thin needs to be deployed - prep the target directory and emit the - delimeter and exit code that signals a required deployment. + delimiter and exit code that signals a required deployment. """ if os.path.exists(OPTIONS.saltdir): shutil.rmtree(OPTIONS.saltdir) diff --git a/salt/engines/hipchat.py b/salt/engines/hipchat.py index 66cc6d8bdc..705a5b693e 100644 --- a/salt/engines/hipchat.py +++ b/salt/engines/hipchat.py @@ -235,13 +235,13 @@ def start(token, - ``html``: send the output as HTML - ``code``: send the output as code - This can be overriden when executing a command, using the ``--out-type`` argument. + This can be overridden when executing a command, using the ``--out-type`` argument. .. versionadded:: 2017.7.0 outputter: ``nested`` The format to display the data, using the outputters available on the CLI. - This argument can also be overriden when executing a command, using the ``--out`` option. + This argument can also be overridden when executing a command, using the ``--out`` option. .. versionadded:: 2017.7.0 diff --git a/salt/fileserver/__init__.py b/salt/fileserver/__init__.py index 1496e2898e..cbdf99d056 100644 --- a/salt/fileserver/__init__.py +++ b/salt/fileserver/__init__.py @@ -536,7 +536,7 @@ class Fileserver(object): if '../' in path: return fnd if salt.utils.url.is_escaped(path): - # don't attempt to find URL query arguements in the path + # don't attempt to find URL query arguments in the path path = salt.utils.url.unescape(path) else: if '?' in path: diff --git a/salt/grains/napalm.py b/salt/grains/napalm.py index fcfbdcfe9f..f15c970d4a 100644 --- a/salt/grains/napalm.py +++ b/salt/grains/napalm.py @@ -326,7 +326,7 @@ def host(proxy=None): .. note:: - The diference betwen ``host`` and ``hostname`` is that + The diference between ``host`` and ``hostname`` is that ``host`` provides the physical location - either domain name or IP address, while ``hostname`` provides the hostname as configured on the device. They are not necessarily the same. diff --git a/salt/modules/bcache.py b/salt/modules/bcache.py index 8e87256dbd..9e39827d15 100644 --- a/salt/modules/bcache.py +++ b/salt/modules/bcache.py @@ -927,7 +927,7 @@ def _wipe(dev): def _wait(lfunc, log_lvl=None, log_msg=None, tries=10): ''' Wait for lfunc to be True - :return: True if lfunc succeeded within tries, False if it didnt + :return: True if lfunc succeeded within tries, False if it didn't ''' i = 0 while i < tries: diff --git a/salt/modules/boto_vpc.py b/salt/modules/boto_vpc.py index 51473dfe97..612afd7537 100644 --- a/salt/modules/boto_vpc.py +++ b/salt/modules/boto_vpc.py @@ -2634,7 +2634,7 @@ def _maybe_set_tags(tags, obj): def _maybe_set_dns(conn, vpcid, dns_support, dns_hostnames): if dns_support: conn.modify_vpc_attribute(vpc_id=vpcid, enable_dns_support=dns_support) - log.debug('DNS spport was set to: {0} on vpc {1}'.format(dns_support, vpcid)) + log.debug('DNS support was set to: {0} on vpc {1}'.format(dns_support, vpcid)) if dns_hostnames: conn.modify_vpc_attribute(vpc_id=vpcid, enable_dns_hostnames=dns_hostnames) log.debug('DNS hostnames was set to: {0} on vpc {1}'.format(dns_hostnames, vpcid)) diff --git a/salt/modules/capirca_acl.py b/salt/modules/capirca_acl.py index f925f71df8..d780bb7425 100644 --- a/salt/modules/capirca_acl.py +++ b/salt/modules/capirca_acl.py @@ -424,7 +424,7 @@ def _merge_list_of_dict(first, second, prepend=True): if first and not second: return first # Determine overlaps - # So we dont change the position of the existing terms/filters + # So we don't change the position of the existing terms/filters overlaps = [] merged = [] appended = [] diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py index 00ec4dd55d..db188f0da1 100644 --- a/salt/modules/dockermod.py +++ b/salt/modules/dockermod.py @@ -5454,7 +5454,7 @@ def sls_build(name, base='opensuse/python', mods=None, saltenv='base', the salt environment to use dryrun: False - when set to True the container will not be commited at the end of + when set to True the container will not be committed at the end of the build. The dryrun succeed also when the state contains errors. **RETURN DATA** diff --git a/salt/modules/etcd_mod.py b/salt/modules/etcd_mod.py index 78088c306a..d6b0d66eec 100644 --- a/salt/modules/etcd_mod.py +++ b/salt/modules/etcd_mod.py @@ -215,7 +215,7 @@ def rm_(key, recurse=False, profile=None): ''' .. versionadded:: 2014.7.0 - Delete a key from etcd. Returns True if the key was deleted, False if it wasn + Delete a key from etcd. Returns True if the key was deleted, False if it was not and None if there was a failure. CLI Example: diff --git a/salt/modules/glusterfs.py b/salt/modules/glusterfs.py index fb26e1c610..72fb75d558 100644 --- a/salt/modules/glusterfs.py +++ b/salt/modules/glusterfs.py @@ -124,7 +124,7 @@ def peer_status(): The return value is a dictionary with peer UUIDs as keys and dicts of peer information as values. Hostnames are listed in one list. GlusterFS separates one of the hostnames but the only reason for this seems to be which hostname - happens to be used firts in peering. + happens to be used first in peering. CLI Example: diff --git a/salt/modules/junos.py b/salt/modules/junos.py index 5cc218674c..845e6ac970 100644 --- a/salt/modules/junos.py +++ b/salt/modules/junos.py @@ -589,7 +589,7 @@ def ping(dest_ip=None, **kwargs): def cli(command=None, format='text', **kwargs): ''' Executes the CLI commands and returns the output in specified format. \ - (default is text) The ouput can also be stored in a file. + (default is text) The output can also be stored in a file. Usage: diff --git a/salt/modules/kubernetes.py b/salt/modules/kubernetes.py index 12d965bb4d..1f02acca74 100644 --- a/salt/modules/kubernetes.py +++ b/salt/modules/kubernetes.py @@ -17,7 +17,7 @@ Module for handling kubernetes calls. kubernetes.client-key-file: '/path/to/client.key' -These settings can be also overrided by adding `api_url`, `api_user`, +These settings can be also overridden by adding `api_url`, `api_user`, `api_password`, `api_certificate_authority_file`, `api_client_certificate_file` or `api_client_key_file` parameters when calling a function: @@ -25,7 +25,7 @@ The data format for `kubernetes.*-data` values is the same as provided in `kubec It's base64 encoded certificates/keys in one line. For an item only one field should be provided. Either a `data` or a `file` entry. -In case both are provided the `file` entry is prefered. +In case both are provided the `file` entry is preferred. .. code-block:: bash salt '*' kubernetes.nodes api_url=http://k8s-api-server:port api_user=myuser api_password=pass diff --git a/salt/modules/logmod.py b/salt/modules/logmod.py index aa6fcbf165..2c8ab7b512 100644 --- a/salt/modules/logmod.py +++ b/salt/modules/logmod.py @@ -18,7 +18,7 @@ CLI Example: .. code-block:: bash - salt '*' log.error 'Please dont do that, this module is not for CLI use!' + salt '*' log.error "Please don't do that, this module is not for CLI use!" ''' from __future__ import absolute_import diff --git a/salt/modules/lxc.py b/salt/modules/lxc.py index 0e369e5cc6..4a3a5ad9dc 100644 --- a/salt/modules/lxc.py +++ b/salt/modules/lxc.py @@ -868,7 +868,7 @@ def _network_conf(conf_tuples=None, **kwargs): for row in val: ret.append(salt.utils.odict.OrderedDict([(row, val[row])])) # on old versions of lxc, still support the gateway auto mode - # if we didnt explicitly say no to + # if we didn't explicitly say no to # (lxc.network.ipv4.gateway: auto) if _LooseVersion(version()) <= '1.0.7' and \ True not in ['lxc.network.ipv4.gateway' in a for a in ret] and \ diff --git a/salt/modules/mssql.py b/salt/modules/mssql.py index 1c9d8d5929..841e85aa53 100644 --- a/salt/modules/mssql.py +++ b/salt/modules/mssql.py @@ -102,7 +102,7 @@ def version(**kwargs): def db_list(**kwargs): ''' - Return the databse list created on a MS SQL server. + Return the database list created on a MS SQL server. CLI Example: diff --git a/salt/modules/napalm_acl.py b/salt/modules/napalm_acl.py index 9b0b60a8ed..0a37dd7c50 100644 --- a/salt/modules/napalm_acl.py +++ b/salt/modules/napalm_acl.py @@ -198,7 +198,7 @@ def load_term_config(filter_name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. source_service A special service to choose from. This is a helper so the user is able to @@ -543,7 +543,7 @@ def load_filter_config(filter_name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. The output is a dictionary having the same form as :mod:`net.load_config `. @@ -743,7 +743,7 @@ def load_policy_config(filters=None, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. The output is a dictionary having the same form as :mod:`net.load_config `. diff --git a/salt/modules/napalm_network.py b/salt/modules/napalm_network.py index 411843c4cf..81af4cbf03 100644 --- a/salt/modules/napalm_network.py +++ b/salt/modules/napalm_network.py @@ -853,10 +853,10 @@ def config(source=None, **kwargs): # pylint: disable=unused-argument - running (string): Representation of the native running configuration. - candidate (string): Representation of the native candidate configuration. - If the device doesnt differentiate between running and startup + If the device doesn't differentiate between running and startup configuration this will an empty string. - startup (string): Representation of the native startup configuration. - If the device doesnt differentiate between running and startup + If the device doesn't differentiate between running and startup configuration this will an empty string. CLI Example: @@ -1366,7 +1366,7 @@ def load_template(template_name, # use the custom template path saltenv = template_path if not salt_render else 'base' elif salt_render and not saltenv: - # if saltenv not overrided and path specified as salt:// or http:// etc. + # if saltenv not overridden and path specified as salt:// or http:// etc. # will use the default environment, from the base saltenv = template_path if template_path else 'base' if not saltenv: @@ -1454,7 +1454,7 @@ def load_template(template_name, # after running the other features: # compare_config, discard / commit # which have to be over the same session - # so we'll set the CLOSE global explicitely as False + # so we'll set the CLOSE global explicitly as False napalm_device['CLOSE'] = False # pylint: disable=undefined-variable _loaded = salt.utils.napalm.call( napalm_device, # pylint: disable=undefined-variable diff --git a/salt/modules/napalm_route.py b/salt/modules/napalm_route.py index 1a81d53e9d..cffdb5bb73 100644 --- a/salt/modules/napalm_route.py +++ b/salt/modules/napalm_route.py @@ -67,7 +67,7 @@ def show(destination, protocol=None, **kwargs): # pylint: disable=unused-argume In case the destination prefix is too short, there may be too many routes matched. Therefore in cases of devices having a very high number of routes - it may be necessary to adjust the prefix lenght and request + it may be necessary to adjust the prefix length and request using a longer prefix. destination diff --git a/salt/modules/napalm_yang_mod.py b/salt/modules/napalm_yang_mod.py index f4dd9f6358..b655df47b2 100644 --- a/salt/modules/napalm_yang_mod.py +++ b/salt/modules/napalm_yang_mod.py @@ -435,7 +435,7 @@ def load_config(data, models, **kwargs): debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. replace: ``False`` Should replace the config with the new generate one? diff --git a/salt/modules/pdbedit.py b/salt/modules/pdbedit.py index d8cb4a75a6..d1be29b632 100644 --- a/salt/modules/pdbedit.py +++ b/salt/modules/pdbedit.py @@ -266,7 +266,7 @@ def modify( specify user account control properties .. note:: - Only the follwing can be set: + Only the following can be set: - N: No password required - D: Account disabled - H: Home directory required diff --git a/salt/modules/selinux.py b/salt/modules/selinux.py index ef513ecf14..e65913a9ea 100644 --- a/salt/modules/selinux.py +++ b/salt/modules/selinux.py @@ -561,7 +561,7 @@ def fcontext_apply_policy(name, recursive=False): .. versionadded:: 2017.7.0 Applies SElinux policies to filespec using `restorecon [-R] - filespec`. Returns dict with changes if succesful, the output of + filespec`. Returns dict with changes if successful, the output of the restorecon command otherwise. name diff --git a/salt/modules/sensehat.py b/salt/modules/sensehat.py index 12da3b0742..132663728b 100644 --- a/salt/modules/sensehat.py +++ b/salt/modules/sensehat.py @@ -140,7 +140,7 @@ def show_message(message, msg_type=None, message The message to display msg_type - The type of the message. Changes the appearence of the message. + The type of the message. Changes the appearance of the message. Available types are:: diff --git a/salt/modules/smartos_vmadm.py b/salt/modules/smartos_vmadm.py index 90f0d83599..1bf857912f 100644 --- a/salt/modules/smartos_vmadm.py +++ b/salt/modules/smartos_vmadm.py @@ -165,7 +165,7 @@ def _create_update_from_cfg(mode='create', uuid=None, vmcfg=None): ret['Error'] = res['stderr'] return ret else: - # cleanup json file (only when succesful to help troubleshooting) + # cleanup json file (only when successful to help troubleshooting) salt.utils.safe_rm(vmadm_json_file) # return uuid diff --git a/salt/modules/solrcloud.py b/salt/modules/solrcloud.py index ab95f02af4..5a55353aa1 100644 --- a/salt/modules/solrcloud.py +++ b/salt/modules/solrcloud.py @@ -195,7 +195,7 @@ def cluster_status(**kwargs): def alias_exists(alias_name, **kwargs): ''' - Check alias existance + Check alias existence Additional parameters (kwargs) may be passed, they will be proxied to http.query diff --git a/salt/modules/win_groupadd.py b/salt/modules/win_groupadd.py index 9c0307265b..3ee9f81056 100644 --- a/salt/modules/win_groupadd.py +++ b/salt/modules/win_groupadd.py @@ -218,7 +218,7 @@ def getent(refresh=False): refresh (bool): Refresh the info for all groups in ``__context__``. If False only - the groups in ``__context__`` wil be returned. If True the + the groups in ``__context__`` will be returned. If True the ``__context__`` will be refreshed with current data and returned. Default is False @@ -469,7 +469,7 @@ def list_groups(refresh=False): refresh (bool): Refresh the info for all groups in ``__context__``. If False only - the groups in ``__context__`` wil be returned. If True, the + the groups in ``__context__`` will be returned. If True, the ``__context__`` will be refreshed with current data and returned. Default is False diff --git a/salt/modules/win_system.py b/salt/modules/win_system.py index b8cf4ff20f..f3b6eab46f 100644 --- a/salt/modules/win_system.py +++ b/salt/modules/win_system.py @@ -1285,7 +1285,7 @@ def get_pending_servermanager(): key = r'SOFTWARE\Microsoft\ServerManager' # There are situations where it's possible to have '(value not set)' as - # the value data, and since an actual reboot wont be pending in that + # the value data, and since an actual reboot won't be pending in that # instance, just catch instances where we try unsuccessfully to cast as int. reg_ret = __salt__['reg.read_value']('HKLM', key, vname) diff --git a/salt/netapi/rest_cherrypy/app.py b/salt/netapi/rest_cherrypy/app.py index becb45b0a5..8ac79512a7 100644 --- a/salt/netapi/rest_cherrypy/app.py +++ b/salt/netapi/rest_cherrypy/app.py @@ -1993,7 +1993,7 @@ class Run(LowDataAdapter): The /run enpoint can also be used to issue commands using the salt-ssh subsystem. - When using salt-ssh, eauth credentials should not be supplied. Instad, + When using salt-ssh, eauth credentials should not be supplied. Instead, authentication should be handled by the SSH layer itself. The use of the salt-ssh client does not require a salt master to be running. Instead, only a roster file must be present in the salt configuration @@ -2178,7 +2178,7 @@ class Events(object): very busy and can quickly overwhelm the memory allocated to a browser tab. - A full, working proof-of-concept JavaScript appliction is available + A full, working proof-of-concept JavaScript application is available :blob:`adjacent to this file `. It can be viewed by pointing a browser at the ``/app`` endpoint in a running ``rest_cherrypy`` instance. diff --git a/salt/renderers/pyobjects.py b/salt/renderers/pyobjects.py index ae32a40d18..dda3d77806 100644 --- a/salt/renderers/pyobjects.py +++ b/salt/renderers/pyobjects.py @@ -263,7 +263,7 @@ different grain matches. **Ubuntu** classes, since Ubuntu has an ``os_family`` grain of **Debian** an an ``os`` grain of **Ubuntu**. As of the 2017.7.0 release, the order is dictated by the order of declaration, with classes defined later overriding - earlier ones. Addtionally, 2017.7.0 adds support for explicitly defining + earlier ones. Additionally, 2017.7.0 adds support for explicitly defining the ordering using an optional attribute called ``priority``. Given the above example, ``os_family`` matches will be processed first, diff --git a/salt/returners/elasticsearch_return.py b/salt/returners/elasticsearch_return.py index 2ddb4a26eb..2acf47e361 100644 --- a/salt/returners/elasticsearch_return.py +++ b/salt/returners/elasticsearch_return.py @@ -227,7 +227,7 @@ def returner(ret): if ret.get('return', None) is None: log.info('Won\'t push new data to Elasticsearch, job with jid={0} was ' - 'not succesful'.format(job_id)) + 'not successful'.format(job_id)) return # Build the index name diff --git a/salt/runners/bgp.py b/salt/runners/bgp.py index b460bdcef9..c8a70b2ec8 100644 --- a/salt/runners/bgp.py +++ b/salt/runners/bgp.py @@ -53,7 +53,7 @@ Configuration By default, the following extra fields are returned (displayed): - - ``connection_stats``: connection stats, as descibed below + - ``connection_stats``: connection stats, as described below - ``import_policy``: the name of the import policy - ``export_policy``: the name of the export policy diff --git a/salt/runners/manage.py b/salt/runners/manage.py index ef60bc0da9..fcb7353bbe 100644 --- a/salt/runners/manage.py +++ b/salt/runners/manage.py @@ -262,7 +262,7 @@ def list_state(subset=None, show_ipv4=False, state=None): minions = [m for m in minions if m in subset] else: # Always return 'present' for 0MQ for now - # TODO: implement other states spport for 0MQ + # TODO: implement other states support for 0MQ ckminions = salt.utils.minions.CkMinions(__opts__) minions = ckminions.connected_ids(show_ipv4=show_ipv4, subset=subset, include_localhost=True) diff --git a/salt/state.py b/salt/state.py index 6633745619..17c7fd3b54 100644 --- a/salt/state.py +++ b/salt/state.py @@ -1742,7 +1742,7 @@ class State(object): ret = {'name': cdata['args'][0], 'result': None, 'changes': {}, - 'comment': 'Started in a seperate process', + 'comment': 'Started in a separate process', 'proc': proc} return ret diff --git a/salt/states/docker_container.py b/salt/states/docker_container.py index 4e6bb62346..72a84cf9b8 100644 --- a/salt/states/docker_container.py +++ b/salt/states/docker_container.py @@ -256,7 +256,7 @@ def running(name, .. versionchanged:: 2017.7.0 This option was renamed from ``stop_timeout`` to - ``shutdown_timeout`` to acommodate the ``stop_timeout`` container + ``shutdown_timeout`` to accommodate the ``stop_timeout`` container configuration setting. client_timeout : 60 diff --git a/salt/states/elasticsearch.py b/salt/states/elasticsearch.py index 2c37a304ce..c1913eb5e5 100644 --- a/salt/states/elasticsearch.py +++ b/salt/states/elasticsearch.py @@ -175,7 +175,7 @@ def alias_present(name, index, definition=None): if not old: ret['comment'] = 'Alias {0} for index {1} does not exist and will be created'.format(name, index) else: - ret['comment'] = 'Alias {0} for index {1} exists with wrong configuration and will be overriden'.format(name, index) + ret['comment'] = 'Alias {0} for index {1} exists with wrong configuration and will be overridden'.format(name, index) ret['result'] = None else: @@ -348,7 +348,7 @@ def pipeline_present(name, definition): if not pipeline: ret['comment'] = 'Pipeline {0} does not exist and will be created'.format(name) else: - ret['comment'] = 'Pipeline {0} exists with wrong configuration and will be overriden'.format(name) + ret['comment'] = 'Pipeline {0} exists with wrong configuration and will be overridden'.format(name) ret['result'] = None else: @@ -439,7 +439,7 @@ def search_template_present(name, definition): if not template: ret['comment'] = 'Search template {0} does not exist and will be created'.format(name) else: - ret['comment'] = 'Search template {0} exists with wrong configuration and will be overriden'.format(name) + ret['comment'] = 'Search template {0} exists with wrong configuration and will be overridden'.format(name) ret['result'] = None else: diff --git a/salt/states/grafana4_datasource.py b/salt/states/grafana4_datasource.py index 11e2d4d3e3..29728815d7 100644 --- a/salt/states/grafana4_datasource.py +++ b/salt/states/grafana4_datasource.py @@ -27,7 +27,7 @@ Manage Grafana v4.0 data sources grafana_token: token grafana_timeout: 3 -The bahavior of this module is to create data sources if the do not exists, and +The behavior of this module is to create data sources if the do not exists, and to update data sources if the already exists. .. code-block:: yaml diff --git a/salt/states/heat.py b/salt/states/heat.py index a042751225..6abd022a12 100644 --- a/salt/states/heat.py +++ b/salt/states/heat.py @@ -141,7 +141,7 @@ def deployed(name, template=None, enviroment=None, params=None, poll=5, Parameter dict used to create the stack poll - Poll(in sec.) and report events until stack complete + Poll (in sec.) and report events until stack complete rollback Enable rollback on create failure diff --git a/salt/states/http.py b/salt/states/http.py index 58399293e9..55505221c6 100644 --- a/salt/states/http.py +++ b/salt/states/http.py @@ -143,7 +143,7 @@ def wait_for_successful_query(name, wait_for=300, **kwargs): .. note:: - All other arguements are passed to the http.query state. + All other arguments are passed to the http.query state. ''' starttime = time.time() diff --git a/salt/states/junos.py b/salt/states/junos.py index e132c15605..d7236e0ce6 100644 --- a/salt/states/junos.py +++ b/salt/states/junos.py @@ -40,7 +40,7 @@ def rpc(name, dest=None, format='xml', args=None, **kwargs): The rpc to be executed. (default = None) Optional * dest: - Destination file where the rpc ouput is stored. (default = None) + Destination file where the rpc output is stored. (default = None) Note that the file will be stored on the proxy minion. To push the files to the master use the salt's following execution module: \ :py:func:`cp.push ` @@ -319,7 +319,7 @@ def install_config(name, **kwargs): the given time unless the commit is confirmed. * diffs_file: Path to the file where the diff (difference in old configuration - and the commited configuration) will be stored.(default = None) + and the committed configuration) will be stored.(default = None) Note that the file will be stored on the proxy minion. To push the files to the master use the salt's following execution module: \ :py:func:`cp.push ` diff --git a/salt/states/netacl.py b/salt/states/netacl.py index df97c76b6e..bb441443f8 100644 --- a/salt/states/netacl.py +++ b/salt/states/netacl.py @@ -155,7 +155,7 @@ def term(name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. source_service A special service to choose from. This is a helper so the user is able to @@ -406,7 +406,7 @@ def term(name, .. note:: The first method allows the user to eventually apply complex manipulation and / or retrieve the data from external services before passing the - data to the state. The second one is more straighforward, for less + data to the state. The second one is more straightforward, for less complex cases when loading the data directly from the pillar is sufficient. .. note:: @@ -526,7 +526,7 @@ def filter(name, # pylint: disable=redefined-builtin debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. CLI Example: @@ -636,7 +636,7 @@ def filter(name, # pylint: disable=redefined-builtin .. note:: The first method allows the user to eventually apply complex manipulation and / or retrieve the data from external services before passing the - data to the state. The second one is more straighforward, for less + data to the state. The second one is more straightforward, for less complex cases when loading the data directly from the pillar is sufficient. .. note:: @@ -710,7 +710,7 @@ def managed(name, :conf_minion:`pillarenv_from_saltenv`, and is otherwise ignored. merge_pillar: ``False`` - Merge the ``filters`` wil the corresponding values from the pillar. Default: ``False``. + Merge the ``filters`` will the corresponding values from the pillar. Default: ``False``. .. note:: By default this state does not merge, to avoid any unexpected behaviours. @@ -746,7 +746,7 @@ def managed(name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. CLI Example: @@ -933,7 +933,7 @@ def managed(name, .. note:: The first method allows the user to eventually apply complex manipulation and / or retrieve the data from external services before passing the - data to the state. The second one is more straighforward, for less + data to the state. The second one is more straightforward, for less complex cases when loading the data directly from the pillar is sufficient. .. note:: diff --git a/salt/states/netconfig.py b/salt/states/netconfig.py index fa9ea90937..8ba7192f4a 100644 --- a/salt/states/netconfig.py +++ b/salt/states/netconfig.py @@ -135,8 +135,8 @@ def managed(name, To replace the config, set ``replace`` to ``True``. This option is recommended to be used with caution! .. warning:: - The spport for NAPALM native templates will be dropped beginning with Salt Fluorine. - Implicitly, the ``template_path`` argument will be depreacted and removed. + The support for NAPALM native templates will be dropped beginning with Salt Fluorine. + Implicitly, the ``template_path`` argument will be deprecated and removed. template_name Identifies path to the template source. The template can be either stored on the local machine, @@ -152,7 +152,7 @@ def managed(name, Placing the template under ``/etc/salt/states/templates/example.jinja``, it can be used as ``salt://templates/example.jinja``. - Alternatively, for local files, the user can specify the abolute path. + Alternatively, for local files, the user can specify the absolute path. If remotely, the source can be retrieved via ``http``, ``https`` or ``ftp``. Examples: @@ -213,7 +213,7 @@ def managed(name, Commit? Default: ``True``. debug: False - Debug mode. Will insert a new key under the output dictionary, as ``loaded_config`` contaning the raw + Debug mode. Will insert a new key under the output dictionary, as ``loaded_config`` containing the raw result after the template was rendered. replace: False @@ -223,7 +223,7 @@ def managed(name, Default variables/context passed to the template. **template_vars - Dictionary with the arguments/context to be used when the template is rendered. Do not explicitely specify this + Dictionary with the arguments/context to be used when the template is rendered. Do not explicitly specify this argument. This represents any other variable that will be sent to the template rendering system. Please see an example below! In both ``ntp_peers_example_using_pillar`` and ``ntp_peers_example``, ``peers`` is sent as template variable. diff --git a/salt/states/netyang.py b/salt/states/netyang.py index 2db6208f57..e500e5e449 100644 --- a/salt/states/netyang.py +++ b/salt/states/netyang.py @@ -109,7 +109,7 @@ def managed(name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. replace: ``False`` Should replace the config with the new generate one? @@ -212,7 +212,7 @@ def configured(name, configuration on the device and the expected configuration. Depending on the platform and hardware capabilities, one could be more optimal than the other. - Additionally, the output of the ``managed`` is diferent, + Additionally, the output of the ``managed`` is different, in such a way that the ``pchange`` field in the output contains structured data, rather than text. @@ -236,7 +236,7 @@ def configured(name, debug: ``False`` Debug mode. Will insert a new key under the output dictionary, - as ``loaded_config`` contaning the raw configuration loaded on the device. + as ``loaded_config`` containing the raw configuration loaded on the device. replace: ``False`` Should replace the config with the new generate one? diff --git a/salt/states/pdbedit.py b/salt/states/pdbedit.py index 9b4c7b0b98..f420b7adf8 100644 --- a/salt/states/pdbedit.py +++ b/salt/states/pdbedit.py @@ -107,7 +107,7 @@ def managed(name, **kwargs): specify user account control properties .. note:: - Only the follwing can be set: + Only the following can be set: - N: No password required - D: Account disabled - H: Home directory required diff --git a/salt/states/pip_state.py b/salt/states/pip_state.py index a6eb84b282..3238078014 100644 --- a/salt/states/pip_state.py +++ b/salt/states/pip_state.py @@ -827,7 +827,7 @@ def installed(name, user=user, cwd=cwd, env_vars=env_vars) - # If we didnt find the package in the system after + # If we didn't find the package in the system after # installing it report it if not pipsearch: pkg_404_comms.append( diff --git a/salt/states/pkg.py b/salt/states/pkg.py index 60eec6b55f..0ab5a5f9d1 100644 --- a/salt/states/pkg.py +++ b/salt/states/pkg.py @@ -1953,7 +1953,7 @@ def downloaded(name, return ret # It doesn't make sense here to received 'downloadonly' as kwargs - # as we're explicitely passing 'downloadonly=True' to execution module. + # as we're explicitly passing 'downloadonly=True' to execution module. if 'downloadonly' in kwargs: del kwargs['downloadonly'] @@ -2126,7 +2126,7 @@ def patch_downloaded(name, advisory_ids=None, **kwargs): 'this platform'} # It doesn't make sense here to received 'downloadonly' as kwargs - # as we're explicitely passing 'downloadonly=True' to execution module. + # as we're explicitly passing 'downloadonly=True' to execution module. if 'downloadonly' in kwargs: del kwargs['downloadonly'] return patch_installed(name=name, advisory_ids=advisory_ids, downloadonly=True, **kwargs) diff --git a/salt/states/rabbitmq_user.py b/salt/states/rabbitmq_user.py index a990b5a2d2..d6f1cf664b 100644 --- a/salt/states/rabbitmq_user.py +++ b/salt/states/rabbitmq_user.py @@ -126,7 +126,7 @@ def present(name, return ret if user and not any((force, perms, tags, passwd_reqs_update)): - log.debug(('RabbitMQ user \'%s\' exists, password is upto' + log.debug(('RabbitMQ user \'%s\' exists, password is up to' ' date and force is not set.'), name) ret['comment'] = 'User \'{0}\' is already present.'.format(name) ret['result'] = True diff --git a/salt/states/smartos.py b/salt/states/smartos.py index fa01d613f9..42038b0a39 100644 --- a/salt/states/smartos.py +++ b/salt/states/smartos.py @@ -368,7 +368,7 @@ def image_vacuum(name): # list of images to keep images = [] - # retreive image_present state data for host + # retrieve image_present state data for host for state in __salt__['state.show_lowstate'](): # don't throw exceptions when not highstate run if 'state' not in state: diff --git a/salt/states/win_lgpo.py b/salt/states/win_lgpo.py index 96e6ff729d..922161abf2 100644 --- a/salt/states/win_lgpo.py +++ b/salt/states/win_lgpo.py @@ -23,7 +23,7 @@ Example single policy configuration .. code-block:: yaml - Acount lockout duration: + Account lockout duration: gpo.set: - setting: 120 - policy_class: Machine diff --git a/salt/states/zfs.py b/salt/states/zfs.py index c06d972102..969ad6c020 100644 --- a/salt/states/zfs.py +++ b/salt/states/zfs.py @@ -764,7 +764,7 @@ def scheduled_snapshot(name, prefix, recursive=True, schedule=None): ## manage snapshots if ret['result']: - # retreive snapshots + # retrieve snapshots prunable = [] snapshots = {} for key in schedule: diff --git a/salt/states/zone.py b/salt/states/zone.py index 22900f89c5..b3357b3d82 100644 --- a/salt/states/zone.py +++ b/salt/states/zone.py @@ -9,7 +9,7 @@ Management of Solaris Zones .. versionadded:: 2017.7.0 -Bellow are some examples of how to use this state. +Below are some examples of how to use this state. Lets start with creating a zone and installing it. .. code-block:: yaml @@ -47,7 +47,7 @@ Lets start with creating a zone and installing it. A zone without network access is not very useful. We could update the zone.present state in the example above to add a network interface -or we could use a seperate state for this. +or we could use a separate state for this. .. code-block:: yaml @@ -836,7 +836,7 @@ def import_(name, path, mode='import', nodataset=False, brand_opts=None): def present(name, brand, zonepath, properties=None, resources=None): ''' - Ensure a zone with certain properties and resouces + Ensure a zone with certain properties and resources name : string name of the zone diff --git a/salt/utils/docker/__init__.py b/salt/utils/docker/__init__.py index 29a0b621cb..c9ad6031c7 100644 --- a/salt/utils/docker/__init__.py +++ b/salt/utils/docker/__init__.py @@ -2,7 +2,7 @@ ''' Common logic used by the docker state and execution module -This module contains logic to accomodate docker/salt CLI usage, as well as +This module contains logic to accommodate docker/salt CLI usage, as well as input as formatted by states. ''' diff --git a/salt/utils/napalm.py b/salt/utils/napalm.py index 1f560fd280..e81b97152e 100644 --- a/salt/utils/napalm.py +++ b/salt/utils/napalm.py @@ -238,7 +238,7 @@ def call(napalm_device, method, *args, **kwargs): # either running in a not-always-alive proxy # either running in a regular minion # close the connection when the call is over - # unless the CLOSE is explicitely set as False + # unless the CLOSE is explicitly set as False napalm_device['DRIVER'].close() return { 'out': out, @@ -392,7 +392,7 @@ def proxy_napalm_wrap(func): else: # in case the `inherit_napalm_device` is set # and it also has a non-empty value, - # the global var `napalm_device` will be overriden. + # the global var `napalm_device` will be overridden. # this is extremely important for configuration-related features # as all actions must be issued within the same configuration session # otherwise we risk to open multiple sessions @@ -418,7 +418,7 @@ def proxy_napalm_wrap(func): else: # in case the `inherit_napalm_device` is set # and it also has a non-empty value, - # the global var `napalm_device` will be overriden. + # the global var `napalm_device` will be overridden. # this is extremely important for configuration-related features # as all actions must be issued within the same configuration session # otherwise we risk to open multiple sessions diff --git a/salt/utils/schema.py b/salt/utils/schema.py index 6f1d824b3a..6469524606 100644 --- a/salt/utils/schema.py +++ b/salt/utils/schema.py @@ -731,7 +731,7 @@ class SchemaItem(six.with_metaclass(BaseSchemaItemMeta, object)): ''' Return the argname value looking up on all possible attributes ''' - # Let's see if there's a private fuction to get the value + # Let's see if there's a private function to get the value argvalue = getattr(self, '__get_{0}__'.format(argname), None) if argvalue is not None and callable(argvalue): argvalue = argvalue() diff --git a/salt/version.py b/salt/version.py index 741d59ce25..f049d84486 100644 --- a/salt/version.py +++ b/salt/version.py @@ -10,7 +10,7 @@ import sys import locale import platform -# linux_distribution depreacted in py3.7 +# linux_distribution deprecated in py3.7 try: from platform import linux_distribution except ImportError: diff --git a/tests/integration/runners/test_state.py b/tests/integration/runners/test_state.py index ad03fed3b0..d99b77e278 100644 --- a/tests/integration/runners/test_state.py +++ b/tests/integration/runners/test_state.py @@ -107,7 +107,7 @@ class StateRunnerTest(ShellCase): def test_orchestrate_target_doesnt_exists(self): ''' - test orchestration when target doesnt exist + test orchestration when target doesn't exist while using multiple states ''' ret = self.run_run('state.orchestrate orch.target-doesnt-exists') diff --git a/tests/unit/cloud/clouds/__init__.py b/tests/unit/cloud/clouds/__init__.py index 15d1e2c5c6..2efb80de66 100644 --- a/tests/unit/cloud/clouds/__init__.py +++ b/tests/unit/cloud/clouds/__init__.py @@ -3,7 +3,7 @@ def _preferred_ip(ip_set, preferred=None): ''' - Returns a function that reacts which ip is prefered + Returns a function that reacts which ip is preferred :param ip_set: :param private: :return: diff --git a/tests/unit/states/test_boto_apigateway.py b/tests/unit/states/test_boto_apigateway.py index b1ff876d30..80c5c2e2bb 100644 --- a/tests/unit/states/test_boto_apigateway.py +++ b/tests/unit/states/test_boto_apigateway.py @@ -383,7 +383,7 @@ class TempSwaggerFile(object): self.swaggerdict['invalid_key'] = 'invalid' # remove one of the required keys 'schemes' self.swaggerdict.pop('schemes', None) - # set swagger version to an unsupported verison 3.0 + # set swagger version to an unsupported version 3.0 self.swaggerdict['swagger'] = '3.0' # missing info object self.swaggerdict.pop('info', None) diff --git a/tests/unit/states/test_elasticsearch.py b/tests/unit/states/test_elasticsearch.py index 112ee869bb..7e7859740d 100644 --- a/tests/unit/states/test_elasticsearch.py +++ b/tests/unit/states/test_elasticsearch.py @@ -187,7 +187,7 @@ class ElasticsearchTestCase(TestCase, LoaderModuleMockMixin): ret.update({'comment': "Alias foo for index bar does not exist and will be created", 'result': None, 'changes': {'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.alias_present(name, index, {"test2": "key"}), ret) - ret.update({'comment': "Alias foo for index bar exists with wrong configuration and will be overriden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) + ret.update({'comment': "Alias foo for index bar exists with wrong configuration and will be overridden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.alias_present(name, index, {"test2": "key"}), ret) ret.update({'comment': '', 'result': False, 'changes': {}}) @@ -352,7 +352,7 @@ class ElasticsearchTestCase(TestCase, LoaderModuleMockMixin): ret.update({'comment': "Pipeline foo does not exist and will be created", 'result': None, 'changes': {'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.pipeline_present(name, {"test2": "key"}), ret) - ret.update({'comment': "Pipeline foo exists with wrong configuration and will be overriden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) + ret.update({'comment': "Pipeline foo exists with wrong configuration and will be overridden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.pipeline_present(name, {"test2": "key"}), ret) ret.update({'comment': '', 'result': False, 'changes': {}}) @@ -434,7 +434,7 @@ class ElasticsearchTestCase(TestCase, LoaderModuleMockMixin): ret.update({'comment': "Search template foo does not exist and will be created", 'result': None, 'changes': {'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.search_template_present(name, {"test2": "key"}), ret) - ret.update({'comment': "Search template foo exists with wrong configuration and will be overriden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) + ret.update({'comment': "Search template foo exists with wrong configuration and will be overridden", 'result': None, 'changes': {'old': {"test": "key"}, 'new': {"test2": "key"}}}) self.assertDictEqual(elasticsearch.search_template_present(name, {"test2": "key"}), ret) ret.update({'comment': '', 'result': False, 'changes': {}}) diff --git a/tests/unit/utils/test_cache.py b/tests/unit/utils/test_cache.py index 88b9d07ac9..86e40468ea 100644 --- a/tests/unit/utils/test_cache.py +++ b/tests/unit/utils/test_cache.py @@ -77,7 +77,7 @@ class CacheContextTestCase(TestCase): def test_context_wrapper(self): ''' Test to ensure that a module which decorates itself - with a context cache can store and retreive its contextual + with a context cache can store and retrieve its contextual data ''' opts = salt.config.DEFAULT_MINION_OPTS From 7e6e80be8730c90b0a6aa4f576f9eb2cc8284a30 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Thu, 8 Mar 2018 14:03:46 +0100 Subject: [PATCH 035/117] heat: Fix spelling mistake of environment The parameter `environment` is misspelled as `enviroment` in the heat module/state. Fix the typo, document the rename, and add support for the misspelled version for backward compatibility. Signed-off-by: Benjamin Drung --- salt/modules/heat.py | 94 +++++++++++++++++++++++++++----------------- salt/states/heat.py | 32 +++++++++++---- 2 files changed, 84 insertions(+), 42 deletions(-) diff --git a/salt/modules/heat.py b/salt/modules/heat.py index e2b3f97ded..92f2e6a251 100644 --- a/salt/modules/heat.py +++ b/salt/modules/heat.py @@ -223,7 +223,7 @@ def _parse_template(tmpl_str): return tpl -def _parse_enviroment(env_str): +def _parse_environment(env_str): ''' Parsing template ''' @@ -460,9 +460,9 @@ def delete_stack(name=None, poll=0, timeout=60, profile=None): return ret -def create_stack(name=None, template_file=None, enviroment=None, +def create_stack(name=None, template_file=None, environment=None, parameters=None, poll=0, rollback=False, timeout=60, - profile=None): + profile=None, enviroment=None): ''' Create a stack (heat stack-create) @@ -472,8 +472,8 @@ def create_stack(name=None, template_file=None, enviroment=None, template_file File of template - enviroment - File of enviroment + environment + File of environment parameters Parameter dict used to create the stack @@ -496,11 +496,23 @@ def create_stack(name=None, template_file=None, enviroment=None, salt '*' heat.create_stack name=mystack \\ template_file=salt://template.yaml \\ - enviroment=salt://enviroment.yaml \\ + environment=salt://environment.yaml \\ parameters="{"image": "Debian 8", "flavor": "m1.small"}" \\ poll=5 rollback=False timeout=60 profile=openstack1 + .. versionadded:: 2017.7.5,2018.3.1 + + The spelling mistake in parameter `enviroment` was corrected to `environment`. + The misspelled version is still supported for backward compatibility, but will + be removed in Salt Neon. + ''' + if environment is None and enviroment is not None: + salt.utils.warn_until('Neon', ( + "Please use the 'environment' parameter instead of the misspelled 'enviroment' " + "parameter which will be removed in Salt Neon." + )) + environment = enviroment h_client = _auth(profile) ret = { 'result': True, @@ -570,12 +582,12 @@ def create_stack(name=None, template_file=None, enviroment=None, ret['comment'] = 'Template not valid {0}'.format(ex) return ret env = {} - if enviroment: - enviroment_tmp_file = salt.utils.files.mkstemp() + if environment: + environment_tmp_file = salt.utils.files.mkstemp() esfn, source_sum, comment_ = __salt__['file.get_managed']( - name=enviroment_tmp_file, + name=environment_tmp_file, template=None, - source=enviroment, + source=environment, source_hash=None, user=None, group=None, @@ -586,11 +598,11 @@ def create_stack(name=None, template_file=None, enviroment=None, skip_verify=False, kwargs=None) - enviroment_manage_result = __salt__['file.manage_file']( - name=enviroment_tmp_file, + environment_manage_result = __salt__['file.manage_file']( + name=environment_tmp_file, sfn=esfn, ret=None, - source=enviroment, + source=environment, source_sum=source_sum, user=None, group=None, @@ -602,18 +614,18 @@ def create_stack(name=None, template_file=None, enviroment=None, show_changes=False, contents=None, dir_mode=None) - if enviroment_manage_result['result']: - with salt.utils.fopen(enviroment_tmp_file, 'r') as efp_: + if environment_manage_result['result']: + with salt.utils.fopen(environment_tmp_file, 'r') as efp_: env_str = efp_.read() - salt.utils.safe_rm(enviroment_tmp_file) + salt.utils.safe_rm(environment_tmp_file) try: - env = _parse_enviroment(env_str) + env = _parse_environment(env_str) except ValueError as ex: ret['result'] = False ret['comment'] = 'Error parsing template {0}'.format(ex) else: ret['result'] = False - ret['comment'] = 'Can not open enviroment: {0}, {1}'.format(enviroment, comment_) + ret['comment'] = 'Can not open environment: {0}, {1}'.format(environment, comment_) if ret['result'] is False: return ret @@ -645,9 +657,9 @@ def create_stack(name=None, template_file=None, enviroment=None, return ret -def update_stack(name=None, template_file=None, enviroment=None, +def update_stack(name=None, template_file=None, environment=None, parameters=None, poll=0, rollback=False, timeout=60, - profile=None): + profile=None, enviroment=None): ''' Update a stack (heat stack-template) @@ -657,8 +669,8 @@ def update_stack(name=None, template_file=None, enviroment=None, template_file File of template - enviroment - File of enviroment + environment + File of environment parameters Parameter dict used to update the stack @@ -681,11 +693,23 @@ def update_stack(name=None, template_file=None, enviroment=None, salt '*' heat.update_stack name=mystack \\ template_file=salt://template.yaml \\ - enviroment=salt://enviroment.yaml \\ + environment=salt://environment.yaml \\ parameters="{"image": "Debian 8", "flavor": "m1.small"}" \\ poll=5 rollback=False timeout=60 profile=openstack1 + .. versionadded:: 2017.7.5,2018.3.1 + + The spelling mistake in parameter `enviroment` was corrected to `environment`. + The misspelled version is still supported for backward compatibility, but will + be removed in Salt Neon. + ''' + if environment is None and enviroment is not None: + salt.utils.warn_until('Neon', ( + "Please use the 'environment' parameter instead of the misspelled 'enviroment' " + "parameter which will be removed in Salt Neon." + )) + environment = enviroment h_client = _auth(profile) ret = { 'result': True, @@ -759,12 +783,12 @@ def update_stack(name=None, template_file=None, enviroment=None, ret['comment'] = 'Template not valid {0}'.format(ex) return ret env = {} - if enviroment: - enviroment_tmp_file = salt.utils.files.mkstemp() + if environment: + environment_tmp_file = salt.utils.files.mkstemp() esfn, source_sum, comment_ = __salt__['file.get_managed']( - name=enviroment_tmp_file, + name=environment_tmp_file, template=None, - source=enviroment, + source=environment, source_hash=None, user=None, group=None, @@ -775,11 +799,11 @@ def update_stack(name=None, template_file=None, enviroment=None, skip_verify=False, kwargs=None) - enviroment_manage_result = __salt__['file.manage_file']( - name=enviroment_tmp_file, + environment_manage_result = __salt__['file.manage_file']( + name=environment_tmp_file, sfn=esfn, ret=None, - source=enviroment, + source=environment, source_sum=source_sum, user=None, group=None, @@ -791,18 +815,18 @@ def update_stack(name=None, template_file=None, enviroment=None, show_changes=False, contents=None, dir_mode=None) - if enviroment_manage_result['result']: - with salt.utils.fopen(enviroment_tmp_file, 'r') as efp_: + if environment_manage_result['result']: + with salt.utils.fopen(environment_tmp_file, 'r') as efp_: env_str = efp_.read() - salt.utils.safe_rm(enviroment_tmp_file) + salt.utils.safe_rm(environment_tmp_file) try: - env = _parse_enviroment(env_str) + env = _parse_environment(env_str) except ValueError as ex: ret['result'] = False ret['comment'] = 'Error parsing template {0}'.format(ex) else: ret['result'] = False - ret['comment'] = 'Can not open enviroment: {0}, {1}'.format(enviroment, comment_) + ret['comment'] = 'Can not open environment: {0}, {1}'.format(environment, comment_) if ret['result'] is False: return ret diff --git a/salt/states/heat.py b/salt/states/heat.py index 6abd022a12..26dd0f37d8 100644 --- a/salt/states/heat.py +++ b/salt/states/heat.py @@ -16,7 +16,7 @@ Stack can be set as either absent or deploy. heat.deployed: - name: - template: #Required - - enviroment: + - environment: - params: {} - poll: 5 - rollback: False @@ -33,6 +33,12 @@ mysql: image: Debian 7 - rollback: True +.. versionadded:: 2017.7.5,2018.3.1 + + The spelling mistake in parameter `enviroment` was corrected to `environment`. + The misspelled version is still supported for backward compatibility, but will + be removed in Salt Neon. + ''' from __future__ import absolute_import import json @@ -122,7 +128,7 @@ def _parse_template(tmpl_str): return tpl -def deployed(name, template=None, enviroment=None, params=None, poll=5, +def deployed(name, template=None, environment=None, params=None, poll=5, rollback=False, timeout=60, update=False, profile=None, **connection_args): ''' @@ -134,8 +140,8 @@ def deployed(name, template=None, enviroment=None, params=None, poll=5, template File of template - enviroment - File of enviroment + environment + File of environment params Parameter dict used to create the stack @@ -152,10 +158,22 @@ def deployed(name, template=None, enviroment=None, params=None, poll=5, profile Profile to use + .. versionadded:: 2017.7.5,2018.3.1 + + The spelling mistake in parameter `enviroment` was corrected to `environment`. + The misspelled version is still supported for backward compatibility, but will + be removed in Salt Neon. + ''' + if environment is None and 'enviroment' in connection_args: + salt.utils.warn_until('Neon', ( + "Please use the 'environment' parameter instead of the misspelled 'enviroment' " + "parameter which will be removed in Salt Neon." + )) + environment = connection_args.pop('enviroment') log.debug('Deployed with(' + '{0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9})' - .format(name, template, enviroment, params, poll, rollback, + .format(name, template, environment, params, poll, rollback, timeout, update, profile, connection_args)) ret = {'name': None, 'comment': '', @@ -266,7 +284,7 @@ def deployed(name, template=None, enviroment=None, params=None, poll=5, else: stack = __salt__['heat.update_stack'](name=name, template_file=template, - enviroment=enviroment, + environment=environment, parameters=params, poll=poll, rollback=rollback, timeout=timeout, @@ -282,7 +300,7 @@ def deployed(name, template=None, enviroment=None, params=None, poll=5, else: stack = __salt__['heat.create_stack'](name=name, template_file=template, - enviroment=enviroment, + environment=environment, parameters=params, poll=poll, rollback=rollback, timeout=timeout, From 4a5da2d144ea854dbcb5e237614c1bbbf876f9b5 Mon Sep 17 00:00:00 2001 From: Benjamin Drung Date: Thu, 8 Mar 2018 16:55:15 +0100 Subject: [PATCH 036/117] Make documentation theme configurable Debian uses the previous documentation theme. Make the documentation theme configurable via an environment variable HTML_THEME. Signed-off-by: Benjamin Drung --- doc/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/conf.py b/doc/conf.py index de302f673a..d895ffe99f 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -370,7 +370,7 @@ gettext_compact = False ### HTML options -html_theme = 'saltstack2' #change to 'saltstack' to use previous theme +html_theme = os.environ.get('HTML_THEME', 'saltstack2') # set 'HTML_THEME=saltstack' to use previous theme html_theme_path = ['_themes'] html_title = u'' html_short_title = 'Salt' From b8ab8434a53eaf417eda87df0324650419dd6632 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Wed, 7 Mar 2018 08:23:34 -0700 Subject: [PATCH 037/117] fix windows for kitchen --- .kitchen.yml | 81 ++++++++++++++++++++++++++++++++++++---------------- Gemfile | 6 ++-- 2 files changed, 59 insertions(+), 28 deletions(-) diff --git a/.kitchen.yml b/.kitchen.yml index 30d72c426c..28fe65c62d 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -49,6 +49,9 @@ provisioner: - .travis.yml state_top: base: + "os:Windows": + - match: grain + - prep_windows "*": - git.salt <% if File.exists?(platformsfile) %> @@ -117,39 +120,57 @@ platforms: - name: windows-2012r2 driver: box: mwrock/Windows2012R2 - communicator: winrm name: vagrant gui: true - username: administrator + transport: + name: winrm + username: Administrator password: Pass@word1 - provisioner: - init_environment: | - Clear-Host - $AddedLocation ="c:\salt" - $Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment" - $OldPath = (Get-ItemProperty -Path "$Reg" -Name PATH).Path - $NewPath= $OldPath + ’;’ + $AddedLocation - Set-ItemProperty -Path "$Reg" -Name PATH –Value $NewPath - salt_bootstrap_url: https://raw.githubusercontent.com/saltstack/salt-bootstrap/develop/bootstrap-salt.ps1 - salt_bootstrap_options: '' - - name: windows-2016 - driver: - box: mwrock/Windows2016 - communicator: winrm - name: vagrant - username: Vagrant - password: vagrant - gui: true provisioner: init_environment: | Clear-Host $AddedLocation ="c:\salt;c:\salt\bin\Scripts" $Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment" - $OldPath = (Get-ItemProperty -Path "$Reg" -Name PATH).Path - $NewPath= $OldPath + ’;’ + $AddedLocation - Set-ItemProperty -Path "$Reg" -Name PATH –Value $NewPath + $OldPath = (Get-ItemProperty -Path $Reg -Name PATH).Path + $NewPath= $OldPath + ";" + $AddedLocation + Set-ItemProperty -Path $Reg -Value $NewPath -Name PATH salt_bootstrap_url: https://raw.githubusercontent.com/saltstack/salt-bootstrap/develop/bootstrap-salt.ps1 salt_bootstrap_options: '' + verifier: + windows: true + types: + - unit + coverage_xml: false + save: + $env:TEMP/salt-runtests.log: artifacts/logs/salt-runtests.log + /salt/var/log/salt/minion: artifacts/logs/minion + - name: windows-2016 + driver: + box: mwrock/Windows2016 + name: vagrant + gui: true + transport: + name: winrm + username: Administrator + password: vagrant + provisioner: + init_environment: | + Clear-Host + $AddedLocation ="c:\salt;c:\salt\bin\Scripts" + $Reg = "Registry::HKLM\System\CurrentControlSet\Control\Session Manager\Environment" + $OldPath = (Get-ItemProperty -Path $Reg -Name PATH).Path + $NewPath= $OldPath + ";" + $AddedLocation + Set-ItemProperty -Path $Reg -Value $NewPath -Name PATH + salt_bootstrap_url: https://raw.githubusercontent.com/saltstack/salt-bootstrap/develop/bootstrap-salt.ps1 + salt_bootstrap_options: '' + verifier: + windows: true + types: + - unit + coverage_xml: false + save: + $env:TEMP/salt-runtests.log: artifacts/logs/salt-runtests.log + /salt/var/log/salt/minion: artifacts/logs/minion <% end %> <% end %> suites: @@ -162,10 +183,15 @@ suites: base: "*": - jenkins + "os:Windows": + - match: grain + - windows jenkins.sls: - testing_dir: /tmp/kitchen/testing + testing_dir: "{{salt.config.get('root_dir')|replace('\\', '\\\\')}}/testing" clone_repo: false salttesting_namespec: salttesting==2017.6.1 + windows.sls: + virtualenv_path: 'c:\Python27\Scripts\pip.exe' - name: py3 excludes: - centos-6 @@ -178,11 +204,16 @@ suites: base: "*": - jenkins + "os:Windows": + - match: grain + - windows jenkins.sls: - testing_dir: /tmp/kitchen/testing + testing_dir: "{{salt.config.get('root_dir')|replace('\\', '\\\\')}}/testing" clone_repo: false py3: true salttesting_namespec: salttesting==2017.6.1 + windows.sls: + virtualenv_path: 'c:\Python35\Scripts\pip.exe' <% if File.exists?(verifierfile) %> <%= ERB.new(File.read(verifierfile)).result %> diff --git a/Gemfile b/Gemfile index a73a91c9b2..6fb12dd509 100644 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'test-kitchen', :git => 'https://github.com/test-kitchen/test-kitchen.git' +gem 'test-kitchen', '~>1.20' gem 'kitchen-salt', :git => 'https://github.com/saltstack/kitchen-salt.git' gem 'kitchen-sync' gem 'git' @@ -12,7 +12,7 @@ group :docker do end group :opennebula do - gem 'kitchen-opennebula', :git => 'https://github.com/gtmanfred/kitchen-opennebula.git' + gem 'kitchen-opennebula', '>=0.2.3' gem 'xmlrpc' end @@ -20,7 +20,7 @@ group :windows do gem 'vagrant-wrapper' gem 'kitchen-vagrant' gem 'winrm', '~>2.0' - gem 'winrm-fs', '~>1.0' + gem 'winrm-fs', '>=1.1.1' end group :ec2 do From 169cf7a4e2264ce5b2200159a582e6e08705e4e5 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Thu, 8 Mar 2018 10:27:16 -0700 Subject: [PATCH 038/117] make spm cache_dir instead of all cachedirs This will allow spm to be run as a non root user, with a cache dir set to somewhere other than /var/cache/salt. --- salt/cli/spm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cli/spm.py b/salt/cli/spm.py index 3cecc76905..b279cecc2a 100644 --- a/salt/cli/spm.py +++ b/salt/cli/spm.py @@ -30,7 +30,7 @@ class SPM(parsers.SPMParser): self.parse_args() self.setup_logfile_logger() v_dirs = [ - self.config['cachedir'], + self.config['spm_cache_dir'], ] verify_env(v_dirs, self.config['user'], From 9e2c3f7991f1325449594fe83ea8567b08536ca5 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Wed, 7 Mar 2018 17:56:58 -0700 Subject: [PATCH 039/117] split return key value correctly --- salt/returners/highstate_return.py | 3 +- tests/unit/returners/test_highstate_return.py | 114 ++++++++++++++++++ 2 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 tests/unit/returners/test_highstate_return.py diff --git a/salt/returners/highstate_return.py b/salt/returners/highstate_return.py index 04054cfb6e..c299a55f43 100644 --- a/salt/returners/highstate_return.py +++ b/salt/returners/highstate_return.py @@ -293,8 +293,7 @@ def _generate_states_report(sorted_data): ''' states = [] for state, data in sorted_data: - module, stateid, name, function = \ - [x.rstrip('_').lstrip('-') for x in state.split('|')] + module, stateid, name, function = state.split('_|-') module_function = '.'.join((module, function)) result = data.get('result', '') single = [ diff --git a/tests/unit/returners/test_highstate_return.py b/tests/unit/returners/test_highstate_return.py new file mode 100644 index 0000000000..f61bb9731d --- /dev/null +++ b/tests/unit/returners/test_highstate_return.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +''' +tests.unit.returners.test_highstate_return +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Unit tests for the Highstate Returner Cache. +''' + +# Import Python libs +from __future__ import absolute_import +import json +import logging +import os + +# Import Salt Testing libs +from tests.support.mixins import LoaderModuleMockMixin +from tests.support.runtests import RUNTIME_VARS +from tests.support.unit import TestCase + +# Import Salt libs +import salt.utils +import salt.returners.highstate_return as highstate + +log = logging.getLogger(__name__) + + +class HighstateReturnerTestCase(TestCase, LoaderModuleMockMixin): + ''' + Tests for the highstate_return returner + ''' + output_file = os.path.join(RUNTIME_VARS.TMP, 'highstate_return') + + def tearDown(self): + os.unlink(self.output_file) + + def setup_loader_modules(self): + return {highstate: {'__opts__': { + 'highstate.report_everything': True, + 'highstate.report_format': 'json', + 'highstate.report_delivery': 'file', + 'highstate.file_output': self.output_file, + }}} + + def test_pipe_in_name(self): + ret = { + 'fun_args': ['test'], + 'jid': '20180308201402941603', + 'return': { + 'cmd_|-test_|-echo hi | grep h\n_|-run': { + 'comment': 'Command "echo hi | grep h\n" run', + 'name': 'echo hi | grep h\n', + 'start_time': '20:14:03.053612', + 'result': True, + 'duration': 75.198, + '__run_num__': 0, + '__sls__': u'test', + 'changes': { + 'pid': 1429, + 'retcode': 0, + 'stderr': '', + 'stdout': 'hi', + }, + '__id__': 'test', + } + }, + 'retcode': 0, + 'success': True, + 'fun': 'state.apply', + 'id': 'salt', + 'out': 'highstate', + } + expected = [ + { + "stats": [ + {"total": 1}, + {"failed": 0, "__style__": "failed"}, + {"unchanged": 0, "__style__": "unchanged"}, + {"changed": 1, "__style__": "changed"}, {"duration": 75.198}, + ], + }, + { + "job": [ + {"function": "state.apply"}, + {"arguments": ["test"]}, + {"jid": "20180308201402941603"}, + {"success": True}, + {"retcode": 0} + ], + }, + { + "states": [ + { + "test": [ + {"function": "cmd.run"}, + {"name": "echo hi | grep h\n"}, + {"result": True}, + {"duration": 75.198}, + {"comment": "Command \"echo hi | grep h\n\" run"}, + {"changes": [ + {"pid": 1429}, + {"retcode": 0}, + {"stderr": ""}, + {"stdout": "hi"} + ]}, + {"started": "20:14:03.053612"} + ], + "__style__": "changed" + } + ] + } + ] + highstate.returner(ret) + with open(self.output_file) as fh_: + self.assertEqual(json.load(fh_), expected) From 5fe474b1a8e23f8d1ce64c96ece0a2f29bd6579f Mon Sep 17 00:00:00 2001 From: Thomas Phipps Date: Thu, 8 Mar 2018 13:57:18 -0800 Subject: [PATCH 040/117] .format remove fix for #44452 --- salt/cloud/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/cloud/__init__.py b/salt/cloud/__init__.py index 35b01aa241..566b75a5fa 100644 --- a/salt/cloud/__init__.py +++ b/salt/cloud/__init__.py @@ -1493,8 +1493,8 @@ class Cloud(object): vm_name = vm_details['id'] else: log.debug( - 'vm:{0} in provider:{1} is not in name ' - 'list:\'{2}\''.format(vm_name, driver, names) + 'vm:%s in provider:%s is not in name ' + 'list:\'%s\'', vm_name, driver, names ) continue From d18f1a55a7f86a4e47374f152519b61dbefbc8d8 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 9 Mar 2018 08:23:17 -0700 Subject: [PATCH 041/117] fix pylint --- tests/unit/returners/test_highstate_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/returners/test_highstate_return.py b/tests/unit/returners/test_highstate_return.py index f61bb9731d..7f2ac910e7 100644 --- a/tests/unit/returners/test_highstate_return.py +++ b/tests/unit/returners/test_highstate_return.py @@ -110,5 +110,5 @@ class HighstateReturnerTestCase(TestCase, LoaderModuleMockMixin): } ] highstate.returner(ret) - with open(self.output_file) as fh_: + with salt.utils.fopen(self.output_file) as fh_: self.assertEqual(json.load(fh_), expected) From 7974ff7264f145722eaf554a10abfbca26c6778f Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Thu, 8 Mar 2018 09:19:18 -0700 Subject: [PATCH 042/117] load grains for salt.cmd runner Without this, none of the modules like pkg.install or service.restart can be used because there are no grains for the __virtual__ function to check. --- salt/runners/salt.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/salt/runners/salt.py b/salt/runners/salt.py index 1cbae0ac27..38eb7b2e2e 100644 --- a/salt/runners/salt.py +++ b/salt/runners/salt.py @@ -32,11 +32,12 @@ Execution modules are also available to salt runners: # import python libs from __future__ import absolute_import from __future__ import print_function +import copy import logging # import salt libs import salt.client -from salt.loader import minion_mods, utils +import salt.loader from salt.exceptions import SaltClientError log = logging.getLogger(__name__) # pylint: disable=invalid-name @@ -64,9 +65,11 @@ def cmd(fun, *args, **kwargs): kws = dict((k, v) for k, v in kwargs.items() if not k.startswith('__')) # pylint: disable=undefined-variable - return minion_mods( - __opts__, - utils=utils(__opts__)).get(fun)(*args, **kws) + opts = copy.deepcopy(__opts__) + opts['grains'] = salt.loader.grains(opts) + utils = salt.loader.utils(opts) + mods = salt.loader.minion_mods(opts, utils=utils) + return mods.get(fun)(*args, **kws) def execute(tgt, From b958b4699cf8ae3e7afa8774c4ba2f3fdb0d73ac Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 9 Mar 2018 11:16:13 -0600 Subject: [PATCH 043/117] Update requirements files to depend on mock>=2.0.0 This requirement was changed in salt-jenkins but never done here. --- requirements/dev_python27.txt | 2 +- requirements/dev_python34.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements/dev_python27.txt b/requirements/dev_python27.txt index c714cec795..f0ad301234 100644 --- a/requirements/dev_python27.txt +++ b/requirements/dev_python27.txt @@ -1,6 +1,6 @@ -r base.txt -mock +mock>=2.0.0 apache-libcloud>=0.14.0 boto>=2.32.1 boto3>=1.2.1 diff --git a/requirements/dev_python34.txt b/requirements/dev_python34.txt index b42bac072e..2add0a8bbc 100644 --- a/requirements/dev_python34.txt +++ b/requirements/dev_python34.txt @@ -1,6 +1,6 @@ -r base.txt -mock +mock>=2.0.0 apache-libcloud>=0.14.0 boto>=2.32.1 boto3>=1.2.1 From 70aca0da570a53551ae73e29b6713dc933b62555 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 6 Mar 2018 13:08:01 -0800 Subject: [PATCH 044/117] Recent unicode changes into Oxygen broke some aspects of the x509 module. When processing PEM text, it needs to be treated as ascii not unicode. Adding a couple tests for affected functions. --- salt/modules/x509.py | 4 +-- salt/states/x509.py | 2 +- tests/unit/modules/test_x509.py | 51 +++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/salt/modules/x509.py b/salt/modules/x509.py index 153ba0ebe6..5b7348ac3c 100644 --- a/salt/modules/x509.py +++ b/salt/modules/x509.py @@ -316,7 +316,7 @@ def _text_or_file(input_): ''' if os.path.isfile(input_): with salt.utils.files.fopen(input_) as fp_: - return salt.utils.stringutils.to_unicode(fp_.read()) + return salt.utils.stringutils.to_str(fp_.read()) else: return input_ @@ -493,7 +493,7 @@ def get_pem_entry(text, pem_type=None): ret += pem_body[i:i + 64] + '\n' ret += pem_footer + '\n' - return ret + return ret.encode('ascii') def get_pem_entries(glob_path): diff --git a/salt/states/x509.py b/salt/states/x509.py index e8ba08905b..b2de03d281 100644 --- a/salt/states/x509.py +++ b/salt/states/x509.py @@ -310,7 +310,7 @@ def private_key_managed(name, ret = __states__['file.managed'](**file_args) if ret['changes'] and new_key: - ret['changes'] = 'New private key generated' + ret['changes'] = {'new': 'New private key generated'} return ret diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index edafe4ff44..3f7612b279 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -65,3 +65,54 @@ class X509TestCase(TestCase, LoaderModuleMockMixin): assert x509.log.trace.call_args[0][0] == "Missing attribute '%s'. Error: %s" assert x509.log.trace.call_args[0][1] == list(subj.nid.keys())[0] assert isinstance(x509.log.trace.call_args[0][2], TypeError) + + def test_get_pem_entry(self): + ''' + Test private function _parse_subject(subject) it handles a missing fields + :return: + ''' + ca_key = '''-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCjdjbgL4kQ8Lu73xeRRM1q3C3K3ptfCLpyfw38LRnymxaoJ6ls +pNSx2dU1uJ89YKFlYLo1QcEk4rJ2fdIjarV0kuNCY3rC8jYUp9BpAU5Z6p9HKeT1 +2rTPH81JyjbQDR5PyfCyzYOQtpwpB4zIUUK/Go7tTm409xGKbbUFugJNgQIDAQAB +AoGAF24we34U1ZrMLifSRv5nu3OIFNZHyx2DLDpOFOGaII5edwgIXwxZeIzS5Ppr +yO568/8jcdLVDqZ4EkgCwRTgoXRq3a1GLHGFmBdDNvWjSTTMLoozuM0t2zjRmIsH +hUd7tnai9Lf1Bp5HlBEhBU2gZWk+SXqLvxXe74/+BDAj7gECQQDRw1OPsrgTvs3R +3MNwX6W8+iBYMTGjn6f/6rvEzUs/k6rwJluV7n8ISNUIAxoPy5g5vEYK6Ln/Ttc7 +u0K1KNlRAkEAx34qcxjuswavL3biNGE+8LpDJnJx1jaNWoH+ObuzYCCVMusdT2gy +kKuq9ytTDgXd2qwZpIDNmscvReFy10glMQJAXebMz3U4Bk7SIHJtYy7OKQzn0dMj +35WnRV81c2Jbnzhhu2PQeAvt/i1sgEuzLQL9QEtSJ6wLJ4mJvImV0TdaIQJAAYyk +TcKK0A8kOy0kMp3yvDHmJZ1L7wr7bBGIZPBlQ0Ddh8i1sJExm1gJ+uN2QKyg/XrK +tDFf52zWnCdVGgDwcQJALW/WcbSEK+JVV6KDJYpwCzWpKIKpBI0F6fdCr1G7Xcwj +c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== +-----END RSA PRIVATE KEY----- +''' + + ret = x509.get_pem_entry(ca_key) + self.assertEqual(ret, ca_key) + + def test_get_private_key_size(self): + ''' + Test private function _parse_subject(subject) it handles a missing fields + :return: + ''' + ca_key = ''' +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCjdjbgL4kQ8Lu73xeRRM1q3C3K3ptfCLpyfw38LRnymxaoJ6ls +pNSx2dU1uJ89YKFlYLo1QcEk4rJ2fdIjarV0kuNCY3rC8jYUp9BpAU5Z6p9HKeT1 +2rTPH81JyjbQDR5PyfCyzYOQtpwpB4zIUUK/Go7tTm409xGKbbUFugJNgQIDAQAB +AoGAF24we34U1ZrMLifSRv5nu3OIFNZHyx2DLDpOFOGaII5edwgIXwxZeIzS5Ppr +yO568/8jcdLVDqZ4EkgCwRTgoXRq3a1GLHGFmBdDNvWjSTTMLoozuM0t2zjRmIsH +hUd7tnai9Lf1Bp5HlBEhBU2gZWk+SXqLvxXe74/+BDAj7gECQQDRw1OPsrgTvs3R +3MNwX6W8+iBYMTGjn6f/6rvEzUs/k6rwJluV7n8ISNUIAxoPy5g5vEYK6Ln/Ttc7 +u0K1KNlRAkEAx34qcxjuswavL3biNGE+8LpDJnJx1jaNWoH+ObuzYCCVMusdT2gy +kKuq9ytTDgXd2qwZpIDNmscvReFy10glMQJAXebMz3U4Bk7SIHJtYy7OKQzn0dMj +35WnRV81c2Jbnzhhu2PQeAvt/i1sgEuzLQL9QEtSJ6wLJ4mJvImV0TdaIQJAAYyk +TcKK0A8kOy0kMp3yvDHmJZ1L7wr7bBGIZPBlQ0Ddh8i1sJExm1gJ+uN2QKyg/XrK +tDFf52zWnCdVGgDwcQJALW/WcbSEK+JVV6KDJYpwCzWpKIKpBI0F6fdCr1G7Xcwj +c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== +-----END RSA PRIVATE KEY----- +''' + + ret = x509.get_private_key_size(ca_key) + self.assertEqual(ret, 1024) From 1d57787a6f29c3ef0c0543ccf691eb8a6d48efb9 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Wed, 7 Mar 2018 10:28:51 -0800 Subject: [PATCH 045/117] Gating the two tests that require m2crypto to be available. --- tests/unit/modules/test_x509.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index 3f7612b279..1e30053af9 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -34,6 +34,11 @@ from tests.support.mock import ( from salt.modules import x509 +try: + import m2crypto + HAS_M2CRYPTO = True +except ImportError: + HAS_M2CRYPTO = False @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not bool(pytest), False) @@ -66,6 +71,7 @@ class X509TestCase(TestCase, LoaderModuleMockMixin): assert x509.log.trace.call_args[0][1] == list(subj.nid.keys())[0] assert isinstance(x509.log.trace.call_args[0][2], TypeError) + @skipIf(not HAS_M2CRYPTO, 'Skipping, M2Crypt is unavailble') def test_get_pem_entry(self): ''' Test private function _parse_subject(subject) it handles a missing fields @@ -91,6 +97,7 @@ c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== ret = x509.get_pem_entry(ca_key) self.assertEqual(ret, ca_key) + @skipIf(not HAS_M2CRYPTO, 'Skipping, M2Crypt is unavailble') def test_get_private_key_size(self): ''' Test private function _parse_subject(subject) it handles a missing fields From 53b462241cb0dd70ec20d03a23bd6e6c96acf155 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Wed, 7 Mar 2018 10:40:07 -0800 Subject: [PATCH 046/117] Swapping to_str for to_bytes. --- salt/modules/x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/x509.py b/salt/modules/x509.py index 5b7348ac3c..dfdcdb0e83 100644 --- a/salt/modules/x509.py +++ b/salt/modules/x509.py @@ -316,7 +316,7 @@ def _text_or_file(input_): ''' if os.path.isfile(input_): with salt.utils.files.fopen(input_) as fp_: - return salt.utils.stringutils.to_str(fp_.read()) + return salt.utils.stringutils.to_bytes(fp_.read()) else: return input_ From e813b74fbeeaf8ac867021a55a2906b1ea7cbfe2 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Wed, 7 Mar 2018 12:29:38 -0800 Subject: [PATCH 047/117] Update test_x509.py Lint fixes. --- tests/unit/modules/test_x509.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index 1e30053af9..c524c19705 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -35,11 +35,12 @@ from tests.support.mock import ( from salt.modules import x509 try: - import m2crypto + import m2crypto # pylint: disable=unused-import HAS_M2CRYPTO = True except ImportError: HAS_M2CRYPTO = False + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not bool(pytest), False) class X509TestCase(TestCase, LoaderModuleMockMixin): From 2c43911f626eaa8336924fe63d378f282140583d Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Fri, 9 Mar 2018 07:33:24 -0800 Subject: [PATCH 048/117] Fixing lint. --- tests/unit/modules/test_x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index c524c19705..d54aa50f88 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -35,7 +35,7 @@ from tests.support.mock import ( from salt.modules import x509 try: - import m2crypto # pylint: disable=unused-import + import m2crypto # pylint: disable=unused-import HAS_M2CRYPTO = True except ImportError: HAS_M2CRYPTO = False From ed8c83e89ada72a81157940f7bddc3da5cde6363 Mon Sep 17 00:00:00 2001 From: Rongsheng Fang Date: Sat, 10 Mar 2018 09:19:37 -0500 Subject: [PATCH 049/117] Fix KeyError in salt/states/boto_ec2.py when an EIP is being associated to an existing instance with the instance_present state. Fixes #46479. --- salt/states/boto_ec2.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/states/boto_ec2.py b/salt/states/boto_ec2.py index ac88c30c11..5f373ecbce 100644 --- a/salt/states/boto_ec2.py +++ b/salt/states/boto_ec2.py @@ -886,6 +886,8 @@ def instance_present(name, instance_name=None, instance_id=None, image_id=None, allocation_id=allocation_id, region=region, key=key, keyid=keyid, profile=profile) if r: + if 'new' not in ret['changes']: + ret['changes']['new'] = {} ret['changes']['new']['public_ip'] = ip else: ret['result'] = False From 84d720b14c846ced4610b45189ec26d17a651ed3 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Sat, 10 Mar 2018 06:21:47 -0800 Subject: [PATCH 050/117] Adding some code that was accidently reverted back in. --- salt/modules/x509.py | 4 ++++ tests/unit/modules/test_x509.py | 42 ++++++++++++++++++++++++++++++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/salt/modules/x509.py b/salt/modules/x509.py index dfdcdb0e83..73da9530cf 100644 --- a/salt/modules/x509.py +++ b/salt/modules/x509.py @@ -131,6 +131,10 @@ def _new_extension(name, value, critical=0, issuer=None, _pyfree=1): raise salt.exceptions.SaltInvocationError( 'value must be precomputed hash') + # ensure name and value are bytes + name = salt.utils.stringutils.to_bytes(name) + value = salt.utils.stringutils.to_bytes(value) + try: ctx = M2Crypto.m2.x509v3_set_nconf() _fix_ctx(ctx, issuer) diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index d54aa50f88..edfcad7246 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -35,7 +35,7 @@ from tests.support.mock import ( from salt.modules import x509 try: - import m2crypto # pylint: disable=unused-import + import M2Crypto # pylint: disable=unused-import HAS_M2CRYPTO = True except ImportError: HAS_M2CRYPTO = False @@ -124,3 +124,43 @@ c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== ret = x509.get_private_key_size(ca_key) self.assertEqual(ret, 1024) + + @skipIf(not HAS_M2CRYPTO, 'Skipping, M2Crypt is unavailble') + def test_create_certificate(self): + ''' + Test private function _parse_subject(subject) it handles a missing fields + :return: + ''' + ca_key = ''' +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCjdjbgL4kQ8Lu73xeRRM1q3C3K3ptfCLpyfw38LRnymxaoJ6ls +pNSx2dU1uJ89YKFlYLo1QcEk4rJ2fdIjarV0kuNCY3rC8jYUp9BpAU5Z6p9HKeT1 +2rTPH81JyjbQDR5PyfCyzYOQtpwpB4zIUUK/Go7tTm409xGKbbUFugJNgQIDAQAB +AoGAF24we34U1ZrMLifSRv5nu3OIFNZHyx2DLDpOFOGaII5edwgIXwxZeIzS5Ppr +yO568/8jcdLVDqZ4EkgCwRTgoXRq3a1GLHGFmBdDNvWjSTTMLoozuM0t2zjRmIsH +hUd7tnai9Lf1Bp5HlBEhBU2gZWk+SXqLvxXe74/+BDAj7gECQQDRw1OPsrgTvs3R +3MNwX6W8+iBYMTGjn6f/6rvEzUs/k6rwJluV7n8ISNUIAxoPy5g5vEYK6Ln/Ttc7 +u0K1KNlRAkEAx34qcxjuswavL3biNGE+8LpDJnJx1jaNWoH+ObuzYCCVMusdT2gy +kKuq9ytTDgXd2qwZpIDNmscvReFy10glMQJAXebMz3U4Bk7SIHJtYy7OKQzn0dMj +35WnRV81c2Jbnzhhu2PQeAvt/i1sgEuzLQL9QEtSJ6wLJ4mJvImV0TdaIQJAAYyk +TcKK0A8kOy0kMp3yvDHmJZ1L7wr7bBGIZPBlQ0Ddh8i1sJExm1gJ+uN2QKyg/XrK +tDFf52zWnCdVGgDwcQJALW/WcbSEK+JVV6KDJYpwCzWpKIKpBI0F6fdCr1G7Xcwj +c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== +-----END RSA PRIVATE KEY----- +''' + + ret = x509.create_certificate(text=True, + signing_private_key=ca_key, + CN='Redacted Root CA', + O=R'edacted', + C='BE', + ST='Antwerp', + L='Local Town', + Email='certadm@example.org', + basicConstraints="critical CA:true", + keyUsage="critical cRLSign, keyCertSign", + subjectKeyIdentifier='hash', + authorityKeyIdentifier='keyid,issuer:always', + days_valid=3650, + days_remaining=0) + self.assertIn('BEGIN CERTIFICATE', ret) From 08fce29653e07b17b6c38ce32e35907f23e81ae4 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Sat, 10 Mar 2018 06:23:00 -0800 Subject: [PATCH 051/117] Silly typo in test. --- tests/unit/modules/test_x509.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/modules/test_x509.py b/tests/unit/modules/test_x509.py index edfcad7246..a56cc4a9da 100644 --- a/tests/unit/modules/test_x509.py +++ b/tests/unit/modules/test_x509.py @@ -152,7 +152,7 @@ c9bcgp7D7xD+TxWWNj4CSXEccJgGr91StV+gFg4ARQ== ret = x509.create_certificate(text=True, signing_private_key=ca_key, CN='Redacted Root CA', - O=R'edacted', + O='Redacted', C='BE', ST='Antwerp', L='Local Town', From f06ff68f10012866a84e826ea6d926d04b18ff4b Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 12 Mar 2018 11:36:08 -0500 Subject: [PATCH 052/117] salt-call: don't re-use initial pillar if CLI overrides passed A salt-call run already compiles pillar before executing the specified function. Therefore, a performance improvement was made to re-use that initial pillar data when running states. However, when we just re-use that pillar data, we lose the ability to have custom external pillar modules gain access to CLI pillar overrides. Therefore, this commit changes the code in the state compiler which gathers/re-uses the pillar data so that it only re-uses the existing in-memory pillar data when no CLI pillar overrides were passed. --- salt/state.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/salt/state.py b/salt/state.py index 17c7fd3b54..8edbce0e4a 100644 --- a/salt/state.py +++ b/salt/state.py @@ -703,8 +703,12 @@ class State(object): except AttributeError: pillar_enc = str(pillar_enc).lower() self._pillar_enc = pillar_enc - if initial_pillar: + if initial_pillar and not self._pillar_override: self.opts['pillar'] = initial_pillar + else: + # Compile pillar data + self.opts['pillar'] = self._gather_pillar() + # Reapply overrides on top of compiled pillar if self._pillar_override: self.opts['pillar'] = salt.utils.dictupdate.merge( self.opts['pillar'], @@ -712,8 +716,6 @@ class State(object): self.opts.get('pillar_source_merging_strategy', 'smart'), self.opts.get('renderer', 'yaml'), self.opts.get('pillar_merge_lists', False)) - else: - self.opts['pillar'] = self._gather_pillar() self.state_con = context or {} self.load_modules() self.active = set() From fe2efe03ea344b9affdba517a174dfd72644926b Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Fri, 9 Mar 2018 15:50:00 -0700 Subject: [PATCH 053/117] remove duplicate setup --- .kitchen.yml | 43 +++++++++++++++---------------------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/.kitchen.yml b/.kitchen.yml index 28fe65c62d..00546457a1 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -1,6 +1,6 @@ --- <% vagrant = system('which vagrant 2>/dev/null >/dev/null') %> -<% version = '2017.7.1' %> +<% version = '2017.7.4' %> <% platformsfile = ENV['SALT_KITCHEN_PLATFORMS'] || '.kitchen/platforms.yml' %> <% driverfile = ENV['SALT_KITCHEN_DRIVER'] || '.kitchen/driver.yml' %> <% verifierfile = ENV['SALT_KITCHEN_VERIFIER'] || '.kitchen/verifier.yml' %> @@ -54,6 +54,20 @@ provisioner: - prep_windows "*": - git.salt + pillars: + top.sls: + base: + "*": + - jenkins + "os:Windows": + - match: grain + - windows + jenkins.sls: + testing_dir: "{{salt.config.get('root_dir')|replace('\\', '\\\\')}}/testing" + clone_repo: false + salttesting_namespec: salttesting==2017.6.1 + windows.sls: + virtualenv_path: 'c:\Python27\Scripts\pip.exe' <% if File.exists?(platformsfile) %> <%= ERB.new(File.read(platformsfile)).result %> <% else %> @@ -177,21 +191,6 @@ suites: - name: py2 verifier: python_bin: python2.7 - provisioner: - pillars: - top.sls: - base: - "*": - - jenkins - "os:Windows": - - match: grain - - windows - jenkins.sls: - testing_dir: "{{salt.config.get('root_dir')|replace('\\', '\\\\')}}/testing" - clone_repo: false - salttesting_namespec: salttesting==2017.6.1 - windows.sls: - virtualenv_path: 'c:\Python27\Scripts\pip.exe' - name: py3 excludes: - centos-6 @@ -200,20 +199,8 @@ suites: python_bin: python3 provisioner: pillars: - top.sls: - base: - "*": - - jenkins - "os:Windows": - - match: grain - - windows jenkins.sls: - testing_dir: "{{salt.config.get('root_dir')|replace('\\', '\\\\')}}/testing" - clone_repo: false py3: true - salttesting_namespec: salttesting==2017.6.1 - windows.sls: - virtualenv_path: 'c:\Python35\Scripts\pip.exe' <% if File.exists?(verifierfile) %> <%= ERB.new(File.read(verifierfile)).result %> From da002f78d05a67a007525ae1845f52c5b2d0b169 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Mon, 12 Mar 2018 12:24:51 -0600 Subject: [PATCH 054/117] include virtualenv path for py3 windows --- .kitchen.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.kitchen.yml b/.kitchen.yml index 00546457a1..b7887dd32a 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -201,6 +201,8 @@ suites: pillars: jenkins.sls: py3: true + windows.sls: + virtualenv_path: 'c:\Python35\Scripts\pip.exe' <% if File.exists?(verifierfile) %> <%= ERB.new(File.read(verifierfile)).result %> From a8ffceda53657d42d502c7bc20d8386e2ecc8f65 Mon Sep 17 00:00:00 2001 From: Stefan Reimer Date: Mon, 29 Jan 2018 14:04:59 -0800 Subject: [PATCH 055/117] Surpress boto WARNING during decode, reference: https://github.com/boto/boto/issues/2965 --- salt/engines/sqs_events.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/salt/engines/sqs_events.py b/salt/engines/sqs_events.py index 2120a4230c..c6a5bb7d3f 100644 --- a/salt/engines/sqs_events.py +++ b/salt/engines/sqs_events.py @@ -165,4 +165,6 @@ def start(queue, profile=None, tag='salt/engine/sqs', owner_acct_id=None): while True: if not q: q = sqs.get_queue(queue, owner_acct_id=owner_acct_id) + q.set_message_class(boto.sqs.message.RawMessage) + _process_queue(q, queue, fire_master, tag=tag, owner_acct_id=owner_acct_id, message_format=message_format) From 096bcb3ca9b086ecbd05c2f40b724b61d053f9cc Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Fri, 9 Mar 2018 15:48:58 -0600 Subject: [PATCH 056/117] salt-ssh: fix JSON load of return data when it contains non-ascii For reasons I can't explain, in `salt.utils.json.find_json()` using `.splitlines()` will sometimes convert a unicode string into a list of str types on Python 2. So, that's weird. This can be triggered in salt-ssh whenever there are non-ascii chars in the return data. [DEBUG] raw = u'{"local": {"return": {"foo": "\xf6\xe4\xfc"}}}' [DEBUG] raw.splitlines() = ['{"local": {"return": {"foo": "\xc3\xb6\xc3\xa4\xc3\xbc"}}}'] To resolve this, the UnicodeDecodeError is caught and reattempted with a decoded list. Additionally, this fixes a performance oversight. We process the string one line at a time but we are iterating a number of times equal to the length of the string. This means that we will nearly always end up doing a bunch of extra list slices resulting in empty lists, which when joined and loaded will produce ValueErrors, which we are catching and ignoring. By enumerating over the split string, we ensure that we only iterate at most a number of times equal to the amount of lines in the string. --- salt/utils/json.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/salt/utils/json.py b/salt/utils/json.py index 788505cb25..3a71f73b38 100644 --- a/salt/utils/json.py +++ b/salt/utils/json.py @@ -25,8 +25,13 @@ def find_json(raw): string to start with garbage and end with json but be cleanly loaded ''' ret = {} - for ind, _ in enumerate(raw): - working = '\n'.join(raw.splitlines()[ind:]) + lines = raw.splitlines() + for ind, _ in enumerate(lines): + try: + working = '\n'.join(lines[ind:]) + except UnicodeDecodeError: + working = '\n'.join(salt.utils.data.decode(lines[ind:])) + try: ret = json.loads(working) # future lint: blacklisted-function except ValueError: From 3cd371efe6201353dd6006c5bc40964fa5984579 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Mar 2018 10:41:55 -0500 Subject: [PATCH 057/117] Add with_tempfile helper from f9f187e This helper was part of a PR that did not get ported into the 2018.3.0rc1 branch, but to help with code re-use this commit adds the helper to 2018.3.0rc1. --- tests/support/helpers.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/tests/support/helpers.py b/tests/support/helpers.py index 05989848b7..5864c83b14 100644 --- a/tests/support/helpers.py +++ b/tests/support/helpers.py @@ -24,6 +24,7 @@ import signal import socket import string import sys +import tempfile import threading import time import tornado.ioloop @@ -50,7 +51,7 @@ except ImportError: # Import Salt Tests Support libs from tests.support.unit import skip, _id from tests.support.mock import patch -from tests.support.paths import FILES +from tests.support.paths import FILES, TMP log = logging.getLogger(__name__) @@ -954,6 +955,24 @@ def with_system_user_and_group(username, group, return decorator +def with_tempfile(func): + ''' + Generates a tempfile and cleans it up when test completes. + ''' + @functools.wraps(func) + def wrapper(self, *args, **kwargs): + fd_, name = tempfile.mkstemp(prefix='__salt.test.', dir=TMP) + os.close(fd_) + del fd_ + ret = func(self, name, *args, **kwargs) + try: + os.remove(name) + except Exception: + pass + return ret + return wrapper + + def requires_system_grains(func): ''' Function decorator which loads and passes the system's grains to the test From f721218166969e3bf218bc8a60360cc67ab983aa Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Tue, 13 Mar 2018 09:28:14 -0700 Subject: [PATCH 058/117] Claryfing the documentation for the new require_any requisite. --- doc/ref/states/requisites.rst | 2 +- doc/topics/releases/2018.3.0.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ref/states/requisites.rst b/doc/ref/states/requisites.rst index f55fd6c031..15242a7b77 100644 --- a/doc/ref/states/requisites.rst +++ b/doc/ref/states/requisites.rst @@ -263,7 +263,7 @@ The use of ``require_any`` demands that one of the required states executes befo dependent state. The state containing the ``require_any`` requisite is defined as the dependent state. The states specified in the ``require_any`` statement are defined as the required states. If at least one of the required state's execution succeeds, the dependent state -will then execute. If at least one of the required state's execution fails, the dependent state +will then execute. If all of the executions by the required states fail, the dependent state will not execute. .. code-block:: yaml diff --git a/doc/topics/releases/2018.3.0.rst b/doc/topics/releases/2018.3.0.rst index f92d63136a..74525f7bf9 100644 --- a/doc/topics/releases/2018.3.0.rst +++ b/doc/topics/releases/2018.3.0.rst @@ -1522,7 +1522,7 @@ The use of ``require_any`` demands that one of the required states executes befo dependent state. The state containing the ``require_any`` requisite is defined as the dependent state. The states specified in the ``require_any`` statement are defined as the required states. If at least one of the required state's execution succeeds, the dependent state -will then execute. If at least one of the required state's execution fails, the dependent state +will then execute. If all of the executions by the required states fail, the dependent state will not execute. - ``watch_any`` From a8b1f0d64081a39281bf9371eb022ee6227a93fd Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Tue, 13 Mar 2018 10:45:37 -0500 Subject: [PATCH 059/117] Add unit test for salt-ssh edge case --- salt/utils/json.py | 11 ++++++++- tests/unit/utils/test_json.py | 43 ++++++++++++++++------------------- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/salt/utils/json.py b/salt/utils/json.py index 3a71f73b38..a578b8f843 100644 --- a/salt/utils/json.py +++ b/salt/utils/json.py @@ -19,13 +19,22 @@ from salt.ext import six log = logging.getLogger(__name__) +def __split(raw): + ''' + Performs a splitlines on the string. This function exists to make mocking + possible in unit tests, since the member functions of the str/unicode + builtins cannot be mocked. + ''' + return raw.splitlines() + + def find_json(raw): ''' Pass in a raw string and load the json when it starts. This allows for a string to start with garbage and end with json but be cleanly loaded ''' ret = {} - lines = raw.splitlines() + lines = __split(raw) for ind, _ in enumerate(lines): try: working = '\n'.join(lines[ind:]) diff --git a/tests/unit/utils/test_json.py b/tests/unit/utils/test_json.py index 12ca1c37a1..20220459a3 100644 --- a/tests/unit/utils/test_json.py +++ b/tests/unit/utils/test_json.py @@ -4,37 +4,19 @@ Tests for salt.utils.json ''' # Import Python libs from __future__ import absolute_import, print_function, unicode_literals -import errno -import functools -import os import textwrap # Import Salt Testing libs -from tests.support.paths import TMP -from tests.support.unit import TestCase, LOREM_IPSUM +from tests.support.helpers import with_tempfile +from tests.support.mock import patch, MagicMock, NO_MOCK, NO_MOCK_REASON +from tests.support.unit import TestCase, LOREM_IPSUM, skipIf # Import Salt libs import salt.utils.files import salt.utils.json +import salt.utils.platform import salt.utils.stringutils - - -def with_tempfile(func): - ''' - Generate a temp directory for a test - ''' - @functools.wraps(func) - def wrapper(self, *args, **kwargs): - temp_file = salt.utils.files.mkstemp(dir=TMP) - try: - return func(self, temp_file, *args, **kwargs) - finally: - try: - os.remove(temp_file) - except OSError as exc: - if exc.errno != errno.ENOENT: - raise - return wrapper +from salt.ext import six class JSONTestCase(TestCase): @@ -113,6 +95,21 @@ class JSONTestCase(TestCase): # Test to see if a ValueError is raised if no JSON is passed in self.assertRaises(ValueError, salt.utils.json.find_json, LOREM_IPSUM) + @skipIf(salt.utils.platform.is_windows(), 'skip until we figure out what to do about decoding unicode on windows') + @skipIf(not six.PY2, 'Test only needed on Python 2') + @skipIf(NO_MOCK, NO_MOCK_REASON) + def test_find_json_unicode_splitlines(self): + ''' + Tests a case in salt-ssh where a unicode string is split into a list of + str types by .splitlines(). + ''' + raw = '{"foo": "öäü"}' + mock_split = MagicMock(return_value=[raw.encode('utf8')]) + + with patch.object(salt.utils.json, '__split', mock_split): + ret = salt.utils.json.find_json(raw) + self.assertEqual(ret, {'foo': 'öäü'}) + def test_dumps_loads(self): ''' Test dumping to and loading from a string From 50d6e2c7be86b46e8a37db130eb150e806f93994 Mon Sep 17 00:00:00 2001 From: Daniel Wallace Date: Tue, 13 Mar 2018 15:55:43 -0600 Subject: [PATCH 060/117] retry if there is a segfault --- .kitchen.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.kitchen.yml b/.kitchen.yml index b7887dd32a..99c6ae6974 100644 --- a/.kitchen.yml +++ b/.kitchen.yml @@ -34,6 +34,9 @@ provisioner: log_level: info sudo: true require_chef: false + retry_on_exit_code: + - 139 + max_retries: 2 remote_states: name: git://github.com/saltstack/salt-jenkins.git branch: 2017.7 From d3335b78eb32e154dea2f43875313b34bef4fceb Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 13 Mar 2018 17:12:16 -0600 Subject: [PATCH 061/117] Make iSCSI and Fibre Channel grains optional Creates iscsi.py and fibre_channel.py Gates the grains with options from the config file: - fibre_channel_grains - iscsi_grains --- salt/grains/core.py | 122 ----------------------------------- salt/grains/fibre_channel.py | 76 ++++++++++++++++++++++ salt/grains/iscsi.py | 115 +++++++++++++++++++++++++++++++++ 3 files changed, 191 insertions(+), 122 deletions(-) create mode 100644 salt/grains/fibre_channel.py create mode 100644 salt/grains/iscsi.py diff --git a/salt/grains/core.py b/salt/grains/core.py index 9352987abd..b7d446676e 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -15,7 +15,6 @@ from __future__ import absolute_import, print_function, unicode_literals import os import socket import sys -import glob import re import platform import logging @@ -64,7 +63,6 @@ __salt__ = { 'cmd.run_all': salt.modules.cmdmod._run_all_quiet, 'smbios.records': salt.modules.smbios.records, 'smbios.get': salt.modules.smbios.get, - 'cmd.run_ps': salt.modules.cmdmod.powershell, } log = logging.getLogger(__name__) @@ -2451,123 +2449,3 @@ def default_gateway(): except Exception: continue return grains - - -def fc_wwn(): - ''' - Return list of fiber channel HBA WWNs - ''' - grains = {} - grains['fc_wwn'] = False - if salt.utils.platform.is_linux(): - grains['fc_wwn'] = _linux_wwns() - elif salt.utils.platform.is_windows(): - grains['fc_wwn'] = _windows_wwns() - return grains - - -def iscsi_iqn(): - ''' - Return iSCSI IQN - ''' - grains = {} - grains['iscsi_iqn'] = False - if salt.utils.platform.is_linux(): - grains['iscsi_iqn'] = _linux_iqn() - elif salt.utils.platform.is_windows(): - grains['iscsi_iqn'] = _windows_iqn() - elif salt.utils.platform.is_aix(): - grains['iscsi_iqn'] = _aix_iqn() - return grains - - -def _linux_iqn(): - ''' - Return iSCSI IQN from a Linux host. - ''' - ret = [] - - initiator = '/etc/iscsi/initiatorname.iscsi' - try: - with salt.utils.files.fopen(initiator, 'r') as _iscsi: - for line in _iscsi: - line = line.strip() - if line.startswith('InitiatorName='): - ret.append(line.split('=', 1)[1]) - except IOError as ex: - if ex.errno != os.errno.ENOENT: - log.debug("Error while accessing '%s': %s", initiator, ex) - - return ret - - -def _aix_iqn(): - ''' - Return iSCSI IQN from an AIX host. - ''' - ret = [] - - aixcmd = 'lsattr -E -l iscsi0 | grep initiator_name' - - aixret = __salt__['cmd.run'](aixcmd) - if aixret[0].isalpha(): - try: - ret.append(aixret.split()[1].rstrip()) - except IndexError: - pass - return ret - - -def _linux_wwns(): - ''' - Return Fibre Channel port WWNs from a Linux host. - ''' - ret = [] - - for fcfile in glob.glob('/sys/class/fc_host/*/port_name'): - with salt.utils.files.fopen(fcfile, 'r') as _wwn: - for line in _wwn: - ret.append(line.rstrip()[2:]) - return ret - - -def _windows_iqn(): - ''' - Return iSCSI IQN from a Windows host. - ''' - ret = [] - - wmic = salt.utils.path.which('wmic') - - if not wmic: - return ret - - namespace = r'\\root\WMI' - mspath = 'MSiSCSIInitiator_MethodClass' - get = 'iSCSINodeName' - - cmdret = __salt__['cmd.run_all']( - '{0} /namespace:{1} path {2} get {3} /format:table'.format( - wmic, namespace, mspath, get)) - - for line in cmdret['stdout'].splitlines(): - if line.startswith('iqn.'): - line = line.rstrip() - ret.append(line.rstrip()) - return ret - - -def _windows_wwns(): - ''' - Return Fibre Channel port WWNs from a Windows host. - ''' - ps_cmd = r'Get-WmiObject -ErrorAction Stop -class MSFC_FibrePortHBAAttributes -namespace "root\WMI" | Select -Expandproperty Attributes | %{($_.PortWWN | % {"{0:x2}" -f $_}) -join ""}' - - ret = [] - - cmdret = __salt__['cmd.run_ps'](ps_cmd) - - for line in cmdret: - ret.append(line.rstrip()) - - return ret diff --git a/salt/grains/fibre_channel.py b/salt/grains/fibre_channel.py new file mode 100644 index 0000000000..9741054631 --- /dev/null +++ b/salt/grains/fibre_channel.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +''' +Grains for Fibre Channel WWN's. On Windows this runs a PowerShell command that +queries WMI to get the Fibre Channel WWN's available. + +.. versionadded:: 2018.3.0 + +To enable these grains set ``fibre_channel_grains: True``. + +.. code-block:: yaml + + fibre_channel_grains: True +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals + +import glob +import logging + +# Import Salt libs +import salt.modules.cmdmod +import salt.utils.platform +import salt.utils.files + +__proxyenabled__ = ['fibre_channel'] +__virtualname__ = 'fibre_channel' + +# Get logging started +log = logging.getLogger(__name__) + + +def __virtual__(): + if __opts__.get('fibre_channel_grains', False) is False: + return False + else: + return __virtualname__ + + +def _linux_wwns(): + ''' + Return Fibre Channel port WWNs from a Linux host. + ''' + ret = [] + for fc_file in glob.glob('/sys/class/fc_host/*/port_name'): + with salt.utils.files.fopen(fc_file, 'r') as _wwn: + for line in _wwn: + ret.append(line.rstrip()[2:]) + return ret + + +def _windows_wwns(): + ''' + Return Fibre Channel port WWNs from a Windows host. + ''' + ps_cmd = r'Get-WmiObject -ErrorAction Stop ' \ + r'-class MSFC_FibrePortHBAAttributes ' \ + r'-namespace "root\WMI" | ' \ + r'Select -Expandproperty Attributes | ' \ + r'%{($_.PortWWN | % {"{0:x2}" -f $_}) -join ""}' + ret = [] + cmd_ret = salt.modules.cmdmod.powershell(ps_cmd) + for line in cmd_ret: + ret.append(line.rstrip()) + return ret + + +def fibre_channel_wwns(): + ''' + Return list of fiber channel HBA WWNs + ''' + grains = {'fc_wwn': False} + if salt.utils.platform.is_linux(): + grains['fc_wwn'] = _linux_wwns() + elif salt.utils.platform.is_windows(): + grains['fc_wwn'] = _windows_wwns() + return grains diff --git a/salt/grains/iscsi.py b/salt/grains/iscsi.py new file mode 100644 index 0000000000..18aefbe36c --- /dev/null +++ b/salt/grains/iscsi.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +''' +Grains for iSCSI Qualified Names (IQN). + +.. versionadded:: 2018.3.0 + +To enable these grains set `iscsi_grains: True`. + +.. code-block:: yaml + + iscsi_grains: True +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals + +import logging +import os + +# Import Salt libs +import salt.modules.cmdmod +import salt.utils.files +import salt.utils.path +import salt.utils.platform + +__proxyenabled__ = ['iscsi'] +__virtualname__ = 'iscsi' + +# Get logging started +log = logging.getLogger(__name__) + + +def __virtual__(): + if __opts__.get('iscsi_grains', False) is False: + return False + else: + return __virtualname__ + + +def iscsi_iqn(): + ''' + Return iSCSI IQN + ''' + grains = {} + grains['iscsi_iqn'] = False + if salt.utils.platform.is_linux(): + grains['iscsi_iqn'] = _linux_iqn() + elif salt.utils.platform.is_windows(): + grains['iscsi_iqn'] = _windows_iqn() + elif salt.utils.platform.is_aix(): + grains['iscsi_iqn'] = _aix_iqn() + return grains + + +def _linux_iqn(): + ''' + Return iSCSI IQN from a Linux host. + ''' + ret = [] + + initiator = '/etc/iscsi/initiatorname.iscsi' + try: + with salt.utils.files.fopen(initiator, 'r') as _iscsi: + for line in _iscsi: + line = line.strip() + if line.startswith('InitiatorName='): + ret.append(line.split('=', 1)[1]) + except IOError as ex: + if ex.errno != os.errno.ENOENT: + log.debug("Error while accessing '%s': %s", initiator, ex) + + return ret + + +def _aix_iqn(): + ''' + Return iSCSI IQN from an AIX host. + ''' + ret = [] + + aix_cmd = 'lsattr -E -l iscsi0 | grep initiator_name' + + aix_ret = salt.modules.cmdmod.run(aix_cmd) + if aix_ret[0].isalpha(): + try: + ret.append(aix_ret.split()[1].rstrip()) + except IndexError: + pass + return ret + + +def _windows_iqn(): + ''' + Return iSCSI IQN from a Windows host. + ''' + ret = [] + + wmic = salt.utils.path.which('wmic') + + if not wmic: + return ret + + namespace = r'\\root\WMI' + path = 'MSiSCSIInitiator_MethodClass' + get = 'iSCSINodeName' + + cmd_ret = salt.modules.cmdmod.run_all( + '{0} /namespace:{1} path {2} get {3} /format:table' + ''.format(wmic, namespace, path, get)) + + for line in cmd_ret['stdout'].splitlines(): + if line.startswith('iqn.'): + line = line.rstrip() + ret.append(line.rstrip()) + + return ret From ea3c16080e8226b1b2b5931d72d9592874b40ff6 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Wed, 14 Mar 2018 11:24:43 -0700 Subject: [PATCH 062/117] Disable the `service` module on Cumulus since it is using systemd. --- salt/modules/service.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/modules/service.py b/salt/modules/service.py index 49186e4c9d..618c3b1f9c 100644 --- a/salt/modules/service.py +++ b/salt/modules/service.py @@ -45,7 +45,8 @@ def __virtual__(): 'Void', 'Mint', 'Raspbian', - 'XenServer' + 'XenServer', + 'Cumulus' )) if __grains__.get('os', '') in disable: return (False, 'Your OS is on the disabled list') From db96c4e72edb89c7f76e52d5c978e70b320d6a06 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 14 Mar 2018 14:57:48 -0400 Subject: [PATCH 063/117] check for foo,bar username,password set in profitbrick config --- tests/integration/cloud/providers/test_profitbricks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/cloud/providers/test_profitbricks.py b/tests/integration/cloud/providers/test_profitbricks.py index d51fd9a0e3..d004711113 100644 --- a/tests/integration/cloud/providers/test_profitbricks.py +++ b/tests/integration/cloud/providers/test_profitbricks.py @@ -78,7 +78,7 @@ class ProfitBricksTest(ShellCase): username = config[profile_str][DRIVER_NAME]['username'] password = config[profile_str][DRIVER_NAME]['password'] datacenter_id = config[profile_str][DRIVER_NAME]['datacenter_id'] - if username == '' or password == '' or datacenter_id == '': + if username in ('' or 'foo') or password in ('' or 'bar') or datacenter_id == '': self.skipTest( 'A username, password, and an datacenter must be provided to ' 'run these tests. Check ' From bb338c464c082464aeb175c8fab8aeab75e331ef Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 14 Mar 2018 15:28:06 -0400 Subject: [PATCH 064/117] Fix dimensionsdata test random_name call --- tests/integration/cloud/providers/test_dimensiondata.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/integration/cloud/providers/test_dimensiondata.py b/tests/integration/cloud/providers/test_dimensiondata.py index bd8425f528..504c34cb4e 100644 --- a/tests/integration/cloud/providers/test_dimensiondata.py +++ b/tests/integration/cloud/providers/test_dimensiondata.py @@ -18,10 +18,6 @@ from tests.support.helpers import expensiveTest from salt.config import cloud_providers_config from salt.ext.six.moves import range # pylint: disable=import-error,redefined-builtin -# Create the cloud instance name to be used throughout the tests -INSTANCE_NAME = _random_name('CLOUD-TEST-') -PROVIDER_NAME = 'dimensiondata' - def _random_name(size=6): ''' @@ -32,6 +28,10 @@ def _random_name(size=6): for x in range(size) ) +# Create the cloud instance name to be used throughout the tests +INSTANCE_NAME = _random_name() +PROVIDER_NAME = 'dimensiondata' + class DimensionDataTest(ShellCase): ''' From 16949c0b93ad94b574c065a923cb74c462e9ff93 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 14 Mar 2018 16:37:42 -0600 Subject: [PATCH 065/117] Fix failing tests, add new tests --- tests/unit/grains/test_core.py | 79 --------------------- tests/unit/grains/test_fibre_channel.py | 54 ++++++++++++++ tests/unit/grains/test_iscsi.py | 94 +++++++++++++++++++++++++ 3 files changed, 148 insertions(+), 79 deletions(-) create mode 100644 tests/unit/grains/test_fibre_channel.py create mode 100644 tests/unit/grains/test_iscsi.py diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py index e781fadefe..50babe3ed3 100644 --- a/tests/unit/grains/test_core.py +++ b/tests/unit/grains/test_core.py @@ -487,58 +487,6 @@ PATCHLEVEL = 3 self.assertListEqual(list(os_grains.get('osrelease_info')), os_release_map['osrelease_info']) self.assertEqual(os_grains.get('osmajorrelease'), os_release_map['osmajorrelease']) - def test_windows_iscsi_iqn_grains(self): - cmd_run_mock = MagicMock( - return_value={'stdout': 'iSCSINodeName\niqn.1991-05.com.microsoft:simon-x1\n'} - ) - - with patch.object(salt.utils.platform, 'is_linux', - MagicMock(return_value=False)): - with patch.object(salt.utils.platform, 'is_windows', - MagicMock(return_value=True)): - with patch.dict(core.__salt__, {'run_all': cmd_run_mock}): - with patch.object(salt.utils.path, 'which', - MagicMock(return_value=True)): - with patch.dict(core.__salt__, {'cmd.run_all': cmd_run_mock}): - _grains = core.iscsi_iqn() - - self.assertEqual(_grains.get('iscsi_iqn'), - ['iqn.1991-05.com.microsoft:simon-x1']) - - @skipIf(salt.utils.platform.is_windows(), 'System is Windows') - def test_aix_iscsi_iqn_grains(self): - cmd_run_mock = MagicMock( - return_value='initiator_name iqn.localhost.hostid.7f000001' - ) - - with patch.object(salt.utils.platform, 'is_linux', - MagicMock(return_value=False)): - with patch.object(salt.utils.platform, 'is_aix', - MagicMock(return_value=True)): - with patch.dict(core.__salt__, {'cmd.run': cmd_run_mock}): - _grains = core.iscsi_iqn() - - self.assertEqual(_grains.get('iscsi_iqn'), - ['iqn.localhost.hostid.7f000001']) - - @patch('salt.grains.core.os.path.isfile', MagicMock(return_value=True)) - @patch('salt.grains.core.os.access', MagicMock(return_value=True)) - def test_linux_iscsi_iqn_grains(self): - _iscsi_file = '## DO NOT EDIT OR REMOVE THIS FILE!\n' \ - '## If you remove this file, the iSCSI daemon will not start.\n' \ - '## If you change the InitiatorName, existing access control lists\n' \ - '## may reject this initiator. The InitiatorName must be unique\n' \ - '## for each iSCSI initiator. Do NOT duplicate iSCSI InitiatorNames.\n' \ - 'InitiatorName=iqn.1993-08.org.debian:01:d12f7aba36\n' - - with patch('salt.utils.files.fopen', mock_open()) as iscsi_initiator_file: - iscsi_initiator_file.return_value.__iter__.return_value = _iscsi_file.splitlines() - iqn = core._linux_iqn() - - assert isinstance(iqn, list) - assert len(iqn) == 1 - assert iqn == ['iqn.1993-08.org.debian:01:d12f7aba36'] - @skipIf(not salt.utils.platform.is_linux(), 'System is not Linux') def test_linux_memdata(self): ''' @@ -835,30 +783,3 @@ SwapTotal: 4789244 kB''' []}} with patch.object(salt.utils.dns, 'parse_resolv', MagicMock(return_value=resolv_mock)): assert core.dns() == ret - - @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.EPERM, - 'The cables are not the same length.'))) - @patch('salt.grains.core.log', MagicMock()) - def test_linux_iqn_non_root(self): - ''' - Test if linux_iqn is running on salt-master as non-root - and handling access denial properly. - :return: - ''' - assert core._linux_iqn() == [] - core.log.debug.assert_called() - assert 'Error while accessing' in core.log.debug.call_args[0][0] - assert 'cables are not the same' in core.log.debug.call_args[0][2].strerror - assert core.log.debug.call_args[0][2].errno == os.errno.EPERM - assert core.log.debug.call_args[0][1] == '/etc/iscsi/initiatorname.iscsi' - - @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.ENOENT, ''))) - @patch('salt.grains.core.log', MagicMock()) - def test_linux_iqn_no_iscsii_initiator(self): - ''' - Test if linux_iqn is running on salt-master as root. - iscsii initiator is not there accessible or is not supported. - :return: - ''' - assert core._linux_iqn() == [] - core.log.debug.assert_not_called() diff --git a/tests/unit/grains/test_fibre_channel.py b/tests/unit/grains/test_fibre_channel.py new file mode 100644 index 0000000000..b8cb680844 --- /dev/null +++ b/tests/unit/grains/test_fibre_channel.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Shane Lee ` +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals + +# Import Salt Testing Libs +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + mock_open, + MagicMock, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +import salt.grains.fibre_channel as fibre_channel + + + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class FibreChannelGrainsTestCase(TestCase): + ''' + Test cases for iscsi grains + ''' + def test_windows_fibre_channel_wwns_grains(self): + wwns = ['20:00:00:25:b5:11:11:4c', + '20:00:00:25:b5:11:11:5c', + '20:00:00:25:b5:44:44:4c', + '20:00:00:25:b5:44:44:5c'] + cmd_run_mock = MagicMock(return_value=wwns) + with patch('salt.modules.cmdmod.powershell', cmd_run_mock): + ret = fibre_channel._windows_wwns() + self.assertEqual(ret, wwns) + + @skipIf(True, "I can't get this to work") + def test_linux_fibre_channel_wwns_grains(self): + + def multi_mock_open(*file_contents): + mock_files = [mock_open(read_data=content).return_value for content in file_contents] + mock_opener = mock_open() + mock_opener.side_effect = mock_files + + return mock_opener + + files = ['file1', 'file2'] + with patch('glob.glob', MagicMock(return_value=files)): + with patch('salt.utils.files.fopen', multi_mock_open('0x500143802426baf4', '0x500143802426baf5')): + ret = fibre_channel._linux_wwns() + + self.assertEqual(ret, ['0x500143802426baf4', '0x500143802426baf5']) diff --git a/tests/unit/grains/test_iscsi.py b/tests/unit/grains/test_iscsi.py new file mode 100644 index 0000000000..d4cf655d51 --- /dev/null +++ b/tests/unit/grains/test_iscsi.py @@ -0,0 +1,94 @@ +# -*- coding: utf-8 -*- +''' + :codeauthor: :email:`Shane Lee ` +''' +# Import Python libs +from __future__ import absolute_import, print_function, unicode_literals +import os + +# Import Salt Testing Libs +from tests.support.unit import TestCase, skipIf +from tests.support.mock import ( + patch, + mock_open, + MagicMock, + NO_MOCK, + NO_MOCK_REASON +) + +# Import Salt Libs +import salt.grains.iscsi as iscsi + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +class IscsiGrainsTestCase(TestCase): + ''' + Test cases for iscsi grains + ''' + def test_windows_iscsi_iqn_grains(self): + cmd_run_mock = MagicMock( + return_value={'stdout': 'iSCSINodeName\n' + 'iqn.1991-05.com.microsoft:simon-x1\n'} + ) + _grains = {} + with patch('salt.utils.path.which', MagicMock(return_value=True)) and \ + patch('salt.modules.cmdmod.run_all', cmd_run_mock): + _grains['iscsi_iqn'] = iscsi._windows_iqn() + + self.assertEqual(_grains.get('iscsi_iqn'), + ['iqn.1991-05.com.microsoft:simon-x1']) + + def test_aix_iscsi_iqn_grains(self): + cmd_run_mock = MagicMock( + return_value='initiator_name iqn.localhost.hostid.7f000001' + ) + + _grains = {} + with patch('salt.modules.cmdmod.run', cmd_run_mock): + _grains['iscsi_iqn'] = iscsi._aix_iqn() + + self.assertEqual(_grains.get('iscsi_iqn'), + ['iqn.localhost.hostid.7f000001']) + + def test_linux_iscsi_iqn_grains(self): + _iscsi_file = '## DO NOT EDIT OR REMOVE THIS FILE!\n' \ + '## If you remove this file, the iSCSI daemon will not start.\n' \ + '## If you change the InitiatorName, existing access control lists\n' \ + '## may reject this initiator. The InitiatorName must be unique\n' \ + '## for each iSCSI initiator. Do NOT duplicate iSCSI InitiatorNames.\n' \ + 'InitiatorName=iqn.1993-08.org.debian:01:d12f7aba36\n' + + with patch('salt.utils.files.fopen', mock_open()) as iscsi_initiator_file: + iscsi_initiator_file.return_value.__iter__.return_value = _iscsi_file.splitlines() + iqn = iscsi._linux_iqn() + + assert isinstance(iqn, list) + assert len(iqn) == 1 + assert iqn == ['iqn.1993-08.org.debian:01:d12f7aba36'] + + @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.EPERM, + 'The cables are not the same length.'))) + @patch('salt.grains.iscsi.log', MagicMock()) + def test_linux_iqn_non_root(self): + ''' + Test if linux_iqn is running on salt-master as non-root + and handling access denial properly. + :return: + ''' + assert iscsi._linux_iqn() == [] + iscsi.log.debug.assert_called() + assert 'Error while accessing' in iscsi.log.debug.call_args[0][0] + assert 'cables are not the same' in iscsi.log.debug.call_args[0][2].strerror + assert iscsi.log.debug.call_args[0][2].errno == os.errno.EPERM + assert iscsi.log.debug.call_args[0][1] == '/etc/iscsi/initiatorname.iscsi' + + @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.ENOENT, ''))) + @patch('salt.grains.iscsi.log', MagicMock()) + def test_linux_iqn_no_iscsii_initiator(self): + ''' + Test if linux_iqn is running on salt-master as root. + iscsii initiator is not there accessible or is not supported. + :return: + ''' + assert iscsi._linux_iqn() == [] + iscsi.log.debug.assert_not_called() From 5636ab7cd4295e14b379cad5680e17653ad446c8 Mon Sep 17 00:00:00 2001 From: twangboy Date: Wed, 14 Mar 2018 16:39:40 -0600 Subject: [PATCH 066/117] Fix some lint --- tests/unit/grains/test_fibre_channel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/grains/test_fibre_channel.py b/tests/unit/grains/test_fibre_channel.py index b8cb680844..59eed81689 100644 --- a/tests/unit/grains/test_fibre_channel.py +++ b/tests/unit/grains/test_fibre_channel.py @@ -19,8 +19,6 @@ from tests.support.mock import ( import salt.grains.fibre_channel as fibre_channel - - @skipIf(NO_MOCK, NO_MOCK_REASON) class FibreChannelGrainsTestCase(TestCase): ''' From 536ba0fa1ed8e752a0be7d86154f287329419663 Mon Sep 17 00:00:00 2001 From: Mihai Dinca Date: Wed, 7 Mar 2018 13:11:16 +0100 Subject: [PATCH 067/117] Fix cp.push empty file Co-authored-by: Jochen Breuer --- salt/master.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/master.py b/salt/master.py index c9f0bac35c..5d5c8f71fb 100644 --- a/salt/master.py +++ b/salt/master.py @@ -1451,7 +1451,7 @@ class AESFuncs(object): if load['loc']: fp_.seek(load['loc']) - fp_.write(load['data']) + fp_.write(salt.utils.stringutils.to_bytes(load['data'])) return True def _pillar(self, load): From 8474006dabc9c0c8c9d8f603c3c340f2adfdac66 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 15 Mar 2018 11:15:57 -0600 Subject: [PATCH 068/117] Fix 2 tests Modifies the fibre_channel.py grain for linux to allow mocking. You can't mock a file handle used in a for loop. Fixes the mocking in test_windows_iscsi_iqn_grains. Apparently, you can't stack multiple mocks in the same with statement. --- salt/grains/fibre_channel.py | 3 ++- tests/unit/grains/test_fibre_channel.py | 5 ++--- tests/unit/grains/test_iscsi.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/salt/grains/fibre_channel.py b/salt/grains/fibre_channel.py index 9741054631..a189feefdf 100644 --- a/salt/grains/fibre_channel.py +++ b/salt/grains/fibre_channel.py @@ -43,7 +43,8 @@ def _linux_wwns(): ret = [] for fc_file in glob.glob('/sys/class/fc_host/*/port_name'): with salt.utils.files.fopen(fc_file, 'r') as _wwn: - for line in _wwn: + content = _wwn.read() + for line in content.splitlines(): ret.append(line.rstrip()[2:]) return ret diff --git a/tests/unit/grains/test_fibre_channel.py b/tests/unit/grains/test_fibre_channel.py index 59eed81689..2963fb6f98 100644 --- a/tests/unit/grains/test_fibre_channel.py +++ b/tests/unit/grains/test_fibre_channel.py @@ -34,7 +34,6 @@ class FibreChannelGrainsTestCase(TestCase): ret = fibre_channel._windows_wwns() self.assertEqual(ret, wwns) - @skipIf(True, "I can't get this to work") def test_linux_fibre_channel_wwns_grains(self): def multi_mock_open(*file_contents): @@ -46,7 +45,7 @@ class FibreChannelGrainsTestCase(TestCase): files = ['file1', 'file2'] with patch('glob.glob', MagicMock(return_value=files)): - with patch('salt.utils.files.fopen', multi_mock_open('0x500143802426baf4', '0x500143802426baf5')): + with patch('salt.utils.files.fopen', multi_mock_open(u'0x500143802426baf4', u'0x500143802426baf5')): ret = fibre_channel._linux_wwns() - self.assertEqual(ret, ['0x500143802426baf4', '0x500143802426baf5']) + self.assertEqual(ret, ['500143802426baf4', '500143802426baf5']) diff --git a/tests/unit/grains/test_iscsi.py b/tests/unit/grains/test_iscsi.py index d4cf655d51..9ca4ad4809 100644 --- a/tests/unit/grains/test_iscsi.py +++ b/tests/unit/grains/test_iscsi.py @@ -31,9 +31,9 @@ class IscsiGrainsTestCase(TestCase): 'iqn.1991-05.com.microsoft:simon-x1\n'} ) _grains = {} - with patch('salt.utils.path.which', MagicMock(return_value=True)) and \ - patch('salt.modules.cmdmod.run_all', cmd_run_mock): - _grains['iscsi_iqn'] = iscsi._windows_iqn() + with patch('salt.utils.path.which', MagicMock(return_value=True)): + with patch('salt.modules.cmdmod.run_all', cmd_run_mock): + _grains['iscsi_iqn'] = iscsi._windows_iqn() self.assertEqual(_grains.get('iscsi_iqn'), ['iqn.1991-05.com.microsoft:simon-x1']) From 50a860b83911c7aa4d9a138b2fff68b1ba753d18 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 15 Mar 2018 12:55:26 -0600 Subject: [PATCH 069/117] Remove unicode things --- tests/unit/grains/test_fibre_channel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/grains/test_fibre_channel.py b/tests/unit/grains/test_fibre_channel.py index 2963fb6f98..4b75c1d73b 100644 --- a/tests/unit/grains/test_fibre_channel.py +++ b/tests/unit/grains/test_fibre_channel.py @@ -45,7 +45,7 @@ class FibreChannelGrainsTestCase(TestCase): files = ['file1', 'file2'] with patch('glob.glob', MagicMock(return_value=files)): - with patch('salt.utils.files.fopen', multi_mock_open(u'0x500143802426baf4', u'0x500143802426baf5')): + with patch('salt.utils.files.fopen', multi_mock_open('0x500143802426baf4', '0x500143802426baf5')): ret = fibre_channel._linux_wwns() self.assertEqual(ret, ['500143802426baf4', '500143802426baf5']) From 8886b615766710d489f478508f6b17e4261f7412 Mon Sep 17 00:00:00 2001 From: rallytime Date: Thu, 15 Mar 2018 17:31:29 -0400 Subject: [PATCH 070/117] Update old utils paths to new paths --- salt/modules/debconfmod.py | 3 ++- salt/modules/heat.py | 5 +++-- salt/states/heat.py | 3 ++- tests/integration/states/test_file.py | 10 ++++++---- tests/unit/returners/test_highstate_return.py | 4 ++-- 5 files changed, 15 insertions(+), 10 deletions(-) diff --git a/salt/modules/debconfmod.py b/salt/modules/debconfmod.py index 9249b5ba41..a4e08870f0 100644 --- a/salt/modules/debconfmod.py +++ b/salt/modules/debconfmod.py @@ -12,6 +12,7 @@ import re # Import salt libs import salt.utils.path import salt.utils.files +import salt.utils.stringutils import salt.utils.versions log = logging.getLogger(__name__) @@ -128,7 +129,7 @@ def set_(package, question, type, value, *extra): fd_, fname = salt.utils.files.mkstemp(prefix="salt-", close_fd=False) line = "{0} {1} {2} {3}".format(package, question, type, value) - os.write(fd_, salt.utils.to_bytes(line)) + os.write(fd_, salt.utils.stringutils.to_bytes(line)) os.close(fd_) _set_file(fname) diff --git a/salt/modules/heat.py b/salt/modules/heat.py index 6fa48b51cd..170c56cb17 100644 --- a/salt/modules/heat.py +++ b/salt/modules/heat.py @@ -51,6 +51,7 @@ from salt.ext import six import salt.utils.files import salt.utils.json import salt.utils.stringutils +import salt.utils.versions import salt.utils.yaml # pylint: disable=import-error @@ -476,7 +477,7 @@ def create_stack(name=None, template_file=None, environment=None, ''' if environment is None and enviroment is not None: - salt.utils.warn_until('Neon', ( + salt.utils.versions.warn_until('Neon', ( "Please use the 'environment' parameter instead of the misspelled 'enviroment' " "parameter which will be removed in Salt Neon." )) @@ -671,7 +672,7 @@ def update_stack(name=None, template_file=None, environment=None, ''' if environment is None and enviroment is not None: - salt.utils.warn_until('Neon', ( + salt.utils.versions.warn_until('Neon', ( "Please use the 'environment' parameter instead of the misspelled 'enviroment' " "parameter which will be removed in Salt Neon." )) diff --git a/salt/states/heat.py b/salt/states/heat.py index a5048ca2ca..47f667451b 100644 --- a/salt/states/heat.py +++ b/salt/states/heat.py @@ -49,6 +49,7 @@ import salt.exceptions import salt.utils.files import salt.utils.json import salt.utils.stringutils +import salt.utils.versions import salt.utils.yaml # Import 3rd-party libs @@ -136,7 +137,7 @@ def deployed(name, template=None, environment=None, params=None, poll=5, ''' if environment is None and 'enviroment' in connection_args: - salt.utils.warn_until('Neon', ( + salt.utils.versions.warn_until('Neon', ( "Please use the 'environment' parameter instead of the misspelled 'enviroment' " "parameter which will be removed in Salt Neon." )) diff --git a/tests/integration/states/test_file.py b/tests/integration/states/test_file.py index 0a3c5778c1..e3f7d20d08 100644 --- a/tests/integration/states/test_file.py +++ b/tests/integration/states/test_file.py @@ -33,9 +33,11 @@ from tests.support.helpers import ( from tests.support.mixins import SaltReturnAssertsMixin # Import Salt libs +import salt.utils.data import salt.utils.files import salt.utils.path import salt.utils.platform +import salt.utils.stringutils HAS_PWD = True try: @@ -2592,13 +2594,13 @@ class BlockreplaceTest(ModuleCase, SaltReturnAssertsMixin): @staticmethod def _write(dest, content): - with salt.utils.fopen(dest, 'wb') as fp_: - fp_.write(salt.utils.to_bytes(content)) + with salt.utils.files.fopen(dest, 'wb') as fp_: + fp_.write(salt.utils.stringutils.to_bytes(content)) @staticmethod def _read(src): - with salt.utils.fopen(src, 'rb') as fp_: - return salt.utils.to_unicode(fp_.read()) + with salt.utils.files.fopen(src, 'rb') as fp_: + return salt.utils.stringutils.to_unicode(fp_.read()) @with_tempfile def test_prepend(self, name): diff --git a/tests/unit/returners/test_highstate_return.py b/tests/unit/returners/test_highstate_return.py index 7f2ac910e7..a00cf70f50 100644 --- a/tests/unit/returners/test_highstate_return.py +++ b/tests/unit/returners/test_highstate_return.py @@ -18,7 +18,7 @@ from tests.support.runtests import RUNTIME_VARS from tests.support.unit import TestCase # Import Salt libs -import salt.utils +import salt.utils.files import salt.returners.highstate_return as highstate log = logging.getLogger(__name__) @@ -110,5 +110,5 @@ class HighstateReturnerTestCase(TestCase, LoaderModuleMockMixin): } ] highstate.returner(ret) - with salt.utils.fopen(self.output_file) as fh_: + with salt.utils.files.fopen(self.output_file) as fh_: self.assertEqual(json.load(fh_), expected) From fe2d46dd0c6bc1fcc701886e2a30d24a8a6f414a Mon Sep 17 00:00:00 2001 From: rallytime Date: Fri, 16 Mar 2018 12:38:29 -0400 Subject: [PATCH 071/117] Better merge conflict resolution for setup.py windows changes --- setup.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/setup.py b/setup.py index 868c641b43..d5fb3eb384 100755 --- a/setup.py +++ b/setup.py @@ -397,6 +397,30 @@ class InstallPyWin32Wheel(Command): call_subprocess(call_arguments) +class InstallM2CryptoWindows(Command): + + description = 'Install M2CryptoWindows' + + def initialize_options(self): + pass + + def finalize_options(self): + pass + + def run(self): + if getattr(self.distribution, 'salt_installing_m2crypto_windows', None) is None: + print('This command is not meant to be called on it\'s own') + exit(1) + import platform + from pip.utils import call_subprocess + from pip.utils.logging import indent_log + platform_bits, _ = platform.architecture() + with indent_log(): + call_subprocess( + ['pip', 'install', '--egg', 'M2CryptoWin{0}'.format(platform_bits[:2])] + ) + + def uri_to_resource(resource_file): # ## Returns the URI for a resource # The basic case is that the resource is on saltstack.com From 352eae3ffe7741d4daf56803baedbf3243d05ea3 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 16 Mar 2018 13:27:12 -0600 Subject: [PATCH 072/117] Add configuration options to docs and release notes Remove __proxyenabled__ from the two new grains files. --- doc/ref/configuration/minion.rst | 29 +++++++++++++++++++++++++++++ doc/topics/releases/2018.3.0.rst | 2 ++ salt/grains/fibre_channel.py | 1 - salt/grains/iscsi.py | 1 - 4 files changed, 31 insertions(+), 2 deletions(-) diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 88a57d1b41..6641cd63bb 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -786,6 +786,35 @@ A value of 10 minutes is a reasonable default. grains_refresh_every: 0 +.. conf_minion:: fibre_channel_grains + +``fibre_channel_grains`` +------------------------ + +Default: ``False`` + +The ``fibre_channel_grains`` setting will enable the ``fc_wwn`` grain for +Fibre Channel WWN's on the minion. Since this grain is expensive is +disabled by default. + +.. code-block:: yaml + + fibre_channel_grains: True + +.. conf_minion:: iscsi_grains + +``iscsi_grains`` +------------------------ + +Default: ``False`` + +The ``iscsi_grains`` setting will enable the ``iscsi_iqn`` grain on the +minion. Since this grain is expensive is disabled by default. + +.. code-block:: yaml + + iscsi_grains: True + .. conf_minion:: mine_enabled ``mine_enabled`` diff --git a/doc/topics/releases/2018.3.0.rst b/doc/topics/releases/2018.3.0.rst index 74525f7bf9..7fb3a1babf 100644 --- a/doc/topics/releases/2018.3.0.rst +++ b/doc/topics/releases/2018.3.0.rst @@ -521,6 +521,8 @@ In addition to the ``mapping`` and ``port`` options, the following additional op match a given Master. If set to ``any`` (the default), then any match to a key/value mapping will constitute a match. - ``pause`` - The interval in seconds between attempts (default: 5). +- ``fibre_channel_grains`` - Enables the ``fc_wwn`` grain. (Default: False) +- ``iscsi_grains`` - Enables the ``iscsi_iqn`` grain. (Default: False) Connection to a type instead of DNS =================================== diff --git a/salt/grains/fibre_channel.py b/salt/grains/fibre_channel.py index a189feefdf..5396bbde7a 100644 --- a/salt/grains/fibre_channel.py +++ b/salt/grains/fibre_channel.py @@ -22,7 +22,6 @@ import salt.modules.cmdmod import salt.utils.platform import salt.utils.files -__proxyenabled__ = ['fibre_channel'] __virtualname__ = 'fibre_channel' # Get logging started diff --git a/salt/grains/iscsi.py b/salt/grains/iscsi.py index 18aefbe36c..80d239f2bd 100644 --- a/salt/grains/iscsi.py +++ b/salt/grains/iscsi.py @@ -22,7 +22,6 @@ import salt.utils.files import salt.utils.path import salt.utils.platform -__proxyenabled__ = ['iscsi'] __virtualname__ = 'iscsi' # Get logging started From 771e21e28b71153143a325be0ecd76aa035f5250 Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Fri, 16 Mar 2018 18:58:45 -0400 Subject: [PATCH 073/117] Preserve tuples when decoding grain data in loader --- salt/loader.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/loader.py b/salt/loader.py index 33f42ddff6..0aee0be751 100644 --- a/salt/loader.py +++ b/salt/loader.py @@ -652,7 +652,7 @@ def _load_cached_grains(opts, cfn): try: serial = salt.payload.Serial(opts) with salt.utils.files.fopen(cfn, 'rb') as fp_: - cached_grains = salt.utils.data.decode(serial.load(fp_)) + cached_grains = salt.utils.data.decode(serial.load(fp_), preserve_tuples=True) if not cached_grains: log.debug('Cached grains are empty, cache might be corrupted. Refreshing.') return None @@ -820,7 +820,7 @@ def grains(opts, force_refresh=False, proxy=None): salt.utils.dictupdate.update(grains_data, opts['grains']) else: grains_data.update(opts['grains']) - return salt.utils.data.decode(grains_data) + return salt.utils.data.decode(grains_data, preserve_tuples=True) # TODO: get rid of? Does anyone use this? You should use raw() instead From 93ab582230f1578b2965a9cefe0de2ec7fe1da92 Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Mon, 19 Mar 2018 07:59:41 -0400 Subject: [PATCH 074/117] Small grammar fix to new grains settings --- doc/ref/configuration/minion.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index 6641cd63bb..aa6c9cd1e4 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -794,7 +794,7 @@ A value of 10 minutes is a reasonable default. Default: ``False`` The ``fibre_channel_grains`` setting will enable the ``fc_wwn`` grain for -Fibre Channel WWN's on the minion. Since this grain is expensive is +Fibre Channel WWN's on the minion. Since this grain is expensive, it is disabled by default. .. code-block:: yaml @@ -809,7 +809,7 @@ disabled by default. Default: ``False`` The ``iscsi_grains`` setting will enable the ``iscsi_iqn`` grain on the -minion. Since this grain is expensive is disabled by default. +minion. Since this grain is expensivem it is disabled by default. .. code-block:: yaml From 20ed81f009f2a3fe10203fa8a4f52b0070c16657 Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Mon, 19 Mar 2018 08:00:15 -0400 Subject: [PATCH 075/117] Fix typo --- doc/ref/configuration/minion.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/ref/configuration/minion.rst b/doc/ref/configuration/minion.rst index aa6c9cd1e4..9683a0a20a 100644 --- a/doc/ref/configuration/minion.rst +++ b/doc/ref/configuration/minion.rst @@ -809,7 +809,7 @@ disabled by default. Default: ``False`` The ``iscsi_grains`` setting will enable the ``iscsi_iqn`` grain on the -minion. Since this grain is expensivem it is disabled by default. +minion. Since this grain is expensive, it is disabled by default. .. code-block:: yaml From 6eab6a7dc4ee59d7e32eb4c4eaf007156f063f6c Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Mon, 19 Mar 2018 11:06:37 -0400 Subject: [PATCH 076/117] add autodoc topics for infoblox state modules --- doc/ref/states/all/index.rst | 4 ++++ doc/ref/states/all/salt.states.infoblox_a.rst | 6 ++++++ doc/ref/states/all/salt.states.infoblox_cname.rst | 6 ++++++ doc/ref/states/all/salt.states.infoblox_host_record.rst | 6 ++++++ doc/ref/states/all/salt.states.infoblox_range.rst | 6 ++++++ 5 files changed, 28 insertions(+) create mode 100644 doc/ref/states/all/salt.states.infoblox_a.rst create mode 100644 doc/ref/states/all/salt.states.infoblox_cname.rst create mode 100644 doc/ref/states/all/salt.states.infoblox_host_record.rst create mode 100644 doc/ref/states/all/salt.states.infoblox_range.rst diff --git a/doc/ref/states/all/index.rst b/doc/ref/states/all/index.rst index b79d6068d7..81559d11c0 100644 --- a/doc/ref/states/all/index.rst +++ b/doc/ref/states/all/index.rst @@ -124,6 +124,10 @@ state modules influxdb_retention_policy influxdb_user infoblox + infoblox_a + infoblox_cname + infoblox_host_record + infoblox_range ini_manage ipmi ipset diff --git a/doc/ref/states/all/salt.states.infoblox_a.rst b/doc/ref/states/all/salt.states.infoblox_a.rst new file mode 100644 index 0000000000..b3f53941ae --- /dev/null +++ b/doc/ref/states/all/salt.states.infoblox_a.rst @@ -0,0 +1,6 @@ +salt.states.infoblox_a module +=========================== + +.. automodule:: salt.states.infoblox_a + :members: + :undoc-members: diff --git a/doc/ref/states/all/salt.states.infoblox_cname.rst b/doc/ref/states/all/salt.states.infoblox_cname.rst new file mode 100644 index 0000000000..c2fbbed0c4 --- /dev/null +++ b/doc/ref/states/all/salt.states.infoblox_cname.rst @@ -0,0 +1,6 @@ +salt.states.infoblox_cname module +=========================== + +.. automodule:: salt.states.infoblox_cname + :members: + :undoc-members: diff --git a/doc/ref/states/all/salt.states.infoblox_host_record.rst b/doc/ref/states/all/salt.states.infoblox_host_record.rst new file mode 100644 index 0000000000..b8a550b261 --- /dev/null +++ b/doc/ref/states/all/salt.states.infoblox_host_record.rst @@ -0,0 +1,6 @@ +salt.states.infoblox_host_record module +=========================== + +.. automodule:: salt.states.infoblox_host_record + :members: + :undoc-members: diff --git a/doc/ref/states/all/salt.states.infoblox_range.rst b/doc/ref/states/all/salt.states.infoblox_range.rst new file mode 100644 index 0000000000..9cdccdee9d --- /dev/null +++ b/doc/ref/states/all/salt.states.infoblox_range.rst @@ -0,0 +1,6 @@ +salt.states.infoblox_range module +=========================== + +.. automodule:: salt.states.infoblox_range + :members: + :undoc-members: From da9a9b6e37aed98d637fae9d0286617d895ccef7 Mon Sep 17 00:00:00 2001 From: "Gareth J. Greenaway" Date: Mon, 19 Mar 2018 09:23:22 -0700 Subject: [PATCH 077/117] Fixing a bug that would result in constant changes using the schedule state module when a job was added with run_on_start. Adding an eval test to test run_on_start functionality. --- salt/modules/schedule.py | 2 +- salt/utils/schedule.py | 2 +- tests/integration/scheduler/test_eval.py | 30 ++++++++++++++++++++++++ 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/salt/modules/schedule.py b/salt/modules/schedule.py index a77693e66f..0491ee791c 100644 --- a/salt/modules/schedule.py +++ b/salt/modules/schedule.py @@ -58,7 +58,7 @@ SCHEDULE_CONF = [ 'after', 'return_config', 'return_kwargs', - 'run_on_start' + 'run_on_start', 'skip_during_range', 'run_after_skip_range', ] diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 278b93b345..de057477a3 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -1234,7 +1234,7 @@ class Schedule(object): # If there is no job specific skip_during_range available, # grab the global which defaults to None. - if 'skip_during_range' not in data: + if 'skip_during_range' not in data and self.skip_during_range: data['skip_during_range'] = self.skip_during_range if 'skip_during_range' in data and data['skip_during_range']: diff --git a/tests/integration/scheduler/test_eval.py b/tests/integration/scheduler/test_eval.py index 7d24fb1340..16079b944d 100644 --- a/tests/integration/scheduler/test_eval.py +++ b/tests/integration/scheduler/test_eval.py @@ -368,3 +368,33 @@ class SchedulerEvalTest(ModuleCase, SaltReturnAssertsMixin): self.schedule.eval(now=run_time) ret = self.schedule.job_status('job1') self.assertEqual(ret['_last_run'], run_time) + + def test_eval_run_on_start(self): + ''' + verify that scheduled job is run when minion starts + ''' + job = { + 'schedule': { + 'job1': { + 'function': 'test.ping', + 'hours': '1', + 'run_on_start': True + } + } + } + + # Add job to schedule + self.schedule.opts.update(job) + + # eval at 2:00pm, will run. + run_time = dateutil_parser.parse('11/29/2017 2:00pm') + self.schedule.eval(now=run_time) + ret = self.schedule.job_status('job1') + self.assertEqual(ret['_last_run'], run_time) + + # eval at 3:00pm, will run. + run_time = dateutil_parser.parse('11/29/2017 3:00pm') + self.schedule.eval(now=run_time) + ret = self.schedule.job_status('job1') + self.assertEqual(ret['_last_run'], run_time) + From c4712135a6426c93d708c9bbb13b05a2211c4b32 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Sat, 17 Mar 2018 15:52:51 -0500 Subject: [PATCH 078/117] Revert PR 41278 This PR should not have been merged, as there are very good reasons for keeping the mongodb_database and mongodb_user state modules separate. Additionally, role management was already supported at the time the PR was merged. Since this PR has not been in an official release of Salt, I'm reverting it now to prevent any confusion. --- salt/modules/mongodb.py | 20 +- salt/states/mongodb.py | 435 -------------------------------- salt/states/mongodb_database.py | 37 ++- salt/states/mongodb_user.py | 31 +-- 4 files changed, 30 insertions(+), 493 deletions(-) delete mode 100644 salt/states/mongodb.py diff --git a/salt/modules/mongodb.py b/salt/modules/mongodb.py index a752159961..bf330eb75d 100644 --- a/salt/modules/mongodb.py +++ b/salt/modules/mongodb.py @@ -90,7 +90,7 @@ def _to_dict(objects): def db_list(user=None, password=None, host=None, port=None, authdb=None): ''' - List all Mongodb databases + List all MongoDB databases CLI Example: @@ -112,7 +112,7 @@ def db_list(user=None, password=None, host=None, port=None, authdb=None): def db_exists(name, user=None, password=None, host=None, port=None, authdb=None): ''' - Checks if a database exists in Mongodb + Checks if a database exists in MongoDB CLI Example: @@ -130,7 +130,7 @@ def db_exists(name, user=None, password=None, host=None, port=None, authdb=None) def db_remove(name, user=None, password=None, host=None, port=None, authdb=None): ''' - Remove a Mongodb database + Remove a MongoDB database CLI Example: @@ -207,7 +207,7 @@ def user_find(name, user=None, password=None, host=None, port=None, def user_list(user=None, password=None, host=None, port=None, database='admin', authdb=None): ''' - List users of a Mongodb database + List users of a MongoDB database CLI Example: @@ -248,7 +248,7 @@ def user_list(user=None, password=None, host=None, port=None, database='admin', def user_exists(name, user=None, password=None, host=None, port=None, database='admin', authdb=None): ''' - Checks if a user exists in Mongodb + Checks if a user exists in MongoDB CLI Example: @@ -271,7 +271,7 @@ def user_exists(name, user=None, password=None, host=None, port=None, def user_create(name, passwd, user=None, password=None, host=None, port=None, database='admin', authdb=None, roles=None): ''' - Create a Mongodb user + Create a MongoDB user CLI Example: @@ -299,7 +299,7 @@ def user_create(name, passwd, user=None, password=None, host=None, port=None, def user_remove(name, user=None, password=None, host=None, port=None, database='admin', authdb=None): ''' - Remove a Mongodb user + Remove a MongoDB user CLI Example: @@ -325,7 +325,7 @@ def user_remove(name, user=None, password=None, host=None, port=None, def user_roles_exists(name, roles, database, user=None, password=None, host=None, port=None, authdb=None): ''' - Checks if a user of a Mongodb database has specified roles + Checks if a user of a MongoDB database has specified roles CLI Examples: @@ -363,7 +363,7 @@ def user_roles_exists(name, roles, database, user=None, password=None, host=None def user_grant_roles(name, roles, database, user=None, password=None, host=None, port=None, authdb=None): ''' - Grant one or many roles to a Mongodb user + Grant one or many roles to a MongoDB user CLI Examples: @@ -398,7 +398,7 @@ def user_grant_roles(name, roles, database, user=None, password=None, host=None, def user_revoke_roles(name, roles, database, user=None, password=None, host=None, port=None, authdb=None): ''' - Revoke one or many roles to a Mongodb user + Revoke one or many roles to a MongoDB user CLI Examples: diff --git a/salt/states/mongodb.py b/salt/states/mongodb.py deleted file mode 100644 index 1440d6d93a..0000000000 --- a/salt/states/mongodb.py +++ /dev/null @@ -1,435 +0,0 @@ -# -*- coding: utf-8 -*- -''' -Management of Mongodb users and databases -========================================= - -.. note:: - This module requires PyMongo to be installed. -''' - -# Import Python libs -from __future__ import absolute_import, print_function, unicode_literals - -# Define the module's virtual name -__virtualname__ = 'mongodb' - - -def __virtual__(): - if 'mongodb.user_exists' not in __salt__: - return False - return __virtualname__ - - -def database_absent(name, - user=None, - password=None, - host=None, - port=None, - authdb=None): - ''' - Ensure that the named database is absent. Note that creation doesn't make sense in MongoDB. - - name - The name of the database to remove - - user - The user to connect as (must be able to create the user) - - password - The password of the user - - host - The host to connect to - - port - The port to connect to - - authdb - The database in which to authenticate - ''' - ret = {'name': name, - 'changes': {}, - 'result': True, - 'comment': ''} - - #check if database exists and remove it - if __salt__['mongodb.db_exists'](name, user, password, host, port, authdb=authdb): - if __opts__['test']: - ret['result'] = None - ret['comment'] = ('Database {0} is present and needs to be removed' - ).format(name) - return ret - if __salt__['mongodb.db_remove'](name, user, password, host, port, authdb=authdb): - ret['comment'] = 'Database {0} has been removed'.format(name) - ret['changes'][name] = 'Absent' - return ret - - # fallback - ret['comment'] = ('User {0} is not present, so it cannot be removed' - ).format(name) - return ret - - -def user_present(name, - passwd, - database="admin", - user=None, - password=None, - host="localhost", - port=27017, - authdb=None): - ''' - Ensure that the user is present with the specified properties - - name - The name of the user to manage - - passwd - The password of the user to manage - - user - MongoDB user with sufficient privilege to create the user - - password - Password for the admin user specified with the ``user`` parameter - - host - The hostname/IP address of the MongoDB server - - port - The port on which MongoDB is listening - - database - The database in which to create the user - - .. note:: - If the database doesn't exist, it will be created. - - authdb - The database in which to authenticate - - Example: - - .. code-block:: yaml - - mongouser-myapp: - mongodb.user_present: - - name: myapp - - passwd: password-of-myapp - # Connect as admin:sekrit - - user: admin - - password: sekrit - - ''' - ret = {'name': name, - 'changes': {}, - 'result': True, - 'comment': 'User {0} is already present'.format(name)} - - # Check for valid port - try: - port = int(port) - except TypeError: - ret['result'] = False - ret['comment'] = 'Port ({0}) is not an integer.'.format(port) - return ret - - # check if user exists - user_exists = __salt__['mongodb.user_exists'](name, user, password, host, port, database, authdb) - if user_exists is True: - return ret - - # if the check does not return a boolean, return an error - # this may be the case if there is a database connection error - if not isinstance(user_exists, bool): - ret['comment'] = user_exists - ret['result'] = False - return ret - - if __opts__['test']: - ret['result'] = None - ret['comment'] = ('User {0} is not present and needs to be created' - ).format(name) - return ret - # The user is not present, make it! - if __salt__['mongodb.user_create'](name, passwd, user, password, host, port, database=database, authdb=authdb): - ret['comment'] = 'User {0} has been created'.format(name) - ret['changes'][name] = 'Present' - else: - ret['comment'] = 'Failed to create database {0}'.format(name) - ret['result'] = False - - return ret - - -def user_absent(name, - user=None, - password=None, - host=None, - port=None, - database="admin", - authdb=None): - ''' - Ensure that the named user is absent - - name - The name of the user to remove - - user - MongoDB user with sufficient privilege to create the user - - password - Password for the admin user specified by the ``user`` parameter - - host - The hostname/IP address of the MongoDB server - - port - The port on which MongoDB is listening - - database - The database from which to remove the user specified by the ``name`` - parameter - - authdb - The database in which to authenticate - ''' - ret = {'name': name, - 'changes': {}, - 'result': True, - 'comment': ''} - - #check if user exists and remove it - user_exists = __salt__['mongodb.user_exists'](name, user, password, host, port, database=database, authdb=authdb) - if user_exists is True: - if __opts__['test']: - ret['result'] = None - ret['comment'] = ('User {0} is present and needs to be removed' - ).format(name) - return ret - if __salt__['mongodb.user_remove'](name, user, password, host, port, database=database, authdb=authdb): - ret['comment'] = 'User {0} has been removed'.format(name) - ret['changes'][name] = 'Absent' - return ret - - # if the check does not return a boolean, return an error - # this may be the case if there is a database connection error - if not isinstance(user_exists, bool): - ret['comment'] = user_exists - ret['result'] = False - return ret - - # fallback - ret['comment'] = ('User {0} is not present, so it cannot be removed' - ).format(name) - return ret - - -def _roles_to_set(roles, database): - ret = set() - for r in roles: - if isinstance(r, dict): - if r['db'] == database: - ret.add(r['role']) - else: - ret.add(r) - return ret - - -def _user_roles_to_set(user_list, name, database): - ret = set() - - for item in user_list: - if item['user'] == name: - ret = ret.union(_roles_to_set(item['roles'], database)) - return ret - - -def user_grant_roles(name, roles, - database="admin", - user=None, - password=None, - host="localhost", - port=27017, - authdb=None): - - ''' - Ensure that the named user is granted certain roles - - name - The name of the user to remove - - roles - The roles to grant to the user - - user - MongoDB user with sufficient privilege to create the user - - password - Password for the admin user specified by the ``user`` parameter - - host - The hostname/IP address of the MongoDB server - - port - The port on which MongoDB is listening - - database - The database from which to remove the user specified by the ``name`` - parameter - - authdb - The database in which to authenticate - ''' - - ret = {'name': name, - 'changes': {}, - 'result': False, - 'comment': ''} - - if not isinstance(roles, (list, tuple)): - roles = [roles] - - if not roles: - ret['result'] = True - ret['comment'] = "nothing to do (no roles given)" - return ret - - # Check for valid port - try: - port = int(port) - except TypeError: - ret['result'] = False - ret['comment'] = 'Port ({0}) is not an integer.'.format(port) - return ret - - # check if grant exists - user_roles_exists = __salt__['mongodb.user_roles_exists'](name, roles, database, - user=user, password=password, host=host, port=port, authdb=authdb) - if user_roles_exists is True: - ret['result'] = True - ret['comment'] = "Roles already assigned" - return ret - - user_list = __salt__['mongodb.user_list'](database=database, - user=user, password=password, host=host, port=port, authdb=authdb) - - user_set = _user_roles_to_set(user_list, name, database) - roles_set = _roles_to_set(roles, database) - diff = roles_set - user_set - - if __opts__['test']: - ret['result'] = None - ret['comment'] = "Would have modified roles (missing: {0})".format(diff) - return ret - - # The user is not present, make it! - if __salt__['mongodb.user_grant_roles'](name, roles, database, - user=user, password=password, host=host, port=port, authdb=authdb): - ret['comment'] = 'Granted roles to {0} on {1}'.format(name, database) - ret['changes'][name] = ['{0} granted'.format(i) for i in diff] - ret['result'] = True - else: - ret['comment'] = 'Failed to grant roles ({2}) to {0} on {1}'.format(name, database, diff) - - return ret - - -def user_set_roles(name, roles, - database="admin", - user=None, - password=None, - host="localhost", - port=27017, - authdb=None): - - ''' - Ensure that the named user has the given roles and no other roles - - name - The name of the user to remove - - roles - The roles the given user should have - - user - MongoDB user with sufficient privilege to create the user - - password - Password for the admin user specified by the ``user`` parameter - - host - The hostname/IP address of the MongoDB server - - port - The port on which MongoDB is listening - - database - The database from which to remove the user specified by the ``name`` - parameter - - authdb - The database in which to authenticate - ''' - - ret = {'name': name, - 'changes': {}, - 'result': False, - 'comment': ''} - - if not isinstance(roles, (list, tuple)): - roles = [roles] - - if not roles: - ret['result'] = True - ret['comment'] = "nothing to do (no roles given)" - return ret - - # Check for valid port - try: - port = int(port) - except TypeError: - ret['result'] = False - ret['comment'] = 'Port ({0}) is not an integer.'.format(port) - return ret - - user_list = __salt__['mongodb.user_list'](database=database, - user=user, password=password, host=host, port=port, authdb=authdb) - - user_set = _user_roles_to_set(user_list, name, database) - roles_set = _roles_to_set(roles, database) - to_grant = list(roles_set - user_set) - to_revoke = list(user_set - roles_set) - - if not to_grant and not to_revoke: - ret['result'] = True - ret['comment'] = "User {0} has the appropriate roles on {1}".format(name, database) - return ret - - if __opts__['test']: - lsg = ', '.join(to_grant) - lsr = ', '.join(to_revoke) - ret['result'] = None - ret['comment'] = "Would have modified roles (grant: {0}; revoke: {1})".format(lsg, lsr) - return ret - - ret['changes'][name] = changes = {} - - if to_grant: - if not __salt__['mongodb.user_grant_roles'](name, to_grant, database, - user=user, password=password, host=host, port=port, authdb=authdb): - ret['comment'] = "failed to grant some or all of {0} to {1} on {2}".format(to_grant, name, database) - return ret - else: - changes['granted'] = list(to_grant) - - if to_revoke: - if not __salt__['mongodb.user_revoke_roles'](name, to_revoke, database, - user=user, password=password, host=host, port=port, authdb=authdb): - ret['comment'] = "failed to revoke some or all of {0} to {1} on {2}".format(to_revoke, name, database) - return ret - else: - changes['revoked'] = list(to_revoke) - - ret['result'] = True - return ret diff --git a/salt/states/mongodb_database.py b/salt/states/mongodb_database.py index 08e3ea950e..579f188e4a 100644 --- a/salt/states/mongodb_database.py +++ b/salt/states/mongodb_database.py @@ -1,14 +1,23 @@ # -*- coding: utf-8 -*- ''' -Management of Mongodb databases +Management of MongoDB Databases +=============================== -Only deletion is supported, creation doesn't make sense -and can be done using mongodb_user.present +:depends: - pymongo Python module + +Only deletion is supported, creation doesn't make sense and can be done using +:py:func:`mongodb_user.present `. ''' - from __future__ import absolute_import, print_function, unicode_literals -import salt.utils.versions +# Define the module's virtual name +__virtualname__ = 'mongodb_database' + + +def __virtual__(): + if 'mongodb.db_exists' in __salt__: + return __virtualname__ + return False def absent(name, @@ -18,10 +27,8 @@ def absent(name, port=None, authdb=None): ''' - .. deprecated:: Fluorine - Use ``mongodb.database_absent`` instead - - Ensure that the named database is absent + Ensure that the named database is absent. Note that creation doesn't make + sense in MongoDB. name The name of the database to remove @@ -41,19 +48,11 @@ def absent(name, authdb The database in which to authenticate ''' - ret = {'name': name, 'changes': {}, 'result': True, 'comment': ''} - salt.utils.versions.warn_until( - 'Fluorine', - 'The \'mongodb_database.absent\' function has been deprecated and will be removed in Salt ' - '{version}. Please use \'mongodb.database_absent\' instead.' - ) - - #check if database exists and remove it if __salt__['mongodb.db_exists'](name, user, password, host, port, authdb=authdb): if __opts__['test']: ret['result'] = None @@ -65,7 +64,5 @@ def absent(name, ret['changes'][name] = 'Absent' return ret - # fallback - ret['comment'] = ('User {0} is not present, so it cannot be removed' - ).format(name) + ret['comment'] = 'Database {0} is not present'.format(name) return ret diff --git a/salt/states/mongodb_user.py b/salt/states/mongodb_user.py index e10b6b1873..42a86eea68 100644 --- a/salt/states/mongodb_user.py +++ b/salt/states/mongodb_user.py @@ -1,16 +1,12 @@ # -*- coding: utf-8 -*- ''' -Management of Mongodb users +Management of MongoDB Users =========================== -.. note:: - This module requires PyMongo to be installed. +:depends: - pymongo Python module ''' - from __future__ import absolute_import, print_function, unicode_literals -import salt.utils.versions - # Define the module's virtual name __virtualname__ = 'mongodb_user' @@ -31,9 +27,6 @@ def present(name, authdb=None, roles=None): ''' - .. deprecated:: Fluorine - Use ``mongodb.user_present`` instead - Ensure that the user is present with the specified properties name @@ -84,13 +77,6 @@ def present(name, - dbOwner ''' - - salt.utils.versions.warn_until( - 'Fluorine', - 'The \'mongodb_user.present\' function has been deprecated and will be removed in Salt ' - '{version}. Please use \'mongodb.user_present\' instead.' - ) - ret = {'name': name, 'changes': {}, 'result': True, @@ -167,9 +153,6 @@ def absent(name, database="admin", authdb=None): ''' - .. deprecated:: Fluorine - Use ``mongodb.user_absent`` instead - Ensure that the named user is absent name @@ -194,13 +177,6 @@ def absent(name, authdb The database in which to authenticate ''' - - salt.utils.versions.warn_until( - 'Fluorine', - 'The \'mongodb_user.absent\' function has been deprecated and will be removed in Salt ' - '{version}. Please use \'mongodb.user_absent\' instead.' - ) - ret = {'name': name, 'changes': {}, 'result': True, @@ -227,6 +203,5 @@ def absent(name, return ret # fallback - ret['comment'] = ('User {0} is not present, so it cannot be removed' - ).format(name) + ret['comment'] = 'User {0} is not present'.format(name) return ret From db304c3eff8e6cc7891ac4d6301072ee1a7197fe Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Mon, 19 Mar 2018 15:17:45 -0500 Subject: [PATCH 079/117] Update tests to reflect changes to state returns --- tests/unit/states/test_mongodb_database.py | 3 +-- tests/unit/states/test_mongodb_user.py | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/unit/states/test_mongodb_database.py b/tests/unit/states/test_mongodb_database.py index 7b306fbcc0..1f318aa44d 100644 --- a/tests/unit/states/test_mongodb_database.py +++ b/tests/unit/states/test_mongodb_database.py @@ -56,7 +56,6 @@ class MongodbDatabaseTestCase(TestCase, LoaderModuleMockMixin): 'changes': {'mydb': 'Absent'}}) self.assertDictEqual(mongodb_database.absent(name), ret) - comt = ('User {0} is not present, so it cannot be removed' - .format(name)) + comt = 'Database {0} is not present'.format(name) ret.update({'comment': comt, 'changes': {}}) self.assertDictEqual(mongodb_database.absent(name), ret) diff --git a/tests/unit/states/test_mongodb_user.py b/tests/unit/states/test_mongodb_user.py index 3610fc8f7a..a162b9fcea 100644 --- a/tests/unit/states/test_mongodb_user.py +++ b/tests/unit/states/test_mongodb_user.py @@ -98,7 +98,6 @@ class MongodbUserTestCase(TestCase, LoaderModuleMockMixin): 'changes': {name: 'Absent'}}) self.assertDictEqual(mongodb_user.absent(name), ret) - comt = ('User {0} is not present, so it cannot be removed' - .format(name)) + comt = 'User {0} is not present'.format(name) ret.update({'comment': comt, 'result': True, 'changes': {}}) self.assertDictEqual(mongodb_user.absent(name), ret) From 36a64ab2d8d72baf15ce502f57ca6215810a17fa Mon Sep 17 00:00:00 2001 From: Nicole Thomas Date: Mon, 19 Mar 2018 17:03:25 -0400 Subject: [PATCH 080/117] Lint: Remove extra blank line --- tests/integration/scheduler/test_eval.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/integration/scheduler/test_eval.py b/tests/integration/scheduler/test_eval.py index 16079b944d..76843665d8 100644 --- a/tests/integration/scheduler/test_eval.py +++ b/tests/integration/scheduler/test_eval.py @@ -397,4 +397,3 @@ class SchedulerEvalTest(ModuleCase, SaltReturnAssertsMixin): self.schedule.eval(now=run_time) ret = self.schedule.job_status('job1') self.assertEqual(ret['_last_run'], run_time) - From 46ba72fb1c86ab1b93a993d0b3efa03b0a065e41 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 20 Mar 2018 17:32:21 -0400 Subject: [PATCH 081/117] Fix pillar unit test failures: file_roots and pillar_roots environments should be lists --- tests/unit/test_pillar.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_pillar.py b/tests/unit/test_pillar.py index 77555446a8..ee23e3ee9f 100644 --- a/tests/unit/test_pillar.py +++ b/tests/unit/test_pillar.py @@ -302,13 +302,13 @@ class PillarTestCase(TestCase): 'renderer_blacklist': [], 'renderer_whitelist': [], 'state_top': '', - 'pillar_roots': {'__env__': '/srv/pillar/__env__', 'base': '/srv/pillar/base'}, - 'file_roots': {'base': '/srv/salt/base', 'dev': '/svr/salt/dev'}, + 'pillar_roots': {'__env__': ['/srv/pillar/__env__'], 'base': ['/srv/pillar/base']}, + 'file_roots': {'base': ['/srv/salt/base'], 'dev': ['/svr/salt/dev']}, 'extension_modules': '', } pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='dev') self.assertEqual(pillar.opts['file_roots'], - {'base': '/srv/pillar/base', 'dev': '/srv/pillar/__env__'}) + {'base': ['/srv/pillar/base'], 'dev': ['/srv/pillar/__env__']}) def test_ignored_dynamic_pillarenv(self): opts = { @@ -316,12 +316,12 @@ class PillarTestCase(TestCase): 'renderer_blacklist': [], 'renderer_whitelist': [], 'state_top': '', - 'pillar_roots': {'__env__': '/srv/pillar/__env__', 'base': '/srv/pillar/base'}, - 'file_roots': {'base': '/srv/salt/base', 'dev': '/svr/salt/dev'}, + 'pillar_roots': {'__env__': ['/srv/pillar/__env__'], 'base': ['/srv/pillar/base']}, + 'file_roots': {'base': ['/srv/salt/base'], 'dev': ['/svr/salt/dev']}, 'extension_modules': '', } pillar = salt.pillar.Pillar(opts, {}, 'mocked-minion', 'base', pillarenv='base') - self.assertEqual(pillar.opts['file_roots'], {'base': '/srv/pillar/base'}) + self.assertEqual(pillar.opts['file_roots'], {'base': ['/srv/pillar/base']}) def test_malformed_pillar_sls(self): with patch('salt.pillar.compile_template') as compile_template: From 641f1f22931c41b0ca5f67b6a93bd4da06f98f4f Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Tue, 20 Mar 2018 22:29:34 -0700 Subject: [PATCH 082/117] Add `fail-fast` option to test runner --- tests/runtests.py | 17 +++++++++++++---- tests/support/parser/__init__.py | 25 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 11 deletions(-) diff --git a/tests/runtests.py b/tests/runtests.py index c5a6033910..2329db44cd 100755 --- a/tests/runtests.py +++ b/tests/runtests.py @@ -550,7 +550,10 @@ class SaltTestsuiteParser(SaltCoverageTestingParser): Run an integration test suite ''' full_path = os.path.join(TEST_DIR, path) - return self.run_suite(full_path, display_name, suffix='test_*.py') + return self.run_suite( + full_path, display_name, suffix='test_*.py', + failfast=self.options.failfast, + ) def start_daemons_only(self): if not salt.utils.platform.is_windows(): @@ -729,12 +732,16 @@ class SaltTestsuiteParser(SaltCoverageTestingParser): results = self.run_suite(os.path.dirname(name), name, suffix=os.path.basename(name), + failfast=self.options.failfast, load_from_name=False) status.append(results) continue if name.startswith(('tests.unit.', 'unit.')): continue - results = self.run_suite('', name, suffix='test_*.py', load_from_name=True) + results = self.run_suite( + '', name, suffix='test_*.py', load_from_name=True, + failfast=self.options.failfast, + ) status.append(results) for suite in TEST_SUITES: if suite != 'unit' and getattr(self.options, suite): @@ -763,7 +770,8 @@ class SaltTestsuiteParser(SaltCoverageTestingParser): self.set_filehandle_limits('unit') results = self.run_suite( - os.path.join(TEST_DIR, 'unit'), 'Unit', suffix='test_*.py' + os.path.join(TEST_DIR, 'unit'), 'Unit', suffix='test_*.py', + failfast=self.options.failfast, ) status.append(results) # We executed ALL unittests, we can skip running unittests by name @@ -772,7 +780,8 @@ class SaltTestsuiteParser(SaltCoverageTestingParser): for name in named_unit_test: results = self.run_suite( - os.path.join(TEST_DIR, 'unit'), name, suffix='test_*.py', load_from_name=True + os.path.join(TEST_DIR, 'unit'), name, suffix='test_*.py', + load_from_name=True, failfast=self.options.failfast, ) status.append(results) return status diff --git a/tests/support/parser/__init__.py b/tests/support/parser/__init__.py index 188d2fea4e..a15b5f0e90 100644 --- a/tests/support/parser/__init__.py +++ b/tests/support/parser/__init__.py @@ -241,6 +241,14 @@ class SaltTestingParser(optparse.OptionParser): self.output_options_group = optparse.OptionGroup( self, 'Output Options' ) + self.output_options_group.add_option( + '-f', + '--fail-fast', + dest='failfast', + default=False, + action='store_true', + help='Stop on first failure' + ) self.output_options_group.add_option( '-v', '--verbose', @@ -476,7 +484,7 @@ class SaltTestingParser(optparse.OptionParser): shutil.rmtree(path) def run_suite(self, path, display_name, suffix='test_*.py', - load_from_name=False, additional_test_dirs=None): + load_from_name=False, additional_test_dirs=None, failfast=False): ''' Execute a unit test suite ''' @@ -508,22 +516,25 @@ class SaltTestingParser(optparse.OptionParser): runner = XMLTestRunner( stream=sys.stdout, output=self.xml_output_dir, - verbosity=self.options.verbosity + verbosity=self.options.verbosity, + failfast=failfast, ).run(tests) else: runner = TextTestRunner( stream=sys.stdout, - verbosity=self.options.verbosity).run(tests) + verbosity=self.options.verbosity, + failfast=failfast + ).run(tests) errors = [] skipped = [] failures = [] for testcase, reason in runner.errors: - errors.append(TestResult(testcase.id(), reason)) + errors.append(TestResult(testcase.id(), reason, failfast=failfast)) for testcase, reason in runner.skipped: - skipped.append(TestResult(testcase.id(), reason)) + skipped.append(TestResult(testcase.id(), reason, failfast=failfast)) for testcase, reason in runner.failures: - failures.append(TestResult(testcase.id(), reason)) + failures.append(TestResult(testcase.id(), reason, failfast=failfast)) self.testsuite_results.append( TestsuiteResult(header, errors, @@ -930,6 +941,6 @@ class SaltTestcaseParser(SaltTestingParser): width=self.options.output_columns) runner = TextTestRunner( - verbosity=self.options.verbosity).run(tests) + verbosity=self.options.verbosity, failfast=True).run(tests) self.testsuite_results.append((header, runner)) return runner.wasSuccessful() From 2f7660fe358f63f328325f7febd7a9a9b52d017b Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 21 Mar 2018 08:13:11 -0500 Subject: [PATCH 083/117] Use the correct path for nacl certificates in Windows --- salt/modules/nacl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/modules/nacl.py b/salt/modules/nacl.py index 1f8e27ff4e..5a5da2a095 100644 --- a/salt/modules/nacl.py +++ b/salt/modules/nacl.py @@ -185,9 +185,9 @@ def _get_config(**kwargs): config = { 'box_type': 'sealedbox', 'sk': None, - 'sk_file': os.path.join(__opts__['pki_dir'], 'master/nacl'), + 'sk_file': os.path.join(__opts__['pki_dir'], 'master', 'nacl'), 'pk': None, - 'pk_file': os.path.join(__opts__['pki_dir'], 'master/nacl.pub'), + 'pk_file': os.path.join(__opts__['pki_dir'], 'master', 'nacl.pub'), } config_key = '{0}.config'.format(__virtualname__) try: From 064bc83276bd8966beac08df543bfbd8d290e33e Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Wed, 21 Mar 2018 12:40:10 -0500 Subject: [PATCH 084/117] Add Unicode / Python 3 update to 2018.3.0 release notes --- doc/topics/releases/2018.3.0.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/topics/releases/2018.3.0.rst b/doc/topics/releases/2018.3.0.rst index 2a16022471..17b7c6407d 100644 --- a/doc/topics/releases/2018.3.0.rst +++ b/doc/topics/releases/2018.3.0.rst @@ -4,6 +4,14 @@ Salt 2018.3.0 Release Notes - Codename Oxygen ============================================= +Unicode/Python 3 Compatibility Improvements +------------------------------------------- + +Support for Unicode has been greatly improved, and many Python 3 compatibility +fixes have been made. We continue to work toward improving both, and welcome +any feedback. + + Lots of Docker Improvements --------------------------- From f11d58a8e977445f0b3d5aa403e356aab21e9d6e Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 21 Mar 2018 15:10:46 -0400 Subject: [PATCH 085/117] Mirror libnacl imports in test from the nacl module --- tests/integration/modules/test_nacl.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/integration/modules/test_nacl.py b/tests/integration/modules/test_nacl.py index 10a2c50572..dab624ee9a 100644 --- a/tests/integration/modules/test_nacl.py +++ b/tests/integration/modules/test_nacl.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- ''' -Tests for the salt-run command +Tests for the nacl execution module ''' # Import Python libs from __future__ import absolute_import, print_function, unicode_literals @@ -12,7 +12,8 @@ from tests.support.case import ModuleCase from tests.support.unit import skipIf try: - import libnacl # pylint: disable=unused-import + import libnacl.secret # pylint: disable=unused-import + import libnacl.sealed # pylint: disable=unused-import HAS_LIBNACL = True except ImportError: HAS_LIBNACL = False From 9fef8bc431b1336d5d116eeee9ba055e1cbb02cd Mon Sep 17 00:00:00 2001 From: Ch3LL Date: Wed, 21 Mar 2018 15:18:58 -0400 Subject: [PATCH 086/117] Mirror libnacl imports in test from the nacl runner --- tests/integration/runners/test_nacl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/integration/runners/test_nacl.py b/tests/integration/runners/test_nacl.py index 4c93da6211..a564ed03b2 100644 --- a/tests/integration/runners/test_nacl.py +++ b/tests/integration/runners/test_nacl.py @@ -10,7 +10,8 @@ from tests.support.case import ShellCase from tests.support.unit import skipIf try: - import libnacl # pylint: disable=unused-import + import libnacl.secret # pylint: disable=unused-import + import libnacl.sealed # pylint: disable=unused-import HAS_LIBNACL = True except ImportError: HAS_LIBNACL = False From 7a81bf7ff65e65415d632b879fb70af1009bda37 Mon Sep 17 00:00:00 2001 From: denza Date: Wed, 21 Mar 2018 22:38:44 +0100 Subject: [PATCH 087/117] ssh is required and password is optional --- salt/cloud/clouds/profitbricks.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/salt/cloud/clouds/profitbricks.py b/salt/cloud/clouds/profitbricks.py index 998dac647a..a9f720db34 100644 --- a/salt/cloud/clouds/profitbricks.py +++ b/salt/cloud/clouds/profitbricks.py @@ -1130,10 +1130,10 @@ def _get_system_volume(vm_): if 'image_password' in vm_: image_password = vm_['image_password'] volume.image_password = image_password - else: - # Retrieve list of SSH public keys - ssh_keys = get_public_keys(vm_) - volume.ssh_keys = ssh_keys + + # Retrieve list of SSH public keys + ssh_keys = get_public_keys(vm_) + volume.ssh_keys = ssh_keys if 'image_alias' in vm_.keys(): volume.image_alias = vm_['image_alias'] From 4cb0c3f22879add23b15cc55f81530a19aeab9d9 Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 21 Mar 2018 20:30:20 -0700 Subject: [PATCH 088/117] Failfast will be '-F' instead of '-f' Avoid potential confusion with '-f' for '--force' --- tests/support/parser/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/support/parser/__init__.py b/tests/support/parser/__init__.py index a15b5f0e90..d63066e776 100644 --- a/tests/support/parser/__init__.py +++ b/tests/support/parser/__init__.py @@ -242,7 +242,7 @@ class SaltTestingParser(optparse.OptionParser): self, 'Output Options' ) self.output_options_group.add_option( - '-f', + '-F', '--fail-fast', dest='failfast', default=False, @@ -941,6 +941,8 @@ class SaltTestcaseParser(SaltTestingParser): width=self.options.output_columns) runner = TextTestRunner( - verbosity=self.options.verbosity, failfast=True).run(tests) + verbosity=self.options.verbosity, + failfast=self.options.failfast, + ).run(tests) self.testsuite_results.append((header, runner)) return runner.wasSuccessful() From 0fc7989236c799f16b0a4d876f763b03af0ffe9a Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Thu, 22 Mar 2018 08:56:58 +0100 Subject: [PATCH 089/117] make it possible to use login, pull and push from module.run and detect errors when using state.apply module.run doing docker operations retcode is tracked to find out if the call was successful or not. --- salt/modules/dockermod.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/salt/modules/dockermod.py b/salt/modules/dockermod.py index 630e13a45e..5d1f83128a 100644 --- a/salt/modules/dockermod.py +++ b/salt/modules/dockermod.py @@ -1369,7 +1369,7 @@ def login(*registries): # information is added to the config.json, since docker-py isn't designed # to do so. registry_auth = __pillar__.get('docker-registries', {}) - ret = {} + ret = {'retcode': 0} errors = ret.setdefault('Errors', []) if not isinstance(registry_auth, dict): errors.append('\'docker-registries\' Pillar value must be a dictionary') @@ -1427,6 +1427,8 @@ def login(*registries): errors.append(login_cmd['stderr']) elif login_cmd['stdout']: errors.append(login_cmd['stdout']) + if errors: + ret['retcode'] = 1 return ret @@ -4505,7 +4507,7 @@ def pull(image, time_started = time.time() response = _client_wrapper('pull', image, **kwargs) - ret = {'Time_Elapsed': time.time() - time_started} + ret = {'Time_Elapsed': time.time() - time_started, 'retcode': 0} _clear_context() if not response: @@ -4538,6 +4540,7 @@ def pull(image, if errors: ret['Errors'] = errors + ret['retcode'] = 1 return ret @@ -4600,7 +4603,7 @@ def push(image, time_started = time.time() response = _client_wrapper('push', image, **kwargs) - ret = {'Time_Elapsed': time.time() - time_started} + ret = {'Time_Elapsed': time.time() - time_started, 'retcode': 0} _clear_context() if not response: @@ -4632,6 +4635,7 @@ def push(image, if errors: ret['Errors'] = errors + ret['retcode'] = 1 return ret @@ -4703,9 +4707,11 @@ def rmi(*names, **kwargs): _clear_context() ret = {'Layers': [x for x in pre_images if x not in images(all=True)], - 'Tags': [x for x in pre_tags if x not in list_tags()]} + 'Tags': [x for x in pre_tags if x not in list_tags()], + 'retcode': 0} if errors: ret['Errors'] = errors + ret['retcode'] = 1 return ret From af64632bf374c525464811552b73c5d0107b6fc0 Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Thu, 22 Mar 2018 08:57:16 +0100 Subject: [PATCH 090/117] add unit test for failed login --- tests/unit/modules/test_dockermod.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/unit/modules/test_dockermod.py b/tests/unit/modules/test_dockermod.py index 2ce58929b8..b32044b90c 100644 --- a/tests/unit/modules/test_dockermod.py +++ b/tests/unit/modules/test_dockermod.py @@ -64,6 +64,26 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): ''' docker_mod.__context__.pop('docker.client', None) + def test_failed_login(self): + ''' + Check that when docker.login failed a retcode other then 0 + is part of the return. + ''' + client = Mock() + get_client_mock = MagicMock(return_value=client) + ref_out = { + 'stdout': '', + 'stderr': 'login failed', + 'retcode': 1 + } + with patch.dict(docker_mod.__pillar__, {'docker-registries': {'portus.example.com:5000': + {'username': 'admin', 'password': 'linux12345', 'email': 'tux@example.com'}}}): + with patch.object(docker_mod, '_get_client', get_client_mock): + with patch.dict(docker_mod.__salt__, {'cmd.run_all': MagicMock(return_value=ref_out)}): + ret = docker_mod.login('portus.example.com:5000') + self.assertTrue('retcode' in ret) + self.assertTrue(ret['retcode'] > 0) + def test_ps_with_host_true(self): ''' Check that docker.ps called with host is ``True``, From 4be1a991c2042e273d81cc698c3c7afc7e42aecb Mon Sep 17 00:00:00 2001 From: Karol Dabkowski Date: Thu, 22 Mar 2018 12:13:19 +0100 Subject: [PATCH 091/117] Lowered name of available packages before comparing with local packages --- salt/modules/chocolatey.py | 1 + 1 file changed, 1 insertion(+) diff --git a/salt/modules/chocolatey.py b/salt/modules/chocolatey.py index fa33d3a76f..d08a5340a0 100644 --- a/salt/modules/chocolatey.py +++ b/salt/modules/chocolatey.py @@ -928,6 +928,7 @@ def version(name, check_remote=False, source=None, pre_versions=False): if check_remote: available = list_(narrow=name, pre_versions=pre_versions, source=source) + available = {k.lower(): v for k, v in available.items()} for pkg in packages: packages[pkg] = {'installed': installed[pkg], From 986c7bcdaebfd2c7b3132ac6a9fa797e66faec96 Mon Sep 17 00:00:00 2001 From: Erik Johnson Date: Thu, 22 Mar 2018 09:39:06 -0500 Subject: [PATCH 092/117] Rewrite unicode/py3 section --- doc/topics/releases/2018.3.0.rst | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/doc/topics/releases/2018.3.0.rst b/doc/topics/releases/2018.3.0.rst index 17b7c6407d..bd22bd2c3d 100644 --- a/doc/topics/releases/2018.3.0.rst +++ b/doc/topics/releases/2018.3.0.rst @@ -7,9 +7,17 @@ Salt 2018.3.0 Release Notes - Codename Oxygen Unicode/Python 3 Compatibility Improvements ------------------------------------------- -Support for Unicode has been greatly improved, and many Python 3 compatibility -fixes have been made. We continue to work toward improving both, and welcome -any feedback. +This release fixes a number of nagging issues with Unicode strings in Salt +under Python 2 (ex. ``'ascii' codec can't decode byte 0xd0``). For best +results, use a UTF-8 locale (such as by setting the ``LANG`` environment +variable to one which supports UTF-8. For example ``en_US.UTF-8``, +``de_DE.UTF-8``, ``ru_RU.UTF-8``, ``C.UTF-8``). + +Additionally, a number of Python 3 compatibility fixes have been made, many of +them having to do with file I/O and str/bytes mismatches. + +We continue to work toward improving both Unicode and Python 3 compatibility +and welcome any feedback. Lots of Docker Improvements From 3ceb63f607936ff4be4e53db5a9c08b8d30b74d1 Mon Sep 17 00:00:00 2001 From: Michael Calmer Date: Thu, 22 Mar 2018 16:31:46 +0100 Subject: [PATCH 093/117] fix checking test results --- tests/unit/modules/test_dockermod.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/modules/test_dockermod.py b/tests/unit/modules/test_dockermod.py index b32044b90c..d4183818a9 100644 --- a/tests/unit/modules/test_dockermod.py +++ b/tests/unit/modules/test_dockermod.py @@ -81,8 +81,8 @@ class DockerTestCase(TestCase, LoaderModuleMockMixin): with patch.object(docker_mod, '_get_client', get_client_mock): with patch.dict(docker_mod.__salt__, {'cmd.run_all': MagicMock(return_value=ref_out)}): ret = docker_mod.login('portus.example.com:5000') - self.assertTrue('retcode' in ret) - self.assertTrue(ret['retcode'] > 0) + self.assertIn('retcode', ret) + self.assertNotEqual(ret['retcode'], 0) def test_ps_with_host_true(self): ''' From 123a86947ca9c082e57f2848df274b921db6956f Mon Sep 17 00:00:00 2001 From: Karol Dabkowski Date: Thu, 22 Mar 2018 19:37:37 +0100 Subject: [PATCH 094/117] Chocolatey - Added lowering local packages for unifing both local and remote names to lowercase for comparison. --- salt/modules/chocolatey.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/modules/chocolatey.py b/salt/modules/chocolatey.py index d08a5340a0..d7345c2e64 100644 --- a/salt/modules/chocolatey.py +++ b/salt/modules/chocolatey.py @@ -919,7 +919,8 @@ def version(name, check_remote=False, source=None, pre_versions=False): salt "*" chocolatey.version check_remote=True ''' installed = list_(narrow=name, local_only=True) - + installed = {k.lower(): v for k, v in installed.items()} + packages = {} lower_name = name.lower() for pkg in installed: From f7112b19a2014d545b4d3dcd97d0d66e31543906 Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 15 Mar 2018 13:39:27 -0600 Subject: [PATCH 095/117] Submit #46527 agains 2018.3 --- salt/grains/core.py | 11 +- salt/modules/cmdmod.py | 32 +- salt/modules/reg.py | 398 ++--------------------- salt/states/reg.py | 62 ++-- salt/utils/win_reg.py | 716 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 797 insertions(+), 422 deletions(-) create mode 100644 salt/utils/win_reg.py diff --git a/salt/grains/core.py b/salt/grains/core.py index 308d02921d..8a732141d3 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -74,9 +74,8 @@ if salt.utils.platform.is_windows(): import wmi # pylint: disable=import-error import salt.utils.winapi import win32api - import salt.modules.reg + import salt.utils.win_reg HAS_WMI = True - __salt__['reg.read_value'] = salt.modules.reg.read_value except ImportError: log.exception( 'Unable to import Python wmi module, some core grains ' @@ -101,10 +100,10 @@ def _windows_cpudata(): grains['num_cpus'] = int(os.environ['NUMBER_OF_PROCESSORS']) except ValueError: grains['num_cpus'] = 1 - grains['cpu_model'] = __salt__['reg.read_value']( - "HKEY_LOCAL_MACHINE", - "HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", - "ProcessorNameString").get('vdata') + grains['cpu_model'] = salt.utils.win_reg.read_value( + hive="HKEY_LOCAL_MACHINE", + key="HARDWARE\\DESCRIPTION\\System\\CentralProcessor\\0", + vname="ProcessorNameString").get('vdata') return grains diff --git a/salt/modules/cmdmod.py b/salt/modules/cmdmod.py index 26815e4af8..9dddc29d61 100644 --- a/salt/modules/cmdmod.py +++ b/salt/modules/cmdmod.py @@ -12,7 +12,6 @@ import functools import glob import logging import os -import platform import shutil import subprocess import sys @@ -37,6 +36,7 @@ import salt.utils.timed_subprocess import salt.utils.user import salt.utils.versions import salt.utils.vt +import salt.utils.win_reg import salt.grains.extra from salt.ext import six from salt.exceptions import CommandExecutionError, TimedProcTimeoutError, \ @@ -2839,9 +2839,9 @@ def shell_info(shell, list_modules=False): # Ensure ret['installed'] always as a value of True, False or None (not sure) ret = {'installed': False} if salt.utils.platform.is_windows() and shell == 'powershell': - pw_keys = __salt__['reg.list_keys']( - 'HKEY_LOCAL_MACHINE', - 'Software\\Microsoft\\PowerShell') + pw_keys = salt.utils.win_reg.list_keys( + hive='HKEY_LOCAL_MACHINE', + key='Software\\Microsoft\\PowerShell') pw_keys.sort(key=int) if len(pw_keys) == 0: return { @@ -2850,16 +2850,16 @@ def shell_info(shell, list_modules=False): 'installed': False, } for reg_ver in pw_keys: - install_data = __salt__['reg.read_value']( - 'HKEY_LOCAL_MACHINE', - 'Software\\Microsoft\\PowerShell\\{0}'.format(reg_ver), - 'Install') + install_data = salt.utils.win_reg.read_value( + hive='HKEY_LOCAL_MACHINE', + key='Software\\Microsoft\\PowerShell\\{0}'.format(reg_ver), + vname='Install') if install_data.get('vtype') == 'REG_DWORD' and \ install_data.get('vdata') == 1: - details = __salt__['reg.list_values']( - 'HKEY_LOCAL_MACHINE', - 'Software\\Microsoft\\PowerShell\\{0}\\' - 'PowerShellEngine'.format(reg_ver)) + details = salt.utils.win_reg.list_values( + hive='HKEY_LOCAL_MACHINE', + key='Software\\Microsoft\\PowerShell\\{0}\\' + 'PowerShellEngine'.format(reg_ver)) # reset data, want the newest version details only as powershell # is backwards compatible @@ -3138,11 +3138,9 @@ def powershell(cmd, python_shell = True # Append PowerShell Object formatting - # ConvertTo-JSON is only available on Versions of Windows greater than - # `7.1.7600`. We have to use `platform.version` instead of `__grains__` here - # because this function is called by `salt/grains/core.py` before - # `__grains__` is populated - if salt.utils.versions.version_cmp(platform.version(), '7.1.7600') == 1: + # ConvertTo-JSON is only available on PowerShell 3.0 and later + psversion = shell_info('powershell')['psversion'] + if salt.utils.versions.version_cmp(psversion, '2.0') == 1: cmd += ' | ConvertTo-JSON' if depth is not None: cmd += ' -Depth {0}'.format(depth) diff --git a/salt/modules/reg.py b/salt/modules/reg.py index 8ec0878411..eafae0b952 100644 --- a/salt/modules/reg.py +++ b/salt/modules/reg.py @@ -28,26 +28,12 @@ Values/Entries are name/data pairs. There can be many values in a key. The from __future__ import absolute_import, print_function, unicode_literals # Import python libs -import sys import logging -from salt.ext.six.moves import range # pylint: disable=W0622,import-error - -# Import third party libs -try: - import win32api - import win32con - import pywintypes - HAS_WINDOWS_MODULES = True -except ImportError: - HAS_WINDOWS_MODULES = False # Import Salt libs import salt.utils.platform -import salt.utils.stringutils -import salt.utils.win_functions from salt.exceptions import CommandExecutionError -PY2 = sys.version_info[0] == 2 log = logging.getLogger(__name__) # Define the module's virtual name @@ -62,99 +48,13 @@ def __virtual__(): return (False, 'reg execution module failed to load: ' 'The module will only run on Windows systems') - if not HAS_WINDOWS_MODULES: + if not 'reg.read_value' in __utils__: return (False, 'reg execution module failed to load: ' - 'One of the following libraries did not load: ' - 'win32con, win32api, pywintypes') + 'The reg salt util is unavailable') return __virtualname__ -def _to_mbcs(vdata): - ''' - Converts unicode to to current users character encoding. Use this for values - returned by reg functions - ''' - return salt.utils.stringutils.to_unicode(vdata, 'mbcs') - - -def _to_unicode(vdata): - ''' - Converts from current users character encoding to unicode. Use this for - parameters being pass to reg functions - ''' - # None does not convert to Unicode - if vdata is None: - return None - return salt.utils.stringutils.to_unicode(vdata, 'utf-8') - - -class Registry(object): # pylint: disable=R0903 - ''' - Delay usage until this module is used - ''' - def __init__(self): - self.hkeys = { - 'HKEY_CURRENT_CONFIG': win32con.HKEY_CURRENT_CONFIG, - 'HKEY_CLASSES_ROOT': win32con.HKEY_CLASSES_ROOT, - 'HKEY_CURRENT_USER': win32con.HKEY_CURRENT_USER, - 'HKEY_LOCAL_MACHINE': win32con.HKEY_LOCAL_MACHINE, - 'HKEY_USERS': win32con.HKEY_USERS, - 'HKCC': win32con.HKEY_CURRENT_CONFIG, - 'HKCR': win32con.HKEY_CLASSES_ROOT, - 'HKCU': win32con.HKEY_CURRENT_USER, - 'HKLM': win32con.HKEY_LOCAL_MACHINE, - 'HKU': win32con.HKEY_USERS, - } - self.vtype = { - 'REG_BINARY': win32con.REG_BINARY, - 'REG_DWORD': win32con.REG_DWORD, - 'REG_EXPAND_SZ': win32con.REG_EXPAND_SZ, - 'REG_MULTI_SZ': win32con.REG_MULTI_SZ, - 'REG_SZ': win32con.REG_SZ, - 'REG_QWORD': win32con.REG_QWORD - } - self.opttype = { - 'REG_OPTION_NON_VOLATILE': 0, - 'REG_OPTION_VOLATILE': 1 - } - # Return Unicode due to from __future__ import unicode_literals - self.vtype_reverse = { - win32con.REG_BINARY: 'REG_BINARY', - win32con.REG_DWORD: 'REG_DWORD', - win32con.REG_EXPAND_SZ: 'REG_EXPAND_SZ', - win32con.REG_MULTI_SZ: 'REG_MULTI_SZ', - win32con.REG_SZ: 'REG_SZ', - win32con.REG_QWORD: 'REG_QWORD' - } - self.opttype_reverse = { - 0: 'REG_OPTION_NON_VOLATILE', - 1: 'REG_OPTION_VOLATILE' - } - # delete_key_recursive uses this to check the subkey contains enough \ - # as we do not want to remove all or most of the registry - self.subkey_slash_check = { - win32con.HKEY_CURRENT_USER: 0, - win32con.HKEY_LOCAL_MACHINE: 1, - win32con.HKEY_USERS: 1, - win32con.HKEY_CURRENT_CONFIG: 1, - win32con.HKEY_CLASSES_ROOT: 1 - } - - self.registry_32 = { - True: win32con.KEY_READ | win32con.KEY_WOW64_32KEY, - False: win32con.KEY_READ, - } - - def __getattr__(self, k): - try: - return self.hkeys[k] - except KeyError: - msg = 'No hkey named \'{0}. Try one of {1}\'' - hkeys = ', '.join(self.hkeys) - raise CommandExecutionError(msg.format(k, hkeys)) - - def key_exists(hive, key, use_32bit_registry=False): ''' Check that the key is found in the registry. This refers to keys and not @@ -167,23 +67,9 @@ def key_exists(hive, key, use_32bit_registry=False): :return: Returns True if found, False if not found :rtype: bool ''' - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - - registry = Registry() - hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] - - try: - handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - win32api.RegCloseKey(handle) - return True - except WindowsError: # pylint: disable=E0602 - return False - except pywintypes.error as exc: - if exc.winerror == 2: - return False - raise + return __utils__['reg.key_exists'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry) def broadcast_change(): @@ -228,32 +114,9 @@ def list_keys(hive, key=None, use_32bit_registry=False): salt '*' reg.list_keys HKLM 'SOFTWARE' ''' - - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - - registry = Registry() - hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] - - subkeys = [] - try: - handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - - for i in range(win32api.RegQueryInfoKey(handle)[0]): - subkey = win32api.RegEnumKey(handle, i) - if PY2: - subkeys.append(_to_mbcs(subkey)) - else: - subkeys.append(subkey) - - handle.Close() - - except pywintypes.error: # pylint: disable=E0602 - log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) - return False, r'Cannot find key: {0}\{1}'.format(hive, key) - - return subkeys + return __utils__['reg.list_keys'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry) def list_values(hive, key=None, use_32bit_registry=False, include_default=True): @@ -285,44 +148,10 @@ def list_values(hive, key=None, use_32bit_registry=False, include_default=True): salt '*' reg.list_values HKLM 'SYSTEM\\CurrentControlSet\\Services\\Tcpip' ''' - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - - registry = Registry() - hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] - handle = None - values = list() - - try: - handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - - for i in range(win32api.RegQueryInfoKey(handle)[1]): - vname, vdata, vtype = win32api.RegEnumValue(handle, i) - - if not vname: - vname = "(Default)" - - value = {'hive': local_hive, - 'key': local_key, - 'vname': _to_mbcs(vname), - 'vtype': registry.vtype_reverse[vtype], - 'success': True} - # Only convert text types to unicode - if vtype == win32con.REG_MULTI_SZ: - value['vdata'] = [_to_mbcs(i) for i in vdata] - elif vtype in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: - value['vdata'] = _to_mbcs(vdata) - else: - value['vdata'] = vdata - values.append(value) - except pywintypes.error as exc: # pylint: disable=E0602 - log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) - return False, r'Cannot find key: {0}\{1}'.format(hive, key) - finally: - if handle: - handle.Close() - return values + return __utils__['reg.list_values'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry, + include_default=include_default) def read_value(hive, key, vname=None, use_32bit_registry=False): @@ -362,60 +191,10 @@ def read_value(hive, key, vname=None, use_32bit_registry=False): salt '*' reg.read_value HKEY_LOCAL_MACHINE 'SOFTWARE\Salt' 'version' ''' - # If no name is passed, the default value of the key will be returned - # The value name is Default - - # Setup the return array - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - local_vname = _to_unicode(vname) - - ret = {'hive': local_hive, - 'key': local_key, - 'vname': local_vname, - 'vdata': None, - 'success': True} - - if not vname: - ret['vname'] = '(Default)' - - registry = Registry() - hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] - - try: - handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - try: - # RegQueryValueEx returns and accepts unicode data - vdata, vtype = win32api.RegQueryValueEx(handle, local_vname) - if vdata or vdata in [0, '']: - # Only convert text types to unicode - ret['vtype'] = registry.vtype_reverse[vtype] - if vtype == win32con.REG_MULTI_SZ: - ret['vdata'] = [_to_mbcs(i) for i in vdata] - elif vtype in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: - ret['vdata'] = _to_mbcs(vdata) - else: - ret['vdata'] = vdata - else: - ret['comment'] = 'Empty Value' - except WindowsError: # pylint: disable=E0602 - ret['vdata'] = ('(value not set)') - ret['vtype'] = 'REG_SZ' - except pywintypes.error as exc: # pylint: disable=E0602 - msg = 'Cannot find {0} in {1}\\{2}' \ - ''.format(local_vname, local_hive, local_key) - log.trace(exc) - log.trace(msg) - ret['comment'] = msg - ret['success'] = False - except pywintypes.error as exc: # pylint: disable=E0602 - msg = 'Cannot find key: {0}\\{1}'.format(local_hive, local_key) - log.trace(exc) - log.trace(msg) - ret['comment'] = msg - ret['success'] = False - return ret + return __utils__['reg.read_value'](hive=hive, + key=key, + vname=vname, + use_32bit_registry=use_32bit_registry) def set_value(hive, @@ -519,49 +298,13 @@ def set_value(hive, salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ vtype=REG_LIST vdata='[a,b,c]' ''' - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - local_vname = _to_unicode(vname) - local_vtype = _to_unicode(vtype) - - registry = Registry() - hkey = registry.hkeys[local_hive] - vtype_value = registry.vtype[local_vtype] - access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS - - # Check data type and cast to expected type - # int will automatically become long on 64bit numbers - # https://www.python.org/dev/peps/pep-0237/ - - # String Types to Unicode - if vtype_value in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: - local_vdata = _to_unicode(vdata) - # Don't touch binary... - elif vtype_value == win32con.REG_BINARY: - local_vdata = vdata - # Make sure REG_MULTI_SZ is a list of strings - elif vtype_value == win32con.REG_MULTI_SZ: - local_vdata = [_to_unicode(i) for i in vdata] - # Everything else is int - else: - local_vdata = int(vdata) - - if volatile: - create_options = registry.opttype['REG_OPTION_VOLATILE'] - else: - create_options = registry.opttype['REG_OPTION_NON_VOLATILE'] - - try: - handle, _ = win32api.RegCreateKeyEx(hkey, local_key, access_mask, - Options=create_options) - win32api.RegSetValueEx(handle, local_vname, 0, vtype_value, local_vdata) - win32api.RegFlushKey(handle) - win32api.RegCloseKey(handle) - broadcast_change() - return True - except (win32api.error, SystemError, ValueError, TypeError): # pylint: disable=E0602 - log.exception('Encountered error setting registry value') - return False + return __utils__['reg.set_value'](hive=hive, + key=key, + vname=vname, + vdata=vdata, + vtype=vtype, + use_32bit_registry=use_32bit_registry, + volatile=volatile) def delete_key_recursive(hive, key, use_32bit_registry=False): @@ -596,73 +339,9 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): salt '*' reg.delete_key_recursive HKLM SOFTWARE\\salt ''' - - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - - # Instantiate the registry object - registry = Registry() - hkey = registry.hkeys[local_hive] - key_path = local_key - access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS - - if not key_exists(local_hive, local_key, use_32bit_registry): - return False - - if (len(key) > 1) and (key.count('\\', 1) < registry.subkey_slash_check[hkey]): - log.error( - 'Hive:%s Key:%s; key is too close to root, not safe to remove', - hive, key - ) - return False - - # Functions for traversing the registry tree - def _subkeys(_key): - ''' - Enumerate keys - ''' - i = 0 - while True: - try: - subkey = win32api.RegEnumKey(_key, i) - yield subkey - i += 1 - except pywintypes.error: # pylint: disable=E0602 - break - - def _traverse_registry_tree(_hkey, _keypath, _ret, _access_mask): - ''' - Traverse the registry tree i.e. dive into the tree - ''' - _key = win32api.RegOpenKeyEx(_hkey, _keypath, 0, _access_mask) - for subkeyname in _subkeys(_key): - subkeypath = r'{0}\{1}'.format(_keypath, subkeyname) - _ret = _traverse_registry_tree(_hkey, subkeypath, _ret, access_mask) - _ret.append(subkeypath) - return _ret - - # Get a reverse list of registry keys to be deleted - key_list = [] - key_list = _traverse_registry_tree(hkey, key_path, key_list, access_mask) - # Add the top level key last, all subkeys must be deleted first - key_list.append(key_path) - - ret = {'Deleted': [], - 'Failed': []} - - # Delete all sub_keys - for sub_key_path in key_list: - try: - key_handle = win32api.RegOpenKeyEx(hkey, sub_key_path, 0, access_mask) - win32api.RegDeleteKey(key_handle, '') - ret['Deleted'].append(r'{0}\{1}'.format(hive, sub_key_path)) - except WindowsError as exc: # pylint: disable=E0602 - log.error(exc, exc_info=True) - ret['Failed'].append(r'{0}\{1} {2}'.format(hive, sub_key_path, exc)) - - broadcast_change() - - return ret + return __utils__['reg.delete_key_recursive'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry) def delete_value(hive, key, vname=None, use_32bit_registry=False): @@ -694,27 +373,10 @@ def delete_value(hive, key, vname=None, use_32bit_registry=False): salt '*' reg.delete_value HKEY_CURRENT_USER 'SOFTWARE\\Salt' 'version' ''' - local_hive = _to_unicode(hive) - local_key = _to_unicode(key) - local_vname = _to_unicode(vname) - - registry = Registry() - hkey = registry.hkeys[local_hive] - access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS - - try: - handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) - win32api.RegDeleteValue(handle, local_vname) - win32api.RegCloseKey(handle) - broadcast_change() - return True - except WindowsError as exc: # pylint: disable=E0602 - log.error(exc, exc_info=True) - log.error('Hive: %s', local_hive) - log.error('Key: %s', local_key) - log.error('ValueName: %s', local_vname) - log.error('32bit Reg: %s', use_32bit_registry) - return False + return __utils__['reg.delete_value'](hive=hive, + key=key, + vname=vname, + use_32bit_registry=use_32bit_registry) def import_file(source, use_32bit_registry=False): diff --git a/salt/states/reg.py b/salt/states/reg.py index d4ff1e74d3..8f584770cb 100644 --- a/salt/states/reg.py +++ b/salt/states/reg.py @@ -68,19 +68,19 @@ def __virtual__(): ''' Load this state if the reg module exists ''' - if 'reg.read_value' not in __salt__: + if 'reg.read_value' not in __utils__: return (False, 'reg state module failed to load: ' 'missing module function: reg.read_value') - if 'reg.set_value' not in __salt__: + if 'reg.set_value' not in __utils__: return (False, 'reg state module failed to load: ' 'missing module function: reg.set_value') - if 'reg.delete_value' not in __salt__: + if 'reg.delete_value' not in __utils__: return (False, 'reg state module failed to load: ' 'missing module function: reg.delete_value') - if 'reg.delete_key_recursive' not in __salt__: + if 'reg.delete_key_recursive' not in __utils__: return (False, 'reg state module failed to load: ' 'missing module function: reg.delete_key_recursive') @@ -181,10 +181,10 @@ def present(name, hive, key = _parse_key(name) # Determine what to do - reg_current = __salt__['reg.read_value'](hive=hive, - key=key, - vname=vname, - use_32bit_registry=use_32bit_registry) + reg_current = __utils__['reg.read_value'](hive=hive, + key=key, + vname=vname, + use_32bit_registry=use_32bit_registry) if vdata == reg_current['vdata'] and reg_current['success']: ret['comment'] = '{0} in {1} is already configured' \ @@ -208,12 +208,12 @@ def present(name, return ret # Configure the value - ret['result'] = __salt__['reg.set_value'](hive=hive, - key=key, - vname=vname, - vdata=vdata, - vtype=vtype, - use_32bit_registry=use_32bit_registry) + ret['result'] = __utils__['reg.set_value'](hive=hive, + key=key, + vname=vname, + vdata=vdata, + vtype=vtype, + use_32bit_registry=use_32bit_registry) if not ret['result']: ret['changes'] = {} @@ -271,10 +271,10 @@ def absent(name, vname=None, use_32bit_registry=False): hive, key = _parse_key(name) # Determine what to do - reg_check = __salt__['reg.read_value'](hive=hive, - key=key, - vname=vname, - use_32bit_registry=use_32bit_registry) + reg_check = __utils__['reg.read_value'](hive=hive, + key=key, + vname=vname, + use_32bit_registry=use_32bit_registry) if not reg_check['success'] or reg_check['vdata'] == '(value not set)': ret['comment'] = '{0} is already absent'.format(name) return ret @@ -289,10 +289,10 @@ def absent(name, vname=None, use_32bit_registry=False): return ret # Delete the value - ret['result'] = __salt__['reg.delete_value'](hive=hive, - key=key, - vname=vname, - use_32bit_registry=use_32bit_registry) + ret['result'] = __utils__['reg.delete_value'](hive=hive, + key=key, + vname=vname, + use_32bit_registry=use_32bit_registry) if not ret['result']: ret['changes'] = {} ret['comment'] = r'Failed to remove {0} from {1}'.format(key, hive) @@ -349,9 +349,9 @@ def key_absent(name, use_32bit_registry=False): hive, key = _parse_key(name) # Determine what to do - if not __salt__['reg.read_value'](hive=hive, - key=key, - use_32bit_registry=use_32bit_registry)['success']: + if not __utils__['reg.read_value'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry)['success']: ret['comment'] = '{0} is already absent'.format(name) return ret @@ -366,12 +366,12 @@ def key_absent(name, use_32bit_registry=False): return ret # Delete the value - __salt__['reg.delete_key_recursive'](hive=hive, - key=key, - use_32bit_registry=use_32bit_registry) - if __salt__['reg.read_value'](hive=hive, - key=key, - use_32bit_registry=use_32bit_registry)['success']: + __utils__['reg.delete_key_recursive'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry) + if __utils__['reg.read_value'](hive=hive, + key=key, + use_32bit_registry=use_32bit_registry)['success']: ret['result'] = False ret['changes'] = {} ret['comment'] = 'Failed to remove registry key {0}'.format(name) diff --git a/salt/utils/win_reg.py b/salt/utils/win_reg.py new file mode 100644 index 0000000000..999a46e799 --- /dev/null +++ b/salt/utils/win_reg.py @@ -0,0 +1,716 @@ +# -*- coding: utf-8 -*- +''' +Manage the Windows registry + +----- +Hives +----- +Hives are the main sections of the registry and all begin with the word HKEY. +- HKEY_LOCAL_MACHINE +- HKEY_CURRENT_USER +- HKEY_USER + +---- +Keys +---- +Keys are the folders in the registry. Keys can have many nested subkeys. Keys +can have a value assigned to them under the (Default) + +----------------- +Values or Entries +----------------- +Values/Entries are name/data pairs. There can be many values in a key. The +(Default) value corresponds to the Key, the rest are their own value pairs. + +:depends: - PyWin32 +''' +# When production windows installer is using Python 3, Python 2 code can be removed +from __future__ import absolute_import, print_function, unicode_literals + +# Import python libs +import sys +import logging +from salt.ext.six.moves import range # pylint: disable=W0622,import-error + +# Import third party libs +try: + import win32gui + import win32api + import win32con + import pywintypes + HAS_WINDOWS_MODULES = True +except ImportError: + HAS_WINDOWS_MODULES = False + +# Import Salt libs +import salt.utils.platform +import salt.utils.stringutils +from salt.exceptions import CommandExecutionError + +PY2 = sys.version_info[0] == 2 +log = logging.getLogger(__name__) + +# Define the module's virtual name +__virtualname__ = 'reg' + + +def __virtual__(): + ''' + Only works on Windows systems with the PyWin32 + ''' + if not salt.utils.platform.is_windows(): + return (False, 'reg execution module failed to load: ' + 'The module will only run on Windows systems') + + if not HAS_WINDOWS_MODULES: + return (False, 'reg execution module failed to load: ' + 'One of the following libraries did not load: ' + 'win32gui, win32con, win32api') + + return __virtualname__ + + +def _to_mbcs(vdata): + ''' + Converts unicode to to current users character encoding. Use this for values + returned by reg functions + ''' + return salt.utils.stringutils.to_unicode(vdata, 'mbcs') + + +def _to_unicode(vdata): + ''' + Converts from current users character encoding to unicode. Use this for + parameters being pass to reg functions + ''' + # None does not convert to Unicode + if vdata is None: + return None + return salt.utils.stringutils.to_unicode(vdata, 'utf-8') + + +class Registry(object): # pylint: disable=R0903 + ''' + Delay usage until this module is used + ''' + def __init__(self): + self.hkeys = { + 'HKEY_CURRENT_CONFIG': win32con.HKEY_CURRENT_CONFIG, + 'HKEY_CLASSES_ROOT': win32con.HKEY_CLASSES_ROOT, + 'HKEY_CURRENT_USER': win32con.HKEY_CURRENT_USER, + 'HKEY_LOCAL_MACHINE': win32con.HKEY_LOCAL_MACHINE, + 'HKEY_USERS': win32con.HKEY_USERS, + 'HKCC': win32con.HKEY_CURRENT_CONFIG, + 'HKCR': win32con.HKEY_CLASSES_ROOT, + 'HKCU': win32con.HKEY_CURRENT_USER, + 'HKLM': win32con.HKEY_LOCAL_MACHINE, + 'HKU': win32con.HKEY_USERS, + } + self.vtype = { + 'REG_BINARY': win32con.REG_BINARY, + 'REG_DWORD': win32con.REG_DWORD, + 'REG_EXPAND_SZ': win32con.REG_EXPAND_SZ, + 'REG_MULTI_SZ': win32con.REG_MULTI_SZ, + 'REG_SZ': win32con.REG_SZ, + 'REG_QWORD': win32con.REG_QWORD + } + self.opttype = { + 'REG_OPTION_NON_VOLATILE': 0, + 'REG_OPTION_VOLATILE': 1 + } + # Return Unicode due to from __future__ import unicode_literals + self.vtype_reverse = { + win32con.REG_BINARY: 'REG_BINARY', + win32con.REG_DWORD: 'REG_DWORD', + win32con.REG_EXPAND_SZ: 'REG_EXPAND_SZ', + win32con.REG_MULTI_SZ: 'REG_MULTI_SZ', + win32con.REG_SZ: 'REG_SZ', + win32con.REG_QWORD: 'REG_QWORD' + } + self.opttype_reverse = { + 0: 'REG_OPTION_NON_VOLATILE', + 1: 'REG_OPTION_VOLATILE' + } + # delete_key_recursive uses this to check the subkey contains enough \ + # as we do not want to remove all or most of the registry + self.subkey_slash_check = { + win32con.HKEY_CURRENT_USER: 0, + win32con.HKEY_LOCAL_MACHINE: 1, + win32con.HKEY_USERS: 1, + win32con.HKEY_CURRENT_CONFIG: 1, + win32con.HKEY_CLASSES_ROOT: 1 + } + + self.registry_32 = { + True: win32con.KEY_READ | win32con.KEY_WOW64_32KEY, + False: win32con.KEY_READ, + } + + def __getattr__(self, k): + try: + return self.hkeys[k] + except KeyError: + msg = 'No hkey named \'{0}. Try one of {1}\'' + hkeys = ', '.join(self.hkeys) + raise CommandExecutionError(msg.format(k, hkeys)) + + +def key_exists(hive, key, use_32bit_registry=False): + ''' + Check that the key is found in the registry + + :param str hive: The hive to connect to. + :param str key: The key to check + :param bool use_32bit_registry: Look in the 32bit portion of the registry + + :return: Returns True if found, False if not found + :rtype: bool + ''' + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + + registry = Registry() + hkey = registry.hkeys[local_hive] + access_mask = registry.registry_32[use_32bit_registry] + + try: + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + win32api.RegCloseKey(handle) + return True + except WindowsError: # pylint: disable=E0602 + return False + + +def broadcast_change(): + ''' + Refresh the windows environment. + + Returns (bool): True if successful, otherwise False + + CLI Example: + + .. code-block:: bash + + salt '*' reg.broadcast_change + ''' + # https://msdn.microsoft.com/en-us/library/windows/desktop/ms644952(v=vs.85).aspx + _, res = win32gui.SendMessageTimeout( + win32con.HWND_BROADCAST, win32con.WM_SETTINGCHANGE, 0, 0, + win32con.SMTO_ABORTIFHUNG, 5000) + return not bool(res) + + +def list_keys(hive, key=None, use_32bit_registry=False): + ''' + Enumerates the subkeys in a registry key or hive. + + :param str hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param str key: The key (looks like a path) to the value name. If a key is + not passed, the keys under the hive will be returned. + + :param bool use_32bit_registry: Accesses the 32bit portion of the registry + on 64 bit installations. On 32bit machines this is ignored. + + :return: A list of keys/subkeys under the hive or key. + :rtype: list + + CLI Example: + + .. code-block:: bash + + salt '*' reg.list_keys HKLM 'SOFTWARE' + ''' + + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + + registry = Registry() + hkey = registry.hkeys[local_hive] + access_mask = registry.registry_32[use_32bit_registry] + + subkeys = [] + try: + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + + for i in range(win32api.RegQueryInfoKey(handle)[0]): + subkey = win32api.RegEnumKey(handle, i) + if PY2: + subkeys.append(_to_mbcs(subkey)) + else: + subkeys.append(subkey) + + handle.Close() + + except pywintypes.error: # pylint: disable=E0602 + log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) + return False, r'Cannot find key: {0}\{1}'.format(hive, key) + + return subkeys + + +def list_values(hive, key=None, use_32bit_registry=False, include_default=True): + ''' + Enumerates the values in a registry key or hive. + + :param str hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param str key: The key (looks like a path) to the value name. If a key is + not passed, the values under the hive will be returned. + + :param bool use_32bit_registry: Accesses the 32bit portion of the registry + on 64 bit installations. On 32bit machines this is ignored. + + :param bool include_default: Toggle whether to include the '(Default)' value. + + :return: A list of values under the hive or key. + :rtype: list + + CLI Example: + + .. code-block:: bash + + salt '*' reg.list_values HKLM 'SYSTEM\\CurrentControlSet\\Services\\Tcpip' + ''' + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + + registry = Registry() + hkey = registry.hkeys[local_hive] + access_mask = registry.registry_32[use_32bit_registry] + handle = None + values = list() + + try: + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + + for i in range(win32api.RegQueryInfoKey(handle)[1]): + vname, vdata, vtype = win32api.RegEnumValue(handle, i) + + if not vname: + vname = "(Default)" + + value = {'hive': local_hive, + 'key': local_key, + 'vname': _to_mbcs(vname), + 'vtype': registry.vtype_reverse[vtype], + 'success': True} + # Only convert text types to unicode + if vtype == win32con.REG_MULTI_SZ: + value['vdata'] = [_to_mbcs(i) for i in vdata] + elif vtype in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: + value['vdata'] = _to_mbcs(vdata) + else: + value['vdata'] = vdata + values.append(value) + except pywintypes.error as exc: # pylint: disable=E0602 + log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) + return False, r'Cannot find key: {0}\{1}'.format(hive, key) + finally: + if handle: + handle.Close() + return values + + +def read_value(hive, key, vname=None, use_32bit_registry=False): + r''' + Reads a registry value entry or the default value for a key. + + :param str hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param str key: The key (looks like a path) to the value name. + + :param str vname: The value name. These are the individual name/data pairs + under the key. If not passed, the key (Default) value will be returned + + :param bool use_32bit_registry: Accesses the 32bit portion of the registry + on 64bit installations. On 32bit machines this is ignored. + + :return: A dictionary containing the passed settings as well as the + value_data if successful. If unsuccessful, sets success to False. + + :rtype: dict + + If vname is not passed: + + - Returns the first unnamed value (Default) as a string. + - Returns none if first unnamed value is empty. + - Returns False if key not found. + + CLI Example: + + .. code-block:: bash + + salt '*' reg.read_value HKEY_LOCAL_MACHINE 'SOFTWARE\Salt' 'version' + ''' + # If no name is passed, the default value of the key will be returned + # The value name is Default + + # Setup the return array + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) + + ret = {'hive': local_hive, + 'key': local_key, + 'vname': local_vname, + 'vdata': None, + 'success': True} + + if not vname: + ret['vname'] = '(Default)' + + registry = Registry() + hkey = registry.hkeys[local_hive] + access_mask = registry.registry_32[use_32bit_registry] + + try: + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + try: + # RegQueryValueEx returns and accepts unicode data + vdata, vtype = win32api.RegQueryValueEx(handle, local_vname) + if vdata or vdata in [0, '']: + # Only convert text types to unicode + ret['vtype'] = registry.vtype_reverse[vtype] + if vtype == win32con.REG_MULTI_SZ: + ret['vdata'] = [_to_mbcs(i) for i in vdata] + elif vtype in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: + ret['vdata'] = _to_mbcs(vdata) + else: + ret['vdata'] = vdata + else: + ret['comment'] = 'Empty Value' + except WindowsError: # pylint: disable=E0602 + ret['vdata'] = ('(value not set)') + ret['vtype'] = 'REG_SZ' + except pywintypes.error as exc: # pylint: disable=E0602 + msg = 'Cannot find {0} in {1}\\{2}' \ + ''.format(local_vname, local_hive, local_key) + log.trace(exc) + log.trace(msg) + ret['comment'] = msg + ret['success'] = False + except pywintypes.error as exc: # pylint: disable=E0602 + msg = 'Cannot find key: {0}\\{1}'.format(local_hive, local_key) + log.trace(exc) + log.trace(msg) + ret['comment'] = msg + ret['success'] = False + return ret + + +def set_value(hive, + key, + vname=None, + vdata=None, + vtype='REG_SZ', + use_32bit_registry=False, + volatile=False): + ''' + Sets a registry value entry or the default value for a key. + + :param str hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param str key: The key (looks like a path) to the value name. + + :param str vname: The value name. These are the individual name/data pairs + under the key. If not passed, the key (Default) value will be set. + + :param object vdata: The value data to be set. + What the type of this parameter + should be is determined by the value of the vtype + parameter. The correspondence + is as follows: + + .. glossary:: + + REG_BINARY + binary data (i.e. str in python version < 3 and bytes in version >=3) + REG_DWORD + int + REG_EXPAND_SZ + str + REG_MULTI_SZ + list of objects of type str + REG_SZ + str + + :param str vtype: The value type. + The possible values of the vtype parameter are indicated + above in the description of the vdata parameter. + + :param bool use_32bit_registry: Sets the 32bit portion of the registry on + 64bit installations. On 32bit machines this is ignored. + + :param bool volatile: When this parameter has a value of True, the registry key will be + made volatile (i.e. it will not persist beyond a system reset or shutdown). + This parameter only has an effect when a key is being created and at no + other time. + + :return: Returns True if successful, False if not + :rtype: bool + + CLI Example: + + .. code-block:: bash + + salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' + + This function is strict about the type of vdata. For instance the + the next example will fail because vtype has a value of REG_SZ and vdata + has a type of int (as opposed to str as expected). + + CLI Example: + + .. code-block:: bash + + salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ + vtype=REG_SZ vdata=0 + + However, this next example where vdata is properly quoted should succeed. + + CLI Example: + + .. code-block:: bash + + salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ + vtype=REG_SZ vdata="'0'" + + An example of using vtype REG_BINARY is as follows: + + CLI Example: + + .. code-block:: bash + + salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ + vtype=REG_BINARY vdata='!!binary d2hhdCdzIHRoZSBwb2ludA==' + + An example of using vtype REG_LIST is as follows: + + CLI Example: + + .. code-block:: bash + + salt '*' reg.set_value HKEY_LOCAL_MACHINE 'SOFTWARE\\Salt' 'version' '2015.5.2' \\ + vtype=REG_LIST vdata='[a,b,c]' + ''' + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) + local_vtype = _to_unicode(vtype) + + registry = Registry() + hkey = registry.hkeys[local_hive] + vtype_value = registry.vtype[local_vtype] + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS + + # Check data type and cast to expected type + # int will automatically become long on 64bit numbers + # https://www.python.org/dev/peps/pep-0237/ + + # String Types to Unicode + if vtype_value in [win32con.REG_SZ, win32con.REG_EXPAND_SZ]: + local_vdata = _to_unicode(vdata) + # Don't touch binary... + elif vtype_value == win32con.REG_BINARY: + local_vdata = vdata + # Make sure REG_MULTI_SZ is a list of strings + elif vtype_value == win32con.REG_MULTI_SZ: + local_vdata = [_to_unicode(i) for i in vdata] + # Everything else is int + else: + local_vdata = int(vdata) + + if volatile: + create_options = registry.opttype['REG_OPTION_VOLATILE'] + else: + create_options = registry.opttype['REG_OPTION_NON_VOLATILE'] + + try: + handle, _ = win32api.RegCreateKeyEx(hkey, local_key, access_mask, + Options=create_options) + win32api.RegSetValueEx(handle, local_vname, 0, vtype_value, local_vdata) + win32api.RegFlushKey(handle) + win32api.RegCloseKey(handle) + broadcast_change() + return True + except (win32api.error, SystemError, ValueError, TypeError): # pylint: disable=E0602 + log.exception('Encountered error setting registry value') + return False + + +def delete_key_recursive(hive, key, use_32bit_registry=False): + ''' + .. versionadded:: 2015.5.4 + + Delete a registry key to include all subkeys. + + :param hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param key: The key to remove (looks like a path) + + :param bool use_32bit_registry: Deletes the 32bit portion of the registry on + 64bit installations. On 32bit machines this is ignored. + + :return: A dictionary listing the keys that deleted successfully as well as + those that failed to delete. + :rtype: dict + + The following example will remove ``salt`` and all its subkeys from the + ``SOFTWARE`` key in ``HKEY_LOCAL_MACHINE``: + + CLI Example: + + .. code-block:: bash + + salt '*' reg.delete_key_recursive HKLM SOFTWARE\\salt + ''' + + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + + # Instantiate the registry object + registry = Registry() + hkey = registry.hkeys[local_hive] + key_path = local_key + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS + + if not key_exists(local_hive, local_key, use_32bit_registry): + return False + + if (len(key) > 1) and (key.count('\\', 1) < registry.subkey_slash_check[hkey]): + log.error( + 'Hive:%s Key:%s; key is too close to root, not safe to remove', + hive, key + ) + return False + + # Functions for traversing the registry tree + def _subkeys(_key): + ''' + Enumerate keys + ''' + i = 0 + while True: + try: + subkey = win32api.RegEnumKey(_key, i) + yield subkey + i += 1 + except pywintypes.error: # pylint: disable=E0602 + break + + def _traverse_registry_tree(_hkey, _keypath, _ret, _access_mask): + ''' + Traverse the registry tree i.e. dive into the tree + ''' + _key = win32api.RegOpenKeyEx(_hkey, _keypath, 0, _access_mask) + for subkeyname in _subkeys(_key): + subkeypath = r'{0}\{1}'.format(_keypath, subkeyname) + _ret = _traverse_registry_tree(_hkey, subkeypath, _ret, access_mask) + _ret.append(subkeypath) + return _ret + + # Get a reverse list of registry keys to be deleted + key_list = [] + key_list = _traverse_registry_tree(hkey, key_path, key_list, access_mask) + # Add the top level key last, all subkeys must be deleted first + key_list.append(key_path) + + ret = {'Deleted': [], + 'Failed': []} + + # Delete all sub_keys + for sub_key_path in key_list: + try: + key_handle = win32api.RegOpenKeyEx(hkey, sub_key_path, 0, access_mask) + win32api.RegDeleteKey(key_handle, '') + ret['Deleted'].append(r'{0}\{1}'.format(hive, sub_key_path)) + except WindowsError as exc: # pylint: disable=E0602 + log.error(exc, exc_info=True) + ret['Failed'].append(r'{0}\{1} {2}'.format(hive, sub_key_path, exc)) + + broadcast_change() + + return ret + + +def delete_value(hive, key, vname=None, use_32bit_registry=False): + ''' + Delete a registry value entry or the default value for a key. + + :param str hive: The name of the hive. Can be one of the following + + - HKEY_LOCAL_MACHINE or HKLM + - HKEY_CURRENT_USER or HKCU + - HKEY_USER or HKU + - HKEY_CLASSES_ROOT or HKCR + - HKEY_CURRENT_CONFIG or HKCC + + :param str key: The key (looks like a path) to the value name. + + :param str vname: The value name. These are the individual name/data pairs + under the key. If not passed, the key (Default) value will be deleted. + + :param bool use_32bit_registry: Deletes the 32bit portion of the registry on + 64bit installations. On 32bit machines this is ignored. + + :return: Returns True if successful, False if not + :rtype: bool + + CLI Example: + + .. code-block:: bash + + salt '*' reg.delete_value HKEY_CURRENT_USER 'SOFTWARE\\Salt' 'version' + ''' + local_hive = _to_unicode(hive) + local_key = _to_unicode(key) + local_vname = _to_unicode(vname) + + registry = Registry() + hkey = registry.hkeys[local_hive] + access_mask = registry.registry_32[use_32bit_registry] | win32con.KEY_ALL_ACCESS + + try: + handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) + win32api.RegDeleteValue(handle, local_vname) + win32api.RegCloseKey(handle) + broadcast_change() + return True + except WindowsError as exc: # pylint: disable=E0602 + log.error(exc, exc_info=True) + log.error('Hive: %s', local_hive) + log.error('Key: %s', local_key) + log.error('ValueName: %s', local_vname) + log.error('32bit Reg: %s', use_32bit_registry) + return False From f15f92318d8d7c241008cb6f6be9e6d946e09c8b Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 16 Mar 2018 18:01:43 -0600 Subject: [PATCH 096/117] Add tests for salt.utils.win_reg Fix some issues found in testing --- salt/utils/win_reg.py | 52 +++--- tests/unit/utils/test_win_reg.py | 306 +++++++++++++++++++++++++++++++ 2 files changed, 334 insertions(+), 24 deletions(-) create mode 100644 tests/unit/utils/test_win_reg.py diff --git a/salt/utils/win_reg.py b/salt/utils/win_reg.py index 999a46e799..7d9cbfa83f 100644 --- a/salt/utils/win_reg.py +++ b/salt/utils/win_reg.py @@ -37,7 +37,6 @@ try: import win32gui import win32api import win32con - import pywintypes HAS_WINDOWS_MODULES = True except ImportError: HAS_WINDOWS_MODULES = False @@ -177,7 +176,7 @@ def key_exists(hive, key, use_32bit_registry=False): handle = win32api.RegOpenKeyEx(hkey, local_key, 0, access_mask) win32api.RegCloseKey(handle) return True - except WindowsError: # pylint: disable=E0602 + except Exception: # pylint: disable=E0602 return False @@ -248,7 +247,7 @@ def list_keys(hive, key=None, use_32bit_registry=False): handle.Close() - except pywintypes.error: # pylint: disable=E0602 + except Exception: # pylint: disable=E0602 log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) return False, r'Cannot find key: {0}\{1}'.format(hive, key) @@ -315,7 +314,7 @@ def list_values(hive, key=None, use_32bit_registry=False, include_default=True): else: value['vdata'] = vdata values.append(value) - except pywintypes.error as exc: # pylint: disable=E0602 + except Exception as exc: # pylint: disable=E0602 log.debug(r'Cannot find key: %s\%s', hive, key, exc_info=True) return False, r'Cannot find key: {0}\{1}'.format(hive, key) finally: @@ -398,17 +397,18 @@ def read_value(hive, key, vname=None, use_32bit_registry=False): ret['vdata'] = vdata else: ret['comment'] = 'Empty Value' - except WindowsError: # pylint: disable=E0602 - ret['vdata'] = ('(value not set)') - ret['vtype'] = 'REG_SZ' - except pywintypes.error as exc: # pylint: disable=E0602 - msg = 'Cannot find {0} in {1}\\{2}' \ - ''.format(local_vname, local_hive, local_key) - log.trace(exc) - log.trace(msg) - ret['comment'] = msg - ret['success'] = False - except pywintypes.error as exc: # pylint: disable=E0602 + except Exception as exc: + if exc.winerror == 2 and vname is None: + ret['vdata'] = ('(value not set)') + ret['vtype'] = 'REG_SZ' + else: + msg = 'Cannot find {0} in {1}\\{2}' \ + ''.format(local_vname, local_hive, local_key) + log.trace(exc) + log.trace(msg) + ret['comment'] = msg + ret['success'] = False + except Exception as exc: # pylint: disable=E0602 msg = 'Cannot find key: {0}\\{1}'.format(local_hive, local_key) log.trace(exc) log.trace(msg) @@ -626,7 +626,7 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): subkey = win32api.RegEnumKey(_key, i) yield subkey i += 1 - except pywintypes.error: # pylint: disable=E0602 + except Exception: # pylint: disable=E0602 break def _traverse_registry_tree(_hkey, _keypath, _ret, _access_mask): @@ -684,7 +684,8 @@ def delete_value(hive, key, vname=None, use_32bit_registry=False): :param bool use_32bit_registry: Deletes the 32bit portion of the registry on 64bit installations. On 32bit machines this is ignored. - :return: Returns True if successful, False if not + :return: Returns True if successful, None if the value didn't exist, and + False if unsuccessful :rtype: bool CLI Example: @@ -707,10 +708,13 @@ def delete_value(hive, key, vname=None, use_32bit_registry=False): win32api.RegCloseKey(handle) broadcast_change() return True - except WindowsError as exc: # pylint: disable=E0602 - log.error(exc, exc_info=True) - log.error('Hive: %s', local_hive) - log.error('Key: %s', local_key) - log.error('ValueName: %s', local_vname) - log.error('32bit Reg: %s', use_32bit_registry) - return False + except Exception as exc: # pylint: disable=E0602 + if exc.winerror == 2: + return None + else: + log.error(exc, exc_info=True) + log.error('Hive: %s', local_hive) + log.error('Key: %s', local_key) + log.error('ValueName: %s', local_vname) + log.error('32bit Reg: %s', use_32bit_registry) + return False diff --git a/tests/unit/utils/test_win_reg.py b/tests/unit/utils/test_win_reg.py new file mode 100644 index 0000000000..e31094448c --- /dev/null +++ b/tests/unit/utils/test_win_reg.py @@ -0,0 +1,306 @@ +# -*- coding: utf-8 -*- + +# Import Python Libs +from __future__ import absolute_import, unicode_literals, print_function + +# Import Salt Testing Libs +from tests.support.helpers import destructiveTest +from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch +from tests.support.unit import TestCase, skipIf + +# Import Salt Libs +import salt.utils.platform +import salt.utils.win_reg as win_reg + + +@skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not salt.utils.platform.is_windows(), 'System is not Windows') +class WinFunctionsTestCase(TestCase): + ''' + Test cases for salt.utils.win_reg + ''' + + def test_key_exists_existing(self): + ''' + Tests the key exists function using a well known registry key + ''' + self.assertEqual( + win_reg.key_exists( + hive='HKLM', + key='SOFTWARE\\Microsoft' + ), + True + ) + + def test_key_exists_non_existing(self): + ''' + Tests the key exists function using a non existing registry key + ''' + self.assertEqual( + win_reg.key_exists( + hive='HKLM', + key='SOFTWARE\\Salt\\fake_key' + ), + False + ) + + def test_broadcast_change_success(self): + ''' + Tests the broadcast_change function + ''' + with patch('win32gui.SendMessageTimeout', return_value=('', 0)): + self.assertEqual(win_reg.broadcast_change(), True) + + def test_broadcast_change_fail(self): + ''' + Tests the broadcast_change function failure + ''' + with patch('win32gui.SendMessageTimeout', return_value=('', 1)): + self.assertEqual(win_reg.broadcast_change(), False) + + def test_list_keys_existing(self): + ''' + Test the list_keys function using a well known registry key + ''' + self.assertIn( + 'Microsoft', + win_reg.list_keys( + hive='HKLM', + key='SOFTWARE' + ) + ) + + def test_list_keys_non_existing(self): + ''' + Test the list_keys function using a non existing registry key + ''' + expected = (False, 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key') + self.assertEqual( + win_reg.list_keys( + hive='HKLM', + key='SOFTWARE\\Salt\\fake_key'), + expected + ) + + def test_list_values_existing(self): + ''' + Test the list_values function using a well known registry key + ''' + values = win_reg.list_values( + hive='HKLM', + key='SOFTWARE\\Microsoft\\Windows\\CurrentVersion' + ) + keys = [] + for value in values: + keys.append(value['vname']) + self.assertIn('ProgramFilesDir', keys) + + def test_list_values_non_existing(self): + ''' + Test the list_values function using a non existing registry key + ''' + expected = (False, 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key') + self.assertEqual( + win_reg.list_values( + hive='HKLM', + key='SOFTWARE\\Salt\\fake_key' + ), + expected + ) + + def test_read_value_existing(self): + ''' + Test the list_values function using a well known registry key + ''' + ret = win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Microsoft\\Windows\\CurrentVersion', + vname='ProgramFilesPath' + ) + self.assertEqual(ret['vdata'], '%ProgramFiles%') + + def test_read_value_default(self): + ''' + Test the read_value function reading the default value + ''' + ret = win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Microsoft\\Windows\\CurrentVersion' + ) + self.assertEqual(ret['vdata'], '(value not set)') + + def test_read_value_non_existing(self): + ''' + Test the list_values function using a non existing registry key + ''' + expected = { + 'comment': 'Cannot find fake_name in HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion', + 'vdata': None, + 'vname': 'fake_name', + 'success': False, + 'hive': 'HKLM', + 'key': 'SOFTWARE\\Microsoft\\Windows\\CurrentVersion' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Microsoft\\Windows\\CurrentVersion', + vname='fake_name' + ), + expected + ) + + def test_read_value_non_existing_key(self): + ''' + Test the list_values function using a non existing registry key + ''' + expected = { + 'comment': 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key', + 'vdata': None, + 'vname': 'fake_name', + 'success': False, + 'hive': 'HKLM', + 'key': 'SOFTWARE\\Salt\\fake_key' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Salt\\fake_key', + vname='fake_name' + ), + expected + ) + + @destructiveTest + def test_set_value(self): + ''' + Test the set_value function + ''' + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vname='fake_name', + vdata='fake_data' + ) + ) + expected = { + 'hive': 'HKLM', + 'key': 'SOFTWARE\\Salt\\Test\\', + 'success': True, + 'vdata': 'fake_data', + 'vname': 'fake_name', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vname='fake_name' + ), + expected + ) + expected = { + 'Deleted': [ + 'HKLM\\SOFTWARE\\Salt\\Test', + 'HKLM\\SOFTWARE\\Salt' + ], + 'Failed': [] + } + self.assertEqual( + win_reg.delete_key_recursive( + hive='HKLM', + key='SOFTWARE\\Salt' + ), + expected + ) + + @destructiveTest + def test_set_value_default(self): + ''' + Test the set_value function + ''' + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vdata='fake_default_data' + ) + ) + expected = { + 'hive': 'HKLM', + 'key': 'SOFTWARE\\Salt\\Test\\', + 'success': True, + 'vdata': 'fake_default_data', + 'vname': '(Default)', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + ), + expected + ) + expected = { + 'Deleted': [ + 'HKLM\\SOFTWARE\\Salt\\Test', + 'HKLM\\SOFTWARE\\Salt' + ], + 'Failed': [] + } + self.assertEqual( + win_reg.delete_key_recursive( + hive='HKLM', + key='SOFTWARE\\Salt' + ), + expected + ) + + @destructiveTest + def test_delete_value(self): + ''' + Test the delete_value function + ''' + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vname='fake_name', + vdata='fake_data' + ) + ) + self.assertTrue( + win_reg.delete_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vname='fake_name' + ) + ) + expected = { + 'Deleted': [ + 'HKLM\\SOFTWARE\\Salt\\Test', + 'HKLM\\SOFTWARE\\Salt' + ], + 'Failed': [] + } + self.assertEqual( + win_reg.delete_key_recursive( + hive='HKLM', + key='SOFTWARE\\Salt' + ), + expected + ) + + def test_delete_value_non_existing(self): + ''' + Test the delete_value function + ''' + self.assertEqual( + win_reg.delete_value( + hive='HKLM', + key='SOFTWARE\\Salt\\Test\\', + vname='fake_name' + ), + None + ) From e0d201a96fa61e25b08dceee7474535541dd9a61 Mon Sep 17 00:00:00 2001 From: twangboy Date: Fri, 16 Mar 2018 18:06:19 -0600 Subject: [PATCH 097/117] Make sure the docs are correct for the tests --- tests/unit/utils/test_win_reg.py | 37 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/tests/unit/utils/test_win_reg.py b/tests/unit/utils/test_win_reg.py index e31094448c..d4d4a36921 100644 --- a/tests/unit/utils/test_win_reg.py +++ b/tests/unit/utils/test_win_reg.py @@ -19,6 +19,19 @@ class WinFunctionsTestCase(TestCase): ''' Test cases for salt.utils.win_reg ''' + def test_broadcast_change_success(self): + ''' + Tests the broadcast_change function + ''' + with patch('win32gui.SendMessageTimeout', return_value=('', 0)): + self.assertEqual(win_reg.broadcast_change(), True) + + def test_broadcast_change_fail(self): + ''' + Tests the broadcast_change function failure + ''' + with patch('win32gui.SendMessageTimeout', return_value=('', 1)): + self.assertEqual(win_reg.broadcast_change(), False) def test_key_exists_existing(self): ''' @@ -44,20 +57,6 @@ class WinFunctionsTestCase(TestCase): False ) - def test_broadcast_change_success(self): - ''' - Tests the broadcast_change function - ''' - with patch('win32gui.SendMessageTimeout', return_value=('', 0)): - self.assertEqual(win_reg.broadcast_change(), True) - - def test_broadcast_change_fail(self): - ''' - Tests the broadcast_change function failure - ''' - with patch('win32gui.SendMessageTimeout', return_value=('', 1)): - self.assertEqual(win_reg.broadcast_change(), False) - def test_list_keys_existing(self): ''' Test the list_keys function using a well known registry key @@ -110,7 +109,7 @@ class WinFunctionsTestCase(TestCase): def test_read_value_existing(self): ''' - Test the list_values function using a well known registry key + Test the read_value function using a well known registry value ''' ret = win_reg.read_value( hive='HKLM', @@ -131,7 +130,7 @@ class WinFunctionsTestCase(TestCase): def test_read_value_non_existing(self): ''' - Test the list_values function using a non existing registry key + Test the read_value function using a non existing value pair ''' expected = { 'comment': 'Cannot find fake_name in HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion', @@ -152,7 +151,7 @@ class WinFunctionsTestCase(TestCase): def test_read_value_non_existing_key(self): ''' - Test the list_values function using a non existing registry key + Test the read_value function using a non existing registry key ''' expected = { 'comment': 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key', @@ -218,7 +217,7 @@ class WinFunctionsTestCase(TestCase): @destructiveTest def test_set_value_default(self): ''' - Test the set_value function + Test the set_value function on the default value ''' self.assertTrue( win_reg.set_value( @@ -294,7 +293,7 @@ class WinFunctionsTestCase(TestCase): def test_delete_value_non_existing(self): ''' - Test the delete_value function + Test the delete_value function on non existing value ''' self.assertEqual( win_reg.delete_value( From aa98bdf2505b11779585fa5625bfe92c78f4f610 Mon Sep 17 00:00:00 2001 From: twangboy Date: Tue, 20 Mar 2018 15:52:37 -0600 Subject: [PATCH 098/117] Fix some lint --- salt/modules/reg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/reg.py b/salt/modules/reg.py index eafae0b952..363f01d98c 100644 --- a/salt/modules/reg.py +++ b/salt/modules/reg.py @@ -48,7 +48,7 @@ def __virtual__(): return (False, 'reg execution module failed to load: ' 'The module will only run on Windows systems') - if not 'reg.read_value' in __utils__: + if 'reg.read_value' not in __utils__: return (False, 'reg execution module failed to load: ' 'The reg salt util is unavailable') From fc9ecd75e26d4e6b61dfd2acdba32689fc357a9a Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 22 Mar 2018 14:25:26 -0600 Subject: [PATCH 099/117] Skip unit.state.test_reg unless on Windows --- tests/unit/states/test_reg.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/unit/states/test_reg.py b/tests/unit/states/test_reg.py index 8441f687b0..8f2ca06e5e 100644 --- a/tests/unit/states/test_reg.py +++ b/tests/unit/states/test_reg.py @@ -17,9 +17,11 @@ from tests.support.mock import ( # Import Salt Libs import salt.states.reg as reg +import salt.utils.platform @skipIf(NO_MOCK, NO_MOCK_REASON) +@skipIf(not salt.utils.platform.is_windows(), 'System is not Windows') class RegTestCase(TestCase, LoaderModuleMockMixin): ''' Test cases for salt.states.reg @@ -46,7 +48,7 @@ class RegTestCase(TestCase, LoaderModuleMockMixin): {'vdata': 'a', 'success': True}, {'vdata': 'a', 'success': True}]) mock_t = MagicMock(return_value=True) - with patch.dict(reg.__salt__, {'reg.read_value': mock_read, + with patch.dict(reg.__utils__, {'reg.read_value': mock_read, 'reg.set_value': mock_t}): self.assertDictEqual(reg.present(name, vname=vname, @@ -92,17 +94,17 @@ class RegTestCase(TestCase, LoaderModuleMockMixin): mock_read_false = MagicMock(return_value={'success': False, 'vdata': False}) mock_t = MagicMock(return_value=True) - with patch.dict(reg.__salt__, {'reg.read_value': mock_read_false, + with patch.dict(reg.__utils__, {'reg.read_value': mock_read_false, 'reg.delete_value': mock_t}): self.assertDictEqual(reg.absent(name, vname), ret) - with patch.dict(reg.__salt__, {'reg.read_value': mock_read_true}): + with patch.dict(reg.__utils__, {'reg.read_value': mock_read_true}): with patch.dict(reg.__opts__, {'test': True}): ret.update({'comment': '', 'result': None, 'changes': {'reg': {'Will remove': {'Entry': vname, 'Key': name}}}}) self.assertDictEqual(reg.absent(name, vname), ret) - with patch.dict(reg.__salt__, {'reg.read_value': mock_read_true, + with patch.dict(reg.__utils__, {'reg.read_value': mock_read_true, 'reg.delete_value': mock_t}): with patch.dict(reg.__opts__, {'test': False}): ret.update({'result': True, From 0de54ed953d83607287be72b0f8bf2764121d90e Mon Sep 17 00:00:00 2001 From: twangboy Date: Thu, 22 Mar 2018 15:59:46 -0600 Subject: [PATCH 100/117] Additional tests Adds additional tests for unicode scenarios Fixes an issue with reg.py when deleting unicode keys Puts the destructive tests in a try/except block so the delete occurs no matter what Uses a randomly generated key name for testing --- salt/utils/win_reg.py | 4 +- tests/unit/modules/test_reg_win.py | 301 ---------------------------- tests/unit/utils/test_win_reg.py | 312 +++++++++++++++++++---------- 3 files changed, 204 insertions(+), 413 deletions(-) delete mode 100644 tests/unit/modules/test_reg_win.py diff --git a/salt/utils/win_reg.py b/salt/utils/win_reg.py index 7d9cbfa83f..958c3bc278 100644 --- a/salt/utils/win_reg.py +++ b/salt/utils/win_reg.py @@ -624,7 +624,7 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): while True: try: subkey = win32api.RegEnumKey(_key, i) - yield subkey + yield _to_mbcs(subkey) i += 1 except Exception: # pylint: disable=E0602 break @@ -635,7 +635,7 @@ def delete_key_recursive(hive, key, use_32bit_registry=False): ''' _key = win32api.RegOpenKeyEx(_hkey, _keypath, 0, _access_mask) for subkeyname in _subkeys(_key): - subkeypath = r'{0}\{1}'.format(_keypath, subkeyname) + subkeypath = '{0}\\{1}'.format(_keypath, subkeyname) _ret = _traverse_registry_tree(_hkey, subkeypath, _ret, access_mask) _ret.append(subkeypath) return _ret diff --git a/tests/unit/modules/test_reg_win.py b/tests/unit/modules/test_reg_win.py deleted file mode 100644 index 5964237560..0000000000 --- a/tests/unit/modules/test_reg_win.py +++ /dev/null @@ -1,301 +0,0 @@ -# -*- coding: utf-8 -*- -''' - :synopsis: Unit Tests for Windows Registry Module 'module.reg' - :platform: Windows - :maturity: develop - :codeauthor: Damon Atkins - versionadded:: 2016.11.0 -''' -# Import Python future libs -from __future__ import absolute_import, print_function, unicode_literals -# Import Python Libs -import sys -import time -# Import Salt Testing Libs -from tests.support.unit import TestCase, skipIf -from tests.support.helpers import destructiveTest -# Import Salt Libs -import salt.modules.reg as win_mod_reg -from salt.ext import six -try: - from salt.ext.six.moves import winreg as _winreg # pylint: disable=import-error,no-name-in-module - NO_WINDOWS_MODULES = False -except ImportError: - NO_WINDOWS_MODULES = True - -PY2 = sys.version_info[0] == 2 -# The following used to make sure we are not -# testing already existing data -# Note strftime returns a str, so we need to make it unicode -TIMEINT = int(time.time()) - -if PY2: - TIME_INT_UNICODE = six.text_type(TIMEINT) - TIMESTR = time.strftime('%X %x %Z').decode('utf-8') -else: - TIMESTR = time.strftime('%X %x %Z') - TIME_INT_UNICODE = str(TIMEINT) # pylint: disable=R0204 - - -# we do not need to prefix this with u, as we are -# using from __future__ import unicode_literals -UNICODETEST_WITH_SIGNS = 'Testing Unicode \N{COPYRIGHT SIGN},\N{TRADE MARK SIGN},\N{REGISTERED SIGN} '+TIMESTR -UNICODETEST_WITHOUT_SIGNS = 'Testing Unicode'+TIMESTR -UNICODE_TEST_KEY = 'UnicodeKey \N{TRADE MARK SIGN} '+TIME_INT_UNICODE -UNICODE_TEST_KEY_DEL = 'Delete Me \N{TRADE MARK SIGN} '+TIME_INT_UNICODE - - -@skipIf(NO_WINDOWS_MODULES, 'requires Windows OS to test Windows registry') -class RegWinTestCase(TestCase): - ''' - Test cases for salt.modules.reg - ''' - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_read_reg_plain(self): - ''' - Test - Read a registry value from a subkey using Pythen 2 Strings or - Pythen 3 Bytes - ''' - if not PY2: - self.skipTest('Invalid for Python Version 2') - - subkey = b'Software\\Microsoft\\Windows NT\\CurrentVersion' - vname = b'PathName' - handle = _winreg.OpenKey( - _winreg.HKEY_LOCAL_MACHINE, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - (current_vdata, dummy_current_vtype) = _winreg.QueryValueEx(handle, vname) - _winreg.CloseKey(handle) - - test_vdata = win_mod_reg.read_value(b'HKEY_LOCAL_MACHINE', subkey, vname)[b'vdata'] - self.assertEqual( - test_vdata, current_vdata) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_read_reg_unicode(self): - ''' - Test - Read a registry value from a subkey using Pythen 2 Unicode - or Pythen 3 Str i.e. Unicode - ''' - subkey = 'Software\\Microsoft\\Windows NT\\CurrentVersion' - vname = 'PathName' - handle = _winreg.OpenKey( - _winreg.HKEY_LOCAL_MACHINE, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - (current_vdata, dummy_current_vtype) = _winreg.QueryValueEx(handle, vname) - _winreg.CloseKey(handle) - - test_vdata = win_mod_reg.read_value( - 'HKEY_LOCAL_MACHINE', - subkey, - vname)['vdata'] - self.assertEqual(test_vdata, current_vdata) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_list_keys_fail(self): - ''' - Test - Read list the keys under a subkey which does not exist. - ''' - subkey = 'ThisIsJunkItDoesNotExistIhope' - test_list = win_mod_reg.list_keys('HKEY_LOCAL_MACHINE', subkey) - # returns a tuple with first item false, and second item a reason - test = isinstance(test_list, tuple) and (not test_list[0]) - self.assertTrue(test) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_list_keys(self): - ''' - Test - Read list the keys under a subkey - ''' - subkey = 'Software\\Microsoft\\Windows NT\\CurrentVersion' - test_list = win_mod_reg.list_keys('HKEY_LOCAL_MACHINE', subkey) - test = len(test_list) > 5 # Their should be a lot more than 5 items - self.assertTrue(test) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_list_values_fail(self): - ''' - Test - List the values under a subkey which does not exist. - ''' - subkey = 'ThisIsJunkItDoesNotExistIhope' - test_list = win_mod_reg.list_values('HKEY_LOCAL_MACHINE', subkey) - # returns a tuple with first item false, and second item a reason - test = isinstance(test_list, tuple) and (not test_list[0]) - self.assertTrue(test) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_list_values(self): - ''' - Test - List the values under a subkey. - ''' - subkey = r'Software\Microsoft\Windows NT\CurrentVersion' - test_list = win_mod_reg.list_values('HKEY_LOCAL_MACHINE', subkey) - test = len(test_list) > 5 # There should be a lot more than 5 items - self.assertTrue(test) - - # Not considering this destructive as its writing to a private space - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_set_value_unicode(self): - ''' - Test - set a registry plain text subkey name to a unicode string value - ''' - vname = 'TestUniccodeString' - subkey = 'Software\\SaltStackTest' - test1_success = False - test2_success = False - test1_success = win_mod_reg.set_value( - 'HKEY_LOCAL_MACHINE', - subkey, - vname, - UNICODETEST_WITH_SIGNS - ) - # Now use _winreg direct to see if it worked as expected - if test1_success: - handle = _winreg.OpenKey( - _winreg.HKEY_LOCAL_MACHINE, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - (current_vdata, dummy_current_vtype) = _winreg.QueryValueEx(handle, vname) - _winreg.CloseKey(handle) - test2_success = (current_vdata == UNICODETEST_WITH_SIGNS) - self.assertTrue(test1_success and test2_success) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_set_value_unicode_key(self): - ''' - Test - set a registry Unicode subkey name with unicode characters within - to a integer - ''' - test_success = win_mod_reg.set_value( - 'HKEY_LOCAL_MACHINE', - 'Software\\SaltStackTest', - UNICODE_TEST_KEY, - TIMEINT, - 'REG_DWORD' - ) - self.assertTrue(test_success) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_del_value(self): - ''' - Test - Create Directly and Delete with salt a registry value - ''' - subkey = 'Software\\SaltStackTest' - vname = UNICODE_TEST_KEY_DEL - vdata = 'I will be deleted' - if PY2: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_LOCAL_MACHINE, - subkey.encode('mbcs'), - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx( - handle, - vname.encode('mbcs'), - 0, - _winreg.REG_SZ, - vdata.encode('mbcs') - ) - else: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_LOCAL_MACHINE, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx(handle, vname, 0, _winreg.REG_SZ, vdata) - _winreg.CloseKey(handle) - # time.sleep(15) # delays for 15 seconds - test_success = win_mod_reg.delete_value( - 'HKEY_LOCAL_MACHINE', - subkey, - vname - ) - self.assertTrue(test_success) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - def test_del_key_recursive_user(self): - ''' - Test - Create directly key/value pair and Delete recusivly with salt - ''' - subkey = 'Software\\SaltStackTest' - vname = UNICODE_TEST_KEY_DEL - vdata = 'I will be deleted recursive' - if PY2: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_CURRENT_USER, - subkey.encode('mbcs'), - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx( - handle, - vname.encode('mbcs'), - 0, - _winreg.REG_SZ, - vdata.encode('mbcs') - ) - else: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_CURRENT_USER, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx(handle, vname, 0, _winreg.REG_SZ, vdata) - _winreg.CloseKey(handle) - # time.sleep(15) # delays for 15 seconds so you can run regedit & watch it happen - test_success = win_mod_reg.delete_key_recursive('HKEY_CURRENT_USER', subkey) - self.assertTrue(test_success) - - @skipIf(not sys.platform.startswith("win"), "requires Windows OS") - @destructiveTest - def test_del_key_recursive_machine(self): - ''' - This is a DESTRUCTIVE TEST it creates a new registry entry. - And then destroys the registry entry recusively , however it is completed in its own space - within the registry. We mark this as destructiveTest as it has the potential - to detroy a machine if salt reg code has a large error in it. - ''' - subkey = 'Software\\SaltStackTest' - vname = UNICODE_TEST_KEY_DEL - vdata = 'I will be deleted recursive' - if PY2: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_LOCAL_MACHINE, - subkey.encode('mbcs'), - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx( - handle, - vname.encode('mbcs'), - 0, - _winreg.REG_SZ, - vdata.encode('mbcs') - ) - else: - handle = _winreg.CreateKeyEx( - _winreg.HKEY_LOCAL_MACHINE, - subkey, - 0, - _winreg.KEY_ALL_ACCESS - ) - _winreg.SetValueEx(handle, vname, 0, _winreg.REG_SZ, vdata) - _winreg.CloseKey(handle) - # time.sleep(15) # delays for 15 seconds so you can run regedit and watch it happen - test_success = win_mod_reg.delete_key_recursive('HKEY_LOCAL_MACHINE', subkey) - self.assertTrue(test_success) - - # pylint: disable=W0511 - # TODO: Test other hives, other than HKEY_LOCAL_MACHINE and HKEY_CURRENT_USER diff --git a/tests/unit/utils/test_win_reg.py b/tests/unit/utils/test_win_reg.py index d4d4a36921..04df05117e 100644 --- a/tests/unit/utils/test_win_reg.py +++ b/tests/unit/utils/test_win_reg.py @@ -4,7 +4,7 @@ from __future__ import absolute_import, unicode_literals, print_function # Import Salt Testing Libs -from tests.support.helpers import destructiveTest +from tests.support.helpers import destructiveTest, generate_random_name from tests.support.mock import NO_MOCK, NO_MOCK_REASON, patch from tests.support.unit import TestCase, skipIf @@ -12,6 +12,11 @@ from tests.support.unit import TestCase, skipIf import salt.utils.platform import salt.utils.win_reg as win_reg +UNICODE_KEY = 'Unicode Key \N{TRADE MARK SIGN}' +UNICODE_VALUE = 'Unicode Value ' \ + '\N{COPYRIGHT SIGN},\N{TRADE MARK SIGN},\N{REGISTERED SIGN}' +FAKE_KEY = 'SOFTWARE\\{0}'.format(generate_random_name('SaltTesting-')) + @skipIf(NO_MOCK, NO_MOCK_REASON) @skipIf(not salt.utils.platform.is_windows(), 'System is not Windows') @@ -52,7 +57,7 @@ class WinFunctionsTestCase(TestCase): self.assertEqual( win_reg.key_exists( hive='HKLM', - key='SOFTWARE\\Salt\\fake_key' + key=FAKE_KEY ), False ) @@ -73,11 +78,12 @@ class WinFunctionsTestCase(TestCase): ''' Test the list_keys function using a non existing registry key ''' - expected = (False, 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key') + expected = (False, 'Cannot find key: HKLM\\{0}'.format(FAKE_KEY)) self.assertEqual( win_reg.list_keys( hive='HKLM', - key='SOFTWARE\\Salt\\fake_key'), + key=FAKE_KEY + ), expected ) @@ -98,11 +104,11 @@ class WinFunctionsTestCase(TestCase): ''' Test the list_values function using a non existing registry key ''' - expected = (False, 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key') + expected = (False, 'Cannot find key: HKLM\\{0}'.format(FAKE_KEY)) self.assertEqual( win_reg.list_values( hive='HKLM', - key='SOFTWARE\\Salt\\fake_key' + key=FAKE_KEY ), expected ) @@ -120,7 +126,8 @@ class WinFunctionsTestCase(TestCase): def test_read_value_default(self): ''' - Test the read_value function reading the default value + Test the read_value function reading the default value using a well + known registry key ''' ret = win_reg.read_value( hive='HKLM', @@ -154,17 +161,17 @@ class WinFunctionsTestCase(TestCase): Test the read_value function using a non existing registry key ''' expected = { - 'comment': 'Cannot find key: HKLM\\SOFTWARE\\Salt\\fake_key', + 'comment': 'Cannot find key: HKLM\\{0}'.format(FAKE_KEY), 'vdata': None, 'vname': 'fake_name', 'success': False, 'hive': 'HKLM', - 'key': 'SOFTWARE\\Salt\\fake_key' + 'key': FAKE_KEY } self.assertEqual( win_reg.read_value( hive='HKLM', - key='SOFTWARE\\Salt\\fake_key', + key=FAKE_KEY, vname='fake_name' ), expected @@ -175,121 +182,154 @@ class WinFunctionsTestCase(TestCase): ''' Test the set_value function ''' - self.assertTrue( - win_reg.set_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - vname='fake_name', - vdata='fake_data' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_name', + vdata='fake_data' + ) ) - ) - expected = { - 'hive': 'HKLM', - 'key': 'SOFTWARE\\Salt\\Test\\', - 'success': True, - 'vdata': 'fake_data', - 'vname': 'fake_name', - 'vtype': 'REG_SZ' - } - self.assertEqual( - win_reg.read_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - vname='fake_name' - ), - expected - ) - expected = { - 'Deleted': [ - 'HKLM\\SOFTWARE\\Salt\\Test', - 'HKLM\\SOFTWARE\\Salt' - ], - 'Failed': [] - } - self.assertEqual( - win_reg.delete_key_recursive( - hive='HKLM', - key='SOFTWARE\\Salt' - ), - expected - ) + expected = { + 'hive': 'HKLM', + 'key': FAKE_KEY, + 'success': True, + 'vdata': 'fake_data', + 'vname': 'fake_name', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_name' + ), + expected + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) @destructiveTest def test_set_value_default(self): ''' Test the set_value function on the default value ''' - self.assertTrue( - win_reg.set_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - vdata='fake_default_data' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key=FAKE_KEY, + vdata='fake_default_data' + ) ) - ) - expected = { - 'hive': 'HKLM', - 'key': 'SOFTWARE\\Salt\\Test\\', - 'success': True, - 'vdata': 'fake_default_data', - 'vname': '(Default)', - 'vtype': 'REG_SZ' - } - self.assertEqual( - win_reg.read_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - ), - expected - ) - expected = { - 'Deleted': [ - 'HKLM\\SOFTWARE\\Salt\\Test', - 'HKLM\\SOFTWARE\\Salt' - ], - 'Failed': [] - } - self.assertEqual( - win_reg.delete_key_recursive( - hive='HKLM', - key='SOFTWARE\\Salt' - ), - expected - ) + expected = { + 'hive': 'HKLM', + 'key': FAKE_KEY, + 'success': True, + 'vdata': 'fake_default_data', + 'vname': '(Default)', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key=FAKE_KEY, + ), + expected + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) + + @destructiveTest + def test_set_value_unicode_key(self): + ''' + Test the set_value function on a unicode key + ''' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key='{0}\\{1}'.format(FAKE_KEY, UNICODE_KEY), + vname='fake_name', + vdata='fake_value' + ) + ) + expected = { + 'hive': 'HKLM', + 'key': '{0}\\{1}'.format(FAKE_KEY, UNICODE_KEY), + 'success': True, + 'vdata': 'fake_value', + 'vname': 'fake_name', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key='{0}\\{1}'.format(FAKE_KEY, UNICODE_KEY), + vname='fake_name' + ), + expected + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) + + @destructiveTest + def test_set_value_unicode_value(self): + ''' + Test the set_value function on a unicode value + ''' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_unicode', + vdata=UNICODE_VALUE + ) + ) + expected = { + 'hive': 'HKLM', + 'key': FAKE_KEY, + 'success': True, + 'vdata': UNICODE_VALUE, + 'vname': 'fake_unicode', + 'vtype': 'REG_SZ' + } + self.assertEqual( + win_reg.read_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_unicode' + ), + expected + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) @destructiveTest def test_delete_value(self): ''' Test the delete_value function ''' - self.assertTrue( - win_reg.set_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - vname='fake_name', - vdata='fake_data' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_name', + vdata='fake_data' + ) ) - ) - self.assertTrue( - win_reg.delete_value( - hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', - vname='fake_name' + self.assertTrue( + win_reg.delete_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_name' + ) ) - ) - expected = { - 'Deleted': [ - 'HKLM\\SOFTWARE\\Salt\\Test', - 'HKLM\\SOFTWARE\\Salt' - ], - 'Failed': [] - } - self.assertEqual( - win_reg.delete_key_recursive( - hive='HKLM', - key='SOFTWARE\\Salt' - ), - expected - ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) def test_delete_value_non_existing(self): ''' @@ -298,8 +338,60 @@ class WinFunctionsTestCase(TestCase): self.assertEqual( win_reg.delete_value( hive='HKLM', - key='SOFTWARE\\Salt\\Test\\', + key=FAKE_KEY, vname='fake_name' ), None ) + + @destructiveTest + def test_delete_value_unicode(self): + ''' + Test the delete_value function on a unicode value + ''' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_unicode', + vdata=UNICODE_VALUE + ) + ) + self.assertTrue( + win_reg.delete_value( + hive='HKLM', + key=FAKE_KEY, + vname='fake_unicode' + ) + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) + + @destructiveTest + def test_delete_key_unicode(self): + ''' + Test the delete_value function on value within a unicode key + ''' + try: + self.assertTrue( + win_reg.set_value( + hive='HKLM', + key='{0}\\{1}'.format(FAKE_KEY, UNICODE_KEY), + vname='fake_name', + vdata='fake_value' + ) + ) + expected = { + 'Deleted': ['HKLM\\{0}\\{1}\\'.format(FAKE_KEY, UNICODE_KEY)], + 'Failed': [] + } + self.assertEqual( + win_reg.delete_key_recursive( + hive='HKLM', + key='{0}\\{1}\\'.format(FAKE_KEY, UNICODE_KEY), + ), + expectee + ) + finally: + win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) From 52581e7918d4c96ebb48cb13b938343ad0e8ee83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Karol=20D=C4=85bkowski?= Date: Fri, 23 Mar 2018 09:28:07 +0100 Subject: [PATCH 101/117] Removed trailing whitespace --- salt/modules/chocolatey.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/modules/chocolatey.py b/salt/modules/chocolatey.py index d7345c2e64..3547e8d68b 100644 --- a/salt/modules/chocolatey.py +++ b/salt/modules/chocolatey.py @@ -920,7 +920,7 @@ def version(name, check_remote=False, source=None, pre_versions=False): ''' installed = list_(narrow=name, local_only=True) installed = {k.lower(): v for k, v in installed.items()} - + packages = {} lower_name = name.lower() for pkg in installed: From 5ee64e9b0e65746a44635b89ce96af504375ffe4 Mon Sep 17 00:00:00 2001 From: twangboy Date: Mon, 26 Mar 2018 13:02:53 -0600 Subject: [PATCH 102/117] Fix lint (spelling error) --- tests/unit/utils/test_win_reg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/utils/test_win_reg.py b/tests/unit/utils/test_win_reg.py index 04df05117e..ae939da00a 100644 --- a/tests/unit/utils/test_win_reg.py +++ b/tests/unit/utils/test_win_reg.py @@ -391,7 +391,7 @@ class WinFunctionsTestCase(TestCase): hive='HKLM', key='{0}\\{1}\\'.format(FAKE_KEY, UNICODE_KEY), ), - expectee + expected ) finally: win_reg.delete_key_recursive(hive='HKLM', key=FAKE_KEY) From 88c2c1b405df4c60d2a04f02f66198f4a3a91e1d Mon Sep 17 00:00:00 2001 From: rallytime Date: Mon, 26 Mar 2018 16:15:08 -0400 Subject: [PATCH 103/117] Remove support for expr_form This option has been deprecated and marked for removal in Fluorine. ``tgt_type`` should be used instead. --- doc/topics/releases/fluorine.rst | 13 +++ salt/client/__init__.py | 136 ----------------------------- salt/client/raet/__init__.py | 10 --- salt/client/ssh/client.py | 37 +------- salt/client/ssh/wrapper/publish.py | 29 +----- salt/modules/match.py | 13 --- salt/modules/mine.py | 15 +--- salt/modules/publish.py | 29 +----- salt/modules/raet_publish.py | 29 +----- salt/modules/saltutil.py | 10 --- salt/runners/cache.py | 89 ++----------------- salt/runners/manage.py | 42 +-------- salt/runners/ssh.py | 15 +--- salt/states/modjk_worker.py | 40 +-------- salt/states/saltmod.py | 33 ------- salt/utils/master.py | 15 +--- salt/utils/minions.py | 12 +-- 17 files changed, 39 insertions(+), 528 deletions(-) diff --git a/doc/topics/releases/fluorine.rst b/doc/topics/releases/fluorine.rst index 1267e077a8..df47994859 100644 --- a/doc/topics/releases/fluorine.rst +++ b/doc/topics/releases/fluorine.rst @@ -27,6 +27,19 @@ syndic respects :conf_minion:`enable_legacy_startup_events` as well. Deprecations ------------ +API Deprecations +================ + +Support for :ref:`LocalClient `'s ``expr_form`` argument has +been removed. Please use ``tgt_type`` instead. This change was made due to +numerous reports of confusion among community members, since the targeting +method is published to minions as ``tgt_type``, and appears as ``tgt_type`` +in the job cache as well. + +Those who are using the :ref:`LocalClient ` (either directly, +or implicitly via a :ref:`netapi module `) need to update +their code to use ``tgt_type``. + Module Deprecations =================== diff --git a/salt/client/__init__.py b/salt/client/__init__.py index cdc1ce499d..4df7ec6ce4 100644 --- a/salt/client/__init__.py +++ b/salt/client/__init__.py @@ -41,7 +41,6 @@ import salt.utils.platform import salt.utils.stringutils import salt.utils.user import salt.utils.verify -import salt.utils.versions import salt.utils.zeromq import salt.syspaths as syspaths from salt.exceptions import ( @@ -315,15 +314,6 @@ class LocalClient(object): >>> local.run_job('*', 'test.sleep', [300]) {'jid': '20131219215650131543', 'minions': ['jerry']} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) try: @@ -380,15 +370,6 @@ class LocalClient(object): >>> local.run_job_async('*', 'test.sleep', [300]) {'jid': '20131219215650131543', 'minions': ['jerry']} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) try: @@ -437,15 +418,6 @@ class LocalClient(object): >>> local.cmd_async('*', 'test.sleep', [300]) '20131219215921857715' ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) pub_data = self.run_job(tgt, fun, @@ -484,15 +456,6 @@ class LocalClient(object): >>> SLC.cmd_subset('*', 'test.ping', sub=1) {'jerry': True} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - minion_ret = self.cmd(tgt, 'sys.list_functions', tgt_type=tgt_type, @@ -547,15 +510,6 @@ class LocalClient(object): {'dave': {...}} {'stewart': {...}} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - import salt.cli.batch arg = salt.utils.args.condition_input(arg, kwarg) opts = {'tgt': tgt, @@ -705,15 +659,6 @@ class LocalClient(object): minion ID. A compound command will return a sub-dictionary keyed by function name. ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) was_listening = self.event.cpub @@ -774,15 +719,6 @@ class LocalClient(object): :param verbose: Print extra information about the running command :returns: A generator ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) was_listening = self.event.cpub @@ -861,15 +797,6 @@ class LocalClient(object): {'dave': {'ret': True}} {'stewart': {'ret': True}} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) was_listening = self.event.cpub @@ -937,15 +864,6 @@ class LocalClient(object): None {'stewart': {'ret': True}} ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) was_listening = self.event.cpub @@ -994,15 +912,6 @@ class LocalClient(object): ''' Execute a salt command and return ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - arg = salt.utils.args.condition_input(arg, kwarg) was_listening = self.event.cpub @@ -1045,15 +954,6 @@ class LocalClient(object): :returns: all of the information for the JID ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - if verbose: msg = 'Executing job with jid {0}'.format(jid) print(msg) @@ -1124,15 +1024,6 @@ class LocalClient(object): :returns: all of the information for the JID ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - if not isinstance(minions, set): if isinstance(minions, six.string_types): minions = set([minions]) @@ -1571,15 +1462,6 @@ class LocalClient(object): ''' log.trace('func get_cli_event_returns()') - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - if verbose: msg = 'Executing job with jid {0}'.format(jid) print(msg) @@ -1766,15 +1648,6 @@ class LocalClient(object): minions: A set, the targets that the tgt passed should match. ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - # Make sure the publisher is running by checking the unix socket if (self.opts.get('ipc_mode', '') != 'tcp' and not os.path.exists(os.path.join(self.opts['sock_dir'], @@ -1882,15 +1755,6 @@ class LocalClient(object): minions: A set, the targets that the tgt passed should match. ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - # Make sure the publisher is running by checking the unix socket if (self.opts.get('ipc_mode', '') != 'tcp' and not os.path.exists(os.path.join(self.opts['sock_dir'], diff --git a/salt/client/raet/__init__.py b/salt/client/raet/__init__.py index 38ab91bce4..d6606e9ba0 100644 --- a/salt/client/raet/__init__.py +++ b/salt/client/raet/__init__.py @@ -13,7 +13,6 @@ import logging import salt.config import salt.client import salt.utils.kinds as kinds -import salt.utils.versions import salt.syspaths as syspaths try: @@ -49,15 +48,6 @@ class LocalClient(salt.client.LocalClient): ''' Publish the command! ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - payload_kwargs = self._prep_pub( tgt, fun, diff --git a/salt/client/ssh/client.py b/salt/client/ssh/client.py index 994b1adf88..e8e634ca12 100644 --- a/salt/client/ssh/client.py +++ b/salt/client/ssh/client.py @@ -9,7 +9,7 @@ import random # Import Salt libs import salt.config -import salt.utils.versions +import salt.utils.args import salt.syspaths as syspaths from salt.exceptions import SaltClientError # Temporary @@ -52,15 +52,6 @@ class SSHClient(object): ''' Prepare the arguments ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - opts = copy.deepcopy(self.opts) opts.update(kwargs) if timeout: @@ -88,15 +79,6 @@ class SSHClient(object): .. versionadded:: 2015.5.0 ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - ssh = self._prep_ssh( tgt, fun, @@ -122,15 +104,6 @@ class SSHClient(object): .. versionadded:: 2015.5.0 ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - ssh = self._prep_ssh( tgt, fun, @@ -226,14 +199,6 @@ class SSHClient(object): .. versionadded:: 2017.7.0 ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') minion_ret = self.cmd(tgt, 'sys.list_functions', tgt_type=tgt_type, diff --git a/salt/client/ssh/wrapper/publish.py b/salt/client/ssh/wrapper/publish.py index 76a024382f..51e8264a4c 100644 --- a/salt/client/ssh/wrapper/publish.py +++ b/salt/client/ssh/wrapper/publish.py @@ -18,7 +18,6 @@ import logging import salt.client.ssh import salt.runner import salt.utils.args -import salt.utils.versions log = logging.getLogger(__name__) @@ -112,8 +111,7 @@ def publish(tgt, tgt_type='glob', returner='', timeout=5, - roster=None, - expr_form=None): + roster=None): ''' Publish a command "from the minion out to other minions". In reality, the minion does not execute this function, it is executed by the master. Thus, @@ -172,17 +170,6 @@ def publish(tgt, ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, @@ -199,8 +186,7 @@ def full_data(tgt, tgt_type='glob', returner='', timeout=5, - roster=None, - expr_form=None): + roster=None): ''' Return the full data about the publication, this is invoked in the same way as the publish function @@ -222,17 +208,6 @@ def full_data(tgt, salt-ssh '*' publish.full_data test.kwarg arg='cheese=spam' ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, diff --git a/salt/modules/match.py b/salt/modules/match.py index 21c6914521..ba7ed6428a 100644 --- a/salt/modules/match.py +++ b/salt/modules/match.py @@ -11,7 +11,6 @@ import sys # Import salt libs import salt.minion -import salt.utils.versions from salt.defaults import DEFAULT_TARGET_DELIM from salt.ext import six @@ -309,7 +308,6 @@ def glob(tgt, minion_id=None): def filter_by(lookup, tgt_type='compound', minion_id=None, - expr_form=None, default='default'): ''' Return the first match in a dictionary of target patterns @@ -335,17 +333,6 @@ def filter_by(lookup, # Make the filtered data available to Pillar: roles: {{ roles | yaml() }} ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - expr_funcs = dict(inspect.getmembers(sys.modules[__name__], predicate=inspect.isfunction)) diff --git a/salt/modules/mine.py b/salt/modules/mine.py index 5a184e9644..a9dbb793b0 100644 --- a/salt/modules/mine.py +++ b/salt/modules/mine.py @@ -16,7 +16,6 @@ import salt.payload import salt.utils.args import salt.utils.event import salt.utils.network -import salt.utils.versions from salt.exceptions import SaltClientError # Import 3rd-party libs @@ -242,8 +241,7 @@ def send(func, *args, **kwargs): def get(tgt, fun, tgt_type='glob', - exclude_minion=False, - expr_form=None): + exclude_minion=False): ''' Get data from the mine based on the target, function and tgt_type @@ -288,17 +286,6 @@ def get(tgt, fun='network.ip_addrs', tgt_type='glob') %} ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - if __opts__['file_client'] == 'local': ret = {} is_target = {'glob': __salt__['match.glob'], diff --git a/salt/modules/publish.py b/salt/modules/publish.py index 2de99583f4..f83791836a 100644 --- a/salt/modules/publish.py +++ b/salt/modules/publish.py @@ -13,7 +13,6 @@ import salt.crypt import salt.payload import salt.transport import salt.utils.args -import salt.utils.versions from salt.exceptions import SaltReqTimeoutError, SaltInvocationError log = logging.getLogger(__name__) @@ -183,8 +182,7 @@ def publish(tgt, tgt_type='glob', returner='', timeout=5, - via_master=None, - expr_form=None): + via_master=None): ''' Publish a command from the minion out to other minions. @@ -251,17 +249,6 @@ def publish(tgt, master the publication should be sent to. Only one master may be specified. If unset, the publication will be sent only to the first master in minion configuration. ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, @@ -278,8 +265,7 @@ def full_data(tgt, arg=None, tgt_type='glob', returner='', - timeout=5, - expr_form=None): + timeout=5): ''' Return the full data about the publication, this is invoked in the same way as the publish function @@ -301,17 +287,6 @@ def full_data(tgt, salt '*' publish.full_data test.kwarg arg='cheese=spam' ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, diff --git a/salt/modules/raet_publish.py b/salt/modules/raet_publish.py index 53e408f4d6..6c2f54df27 100644 --- a/salt/modules/raet_publish.py +++ b/salt/modules/raet_publish.py @@ -12,7 +12,6 @@ import logging import salt.payload import salt.transport import salt.utils.args -import salt.utils.versions from salt.exceptions import SaltReqTimeoutError # Import 3rd party libs @@ -111,8 +110,7 @@ def publish(tgt, arg=None, tgt_type='glob', returner='', - timeout=5, - expr_form=None): + timeout=5): ''' Publish a command from the minion out to other minions. @@ -167,17 +165,6 @@ def publish(tgt, ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, @@ -192,8 +179,7 @@ def full_data(tgt, arg=None, tgt_type='glob', returner='', - timeout=5, - expr_form=None): + timeout=5): ''' Return the full data about the publication, this is invoked in the same way as the publish function @@ -215,17 +201,6 @@ def full_data(tgt, salt '*' publish.full_data test.kwarg arg='cheese=spam' ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _publish(tgt, fun, arg=arg, diff --git a/salt/modules/saltutil.py b/salt/modules/saltutil.py index 880d4a5d4f..1978fdc8b6 100644 --- a/salt/modules/saltutil.py +++ b/salt/modules/saltutil.py @@ -55,7 +55,6 @@ import salt.utils.minion import salt.utils.path import salt.utils.process import salt.utils.url -import salt.utils.versions import salt.wheel HAS_PSUTIL = True @@ -1366,15 +1365,6 @@ def _get_ssh_or_api_client(cfgfile, ssh=False): def _exec(client, tgt, fun, arg, timeout, tgt_type, ret, kwarg, **kwargs): - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - fcn_ret = {} seen = 0 if 'batch' in kwargs: diff --git a/salt/runners/cache.py b/salt/runners/cache.py index 9f616c697f..49fcdeb445 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -15,7 +15,6 @@ import salt.log import salt.utils.args import salt.utils.gitfs import salt.utils.master -import salt.utils.versions import salt.payload import salt.cache import salt.fileserver.gitfs @@ -31,7 +30,7 @@ __func_alias__ = { } -def grains(tgt=None, tgt_type='glob', **kwargs): +def grains(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -45,15 +44,6 @@ def grains(tgt=None, tgt_type='glob', **kwargs): salt-run cache.grains ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - pillar_util = salt.utils.master.MasterPillarUtil(tgt, tgt_type, use_cached_grains=True, grains_fallback=False, @@ -62,7 +52,7 @@ def grains(tgt=None, tgt_type='glob', **kwargs): return cached_grains -def pillar(tgt=None, tgt_type='glob', **kwargs): +def pillar(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -76,15 +66,6 @@ def pillar(tgt=None, tgt_type='glob', **kwargs): salt-run cache.pillar ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - pillar_util = salt.utils.master.MasterPillarUtil(tgt, tgt_type, use_cached_grains=True, grains_fallback=False, @@ -95,7 +76,7 @@ def pillar(tgt=None, tgt_type='glob', **kwargs): return cached_pillar -def mine(tgt=None, tgt_type='glob', **kwargs): +def mine(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -109,15 +90,6 @@ def mine(tgt=None, tgt_type='glob', **kwargs): salt-run cache.mine ''' - if 'expr_form' in kwargs: - salt.utils.versions.warn_until( - 'Fluorine', - 'The target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = kwargs.pop('expr_form') - pillar_util = salt.utils.master.MasterPillarUtil(tgt, tgt_type, use_cached_grains=False, grains_fallback=False, @@ -151,7 +123,7 @@ def _clear_cache(tgt=None, clear_mine_func=clear_mine_func_flag) -def clear_pillar(tgt=None, tgt_type='glob', expr_form=None): +def clear_pillar(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -165,21 +137,10 @@ def clear_pillar(tgt=None, tgt_type='glob', expr_form=None): salt-run cache.clear_pillar ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _clear_cache(tgt, tgt_type, clear_pillar_flag=True) -def clear_grains(tgt=None, tgt_type='glob', expr_form=None): +def clear_grains(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -193,21 +154,10 @@ def clear_grains(tgt=None, tgt_type='glob', expr_form=None): salt-run cache.clear_grains ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _clear_cache(tgt, tgt_type, clear_grains_flag=True) -def clear_mine(tgt=None, tgt_type='glob', expr_form=None): +def clear_mine(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -221,24 +171,12 @@ def clear_mine(tgt=None, tgt_type='glob', expr_form=None): salt-run cache.clear_mine ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _clear_cache(tgt, tgt_type, clear_mine_flag=True) def clear_mine_func(tgt=None, tgt_type='glob', - clear_mine_func_flag=None, - expr_form=None): + clear_mine_func_flag=None): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -255,7 +193,7 @@ def clear_mine_func(tgt=None, return _clear_cache(tgt, tgt_type, clear_mine_func_flag=clear_mine_func_flag) -def clear_all(tgt=None, tgt_type='glob', expr_form=None): +def clear_all(tgt=None, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -269,17 +207,6 @@ def clear_all(tgt=None, tgt_type='glob', expr_form=None): salt-run cache.clear_all ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _clear_cache(tgt, tgt_type, clear_pillar_flag=True, diff --git a/salt/runners/manage.py b/salt/runners/manage.py index aec10b2f73..70ebc6afc0 100644 --- a/salt/runners/manage.py +++ b/salt/runners/manage.py @@ -26,7 +26,6 @@ import salt.utils.files import salt.utils.minions import salt.utils.path import salt.utils.raetevent -import salt.utils.versions import salt.client import salt.client.ssh import salt.wheel @@ -70,16 +69,7 @@ def _ping(tgt, tgt_type, timeout, gather_job_timeout): return returned, not_returned -def _warn_expr_form(): - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - - -def status(output=True, tgt='*', tgt_type='glob', expr_form=None, timeout=None, gather_job_timeout=None): +def status(output=True, tgt='*', tgt_type='glob', timeout=None, gather_job_timeout=None): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -95,12 +85,6 @@ def status(output=True, tgt='*', tgt_type='glob', expr_form=None, timeout=None, salt-run manage.status tgt="webservers" tgt_type="nodegroup" salt-run manage.status timeout=5 gather_job_timeout=10 ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - _warn_expr_form() - tgt_type = expr_form - ret = {} if not timeout: @@ -165,7 +149,7 @@ def key_regen(): return msg -def down(removekeys=False, tgt='*', tgt_type='glob', expr_form=None): +def down(removekeys=False, tgt='*', tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -183,12 +167,6 @@ def down(removekeys=False, tgt='*', tgt_type='glob', expr_form=None): salt-run manage.down tgt="webservers" tgt_type="nodegroup" ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - _warn_expr_form() - tgt_type = expr_form - ret = status(output=False, tgt=tgt, tgt_type=tgt_type).get('down', []) for minion in ret: if removekeys: @@ -197,7 +175,7 @@ def down(removekeys=False, tgt='*', tgt_type='glob', expr_form=None): return ret -def up(tgt='*', tgt_type='glob', expr_form=None, timeout=None, gather_job_timeout=None): # pylint: disable=C0103 +def up(tgt='*', tgt_type='glob', timeout=None, gather_job_timeout=None): # pylint: disable=C0103 ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -213,12 +191,6 @@ def up(tgt='*', tgt_type='glob', expr_form=None, timeout=None, gather_job_timeou salt-run manage.up tgt="webservers" tgt_type="nodegroup" salt-run manage.up timeout=5 gather_job_timeout=10 ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - _warn_expr_form() - tgt_type = expr_form - ret = status( output=False, tgt=tgt, @@ -598,7 +570,7 @@ def lane_stats(estate=None): return get_stats(estate=estate, stack='lane') -def safe_accept(target, tgt_type='glob', expr_form=None): +def safe_accept(target, tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -613,12 +585,6 @@ def safe_accept(target, tgt_type='glob', expr_form=None): salt-run manage.safe_accept my_minion salt-run manage.safe_accept minion1,minion2 tgt_type=list ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - _warn_expr_form() - tgt_type = expr_form - salt_key = salt.key.Key(__opts__) ssh_client = salt.client.ssh.client.SSHClient() diff --git a/salt/runners/ssh.py b/salt/runners/ssh.py index a39fc83f74..482f6c6769 100644 --- a/salt/runners/ssh.py +++ b/salt/runners/ssh.py @@ -10,7 +10,6 @@ from __future__ import absolute_import, print_function, unicode_literals # Import Salt Libs import salt.client.ssh.client -import salt.utils.versions def cmd(tgt, @@ -18,8 +17,7 @@ def cmd(tgt, arg=(), timeout=None, tgt_type='glob', - kwarg=None, - expr_form=None): + kwarg=None): ''' .. versionadded:: 2015.5.0 .. versionchanged:: 2017.7.0 @@ -32,17 +30,6 @@ def cmd(tgt, A wrapper around the :py:meth:`SSHClient.cmd ` method. ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - client = salt.client.ssh.client.SSHClient(mopts=__opts__) return client.cmd( tgt, diff --git a/salt/states/modjk_worker.py b/salt/states/modjk_worker.py index 4fd6c5a6a9..30a28c9314 100644 --- a/salt/states/modjk_worker.py +++ b/salt/states/modjk_worker.py @@ -19,7 +19,6 @@ Mandatory Settings: execution module :mod:`documentation ` ''' from __future__ import absolute_import, print_function, unicode_literals -import salt.utils.versions def __virtual__(): @@ -172,7 +171,7 @@ def _talk2modjk(name, lbn, target, action, profile='default', tgt_type='glob'): return ret -def stop(name, lbn, target, profile='default', tgt_type='glob', expr_form=None): +def stop(name, lbn, target, profile='default', tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -192,21 +191,10 @@ def stop(name, lbn, target, profile='default', tgt_type='glob', expr_form=None): - target: 'roles:balancer' - tgt_type: grain ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _talk2modjk(name, lbn, target, 'worker_stop', profile, tgt_type) -def activate(name, lbn, target, profile='default', tgt_type='glob', expr_form=None): +def activate(name, lbn, target, profile='default', tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -226,21 +214,10 @@ def activate(name, lbn, target, profile='default', tgt_type='glob', expr_form=No - target: 'roles:balancer' - tgt_type: grain ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _talk2modjk(name, lbn, target, 'worker_activate', profile, tgt_type) -def disable(name, lbn, target, profile='default', tgt_type='glob', expr_form=None): +def disable(name, lbn, target, profile='default', tgt_type='glob'): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -261,15 +238,4 @@ def disable(name, lbn, target, profile='default', tgt_type='glob', expr_form=Non - target: 'roles:balancer' - tgt_type: grain ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - return _talk2modjk(name, lbn, target, 'worker_disable', profile, tgt_type) diff --git a/salt/states/saltmod.py b/salt/states/saltmod.py index 6277a3360f..c51e62710f 100644 --- a/salt/states/saltmod.py +++ b/salt/states/saltmod.py @@ -36,7 +36,6 @@ import salt.exceptions import salt.output import salt.utils.data import salt.utils.event -import salt.utils.versions from salt.ext import six log = logging.getLogger(__name__) @@ -110,7 +109,6 @@ def state(name, tgt, ssh=False, tgt_type='glob', - expr_form=None, ret='', ret_config=None, ret_kwargs=None, @@ -148,10 +146,6 @@ def state(name, tgt_type The target type to resolve, defaults to ``glob`` - expr_form - .. deprecated:: 2017.7.0 - Use tgt_type instead - ret Optionally set a single or a list of returners to use @@ -271,17 +265,6 @@ def state(name, state_ret['comment'] = 'Passed invalid value for \'allow_fail\', must be an int' return state_ret - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - cmd_kw['tgt_type'] = tgt_type cmd_kw['ssh'] = ssh cmd_kw['expect_minions'] = expect_minions @@ -429,7 +412,6 @@ def function( tgt, ssh=False, tgt_type='glob', - expr_form=None, ret='', ret_config=None, ret_kwargs=None, @@ -453,10 +435,6 @@ def function( tgt_type The target type, defaults to ``glob`` - expr_form - .. deprecated:: 2017.7.0 - Use tgt_type instead - arg The list of arguments to pass into the function @@ -508,17 +486,6 @@ def function( cmd_kw = {'arg': arg or [], 'kwarg': kwarg, 'ret': ret, 'timeout': timeout} - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form - if batch is not None: cmd_kw['batch'] = six.text_type(batch) if subset is not None: diff --git a/salt/utils/master.py b/salt/utils/master.py index c78eafdbf4..0a6f1ee761 100644 --- a/salt/utils/master.py +++ b/salt/utils/master.py @@ -25,7 +25,6 @@ import salt.utils.minions import salt.utils.platform import salt.utils.stringutils import salt.utils.verify -import salt.utils.versions import salt.payload from salt.exceptions import SaltException import salt.config @@ -72,19 +71,7 @@ class MasterPillarUtil(object): use_cached_pillar=True, grains_fallback=True, pillar_fallback=True, - opts=None, - expr_form=None): - - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form + opts=None): log.debug('New instance of %s created.', self.__class__.__name__) diff --git a/salt/utils/minions.py b/salt/utils/minions.py index c3acc6ba90..f2c28174b7 100644 --- a/salt/utils/minions.py +++ b/salt/utils/minions.py @@ -718,21 +718,11 @@ class CkMinions(object): _res = self.check_minions(v_expr, v_matcher) return set(_res['minions']) - def validate_tgt(self, valid, expr, tgt_type, minions=None, expr_form=None): + def validate_tgt(self, valid, expr, tgt_type, minions=None): ''' Return a Bool. This function returns if the expression sent in is within the scope of the valid expression ''' - # remember to remove the expr_form argument from this function when - # performing the cleanup on this deprecation. - if expr_form is not None: - salt.utils.versions.warn_until( - 'Fluorine', - 'the target type should be passed using the \'tgt_type\' ' - 'argument instead of \'expr_form\'. Support for using ' - '\'expr_form\' will be removed in Salt Fluorine.' - ) - tgt_type = expr_form v_minions = self._expand_matching(valid) if minions is None: From 9e582c2240f14e1a15356d237eff55161e48dd7b Mon Sep 17 00:00:00 2001 From: denza Date: Mon, 26 Mar 2018 23:03:30 +0200 Subject: [PATCH 104/117] Adding a version tag --- doc/topics/cloud/profitbricks.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/topics/cloud/profitbricks.rst b/doc/topics/cloud/profitbricks.rst index 17beb06e31..3556b1b1c1 100644 --- a/doc/topics/cloud/profitbricks.rst +++ b/doc/topics/cloud/profitbricks.rst @@ -186,6 +186,7 @@ disk_type This option allow the disk type to be set to HDD or SSD. The default is HDD. +.. versionadded:: Fluorine image_password A password is set on the image for the "root" or "Administrator" account. This field may only be set during volume creation. Only valid with From 4d63a3fa086bd16afde28f92370b73adb2f0f913 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 27 Mar 2018 11:06:45 -0400 Subject: [PATCH 105/117] Add **kwargs back into mine function in cache runner This was removed during the deprecation removal process and shouldn't have been. --- salt/runners/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/runners/cache.py b/salt/runners/cache.py index 49fcdeb445..5d4e70deab 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -76,7 +76,7 @@ def pillar(tgt=None, tgt_type='glob'): return cached_pillar -def mine(tgt=None, tgt_type='glob'): +def mine(tgt=None, tgt_type='glob', **kwargs): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier From 3cd716b3496de858f901f2b5b88c23670a9d68f7 Mon Sep 17 00:00:00 2001 From: rallytime Date: Tue, 27 Mar 2018 11:18:23 -0400 Subject: [PATCH 106/117] Add example to release notes about tgt_type usage --- doc/topics/releases/fluorine.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/topics/releases/fluorine.rst b/doc/topics/releases/fluorine.rst index df47994859..2b93671dbe 100644 --- a/doc/topics/releases/fluorine.rst +++ b/doc/topics/releases/fluorine.rst @@ -40,6 +40,13 @@ Those who are using the :ref:`LocalClient ` (either directly, or implicitly via a :ref:`netapi module `) need to update their code to use ``tgt_type``. +.. code-block:: python + + >>> import salt.client + >>> local = salt.client.LocalClient() + >>> local.cmd('*', 'cmd.run', ['whoami'], tgt_type='glob') + {'jerry': 'root'} + Module Deprecations =================== From b70461f1b5764683876f20419866b647d5b7f975 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 12:50:07 +0000 Subject: [PATCH 107/117] Reuse code that actually runs the job --- salt/utils/schedule.py | 86 +++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 48 deletions(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 4cf2e928f1..b8b2fbb365 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -426,27 +426,8 @@ class Schedule(object): # Grab run, assume True run = data.get('run', True) - run_schedule_jobs_in_background = self.opts.get('run_schedule_jobs_in_background', True) if run: - if run_schedule_jobs_in_background: - multiprocessing_enabled = self.opts.get('multiprocessing', True) - if multiprocessing_enabled: - thread_cls = salt.utils.process.SignalHandlingMultiprocessingProcess - else: - thread_cls = threading.Thread - - if multiprocessing_enabled: - with salt.utils.process.default_signals(signal.SIGINT, signal.SIGTERM): - proc = thread_cls(target=self.handle_func, args=(multiprocessing_enabled, func, data)) - # Reset current signals before starting the process in - # order not to inherit the current signal handlers - proc.start() - proc.join() - else: - proc = thread_cls(target=self.handle_func, args=(multiprocessing_enabled, func, data)) - proc.start() - else: - func(data) + self._run_job(func, data) def enable_schedule(self): ''' @@ -1536,16 +1517,6 @@ class Schedule(object): miss_msg = ' (runtime missed ' \ 'by {0} seconds)'.format(abs(seconds)) - multiprocessing_enabled = self.opts.get('multiprocessing', True) - - if salt.utils.platform.is_windows(): - # Temporarily stash our function references. - # You can't pickle function references, and pickling is - # required when spawning new processes on Windows. - functions = self.functions - self.functions = {} - returners = self.returners - self.returners = {} try: # Job is disabled, continue if 'enabled' in data and not data['enabled']: @@ -1577,23 +1548,7 @@ class Schedule(object): if run: log.info('Running scheduled job: %s%s', job, miss_msg) - - if multiprocessing_enabled: - thread_cls = salt.utils.process.SignalHandlingMultiprocessingProcess - else: - thread_cls = threading.Thread - proc = thread_cls(target=self.handle_func, args=(multiprocessing_enabled, func, data)) - - if multiprocessing_enabled: - with salt.utils.process.default_signals(signal.SIGINT, signal.SIGTERM): - # Reset current signals before starting the process in - # order not to inherit the current signal handlers - proc.start() - else: - proc.start() - - if multiprocessing_enabled: - proc.join() + self._run_job(func, data) finally: # Only set _last_run if the job ran if run: @@ -1602,7 +1557,42 @@ class Schedule(object): data['_next_fire_time'] = now + datetime.timedelta(seconds=data['_seconds']) data['_splay'] = None - if salt.utils.platform.is_windows(): + def _run_job(self, func, data): + run_schedule_jobs_in_background = self.opts.get('run_schedule_jobs_in_background', True) + + if run_schedule_jobs_in_background is False: + func() + return + + multiprocessing_enabled = self.opts.get('multiprocessing', True) + + if multiprocessing_enabled and salt.utils.platform.is_windows(): + # Temporarily stash our function references. + # You can't pickle function references, and pickling is + # required when spawning new processes on Windows. + functions = self.functions + self.functions = {} + returners = self.returners + self.returners = {} + + try: + if multiprocessing_enabled: + thread_cls = salt.utils.process.SignalHandlingMultiprocessingProcess + else: + thread_cls = threading.Thread + + if multiprocessing_enabled: + with salt.utils.process.default_signals(signal.SIGINT, signal.SIGTERM): + proc = thread_cls(target=self.handle_func, args=(multiprocessing_enabled, func, data)) + # Reset current signals before starting the process in + # order not to inherit the current signal handlers + proc.start() + proc.join() + else: + proc = thread_cls(target=self.handle_func, args=(multiprocessing_enabled, func, data)) + proc.start() + finally: + if multiprocessing_enabled and salt.utils.platform.is_windows(): # Restore our function references. self.functions = functions self.returners = returners From 21dd4d574548b9549c11551d76b191bfa91979c5 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 12:58:07 +0000 Subject: [PATCH 108/117] Add support for job dry runs --- salt/utils/schedule.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index b8b2fbb365..0bb9da4ea3 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -1558,14 +1558,18 @@ class Schedule(object): data['_splay'] = None def _run_job(self, func, data): + job_dry_run = data.get('dry_run', False) + if job_dry_run: + log.debug('Job %s has \'dry_run\' set to True. Not running it.', data['name']) + return + + multiprocessing_enabled = self.opts.get('multiprocessing', True) run_schedule_jobs_in_background = self.opts.get('run_schedule_jobs_in_background', True) if run_schedule_jobs_in_background is False: func() return - multiprocessing_enabled = self.opts.get('multiprocessing', True) - if multiprocessing_enabled and salt.utils.platform.is_windows(): # Temporarily stash our function references. # You can't pickle function references, and pickling is From 6bf400047fc5bf80f2fd9469d793f6c337763894 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 17:07:46 +0000 Subject: [PATCH 109/117] We need to go through `handle_func` --- salt/utils/schedule.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 0bb9da4ea3..ebbac9cb37 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -1567,7 +1567,8 @@ class Schedule(object): run_schedule_jobs_in_background = self.opts.get('run_schedule_jobs_in_background', True) if run_schedule_jobs_in_background is False: - func() + # Explicitly pass False for multiprocessing_enabled + self.handle_func(False, func, data) return if multiprocessing_enabled and salt.utils.platform.is_windows(): From 19db8b248635746c8889b4d41c494a1676d28156 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 16:21:37 +0000 Subject: [PATCH 110/117] Allow getting a new schedule instance and not the singleton instance. --- salt/utils/schedule.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index ebbac9cb37..4a4e0a11e3 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -75,23 +75,38 @@ class Schedule(object): ''' instance = None - def __new__(cls, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None, standalone=False): + def __new__(cls, opts, functions, + returners=None, + intervals=None, + cleanup=None, + proxy=None, + standalone=False, + new_instance=False): ''' Only create one instance of Schedule ''' - if cls.instance is None: + if cls.instance is None or new_instance is True: log.debug('Initializing new Schedule') # we need to make a local variable for this, as we are going to store # it in a WeakValueDictionary-- which will remove the item if no one # references it-- this forces a reference while we return to the caller - cls.instance = object.__new__(cls) - cls.instance.__singleton_init__(opts, functions, returners, intervals, cleanup, proxy, standalone) + instance = object.__new__(cls) + instance.__singleton_init__(opts, functions, returners, intervals, cleanup, proxy, standalone) + if new_instance is True: + return instance + cls.instance = instance else: log.debug('Re-using Schedule') return cls.instance # has to remain empty for singletons, since __init__ will *always* be called - def __init__(self, opts, functions, returners=None, intervals=None, cleanup=None, proxy=None, standalone=False): + def __init__(self, opts, functions, + returners=None, + intervals=None, + cleanup=None, + proxy=None, + standalone=False, + new_instance=False): pass # an init for the singleton instance to call From 1478316a0d4934cf9c4a78dca636aa700557ef8b Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 16:55:25 +0000 Subject: [PATCH 111/117] Don't change the process name if not on a new process --- salt/utils/schedule.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index 4a4e0a11e3..f7e0f12138 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -616,7 +616,9 @@ class Schedule(object): log.warning('schedule: The metadata parameter must be ' 'specified as a dictionary. Ignoring.') - salt.utils.process.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) + if multiprocessing_enabled: + # We just want to modify the process name if we're on a different process + salt.utils.process.appendproctitle('{0} {1}'.format(self.__class__.__name__, ret['jid'])) if not self.standalone: proc_fn = os.path.join( From 81e851e290dd2ac61f86d1e2a96e9b38ebfd34e1 Mon Sep 17 00:00:00 2001 From: Pedro Algarvio Date: Thu, 22 Mar 2018 16:31:17 +0000 Subject: [PATCH 112/117] Fix inversed logic --- salt/utils/schedule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/utils/schedule.py b/salt/utils/schedule.py index f7e0f12138..e450543bdc 100644 --- a/salt/utils/schedule.py +++ b/salt/utils/schedule.py @@ -1555,7 +1555,7 @@ class Schedule(object): 'job %s, defaulting to 1.', job) data['maxrunning'] = 1 - if self.standalone: + if not self.standalone: data['run'] = run data = self._check_max_running(func, data, From 734f5a77d414eedad6130214c97447f3015dd7e9 Mon Sep 17 00:00:00 2001 From: denza Date: Wed, 28 Mar 2018 05:26:41 +0200 Subject: [PATCH 113/117] pylint error fix --- salt/cloud/clouds/profitbricks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/salt/cloud/clouds/profitbricks.py b/salt/cloud/clouds/profitbricks.py index a9f720db34..6fbe61beb3 100644 --- a/salt/cloud/clouds/profitbricks.py +++ b/salt/cloud/clouds/profitbricks.py @@ -1130,7 +1130,7 @@ def _get_system_volume(vm_): if 'image_password' in vm_: image_password = vm_['image_password'] volume.image_password = image_password - + # Retrieve list of SSH public keys ssh_keys = get_public_keys(vm_) volume.ssh_keys = ssh_keys From c6116a83aa4f58c2adbe97c314212ab83196d8cd Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 28 Mar 2018 09:58:13 -0400 Subject: [PATCH 114/117] Add **kwargs back into grains and pillar funcs in cache runner This was removed during the deprecation removal process and shouldn't have been. --- salt/runners/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/salt/runners/cache.py b/salt/runners/cache.py index 5d4e70deab..0f6167b00b 100644 --- a/salt/runners/cache.py +++ b/salt/runners/cache.py @@ -30,7 +30,7 @@ __func_alias__ = { } -def grains(tgt=None, tgt_type='glob'): +def grains(tgt=None, tgt_type='glob', **kwargs): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier @@ -52,7 +52,7 @@ def grains(tgt=None, tgt_type='glob'): return cached_grains -def pillar(tgt=None, tgt_type='glob'): +def pillar(tgt=None, tgt_type='glob', **kwargs): ''' .. versionchanged:: 2017.7.0 The ``expr_form`` argument has been renamed to ``tgt_type``, earlier From 81211f07fda558579ef0dd54fb96c72bd595358d Mon Sep 17 00:00:00 2001 From: rallytime Date: Wed, 28 Mar 2018 10:14:20 -0400 Subject: [PATCH 115/117] Remove 2 tests from core grains unit tests These tests were moved to tests/unit/grains/test_iscsi.py in PR #46530 During the merge-forward commit, the tests were inadvertently added back into the test_core.py file during the merge-conflict resolution. --- tests/unit/grains/test_core.py | 27 --------------------------- 1 file changed, 27 deletions(-) diff --git a/tests/unit/grains/test_core.py b/tests/unit/grains/test_core.py index 64fc382647..4b153c0e6d 100644 --- a/tests/unit/grains/test_core.py +++ b/tests/unit/grains/test_core.py @@ -893,30 +893,3 @@ SwapTotal: 4789244 kB''' with patch.object(socket, 'gethostbyaddr', side_effect=reverse_resolv_mock): fqdns = core.fqdns() self.assertEqual(fqdns, ret) - - @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.EPERM, - 'The cables are not the same length.'))) - @patch('salt.grains.core.log', MagicMock()) - def test_linux_iqn_non_root(self): - ''' - Test if linux_iqn is running on salt-master as non-root - and handling access denial properly. - :return: - ''' - assert core._linux_iqn() == [] - core.log.debug.assert_called() - assert 'Error while accessing' in core.log.debug.call_args[0][0] - assert 'cables are not the same' in core.log.debug.call_args[0][2].strerror - assert core.log.debug.call_args[0][2].errno == os.errno.EPERM - assert core.log.debug.call_args[0][1] == '/etc/iscsi/initiatorname.iscsi' - - @patch('salt.utils.files.fopen', MagicMock(side_effect=IOError(os.errno.ENOENT, ''))) - @patch('salt.grains.core.log', MagicMock()) - def test_linux_iqn_no_iscsii_initiator(self): - ''' - Test if linux_iqn is running on salt-master as root. - iscsii initiator is not there accessible or is not supported. - :return: - ''' - assert core._linux_iqn() == [] - core.log.debug.assert_not_called() From ac1d943394b61a5a1d1390f555f5108edb7da81f Mon Sep 17 00:00:00 2001 From: Flavio Castelli Date: Wed, 28 Mar 2018 18:53:34 +0200 Subject: [PATCH 116/117] Fix 'virtual_subtype' grain for kubernetes and libpod containers All the containers started by kubelet (hence kubernetes) are assigned to the `kubepods` cgroup slice (see [1]). That happens despite of the Container Runtime Interface being used. Containers started via tools using `libpod` [2] (eg: podman) are assigned to the `libpod_parent` cgroup slice. [1] https://github.com/kubernetes/community/blob/master/contributors/design-proposals/node/node-allocatable.md#recommended-cgroups-setup [2] https://github.com/projectatomic/libpod Signed-off-by: Flavio Castelli --- salt/grains/core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/salt/grains/core.py b/salt/grains/core.py index b6f6b28aea..595eebd3da 100644 --- a/salt/grains/core.py +++ b/salt/grains/core.py @@ -821,6 +821,10 @@ def _virtual(osdata): fhr_contents = fhr.read() if ':/lxc/' in fhr_contents: grains['virtual_subtype'] = 'LXC' + elif ':/kubepods/' in fhr_contents: + grains['virtual_subtype'] = 'kubernetes' + elif ':/libpod_parent/' in fhr_contents: + grains['virtual_subtype'] = 'libpod' else: if any(x in fhr_contents for x in (':/system.slice/docker', ':/docker/', From 78c9cb958445d1ec5ac9e7d5289c498c48ecff4d Mon Sep 17 00:00:00 2001 From: "Daniel A. Wozniak" Date: Wed, 28 Mar 2018 21:09:59 -0700 Subject: [PATCH 117/117] Fix TestResult constructor calls --- tests/support/parser/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/support/parser/__init__.py b/tests/support/parser/__init__.py index d63066e776..dd601241a2 100644 --- a/tests/support/parser/__init__.py +++ b/tests/support/parser/__init__.py @@ -530,11 +530,11 @@ class SaltTestingParser(optparse.OptionParser): skipped = [] failures = [] for testcase, reason in runner.errors: - errors.append(TestResult(testcase.id(), reason, failfast=failfast)) + errors.append(TestResult(testcase.id(), reason)) for testcase, reason in runner.skipped: - skipped.append(TestResult(testcase.id(), reason, failfast=failfast)) + skipped.append(TestResult(testcase.id(), reason)) for testcase, reason in runner.failures: - failures.append(TestResult(testcase.id(), reason, failfast=failfast)) + failures.append(TestResult(testcase.id(), reason)) self.testsuite_results.append( TestsuiteResult(header, errors,