saltstack/salt

View on GitHub
salt/modules/win_psget.py

Summary

Maintainability
C
1 day
Test Coverage
# -*- coding: utf-8 -*-
'''
Module for managing PowerShell through PowerShellGet (PSGet)

:depends:
    - PowerShell 5.0
    - PSGet

Support for PowerShell
'''
from __future__ import absolute_import, print_function, unicode_literals

# Import Python libs
import logging
import xml.etree.ElementTree

# Import Salt libs
import salt.utils.platform
import salt.utils.versions
from salt.exceptions import CommandExecutionError

# Set up logging
log = logging.getLogger(__name__)

# Define the module's virtual name
__virtualname__ = 'psget'


def __virtual__():
    '''
    Set the system module of the kernel is Windows
    '''
    # Verify Windows
    if not salt.utils.platform.is_windows():
        log.debug('Module PSGet: Only available on Windows systems')
        return False, 'Module PSGet: Only available on Windows systems'

    # Verify PowerShell
    powershell_info = __salt__['cmd.shell_info']('powershell')
    if not powershell_info['installed']:
        log.debug('Module PSGet: Requires PowerShell')
        return False, 'Module PSGet: Requires PowerShell'

    # Verify PowerShell 5.0 or greater
    if salt.utils.versions.compare(powershell_info['version'], '<', '5.0'):
        log.debug('Module PSGet: Requires PowerShell 5 or newer')
        return False, 'Module PSGet: Requires PowerShell 5 or newer.'

    return __virtualname__


def _ps_xml_to_dict(parent, dic=None):
    '''
    Formats powershell Xml to a dict.
    Note: This _ps_xml_to_dict is not perfect with powershell Xml.
    '''

    if dic is None:
        dic = {}

    for child in parent:
        if list(child):
            new_dic = _ps_xml_to_dict(child, {})
            if "Name" in new_dic:
                dic[new_dic["Name"]] = new_dic
            else:
                try:
                    dic[[name for ps_type, name in child.items() if ps_type == "Type"][0]] = new_dic
                except IndexError:
                    dic[child.text] = new_dic
        else:
            for xml_type, name in child.items():
                if xml_type == "Name":
                    dic[name] = child.text

    return dic


def _pshell(cmd, cwd=None, depth=2):
    '''
    Execute the desired powershell command and ensure that it returns data
    in Xml format and load that into python
    '''

    cmd = '{0} | ConvertTo-Xml -Depth {1} -As \"stream\"'.format(cmd, depth)
    log.debug('DSC: %s', cmd)

    results = __salt__['cmd.run_all'](cmd, shell='powershell', cwd=cwd, python_shell=True)

    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('Issue executing powershell {0}'.format(cmd), info=results)

    try:
        ret = _ps_xml_to_dict(xml.etree.ElementTree.fromstring(results['stdout'].encode('utf-8')))
    except xml.etree.ElementTree.ParseError:
        results['stdout'] = results['stdout'][:1000] + ". . ."
        raise CommandExecutionError('No XML results from powershell', info=results)

    return ret


def bootstrap():
    '''
    Make sure that nuget-anycpu.exe is installed.
    This will download the official nuget-anycpu.exe from the internet.

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.bootstrap
    '''
    cmd = 'Get-PackageProvider -Name NuGet -ForceBootstrap | Select Name, Version, ProviderPath'
    ret = _pshell(cmd, depth=1)
    return ret


def avail_modules(desc=False):
    '''
    List available modules in registered Powershell module repositories.

    :param desc: If ``True``, the verbose description will be returned.
    :type  desc: ``bool``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.avail_modules
        salt 'win01' psget.avail_modules desc=True
    '''
    cmd = 'Find-Module | Select Name, Description'
    modules = _pshell(cmd, depth=1)
    names = []
    if desc:
        names = {}
    for key in modules:
        module = modules[key]
        if desc:
            names[module['Name']] = module['Description']
            continue
        names.append(module['Name'])
    return names


def list_modules(desc=False):
    '''
    List currently installed PSGet Modules on the system.

    :param desc: If ``True``, the verbose description will be returned.
    :type  desc: ``bool``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.list_modules
        salt 'win01' psget.list_modules desc=True
    '''
    cmd = 'Get-InstalledModule'
    modules = _pshell(cmd)
    names = []
    if desc:
        names = {}
    for key in modules:
        module = modules[key]
        if desc:
            names[module['Name']] = module
            continue
        names.append(module['Name'])
    return names


def install(name, minimum_version=None, required_version=None, scope=None,
            repository=None):
    '''
    Install a Powershell module from powershell gallery on the system.

    :param name: Name of a Powershell module
    :type  name: ``str``

    :param minimum_version: The maximum version to install, e.g. 1.23.2
    :type  minimum_version: ``str``

    :param required_version: Install a specific version
    :type  required_version: ``str``

    :param scope: The scope to install the module to, e.g. CurrentUser, Computer
    :type  scope: ``str``

    :param repository: The friendly name of a private repository, e.g. MyREpo
    :type  repository: ``str``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.install PowerPlan
    '''
    # Putting quotes around the parameter protects against command injection
    flags = [('Name', name)]

    if minimum_version is not None:
        flags.append(('MinimumVersion', minimum_version))
    if required_version is not None:
        flags.append(('RequiredVersion', required_version))
    if scope is not None:
        flags.append(('Scope', scope))
    if repository is not None:
        flags.append(('Repository', repository))
    params = ''
    for flag, value in flags:
        params += '-{0} {1} '.format(flag, value)
    cmd = 'Install-Module {0} -Force'.format(params)
    _pshell(cmd)
    return name in list_modules()


def update(name, maximum_version=None, required_version=None):
    '''
    Update a PowerShell module to a specific version, or the newest

    :param name: Name of a Powershell module
    :type  name: ``str``

    :param maximum_version: The maximum version to install, e.g. 1.23.2
    :type  maximum_version: ``str``

    :param required_version: Install a specific version
    :type  required_version: ``str``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.update PowerPlan
    '''
    # Putting quotes around the parameter protects against command injection
    flags = [('Name', name)]

    if maximum_version is not None:
        flags.append(('MaximumVersion', maximum_version))
    if required_version is not None:
        flags.append(('RequiredVersion', required_version))

    params = ''
    for flag, value in flags:
        params += '-{0} {1} '.format(flag, value)
    cmd = 'Update-Module {0} -Force'.format(params)
    _pshell(cmd)
    return name in list_modules()


def remove(name):
    '''
    Remove a Powershell DSC module from the system.

    :param  name: Name of a Powershell DSC module
    :type   name: ``str``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.remove PowerPlan
    '''
    # Putting quotes around the parameter protects against command injection
    cmd = 'Uninstall-Module "{0}"'.format(name)
    no_ret = _pshell(cmd)
    return name not in list_modules()


def register_repository(name, location, installation_policy=None):
    '''
    Register a PSGet repository on the local machine

    :param name: The name for the repository
    :type  name: ``str``

    :param location: The URI for the repository
    :type  location: ``str``

    :param installation_policy: The installation policy
        for packages, e.g. Trusted, Untrusted
    :type  installation_policy: ``str``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.register_repository MyRepo https://myrepo.mycompany.com/packages
    '''
    # Putting quotes around the parameter protects against command injection
    flags = [('Name', name)]

    flags.append(('SourceLocation', location))
    if installation_policy is not None:
        flags.append(('InstallationPolicy', installation_policy))

    params = ''
    for flag, value in flags:
        params += '-{0} {1} '.format(flag, value)
    cmd = 'Register-PSRepository {0}'.format(params)
    no_ret = _pshell(cmd)
    return name not in list_modules()


def get_repository(name):
    '''
    Get the details of a local PSGet repository

    :param  name: Name of the repository
    :type   name: ``str``

    CLI Example:

    .. code-block:: bash

        salt 'win01' psget.get_repository MyRepo
    '''
    # Putting quotes around the parameter protects against command injection
    cmd = 'Get-PSRepository "{0}"'.format(name)
    no_ret = _pshell(cmd)
    return name not in list_modules()