From 055fd6f5f58a5a1c3afada533b71219c6e1e80a8 Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Thu, 1 Jun 2017 23:50:12 +0200 Subject: [PATCH] New module: win dsc (#24872) * Added win_dsc module file * mute output and track reboot requirements * added tests * proper conditionals for test * Added moar conditionals for test * ci fixes * Added metadata * fixed integration test yaml * ci fix * ci fix * added module_version param and output, no longer chokes on multiple versions found. * ci fix * code review improvements, make return vars more pythonic, cleanup removed reference to handles in commit message * Fixed tests, clearer documentation * fixed trailing whitespace --- lib/ansible/modules/windows/win_dsc.ps1 | 241 ++++++++++++++++++ lib/ansible/modules/windows/win_dsc.py | 110 ++++++++ test/integration/targets/win_dsc/aliases | 1 + .../targets/win_dsc/defaults/main.yml | 4 + .../targets/win_dsc/tasks/main.yml | 119 +++++++++ 5 files changed, 475 insertions(+) create mode 100644 lib/ansible/modules/windows/win_dsc.ps1 create mode 100644 lib/ansible/modules/windows/win_dsc.py create mode 100644 test/integration/targets/win_dsc/aliases create mode 100644 test/integration/targets/win_dsc/defaults/main.yml create mode 100644 test/integration/targets/win_dsc/tasks/main.yml diff --git a/lib/ansible/modules/windows/win_dsc.ps1 b/lib/ansible/modules/windows/win_dsc.ps1 new file mode 100644 index 00000000000..3a6e7d52e22 --- /dev/null +++ b/lib/ansible/modules/windows/win_dsc.ps1 @@ -0,0 +1,241 @@ +#!powershell +# (c) 2015, Trond Hindenes , and others +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# WANT_JSON +# POWERSHELL_COMMON + +#Temporary fix +#Set-StrictMode -Off + +$params = Parse-Args $args -supports_check_mode $true +$result = @{ + changed = $false +} + +#Check that we're on at least Powershell version 5 +if ($PSVersionTable.PSVersion.Major -lt 5) +{ + Fail-Json -obj $Result -message "This module only runs on Powershell version 5 or higher" +} +$check_mode = Get-AnsibleParam -obj $params -name "_ansible_check_mode" -type "bool" -default $false +$resourcename = Get-AnsibleParam -obj $params -name "resource_name" -type "str" -failifempty $true -resultobj $result +$module_version = Get-AnsibleParam -obj $params -name "module_version" -type "str" -default "latest" + +#From Ansible 2.3 onwards, params is now a Hash Array +$Attributes = $params.GetEnumerator() | where {$_.key -ne "resource_name"} | where {$_.key -notlike "_ansible_*"} + +if (!($Attributes)) +{ + Fail-Json -obj $result -message "No attributes specified" +} + +#Always return some basic info +$result["resource_name"] = $resourcename +$result["attributes"] = $Attributes +$result["reboot_required"] = $null + + +# Build Attributes Hashtable for DSC Resource Propertys +$Attrib = @{} +foreach ($key in $Attributes) +{ + $result[$key.name] = $key.value + $Attrib.Add($Key.Key,$Key.Value) +} + +$result["dsc_attributes"] = $attrib + +$Config = @{ + Name = ($resourcename) + Property = @{ + } + } + +#Get the latest version of the module +if ($module_version -eq "latest") +{ + $Resource = Get-DscResource -Name $resourcename -ErrorAction SilentlyContinue | sort Version | select -Last 1 +} +else +{ + $Resource = Get-DscResource -Name $resourcename -ErrorAction SilentlyContinue | where {$_.Version -eq $module_version} +} + +if (!$Resource) +{ + if ($module_version -eq "latest") + { + Fail-Json -obj $result -message "Resource $resourcename not found" + } + else + { + Fail-Json -obj $result -message "Resource $resourcename with version $module_version not found" + } + +} + +#Get the Module that provides the resource. Will be used as +#mandatory argument for Invoke-DscResource +$Module = $Resource.ModuleName +$result["module_version"] = $Module.Version.ToString() + +#Convert params to correct datatype and inject +$attrib.Keys | foreach-object { + $Key = $_.replace("item_name", "name") + $prop = $resource.Properties | where {$_.Name -eq $key} + if (!$prop) + { + #If its a credential specified as "credential", Ansible will support credential_username and credential_password. Need to check for that + $prop = $resource.Properties | where {$_.Name -eq $key.Replace("_username","")} + if ($prop) + { + #We need to construct a cred object. At this point keyvalue is the username, so grab the password + $PropUserNameValue = $attrib.Item($_) + $PropPassword = $key.Replace("_username","_password") + $PropPasswordValue = $attrib.$PropPassword + + $cred = New-Object System.Management.Automation.PSCredential ($PropUserNameValue, ($PropPasswordValue | ConvertTo-SecureString -AsPlainText -Force)) + [System.Management.Automation.PSCredential]$KeyValue = $cred + $config.Property.Add($key.Replace("_username",""),$KeyValue) + } + ElseIf ($key.Contains("_password")) + { + #Do nothing. We suck in the password in the handler for _username, so we can just skip it. + } + Else + { + Fail-Json -obj $result -message "Property $key in resource $resourcename is not a valid property" + } + + } + ElseIf ($prop.PropertyType -eq "[string]") + { + [String]$KeyValue = $attrib.Item($_) + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[string[]]") + { + #KeyValue is an array of strings + [String]$TempKeyValue = $attrib.Item($_) + [String[]]$KeyValue = $TempKeyValue.Split(",").Trim() + + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[UInt32[]]") + { + #KeyValue is an array of integers + [String]$TempKeyValue = $attrib.Item($_) + [UInt32[]]$KeyValue = $attrib.Item($_.split(",").Trim()) + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[bool]") + { + if ($attrib.Item($_) -like "true") + { + [bool]$KeyValue = $true + } + ElseIf ($attrib.Item($_) -like "false") + { + [bool]$KeyValue = $false + } + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[int]") + { + [int]$KeyValue = $attrib.Item($_) + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[CimInstance[]]") + { + #KeyValue is an array of CimInstance + [CimInstance[]]$KeyVal = @() + [String]$TempKeyValue = $attrib.Item($_) + #Need to split on the string }, because some property values have commas in them + [String[]]$KeyValueStr = $TempKeyValue -split("},") + #Go through each string of properties and create a hash of them + foreach($str in $KeyValueStr) + { + [string[]]$properties = $str.Split("{")[1].Replace("}","").Trim().Split([environment]::NewLine).Trim() + $prph = @{} + foreach($p in $properties) + { + $pArr = $p -split "=" + #if the value can be an int we must convert it to an int + if([bool]($pArr[1] -as [int] -is [int])) + { + $prph.Add($pArr[0].Trim(),$pArr[1].Trim() -as [int]) + } + else + { + $prph.Add($pArr[0].Trim(),$pArr[1].Trim()) + } + } + #create the new CimInstance + $cim = New-CimInstance -ClassName $str.Split("{")[0].Trim() -Property $prph -ClientOnly + #add the new CimInstance to the array + $KeyVal += $cim + } + $config.Property.Add($key,$KeyVal) + } + ElseIf ($prop.PropertyType -eq "[Int32]") + { + # Add Supoort for Int32 + [int]$KeyValue = $attrib.Item($_) + $config.Property.Add($key,$KeyValue) + } + ElseIf ($prop.PropertyType -eq "[UInt32]") + { + # Add Support for [UInt32] + [UInt32]$KeyValue = $attrib.Item($_) + $config.Property.Add($key,$KeyValue) + } + + } + +try +{ + #Defined variables in strictmode + $TestError, $TestError = $null + $TestResult = Invoke-DscResource @Config -Method Test -ModuleName $Module -ErrorVariable TestError -ErrorAction SilentlyContinue + if ($TestError) + { + throw ($TestError[0].Exception.Message) + } + ElseIf (($TestResult.InDesiredState) -ne $true) + { + if ($check_mode -eq $False) + { + $SetResult = Invoke-DscResource -Method Set @Config -ModuleName $Module -ErrorVariable SetError -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + $result["reboot_required"] = $SetResult.RebootRequired + } + + $result["changed"] = $true + if ($SetError) + { + throw ($SetError[0].Exception.Message) + } + } +} +Catch +{ + Fail-Json -obj $result -message $_[0].Exception.Message +} + + +#set-attr -obj $result -name "property" -value $property +Exit-Json -obj $result diff --git a/lib/ansible/modules/windows/win_dsc.py b/lib/ansible/modules/windows/win_dsc.py new file mode 100644 index 00000000000..0e438179b2a --- /dev/null +++ b/lib/ansible/modules/windows/win_dsc.py @@ -0,0 +1,110 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2015, Trond Hindenes , and others +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + +# this is a windows documentation stub. actual code lives in the .ps1 +# file of the same name + +ANSIBLE_METADATA = {'metadata_version': '1.0', + 'status': ['preview'], + 'supported_by': 'community'} + +DOCUMENTATION = r''' +--- +module: win_dsc +version_added: "2.4" +short_description: Invokes a PowerShell DSC configuration +description: | + Invokes a PowerShell DSC Configuration. Requires PowerShell version 5 (February release or newer). + Most of the parameters for this module are dynamic and will vary depending on the DSC Resource. + In order to find the required parameters for a given DSC resource, you can use the following on-liner: + 'Get-DscResource | select -ExpandProperty properties' + Also note that credentials are handled as follows: If the resource accepts a credential type property called "cred", + the ansible parameters would be cred_username and cred_password. + These will be used to inject a credential object on the fly for the DSC resource. +options: + resource_name: + description: + - The DSC Resource to use. Must be accessible to PowerShell using any of the default paths. + required: true + module_version: + description: | + Can be used to configure the exact version of the dsc resource to be invoked. + Useful if the target node has multiple versions installed of the module containing the DSC resource. + If not specified, the module will follow standard Powershell convention and use the highest version available. + default: latest +author: Trond Hindenes +''' + +EXAMPLES = r''' +# Playbook example + - name: Extract zip file + win_dsc: + resource_name: archive + ensure: Present + path: "C:\\Temp\\zipfile.zip" + destination: "C:\\Temp\\Temp2" + + - name: Invoke DSC with check mode + win_dsc: + resource_name: windowsfeature + name: telnet-client +''' + +RETURN = r''' +resource_name: + description: The name of the invoked resource + returned: always + type: string + sample: windowsfeature +module_version: + description: The version of the dsc resource/module used. + returned: success + type: string + sample: "1.0.1" +attributes: + description: The attributes/parameters passed in to the DSC resource as key/value pairs + returned: always + type: complex + sample: + contains: + Key: + description: Attribute key + Value: + description: Attribute value +dsc_attributes: + description: The attributes/parameters as returned from the DSC engine in dict format + returned: always + type: complex + contains: + Key: + description: Attribute key + Value: + description: Attribute value +reboot_required: + description: flag returned from the DSC engine indicating whether or not the machine requires a reboot for the invoked changes to take effect + returned: always + type: boolean + sample: True +message: + description: any error message from invoking the DSC resource + returned: error + type: string + sample: Multiple DSC modules found with resource name xyz +''' diff --git a/test/integration/targets/win_dsc/aliases b/test/integration/targets/win_dsc/aliases new file mode 100644 index 00000000000..c6d61981670 --- /dev/null +++ b/test/integration/targets/win_dsc/aliases @@ -0,0 +1 @@ +windows/ci/group3 diff --git a/test/integration/targets/win_dsc/defaults/main.yml b/test/integration/targets/win_dsc/defaults/main.yml new file mode 100644 index 00000000000..e1833cd8a84 --- /dev/null +++ b/test/integration/targets/win_dsc/defaults/main.yml @@ -0,0 +1,4 @@ +--- + +# Feature not normally installed by default. +test_win_feature_name: Telnet-Client diff --git a/test/integration/targets/win_dsc/tasks/main.yml b/test/integration/targets/win_dsc/tasks/main.yml new file mode 100644 index 00000000000..3e8d6ed6625 --- /dev/null +++ b/test/integration/targets/win_dsc/tasks/main.yml @@ -0,0 +1,119 @@ +# test code for the win_feature module +# (c) 2014, Chris Church + +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . + + +- name: check whether servermanager module is available (windows 2008 r2 or later) + raw: PowerShell -Command Import-Module ServerManager + register: win_feature_has_servermanager + ignore_errors: true + + +- name: start with feature absent + win_feature: + name: "{{ test_win_feature_name }}" + state: absent + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Invoke DSC with check mode + win_dsc: + resource_name: windowsfeature + name: "{{ test_win_feature_name }}" + check_mode: yes + register: win_dsc_checkmode_result + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Invoke DSC with check mode + assert: + that: + - "win_dsc_checkmode_result|changed" + - "win_dsc_checkmode_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Make sure the feature is still absent + win_dsc: + resource_name: windowsfeature + name: "{{ test_win_feature_name }}" + ensure: absent + register: win_dsc_1_result + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Make sure the feature is still absent + assert: + that: + - "not win_dsc_1_result|changed" + - "win_dsc_1_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Install feature for realz + win_dsc: + resource_name: windowsfeature + name: "{{ test_win_feature_name }}" + register: win_dsc_2_result + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Install feature for realz + assert: + that: + - "win_dsc_2_result|changed" + - "win_dsc_2_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Ensure clean failure + win_dsc: + resource_name: some_unknown_resource_that_wont_ever_exist + name: "{{ test_win_feature_name }}" + register: win_dsc_3_result + ignore_errors: true + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Ensure clean failure + assert: + that: + - "not win_dsc_3_result|changed" + - "not win_dsc_3_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Make sure the feature is absent + win_dsc: + resource_name: windowsfeature + name: "{{ test_win_feature_name }}" + ensure: absent + register: win_dsc_4_result + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Make sure the feature is absent + assert: + that: + - "win_dsc_4_result|changed" + - "win_dsc_4_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: Make sure the feature is still absent with no changes + win_dsc: + resource_name: windowsfeature + name: "{{ test_win_feature_name }}" + ensure: absent + register: win_dsc_5_result + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5) + +- name: check result of Make sure the feature is absent + assert: + that: + - "not win_dsc_5_result|changed" + - "win_dsc_5_result|success" + when: (win_feature_has_servermanager|success) and (ansible_powershell_version is defined) and (ansible_powershell_version >= 5)