­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ """ Module for working with Windows PowerShell DSC (Desired State Configuration) This module is Alpha This module applies DSC Configurations in the form of PowerShell scripts or MOF (Managed Object Format) schema files. Use the ``psget`` module to manage PowerShell resources. The idea is to leverage Salt to push DSC configuration scripts or MOF files to the Minion. :depends: - PowerShell 5.0 """ import logging import os import salt.utils.json import salt.utils.platform import salt.utils.versions from salt.exceptions import CommandExecutionError, SaltInvocationError # Set up logging log = logging.getLogger(__name__) # Define the module's virtual name __virtualname__ = "dsc" def __virtual__(): """ Set the system module of the kernel is Windows """ # Verify Windows if not salt.utils.platform.is_windows(): 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("DSC: Requires PowerShell") return False, "DSC: Requires PowerShell" # Verify PowerShell 5.0 or greater if salt.utils.versions.compare(powershell_info["version"], "<", "5.0"): 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, ignore_retcode=False): """ Execute the desired PowerShell command and ensure that it returns data in json format and load that into python. Either return a dict or raise a CommandExecutionError. """ if "ConvertTo-Json" not in cmd.lower(): cmd = f"{cmd} | ConvertTo-Json -Depth {json_depth}" log.debug("DSC: %s", cmd) results = __salt__["cmd.run_all"]( cmd, shell="powershell", cwd=cwd, python_shell=True, ignore_retcode=ignore_retcode, ) if "pid" in results: del results["pid"] if "retcode" not in results or results["retcode"] != 0: # run_all logs an error to log.error, fail hard back to the user raise CommandExecutionError(f"Issue executing PowerShell {cmd}", info=results) # Sometimes Powershell returns an empty string, which isn't valid JSON if results["stdout"] == "": results["stdout"] = "{}" try: ret = salt.utils.json.loads(results["stdout"], strict=False) except ValueError: raise CommandExecutionError("No JSON results from PowerShell", info=results) log.info('DSC: Returning "%s"', ret) return ret def run_config( path, source=None, config_name=None, config_data=None, config_data_source=None, script_parameters=None, salt_env="base", ): r""" Compile a DSC Configuration in the form of a PowerShell script (.ps1) and apply it. The PowerShell script can be cached from the master using the ``source`` option. If there is more than one config within the PowerShell 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`` followed by ``dsc.apply_config``. Args: path (str): The local path to the PowerShell script that contains the DSC Configuration. Required. source (:obj:`str`, optional): The path to the script on ``file_roots`` to cache at the location specified by ``path``. The source file will be cached locally and then executed. If source is not passed, the config script located at ``path`` will be compiled. Default is ``None``. config_name (:obj:`str`, optional): The name of the Configuration within the script to apply. If the script contains multiple configurations within the file a ``config_name`` must be specified. If the ``config_name`` is not specified, the name of the file will be used as the ``config_name`` to run. Default is ``None``. config_data (:obj:`str`, optional): Configuration data in the form of a hash table that will be passed to the ``ConfigurationData`` parameter when the ``config_name`` is compiled. This can be the path to a ``.psd1`` file containing the proper hash table or the PowerShell code to create the hash table. Default is ``None``. .. versionadded:: 2017.7.0 config_data_source (:obj:`str`, optional): The path to the ``.psd1`` file on ``file_roots`` to cache at the location specified by ``config_data``. If this is specified, ``config_data`` must be a local path instead of a hash table. Default is ``None``. .. versionadded:: 2017.7.0 script_parameters (:obj:`str`, optional): Any additional parameters expected by the configuration script. These must be defined in the script itself. Note that these are passed to the script (the outermost scope), and not to the dsc configuration inside the script (the inner scope). Default is ``None``. .. versionadded:: 2017.7.0 salt_env (:obj:`str`, optional): The salt environment to use when copying the source. Default is ``base``. Returns: bool: ``True`` if successfully compiled and applied, otherwise ``False`` CLI Example: .. code-block:: bash # To compile a config from a script that already exists on the system salt '*' dsc.run_config 'C:\\DSC\\WebsiteConfig.ps1' .. code-block:: bash # To cache a config script to the system from the master and compile it salt '*' dsc.run_config 'C:\\DSC\\WebsiteConfig.ps1' salt://dsc/configs/WebsiteConfig.ps1 .. code-block:: bash # To cache a config script to the system from the master and compile it, passing in `script_parameters`: salt '*' dsc.run_config path='C:\\DSC\\WebsiteConfig.ps1' source=salt://dsc/configs/WebsiteConfig.ps1 script_parameters='-hostname "my-computer" -ip "192.168.1.10" -DnsArray "192.168.1.3","192.168.1.4","1.1.1.1"' """ ret = compile_config( path=path, source=source, config_name=config_name, config_data=config_data, config_data_source=config_data_source, script_parameters=script_parameters, salt_env=salt_env, ) if ret.get("Exists"): config_path = os.path.dirname(ret["FullName"]) return apply_config(config_path) else: return False def compile_config( path, source=None, config_name=None, config_data=None, config_data_source=None, script_parameters=None, salt_env="base", ): r""" Compile a config from a PowerShell script (``.ps1``) Args: path (str): Path (local) to the script that will create the ``.mof`` configuration file. If no source is passed, the file must exist locally. source (:obj:`str`, optional): Path to the script on ``file_roots`` to cache at the location specified by ``path``. The source file will be cached locally and then executed. If source is not passed, the config script located at ``path`` will be compiled. Default is ``None``. config_name (:obj:`str`, optional): The name of the Configuration within the script to apply. If the script contains multiple configurations within the file a ``config_name`` must be specified. If the ``config_name`` is not specified, the name of the file will be used as the ``config_name`` to run. Default is ``None``. config_data (:obj:`str`, optional): Configuration data in the form of a hash table that will be passed to the ``ConfigurationData`` parameter when the ``config_name`` is compiled. This can be the path to a ``.psd1`` file containing the proper hash table or the PowerShell code to create the hash table. Default is ``None``. .. versionadded:: 2017.7.0 config_data_source (:obj:`str`, optional): The path to the ``.psd1`` file on ``file_roots`` to cache at the location specified by ``config_data``. If this is specified, ``config_data`` must be a local path instead of a hash table. Default is ``None``. .. versionadded:: 2017.7.0 script_parameters (:obj:`str`, optional): Any additional parameters expected by the configuration script. These must be defined in the script itself. Default is ``None``. .. versionadded:: 2017.7.0 salt_env (Optional[str]): salt_env (Optional[str]): The salt environment to use when copying the source. Default is ``base``. Returns: dict: A dictionary containing the results of the compilation CLI Example: .. code-block:: bash # To compile a config from a script that already exists on the system salt '*' dsc.compile_config 'C:\\DSC\\WebsiteConfig.ps1' .. code-block:: bash # To cache a config script to the system from the master and compile it: salt '*' dsc.compile_config 'C:\\DSC\\WebsiteConfig.ps1' salt://dsc/configs/WebsiteConfig.ps1 """ if source: log.info("DSC: Caching %s", source) cached_files = __salt__["cp.get_file"]( path=source, dest=path, saltenv=salt_env, makedirs=True ) if not cached_files: error = f"Failed to cache {source}" log.error("DSC: %s", error) raise CommandExecutionError(error) if config_data_source: log.info("DSC: Caching %s", 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 = f"Failed to cache {config_data_source}" log.error("DSC: %s", error) raise CommandExecutionError(error) # Make sure the path exists if not os.path.exists(path): error = f"{path} not found" log.error("DSC: %s", error) raise CommandExecutionError(error) if config_name is None: # If the name of the config isn't passed, make it the name of the .ps1 config_name = os.path.splitext(os.path.basename(path))[0] cwd = os.path.dirname(path) # Run the script and see if the compile command is in the script cmd = [path] # Add any script parameters if script_parameters: cmd.append(script_parameters) # Select properties of the generated .mof file to return, avoiding the .meta.mof cmd.append( r"| Where-Object FullName -match '(?