diff --git a/cloud/amazon/dynamodb_table.py b/cloud/amazon/dynamodb_table.py index c97ff6f0be0..29ba230fe48 100644 --- a/cloud/amazon/dynamodb_table.py +++ b/cloud/amazon/dynamodb_table.py @@ -143,10 +143,15 @@ def create_or_update_dynamo_table(connection, module): read_capacity = module.params.get('read_capacity') write_capacity = module.params.get('write_capacity') - schema = [ - HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), - RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) - ] + if range_key_name: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)), + RangeKey(range_key_name, DYNAMO_TYPE_MAP.get(range_key_type)) + ] + else: + schema = [ + HashKey(hash_key_name, DYNAMO_TYPE_MAP.get(hash_key_type)) + ] throughput = { 'read': read_capacity, 'write': write_capacity diff --git a/cloud/amazon/ec2_vpc_igw.py b/cloud/amazon/ec2_vpc_igw.py index 63be48248ef..5218bff5e6e 100644 --- a/cloud/amazon/ec2_vpc_igw.py +++ b/cloud/amazon/ec2_vpc_igw.py @@ -133,7 +133,7 @@ def main(): if region: try: - connection = connect_to_aws(boto.ec2, region, **aws_connect_params) + connection = connect_to_aws(boto.vpc, region, **aws_connect_params) except (boto.exception.NoAuthHandlerFound, StandardError), e: module.fail_json(msg=str(e)) else: diff --git a/cloud/amazon/s3_bucket.py b/cloud/amazon/s3_bucket.py index 25c085f8173..8e660283472 100644 --- a/cloud/amazon/s3_bucket.py +++ b/cloud/amazon/s3_bucket.py @@ -20,10 +20,10 @@ short_description: Manage s3 buckets in AWS description: - Manage s3 buckets in AWS version_added: "2.0" -author: Rob White (@wimnat) +author: "Rob White (@wimnat)" options: force: - description: + description: - When trying to delete a bucket, delete all keys in the bucket first (an s3 bucket must be empty for a successful deletion) required: false default: no @@ -40,11 +40,12 @@ options: default: null region: description: - - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. + - AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard. required: false default: null s3_url: - description: S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS + description: + - S3 URL endpoint for usage with Eucalypus, fakes3, etc. Otherwise assumes AWS default: null aliases: [ S3_URL ] requester_pays: @@ -65,12 +66,12 @@ options: required: false default: null versioning: - description: + description: - Whether versioning is enabled or disabled (note that once versioning is enabled, it can only be suspended) required: false default: no choices: [ 'yes', 'no' ] - + extends_documentation_fragment: aws ''' @@ -387,4 +388,4 @@ from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/cloud/amazon/s3_lifecycle.py b/cloud/amazon/s3_lifecycle.py new file mode 100644 index 00000000000..3328a33f15f --- /dev/null +++ b/cloud/amazon/s3_lifecycle.py @@ -0,0 +1,421 @@ +#!/usr/bin/python +# +# This is a 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. +# +# This Ansible library 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 this library. If not, see . + +DOCUMENTATION = ''' +--- +module: s3_lifecycle +short_description: Manage s3 bucket lifecycle rules in AWS +description: + - Manage s3 bucket lifecycle rules in AWS +version_added: "2.0" +author: Rob White (@wimnat) +notes: + - If specifying expiration time as days then transition time must also be specified in days + - If specifying expiration time as a date then transition time must also be specified as a date +requirements: + - python-dateutil +options: + name: + description: + - "Name of the s3 bucket" + required: true + expiration_date: + description: + - "Indicates the lifetime of the objects that are subject to the rule by the date they will expire. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified." + required: false + default: null + expiration_days: + description: + - "Indicates the lifetime, in days, of the objects that are subject to the rule. The value must be a non-zero positive integer." + required: false + default: null + prefix: + description: + - "Prefix identifying one or more objects to which the rule applies. If no prefix is specified, the rule will apply to the whole bucket." + required: false + default: null + region: + description: + - "AWS region to create the bucket in. If not set then the value of the AWS_REGION and EC2_REGION environment variables are checked, followed by the aws_region and ec2_region settings in the Boto config file. If none of those are set the region defaults to the S3 Location: US Standard." + required: false + default: null + rule_id: + description: + - "Unique identifier for the rule. The value cannot be longer than 255 characters. A unique value for the rule will be generated if no value is provided." + required: false + default: null + state: + description: + - "Create or remove the lifecycle rule" + required: false + default: present + choices: [ 'present', 'absent' ] + status: + description: + - "If 'enabled', the rule is currently being applied. If 'disabled', the rule is not currently being applied." + required: false + default: enabled + choices: [ 'enabled', 'disabled' ] + storage_class: + description: + - "The storage class to transition to. Currently there is only one valid value - 'glacier'." + required: false + default: glacier + choices: [ 'glacier' ] + transition_date: + description: + - "Indicates the lifetime of the objects that are subject to the rule by the date they will transition to a different storage class. The value must be ISO-8601 format, the time must be midnight and a GMT timezone must be specified. If transition_days is not specified, this parameter is required." + required: false + default: null + transition_days: + description: + - "Indicates when, in days, an object transitions to a different storage class. If transition_date is not specified, this parameter is required." + required: false + default: null + +extends_documentation_fragment: aws +''' + +EXAMPLES = ''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +# Configure a lifecycle rule on a bucket to expire (delete) items with a prefix of /logs/ after 30 days +- s3_lifecycle: + name: mybucket + expiration_days: 30 + prefix: /logs/ + status: enabled + state: present + +# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier after 7 days and then delete after 90 days +- s3_lifecycle: + name: mybucket + transition_days: 7 + expiration_days: 90 + prefix: /logs/ + status: enabled + state: present + +# Configure a lifecycle rule to transition all items with a prefix of /logs/ to glacier on 31 Dec 2020 and then delete on 31 Dec 2030. Note that midnight GMT must be specified. +# Be sure to quote your date strings +- s3_lifecycle: + name: mybucket + transition_date: "2020-12-30T00:00:00.000Z" + expiration_date: "2030-12-30T00:00:00.000Z" + prefix: /logs/ + status: enabled + state: present + +# Disable the rule created above +- s3_lifecycle: + name: mybucket + prefix: /logs/ + status: disabled + state: present + +# Delete the lifecycle rule created above +- s3_lifecycle: + name: mybucket + prefix: /logs/ + state: absent + +''' + +import xml.etree.ElementTree as ET +import copy +import datetime + +try: + import dateutil.parser + HAS_DATEUTIL = True +except ImportError: + HAS_DATEUTIL = False + +try: + import boto.ec2 + from boto.s3.connection import OrdinaryCallingFormat, Location + from boto.s3.lifecycle import Lifecycle, Rule, Expiration, Transition + from boto.exception import BotoServerError, S3CreateError, S3ResponseError + HAS_BOTO = True +except ImportError: + HAS_BOTO = False + +def create_lifecycle_rule(connection, module): + + name = module.params.get("name") + expiration_date = module.params.get("expiration_date") + expiration_days = module.params.get("expiration_days") + prefix = module.params.get("prefix") + rule_id = module.params.get("rule_id") + status = module.params.get("status") + storage_class = module.params.get("storage_class") + transition_date = module.params.get("transition_date") + transition_days = module.params.get("transition_days") + changed = False + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + # Get the bucket's current lifecycle rules + try: + current_lifecycle_obj = bucket.get_lifecycle_config() + except S3ResponseError, e: + if e.error_code == "NoSuchLifecycleConfiguration": + current_lifecycle_obj = Lifecycle() + else: + module.fail_json(msg=e.message) + + # Create expiration + if expiration_days is not None: + expiration_obj = Expiration(days=expiration_days) + elif expiration_date is not None: + expiration_obj = Expiration(date=expiration_date) + else: + expiration_obj = None + + # Create transition + if transition_days is not None: + transition_obj = Transition(days=transition_days, storage_class=storage_class.upper()) + elif transition_date is not None: + transition_obj = Transition(date=transition_date, storage_class=storage_class.upper()) + else: + transition_obj = None + + # Create rule + rule = Rule(rule_id, prefix, status.title(), expiration_obj, transition_obj) + + # Create lifecycle + lifecycle_obj = Lifecycle() + + appended = False + # If current_lifecycle_obj is not None then we have rules to compare, otherwise just add the rule + if current_lifecycle_obj: + # If rule ID exists, use that for comparison otherwise compare based on prefix + for existing_rule in current_lifecycle_obj: + if rule.id == existing_rule.id: + if compare_rule(rule, existing_rule): + lifecycle_obj.append(rule) + appended = True + else: + lifecycle_obj.append(rule) + changed = True + appended = True + elif rule.prefix == existing_rule.prefix: + existing_rule.id = None + if compare_rule(rule, existing_rule): + lifecycle_obj.append(rule) + appended = True + else: + lifecycle_obj.append(rule) + changed = True + appended = True + # If nothing appended then append now as the rule must not exist + if not appended: + lifecycle_obj.append(rule) + changed = True + else: + lifecycle_obj.append(rule) + changed = True + + # Write lifecycle to bucket + try: + bucket.configure_lifecycle(lifecycle_obj) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + +def compare_rule(rule_a, rule_b): + + # Copy objects + rule1 = copy.deepcopy(rule_a) + rule2 = copy.deepcopy(rule_b) + + # Delete Rule from Rule + try: + del rule1.Rule + except AttributeError: + pass + + try: + del rule2.Rule + except AttributeError: + pass + + # Extract Expiration and Transition objects + rule1_expiration = rule1.expiration + rule1_transition = rule1.transition + rule2_expiration = rule2.expiration + rule2_transition = rule2.transition + + # Delete the Expiration and Transition objects from the Rule objects + del rule1.expiration + del rule1.transition + del rule2.expiration + del rule2.transition + + # Compare + if rule1_transition is None: + rule1_transition = Transition() + if rule2_transition is None: + rule2_transition = Transition() + if rule1_expiration is None: + rule1_expiration = Expiration() + if rule2_expiration is None: + rule2_expiration = Expiration() + + if (rule1.__dict__ == rule2.__dict__) and (rule1_expiration.__dict__ == rule2_expiration.__dict__) and (rule1_transition.__dict__ == rule2_transition.__dict__): + return True + else: + return False + + +def destroy_lifecycle_rule(connection, module): + + name = module.params.get("name") + prefix = module.params.get("prefix") + rule_id = module.params.get("rule_id") + changed = False + + if prefix is None: + prefix = "" + + try: + bucket = connection.get_bucket(name) + except S3ResponseError, e: + module.fail_json(msg=e.message) + + # Get the bucket's current lifecycle rules + try: + current_lifecycle_obj = bucket.get_lifecycle_config() + except S3ResponseError, e: + if e.error_code == "NoSuchLifecycleConfiguration": + module.exit_json(changed=changed) + else: + module.fail_json(msg=e.message) + + # Create lifecycle + lifecycle_obj = Lifecycle() + + # Check if rule exists + # If an ID exists, use that otherwise compare based on prefix + if rule_id is not None: + for existing_rule in current_lifecycle_obj: + if rule_id == existing_rule.id: + # We're not keeping the rule (i.e. deleting) so mark as changed + changed = True + else: + lifecycle_obj.append(existing_rule) + else: + for existing_rule in current_lifecycle_obj: + if prefix == existing_rule.prefix: + # We're not keeping the rule (i.e. deleting) so mark as changed + changed = True + else: + lifecycle_obj.append(existing_rule) + + + # Write lifecycle to bucket or, if there no rules left, delete lifecycle configuration + try: + if lifecycle_obj: + bucket.configure_lifecycle(lifecycle_obj) + else: + bucket.delete_lifecycle_configuration() + except BotoServerError, e: + module.fail_json(msg=e.message) + + module.exit_json(changed=changed) + + +def main(): + + argument_spec = ec2_argument_spec() + argument_spec.update( + dict( + name = dict(required=True), + expiration_days = dict(default=None, required=False, type='int'), + expiration_date = dict(default=None, required=False, type='str'), + prefix = dict(default=None, required=False), + requester_pays = dict(default='no', type='bool'), + rule_id = dict(required=False), + state = dict(default='present', choices=['present', 'absent']), + status = dict(default='enabled', choices=['enabled', 'disabled']), + storage_class = dict(default='glacier', choices=['glacier']), + transition_days = dict(default=None, required=False, type='int'), + transition_date = dict(default=None, required=False, type='str') + ) + ) + + module = AnsibleModule(argument_spec=argument_spec, + mutually_exclusive = [ + [ 'expiration_days', 'expiration_date' ], + [ 'expiration_days', 'transition_date' ], + [ 'transition_days', 'transition_date' ], + [ 'transition_days', 'expiration_date' ] + ] + ) + + if not HAS_BOTO: + module.fail_json(msg='boto required for this module') + + if not HAS_DATEUTIL: + module.fail_json(msg='dateutil required for this module') + + region, ec2_url, aws_connect_params = get_aws_connection_info(module) + + if region in ('us-east-1', '', None): + # S3ism for the US Standard region + location = Location.DEFAULT + else: + # Boto uses symbolic names for locations but region strings will + # actually work fine for everything except us-east-1 (US Standard) + location = region + try: + connection = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_params) + # use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases + if connection is None: + connection = boto.connect_s3(**aws_connect_params) + except (boto.exception.NoAuthHandlerFound, StandardError), e: + module.fail_json(msg=str(e)) + + expiration_date = module.params.get("expiration_date") + transition_date = module.params.get("transition_date") + state = module.params.get("state") + + # If expiration_date set, check string is valid + if expiration_date is not None: + try: + datetime.datetime.strptime(expiration_date, "%Y-%m-%dT%H:%M:%S.000Z") + except ValueError, e: + module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") + + if transition_date is not None: + try: + datetime.datetime.strptime(transition_date, "%Y-%m-%dT%H:%M:%S.000Z") + except ValueError, e: + module.fail_json(msg="expiration_date is not a valid ISO-8601 format. The time must be midnight and a timezone of GMT must be included") + + if state == 'present': + create_lifecycle_rule(connection, module) + elif state == 'absent': + destroy_lifecycle_rule(connection, module) + +from ansible.module_utils.basic import * +from ansible.module_utils.ec2 import * + +if __name__ == '__main__': + main() diff --git a/cloud/cloudstack/cs_instance.py b/cloud/cloudstack/cs_instance.py index c75ee8144b1..f15241f5354 100644 --- a/cloud/cloudstack/cs_instance.py +++ b/cloud/cloudstack/cs_instance.py @@ -53,6 +53,21 @@ options: - If not set, first found service offering is used. required: false default: null + cpu: + description: + - The number of CPUs to allocate to the instance, used with custom service offerings + required: false + default: null + cpu_speed: + description: + - The clock speed/shares allocated to the instance, used with custom service offerings + required: false + default: null + memory: + description: + - The memory allocated to the instance, used with custom service offerings + required: false + default: null template: description: - Name or id of the template to be used for creating the new instance. @@ -547,6 +562,18 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): user_data = base64.b64encode(user_data) return user_data + def get_details(self): + res = None + cpu = self.module.params.get('cpu') + cpu_speed = self.module.params.get('cpu_speed') + memory = self.module.params.get('memory') + if all([cpu, cpu_speed, memory]): + res = [{ + 'cpuNumber': cpu, + 'cpuSpeed': cpu_speed, + 'memory': memory, + }] + return res def deploy_instance(self, start_vm=True): self.result['changed'] = True @@ -577,6 +604,7 @@ class AnsibleCloudStackInstance(AnsibleCloudStack): args['rootdisksize'] = self.module.params.get('root_disk_size') args['securitygroupnames'] = ','.join(self.module.params.get('security_groups')) args['affinitygroupnames'] = ','.join(self.module.params.get('affinity_groups')) + args['details'] = self.get_details() template_iso = self.get_template_or_iso() if 'hypervisor' not in template_iso: @@ -798,6 +826,9 @@ def main(): group = dict(default=None), state = dict(choices=['present', 'deployed', 'started', 'stopped', 'restarted', 'absent', 'destroyed', 'expunged'], default='present'), service_offering = dict(default=None), + cpu = dict(default=None, type='int'), + cpu_speed = dict(default=None, type='int'), + memory = dict(default=None, type='int'), template = dict(default=None), iso = dict(default=None), networks = dict(type='list', aliases=[ 'network' ], default=None), @@ -832,6 +863,7 @@ def main(): ), required_together = ( ['api_key', 'api_secret', 'api_url'], + ['cpu', 'cpu_speed', 'memory'], ), supports_check_mode=True ) diff --git a/packaging/language/bower.py b/packaging/language/bower.py index bd7d4b26159..c835fbf797d 100644 --- a/packaging/language/bower.py +++ b/packaging/language/bower.py @@ -37,6 +37,13 @@ options: required: false default: no choices: [ "yes", "no" ] + production: + description: + - Install with --production flag + required: false + default: no + choices: [ "yes", "no" ] + version_added: "2.0" path: description: - The base path where to install the bower packages @@ -76,6 +83,7 @@ class Bower(object): self.module = module self.name = kwargs['name'] self.offline = kwargs['offline'] + self.production = kwargs['production'] self.path = kwargs['path'] self.version = kwargs['version'] @@ -94,6 +102,9 @@ class Bower(object): if self.offline: cmd.append('--offline') + if self.production: + cmd.append('--production') + # If path is specified, cd into that path and run the command. cwd = None if self.path: @@ -148,6 +159,7 @@ def main(): arg_spec = dict( name=dict(default=None), offline=dict(default='no', type='bool'), + production=dict(default='no', type='bool'), path=dict(required=True), state=dict(default='present', choices=['present', 'absent', 'latest', ]), version=dict(default=None), @@ -158,6 +170,7 @@ def main(): name = module.params['name'] offline = module.params['offline'] + production = module.params['production'] path = os.path.expanduser(module.params['path']) state = module.params['state'] version = module.params['version'] @@ -165,7 +178,7 @@ def main(): if state == 'absent' and not name: module.fail_json(msg='uninstalling a package is only available for named packages') - bower = Bower(module, name=name, offline=offline, path=path, version=version) + bower = Bower(module, name=name, offline=offline, production=production, path=path, version=version) changed = False if state == 'present': diff --git a/system/locale_gen.py b/system/locale_gen.py index 410f1dfc23d..e17ed5581da 100644 --- a/system/locale_gen.py +++ b/system/locale_gen.py @@ -52,6 +52,13 @@ LOCALE_NORMALIZATION = { ".utf8": ".UTF-8", ".eucjp": ".EUC-JP", ".iso885915": ".ISO-8859-15", + ".cp1251": ".CP1251", + ".koi8r": ".KOI8-R", + ".armscii8": ".ARMSCII-8", + ".euckr": ".EUC-KR", + ".gbk": ".GBK", + ".gb18030": ".GB18030", + ".euctw": ".EUC-TW", } # =========================================== diff --git a/windows/win_package.ps1 b/windows/win_package.ps1 new file mode 100644 index 00000000000..02bb908a944 --- /dev/null +++ b/windows/win_package.ps1 @@ -0,0 +1,1305 @@ +#!powershell +# (c) 2014, 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 + +#region DSC + +data LocalizedData +{ + # culture="en-US" + # TODO: Support WhatIf + ConvertFrom-StringData @' +InvalidIdentifyingNumber=The specified IdentifyingNumber ({0}) is not a valid Guid +InvalidPath=The specified Path ({0}) is not in a valid format. Valid formats are local paths, UNC, and HTTP +InvalidNameOrId=The specified Name ({0}) and IdentifyingNumber ({1}) do not match Name ({2}) and IdentifyingNumber ({3}) in the MSI file +NeedsMoreInfo=Either Name or ProductId is required +InvalidBinaryType=The specified Path ({0}) does not appear to specify an EXE or MSI file and as such is not supported +CouldNotOpenLog=The specified LogPath ({0}) could not be opened +CouldNotStartProcess=The process {0} could not be started +UnexpectedReturnCode=The return code {0} was not expected. Configuration is likely not correct +PathDoesNotExist=The given Path ({0}) could not be found +CouldNotOpenDestFile=Could not open the file {0} for writing +CouldNotGetHttpStream=Could not get the {0} stream for file {1} +ErrorCopyingDataToFile=Encountered error while writing the contents of {0} to {1} +PackageConfigurationComplete=Package configuration finished +PackageConfigurationStarting=Package configuration starting +InstalledPackage=Installed package +UninstalledPackage=Uninstalled package +NoChangeRequired=Package found in desired state, no action required +RemoveExistingLogFile=Remove existing log file +CreateLogFile=Create log file +MountSharePath=Mount share to get media +DownloadHTTPFile=Download the media over HTTP or HTTPS +StartingProcessMessage=Starting process {0} with arguments {1} +RemoveDownloadedFile=Remove the downloaded file +PackageInstalled=Package has been installed +PackageUninstalled=Package has been uninstalled +MachineRequiresReboot=The machine requires a reboot +PackageDoesNotAppearInstalled=The package {0} is not installed +PackageAppearsInstalled=The package {0} is already installed +PostValidationError=Package from {0} was installed, but the specified ProductId and/or Name does not match package details +'@ +} + +$Debug = $true +Function Trace-Message +{ + param([string] $Message) + if($Debug) + { + Write-Verbose $Message + } +} + +$CacheLocation = "$env:ProgramData\Microsoft\Windows\PowerShell\Configuration\BuiltinProvCache\MSFT_PackageResource" + +Function Throw-InvalidArgumentException +{ + param( + [string] $Message, + [string] $ParamName + ) + + $exception = new-object System.ArgumentException $Message,$ParamName + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,$ParamName,"InvalidArgument",$null + throw $errorRecord +} + +Function Throw-InvalidNameOrIdException +{ + param( + [string] $Message + ) + + $exception = new-object System.ArgumentException $Message + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"NameOrIdNotInMSI","InvalidArgument",$null + throw $errorRecord +} + +Function Throw-TerminatingError +{ + param( + [string] $Message, + [System.Management.Automation.ErrorRecord] $ErrorRecord + ) + + $exception = new-object "System.InvalidOperationException" $Message,$ErrorRecord.Exception + $errorRecord = New-Object System.Management.Automation.ErrorRecord $exception,"MachineStateIncorrect","InvalidOperation",$null + throw $errorRecord +} + +Function Get-RegistryValueIgnoreError +{ + param + ( + [parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryHive] + $RegistryHive, + + [parameter(Mandatory = $true)] + [System.String] + $Key, + + [parameter(Mandatory = $true)] + [System.String] + $Value, + + [parameter(Mandatory = $true)] + [Microsoft.Win32.RegistryView] + $RegistryView + ) + + try + { + $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryHive, $RegistryView) + $subKey = $baseKey.OpenSubKey($Key) + if($subKey -ne $null) + { + return $subKey.GetValue($Value) + } + } + catch + { + $exceptionText = ($_ | Out-String).Trim() + Write-Verbose "Exception occured in Get-RegistryValueIgnoreError: $exceptionText" + } + return $null +} + +Function Validate-StandardArguments +{ + param( + $Path, + $ProductId, + $Name + ) + + Trace-Message "Validate-StandardArguments, Path was $Path" + $uri = $null + try + { + $uri = [uri] $Path + } + catch + { + Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" + } + + if(-not @("file", "http", "https") -contains $uri.Scheme) + { + Trace-Message "The uri scheme was $uri.Scheme" + Throw-InvalidArgumentException ($LocalizedData.InvalidPath -f $Path) "Path" + } + + $pathExt = [System.IO.Path]::GetExtension($Path) + Trace-Message "The path extension was $pathExt" + if(-not @(".msi",".exe") -contains $pathExt.ToLower()) + { + Throw-InvalidArgumentException ($LocalizedData.InvalidBinaryType -f $Path) "Path" + } + + $identifyingNumber = $null + if(-not $Name -and -not $ProductId) + { + #It's a tossup here which argument to blame, so just pick ProductId to encourage customers to use the most efficient version + Throw-InvalidArgumentException ($LocalizedData.NeedsMoreInfo -f $Path) "ProductId" + } + elseif($ProductId) + { + try + { + Trace-Message "Parsing $ProductId as an identifyingNumber" + $identifyingNumber = "{{{0}}}" -f [Guid]::Parse($ProductId).ToString().ToUpper() + Trace-Message "Parsed $ProductId as $identifyingNumber" + } + catch + { + Throw-InvalidArgumentException ($LocalizedData.InvalidIdentifyingNumber -f $ProductId) $ProductId + } + } + + return $uri, $identifyingNumber +} + +Function Get-ProductEntry +{ + param + ( + [string] $Name, + [string] $IdentifyingNumber, + [string] $InstalledCheckRegKey, + [string] $InstalledCheckRegValueName, + [string] $InstalledCheckRegValueData + ) + + $uninstallKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall" + $uninstallKeyWow64 = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall" + + if($IdentifyingNumber) + { + $keyLocation = "$uninstallKey\$identifyingNumber" + $item = Get-Item $keyLocation -EA SilentlyContinue + if(-not $item) + { + $keyLocation = "$uninstallKeyWow64\$identifyingNumber" + $item = Get-Item $keyLocation -EA SilentlyContinue + } + + return $item + } + + foreach($item in (Get-ChildItem -EA Ignore $uninstallKey, $uninstallKeyWow64)) + { + if($Name -eq (Get-LocalizableRegKeyValue $item "DisplayName")) + { + return $item + } + } + + if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + $installValue = $null + + #if 64bit OS, check 64bit registry view first + if ((Get-WmiObject -Class Win32_OperatingSystem -ComputerName "localhost" -ea 0).OSArchitecture -eq '64-bit') + { + $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry64 + } + + if($installValue -eq $null) + { + $installValue = Get-RegistryValueIgnoreError LocalMachine "$InstalledCheckRegKey" "$InstalledCheckRegValueName" Registry32 + } + + if($installValue) + { + if($InstalledCheckRegValueData -and $installValue -eq $InstalledCheckRegValueData) + { + return @{ + Installed = $true + } + } + } + } + + return $null +} + +function Test-TargetResource +{ + param + ( + [ValidateSet("Present", "Absent")] + [string] $Ensure = "Present", + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $Arguments, + + [pscredential] $Credential, + + [int[]] $ReturnCode, + + [string] $LogPath, + + [pscredential] $RunAsCredential, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + Trace-Message "Ensure is $Ensure" + if($product) + { + Trace-Message "product found" + } + else + { + Trace-Message "product installation cannot be determined" + } + Trace-Message ("product as boolean is {0}" -f [boolean]$product) + $res = ($product -ne $null -and $Ensure -eq "Present") -or ($product -eq $null -and $Ensure -eq "Absent") + + # install registry test overrides the product id test and there is no true product information + # when doing a lookup via registry key + if ($product -and $InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $Name) + } + else + { + if ($product -ne $null) + { + $name = Get-LocalizableRegKeyValue $product "DisplayName" + Write-Verbose ($LocalizedData.PackageAppearsInstalled -f $name) + } + else + { + $displayName = $null + if($Name) + { + $displayName = $Name + } + else + { + $displayName = $ProductId + } + + Write-Verbose ($LocalizedData.PackageDoesNotAppearInstalled -f $displayName) + } + + } + + return $res +} + +function Get-LocalizableRegKeyValue +{ + param( + [object] $RegKey, + [string] $ValueName + ) + + $res = $RegKey.GetValue("{0}_Localized" -f $ValueName) + if(-not $res) + { + $res = $RegKey.GetValue($ValueName) + } + + return $res +} + +function Get-TargetResource +{ + param + ( + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + #If the user gave the ProductId then we derive $identifyingNumber + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + + $localMsi = $uri.IsFile -and -not $uri.IsUnc + + $product = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + + if(-not $product) + { + return @{ + Ensure = "Absent" + Name = $Name + ProductId = $identifyingNumber + Installed = $false + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + } + + if ($InstalledCheckRegKey -and $InstalledCheckRegValueName -and $InstalledCheckRegValueData) + { + return @{ + Ensure = "Present" + Name = $Name + ProductId = $identifyingNumber + Installed = $true + InstalledCheckRegKey = $InstalledCheckRegKey + InstalledCheckRegValueName = $InstalledCheckRegValueName + InstalledCheckRegValueData = $InstalledCheckRegValueData + } + } + + #$identifyingNumber can still be null here (e.g. remote MSI with Name specified, local EXE) + #If the user gave a ProductId just pass it through, otherwise fill it from the product + if(-not $identifyingNumber) + { + $identifyingNumber = Split-Path -Leaf $product.Name + } + + $date = $product.GetValue("InstallDate") + if($date) + { + try + { + $date = "{0:d}" -f [DateTime]::ParseExact($date, "yyyyMMdd",[System.Globalization.CultureInfo]::CurrentCulture).Date + } + catch + { + $date = $null + } + } + + $publisher = Get-LocalizableRegKeyValue $product "Publisher" + $size = $product.GetValue("EstimatedSize") + if($size) + { + $size = $size/1024 + } + + $version = $product.GetValue("DisplayVersion") + $description = $product.GetValue("Comments") + $name = Get-LocalizableRegKeyValue $product "DisplayName" + return @{ + Ensure = "Present" + Name = $name + Path = $Path + InstalledOn = $date + ProductId = $identifyingNumber + Size = $size + Installed = $true + Version = $version + PackageDescription = $description + Publisher = $publisher + } +} + +Function Get-MsiTools +{ + if($script:MsiTools) + { + return $script:MsiTools + } + + $sig = @' + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern UInt32 MsiOpenPackageW(string szPackagePath, out IntPtr hProduct); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiCloseHandle(IntPtr hAny); + + [DllImport("msi.dll", CharSet = CharSet.Unicode, PreserveSig = true, SetLastError = true, ExactSpelling = true)] + private static extern uint MsiGetPropertyW(IntPtr hAny, string name, StringBuilder buffer, ref int bufferLength); + + private static string GetPackageProperty(string msi, string property) + { + IntPtr MsiHandle = IntPtr.Zero; + try + { + var res = MsiOpenPackageW(msi, out MsiHandle); + if (res != 0) + { + return null; + } + + int length = 256; + var buffer = new StringBuilder(length); + res = MsiGetPropertyW(MsiHandle, property, buffer, ref length); + return buffer.ToString(); + } + finally + { + if (MsiHandle != IntPtr.Zero) + { + MsiCloseHandle(MsiHandle); + } + } + } + public static string GetProductCode(string msi) + { + return GetPackageProperty(msi, "ProductCode"); + } + + public static string GetProductName(string msi) + { + return GetPackageProperty(msi, "ProductName"); + } +'@ + $script:MsiTools = Add-Type -PassThru -Namespace Microsoft.Windows.DesiredStateConfiguration.PackageResource ` + -Name MsiTools -Using System.Text -MemberDefinition $sig + return $script:MsiTools +} + + +Function Get-MsiProductEntry +{ + param + ( + [string] $Path + ) + + if(-not (Test-Path -PathType Leaf $Path) -and ($fileExtension -ne ".msi")) + { + Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + } + + $tools = Get-MsiTools + + $pn = $tools::GetProductName($Path) + + $pc = $tools::GetProductCode($Path) + + return $pn,$pc +} + + +function Set-TargetResource +{ + [CmdletBinding(SupportsShouldProcess=$true)] + param + ( + [ValidateSet("Present", "Absent")] + [string] $Ensure = "Present", + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $Name, + + [parameter(Mandatory = $true)] + [ValidateNotNullOrEmpty()] + [string] $Path, + + [parameter(Mandatory = $true)] + [AllowEmptyString()] + [string] $ProductId, + + [string] $Arguments, + + [pscredential] $Credential, + + [int[]] $ReturnCode, + + [string] $LogPath, + + [pscredential] $RunAsCredential, + + [string] $InstalledCheckRegKey, + + [string] $InstalledCheckRegValueName, + + [string] $InstalledCheckRegValueData + ) + + $ErrorActionPreference = "Stop" + + if((Test-TargetResource -Ensure $Ensure -Name $Name -Path $Path -ProductId $ProductId ` + -InstalledCheckRegKey $InstalledCheckRegKey -InstalledCheckRegValueName $InstalledCheckRegValueName ` + -InstalledCheckRegValueData $InstalledCheckRegValueData)) + { + return + } + + $uri, $identifyingNumber = Validate-StandardArguments $Path $ProductId $Name + + #Path gets overwritten in the download code path. Retain the user's original Path in case the install succeeded + #but the named package wasn't present on the system afterward so we can give a better message + $OrigPath = $Path + + Write-Verbose $LocalizedData.PackageConfigurationStarting + if(-not $ReturnCode) + { + $ReturnCode = @(0) + } + + $logStream = $null + $psdrive = $null + $downloadedFileName = $null + try + { + $fileExtension = [System.IO.Path]::GetExtension($Path).ToLower() + if($LogPath) + { + try + { + if($fileExtension -eq ".msi") + { + #We want to pre-verify the path exists and is writable ahead of time + #even in the MSI case, as detecting WHY the MSI log doesn't exist would + #be rather problematic for the user + if((Test-Path $LogPath) -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveExistingLogFile,$null,$null)) + { + rm $LogPath + } + + if($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + { + New-Item -Type File $LogPath | Out-Null + } + } + elseif($PSCmdlet.ShouldProcess($LocalizedData.CreateLogFile, $null, $null)) + { + $logStream = new-object "System.IO.StreamWriter" $LogPath,$false + } + } + catch + { + Throw-TerminatingError ($LocalizedData.CouldNotOpenLog -f $LogPath) $_ + } + } + + #Download or mount file as necessary + if(-not ($fileExtension -eq ".msi" -and $Ensure -eq "Absent")) + { + if($uri.IsUnc -and $PSCmdlet.ShouldProcess($LocalizedData.MountSharePath, $null, $null)) + { + $psdriveArgs = @{Name=([guid]::NewGuid());PSProvider="FileSystem";Root=(Split-Path $uri.LocalPath)} + if($Credential) + { + #We need to optionally include these and then splat the hash otherwise + #we pass a null for Credential which causes the cmdlet to pop a dialog up + $psdriveArgs["Credential"] = $Credential + } + + $psdrive = New-PSDrive @psdriveArgs + $Path = Join-Path $psdrive.Root (Split-Path -Leaf $uri.LocalPath) #Necessary? + } + elseif(@("http", "https") -contains $uri.Scheme -and $Ensure -eq "Present" -and $PSCmdlet.ShouldProcess($LocalizedData.DownloadHTTPFile, $null, $null)) + { + $scheme = $uri.Scheme + $outStream = $null + $responseStream = $null + + try + { + Trace-Message "Creating cache location" + + if(-not (Test-Path -PathType Container $CacheLocation)) + { + mkdir $CacheLocation | Out-Null + } + + $destName = Join-Path $CacheLocation (Split-Path -Leaf $uri.LocalPath) + + Trace-Message "Need to download file from $scheme, destination will be $destName" + + try + { + Trace-Message "Creating the destination cache file" + $outStream = New-Object System.IO.FileStream $destName, "Create" + } + catch + { + #Should never happen since we own the cache directory + Throw-TerminatingError ($LocalizedData.CouldNotOpenDestFile -f $destName) $_ + } + + try + { + Trace-Message "Creating the $scheme stream" + $request = [System.Net.WebRequest]::Create($uri) + Trace-Message "Setting default credential" + $request.Credentials = [System.Net.CredentialCache]::DefaultCredentials + if ($scheme -eq "http") + { + Trace-Message "Setting authentication level" + # default value is MutualAuthRequested, which applies to https scheme + $request.AuthenticationLevel = [System.Net.Security.AuthenticationLevel]::None + } + if ($scheme -eq "https") + { + Trace-Message "Ignoring bad certificates" + $request.ServerCertificateValidationCallBack = {$true} + } + Trace-Message "Getting the $scheme response stream" + $responseStream = (([System.Net.HttpWebRequest]$request).GetResponse()).GetResponseStream() + } + catch + { + Trace-Message ("Error: " + ($_ | Out-String)) + Throw-TerminatingError ($LocalizedData.CouldNotGetHttpStream -f $scheme, $Path) $_ + } + + try + { + Trace-Message "Copying the $scheme stream bytes to the disk cache" + $responseStream.CopyTo($outStream) + $responseStream.Flush() + $outStream.Flush() + } + catch + { + Throw-TerminatingError ($LocalizedData.ErrorCopyingDataToFile -f $Path,$destName) $_ + } + } + finally + { + if($outStream) + { + $outStream.Close() + } + + if($responseStream) + { + $responseStream.Close() + } + } + Trace-Message "Redirecting package path to cache file location" + $Path = $downloadedFileName = $destName + } + } + + #At this point the Path ought to be valid unless it's an MSI uninstall case + if(-not (Test-Path -PathType Leaf $Path) -and -not ($Ensure -eq "Absent" -and $fileExtension -eq ".msi")) + { + Throw-TerminatingError ($LocalizedData.PathDoesNotExist -f $Path) + } + + $startInfo = New-Object System.Diagnostics.ProcessStartInfo + $startInfo.UseShellExecute = $false #Necessary for I/O redirection and just generally a good idea + $process = New-Object System.Diagnostics.Process + $process.StartInfo = $startInfo + $errLogPath = $LogPath + ".err" #Concept only, will never touch disk + if($fileExtension -eq ".msi") + { + $startInfo.FileName = "$env:windir\system32\msiexec.exe" + if($Ensure -eq "Present") + { + # check if Msi package contains the ProductName and Code specified + + $pName,$pCode = Get-MsiProductEntry -Path $Path + + if ( + ( (-not [String]::IsNullOrEmpty($Name)) -and ($pName -ne $Name)) ` + -or ( (-not [String]::IsNullOrEmpty($identifyingNumber)) -and ($identifyingNumber -ne $pCode)) + ) + { + Throw-InvalidNameOrIdException ($LocalizedData.InvalidNameOrId -f $Name,$identifyingNumber,$pName,$pCode) + } + + $startInfo.Arguments = '/i "{0}"' -f $Path + } + else + { + $product = Get-ProductEntry $Name $identifyingNumber + $id = Split-Path -Leaf $product.Name #We may have used the Name earlier, now we need the actual ID + $startInfo.Arguments = ("/x{0}" -f $id) + } + + if($LogPath) + { + $startInfo.Arguments += ' /log "{0}"' -f $LogPath + } + + $startInfo.Arguments += " /quiet" + + if($Arguments) + { + $startInfo.Arguments += " " + $Arguments + } + } + else #EXE + { + Trace-Message "The binary is an EXE" + $startInfo.FileName = $Path + $startInfo.Arguments = $Arguments + if($LogPath) + { + Trace-Message "User has requested logging, need to attach event handlers to the process" + $startInfo.RedirectStandardError = $true + $startInfo.RedirectStandardOutput = $true + Register-ObjectEvent -InputObject $process -EventName "OutputDataReceived" -SourceIdentifier $LogPath + Register-ObjectEvent -InputObject $process -EventName "ErrorDataReceived" -SourceIdentifier $errLogPath + } + } + + Trace-Message ("Starting {0} with {1}" -f $startInfo.FileName, $startInfo.Arguments) + + if($PSCmdlet.ShouldProcess(($LocalizedData.StartingProcessMessage -f $startInfo.FileName, $startInfo.Arguments), $null, $null)) + { + try + { + $exitCode = 0 + + if($PSBoundParameters.ContainsKey("RunAsCredential")) + { + CallPInvoke + [Source.NativeMethods]::CreateProcessAsUser("""" + $startInfo.FileName + """ " + $startInfo.Arguments, ` + $RunAsCredential.GetNetworkCredential().Domain, $RunAsCredential.GetNetworkCredential().UserName, ` + $RunAsCredential.GetNetworkCredential().Password, [ref] $exitCode) + } + else + { + $process.Start() | Out-Null + + if($logStream) #Identical to $fileExtension -eq ".exe" -and $logPath + { + $process.BeginOutputReadLine(); + $process.BeginErrorReadLine(); + } + + $process.WaitForExit() + + if($process) + { + $exitCode = $process.ExitCode + } + } + } + catch + { + Throw-TerminatingError ($LocalizedData.CouldNotStartProcess -f $Path) $_ + } + + + if($logStream) + { + #We have to re-mux these since they appear to us as different streams + #The underlying Win32 APIs prevent this problem, as would constructing a script + #on the fly and executing it, but the former is highly problematic from PowerShell + #and the latter doesn't let us get the return code for UI-based EXEs + $outputEvents = Get-Event -SourceIdentifier $LogPath + $errorEvents = Get-Event -SourceIdentifier $errLogPath + $masterEvents = @() + $outputEvents + $errorEvents + $masterEvents = $masterEvents | Sort-Object -Property TimeGenerated + + foreach($event in $masterEvents) + { + $logStream.Write($event.SourceEventArgs.Data); + } + + Remove-Event -SourceIdentifier $LogPath + Remove-Event -SourceIdentifier $errLogPath + } + + if(-not ($ReturnCode -contains $exitCode)) + { + Throw-TerminatingError ($LocalizedData.UnexpectedReturnCode -f $exitCode.ToString()) + } + } + } + finally + { + if($psdrive) + { + Remove-PSDrive -Force $psdrive + } + + if($logStream) + { + $logStream.Dispose() + } + } + + if($downloadedFileName -and $PSCmdlet.ShouldProcess($LocalizedData.RemoveDownloadedFile, $null, $null)) + { + #This is deliberately not in the Finally block. We want to leave the downloaded file on disk + #in the error case as a debugging aid for the user + rm $downloadedFileName + } + + $operationString = $LocalizedData.PackageUninstalled + if($Ensure -eq "Present") + { + $operationString = $LocalizedData.PackageInstalled + } + + # Check if reboot is required, if so notify CA. The MSFT_ServerManagerTasks provider is missing on client SKUs + $featureData = invoke-wmimethod -EA Ignore -Name GetServerFeature -namespace root\microsoft\windows\servermanager -Class MSFT_ServerManagerTasks + $regData = Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" "PendingFileRenameOperations" -EA Ignore + if(($featureData -and $featureData.RequiresReboot) -or $regData) + { + Write-Verbose $LocalizedData.MachineRequiresReboot + $global:DSCMachineStatus = 1 + } + + if($Ensure -eq "Present") + { + $productEntry = Get-ProductEntry $Name $identifyingNumber $InstalledCheckRegKey $InstalledCheckRegValueName $InstalledCheckRegValueData + if(-not $productEntry) + { + Throw-TerminatingError ($LocalizedData.PostValidationError -f $OrigPath) + } + } + + Write-Verbose $operationString + Write-Verbose $LocalizedData.PackageConfigurationComplete +} + +function CallPInvoke +{ +$script:ProgramSource = @" +using System; +using System.Collections.Generic; +using System.Text; +using System.Security; +using System.Runtime.InteropServices; +using System.Diagnostics; +using System.Security.Principal; +using System.ComponentModel; +using System.IO; + +namespace Source +{ + [SuppressUnmanagedCodeSecurity] + public static class NativeMethods + { + //The following structs and enums are used by the various Win32 API's that are used in the code below + + [StructLayout(LayoutKind.Sequential)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public Int32 dwProcessID; + public Int32 dwThreadID; + } + + [Flags] + public enum LogonType + { + LOGON32_LOGON_INTERACTIVE = 2, + LOGON32_LOGON_NETWORK = 3, + LOGON32_LOGON_BATCH = 4, + LOGON32_LOGON_SERVICE = 5, + LOGON32_LOGON_UNLOCK = 7, + LOGON32_LOGON_NETWORK_CLEARTEXT = 8, + LOGON32_LOGON_NEW_CREDENTIALS = 9 + } + + [Flags] + public enum LogonProvider + { + LOGON32_PROVIDER_DEFAULT = 0, + LOGON32_PROVIDER_WINNT35, + LOGON32_PROVIDER_WINNT40, + LOGON32_PROVIDER_WINNT50 + } + [StructLayout(LayoutKind.Sequential)] + public struct SECURITY_ATTRIBUTES + { + public Int32 Length; + public IntPtr lpSecurityDescriptor; + public bool bInheritHandle; + } + + public enum SECURITY_IMPERSONATION_LEVEL + { + SecurityAnonymous, + SecurityIdentification, + SecurityImpersonation, + SecurityDelegation + } + + public enum TOKEN_TYPE + { + TokenPrimary = 1, + TokenImpersonation + } + + [StructLayout(LayoutKind.Sequential, Pack = 1)] + internal struct TokPriv1Luid + { + public int Count; + public long Luid; + public int Attr; + } + + public const int GENERIC_ALL_ACCESS = 0x10000000; + public const int CREATE_NO_WINDOW = 0x08000000; + internal const int SE_PRIVILEGE_ENABLED = 0x00000002; + internal const int TOKEN_QUERY = 0x00000008; + internal const int TOKEN_ADJUST_PRIVILEGES = 0x00000020; + internal const string SE_INCRASE_QUOTA = "SeIncreaseQuotaPrivilege"; + + [DllImport("kernel32.dll", + EntryPoint = "CloseHandle", SetLastError = true, + CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)] + public static extern bool CloseHandle(IntPtr handle); + + [DllImport("advapi32.dll", + EntryPoint = "CreateProcessAsUser", SetLastError = true, + CharSet = CharSet.Ansi, CallingConvention = CallingConvention.StdCall)] + public static extern bool CreateProcessAsUser( + IntPtr hToken, + string lpApplicationName, + string lpCommandLine, + ref SECURITY_ATTRIBUTES lpProcessAttributes, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + bool bInheritHandle, + Int32 dwCreationFlags, + IntPtr lpEnvrionment, + string lpCurrentDirectory, + ref STARTUPINFO lpStartupInfo, + ref PROCESS_INFORMATION lpProcessInformation + ); + + [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx")] + public static extern bool DuplicateTokenEx( + IntPtr hExistingToken, + Int32 dwDesiredAccess, + ref SECURITY_ATTRIBUTES lpThreadAttributes, + Int32 ImpersonationLevel, + Int32 dwTokenType, + ref IntPtr phNewToken + ); + + [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)] + public static extern Boolean LogonUser( + String lpszUserName, + String lpszDomain, + String lpszPassword, + LogonType dwLogonType, + LogonProvider dwLogonProvider, + out IntPtr phToken + ); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool AdjustTokenPrivileges( + IntPtr htok, + bool disall, + ref TokPriv1Luid newst, + int len, + IntPtr prev, + IntPtr relen + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern IntPtr GetCurrentProcess(); + + [DllImport("advapi32.dll", ExactSpelling = true, SetLastError = true)] + internal static extern bool OpenProcessToken( + IntPtr h, + int acc, + ref IntPtr phtok + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern int WaitForSingleObject( + IntPtr h, + int milliseconds + ); + + [DllImport("kernel32.dll", ExactSpelling = true)] + internal static extern bool GetExitCodeProcess( + IntPtr h, + out int exitcode + ); + + [DllImport("advapi32.dll", SetLastError = true)] + internal static extern bool LookupPrivilegeValue( + string host, + string name, + ref long pluid + ); + + public static void CreateProcessAsUser(string strCommand, string strDomain, string strName, string strPassword, ref int ExitCode ) + { + var hToken = IntPtr.Zero; + var hDupedToken = IntPtr.Zero; + TokPriv1Luid tp; + var pi = new PROCESS_INFORMATION(); + var sa = new SECURITY_ATTRIBUTES(); + sa.Length = Marshal.SizeOf(sa); + Boolean bResult = false; + try + { + bResult = LogonUser( + strName, + strDomain, + strPassword, + LogonType.LOGON32_LOGON_BATCH, + LogonProvider.LOGON32_PROVIDER_DEFAULT, + out hToken + ); + if (!bResult) + { + throw new Win32Exception("Logon error #" + Marshal.GetLastWin32Error().ToString()); + } + IntPtr hproc = GetCurrentProcess(); + IntPtr htok = IntPtr.Zero; + bResult = OpenProcessToken( + hproc, + TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, + ref htok + ); + if(!bResult) + { + throw new Win32Exception("Open process token error #" + Marshal.GetLastWin32Error().ToString()); + } + tp.Count = 1; + tp.Luid = 0; + tp.Attr = SE_PRIVILEGE_ENABLED; + bResult = LookupPrivilegeValue( + null, + SE_INCRASE_QUOTA, + ref tp.Luid + ); + if(!bResult) + { + throw new Win32Exception("Lookup privilege error #" + Marshal.GetLastWin32Error().ToString()); + } + bResult = AdjustTokenPrivileges( + htok, + false, + ref tp, + 0, + IntPtr.Zero, + IntPtr.Zero + ); + if(!bResult) + { + throw new Win32Exception("Token elevation error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = DuplicateTokenEx( + hToken, + GENERIC_ALL_ACCESS, + ref sa, + (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, + (int)TOKEN_TYPE.TokenPrimary, + ref hDupedToken + ); + if(!bResult) + { + throw new Win32Exception("Duplicate Token error #" + Marshal.GetLastWin32Error().ToString()); + } + var si = new STARTUPINFO(); + si.cb = Marshal.SizeOf(si); + si.lpDesktop = ""; + bResult = CreateProcessAsUser( + hDupedToken, + null, + strCommand, + ref sa, + ref sa, + false, + 0, + IntPtr.Zero, + null, + ref si, + ref pi + ); + if(!bResult) + { + throw new Win32Exception("Create process as user error #" + Marshal.GetLastWin32Error().ToString()); + } + + int status = WaitForSingleObject(pi.hProcess, -1); + if(status == -1) + { + throw new Win32Exception("Wait during create process failed user error #" + Marshal.GetLastWin32Error().ToString()); + } + + bResult = GetExitCodeProcess(pi.hProcess, out ExitCode); + if(!bResult) + { + throw new Win32Exception("Retrieving status error #" + Marshal.GetLastWin32Error().ToString()); + } + } + finally + { + if (pi.hThread != IntPtr.Zero) + { + CloseHandle(pi.hThread); + } + if (pi.hProcess != IntPtr.Zero) + { + CloseHandle(pi.hProcess); + } + if (hDupedToken != IntPtr.Zero) + { + CloseHandle(hDupedToken); + } + } + } + } +} + +"@ + Add-Type -TypeDefinition $ProgramSource -ReferencedAssemblies "System.ServiceProcess" +} + +#endregion + + +$params = Parse-Args $args; +$result = New-Object psobject; +Set-Attr $result "changed" $false; + +$path = Get-Attr -obj $params -name path -failifempty $true -resultobj $result +$name = Get-Attr -obj $params -name name -default $path +$productid = Get-Attr -obj $params -name productid -failifempty $true -resultobj $result +$arguments = Get-Attr -obj $params -name arguments +$ensure = Get-Attr -obj $params -name state -default "present" +if (!$ensure) +{ + $ensure = Get-Attr -obj $params -name ensure -default "present" +} +$username = Get-Attr -obj $params -name user_name +$password = Get-Attr -obj $params -name user_password +$return_code = Get-Attr -obj $params -name expected_return_code -default 0 + +#Construct the DSC param hashtable +$dscparams = @{ + name=$name + path=$path + productid = $productid + arguments = $arguments + ensure = $ensure + returncode = $return_code +} + +if (($username -ne $null) -and ($password -ne $null)) +{ + #Add network credential to the list + $secpassword = $password | ConvertTo-SecureString -AsPlainText -Force + $credential = New-Object pscredential -ArgumentList $username, $secpassword + $dscparams.add("Credential",$credential) +} + +#Always return the name +set-attr -obj $result -name "name" -value $name + +$testdscresult = Test-TargetResource @dscparams +if ($testdscresult -eq $true) +{ + Exit-Json -obj $result +} +Else +{ + try + { + set-TargetResource @dscparams + } + catch + { + $errormsg = $_[0].exception + } + + if ($errormsg) + { + Fail-Json -obj $result -message $errormsg.ToString() + } + Else + { + #Check if DSC thinks the computer needs a reboot: + if ($global:DSCMachineStatus -eq 1) + { + Set-Attr $result "restart_required" $true + } + + #Set-TargetResource did its job. We can assume a change has happened + Set-Attr $result "changed" $true + Exit-Json -obj $result + } +} + diff --git a/windows/win_package.py b/windows/win_package.py new file mode 100644 index 00000000000..3072dbed3de --- /dev/null +++ b/windows/win_package.py @@ -0,0 +1,87 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2014, 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 + +DOCUMENTATION = ''' +--- +module: win_package +version_added: "1.7" +short_description: Installs/Uninstalls a installable package, either from local file system or url +description: + - Installs or uninstalls a package +options: + path: + description: + - Location of the package to be installed (either on file system, network share or url) + required: true + default: null + aliases: [] + name: + description: + - name of the package. Just for logging reasons, will use the value of path if name isn't specified + required: false + default: null + aliases: [] + product_id: + description: + - product id of the installed package (used for checking if already installed) + required: false + default: null + aliases: [] + arguments: + description: + - Any arguments the installer needs + default: null + aliases: [] + state: + description: + - Install or Uninstall + choices: + - present + - absent + default: present + aliases: [ensure] + user_name: + description: + - Username of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_password for this to function properly. + default: null + aliases: [] + user_password: + description: + - Password of an account with access to the package if its located on a file share. Only needed if the winrm user doesn't have access to the package. Also specify user_name for this to function properly. + default: null + aliases: [] +author: Trond Hindenes +''' + +EXAMPLES = ''' +# Playbook example + - name: Install the vc thingy + win_package: + name="Microsoft Visual C thingy" + path="http://download.microsoft.com/download/1/6/B/16B06F60-3B20-4FF2-B699-5E9B7962F9AE/VSU_4/vcredist_x64.exe" + ProductId="{CF2BEA3C-26EA-32F8-AA9B-331F7E34BA97}" + Arguments="/install /passive /norestart" + + +''' +