diff --git a/.travis.yml b/.travis.yml
index 0e3a2af23b3..91d1b9585d7 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,3 +14,4 @@ script:
- python2.4 -m compileall -fq cloud/amazon/_ec2_ami_search.py cloud/amazon/ec2_facts.py
- python2.6 -m compileall -fq .
- python2.7 -m compileall -fq .
+ #- ./test-docs.sh core
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e441a4e3527..ea9c4ced04e 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -22,6 +22,10 @@ I'd also read the community page above, but in particular, make sure you copy [t
Also please make sure you are testing on the latest released version of Ansible or the development branch.
+If you'd like to contribute code to an existing module
+======================================================
+Each module in Core is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a +1 by the owner in the comments will be merged by the Ansible team.
+
Thanks!
diff --git a/cloud/amazon/_ec2_ami_search.py b/cloud/amazon/_ec2_ami_search.py
index 65953af2b5d..8ef0c0046ea 100644
--- a/cloud/amazon/_ec2_ami_search.py
+++ b/cloud/amazon/_ec2_ami_search.py
@@ -83,7 +83,6 @@ EXAMPLES = '''
import csv
import json
-import urllib2
import urlparse
SUPPORTED_DISTROS = ['ubuntu']
@@ -102,11 +101,12 @@ AWS_REGIONS = ['ap-northeast-1',
def get_url(module, url):
""" Get url and return response """
- try:
- r = urllib2.urlopen(url)
- except (urllib2.HTTPError, urllib2.URLError), e:
- code = getattr(e, 'code', -1)
- module.fail_json(msg="Request failed: %s" % str(e), status_code=code)
+
+ r, info = fetch_url(module, url)
+ if info['status'] != 200:
+ # Backwards compat
+ info['status_code'] = info['status']
+ module.fail_json(**info)
return r
@@ -182,7 +182,7 @@ def main():
choices=['i386', 'amd64']),
region=dict(required=False, default='us-east-1', choices=AWS_REGIONS),
virt=dict(required=False, default='paravirtual',
- choices=['paravirtual', 'hvm'])
+ choices=['paravirtual', 'hvm']),
)
module = AnsibleModule(argument_spec=arg_spec)
distro = module.params['distro']
@@ -196,6 +196,7 @@ def main():
# this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import *
+from ansible.module_utils.urls import *
if __name__ == '__main__':
main()
diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py
index dee292aeba3..236bc89000d 100644
--- a/cloud/amazon/cloudformation.py
+++ b/cloud/amazon/cloudformation.py
@@ -26,34 +26,28 @@ options:
description:
- name of the cloudformation stack
required: true
- default: null
- aliases: []
disable_rollback:
description:
- If a stacks fails to form, rollback will remove the stack
required: false
default: "false"
choices: [ "true", "false" ]
- aliases: []
template_parameters:
description:
- a list of hashes of all the template variables for the stack
required: false
default: {}
- aliases: []
state:
description:
- If state is "present", stack will be created. If state is "present" and if stack exists and template has changed, it will be updated.
If state is "absent", stack will be removed.
required: true
- default: null
- aliases: []
template:
description:
- The local path of the cloudformation template. This parameter is mutually exclusive with 'template_url'. Either one of them is required if "state" parameter is "present"
+ Must give full path to the file, relative to the working directory. If using roles this may look like "roles/cloudformation/files/cloudformation-example.json"
required: false
default: null
- aliases: []
notification_arns:
description:
- The Simple Notification Service (SNS) topic ARNs to publish stack related events.
@@ -65,21 +59,18 @@ options:
- the path of the cloudformation stack policy
required: false
default: null
- aliases: []
- version_added: "x.x"
+ version_added: "1.9"
tags:
description:
- Dictionary of tags to associate with stack and it's resources during stack creation. Cannot be updated later.
Requires at least Boto version 2.6.0.
required: false
default: null
- aliases: []
version_added: "1.4"
region:
description:
- The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used.
required: true
- default: null
aliases: ['aws_region', 'ec2_region']
version_added: "1.5"
template_url:
@@ -88,7 +79,8 @@ options:
required: false
version_added: "2.0"
template_format:
- description: For local templates, allows specification of json or yaml format
+ description:
+ - For local templates, allows specification of json or yaml format
default: json
choices: [ json, yaml ]
required: false
@@ -115,6 +107,22 @@ EXAMPLES = '''
tags:
Stack: "ansible-cloudformation"
+# Basic role example
+- name: launch ansible cloudformation example
+ cloudformation:
+ stack_name: "ansible-cloudformation"
+ state: "present"
+ region: "us-east-1"
+ disable_rollback: true
+ template: "roles/cloudformation/files/cloudformation-example.json"
+ template_parameters:
+ KeyName: "jmartin"
+ DiskType: "ephemeral"
+ InstanceType: "m1.small"
+ ClusterSize: 3
+ tags:
+ Stack: "ansible-cloudformation"
+
# Removal example
- name: tear down old deployment
cloudformation:
diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py
index b79395fb3a1..0b0d3c91127 100644
--- a/cloud/amazon/ec2.py
+++ b/cloud/amazon/ec2.py
@@ -144,7 +144,7 @@ options:
instance_tags:
version_added: "1.0"
description:
- - a hash/dictionary of tags to add to the new instance; '{"key":"value"}' and '{"key":"value","key":"value"}'
+ - a hash/dictionary of tags to add to the new instance or for starting/stopping instance by tag; '{"key":"value"}' and '{"key":"value","key":"value"}'
required: false
default: null
aliases: []
@@ -229,19 +229,26 @@ options:
exact_count:
version_added: "1.5"
description:
- - An integer value which indicates how many instances that match the 'count_tag' parameter should be running. Instances are either created or terminated based on this value.
+ - An integer value which indicates how many instances that match the 'count_tag' parameter should be running. Instances are either created or terminated based on this value.
required: false
default: null
aliases: []
count_tag:
version_added: "1.5"
description:
- - Used with 'exact_count' to determine how many nodes based on a specific tag criteria should be running. This can be expressed in multiple ways and is shown in the EXAMPLES section. For instance, one can request 25 servers that are tagged with "class=webserver".
+ - Used with 'exact_count' to determine how many nodes based on a specific tag criteria should be running. This can be expressed in multiple ways and is shown in the EXAMPLES section. For instance, one can request 25 servers that are tagged with "class=webserver".
required: false
default: null
aliases: []
+ network_interfaces:
+ version_added: "2.0"
+ description:
+ - A list of existing network interfaces to attach to the instance at launch. When specifying existing network interfaces, none of the assign_public_ip, private_ip, vpc_subnet_id, group, or group_id parameters may be used. (Those parameters are for creating a new network interface at launch.)
+ required: false
+ default: null
+ aliases: ['network_interface']
-author:
+author:
- "Tim Gerla (@tgerla)"
- "Lester Wade (@lwade)"
- "Seth Vidal"
@@ -271,7 +278,7 @@ EXAMPLES = '''
wait: yes
wait_timeout: 500
count: 5
- instance_tags:
+ instance_tags:
db: postgres
monitoring: yes
vpc_subnet_id: subnet-29e63245
@@ -305,7 +312,7 @@ EXAMPLES = '''
wait: yes
wait_timeout: 500
count: 5
- instance_tags:
+ instance_tags:
db: postgres
monitoring: yes
vpc_subnet_id: subnet-29e63245
@@ -352,6 +359,19 @@ EXAMPLES = '''
vpc_subnet_id: subnet-29e63245
assign_public_ip: yes
+# Examples using pre-existing network interfaces
+- ec2:
+ key_name: mykey
+ instance_type: t2.small
+ image: ami-f005ba11
+ network_interface: eni-deadbeef
+
+- ec2:
+ key_name: mykey
+ instance_type: t2.small
+ image: ami-f005ba11
+ network_interfaces: ['eni-deadbeef', 'eni-5ca1ab1e']
+
# Launch instances, runs some tasks
# and then terminate them
@@ -366,7 +386,7 @@ EXAMPLES = '''
region: us-east-1
tasks:
- name: Launch instance
- ec2:
+ ec2:
key_name: "{{ keypair }}"
group: "{{ security_group }}"
instance_type: "{{ instance_type }}"
@@ -446,6 +466,15 @@ EXAMPLES = '''
vpc_subnet_id: subnet-29e63245
assign_public_ip: yes
+#
+# Start stopped instances specified by tag
+#
+- local_action:
+ module: ec2
+ instance_tags:
+ Name: ExtraPower
+ state: running
+
#
# Enforce that 5 instances with a tag "foo" are running
# (Highly recommended!)
@@ -474,11 +503,11 @@ EXAMPLES = '''
image: ami-40603AD1
wait: yes
group: webserver
- instance_tags:
+ instance_tags:
Name: database
dbtype: postgres
exact_count: 5
- count_tag:
+ count_tag:
Name: database
dbtype: postgres
vpc_subnet_id: subnet-29e63245
@@ -531,8 +560,8 @@ def find_running_instances_by_count_tag(module, ec2, count_tag, zone=None):
for res in reservations:
if hasattr(res, 'instances'):
for inst in res.instances:
- instances.append(inst)
-
+ instances.append(inst)
+
return reservations, instances
@@ -543,7 +572,7 @@ def _set_none_to_blank(dictionary):
result[k] = _set_none_to_blank(result[k])
elif not result[k]:
result[k] = ""
- return result
+ return result
def get_reservations(module, ec2, tags=None, state=None, zone=None):
@@ -682,7 +711,7 @@ def create_block_device(module, ec2, volume):
# http://aws.amazon.com/about-aws/whats-new/2013/10/09/ebs-provisioned-iops-maximum-iops-gb-ratio-increased-to-30-1/
MAX_IOPS_TO_SIZE_RATIO = 30
if 'snapshot' not in volume and 'ephemeral' not in volume:
- if 'volume_size' not in volume:
+ if 'volume_size' not in volume:
module.fail_json(msg = 'Size must be specified when creating a new volume or modifying the root volume')
if 'snapshot' in volume:
if 'device_type' in volume and volume.get('device_type') == 'io1' and 'iops' not in volume:
@@ -692,8 +721,10 @@ def create_block_device(module, ec2, volume):
size = volume.get('volume_size', snapshot.volume_size)
if int(volume['iops']) > MAX_IOPS_TO_SIZE_RATIO * size:
module.fail_json(msg = 'IOPS must be at most %d times greater than size' % MAX_IOPS_TO_SIZE_RATIO)
+ if 'encrypted' in volume:
+ module.fail_json(msg = 'You can not set encyrption when creating a volume from a snapshot')
if 'ephemeral' in volume:
- if 'snapshot' in volume:
+ if 'snapshot' in volume:
module.fail_json(msg = 'Cannot set both ephemeral and snapshot')
return BlockDeviceType(snapshot_id=volume.get('snapshot'),
ephemeral_name=volume.get('ephemeral'),
@@ -701,8 +732,7 @@ def create_block_device(module, ec2, volume):
volume_type=volume.get('device_type'),
delete_on_termination=volume.get('delete_on_termination', False),
iops=volume.get('iops'),
- encrypted=volume.get('encrypted', False))
-
+ encrypted=volume.get('encrypted', None))
def boto_supports_param_in_spot_request(ec2, param):
"""
Check if Boto library has a in its request_spot_instances() method. For example, the placement_group parameter wasn't added until 2.3.0.
@@ -759,18 +789,18 @@ def enforce_count(module, ec2, vpc):
for inst in instance_dict_array:
inst['state'] = "terminated"
terminated_list.append(inst)
- instance_dict_array = terminated_list
-
- # ensure all instances are dictionaries
+ instance_dict_array = terminated_list
+
+ # ensure all instances are dictionaries
all_instances = []
for inst in instances:
if type(inst) is not dict:
inst = get_instance_info(inst)
- all_instances.append(inst)
+ all_instances.append(inst)
return (all_instances, instance_dict_array, changed_instance_ids, changed)
-
-
+
+
def create_instances(module, ec2, vpc, override_count=None):
"""
Creates new instances
@@ -816,6 +846,7 @@ def create_instances(module, ec2, vpc, override_count=None):
count_tag = module.params.get('count_tag')
source_dest_check = module.boolean(module.params.get('source_dest_check'))
termination_protection = module.boolean(module.params.get('termination_protection'))
+ network_interfaces = module.params.get('network_interfaces')
# group_id and group_name are exclusive of each other
if group_id and group_name:
@@ -823,7 +854,10 @@ def create_instances(module, ec2, vpc, override_count=None):
vpc_id = None
if vpc_subnet_id:
- vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
+ if not vpc:
+ module.fail_json(msg="region must be specified")
+ else:
+ vpc_id = vpc.get_all_subnets(subnet_ids=[vpc_subnet_id])[0].vpc_id
else:
vpc_id = None
@@ -878,7 +912,7 @@ def create_instances(module, ec2, vpc, override_count=None):
if ebs_optimized:
params['ebs_optimized'] = ebs_optimized
-
+
# 'tenancy' always has a default value, but it is not a valid parameter for spot instance resquest
if not spot_price:
params['tenancy'] = tenancy
@@ -911,21 +945,33 @@ def create_instances(module, ec2, vpc, override_count=None):
groups=group_id,
associate_public_ip_address=assign_public_ip)
interfaces = boto.ec2.networkinterface.NetworkInterfaceCollection(interface)
- params['network_interfaces'] = interfaces
+ params['network_interfaces'] = interfaces
else:
- params['subnet_id'] = vpc_subnet_id
- if vpc_subnet_id:
- params['security_group_ids'] = group_id
+ if network_interfaces:
+ if isinstance(network_interfaces, basestring):
+ network_interfaces = [network_interfaces]
+ interfaces = []
+ for i, network_interface_id in enumerate(network_interfaces):
+ interface = boto.ec2.networkinterface.NetworkInterfaceSpecification(
+ network_interface_id=network_interface_id,
+ device_index=i)
+ interfaces.append(interface)
+ params['network_interfaces'] = \
+ boto.ec2.networkinterface.NetworkInterfaceCollection(*interfaces)
else:
- params['security_groups'] = group_name
+ params['subnet_id'] = vpc_subnet_id
+ if vpc_subnet_id:
+ params['security_group_ids'] = group_id
+ else:
+ params['security_groups'] = group_name
if volumes:
bdm = BlockDeviceMapping()
- for volume in volumes:
+ for volume in volumes:
if 'device_name' not in volume:
module.fail_json(msg = 'Device name must be set for volume')
# Minimum volume size is 1GB. We'll use volume size explicitly set to 0
- # to be a signal not to create this volume
+ # to be a signal not to create this volume
if 'volume_size' not in volume or int(volume['volume_size']) > 0:
bdm[volume['device_name']] = create_block_device(module, ec2, volume)
@@ -1015,7 +1061,7 @@ def create_instances(module, ec2, vpc, override_count=None):
num_running = 0
wait_timeout = time.time() + wait_timeout
while wait_timeout > time.time() and num_running < len(instids):
- try:
+ try:
res_list = ec2.get_all_instances(instids)
except boto.exception.BotoServerError, e:
if e.error_code == 'InvalidInstanceID.NotFound':
@@ -1028,7 +1074,7 @@ def create_instances(module, ec2, vpc, override_count=None):
for res in res_list:
num_running += len([ i for i in res.instances if i.state=='running' ])
if len(res_list) <= 0:
- # got a bad response of some sort, possibly due to
+ # got a bad response of some sort, possibly due to
# stale/cached data. Wait a second and then try again
time.sleep(1)
continue
@@ -1140,12 +1186,12 @@ def terminate_instances(module, ec2, instance_ids):
filters={'instance-state-name':'terminated'}):
for inst in res.instances:
instance_dict_array.append(get_instance_info(inst))
-
+
return (changed, instance_dict_array, terminated_instance_ids)
-def startstop_instances(module, ec2, instance_ids, state):
+def startstop_instances(module, ec2, instance_ids, state, instance_tags):
"""
Starts or stops a list of existing instances
@@ -1153,6 +1199,8 @@ def startstop_instances(module, ec2, instance_ids, state):
ec2: authenticated ec2 connection object
instance_ids: The list of instances to start in the form of
[ {id: }, ..]
+ instance_tags: A dict of tag keys and values in the form of
+ {key: value, ... }
state: Intended state ("running" or "stopped")
Returns a dictionary of instance information
@@ -1161,19 +1209,33 @@ def startstop_instances(module, ec2, instance_ids, state):
If the instance was not able to change state,
"changed" will be set to False.
+ Note that if instance_ids and instance_tags are both non-empty,
+ this method will process the intersection of the two
"""
-
+
wait = module.params.get('wait')
wait_timeout = int(module.params.get('wait_timeout'))
changed = False
instance_dict_array = []
-
+
if not isinstance(instance_ids, list) or len(instance_ids) < 1:
- module.fail_json(msg='instance_ids should be a list of instances, aborting')
+ # Fail unless the user defined instance tags
+ if not instance_tags:
+ module.fail_json(msg='instance_ids should be a list of instances, aborting')
+
+ # To make an EC2 tag filter, we need to prepend 'tag:' to each key.
+ # An empty filter does no filtering, so it's safe to pass it to the
+ # get_all_instances method even if the user did not specify instance_tags
+ filters = {}
+ if instance_tags:
+ for key, value in instance_tags.items():
+ filters["tag:" + key] = value
+
+ # Check that our instances are not in the state we want to take
# Check (and eventually change) instances attributes and instances state
running_instances_array = []
- for res in ec2.get_all_instances(instance_ids):
+ for res in ec2.get_all_instances(instance_ids, filters=filters):
for inst in res.instances:
# Check "source_dest_check" attribute
@@ -1225,7 +1287,7 @@ def main():
argument_spec.update(dict(
key_name = dict(aliases = ['keypair']),
id = dict(),
- group = dict(type='list'),
+ group = dict(type='list', aliases=['groups']),
group_id = dict(type='list'),
zone = dict(aliases=['aws_zone', 'ec2_zone']),
instance_type = dict(aliases=['type']),
@@ -1255,6 +1317,7 @@ def main():
volumes = dict(type='list'),
ebs_optimized = dict(type='bool', default=False),
tenancy = dict(default='default'),
+ network_interfaces = dict(type='list', aliases=['network_interface'])
)
)
@@ -1263,7 +1326,12 @@ def main():
mutually_exclusive = [
['exact_count', 'count'],
['exact_count', 'state'],
- ['exact_count', 'instance_ids']
+ ['exact_count', 'instance_ids'],
+ ['network_interfaces', 'assign_public_ip'],
+ ['network_interfaces', 'group'],
+ ['network_interfaces', 'group_id'],
+ ['network_interfaces', 'private_ip'],
+ ['network_interfaces', 'vpc_subnet_id'],
],
)
@@ -1280,25 +1348,26 @@ def main():
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg = str(e))
else:
- module.fail_json(msg="region must be specified")
+ vpc = None
- tagged_instances = []
+ tagged_instances = []
- state = module.params.get('state')
+ state = module.params['state']
if state == 'absent':
- instance_ids = module.params.get('instance_ids')
- if not isinstance(instance_ids, list):
- module.fail_json(msg='termination_list needs to be a list of instances to terminate')
+ instance_ids = module.params['instance_ids']
+ if not instance_ids:
+ module.fail_json(msg='instance_ids list is required for absent state')
(changed, instance_dict_array, new_instance_ids) = terminate_instances(module, ec2, instance_ids)
elif state in ('running', 'stopped'):
instance_ids = module.params.get('instance_ids')
- if not isinstance(instance_ids, list):
- module.fail_json(msg='running list needs to be a list of instances to run: %s' % instance_ids)
+ instance_tags = module.params.get('instance_tags')
+ if not (isinstance(instance_ids, list) or isinstance(instance_tags, dict)):
+ module.fail_json(msg='running list needs to be a list of instances or set of tags to run: %s' % instance_ids)
- (changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state)
+ (changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state, instance_tags)
elif state == 'present':
# Changed is always set to true when provisioning new instances
diff --git a/cloud/amazon/ec2_ami_find.py b/cloud/amazon/ec2_ami_find.py
index c8aa5d792df..f5ed91baab5 100644
--- a/cloud/amazon/ec2_ami_find.py
+++ b/cloud/amazon/ec2_ami_find.py
@@ -18,7 +18,7 @@
DOCUMENTATION = '''
---
module: ec2_ami_find
-version_added: 2.0
+version_added: '2.0'
short_description: Searches for AMIs to obtain the AMI ID and other information
description:
- Returns list of matching AMIs with AMI ID, along with other useful information
diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py
index 54d051375e6..c78bf462a8a 100644
--- a/cloud/amazon/ec2_asg.py
+++ b/cloud/amazon/ec2_asg.py
@@ -43,7 +43,7 @@ options:
launch_config_name:
description:
- Name of the Launch configuration to use for the group. See the ec2_lc module for managing these.
- required: false
+ required: true
min_size:
description:
- Minimum number of instances in group
@@ -67,7 +67,7 @@ options:
- Number of instances you'd like to replace at a time. Used with replace_all_instances.
required: false
version_added: "1.8"
- default: 1
+ default: 1
replace_instances:
description:
- List of instance_ids belonging to the named ASG that you would like to terminate and be replaced with instances matching the current launch configuration.
@@ -109,6 +109,12 @@ options:
default: EC2
version_added: "1.7"
choices: ['EC2', 'ELB']
+ default_cooldown:
+ description:
+ - The number of seconds after a scaling activity completes before another can begin.
+ required: false
+ default: 300 seconds
+ version_added: "2.0"
wait_timeout:
description:
- how long before wait instances to become viable when replaced. Used in concjunction with instance_ids option.
@@ -120,6 +126,14 @@ options:
version_added: "1.9"
default: yes
required: False
+ termination_policies:
+ description:
+ - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity.
+ - For 'Default', when used to create a new autoscaling group, the "Default" value is used. When used to change an existent autoscaling group, the current termination policies are mantained
+ required: false
+ default: Default
+ choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default']
+ version_added: "2.0"
extends_documentation_fragment: aws
"""
@@ -374,6 +388,7 @@ def create_autoscaling_group(connection, module):
set_tags = module.params.get('tags')
health_check_period = module.params.get('health_check_period')
health_check_type = module.params.get('health_check_type')
+ default_cooldown = module.params.get('default_cooldown')
wait_for_instances = module.params.get('wait_for_instances')
as_groups = connection.get_all_groups(names=[group_name])
wait_timeout = module.params.get('wait_timeout')
@@ -413,7 +428,9 @@ def create_autoscaling_group(connection, module):
connection=connection,
tags=asg_tags,
health_check_period=health_check_period,
- health_check_type=health_check_type)
+ health_check_type=health_check_type,
+ default_cooldown=default_cooldown,
+ termination_policies=termination_policies)
try:
connection.create_auto_scaling_group(ag)
@@ -774,7 +791,9 @@ def main():
tags=dict(type='list', default=[]),
health_check_period=dict(type='int', default=300),
health_check_type=dict(default='EC2', choices=['EC2', 'ELB']),
- wait_for_instances=dict(type='bool', default=True)
+ default_cooldown=dict(type='int', default=300),
+ wait_for_instances=dict(type='bool', default=True),
+ termination_policies=dict(type='list', default=None)
),
)
diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py
index c3b764b2e63..5d6532b3955 100644
--- a/cloud/amazon/ec2_eip.py
+++ b/cloud/amazon/ec2_eip.py
@@ -1,16 +1,33 @@
#!/usr/bin/python
+# 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 .
+
DOCUMENTATION = '''
---
module: ec2_eip
short_description: associate an EC2 elastic IP with an instance.
description:
- This module associates AWS EC2 elastic IP addresses with instances
-version_added: 1.4
+version_added: "1.4"
options:
- instance_id:
+ device_id:
description:
- - The EC2 instance id
+ - The id of the device for the EIP. Can be an EC2 Instance id or Elastic Network Interface (ENI) id.
required: false
+ aliases: [ instance_id ]
+ version_added: "2.0"
public_ip:
description:
- The elastic IP address to associate with the instance.
@@ -37,14 +54,19 @@ options:
version_added: "1.4"
reuse_existing_ip_allowed:
description:
- - Reuse an EIP that is not associated to an instance (when available),'''
-''' instead of allocating a new one.
+ - Reuse an EIP that is not associated to an instance (when available), instead of allocating a new one.
required: false
default: false
version_added: "1.6"
-
+ release_on_disassociation:
+ description:
+ - whether or not to automatically release the EIP when it is disassociated
+ required: false
+ default: false
+ version_added: "2.0"
extends_documentation_fragment: aws
author: "Lorin Hochstein (@lorin) "
+author: "Rick Mendes (@rickmendes) "
notes:
- This module will return C(public_ip) on success, which will contain the
public IP address associated with the instance.
@@ -52,35 +74,36 @@ notes:
the cloud instance is reachable via the new address. Use wait_for and
pause to delay further playbook execution until the instance is reachable,
if necessary.
+ - This module returns multiple changed statuses on disassociation or release.
+ It returns an overall status based on any changes occuring. It also returns
+ individual changed statuses for disassociation and release.
'''
EXAMPLES = '''
- name: associate an elastic IP with an instance
- ec2_eip: instance_id=i-1212f003 ip=93.184.216.119
-
+ ec2_eip: device_id=i-1212f003 ip=93.184.216.119
+- name: associate an elastic IP with a device
+ ec2_eip: device_id=eni-c8ad70f3 ip=93.184.216.119
- name: disassociate an elastic IP from an instance
- ec2_eip: instance_id=i-1212f003 ip=93.184.216.119 state=absent
-
+ ec2_eip: device_id=i-1212f003 ip=93.184.216.119 state=absent
+- name: disassociate an elastic IP with a device
+ ec2_eip: device_id=eni-c8ad70f3 ip=93.184.216.119 state=absent
- name: allocate a new elastic IP and associate it with an instance
- ec2_eip: instance_id=i-1212f003
-
+ ec2_eip: device_id=i-1212f003
- name: allocate a new elastic IP without associating it to anything
action: ec2_eip
register: eip
- name: output the IP
debug: msg="Allocated IP is {{ eip.public_ip }}"
-
- name: another way of allocating an elastic IP without associating it to anything
ec2_eip: state='present'
-
- name: provision new instances with ec2
ec2: keypair=mykey instance_type=c1.medium image=emi-40603AD1 wait=yes'''
''' group=webserver count=3
register: ec2
- name: associate new elastic IPs with each of the instances
- ec2_eip: "instance_id={{ item }}"
+ ec2_eip: "device_id={{ item }}"
with_items: ec2.instance_ids
-
- name: allocate a new elastic IP inside a VPC in us-west-2
ec2_eip: region=us-west-2 in_vpc=yes
register: eip
@@ -98,27 +121,27 @@ except ImportError:
class EIPException(Exception):
pass
-
-def associate_ip_and_instance(ec2, address, instance_id, check_mode):
- if address_is_associated_with_instance(ec2, address, instance_id):
+def associate_ip_and_device(ec2, address, device_id, check_mode, isinstance=True):
+ if address_is_associated_with_device(ec2, address, device_id, isinstance):
return {'changed': False}
# If we're in check mode, nothing else to do
if not check_mode:
- if address.domain == 'vpc':
- res = ec2.associate_address(instance_id,
- allocation_id=address.allocation_id)
+ if isinstance:
+ if address.domain == "vpc":
+ res = ec2.associate_address(device_id, allocation_id=address.allocation_id)
+ else:
+ res = ec2.associate_address(device_id, public_ip=address.public_ip)
else:
- res = ec2.associate_address(instance_id,
- public_ip=address.public_ip)
+ res = ec2.associate_address(network_interface_id=device_id, allocation_id=address.allocation_id)
if not res:
raise EIPException('association failed')
return {'changed': True}
-def disassociate_ip_and_instance(ec2, address, instance_id, check_mode):
- if not address_is_associated_with_instance(ec2, address, instance_id):
+def disassociate_ip_and_device(ec2, address, device_id, check_mode, isinstance=True):
+ if not address_is_associated_with_device(ec2, address, device_id, isinstance):
return {'changed': False}
# If we're in check mode, nothing else to do
@@ -143,24 +166,33 @@ def _find_address_by_ip(ec2, public_ip):
raise
-def _find_address_by_instance_id(ec2, instance_id):
- addresses = ec2.get_all_addresses(None, {'instance-id': instance_id})
+def _find_address_by_device_id(ec2, device_id, isinstance=True):
+ if isinstance:
+ addresses = ec2.get_all_addresses(None, {'instance-id': device_id})
+ else:
+ addresses = ec2.get_all_addresses(None, {'network-interface-id': device_id})
if addresses:
return addresses[0]
-def find_address(ec2, public_ip, instance_id):
+def find_address(ec2, public_ip, device_id, isinstance=True):
""" Find an existing Elastic IP address """
if public_ip:
return _find_address_by_ip(ec2, public_ip)
- elif instance_id:
- return _find_address_by_instance_id(ec2, instance_id)
+ elif device_id and isinstance:
+ return _find_address_by_device_id(ec2, device_id)
+ elif device_id:
+ return _find_address_by_device_id(ec2, device_id, isinstance=False)
-def address_is_associated_with_instance(ec2, address, instance_id):
- """ Check if the elastic IP is currently associated with the instance """
+def address_is_associated_with_device(ec2, address, device_id, isinstance=True):
+ """ Check if the elastic IP is currently associated with the device """
+ address = ec2.get_all_addresses(address.public_ip)
if address:
- return address and address.instance_id == instance_id
+ if isinstance:
+ return address and address[0].instance_id == device_id
+ else:
+ return address and address[0].network_interface_id == device_id
return False
@@ -171,7 +203,7 @@ def allocate_address(ec2, domain, reuse_existing_ip_allowed):
all_addresses = ec2.get_all_addresses(filters=domain_filter)
unassociated_addresses = [a for a in all_addresses
- if not a.instance_id]
+ if not a.device_id]
if unassociated_addresses:
return unassociated_addresses[0]
@@ -189,21 +221,33 @@ def release_address(ec2, address, check_mode):
return {'changed': True}
-def find_instance(ec2, instance_id):
+def find_device(ec2, device_id, isinstance=True):
""" Attempt to find the EC2 instance and return it """
- reservations = ec2.get_all_reservations(instance_ids=[instance_id])
+ if isinstance:
+ try:
+ reservations = ec2.get_all_reservations(instance_ids=[device_id])
+ except boto.exception.EC2ResponseError, e:
+ module.fail_json(msg=str(e))
- if len(reservations) == 1:
- instances = reservations[0].instances
- if len(instances) == 1:
- return instances[0]
+ if len(reservations) == 1:
+ instances = reservations[0].instances
+ if len(instances) == 1:
+ return instances[0]
+ else:
+ try:
+ interfaces = ec2.get_all_network_interfaces(network_interface_ids=[device_id])
+ except boto.exception.EC2ResponseError, e:
+ module.fail_json(msg=str(e))
- raise EIPException("could not find instance" + instance_id)
+ if len(interfaces) == 1:
+ return interfaces[0]
+
+ raise EIPException("could not find instance" + device_id)
-def ensure_present(ec2, domain, address, instance_id,
- reuse_existing_ip_allowed, check_mode):
+def ensure_present(ec2, domain, address, device_id,
+ reuse_existing_ip_allowed, check_mode, isinstance=True):
changed = False
# Return the EIP object since we've been given a public IP
@@ -214,28 +258,39 @@ def ensure_present(ec2, domain, address, instance_id,
address = allocate_address(ec2, domain, reuse_existing_ip_allowed)
changed = True
- if instance_id:
+ if device_id:
# Allocate an IP for instance since no public_ip was provided
- instance = find_instance(ec2, instance_id)
+ if isinstance:
+ instance = find_device(ec2, device_id)
+ # Associate address object (provided or allocated) with instance
+ assoc_result = associate_ip_and_device(ec2, address, device_id,
+ check_mode)
+ else:
+ instance = find_device(ec2, device_id, isinstance=False)
+ # Associate address object (provided or allocated) with instance
+ assoc_result = associate_ip_and_device(ec2, address, device_id,
+ check_mode, isinstance=False)
+
if instance.vpc_id:
domain = 'vpc'
- # Associate address object (provided or allocated) with instance
- assoc_result = associate_ip_and_instance(ec2, address, instance_id,
- check_mode)
changed = changed or assoc_result['changed']
return {'changed': changed, 'public_ip': address.public_ip}
-def ensure_absent(ec2, domain, address, instance_id, check_mode):
+def ensure_absent(ec2, domain, address, device_id, check_mode, isinstance=True):
if not address:
return {'changed': False}
# disassociating address from instance
- if instance_id:
- return disassociate_ip_and_instance(ec2, address, instance_id,
- check_mode)
+ if device_id:
+ if isinstance:
+ return disassociate_ip_and_device(ec2, address, device_id,
+ check_mode)
+ else:
+ return disassociate_ip_and_device(ec2, address, device_id,
+ check_mode, isinstance=False)
# releasing address
else:
return release_address(ec2, address, check_mode)
@@ -244,13 +299,14 @@ def ensure_absent(ec2, domain, address, instance_id, check_mode):
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
- instance_id=dict(required=False),
+ device_id=dict(required=False, aliases=['instance_id']),
public_ip=dict(required=False, aliases=['ip']),
state=dict(required=False, default='present',
choices=['present', 'absent']),
in_vpc=dict(required=False, type='bool', default=False),
reuse_existing_ip_allowed=dict(required=False, type='bool',
default=False),
+ release_on_disassociation=dict(required=False, type='bool', default=False),
wait_timeout=dict(default=300),
))
@@ -264,28 +320,52 @@ def main():
ec2 = ec2_connect(module)
- instance_id = module.params.get('instance_id')
+ device_id = module.params.get('device_id')
public_ip = module.params.get('public_ip')
state = module.params.get('state')
in_vpc = module.params.get('in_vpc')
domain = 'vpc' if in_vpc else None
reuse_existing_ip_allowed = module.params.get('reuse_existing_ip_allowed')
+ release_on_disassociation = module.params.get('release_on_disassociation')
+
+ if device_id and device_id.startswith('i-'):
+ is_instance=True
+ elif device_id:
+ is_instance=False
try:
- address = find_address(ec2, public_ip, instance_id)
+ if device_id:
+ address = find_address(ec2, public_ip, device_id, isinstance=is_instance)
+ else:
+ address = False
if state == 'present':
- result = ensure_present(ec2, domain, address, instance_id,
+ if device_id:
+ result = ensure_present(ec2, domain, address, device_id,
reuse_existing_ip_allowed,
- module.check_mode)
+ module.check_mode, isinstance=is_instance)
+ else:
+ address = allocate_address(ec2, domain, reuse_existing_ip_allowed)
+ result = {'changed': True, 'public_ip': address.public_ip}
else:
- result = ensure_absent(ec2, domain, address, instance_id, module.check_mode)
+ if device_id:
+ disassociated = ensure_absent(ec2, domain, address, device_id, module.check_mode, isinstance=is_instance)
+
+ if release_on_disassociation and disassociated['changed']:
+ released = release_address(ec2, address, module.check_mode)
+ result = { 'changed': True, 'disassociated': disassociated, 'released': released }
+ else:
+ result = { 'changed': disassociated['changed'], 'disassociated': disassociated, 'released': { 'changed': False } }
+ else:
+ address = find_address(ec2, public_ip, None)
+ released = release_address(ec2, address, module.check_mode)
+ result = { 'changed': released['changed'], 'disassociated': { 'changed': False }, 'released': released }
+
except (boto.exception.EC2ResponseError, EIPException) as e:
module.fail_json(msg=str(e))
module.exit_json(**result)
-
# import module snippets
from ansible.module_utils.basic import * # noqa
from ansible.module_utils.ec2 import * # noqa
diff --git a/cloud/amazon/ec2_elb_lb.py b/cloud/amazon/ec2_elb_lb.py
index 566db2d329a..8c739e1a2b2 100644
--- a/cloud/amazon/ec2_elb_lb.py
+++ b/cloud/amazon/ec2_elb_lb.py
@@ -22,7 +22,9 @@ description:
- Will be marked changed when called only if state is changed.
short_description: Creates or destroys Amazon ELB.
version_added: "1.5"
-author: "Jim Dalton (@jsdalton)"
+author:
+ - "Jim Dalton (@jsdalton)"
+ - "Rick Mendes (@rickmendes)"
options:
state:
description:
@@ -56,6 +58,12 @@ options:
require: false
default: None
version_added: "1.6"
+ security_group_names:
+ description:
+ - A list of security group names to apply to the elb
+ require: false
+ default: None
+ version_added: "2.0"
health_check:
description:
- An associative array of health check configuration settings (see example)
@@ -68,7 +76,7 @@ options:
aliases: ['aws_region', 'ec2_region']
subnets:
description:
- - A list of VPC subnets to use when creating ELB. Zones should be empty if using this.
+ - A list of VPC subnets to use when creating ELB. Zones should be empty if using this.
required: false
default: None
aliases: []
@@ -77,7 +85,7 @@ options:
description:
- Purge existing subnet on ELB that are not found in subnets
required: false
- default: false
+ default: false
version_added: "1.7"
scheme:
description:
@@ -147,7 +155,7 @@ EXAMPLES = """
name: "test-vpc"
scheme: internal
state: present
- subnets:
+ subnets:
- subnet-abcd1234
- subnet-1a2b3c4d
listeners:
@@ -213,7 +221,7 @@ EXAMPLES = """
instance_port: 80
purge_zones: yes
-# Creates a ELB and assigns a list of subnets to it.
+# Creates a ELB and assigns a list of subnets to it.
- local_action:
module: ec2_elb_lb
state: present
@@ -297,10 +305,10 @@ class ElbManager(object):
"""Handles ELB creation and destruction"""
def __init__(self, module, name, listeners=None, purge_listeners=None,
- zones=None, purge_zones=None, security_group_ids=None,
+ zones=None, purge_zones=None, security_group_ids=None,
health_check=None, subnets=None, purge_subnets=None,
scheme="internet-facing", connection_draining_timeout=None,
- cross_az_load_balancing=None,
+ cross_az_load_balancing=None,
stickiness=None, region=None, **aws_connect_params):
self.module = module
@@ -361,7 +369,8 @@ class ElbManager(object):
if not check_elb:
info = {
'name': self.name,
- 'status': self.status
+ 'status': self.status,
+ 'region': self.region
}
else:
try:
@@ -384,9 +393,34 @@ class ElbManager(object):
'hosted_zone_name': check_elb.canonical_hosted_zone_name,
'hosted_zone_id': check_elb.canonical_hosted_zone_name_id,
'lb_cookie_policy': lb_cookie_policy,
- 'app_cookie_policy': app_cookie_policy
+ 'app_cookie_policy': app_cookie_policy,
+ 'instances': [instance.id for instance in check_elb.instances],
+ 'out_of_service_count': 0,
+ 'in_service_count': 0,
+ 'unknown_instance_state_count': 0,
+ 'region': self.region
}
+ # status of instances behind the ELB
+ if info['instances']:
+ info['instance_health'] = [ dict(
+ instance_id = instance_state.instance_id,
+ reason_code = instance_state.reason_code,
+ state = instance_state.state
+ ) for instance_state in self.elb_conn.describe_instance_health(self.name)]
+ else:
+ info['instance_health'] = []
+
+ # instance state counts: InService or OutOfService
+ if info['instance_health']:
+ for instance_state in info['instance_health']:
+ if instance_state['state'] == "InService":
+ info['in_service_count'] += 1
+ elif instance_state['state'] == "OutOfService":
+ info['out_of_service_count'] += 1
+ else:
+ info['unknown_instance_state_count'] += 1
+
if check_elb.health_check:
info['health_check'] = {
'target': check_elb.health_check.target,
@@ -418,7 +452,7 @@ class ElbManager(object):
else:
info['cross_az_load_balancing'] = 'no'
- # return stickiness info?
+ # return stickiness info?
return info
@@ -539,8 +573,8 @@ class ElbManager(object):
# N.B. string manipulations on protocols below (str(), upper()) is to
# ensure format matches output from ELB API
listener_list = [
- listener['load_balancer_port'],
- listener['instance_port'],
+ int(listener['load_balancer_port']),
+ int(listener['instance_port']),
str(listener['protocol'].upper()),
]
@@ -598,7 +632,7 @@ class ElbManager(object):
self._attach_subnets(subnets_to_attach)
if subnets_to_detach:
self._detach_subnets(subnets_to_detach)
-
+
def _set_zones(self):
"""Determine which zones need to be enabled or disabled on the ELB"""
if self.zones:
@@ -703,7 +737,7 @@ class ElbManager(object):
else:
self._create_policy(policy_attrs['param_value'], policy_attrs['method'], policy[0])
self.changed = True
-
+
self._set_listener_policy(listeners_dict, policy)
def select_stickiness_policy(self):
@@ -770,7 +804,7 @@ class ElbManager(object):
else:
self._set_listener_policy(listeners_dict)
-
+
def _get_health_check_target(self):
"""Compose target string from healthcheck parameters"""
protocol = self.health_check['ping_protocol'].upper()
@@ -792,6 +826,7 @@ def main():
zones={'default': None, 'required': False, 'type': 'list'},
purge_zones={'default': False, 'required': False, 'type': 'bool'},
security_group_ids={'default': None, 'required': False, 'type': 'list'},
+ security_group_names={'default': None, 'required': False, 'type': 'list'},
health_check={'default': None, 'required': False, 'type': 'dict'},
subnets={'default': None, 'required': False, 'type': 'list'},
purge_subnets={'default': False, 'required': False, 'type': 'bool'},
@@ -804,6 +839,7 @@ def main():
module = AnsibleModule(
argument_spec=argument_spec,
+ mutually_exclusive = [['security_group_ids', 'security_group_names']]
)
if not HAS_BOTO:
@@ -820,6 +856,7 @@ def main():
zones = module.params['zones']
purge_zones = module.params['purge_zones']
security_group_ids = module.params['security_group_ids']
+ security_group_names = module.params['security_group_names']
health_check = module.params['health_check']
subnets = module.params['subnets']
purge_subnets = module.params['purge_subnets']
@@ -834,6 +871,21 @@ def main():
if state == 'present' and not (zones or subnets):
module.fail_json(msg="At least one availability zone or subnet is required for ELB creation")
+ if security_group_names:
+ security_group_ids = []
+ try:
+ ec2 = ec2_connect(module)
+ grp_details = ec2.get_all_security_groups()
+
+ for group_name in security_group_names:
+ if isinstance(group_name, basestring):
+ group_name = [group_name]
+
+ group_id = [ str(grp.id) for grp in grp_details if str(grp.name) in group_name ]
+ security_group_ids.extend(group_id)
+ except boto.exception.NoAuthHandlerFound, e:
+ module.fail_json(msg = str(e))
+
elb_man = ElbManager(module, name, listeners, purge_listeners, zones,
purge_zones, security_group_ids, health_check,
subnets, purge_subnets, scheme,
diff --git a/cloud/amazon/ec2_facts.py b/cloud/amazon/ec2_facts.py
index 6bd587bf018..5147428f646 100644
--- a/cloud/amazon/ec2_facts.py
+++ b/cloud/amazon/ec2_facts.py
@@ -29,7 +29,7 @@ options:
required: false
default: 'yes'
choices: ['yes', 'no']
- version_added: 1.5.1
+ version_added: '1.5.1'
description:
- This module fetches data from the metadata servers in ec2 (aws) as per
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html.
diff --git a/cloud/amazon/ec2_group.py b/cloud/amazon/ec2_group.py
index bde2f5cc19e..d2fe04c968d 100644
--- a/cloud/amazon/ec2_group.py
+++ b/cloud/amazon/ec2_group.py
@@ -1,6 +1,19 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-
+# 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 .
DOCUMENTATION = '''
---
@@ -336,19 +349,24 @@ def main():
rule['from_port'] = None
rule['to_port'] = None
- # If rule already exists, don't later delete it
- ruleId = make_rule_key('in', rule, group_id, ip)
- if ruleId in groupRules:
- del groupRules[ruleId]
- # Otherwise, add new rule
- else:
- grantGroup = None
- if group_id:
- grantGroup = groups[group_id]
+ # Convert ip to list we can iterate over
+ if not isinstance(ip, list):
+ ip = [ip]
- if not module.check_mode:
- group.authorize(rule['proto'], rule['from_port'], rule['to_port'], ip, grantGroup)
- changed = True
+ # If rule already exists, don't later delete it
+ for thisip in ip:
+ ruleId = make_rule_key('in', rule, group_id, thisip)
+ if ruleId in groupRules:
+ del groupRules[ruleId]
+ # Otherwise, add new rule
+ else:
+ grantGroup = None
+ if group_id:
+ grantGroup = groups[group_id]
+
+ if not module.check_mode:
+ group.authorize(rule['proto'], rule['from_port'], rule['to_port'], thisip, grantGroup)
+ changed = True
# Finally, remove anything left in the groupRules -- these will be defunct rules
if purge_rules:
@@ -383,25 +401,30 @@ def main():
rule['from_port'] = None
rule['to_port'] = None
- # If rule already exists, don't later delete it
- ruleId = make_rule_key('out', rule, group_id, ip)
- if ruleId in groupRules:
- del groupRules[ruleId]
- # Otherwise, add new rule
- else:
- grantGroup = None
- if group_id:
- grantGroup = groups[group_id].id
+ # Convert ip to list we can iterate over
+ if not isinstance(ip, list):
+ ip = [ip]
- if not module.check_mode:
- ec2.authorize_security_group_egress(
- group_id=group.id,
- ip_protocol=rule['proto'],
- from_port=rule['from_port'],
- to_port=rule['to_port'],
- src_group_id=grantGroup,
- cidr_ip=ip)
- changed = True
+ # If rule already exists, don't later delete it
+ for thisip in ip:
+ ruleId = make_rule_key('out', rule, group_id, thisip)
+ if ruleId in groupRules:
+ del groupRules[ruleId]
+ # Otherwise, add new rule
+ else:
+ grantGroup = None
+ if group_id:
+ grantGroup = groups[group_id].id
+
+ if not module.check_mode:
+ ec2.authorize_security_group_egress(
+ group_id=group.id,
+ ip_protocol=rule['proto'],
+ from_port=rule['from_port'],
+ to_port=rule['to_port'],
+ src_group_id=grantGroup,
+ cidr_ip=thisip)
+ changed = True
elif vpc_id and not module.check_mode:
# when using a vpc, but no egress rules are specified,
# we add in a default allow all out rule, which was the
diff --git a/cloud/amazon/ec2_key.py b/cloud/amazon/ec2_key.py
index a9217bd69db..fc33257cf34 100644
--- a/cloud/amazon/ec2_key.py
+++ b/cloud/amazon/ec2_key.py
@@ -1,6 +1,19 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
-
+# 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 .
DOCUMENTATION = '''
---
@@ -127,25 +140,23 @@ def main():
if state == 'absent':
if key:
'''found a match, delete it'''
- try:
- key.delete()
- if wait:
- start = time.time()
- action_complete = False
- while (time.time() - start) < wait_timeout:
- if not ec2.get_key_pair(name):
- action_complete = True
- break
- time.sleep(1)
- if not action_complete:
- module.fail_json(msg="timed out while waiting for the key to be removed")
- except Exception, e:
- module.fail_json(msg="Unable to delete key pair '%s' - %s" % (key, e))
- else:
- key = None
- changed = True
- else:
- '''no match found, no changes required'''
+ if not module.check_mode:
+ try:
+ key.delete()
+ if wait:
+ start = time.time()
+ action_complete = False
+ while (time.time() - start) < wait_timeout:
+ if not ec2.get_key_pair(name):
+ action_complete = True
+ break
+ time.sleep(1)
+ if not action_complete:
+ module.fail_json(msg="timed out while waiting for the key to be removed")
+ except Exception, e:
+ module.fail_json(msg="Unable to delete key pair '%s' - %s" % (key, e))
+ key = None
+ changed = True
# Ensure requested key is present
elif state == 'present':
diff --git a/cloud/amazon/ec2_lc.py b/cloud/amazon/ec2_lc.py
index 3c292377a58..fa6c64490ad 100644
--- a/cloud/amazon/ec2_lc.py
+++ b/cloud/amazon/ec2_lc.py
@@ -77,7 +77,7 @@ options:
- Kernel id for the EC2 instance
required: false
default: null
- aliases: []
+ aliases: []
spot_price:
description:
- The spot price you are bidding. Only applies for an autoscaling group with spot instances.
@@ -116,6 +116,18 @@ options:
default: false
aliases: []
version_added: "1.8"
+ classic_link_vpc_id:
+ description:
+ - Id of ClassicLink enabled VPC
+ required: false
+ default: null
+ version_added: "2.0"
+ classic_link_vpc_security_groups:
+ description:
+ - A list of security group id's with which to associate the ClassicLink VPC instances.
+ required: false
+ default: null
+ version_added: "2.0"
extends_documentation_fragment: aws
"""
@@ -184,6 +196,8 @@ def create_launch_config(connection, module):
ramdisk_id = module.params.get('ramdisk_id')
instance_profile_name = module.params.get('instance_profile_name')
ebs_optimized = module.params.get('ebs_optimized')
+ classic_link_vpc_id = module.params.get('classic_link_vpc_id')
+ classic_link_vpc_security_groups = module.params.get('classic_link_vpc_security_groups')
bdm = BlockDeviceMapping()
if volumes:
@@ -206,10 +220,12 @@ def create_launch_config(connection, module):
kernel_id=kernel_id,
spot_price=spot_price,
instance_monitoring=instance_monitoring,
- associate_public_ip_address = assign_public_ip,
+ associate_public_ip_address=assign_public_ip,
ramdisk_id=ramdisk_id,
instance_profile_name=instance_profile_name,
ebs_optimized=ebs_optimized,
+ classic_link_vpc_security_groups=classic_link_vpc_security_groups,
+ classic_link_vpc_id=classic_link_vpc_id,
)
launch_configs = connection.get_all_launch_configurations(names=[name])
@@ -221,11 +237,37 @@ def create_launch_config(connection, module):
changed = True
except BotoServerError, e:
module.fail_json(msg=str(e))
- result = launch_configs[0]
- module.exit_json(changed=changed, name=result.name, created_time=str(result.created_time),
- image_id=result.image_id, arn=result.launch_configuration_arn,
- security_groups=result.security_groups, instance_type=instance_type)
+ result = dict(
+ ((a[0], a[1]) for a in vars(launch_configs[0]).items()
+ if a[0] not in ('connection', 'created_time', 'instance_monitoring', 'block_device_mappings'))
+ )
+ result['created_time'] = str(launch_configs[0].created_time)
+ # Looking at boto's launchconfig.py, it looks like this could be a boolean
+ # value or an object with an enabled attribute. The enabled attribute
+ # could be a boolean or a string representation of a boolean. Since
+ # I can't test all permutations myself to see if my reading of the code is
+ # correct, have to code this *very* defensively
+ if launch_configs[0].instance_monitoring is True:
+ result['instance_monitoring'] = True
+ else:
+ try:
+ result['instance_monitoring'] = module.boolean(launch_configs[0].instance_monitoring.enabled)
+ except AttributeError:
+ result['instance_monitoring'] = False
+ if launch_configs[0].block_device_mappings is not None:
+ result['block_device_mappings'] = []
+ for bdm in launch_configs[0].block_device_mappings:
+ result['block_device_mappings'].append(dict(device_name=bdm.device_name, virtual_name=bdm.virtual_name))
+ if bdm.ebs is not None:
+ result['block_device_mappings'][-1]['ebs'] = dict(snapshot_id=bdm.ebs.snapshot_id, volume_size=bdm.ebs.volume_size)
+
+
+ module.exit_json(changed=changed, name=result['name'], created_time=result['created_time'],
+ image_id=result['image_id'], arn=result['launch_configuration_arn'],
+ security_groups=result['security_groups'],
+ instance_type=result['instance_type'],
+ result=result)
def delete_launch_config(connection, module):
@@ -257,7 +299,9 @@ def main():
ebs_optimized=dict(default=False, type='bool'),
associate_public_ip_address=dict(type='bool'),
instance_monitoring=dict(default=False, type='bool'),
- assign_public_ip=dict(type='bool')
+ assign_public_ip=dict(type='bool'),
+ classic_link_vpc_security_groups=dict(type='list'),
+ classic_link_vpc_id=dict(type='str')
)
)
diff --git a/cloud/amazon/ec2_metric_alarm.py b/cloud/amazon/ec2_metric_alarm.py
index 578a1af7297..b9ac1524794 100644
--- a/cloud/amazon/ec2_metric_alarm.py
+++ b/cloud/amazon/ec2_metric_alarm.py
@@ -184,7 +184,7 @@ def create_metric_alarm(connection, module):
comparisons = {'<=' : 'LessThanOrEqualToThreshold', '<' : 'LessThanThreshold', '>=' : 'GreaterThanOrEqualToThreshold', '>' : 'GreaterThanThreshold'}
alarm.comparison = comparisons[comparison]
- dim1 = module.params.get('dimensions')
+ dim1 = module.params.get('dimensions', {})
dim2 = alarm.dimensions
for keys in dim1:
diff --git a/cloud/amazon/ec2_scaling_policy.py b/cloud/amazon/ec2_scaling_policy.py
index 10f03e9fc46..2856644ee9c 100644
--- a/cloud/amazon/ec2_scaling_policy.py
+++ b/cloud/amazon/ec2_scaling_policy.py
@@ -1,4 +1,18 @@
#!/usr/bin/python
+# 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 .
DOCUMENTATION = """
module: ec2_scaling_policy
diff --git a/cloud/amazon/ec2_snapshot.py b/cloud/amazon/ec2_snapshot.py
index ee9d5ab1110..29fd559bea5 100644
--- a/cloud/amazon/ec2_snapshot.py
+++ b/cloud/amazon/ec2_snapshot.py
@@ -74,6 +74,12 @@ options:
- snapshot id to remove
required: false
version_added: "1.9"
+ last_snapshot_min_age:
+ description:
+ - If the volume's most recent snapshot has started less than `last_snapshot_min_age' minutes ago, a new snapshot will not be created.
+ required: false
+ default: 0
+ version_added: "1.9"
author: "Will Thames (@willthames)"
extends_documentation_fragment: aws
@@ -82,7 +88,7 @@ extends_documentation_fragment: aws
EXAMPLES = '''
# Simple snapshot of volume using volume_id
- ec2_snapshot:
- volume_id: vol-abcdef12
+ volume_id: vol-abcdef12
description: snapshot of /data from DB123 taken 2013/11/28 12:18:32
# Snapshot of volume mounted on device_name attached to instance_id
@@ -104,9 +110,16 @@ EXAMPLES = '''
module: ec2_snapshot
snapshot_id: snap-abcd1234
state: absent
-'''
+
+# Create a snapshot only if the most recent one is older than 1 hour
+- local_action:
+ module: ec2_snapshot
+ volume_id: vol-abcdef12
+ last_snapshot_min_age: 60
+'''
import time
+import datetime
try:
import boto.ec2
@@ -115,7 +128,128 @@ except ImportError:
HAS_BOTO = False
-def main():
+# Find the most recent snapshot
+def _get_snapshot_starttime(snap):
+ return datetime.datetime.strptime(snap.start_time, '%Y-%m-%dT%H:%M:%S.000Z')
+
+
+def _get_most_recent_snapshot(snapshots, max_snapshot_age_secs=None, now=None):
+ """
+ Gets the most recently created snapshot and optionally filters the result
+ if the snapshot is too old
+ :param snapshots: list of snapshots to search
+ :param max_snapshot_age_secs: filter the result if its older than this
+ :param now: simulate time -- used for unit testing
+ :return:
+ """
+ if len(snapshots) == 0:
+ return None
+
+ if not now:
+ now = datetime.datetime.utcnow()
+
+ youngest_snapshot = min(snapshots, key=_get_snapshot_starttime)
+
+ # See if the snapshot is younger that the given max age
+ snapshot_start = datetime.datetime.strptime(youngest_snapshot.start_time, '%Y-%m-%dT%H:%M:%S.000Z')
+ snapshot_age = now - snapshot_start
+
+ if max_snapshot_age_secs is not None:
+ if snapshot_age.total_seconds() > max_snapshot_age_secs:
+ return None
+
+ return youngest_snapshot
+
+
+def _create_with_wait(snapshot, wait_timeout_secs, sleep_func=time.sleep):
+ """
+ Wait for the snapshot to be created
+ :param snapshot:
+ :param wait_timeout_secs: fail this step after this many seconds
+ :param sleep_func:
+ :return:
+ """
+ time_waited = 0
+ snapshot.update()
+ while snapshot.status != 'completed':
+ sleep_func(3)
+ snapshot.update()
+ time_waited += 3
+ if wait_timeout_secs and time_waited > wait_timeout_secs:
+ return False
+ return True
+
+
+def create_snapshot(module, ec2, state=None, description=None, wait=None,
+ wait_timeout=None, volume_id=None, instance_id=None,
+ snapshot_id=None, device_name=None, snapshot_tags=None,
+ last_snapshot_min_age=None):
+ snapshot = None
+ changed = False
+
+ required = [volume_id, snapshot_id, instance_id]
+ if required.count(None) != len(required) - 1: # only 1 must be set
+ module.fail_json(msg='One and only one of volume_id or instance_id or snapshot_id must be specified')
+ if instance_id and not device_name or device_name and not instance_id:
+ module.fail_json(msg='Instance ID and device name must both be specified')
+
+ if instance_id:
+ try:
+ volumes = ec2.get_all_volumes(filters={'attachment.instance-id': instance_id, 'attachment.device': device_name})
+ except boto.exception.BotoServerError, e:
+ module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
+
+ if not volumes:
+ module.fail_json(msg="Could not find volume with name %s attached to instance %s" % (device_name, instance_id))
+
+ volume_id = volumes[0].id
+
+ if state == 'absent':
+ if not snapshot_id:
+ module.fail_json(msg = 'snapshot_id must be set when state is absent')
+ try:
+ ec2.delete_snapshot(snapshot_id)
+ except boto.exception.BotoServerError, e:
+ # exception is raised if snapshot does not exist
+ if e.error_code == 'InvalidSnapshot.NotFound':
+ module.exit_json(changed=False)
+ else:
+ module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
+
+ # successful delete
+ module.exit_json(changed=True)
+
+ if last_snapshot_min_age > 0:
+ try:
+ current_snapshots = ec2.get_all_snapshots(filters={'volume_id': volume_id})
+ except boto.exception.BotoServerError, e:
+ module.fail_json(msg="%s: %s" % (e.error_code, e.error_message))
+
+ last_snapshot_min_age = last_snapshot_min_age * 60 # Convert to seconds
+ snapshot = _get_most_recent_snapshot(current_snapshots,
+ max_snapshot_age_secs=last_snapshot_min_age)
+ try:
+ # Create a new snapshot if we didn't find an existing one to use
+ if snapshot is None:
+ snapshot = ec2.create_snapshot(volume_id, description=description)
+ changed = True
+ if wait:
+ if not _create_with_wait(snapshot, wait_timeout):
+ module.fail_json(msg='Timed out while creating snapshot.')
+ if snapshot_tags:
+ for k, v in snapshot_tags.items():
+ snapshot.add_tag(k, v)
+ except boto.exception.BotoServerError, e:
+ module.fail_json(msg="%s: %s" % (e.error_code, e.error_message))
+
+ module.exit_json(changed=changed,
+ snapshot_id=snapshot.id,
+ volume_id=snapshot.volume_id,
+ volume_size=snapshot.volume_size,
+ tags=snapshot.tags.copy())
+
+
+def create_snapshot_ansible_module():
argument_spec = ec2_argument_spec()
argument_spec.update(
dict(
@@ -124,13 +258,19 @@ def main():
instance_id = dict(),
snapshot_id = dict(),
device_name = dict(),
- wait = dict(type='bool', default='true'),
- wait_timeout = dict(default=0),
+ wait = dict(type='bool', default=True),
+ wait_timeout = dict(type='int', default=0),
+ last_snapshot_min_age = dict(type='int', default=0),
snapshot_tags = dict(type='dict', default=dict()),
state = dict(choices=['absent','present'], default='present'),
)
)
module = AnsibleModule(argument_spec=argument_spec)
+ return module
+
+
+def main():
+ module = create_snapshot_ansible_module()
if not HAS_BOTO:
module.fail_json(msg='boto required for this module')
@@ -142,60 +282,30 @@ def main():
device_name = module.params.get('device_name')
wait = module.params.get('wait')
wait_timeout = module.params.get('wait_timeout')
+ last_snapshot_min_age = module.params.get('last_snapshot_min_age')
snapshot_tags = module.params.get('snapshot_tags')
state = module.params.get('state')
- if not volume_id and not instance_id and not snapshot_id or volume_id and instance_id and snapshot_id:
- module.fail_json('One and only one of volume_id or instance_id or snapshot_id must be specified')
- if instance_id and not device_name or device_name and not instance_id:
- module.fail_json('Instance ID and device name must both be specified')
-
ec2 = ec2_connect(module)
- if instance_id:
- try:
- volumes = ec2.get_all_volumes(filters={'attachment.instance-id': instance_id, 'attachment.device': device_name})
- if not volumes:
- module.fail_json(msg="Could not find volume with name %s attached to instance %s" % (device_name, instance_id))
- volume_id = volumes[0].id
- except boto.exception.BotoServerError, e:
- module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
-
- if state == 'absent':
- if not snapshot_id:
- module.fail_json(msg = 'snapshot_id must be set when state is absent')
- try:
- snapshots = ec2.get_all_snapshots([snapshot_id])
- ec2.delete_snapshot(snapshot_id)
- module.exit_json(changed=True)
- except boto.exception.BotoServerError, e:
- # exception is raised if snapshot does not exist
- if e.error_code == 'InvalidSnapshot.NotFound':
- module.exit_json(changed=False)
- else:
- module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
-
- try:
- snapshot = ec2.create_snapshot(volume_id, description=description)
- time_waited = 0
- if wait:
- snapshot.update()
- while snapshot.status != 'completed':
- time.sleep(3)
- snapshot.update()
- time_waited += 3
- if wait_timeout and time_waited > wait_timeout:
- module.fail_json('Timed out while creating snapshot.')
- for k, v in snapshot_tags.items():
- snapshot.add_tag(k, v)
- except boto.exception.BotoServerError, e:
- module.fail_json(msg = "%s: %s" % (e.error_code, e.error_message))
-
- module.exit_json(changed=True, snapshot_id=snapshot.id, volume_id=snapshot.volume_id,
- volume_size=snapshot.volume_size, tags=snapshot.tags.copy())
+ create_snapshot(
+ module=module,
+ state=state,
+ description=description,
+ wait=wait,
+ wait_timeout=wait_timeout,
+ ec2=ec2,
+ volume_id=volume_id,
+ instance_id=instance_id,
+ snapshot_id=snapshot_id,
+ device_name=device_name,
+ snapshot_tags=snapshot_tags,
+ last_snapshot_min_age=last_snapshot_min_age
+ )
# import module snippets
from ansible.module_utils.basic import *
from ansible.module_utils.ec2 import *
-main()
+if __name__ == '__main__':
+ main()
diff --git a/cloud/amazon/ec2_vol.py b/cloud/amazon/ec2_vol.py
index 712be248af3..228bb12cfbc 100644
--- a/cloud/amazon/ec2_vol.py
+++ b/cloud/amazon/ec2_vol.py
@@ -140,12 +140,13 @@ EXAMPLES = '''
- ec2_vol:
instance: "{{ item.id }} "
volume_size: 5
- with_items: ec2.instances
+ with_items: ec2.instances
register: ec2_vol
# Example: Launch an instance and then add a volume if not already attached
# * Volume will be created with the given name if not already created.
# * Nothing will happen if the volume is already attached.
+# * Requires Ansible 2.0
- ec2:
keypair: "{{ keypair }}"
@@ -436,11 +437,11 @@ def main():
# Delaying the checks until after the instance check allows us to get volume ids for existing volumes
# without needing to pass an unused volume_size
- if not volume_size and not (id or name):
- module.fail_json(msg="You must specify an existing volume with id or name or a volume_size")
+ if not volume_size and not (id or name or snapshot):
+ module.fail_json(msg="You must specify volume_size or identify an existing volume by id, name, or snapshot")
- if volume_size and id:
- module.fail_json(msg="Cannot specify volume_size and id")
+ if volume_size and (id or snapshot):
+ module.fail_json(msg="Cannot specify volume_size together with id or snapshot")
if state == 'absent':
delete_volume(module, ec2)
diff --git a/cloud/amazon/ec2_vpc_net.py b/cloud/amazon/ec2_vpc_net.py
new file mode 100644
index 00000000000..2ee730f59cb
--- /dev/null
+++ b/cloud/amazon/ec2_vpc_net.py
@@ -0,0 +1,295 @@
+#!/usr/bin/python
+# 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 .
+
+DOCUMENTATION = '''
+---
+module: ec2_vpc_net
+short_description: Configure AWS virtual private clouds
+description:
+ - Create or terminate AWS virtual private clouds. This module has a dependency on python-boto.
+version_added: "2.0"
+author: Jonathan Davila (@defionscode)
+options:
+ name:
+ description:
+ - The name to give your VPC. This is used in combination with the cidr_block paramater to determine if a VPC already exists.
+ required: yes
+ cidr_block:
+ description:
+ - The CIDR of the VPC
+ required: yes
+ tenancy:
+ description:
+ - Whether to be default or dedicated tenancy. This cannot be changed after the VPC has been created.
+ required: false
+ default: default
+ choices: [ 'default', 'dedicated' ]
+ dns_support:
+ description:
+ - Whether to enable AWS DNS support.
+ required: false
+ default: yes
+ choices: [ 'yes', 'no' ]
+ dns_hostnames:
+ description:
+ - Whether to enable AWS hostname support.
+ required: false
+ default: yes
+ choices: [ 'yes', 'no' ]
+ dhcp_opts_id:
+ description:
+ - the id of the DHCP options to use for this vpc
+ default: null
+ required: false
+ tags:
+ description:
+ - The tags you want attached to the VPC. This is independent of the name value, note if you pass a 'Name' key it would override the Name of the VPC if it's different.
+ default: None
+ required: false
+ aliases: [ 'resource_tags' ]
+ state:
+ description:
+ - The state of the VPC. Either absent or present.
+ default: present
+ required: false
+ choices: [ 'present', 'absent' ]
+ multi_ok:
+ description:
+ - By default the module will not create another VPC if there is another VPC with the same name and CIDR block. Specify this as true if you want duplicate VPCs created.
+ default: false
+ required: false
+
+extends_documentation_fragment: aws
+'''
+
+EXAMPLES = '''
+# Note: These examples do not set authentication details, see the AWS Guide for details.
+
+# Create a VPC with dedicate tenancy and a couple of tags
+
+- ec2_vpc_net:
+ name: Module_dev2
+ cidr_block: 10.10.0.0/16
+ region: us-east-1
+ tags:
+ module: ec2_vpc_net
+ this: works
+ tenancy: dedicated
+
+'''
+
+import time
+import sys
+
+try:
+ import boto
+ import boto.ec2
+ import boto.vpc
+ from boto.exception import BotoServerError
+ HAS_BOTO=True
+except ImportError:
+ HAS_BOTO=False
+
+def boto_exception(err):
+ '''generic error message handler'''
+ if hasattr(err, 'error_message'):
+ error = err.error_message
+ elif hasattr(err, 'message'):
+ error = err.message
+ else:
+ error = '%s: %s' % (Exception, err)
+
+ return error
+
+def vpc_exists(module, vpc, name, cidr_block, multi):
+ """Returns True or False in regards to the existence of a VPC. When supplied
+ with a CIDR, it will check for matching tags to determine if it is a match
+ otherwise it will assume the VPC does not exist and thus return false.
+ """
+ matched_vpc = None
+
+ try:
+ matching_vpcs=vpc.get_all_vpcs(filters={'tag:Name' : name, 'cidr-block' : cidr_block})
+ except Exception, e:
+ e_msg=boto_exception(e)
+ module.fail_json(msg=e_msg)
+
+ if len(matching_vpcs) == 1:
+ matched_vpc = matching_vpcs[0]
+ elif len(matching_vpcs) > 1:
+ if multi:
+ module.fail_json(msg='Currently there are %d VPCs that have the same name and '
+ 'CIDR block you specified. If you would like to create '
+ 'the VPC anyway please pass True to the multi_ok param.' % len(matching_vpcs))
+
+ return matched_vpc
+
+
+def update_vpc_tags(vpc, module, vpc_obj, tags, name):
+
+ if tags is None:
+ tags = dict()
+
+ tags.update({'Name': name})
+ try:
+ current_tags = dict((t.name, t.value) for t in vpc.get_all_tags(filters={'resource-id': vpc_obj.id}))
+ if cmp(tags, current_tags):
+ vpc.create_tags(vpc_obj.id, tags)
+ return True
+ else:
+ return False
+ except Exception, e:
+ e_msg=boto_exception(e)
+ module.fail_json(msg=e_msg)
+
+
+def update_dhcp_opts(connection, module, vpc_obj, dhcp_id):
+
+ if vpc_obj.dhcp_options_id != dhcp_id:
+ connection.associate_dhcp_options(dhcp_id, vpc_obj.id)
+ return True
+ else:
+ return False
+
+def get_vpc_values(vpc_obj):
+
+ if vpc_obj is not None:
+ vpc_values = vpc_obj.__dict__
+ if "region" in vpc_values:
+ vpc_values.pop("region")
+ if "item" in vpc_values:
+ vpc_values.pop("item")
+ if "connection" in vpc_values:
+ vpc_values.pop("connection")
+ return vpc_values
+ else:
+ return None
+
+def main():
+ argument_spec=ec2_argument_spec()
+ argument_spec.update(dict(
+ name = dict(type='str', default=None, required=True),
+ cidr_block = dict(type='str', default=None, required=True),
+ tenancy = dict(choices=['default', 'dedicated'], default='default'),
+ dns_support = dict(type='bool', default=True),
+ dns_hostnames = dict(type='bool', default=True),
+ dhcp_opts_id = dict(type='str', default=None, required=False),
+ tags = dict(type='dict', required=False, default=None, aliases=['resource_tags']),
+ state = dict(choices=['present', 'absent'], default='present'),
+ multi_ok = dict(type='bool', default=False)
+ )
+ )
+
+ module = AnsibleModule(
+ argument_spec=argument_spec,
+ )
+
+ if not HAS_BOTO:
+ module.fail_json(msg='boto is required for this module')
+
+ name=module.params.get('name')
+ cidr_block=module.params.get('cidr_block')
+ tenancy=module.params.get('tenancy')
+ dns_support=module.params.get('dns_support')
+ dns_hostnames=module.params.get('dns_hostnames')
+ dhcp_id=module.params.get('dhcp_opts_id')
+ tags=module.params.get('tags')
+ state=module.params.get('state')
+ multi=module.params.get('multi_ok')
+
+ changed=False
+
+ region, ec2_url, aws_connect_params = get_aws_connection_info(module)
+
+ if region:
+ try:
+ connection = connect_to_aws(boto.vpc, region, **aws_connect_params)
+ except (boto.exception.NoAuthHandlerFound, StandardError), e:
+ module.fail_json(msg=str(e))
+ else:
+ module.fail_json(msg="region must be specified")
+
+ if dns_hostnames and not dns_support:
+ module.fail_json('In order to enable DNS Hostnames you must also enable DNS support')
+
+ if state == 'present':
+
+ # Check if VPC exists
+ vpc_obj = vpc_exists(module, connection, name, cidr_block, multi)
+
+ if vpc_obj is None:
+ try:
+ vpc_obj = connection.create_vpc(cidr_block, instance_tenancy=tenancy)
+ changed = True
+ except BotoServerError, e:
+ module.fail_json(msg=e)
+
+ if dhcp_id is not None:
+ try:
+ if update_dhcp_opts(connection, module, vpc_obj, dhcp_id):
+ changed = True
+ except BotoServerError, e:
+ module.fail_json(msg=e)
+
+ if tags is not None or name is not None:
+ try:
+ if update_vpc_tags(connection, module, vpc_obj, tags, name):
+ changed = True
+ except BotoServerError, e:
+ module.fail_json(msg=e)
+
+
+ # Note: Boto currently doesn't currently provide an interface to ec2-describe-vpc-attribute
+ # which is needed in order to detect the current status of DNS options. For now we just update
+ # the attribute each time and is not used as a changed-factor.
+ try:
+ connection.modify_vpc_attribute(vpc_obj.id, enable_dns_support=dns_support)
+ connection.modify_vpc_attribute(vpc_obj.id, enable_dns_hostnames=dns_hostnames)
+ except BotoServerError, e:
+ e_msg=boto_exception(e)
+ module.fail_json(msg=e_msg)
+
+ # get the vpc obj again in case it has changed
+ try:
+ vpc_obj = connection.get_all_vpcs(vpc_obj.id)[0]
+ except BotoServerError, e:
+ e_msg=boto_exception(e)
+ module.fail_json(msg=e_msg)
+
+ module.exit_json(changed=changed, vpc=get_vpc_values(vpc_obj))
+
+ elif state == 'absent':
+
+ # Check if VPC exists
+ vpc_obj = vpc_exists(module, connection, name, cidr_block, multi)
+
+ if vpc_obj is not None:
+ try:
+ connection.delete_vpc(vpc_obj.id)
+ vpc_obj = None
+ changed = True
+ except BotoServerError, e:
+ e_msg = boto_exception(e)
+ module.fail_json(msg="%s. You may want to use the ec2_vpc_subnet, ec2_vpc_igw, "
+ "and/or ec2_vpc_route_table modules to ensure the other components are absent." % e_msg)
+
+ module.exit_json(changed=changed, vpc=get_vpc_values(vpc_obj))
+
+# import module snippets
+from ansible.module_utils.basic import *
+from ansible.module_utils.ec2 import *
+
+main()
diff --git a/cloud/amazon/elasticache.py b/cloud/amazon/elasticache.py
index 3ec0fc2e351..31ed4696628 100644
--- a/cloud/amazon/elasticache.py
+++ b/cloud/amazon/elasticache.py
@@ -42,7 +42,7 @@ options:
description:
- The version number of the cache engine
required: false
- default: 1.4.14
+ default: none
node_type:
description:
- The compute and memory capacity of the nodes in the cache cluster
@@ -56,7 +56,7 @@ options:
description:
- The port number on which each of the cache nodes will accept connections
required: false
- default: 11211
+ default: none
cache_subnet_group:
description:
- The subnet group name to associate with. Only use if inside a vpc. Required if inside a vpc
@@ -477,10 +477,10 @@ def main():
state={'required': True, 'choices': ['present', 'absent', 'rebooted']},
name={'required': True},
engine={'required': False, 'default': 'memcached'},
- cache_engine_version={'required': False, 'default': '1.4.14'},
+ cache_engine_version={'required': False},
node_type={'required': False, 'default': 'cache.m1.small'},
num_nodes={'required': False, 'default': None, 'type': 'int'},
- cache_port={'required': False, 'default': 11211, 'type': 'int'},
+ cache_port={'required': False, 'type': 'int'},
cache_subnet_group={'required': False, 'default': None},
cache_security_groups={'required': False, 'default': [default],
'type': 'list'},
diff --git a/cloud/amazon/iam.py b/cloud/amazon/iam.py
index bda953faab4..29481ea6a5e 100644
--- a/cloud/amazon/iam.py
+++ b/cloud/amazon/iam.py
@@ -280,12 +280,6 @@ def update_user(module, iam, name, new_name, new_path, key_state, key_count, key
module.fail_json(changed=False, msg="Passsword doesn't conform to policy")
else:
module.fail_json(msg=error_msg)
- else:
- try:
- iam.delete_login_profile(name)
- changed = True
- except boto.exception.BotoServerError:
- pass
if key_state == 'create':
try:
@@ -509,7 +503,7 @@ def main():
groups=dict(type='list', default=None, required=False),
state=dict(
default=None, required=True, choices=['present', 'absent', 'update']),
- password=dict(default=None, required=False),
+ password=dict(default=None, required=False, no_log=True),
update_password=dict(default='always', required=False, choices=['always', 'on_create']),
access_key_state=dict(default=None, required=False, choices=[
'active', 'inactive', 'create', 'remove',
diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py
index f1a6abdd0a6..9213d1585b0 100644
--- a/cloud/amazon/iam_policy.py
+++ b/cloud/amazon/iam_policy.py
@@ -40,7 +40,12 @@ options:
aliases: []
policy_document:
description:
- - The path to the properly json formatted policy file
+ - The path to the properly json formatted policy file (mutually exclusive with C(policy_json))
+ required: false
+ aliases: []
+ policy_json:
+ description:
+ - A properly json formatted policy as string (mutually exclusive with C(policy_document), see https://github.com/ansible/ansible/issues/7005#issuecomment-42894813 on how to use it properly)
required: false
aliases: []
state:
@@ -109,16 +114,29 @@ task:
state: present
with_items: new_groups.results
+# Create a new S3 policy with prefix per user
+tasks:
+- name: Create S3 policy from template
+ iam_policy:
+ iam_type: user
+ iam_name: "{{ item.user }}"
+ policy_name: "s3_limited_access_{{ item.prefix }}"
+ state: present
+ policy_json: " {{ lookup( 'template', 's3_policy.json.j2') }} "
+ with_items:
+ - user: s3_user
+ prefix: s3_user_prefix
+
'''
import json
import urllib
-import sys
try:
import boto
import boto.iam
+ import boto.ec2
+ HAS_BOTO = True
except ImportError:
- print "failed=True msg='boto required for this module'"
- sys.exit(1)
+ HAS_BOTO = False
def boto_exception(err):
'''generic error message handler'''
@@ -271,6 +289,7 @@ def main():
iam_name=dict(default=None, required=False),
policy_name=dict(default=None, required=True),
policy_document=dict(default=None, required=False),
+ policy_json=dict(type='str', default=None, required=False),
skip_duplicates=dict(type='bool', default=True, required=False)
))
@@ -278,26 +297,35 @@ def main():
argument_spec=argument_spec,
)
+ if not HAS_BOTO:
+ module.fail_json(msg='boto required for this module')
+
state = module.params.get('state').lower()
iam_type = module.params.get('iam_type').lower()
state = module.params.get('state')
name = module.params.get('iam_name')
policy_name = module.params.get('policy_name')
skip = module.params.get('skip_duplicates')
+
+ if module.params.get('policy_document') != None and module.params.get('policy_json') != None:
+ module.fail_json(msg='Only one of "policy_document" or "policy_json" may be set')
+
if module.params.get('policy_document') != None:
with open(module.params.get('policy_document'), 'r') as json_data:
pdoc = json.dumps(json.load(json_data))
json_data.close()
+ elif module.params.get('policy_json') != None:
+ try:
+ pdoc = json.dumps(json.loads(module.params.get('policy_json')))
+ except Exception as e:
+ module.fail_json(msg=str(e) + '\n' + module.params.get('policy_json'))
else:
pdoc=None
- ec2_url, aws_access_key, aws_secret_key, region = get_ec2_creds(module)
+ region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
try:
- iam = boto.iam.connection.IAMConnection(
- aws_access_key_id=aws_access_key,
- aws_secret_access_key=aws_secret_key,
- )
+ iam = boto.iam.connection.IAMConnection(**aws_connect_kwargs)
except boto.exception.NoAuthHandlerFound, e:
module.fail_json(msg=str(e))
diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py
index 71ead8ad10b..d56c4ae12de 100644
--- a/cloud/amazon/rds.py
+++ b/cloud/amazon/rds.py
@@ -24,147 +24,123 @@ description:
options:
command:
description:
- - Specifies the action to take.
+ - Specifies the action to take.
required: true
- default: null
- aliases: []
- choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'restore' ]
+ choices: [ 'create', 'replicate', 'delete', 'facts', 'modify' , 'promote', 'snapshot', 'reboot', 'restore' ]
instance_name:
description:
- Database instance identifier. Required except when using command=facts or command=delete on just a snapshot
required: false
default: null
- aliases: []
source_instance:
description:
- Name of the database to replicate. Used only when command=replicate.
required: false
default: null
- aliases: []
db_engine:
description:
- - The type of database. Used only when command=create.
+ - The type of database. Used only when command=create.
required: false
default: null
- aliases: []
choices: [ 'MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres']
size:
description:
- Size in gigabytes of the initial storage for the DB instance. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
instance_type:
description:
- - The instance type of the database. Must be specified when command=create. Optional when command=replicate, command=modify or command=restore. If not specified then the replica inherits the same instance type as the source instance.
+ - The instance type of the database. Must be specified when command=create. Optional when command=replicate, command=modify or command=restore. If not specified then the replica inherits the same instance type as the source instance.
required: false
default: null
- aliases: []
username:
description:
- Master database username. Used only when command=create.
required: false
default: null
- aliases: []
password:
description:
- Password for the master database username. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: true
- default: null
aliases: [ 'aws_region', 'ec2_region' ]
db_name:
description:
- Name of a database to create within the instance. If not specified then no database is created. Used only when command=create.
required: false
default: null
- aliases: []
engine_version:
description:
- Version number of the database engine to use. Used only when command=create. If not specified then the current Amazon RDS default engine version is used.
required: false
default: null
- aliases: []
parameter_group:
description:
- Name of the DB parameter group to associate with this instance. If omitted then the RDS default DBParameterGroup will be used. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
license_model:
description:
- - The license model for this DB instance. Used only when command=create or command=restore.
+ - The license model for this DB instance. Used only when command=create or command=restore.
required: false
default: null
- aliases: []
choices: [ 'license-included', 'bring-your-own-license', 'general-public-license', 'postgresql-license' ]
multi_zone:
description:
- Specifies if this is a Multi-availability-zone deployment. Can not be used in conjunction with zone parameter. Used only when command=create or command=modify.
- choices: [ "yes", "no" ]
+ choices: [ "yes", "no" ]
required: false
default: null
- aliases: []
iops:
description:
- Specifies the number of IOPS for the instance. Used only when command=create or command=modify. Must be an integer greater than 1000.
required: false
default: null
- aliases: []
security_groups:
description:
- Comma separated list of one or more security groups. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
vpc_security_groups:
description:
- Comma separated list of one or more vpc security group ids. Also requires `subnet` to be specified. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
port:
description:
- Port number that the DB instance uses for connections. Defaults to 3306 for mysql. Must be changed to 1521 for Oracle, 1433 for SQL Server, 5432 for PostgreSQL. Used only when command=create or command=replicate.
required: false
default: null
- aliases: []
upgrade:
description:
- Indicates that minor version upgrades should be applied automatically. Used only when command=create or command=replicate.
required: false
default: no
choices: [ "yes", "no" ]
- aliases: []
option_group:
description:
- The name of the option group to use. If not specified then the default option group is used. Used only when command=create.
required: false
default: null
- aliases: []
maint_window:
description:
- "Maintenance window in format of ddd:hh24:mi-ddd:hh24:mi. (Example: Mon:22:00-Mon:23:15) If not specified then a random maintenance window is assigned. Used only when command=create or command=modify."
required: false
default: null
- aliases: []
backup_window:
description:
- Backup window in format of hh24:mi-hh24:mi. If not specified then a random backup window is assigned. Used only when command=create or command=modify.
required: false
default: null
- aliases: []
backup_retention:
description:
- "Number of days backups are retained. Set to 0 to disable backups. Default is 1 day. Valid range: 0-35. Used only when command=create or command=modify."
required: false
default: null
- aliases: []
zone:
description:
- availability zone in which to launch the instance. Used only when command=create, command=replicate or command=restore.
@@ -176,18 +152,15 @@ options:
- VPC subnet group. If specified then a VPC instance is created. Used only when command=create.
required: false
default: null
- aliases: []
snapshot:
description:
- Name of snapshot to take. When command=delete, if no snapshot name is provided then no snapshot is taken. If used with command=delete with no instance_name, the snapshot is deleted. Used with command=facts, command=delete or command=snapshot.
required: false
default: null
- aliases: []
aws_secret_key:
description:
- AWS secret key. If not set then the value of the AWS_SECRET_KEY environment variable is used.
required: false
- default: null
aliases: [ 'ec2_secret_key', 'secret_key' ]
aws_access_key:
description:
@@ -201,46 +174,46 @@ options:
required: false
default: "no"
choices: [ "yes", "no" ]
- aliases: []
wait_timeout:
description:
- how long before wait gives up, in seconds
default: 300
- aliases: []
apply_immediately:
description:
- Used only when command=modify. If enabled, the modifications will be applied as soon as possible rather than waiting for the next preferred maintenance window.
default: no
choices: [ "yes", "no" ]
- aliases: []
+ force_failover:
+ description:
+ - Used only when command=reboot. If enabled, the reboot is done using a MultiAZ failover.
+ required: false
+ default: "no"
+ choices: [ "yes", "no" ]
+ version_added: "2.0"
new_instance_name:
description:
- Name to rename an instance to. Used only when command=modify.
required: false
default: null
- aliases: []
- version_added: 1.5
+ version_added: "1.5"
character_set_name:
description:
- Associate the DB instance with a specified character set. Used with command=create.
required: false
default: null
- aliases: []
- version_added: 1.9
+ version_added: "1.9"
publicly_accessible:
description:
- explicitly set whether the resource should be publicly accessible or not. Used with command=create, command=replicate. Requires boto >= 2.26.0
required: false
default: null
- aliases: []
- version_added: 1.9
+ version_added: "1.9"
tags:
description:
- tags dict to apply to a resource. Used with command=create, command=replicate, command=restore. Requires boto >= 2.26.0
required: false
default: null
- aliases: []
- version_added: 1.9
+ version_added: "1.9"
requirements:
- "python >= 2.6"
- "boto"
@@ -292,6 +265,13 @@ EXAMPLES = '''
instance_name: new-database
new_instance_name: renamed-database
wait: yes
+
+# Reboot an instance and wait for it to become available again
+- rds
+ command: reboot
+ instance_name: database
+ wait: yes
+
'''
import sys
@@ -380,6 +360,13 @@ class RDSConnection:
except boto.exception.BotoServerError, e:
raise RDSException(e)
+ def reboot_db_instance(self, instance_name, **params):
+ try:
+ result = self.connection.reboot_dbinstance(instance_name)
+ return RDSDBInstance(result)
+ except boto.exception.BotoServerError, e:
+ raise RDSException(e)
+
def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params):
try:
result = self.connection.restore_dbinstance_from_dbsnapshot(snapshot, instance_name, instance_type, **params)
@@ -464,6 +451,13 @@ class RDS2Connection:
except boto.exception.BotoServerError, e:
raise RDSException(e)
+ def reboot_db_instance(self, instance_name, **params):
+ try:
+ result = self.connection.reboot_db_instance(instance_name, **params)['RebootDBInstanceResponse']['RebootDBInstanceResult']['DBInstance']
+ return RDS2DBInstance(result)
+ except boto.exception.BotoServerError, e:
+ raise RDSException(e)
+
def restore_db_instance_from_db_snapshot(self, instance_name, snapshot, instance_type, **params):
try:
result = self.connection.restore_db_instance_from_db_snapshot(instance_name, snapshot, **params)['RestoreDBInstanceFromDBSnapshotResponse']['RestoreDBInstanceFromDBSnapshotResult']['DBInstance']
@@ -616,16 +610,16 @@ def await_resource(conn, resource, status, module):
while wait_timeout > time.time() and resource.status != status:
time.sleep(5)
if wait_timeout <= time.time():
- module.fail_json(msg="Timeout waiting for resource %s" % resource.id)
+ module.fail_json(msg="Timeout waiting for RDS resource %s" % resource.name)
if module.params.get('command') == 'snapshot':
# Temporary until all the rds2 commands have their responses parsed
if resource.name is None:
- module.fail_json(msg="Problem with snapshot %s" % resource.snapshot)
+ module.fail_json(msg="There was a problem waiting for RDS snapshot %s" % resource.snapshot)
resource = conn.get_db_snapshot(resource.name)
else:
# Temporary until all the rds2 commands have their responses parsed
if resource.name is None:
- module.fail_json(msg="Problem with instance %s" % resource.instance)
+ module.fail_json(msg="There was a problem waiting for RDS instance %s" % resource.instance)
resource = conn.get_db_instance(resource.name)
if resource is None:
break
@@ -659,7 +653,7 @@ def create_db_instance(module, conn):
module.params.get('username'), module.params.get('password'), **params)
changed = True
except RDSException, e:
- module.fail_json(msg="failed to create instance: %s" % e.message)
+ module.fail_json(msg="Failed to create instance: %s" % e.message)
if module.params.get('wait'):
resource = await_resource(conn, result, 'available', module)
@@ -686,7 +680,7 @@ def replicate_db_instance(module, conn):
result = conn.create_db_instance_read_replica(instance_name, source_instance, **params)
changed = True
except RDSException, e:
- module.fail_json(msg="failed to create replica instance: %s " % e.message)
+ module.fail_json(msg="Failed to create replica instance: %s " % e.message)
if module.params.get('wait'):
resource = await_resource(conn, result, 'available', module)
@@ -715,14 +709,17 @@ def delete_db_instance_or_snapshot(module, conn):
if instance_name:
if snapshot:
params["skip_final_snapshot"] = False
- params["final_snapshot_id"] = snapshot
+ if has_rds2:
+ params["final_db_snapshot_identifier"] = snapshot
+ else:
+ params["final_snapshot_id"] = snapshot
else:
params["skip_final_snapshot"] = True
result = conn.delete_db_instance(instance_name, **params)
else:
result = conn.delete_db_snapshot(snapshot)
except RDSException, e:
- module.fail_json(msg="failed to delete instance: %s" % e.message)
+ module.fail_json(msg="Failed to delete instance: %s" % e.message)
# If we're not waiting for a delete to complete then we're all done
# so just return
@@ -748,11 +745,11 @@ def facts_db_instance_or_snapshot(module, conn):
snapshot = module.params.get('snapshot')
if instance_name and snapshot:
- module.fail_json(msg="facts must be called with either instance_name or snapshot, not both")
+ module.fail_json(msg="Facts must be called with either instance_name or snapshot, not both")
if instance_name:
resource = conn.get_db_instance(instance_name)
if not resource:
- module.fail_json(msg="DB Instance %s does not exist" % instance_name)
+ module.fail_json(msg="DB instance %s does not exist" % instance_name)
if snapshot:
resource = conn.get_db_snapshot(snapshot)
if not resource:
@@ -844,6 +841,31 @@ def snapshot_db_instance(module, conn):
module.exit_json(changed=changed, snapshot=resource.get_data())
+def reboot_db_instance(module, conn):
+ required_vars = ['instance_name']
+ valid_vars = []
+
+ if has_rds2:
+ valid_vars.append('force_failover')
+
+ params = validate_parameters(required_vars, valid_vars, module)
+ instance_name = module.params.get('instance_name')
+ result = conn.get_db_instance(instance_name)
+ changed = False
+ try:
+ result = conn.reboot_db_instance(instance_name, **params)
+ changed = True
+ except RDSException, e:
+ module.fail_json(msg=e.message)
+
+ if module.params.get('wait'):
+ resource = await_resource(conn, result, 'available', module)
+ else:
+ resource = conn.get_db_instance(instance_name)
+
+ module.exit_json(changed=changed, instance=resource.get_data())
+
+
def restore_db_instance(module, conn):
required_vars = ['instance_name', 'snapshot']
valid_vars = ['db_name', 'iops', 'license_model', 'multi_zone',
@@ -915,6 +937,7 @@ def validate_parameters(required_vars, valid_vars, module):
'instance_type': 'db_instance_class',
'password': 'master_user_password',
'new_instance_name': 'new_db_instance_identifier',
+ 'force_failover': 'force_failover',
}
if has_rds2:
optional_params.update(optional_params_rds2)
@@ -957,7 +980,7 @@ def validate_parameters(required_vars, valid_vars, module):
def main():
argument_spec = ec2_argument_spec()
argument_spec.update(dict(
- command = dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'restore'], required=True),
+ command = dict(choices=['create', 'replicate', 'delete', 'facts', 'modify', 'promote', 'snapshot', 'reboot', 'restore'], required=True),
instance_name = dict(required=False),
source_instance = dict(required=False),
db_engine = dict(choices=['MySQL', 'oracle-se1', 'oracle-se', 'oracle-ee', 'sqlserver-ee', 'sqlserver-se', 'sqlserver-ex', 'sqlserver-web', 'postgres'], required=False),
@@ -989,6 +1012,7 @@ def main():
tags = dict(type='dict', required=False),
publicly_accessible = dict(required=False),
character_set_name = dict(required=False),
+ force_failover = dict(type='bool', required=False, default=False)
)
)
@@ -1007,12 +1031,13 @@ def main():
'modify': modify_db_instance,
'promote': promote_db_instance,
'snapshot': snapshot_db_instance,
+ 'reboot': reboot_db_instance,
'restore': restore_db_instance,
}
region, ec2_url, aws_connect_params = get_aws_connection_info(module)
if not region:
- module.fail_json(msg="region not specified and unable to determine region from EC2_REGION.")
+ module.fail_json(msg="Region not specified. Unable to determine region from EC2_REGION.")
# connect to the rds endpoint
if has_rds2:
diff --git a/cloud/amazon/route53.py b/cloud/amazon/route53.py
index c2ad603a1f4..9b867fb1e72 100644
--- a/cloud/amazon/route53.py
+++ b/cloud/amazon/route53.py
@@ -24,69 +24,61 @@ description:
options:
command:
description:
- - Specifies the action to take.
+ - Specifies the action to take.
required: true
- default: null
- aliases: []
choices: [ 'get', 'create', 'delete' ]
zone:
description:
- The DNS zone to modify
required: true
+ hosted_zone_id:
+ description:
+ - The Hosted Zone ID of the DNS zone to modify
+ required: false
+ version_added: "2.0"
default: null
- aliases: []
record:
description:
- The full DNS record to create or delete
required: true
- default: null
- aliases: []
ttl:
description:
- The TTL to give the new record
required: false
default: 3600 (one hour)
- aliases: []
type:
description:
- The type of DNS record to create
required: true
- default: null
- aliases: []
choices: [ 'A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS' ]
alias:
description:
- Indicates if this is an alias record.
required: false
- version_added: 1.9
+ version_added: "1.9"
default: False
- aliases: []
choices: [ 'True', 'False' ]
alias_hosted_zone_id:
description:
- The hosted zone identifier.
required: false
- version_added: 1.9
+ version_added: "1.9"
default: null
- aliases: []
value:
description:
- The new value when creating a DNS record. Multiple comma-spaced values are allowed for non-alias records. When deleting a record all values for the record must be specified or Route53 will not delete it.
required: false
default: null
- aliases: []
overwrite:
description:
- Whether an existing record should be overwritten on create if values do not match
required: false
default: null
- aliases: []
retry_interval:
description:
- In the case that route53 is still servicing a prior request, this module will wait and try again after this many seconds. If you have many domain names, the default of 500 seconds may be too long.
required: false
default: 500
- aliases: []
private_zone:
description:
- If set to true, the private zone matching the requested name within the domain will be used if there are both public and private zones. The default is to use the public zone.
@@ -132,6 +124,13 @@ options:
required: false
default: null
version_added: "2.0"
+ vpc_id:
+ description:
+ - "When used in conjunction with private_zone: true, this will only modify records in the private hosted zone attached to this VPC."
+ - This allows you to have multiple private hosted zones, all with the same name, attached to different VPCs.
+ required: false
+ default: null
+ version_added: "2.0"
author: "Bruce Pennypacker (@bpennypacker)"
extends_documentation_fragment: aws
'''
@@ -195,6 +194,28 @@ EXAMPLES = '''
alias=True
alias_hosted_zone_id="{{ elb_zone_id }}"
+# Add an AAAA record with Hosted Zone ID. Note that because there are colons in the value
+# that the entire parameter list must be quoted:
+- route53:
+ command: "create"
+ zone: "foo.com"
+ hosted_zone_id: "Z2AABBCCDDEEFF"
+ record: "localhost.foo.com"
+ type: "AAAA"
+ ttl: "7200"
+ value: "::1"
+
+# Add an AAAA record with Hosted Zone ID. Note that because there are colons in the value
+# that the entire parameter list must be quoted:
+- route53:
+ command: "create"
+ zone: "foo.com"
+ hosted_zone_id: "Z2AABBCCDDEEFF"
+ record: "localhost.foo.com"
+ type: "AAAA"
+ ttl: "7200"
+ value: "::1"
+
# Use a routing policy to distribute traffic:
- route53:
command: "create"
@@ -222,14 +243,26 @@ try:
except ImportError:
HAS_BOTO = False
-def get_zone_by_name(conn, module, zone_name, want_private):
- """Finds a zone by name"""
+def get_zone_by_name(conn, module, zone_name, want_private, zone_id, want_vpc_id):
+ """Finds a zone by name or zone_id"""
for zone in conn.get_zones():
# only save this zone id if the private status of the zone matches
# the private_zone_in boolean specified in the params
private_zone = module.boolean(zone.config.get('PrivateZone', False))
- if private_zone == want_private and zone.name == zone_name:
- return zone
+ if private_zone == want_private and ((zone.name == zone_name and zone_id == None) or zone.id.replace('/hostedzone/', '') == zone_id):
+ if want_vpc_id:
+ # NOTE: These details aren't available in other boto methods, hence the necessary
+ # extra API call
+ zone_details = conn.get_hosted_zone(zone.id)['GetHostedZoneResponse']
+ # this is to deal with this boto bug: https://github.com/boto/boto/pull/2882
+ if isinstance(zone_details['VPCs'], dict):
+ if zone_details['VPCs']['VPC']['VPCId'] == want_vpc_id:
+ return zone
+ else: # Forward compatibility for when boto fixes that bug
+ if want_vpc_id in [v['VPCId'] for v in zone_details['VPCs']]:
+ return zone
+ else:
+ return zone
return None
@@ -252,6 +285,7 @@ def main():
argument_spec.update(dict(
command = dict(choices=['get', 'create', 'delete'], required=True),
zone = dict(required=True),
+ hosted_zone_id = dict(required=False, default=None),
record = dict(required=True),
ttl = dict(required=False, type='int', default=3600),
type = dict(choices=['A', 'CNAME', 'MX', 'AAAA', 'TXT', 'PTR', 'SRV', 'SPF', 'NS'], required=True),
@@ -266,6 +300,7 @@ def main():
region = dict(required=False),
health_check = dict(required=False),
failover = dict(required=False),
+ vpc_id = dict(required=False),
)
)
module = AnsibleModule(argument_spec=argument_spec)
@@ -275,6 +310,7 @@ def main():
command_in = module.params.get('command')
zone_in = module.params.get('zone').lower()
+ hosted_zone_id_in = module.params.get('hosted_zone_id')
ttl_in = module.params.get('ttl')
record_in = module.params.get('record').lower()
type_in = module.params.get('type')
@@ -288,6 +324,7 @@ def main():
region_in = module.params.get('region')
health_check_in = module.params.get('health_check')
failover_in = module.params.get('failover')
+ vpc_id_in = module.params.get('vpc_id')
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
@@ -314,6 +351,11 @@ def main():
elif not alias_hosted_zone_id_in:
module.fail_json(msg = "parameter 'alias_hosted_zone_id' required for alias create/delete")
+ if vpc_id_in and not private_zone_in:
+ module.fail_json(msg="parameter 'private_zone' must be true when specifying parameter"
+ " 'vpc_id'")
+
+
# connect to the route53 endpoint
try:
conn = Route53Connection(**aws_connect_kwargs)
@@ -321,7 +363,7 @@ def main():
module.fail_json(msg = e.error_message)
# Find the named zone ID
- zone = get_zone_by_name(conn, module, zone_in, private_zone_in)
+ zone = get_zone_by_name(conn, module, zone_in, private_zone_in, hosted_zone_id_in, vpc_id_in)
# Verify that the requested zone is already defined in Route53
if zone is None:
@@ -355,11 +397,15 @@ def main():
record['ttl'] = rset.ttl
record['value'] = ','.join(sorted(rset.resource_records))
record['values'] = sorted(rset.resource_records)
+ if hosted_zone_id_in:
+ record['hosted_zone_id'] = hosted_zone_id_in
record['identifier'] = rset.identifier
record['weight'] = rset.weight
record['region'] = rset.region
record['failover'] = rset.failover
record['health_check'] = rset.health_check
+ if hosted_zone_id_in:
+ record['hosted_zone_id'] = hosted_zone_id_in
if rset.alias_dns_name:
record['alias'] = True
record['value'] = rset.alias_dns_name
@@ -374,7 +420,13 @@ def main():
break
if command_in == 'get':
- module.exit_json(changed=False, set=record)
+ if type_in == 'NS':
+ ns = record['values']
+ else:
+ # Retrieve name servers associated to the zone.
+ ns = conn.get_zone(zone_in).get_nameservers()
+
+ module.exit_json(changed=False, set=record, nameservers=ns)
if command_in == 'delete' and not found_record:
module.exit_json(changed=False)
diff --git a/cloud/amazon/s3.py b/cloud/amazon/s3.py
index 9f40a3d6434..64f53cc042a 100644
--- a/cloud/amazon/s3.py
+++ b/cloud/amazon/s3.py
@@ -35,7 +35,8 @@ options:
default: null
aliases: ['ec2_secret_key', 'secret_key']
bucket:
- description: Bucket name.
+ description:
+ - Bucket name.
required: true
default: null
aliases: []
@@ -50,12 +51,31 @@ options:
- When set for PUT mode, asks for server-side encryption
required: false
default: no
+ version_added: "2.0"
expiration:
description:
- Time limit (in seconds) for the URL generated and returned by S3/Walrus when performing a mode=put or mode=geturl operation.
required: false
default: 600
aliases: []
+ headers:
+ description:
+ - Custom headers for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'.
+ required: false
+ default: null
+ version_added: "2.0"
+ marker:
+ description:
+ - Specifies the key to start with when using list mode. Object keys are returned in alphabetical order, starting with key after the marker in order.
+ required: false
+ default: null
+ version_added: "2.0"
+ max_keys:
+ description:
+ - Max number of results to return in list mode, set this if you want to retrieve fewer than the default 1000 keys.
+ required: false
+ default: 1000
+ version_added: "2.0"
metadata:
description:
- Metadata for PUT operation, as a dictionary of 'key=value' and 'key=value,key=value'.
@@ -64,7 +84,7 @@ options:
version_added: "1.6"
mode:
description:
- - Switches the module behaviour between put (upload), get (download), geturl (return download url (Ansible 1.3+), getstr (download object as string (1.3+)), create (bucket), delete (bucket), and delobj (delete object).
+ - Switches the module behaviour between put (upload), get (download), geturl (return download url (Ansible 1.3+), getstr (download object as string (1.3+)), list (list keys (2.0+)), create (bucket), delete (bucket), and delobj (delete object).
required: true
default: null
aliases: []
@@ -73,6 +93,18 @@ options:
- Keyname of the object inside the bucket. Can be used to create "virtual directories", see examples.
required: false
default: null
+ permission:
+ description:
+ - This option let's the user set the canned permissions on the object/bucket that are created. The permissions that can be set are 'private', 'public-read', 'public-read-write', 'authenticated-read'. Multiple permissions can be specified as a list.
+ required: false
+ default: private
+ version_added: "2.0"
+ prefix:
+ description:
+ - Limits the response to keys that begin with the specified prefix for list mode
+ required: false
+ default: null
+ version_added: "2.0"
version:
description:
- Version ID of the object inside the bucket. Can be used to get a specific version of a file if versioning is enabled in the target bucket.
@@ -99,18 +131,20 @@ options:
default: 0
version_added: "2.0"
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 ]
src:
- description: The source file path when performing a PUT operation.
+ description:
+ - The source file path when performing a PUT operation.
required: false
default: null
aliases: []
version_added: "1.3"
requirements: [ "boto" ]
-author:
+author:
- "Lester Wade (@lwade)"
- "Ralph Tice (@ralph-tice)"
extends_documentation_fragment: aws
@@ -129,8 +163,17 @@ EXAMPLES = '''
# PUT/upload with metadata
- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put metadata='Content-Encoding=gzip,Cache-Control=no-cache'
+# PUT/upload with custom headers
+- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put headers=x-amz-grant-full-control=emailAddress=owner@example.com
+
+# List keys simple
+- s3: bucket=mybucket mode=list
+
+# List keys all options
+- s3: bucket=mybucket mode=list prefix=/my/desired/ marker=/my/desired/0023.txt max_keys=472
+
# Create an empty bucket
-- s3: bucket=mybucket mode=create
+- s3: bucket=mybucket mode=create permission=public-read
# Create a bucket with key as directory, in the EU region
- s3: bucket=mybucket object=/my/directory/path mode=create region=eu-west-1
@@ -138,7 +181,7 @@ EXAMPLES = '''
# Delete a bucket and all contents
- s3: bucket=mybucket mode=delete
-# GET an object but dont download if the file checksums match
+# GET an object but dont download if the file checksums match
- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get overwrite=different
# Delete an object from a bucket
@@ -147,7 +190,6 @@ EXAMPLES = '''
import os
import urlparse
-import hashlib
from ssl import SSLError
try:
@@ -156,6 +198,7 @@ try:
from boto.s3.connection import Location
from boto.s3.connection import OrdinaryCallingFormat
from boto.s3.connection import S3Connection
+ from boto.s3.acl import CannedACLStrings
HAS_BOTO = True
except ImportError:
HAS_BOTO = False
@@ -200,11 +243,26 @@ def create_bucket(module, s3, bucket, location=None):
location = Location.DEFAULT
try:
bucket = s3.create_bucket(bucket, location=location)
+ for acl in module.params.get('permission'):
+ bucket.set_acl(acl)
except s3.provider.storage_response_error, e:
module.fail_json(msg= str(e))
if bucket:
return True
+def get_bucket(module, s3, bucket):
+ try:
+ return s3.lookup(bucket)
+ except s3.provider.storage_response_error, e:
+ module.fail_json(msg= str(e))
+
+def list_keys(module, bucket_object, prefix, marker, max_keys):
+ all_keys = bucket_object.get_all_keys(prefix=prefix, marker=marker, max_keys=max_keys)
+
+ keys = [x.key for x in all_keys]
+
+ module.exit_json(msg="LIST operation complete", s3_keys=keys)
+
def delete_bucket(module, s3, bucket):
try:
bucket = s3.lookup(bucket)
@@ -232,15 +290,6 @@ def create_dirkey(module, s3, bucket, obj):
except s3.provider.storage_response_error, e:
module.fail_json(msg= str(e))
-def upload_file_check(src):
- if os.path.exists(src):
- file_exists is True
- else:
- file_exists is False
- if os.path.isdir(src):
- module.fail_json(msg="Specifying a directory is not a valid source for upload.", failed=True)
- return file_exists
-
def path_check(path):
if os.path.exists(path):
return True
@@ -248,7 +297,7 @@ def path_check(path):
return False
-def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt):
+def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers):
try:
bucket = s3.lookup(bucket)
key = bucket.new_key(obj)
@@ -256,7 +305,9 @@ def upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt):
for meta_key in metadata.keys():
key.set_metadata(meta_key, metadata[meta_key])
- key.set_contents_from_filename(src, encrypt_key=encrypt)
+ key.set_contents_from_filename(src, encrypt_key=encrypt, headers=headers)
+ for acl in module.params.get('permission'):
+ key.set_acl(acl)
url = key.generate_url(expiry)
module.exit_json(msg="PUT operation complete", url=url, changed=True)
except s3.provider.storage_copy_error, e:
@@ -315,13 +366,6 @@ def is_walrus(s3_url):
else:
return False
-def get_md5_digest(local_file):
- md5 = hashlib.md5()
- with open(local_file, 'rb') as f:
- for data in f.read(1024 ** 2):
- md5.update(data)
- return md5.hexdigest()
-
def main():
argument_spec = ec2_argument_spec()
@@ -330,11 +374,16 @@ def main():
dest = dict(default=None),
encrypt = dict(default=True, type='bool'),
expiry = dict(default=600, aliases=['expiration']),
+ headers = dict(type='dict'),
+ marker = dict(default=None),
+ max_keys = dict(default=1000),
metadata = dict(type='dict'),
- mode = dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj'], required=True),
+ mode = dict(choices=['get', 'put', 'delete', 'create', 'geturl', 'getstr', 'delobj', 'list'], required=True),
object = dict(),
+ permission = dict(type='list', default=['private']),
version = dict(default=None),
overwrite = dict(aliases=['force'], default='always'),
+ prefix = dict(default=None),
retries = dict(aliases=['retry'], type='int', default=0),
s3_url = dict(aliases=['S3_URL']),
src = dict(),
@@ -350,25 +399,33 @@ def main():
expiry = int(module.params['expiry'])
if module.params.get('dest'):
dest = os.path.expanduser(module.params.get('dest'))
+ headers = module.params.get('headers')
+ marker = module.params.get('marker')
+ max_keys = module.params.get('max_keys')
metadata = module.params.get('metadata')
mode = module.params.get('mode')
obj = module.params.get('object')
version = module.params.get('version')
overwrite = module.params.get('overwrite')
+ prefix = module.params.get('prefix')
retries = module.params.get('retries')
s3_url = module.params.get('s3_url')
src = module.params.get('src')
- if overwrite not in ['always', 'never', 'different']:
- if module.boolean(overwrite):
- overwrite = 'always'
- else:
+ for acl in module.params.get('permission'):
+ if acl not in CannedACLStrings:
+ module.fail_json(msg='Unknown permission specified: %s' % str(acl))
+
+ if overwrite not in ['always', 'never', 'different']:
+ if module.boolean(overwrite):
+ overwrite = 'always'
+ else:
overwrite='never'
- if overwrite not in ['always', 'never', 'different']:
- if module.boolean(overwrite):
- overwrite = 'always'
- else:
+ if overwrite not in ['always', 'never', 'different']:
+ if module.boolean(overwrite):
+ overwrite = 'always'
+ else:
overwrite='never'
region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module)
@@ -388,6 +445,12 @@ def main():
if not s3_url and 'S3_URL' in os.environ:
s3_url = os.environ['S3_URL']
+ # bucket names with .'s in them need to use the calling_format option,
+ # otherwise the connection will fail. See https://github.com/boto/boto/issues/2836
+ # for more details.
+ if '.' in bucket:
+ aws_connect_kwargs['calling_format'] = OrdinaryCallingFormat()
+
# Look at s3_url and tweak connection settings
# if connecting to Walrus or fakes3
try:
@@ -404,7 +467,7 @@ def main():
walrus = urlparse.urlparse(s3_url).hostname
s3 = boto.connect_walrus(walrus, **aws_connect_kwargs)
else:
- s3 = boto.s3.connect_to_region(location, is_secure=True, calling_format=OrdinaryCallingFormat(), **aws_connect_kwargs)
+ s3 = boto.s3.connect_to_region(location, is_secure=True, **aws_connect_kwargs)
# use this as fallback because connect_to_region seems to fail in boto + non 'classic' aws accounts in some cases
if s3 is None:
s3 = boto.connect_s3(**aws_connect_kwargs)
@@ -433,16 +496,15 @@ def main():
else:
module.fail_json(msg="Key %s does not exist."%obj, failed=True)
- # If the destination path doesn't exist, no need to md5um etag check, so just download.
+ # If the destination path doesn't exist or overwrite is True, no need to do the md5um etag check, so just download.
pathrtn = path_check(dest)
- if pathrtn is False:
+ if pathrtn is False or overwrite == 'always':
download_s3file(module, s3, bucket, obj, dest, retries, version=version)
# Compare the remote MD5 sum of the object with the local dest md5sum, if it already exists.
if pathrtn is True:
md5_remote = keysum(module, s3, bucket, obj, version=version)
- md5_local = get_md5_digest(dest)
-
+ md5_local = module.md5(dest)
if md5_local == md5_remote:
sum_matches = True
if overwrite == 'always':
@@ -461,10 +523,6 @@ def main():
if sum_matches is True and overwrite == 'never':
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
- # At this point explicitly define the overwrite condition.
- if sum_matches is True and pathrtn is True and overwrite == 'always':
- download_s3file(module, s3, bucket, obj, dest, retries, version=version)
-
# if our mode is a PUT operation (upload), go through the procedure as appropriate ...
if mode == 'put':
@@ -485,29 +543,29 @@ def main():
# Lets check key state. Does it exist and if it does, compute the etag md5sum.
if bucketrtn is True and keyrtn is True:
md5_remote = keysum(module, s3, bucket, obj)
- md5_local = get_md5_digest(src)
+ md5_local = module.md5(src)
if md5_local == md5_remote:
sum_matches = True
if overwrite == 'always':
- upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt)
+ upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers)
else:
get_download_url(module, s3, bucket, obj, expiry, changed=False)
else:
sum_matches = False
if overwrite in ('always', 'different'):
- upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt)
+ upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers)
else:
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force upload.")
# If neither exist (based on bucket existence), we can create both.
if bucketrtn is False and pathrtn is True:
create_bucket(module, s3, bucket, location)
- upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt)
+ upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers)
# If bucket exists but key doesn't, just upload.
if bucketrtn is True and pathrtn is True and keyrtn is False:
- upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt)
+ upload_s3file(module, s3, bucket, obj, src, expiry, metadata, encrypt, headers)
# Delete an object from a bucket, not the entire bucket
if mode == 'delobj':
@@ -538,6 +596,16 @@ def main():
else:
module.fail_json(msg="Bucket parameter is required.", failed=True)
+ # Support for listing a set of keys
+ if mode == 'list':
+ bucket_object = get_bucket(module, s3, bucket)
+
+ # If the bucket does not exist then bail out
+ if bucket_object is None:
+ module.fail_json(msg="Target bucket (%s) cannot be found"% bucket, failed=True)
+
+ list_keys(module, bucket_object, prefix, marker, max_keys)
+
# Need to research how to create directories without "populating" a key, so this should just do bucket creation for now.
# WE SHOULD ENABLE SOME WAY OF CREATING AN EMPTY KEY TO CREATE "DIRECTORY" STRUCTURE, AWS CONSOLE DOES THIS.
if mode == 'create':
diff --git a/cloud/azure/azure.py b/cloud/azure/azure.py
index f1eea46525e..c4fa41a6eb1 100644
--- a/cloud/azure/azure.py
+++ b/cloud/azure/azure.py
@@ -567,8 +567,8 @@ def main():
module.fail_json(msg='location parameter is required for new instance')
if not module.params.get('storage_account'):
module.fail_json(msg='storage_account parameter is required for new instance')
- if not module.params.get('password'):
- module.fail_json(msg='password parameter is required for new instance')
+ if not (module.params.get('password') or module.params.get('ssh_cert_path')):
+ module.fail_json(msg='password or ssh_cert_path parameter is required for new instance')
(changed, public_dns_name, deployment) = create_virtual_machine(module, azure)
module.exit_json(changed=changed, public_dns_name=public_dns_name, deployment=json.loads(json.dumps(deployment, default=lambda o: o.__dict__)))
diff --git a/cloud/docker/docker.py b/cloud/docker/docker.py
index 9986c94f9ec..0ab564208ba 100644
--- a/cloud/docker/docker.py
+++ b/cloud/docker/docker.py
@@ -109,6 +109,14 @@ options:
- none
- syslog
version_added: "2.0"
+ log_opt:
+ description:
+ - Additional options to pass to the logging driver selected above. See Docker log-driver
+ documentation for more information (https://docs.docker.com/reference/logging/overview/).
+ Requires docker >=1.7.0.
+ required: false
+ default: null
+ version_added: "2.0"
memory_limit:
description:
- RAM allocated to the container as a number of bytes or as a human-readable
@@ -160,6 +168,12 @@ options:
specified by docker-py.
default: docker-py default remote API version
version_added: "1.8"
+ docker_user:
+ description:
+ - Username or UID to use within the container
+ required: false
+ default: null
+ version_added: "2.0"
username:
description:
- Remote API username.
@@ -191,8 +205,16 @@ options:
default: null
detach:
description:
- - Enable detached mode to leave the container running in background.
+ - Enable detached mode to leave the container running in background. If
+ disabled, fail unless the process exits cleanly.
default: true
+ signal:
+ version_added: "2.0"
+ description:
+ - With the state "killed", you can alter the signal sent to the
+ container.
+ required: false
+ default: KILL
state:
description:
- Assert the container's desired state. "present" only asserts that the
@@ -251,6 +273,12 @@ options:
default: DockerHub
aliases: []
version_added: "1.8"
+ read_only:
+ description:
+ - Mount the container's root filesystem as read only
+ default: null
+ aliases: []
+ version_added: "2.0"
restart_policy:
description:
- Container restart policy.
@@ -264,6 +292,7 @@ options:
default: 0
version_added: "1.9"
extra_hosts:
+ version_added: "2.0"
description:
- Dict of custom host-to-IP mappings to be defined in the container
insecure_registry:
@@ -272,8 +301,26 @@ options:
docker-py >= 0.5.0.
default: false
version_added: "1.9"
-
-author:
+ cpu_set:
+ description:
+ - CPUs in which to allow execution. Requires docker-py >= 0.6.0.
+ required: false
+ default: null
+ version_added: "2.0"
+ cap_add:
+ description:
+ - Add capabilities for the container. Requires docker-py >= 0.5.0.
+ required: false
+ default: false
+ version_added: "2.0"
+ cap_drop:
+ description:
+ - Drop capabilities for the container. Requires docker-py >= 0.5.0.
+ required: false
+ default: false
+ aliases: []
+ version_added: "2.0"
+author:
- "Cove Schneider (@cove)"
- "Joshua Conner (@joshuaconner)"
- "Pavel Antonov (@softzilla)"
@@ -376,9 +423,23 @@ EXAMPLES = '''
name: ohno
image: someuser/oldandbusted
state: absent
+
+# Example Syslogging Output
+
+- name: myservice container
+ docker:
+ name: myservice
+ image: someservice/someimage
+ state: reloaded
+ log_driver: syslog
+ log_opt:
+ syslog-address: tcp://my-syslog-server:514
+ syslog-facility: daemon
+ syslog-tag: myservice
'''
HAS_DOCKER_PY = True
+DEFAULT_DOCKER_API_VERSION = None
import sys
import json
@@ -388,6 +449,7 @@ from urlparse import urlparse
try:
import docker.client
import docker.utils
+ import docker.errors
from requests.exceptions import RequestException
except ImportError:
HAS_DOCKER_PY = False
@@ -480,6 +542,7 @@ def get_docker_py_versioninfo():
if not char.isdigit():
nondigit = part[idx:]
digit = part[:idx]
+ break
if digit:
version.append(int(digit))
if nondigit:
@@ -528,6 +591,12 @@ class DockerManager(object):
'extra_hosts': ((0, 7, 0), '1.3.1'),
'pid': ((1, 0, 0), '1.17'),
'log_driver': ((1, 2, 0), '1.18'),
+ 'log_opt': ((1, 2, 0), '1.18'),
+ 'host_config': ((0, 7, 0), '1.15'),
+ 'cpu_set': ((0, 6, 0), '1.14'),
+ 'cap_add': ((0, 5, 0), '1.14'),
+ 'cap_drop': ((0, 5, 0), '1.14'),
+ 'read_only': ((1, 0, 0), '1.17'),
# Clientside only
'insecure_registry': ((0, 5, 0), '0.0')
}
@@ -539,24 +608,26 @@ class DockerManager(object):
self.volumes = None
if self.module.params.get('volumes'):
self.binds = {}
- self.volumes = {}
+ self.volumes = []
vols = self.module.params.get('volumes')
for vol in vols:
parts = vol.split(":")
+ # regular volume
+ if len(parts) == 1:
+ self.volumes.append(parts[0])
# host mount (e.g. /mnt:/tmp, bind mounts host's /tmp to /mnt in the container)
- if len(parts) == 2:
- self.volumes[parts[1]] = {}
- self.binds[parts[0]] = parts[1]
- # with bind mode
- elif len(parts) == 3:
- if parts[2] not in ['ro', 'rw']:
- self.module.fail_json(msg='bind mode needs to either be "ro" or "rw"')
- ro = parts[2] == 'ro'
- self.volumes[parts[1]] = {}
- self.binds[parts[0]] = {'bind': parts[1], 'ro': ro}
- # docker mount (e.g. /www, mounts a docker volume /www on the container at the same location)
+ elif 2 <= len(parts) <= 3:
+ # default to read-write
+ ro = False
+ # with supplied bind mode
+ if len(parts) == 3:
+ if parts[2] not in ['ro', 'rw']:
+ self.module.fail_json(msg='bind mode needs to either be "ro" or "rw"')
+ else:
+ ro = parts[2] == 'ro'
+ self.binds[parts[0]] = {'bind': parts[1], 'ro': ro }
else:
- self.volumes[parts[0]] = {}
+ self.module.fail_json(msg='volumes support 1 to 3 arguments')
self.lxc_conf = None
if self.module.params.get('lxc_conf'):
@@ -735,6 +806,82 @@ class DockerManager(object):
else:
return None
+ def get_start_params(self):
+ """
+ Create start params
+ """
+ params = {
+ 'lxc_conf': self.lxc_conf,
+ 'binds': self.binds,
+ 'port_bindings': self.port_bindings,
+ 'publish_all_ports': self.module.params.get('publish_all_ports'),
+ 'privileged': self.module.params.get('privileged'),
+ 'links': self.links,
+ 'network_mode': self.module.params.get('net'),
+ }
+
+ optionals = {}
+ for optional_param in ('dns', 'volumes_from', 'restart_policy',
+ 'restart_policy_retry', 'pid', 'extra_hosts', 'log_driver',
+ 'cap_add', 'cap_drop', 'read_only', 'log_opt'):
+ optionals[optional_param] = self.module.params.get(optional_param)
+
+ if optionals['dns'] is not None:
+ self.ensure_capability('dns')
+ params['dns'] = optionals['dns']
+
+ if optionals['volumes_from'] is not None:
+ self.ensure_capability('volumes_from')
+ params['volumes_from'] = optionals['volumes_from']
+
+ if optionals['restart_policy'] is not None:
+ self.ensure_capability('restart_policy')
+ params['restart_policy'] = { 'Name': optionals['restart_policy'] }
+ if params['restart_policy']['Name'] == 'on-failure':
+ params['restart_policy']['MaximumRetryCount'] = optionals['restart_policy_retry']
+
+ # docker_py only accepts 'host' or None
+ if 'pid' in optionals and not optionals['pid']:
+ optionals['pid'] = None
+
+ if optionals['pid'] is not None:
+ self.ensure_capability('pid')
+ params['pid_mode'] = optionals['pid']
+
+ if optionals['extra_hosts'] is not None:
+ self.ensure_capability('extra_hosts')
+ params['extra_hosts'] = optionals['extra_hosts']
+
+ if optionals['log_driver'] is not None:
+ self.ensure_capability('log_driver')
+ log_config = docker.utils.LogConfig(type=docker.utils.LogConfig.types.JSON)
+ if optionals['log_opt'] is not None:
+ for k, v in optionals['log_opt'].iteritems():
+ log_config.set_config_value(k, v)
+ log_config.type = optionals['log_driver']
+ params['log_config'] = log_config
+
+ if optionals['cap_add'] is not None:
+ self.ensure_capability('cap_add')
+ params['cap_add'] = optionals['cap_add']
+
+ if optionals['cap_drop'] is not None:
+ self.ensure_capability('cap_drop')
+ params['cap_drop'] = optionals['cap_drop']
+
+ if optionals['read_only'] is not None:
+ self.ensure_capability('read_only')
+ params['read_only'] = optionals['read_only']
+
+ return params
+
+ def create_host_config(self):
+ """
+ Create HostConfig object
+ """
+ params = self.get_start_params()
+ return docker.utils.create_host_config(**params)
+
def get_port_bindings(self, ports):
"""
Parse the `ports` string into a port bindings dict for the `start_container` call.
@@ -871,6 +1018,9 @@ class DockerManager(object):
running = self.get_running_containers()
current = self.get_inspect_containers(running)
+ #Get API version
+ api_version = self.client.version()['ApiVersion']
+
image = self.get_inspect_image()
if image is None:
# The image isn't present. Assume that we're about to pull a new
@@ -921,7 +1071,7 @@ class DockerManager(object):
expected_volume_keys = set((image['ContainerConfig']['Volumes'] or {}).keys())
if self.volumes:
- expected_volume_keys.update(self.volumes.keys())
+ expected_volume_keys.update(self.volumes)
actual_volume_keys = set((container['Config']['Volumes'] or {}).keys())
@@ -937,7 +1087,11 @@ class DockerManager(object):
except ValueError as e:
self.module.fail_json(msg=str(e))
- actual_mem = container['Config']['Memory']
+ #For v1.19 API and above use HostConfig, otherwise use Config
+ if api_version >= 1.19:
+ actual_mem = container['HostConfig']['Memory']
+ else:
+ actual_mem = container['Config']['Memory']
if expected_mem and actual_mem != expected_mem:
self.reload_reasons.append('memory ({0} => {1})'.format(actual_mem, expected_mem))
@@ -1063,15 +1217,14 @@ class DockerManager(object):
for container_port, config in self.port_bindings.iteritems():
if isinstance(container_port, int):
container_port = "{0}/tcp".format(container_port)
- bind = {}
if len(config) == 1:
- bind['HostIp'] = "0.0.0.0"
- bind['HostPort'] = ""
+ expected_bound_ports[container_port] = [{'HostIp': "0.0.0.0", 'HostPort': ""}]
+ elif isinstance(config[0], tuple):
+ expected_bound_ports[container_port] = []
+ for hostip, hostport in config:
+ expected_bound_ports[container_port].append({ 'HostIp': hostip, 'HostPort': str(hostport)})
else:
- bind['HostIp'] = config[0]
- bind['HostPort'] = str(config[1])
-
- expected_bound_ports[container_port] = [bind]
+ expected_bound_ports[container_port] = [{'HostIp': config[0], 'HostPort': str(config[1])}]
actual_bound_ports = container['HostConfig']['PortBindings'] or {}
@@ -1108,8 +1261,8 @@ class DockerManager(object):
# NETWORK MODE
- expected_netmode = self.module.params.get('net') or ''
- actual_netmode = container['HostConfig']['NetworkMode']
+ expected_netmode = self.module.params.get('net') or 'bridge'
+ actual_netmode = container['HostConfig']['NetworkMode'] or 'bridge'
if actual_netmode != expected_netmode:
self.reload_reasons.append('net ({0} => {1})'.format(actual_netmode, expected_netmode))
differing.append(container)
@@ -1134,7 +1287,7 @@ class DockerManager(object):
# LOG_DRIVER
- if self.ensure_capability('log_driver', False) :
+ if self.ensure_capability('log_driver', False):
expected_log_driver = self.module.params.get('log_driver') or 'json-file'
actual_log_driver = container['HostConfig']['LogConfig']['Type']
if actual_log_driver != expected_log_driver:
@@ -1142,6 +1295,17 @@ class DockerManager(object):
differing.append(container)
continue
+ if self.ensure_capability('log_opt', False):
+ expected_logging_opts = self.module.params.get('log_opt') or {}
+ actual_log_opts = container['HostConfig']['LogConfig']['Config']
+ if len(set(expected_logging_opts.items()) - set(actual_log_opts.items())) != 0:
+ log_opt_reasons = {
+ 'added': dict(set(expected_logging_opts.items()) - set(actual_log_opts.items())),
+ 'removed': dict(set(actual_log_opts.items()) - set(expected_logging_opts.items()))
+ }
+ self.reload_reasons.append('log_opt ({0})'.format(log_opt_reasons))
+ differing.append(container)
+
return differing
def get_deployed_containers(self):
@@ -1238,63 +1402,17 @@ class DockerManager(object):
except Exception as e:
self.module.fail_json(msg="Failed to pull the specified image: %s" % resource, error=repr(e))
- def create_host_config(self):
- params = {
- 'lxc_conf': self.lxc_conf,
- 'binds': self.binds,
- 'port_bindings': self.port_bindings,
- 'publish_all_ports': self.module.params.get('publish_all_ports'),
- 'privileged': self.module.params.get('privileged'),
- 'links': self.links,
- 'network_mode': self.module.params.get('net'),
- }
-
- optionals = {}
- for optional_param in ('dns', 'volumes_from', 'restart_policy',
- 'restart_policy_retry', 'pid', 'extra_hosts', 'log_driver'):
- optionals[optional_param] = self.module.params.get(optional_param)
-
- if optionals['dns'] is not None:
- self.ensure_capability('dns')
- params['dns'] = optionals['dns']
-
- if optionals['volumes_from'] is not None:
- self.ensure_capability('volumes_from')
- params['volumes_from'] = optionals['volumes_from']
-
- if optionals['restart_policy'] is not None:
- self.ensure_capability('restart_policy')
- params['restart_policy'] = { 'Name': optionals['restart_policy'] }
- if params['restart_policy']['Name'] == 'on-failure':
- params['restart_policy']['MaximumRetryCount'] = optionals['restart_policy_retry']
-
- if optionals['pid'] is not None:
- self.ensure_capability('pid')
- params['pid_mode'] = optionals['pid']
-
- if optionals['extra_hosts'] is not None:
- self.ensure_capability('extra_hosts')
- params['extra_hosts'] = optionals['extra_hosts']
-
- if optionals['log_driver'] is not None:
- self.ensure_capability('log_driver')
- log_config = docker.utils.LogConfig(type=docker.utils.LogConfig.types.JSON)
- log_config.type = optionals['log_driver']
- params['log_config'] = log_config
-
- return docker.utils.create_host_config(**params)
-
def create_containers(self, count=1):
try:
mem_limit = _human_to_bytes(self.module.params.get('memory_limit'))
except ValueError as e:
self.module.fail_json(msg=str(e))
+ api_version = self.client.version()['ApiVersion']
params = {'image': self.module.params.get('image'),
'command': self.module.params.get('command'),
'ports': self.exposed_ports,
'volumes': self.volumes,
- 'mem_limit': mem_limit,
'environment': self.env,
'hostname': self.module.params.get('hostname'),
'domainname': self.module.params.get('domainname'),
@@ -1302,8 +1420,18 @@ class DockerManager(object):
'name': self.module.params.get('name'),
'stdin_open': self.module.params.get('stdin_open'),
'tty': self.module.params.get('tty'),
- 'host_config': self.create_host_config(),
+ 'cpuset': self.module.params.get('cpu_set'),
+ 'user': self.module.params.get('docker_user'),
}
+ if self.ensure_capability('host_config', fail=False):
+ params['host_config'] = self.create_host_config()
+
+ #For v1.19 API and above use HostConfig, otherwise use Config
+ if api_version < 1.19:
+ params['mem_limit'] = mem_limit
+ else:
+ params['host_config']['Memory'] = mem_limit
+
def do_create(count, params):
results = []
@@ -1316,17 +1444,32 @@ class DockerManager(object):
try:
containers = do_create(count, params)
- except:
+ except docker.errors.APIError as e:
+ if e.response.status_code != 404:
+ raise
+
self.pull_image()
containers = do_create(count, params)
return containers
def start_containers(self, containers):
+ params = {}
+
+ if not self.ensure_capability('host_config', fail=False):
+ params = self.get_start_params()
+
for i in containers:
self.client.start(i)
self.increment_counter('started')
+ if not self.module.params.get('detach'):
+ status = self.client.wait(i['Id'])
+ if status != 0:
+ output = self.client.logs(i['Id'], stdout=True, stderr=True,
+ stream=False, timestamps=False)
+ self.module.fail_json(status=status, msg=output)
+
def stop_containers(self, containers):
for i in containers:
self.client.stop(i['Id'])
@@ -1341,7 +1484,7 @@ class DockerManager(object):
def kill_containers(self, containers):
for i in containers:
- self.client.kill(i['Id'])
+ self.client.kill(i['Id'], self.module.params.get('signal'))
self.increment_counter('killed')
def restart_containers(self, containers):
@@ -1495,6 +1638,7 @@ def main():
tls_ca_cert = dict(required=False, default=None, type='str'),
tls_hostname = dict(required=False, type='str', default=None),
docker_api_version = dict(required=False, default=DEFAULT_DOCKER_API_VERSION, type='str'),
+ docker_user = dict(default=None),
username = dict(default=None),
password = dict(),
email = dict(),
@@ -1505,6 +1649,7 @@ def main():
dns = dict(),
detach = dict(default=True, type='bool'),
state = dict(default='started', choices=['present', 'started', 'reloaded', 'restarted', 'stopped', 'killed', 'absent', 'running']),
+ signal = dict(default=None),
restart_policy = dict(default=None, choices=['always', 'on-failure', 'no']),
restart_policy_retry = dict(default=0, type='int'),
extra_hosts = dict(type='dict'),
@@ -1518,6 +1663,11 @@ def main():
pid = dict(default=None),
insecure_registry = dict(default=False, type='bool'),
log_driver = dict(default=None, choices=['json-file', 'none', 'syslog']),
+ log_opt = dict(default=None, type='dict'),
+ cpu_set = dict(default=None),
+ cap_add = dict(default=None, type='list'),
+ cap_drop = dict(default=None, type='list'),
+ read_only = dict(default=None, type='bool'),
),
required_together = (
['tls_client_cert', 'tls_client_key'],
@@ -1543,10 +1693,14 @@ def main():
if count > 1 and name:
module.fail_json(msg="Count and name must not be used together")
- # Explicitly pull new container images, if requested.
- # Do this before noticing running and deployed containers so that the image names will differ
- # if a newer image has been pulled.
- if pull == "always":
+ # Explicitly pull new container images, if requested. Do this before
+ # noticing running and deployed containers so that the image names
+ # will differ if a newer image has been pulled.
+ # Missing images should be pulled first to avoid downtime when old
+ # container is stopped, but image for new one is now downloaded yet.
+ # It also prevents removal of running container before realizing
+ # that requested image cannot be retrieved.
+ if pull == "always" or (state == 'reloaded' and manager.get_inspect_image() is None):
manager.pull_image()
containers = ContainerSet(manager)
@@ -1575,7 +1729,7 @@ def main():
summary=manager.counters,
containers=containers.changed,
reload_reasons=manager.get_reload_reason_message(),
- ansible_facts=_ansible_facts(containers.changed))
+ ansible_facts=_ansible_facts(manager.get_inspect_containers(containers.changed)))
except DockerAPIError as e:
module.fail_json(changed=manager.has_changed(), msg="Docker API Error: %s" % e.explanation)
diff --git a/cloud/docker/docker_image.py b/cloud/docker/docker_image.py
index 09fc61e6b08..e6cfd87ab43 100644
--- a/cloud/docker/docker_image.py
+++ b/cloud/docker/docker_image.py
@@ -137,6 +137,7 @@ try:
except ImportError:
HAS_DOCKER_CLIENT = False
+DEFAULT_DOCKER_API_VERSION = None
if HAS_DOCKER_CLIENT:
try:
from docker.errors import APIError as DockerAPIError
diff --git a/cloud/google/gc_storage.py b/cloud/google/gc_storage.py
index 280bc42a219..37d61b0b268 100644
--- a/cloud/google/gc_storage.py
+++ b/cloud/google/gc_storage.py
@@ -53,7 +53,7 @@ options:
required: false
default: private
headers:
- version_added: 2.0
+ version_added: "2.0"
description:
- Headers to attach to object.
required: false
@@ -211,15 +211,6 @@ def create_dirkey(module, gs, bucket, obj):
except gs.provider.storage_response_error, e:
module.fail_json(msg= str(e))
-def upload_file_check(src):
- if os.path.exists(src):
- file_exists is True
- else:
- file_exists is False
- if os.path.isdir(src):
- module.fail_json(msg="Specifying a directory is not a valid source for upload.", failed=True)
- return file_exists
-
def path_check(path):
if os.path.exists(path):
return True
@@ -284,7 +275,7 @@ def get_download_url(module, gs, bucket, obj, expiry):
def handle_get(module, gs, bucket, obj, overwrite, dest):
md5_remote = keysum(module, gs, bucket, obj)
- md5_local = hashlib.md5(open(dest, 'rb').read()).hexdigest()
+ md5_local = module.md5(dest)
if md5_local == md5_remote:
module.exit_json(changed=False)
if md5_local != md5_remote and not overwrite:
@@ -300,7 +291,7 @@ def handle_put(module, gs, bucket, obj, overwrite, src, expiration):
# Lets check key state. Does it exist and if it does, compute the etag md5sum.
if bucket_rc and key_rc:
md5_remote = keysum(module, gs, bucket, obj)
- md5_local = hashlib.md5(open(src, 'rb').read()).hexdigest()
+ md5_local = module.md5(src)
if md5_local == md5_remote:
module.exit_json(msg="Local and remote object are identical", changed=False)
if md5_local != md5_remote and not overwrite:
diff --git a/cloud/google/gce.py b/cloud/google/gce.py
index 251a3ee9e93..1de351a12fb 100644
--- a/cloud/google/gce.py
+++ b/cloud/google/gce.py
@@ -32,77 +32,65 @@ options:
- image string to use for the instance
required: false
default: "debian-7"
- aliases: []
instance_names:
description:
- a comma-separated list of instance names to create or destroy
required: false
default: null
- aliases: []
machine_type:
description:
- machine type to use for the instance, use 'n1-standard-1' by default
required: false
default: "n1-standard-1"
- aliases: []
metadata:
description:
- a hash/dictionary of custom data for the instance; '{"key":"value",...}'
required: false
default: null
- aliases: []
service_account_email:
- version_added: 1.5.1
+ version_added: "1.5.1"
description:
- service account email
required: false
default: null
- aliases: []
service_account_permissions:
- version_added: 2.0
+ version_added: "2.0"
description:
- service account permissions (see U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), --scopes section for detailed information)
required: false
default: null
- aliases: []
choices: ["bigquery", "cloud-platform", "compute-ro", "compute-rw", "computeaccounts-ro", "computeaccounts-rw", "datastore", "logging-write", "monitoring", "sql", "sql-admin", "storage-full", "storage-ro", "storage-rw", "taskqueue", "userinfo-email"]
pem_file:
- version_added: 1.5.1
+ version_added: "1.5.1"
description:
- path to the pem file associated with the service account email
required: false
default: null
- aliases: []
project_id:
- version_added: 1.5.1
+ version_added: "1.5.1"
description:
- your GCE project ID
required: false
default: null
- aliases: []
name:
description:
- identifier when working with a single instance
required: false
- aliases: []
network:
description:
- name of the network, 'default' will be used if not specified
required: false
default: "default"
- aliases: []
persistent_boot_disk:
description:
- if set, create the instance with a persistent boot disk
required: false
default: "false"
- aliases: []
disks:
description:
- a list of persistent disks to attach to the instance; a string value gives the name of the disk; alternatively, a dictionary value can define 'name' and 'mode' ('READ_ONLY' or 'READ_WRITE'). The first entry will be the boot disk (which must be READ_WRITE).
required: false
default: null
- aliases: []
version_added: "1.7"
state:
description:
@@ -110,40 +98,34 @@ options:
required: false
default: "present"
choices: ["active", "present", "absent", "deleted"]
- aliases: []
tags:
description:
- a comma-separated list of tags to associate with the instance
required: false
default: null
- aliases: []
zone:
description:
- the GCE zone to use
required: true
default: "us-central1-a"
- aliases: []
ip_forward:
version_added: "1.9"
description:
- set to true if the instance can forward ip packets (useful for gateways)
required: false
default: "false"
- aliases: []
external_ip:
version_added: "1.9"
description:
- type of external ip, ephemeral by default
required: false
default: "ephemeral"
- aliases: []
disk_auto_delete:
version_added: "1.9"
description:
- if set boot disk will be removed after instance destruction
required: false
default: "true"
- aliases: []
requirements:
- "python >= 2.6"
@@ -327,7 +309,7 @@ def create_instances(module, gce, instance_names):
# [ {'key': key1, 'value': value1}, {'key': key2, 'value': value2}, ...]
if metadata:
try:
- md = literal_eval(metadata)
+ md = literal_eval(str(metadata))
if not isinstance(md, dict):
raise ValueError('metadata must be a dict')
except ValueError, e:
diff --git a/cloud/google/gce_net.py b/cloud/google/gce_net.py
index 93844901117..3ae1635ded7 100644
--- a/cloud/google/gce_net.py
+++ b/cloud/google/gce_net.py
@@ -75,7 +75,7 @@ options:
aliases: []
state:
description:
- - desired state of the persistent disk
+ - desired state of the network or firewall
required: false
default: "present"
choices: ["active", "present", "absent", "deleted"]
@@ -264,7 +264,7 @@ def main():
if fw:
gce.ex_destroy_firewall(fw)
changed = True
- if name:
+ elif name:
json_output['name'] = name
network = None
try:
diff --git a/cloud/openstack/nova_keypair.py b/cloud/openstack/_nova_keypair.py
similarity index 99%
rename from cloud/openstack/nova_keypair.py
rename to cloud/openstack/_nova_keypair.py
index b2e38ff7db9..68df0c5a2c4 100644
--- a/cloud/openstack/nova_keypair.py
+++ b/cloud/openstack/_nova_keypair.py
@@ -32,6 +32,7 @@ version_added: "1.2"
author:
- "Benno Joy (@bennojoy)"
- "Michael DeHaan"
+deprecated: Deprecated in 2.0. Use os_keypair instead
short_description: Add/Delete key pair from nova
description:
- Add or Remove key pair from nova .
diff --git a/cloud/openstack/quantum_floating_ip.py b/cloud/openstack/_quantum_floating_ip.py
similarity index 99%
rename from cloud/openstack/quantum_floating_ip.py
rename to cloud/openstack/_quantum_floating_ip.py
index b7599da0725..5220d307844 100644
--- a/cloud/openstack/quantum_floating_ip.py
+++ b/cloud/openstack/_quantum_floating_ip.py
@@ -36,6 +36,7 @@ version_added: "1.2"
author:
- "Benno Joy (@bennojoy)"
- "Brad P. Crochet (@bcrochet)"
+deprecated: Deprecated in 2.0. Use os_floating_ip instead
short_description: Add/Remove floating IP from an instance
description:
- Add or Remove a floating IP to an instance
diff --git a/cloud/openstack/quantum_floating_ip_associate.py b/cloud/openstack/_quantum_floating_ip_associate.py
similarity index 99%
rename from cloud/openstack/quantum_floating_ip_associate.py
rename to cloud/openstack/_quantum_floating_ip_associate.py
index a5f39dec133..8960e247b0f 100644
--- a/cloud/openstack/quantum_floating_ip_associate.py
+++ b/cloud/openstack/_quantum_floating_ip_associate.py
@@ -33,6 +33,7 @@ DOCUMENTATION = '''
module: quantum_floating_ip_associate
version_added: "1.2"
author: "Benno Joy (@bennojoy)"
+deprecated: Deprecated in 2.0. Use os_floating_ip instead
short_description: Associate or disassociate a particular floating IP with an instance
description:
- Associates or disassociates a specific floating IP with a particular instance
diff --git a/cloud/openstack/keystone_user.py b/cloud/openstack/keystone_user.py
index de5eed598c7..babcc3cc569 100644
--- a/cloud/openstack/keystone_user.py
+++ b/cloud/openstack/keystone_user.py
@@ -1,5 +1,19 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
+# 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 .
# Based on Jimmy Tang's implementation
@@ -238,8 +252,20 @@ def ensure_user_exists(keystone, user_name, password, email, tenant_name,
email=email, tenant_id=tenant.id)
return (True, user.id)
+def ensure_role_exists(keystone, role_name):
+ # Get the role if it exists
+ try:
+ role = get_role(keystone, role_name)
+ # Role does exist, we're done
+ return (False, role.id)
+ except KeyError:
+ # Role doesn't exist yet
+ pass
-def ensure_role_exists(keystone, user_name, tenant_name, role_name,
+ role = keystone.roles.create(role_name)
+ return (True, role.id)
+
+def ensure_user_role_exists(keystone, user_name, tenant_name, role_name,
check_mode):
""" Check if role exists
@@ -283,9 +309,11 @@ def ensure_user_absent(keystone, user, check_mode):
raise NotImplementedError("Not yet implemented")
-def ensure_role_absent(keystone, uesr, tenant, role, check_mode):
+def ensure_user_role_absent(keystone, uesr, tenant, role, check_mode):
raise NotImplementedError("Not yet implemented")
+def ensure_role_absent(keystone, role_name):
+ raise NotImplementedError("Not yet implemented")
def main():
@@ -364,14 +392,18 @@ def dispatch(keystone, user=None, password=None, tenant=None,
X absent ensure_tenant_absent
X X present ensure_user_exists
X X absent ensure_user_absent
- X X X present ensure_role_exists
- X X X absent ensure_role_absent
-
-
+ X X X present ensure_user_role_exists
+ X X X absent ensure_user_role_absent
+ X present ensure_role_exists
+ X absent ensure_role_absent
"""
changed = False
id = None
- if tenant and not user and not role and state == "present":
+ if not tenant and not user and role and state == "present":
+ changed, id = ensure_role_exists(keystone, role)
+ elif not tenant and not user and role and state == "absent":
+ changed = ensure_role_absent(keystone, role)
+ elif tenant and not user and not role and state == "present":
changed, id = ensure_tenant_exists(keystone, tenant,
tenant_description, check_mode)
elif tenant and not user and not role and state == "absent":
@@ -382,10 +414,10 @@ def dispatch(keystone, user=None, password=None, tenant=None,
elif tenant and user and not role and state == "absent":
changed = ensure_user_absent(keystone, user, check_mode)
elif tenant and user and role and state == "present":
- changed, id = ensure_role_exists(keystone, user, tenant, role,
+ changed, id = ensure_user_role_exists(keystone, user, tenant, role,
check_mode)
elif tenant and user and role and state == "absent":
- changed = ensure_role_absent(keystone, user, tenant, role, check_mode)
+ changed = ensure_user_role_absent(keystone, user, tenant, role, check_mode)
else:
# Should never reach here
raise ValueError("Code should never reach here")
diff --git a/cloud/openstack/os_client_config.py b/cloud/openstack/os_client_config.py
index 100608b0fd0..67c58dfd6ca 100644
--- a/cloud/openstack/os_client_config.py
+++ b/cloud/openstack/os_client_config.py
@@ -25,28 +25,45 @@ short_description: Get OpenStack Client config
description:
- Get I(openstack) client config data from clouds.yaml or environment
version_added: "2.0"
+notes:
+ - Facts are placed in the C(openstack.clouds) variable.
+options:
+ clouds:
+ description:
+ - List of clouds to limit the return list to. No value means return
+ information on all configured clouds
+ required: false
+ default: []
requirements: [ os-client-config ]
author: "Monty Taylor (@emonty)"
'''
EXAMPLES = '''
# Get list of clouds that do not support security groups
-- os-client-config:
+- os_client_config:
- debug: var={{ item }}
with_items: "{{ openstack.clouds|rejectattr('secgroup_source', 'none')|list() }}"
+
+# Get the information back just about the mordred cloud
+- os_client_config:
+ clouds:
+ - mordred
'''
def main():
- module = AnsibleModule({})
+ module = AnsibleModule(argument_spec=dict(
+ clouds=dict(required=False, default=[]),
+ ))
p = module.params
try:
config = os_client_config.OpenStackConfig()
clouds = []
for cloud in config.get_all_clouds():
- cloud.config['name'] = cloud.name
- clouds.append(cloud.config)
+ if not p['clouds'] or cloud.name in p['clouds']:
+ cloud.config['name'] = cloud.name
+ clouds.append(cloud.config)
module.exit_json(ansible_facts=dict(openstack=dict(clouds=clouds)))
except exceptions.OpenStackConfigException as e:
module.fail_json(msg=str(e))
diff --git a/cloud/openstack/os_floating_ip.py b/cloud/openstack/os_floating_ip.py
new file mode 100644
index 00000000000..10827012ae8
--- /dev/null
+++ b/cloud/openstack/os_floating_ip.py
@@ -0,0 +1,198 @@
+#!/usr/bin/python
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# Author: Davide Guerri
+#
+# This module 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.
+#
+# This software 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 software. If not, see .
+
+try:
+ import shade
+ from shade import meta
+
+ HAS_SHADE = True
+except ImportError:
+ HAS_SHADE = False
+
+DOCUMENTATION = '''
+---
+module: os_floating_ip
+version_added: "2.0"
+short_description: Add/Remove floating IP from an instance
+extends_documentation_fragment: openstack
+description:
+ - Add or Remove a floating IP to an instance
+options:
+ server:
+ description:
+ - The name or ID of the instance to which the IP address
+ should be assigned.
+ required: true
+ network:
+ description:
+ - The name or ID of a neutron external network or a nova pool name.
+ required: false
+ floating_ip_address:
+ description:
+ - A floating IP address to attach or to detach. Required only if state
+ is absent. When state is present can be used to specify a IP address
+ to attach.
+ required: false
+ reuse:
+ description:
+ - When state is present, and floating_ip_address is not present,
+ this parameter can be used to specify whether we should try to reuse
+ a floating IP address already allocated to the project.
+ required: false
+ default: false
+ fixed_address:
+ description:
+ - To which fixed IP of server the floating IP address should be
+ attached to.
+ required: false
+ wait:
+ description:
+ - When attaching a floating IP address, specify whether we should
+ wait for it to appear as attached.
+ required: false
+ default: false
+ timeout:
+ description:
+ - Time to wait for an IP address to appear as attached. See wait.
+ required: false
+ default: 60
+ state:
+ description:
+ - Should the resource be present or absent.
+ choices: [present, absent]
+ required: false
+ default: present
+requirements: ["shade"]
+'''
+
+EXAMPLES = '''
+# Assign a floating IP to the fist interface of `cattle001` from an exiting
+# external network or nova pool. A new floating IP from the first available
+# external network is allocated to the project.
+- os_floating_ip:
+ cloud: dguerri
+ server: cattle001
+
+# Assign a new floating IP to the instance fixed ip `192.0.2.3` of
+# `cattle001`. If a free floating IP is already allocated to the project, it is
+# reused; if not, a new one is created.
+- os_floating_ip:
+ cloud: dguerri
+ state: present
+ reuse: yes
+ server: cattle001
+ network: ext_net
+ fixed_address: 192.0.2.3
+ wait: true
+ timeout: 180
+
+# Detach a floating IP address from a server
+- os_floating_ip:
+ cloud: dguerri
+ state: absent
+ floating_ip_address: 203.0.113.2
+ server: cattle001
+'''
+
+
+def _get_floating_ip(cloud, floating_ip_address):
+ f_ips = cloud.search_floating_ips(
+ filters={'floating_ip_address': floating_ip_address})
+ if not f_ips:
+ return None
+
+ return f_ips[0]
+
+
+def main():
+ argument_spec = openstack_full_argument_spec(
+ server=dict(required=True),
+ state=dict(default='present', choices=['absent', 'present']),
+ network=dict(required=False),
+ floating_ip_address=dict(required=False),
+ reuse=dict(required=False, type='bool', default=False),
+ fixed_address=dict(required=False),
+ wait=dict(required=False, type='bool', default=False),
+ timeout=dict(required=False, type='int', default=60),
+ )
+
+ module_kwargs = openstack_module_kwargs()
+ module = AnsibleModule(argument_spec, **module_kwargs)
+
+ if not HAS_SHADE:
+ module.fail_json(msg='shade is required for this module')
+
+ server_name_or_id = module.params['server']
+ state = module.params['state']
+ network = module.params['network']
+ floating_ip_address = module.params['floating_ip_address']
+ reuse = module.params['reuse']
+ fixed_address = module.params['fixed_address']
+ wait = module.params['wait']
+ timeout = module.params['timeout']
+
+ cloud = shade.openstack_cloud(**module.params)
+
+ try:
+ server = cloud.get_server(server_name_or_id)
+ if server is None:
+ module.fail_json(
+ msg="server {0} not found".format(server_name_or_id))
+
+ if state == 'present':
+ if floating_ip_address is None:
+ if reuse:
+ f_ip = cloud.available_floating_ip(network=network)
+ else:
+ f_ip = cloud.create_floating_ip(network=network)
+ else:
+ f_ip = _get_floating_ip(cloud, floating_ip_address)
+ if f_ip is None:
+ module.fail_json(
+ msg="floating IP {0} not found".format(
+ floating_ip_address))
+
+ cloud.attach_ip_to_server(
+ server_id=server['id'], floating_ip_id=f_ip['id'],
+ fixed_address=fixed_address, wait=wait, timeout=timeout)
+ # Update the floating IP status
+ f_ip = cloud.get_floating_ip(id=f_ip['id'])
+ module.exit_json(changed=True, floating_ip=f_ip)
+
+ elif state == 'absent':
+ if floating_ip_address is None:
+ module.fail_json(msg="floating_ip_address is required")
+
+ f_ip = _get_floating_ip(cloud, floating_ip_address)
+
+ cloud.detach_ip_from_server(
+ server_id=server['id'], floating_ip_id=f_ip['id'])
+ # Update the floating IP status
+ f_ip = cloud.get_floating_ip(id=f_ip['id'])
+ module.exit_json(changed=True, floating_ip=f_ip)
+
+ except shade.OpenStackCloudException as e:
+ module.fail_json(msg=e.message, extra_data=e.extra_data)
+
+
+# this is magic, see lib/ansible/module_common.py
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+
+
+if __name__ == '__main__':
+ main()
diff --git a/cloud/openstack/os_keypair.py b/cloud/openstack/os_keypair.py
new file mode 100644
index 00000000000..f62cc51bf64
--- /dev/null
+++ b/cloud/openstack/os_keypair.py
@@ -0,0 +1,167 @@
+#!/usr/bin/python
+
+# Copyright (c) 2014 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2013, Benno Joy
+# Copyright (c) 2013, John Dewey
+#
+# This module 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.
+#
+# This software 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 software. If not, see .
+
+
+try:
+ import shade
+ HAS_SHADE = True
+except ImportError:
+ HAS_SHADE = False
+
+
+DOCUMENTATION = '''
+---
+module: os_keypair
+short_description: Add/Delete a keypair from OpenStack
+extends_documentation_fragment: openstack
+version_added: "2.0"
+description:
+ - Add or Remove key pair from OpenStack
+options:
+ name:
+ description:
+ - Name that has to be given to the key pair
+ required: true
+ default: None
+ public_key:
+ description:
+ - The public key that would be uploaded to nova and injected into VMs
+ upon creation.
+ required: false
+ default: None
+ public_key_file:
+ description:
+ - Path to local file containing ssh public key. Mutually exclusive
+ with public_key.
+ required: false
+ default: None
+ state:
+ description:
+ - Should the resource be present or absent.
+ choices: [present, absent]
+ default: present
+requirements: []
+'''
+
+EXAMPLES = '''
+# Creates a key pair with the running users public key
+- os_keypair:
+ cloud: mordred
+ state: present
+ name: ansible_key
+ public_key_file: /home/me/.ssh/id_rsa.pub
+
+# Creates a new key pair and the private key returned after the run.
+- os_keypair:
+ cloud: rax-dfw
+ state: present
+ name: ansible_key
+'''
+
+RETURN = '''
+id:
+ description: Unique UUID.
+ returned: success
+ type: string
+name:
+ description: Name given to the keypair.
+ returned: success
+ type: string
+public_key:
+ description: The public key value for the keypair.
+ returned: success
+ type: string
+private_key:
+ description: The private key value for the keypair.
+ returned: Only when a keypair is generated for the user (e.g., when creating one
+ and a public key is not specified).
+ type: string
+'''
+
+
+def _system_state_change(module, keypair):
+ state = module.params['state']
+ if state == 'present' and not keypair:
+ return True
+ if state == 'absent' and keypair:
+ return True
+ return False
+
+
+def main():
+ argument_spec = openstack_full_argument_spec(
+ name = dict(required=True),
+ public_key = dict(default=None),
+ public_key_file = dict(default=None),
+ state = dict(default='present',
+ choices=['absent', 'present']),
+ )
+
+ module_kwargs = openstack_module_kwargs(
+ mutually_exclusive=[['public_key', 'public_key_file']])
+
+ module = AnsibleModule(argument_spec,
+ supports_check_mode=True,
+ **module_kwargs)
+
+ if not HAS_SHADE:
+ module.fail_json(msg='shade is required for this module')
+
+ state = module.params['state']
+ name = module.params['name']
+ public_key = module.params['public_key']
+
+ if module.params['public_key_file']:
+ public_key = open(module.params['public_key_file']).read()
+ public_key = public_key.rstrip()
+
+ try:
+ cloud = shade.openstack_cloud(**module.params)
+ keypair = cloud.get_keypair(name)
+
+ if module.check_mode:
+ module.exit_json(changed=_system_state_change(module, keypair))
+
+ if state == 'present':
+ if keypair and keypair['name'] == name:
+ if public_key and (public_key != keypair['public_key']):
+ module.fail_json(
+ msg="Key name %s present but key hash not the same"
+ " as offered. Delete key first." % name
+ )
+ else:
+ module.exit_json(changed=False, key=keypair)
+
+ new_key = cloud.create_keypair(name, public_key)
+ module.exit_json(changed=True, key=new_key)
+
+ elif state == 'absent':
+ if keypair:
+ cloud.delete_keypair(name)
+ module.exit_json(changed=True)
+ module.exit_json(changed=False)
+
+ except shade.OpenStackCloudException as e:
+ module.fail_json(msg=e.message)
+
+# this is magic, see lib/ansible/module_common.py
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+if __name__ == '__main__':
+ main()
diff --git a/cloud/openstack/os_network.py b/cloud/openstack/os_network.py
index 75c431493f6..f911ce71af1 100644
--- a/cloud/openstack/os_network.py
+++ b/cloud/openstack/os_network.py
@@ -57,8 +57,13 @@ requirements: ["shade"]
EXAMPLES = '''
- os_network:
- name=t1network
- state=present
+ name: t1network
+ state: present
+ auth:
+ auth_url: https://your_api_url.com:9000/v2.0
+ username: user
+ password: password
+ project_name: someproject
'''
diff --git a/cloud/openstack/os_nova_flavor.py b/cloud/openstack/os_nova_flavor.py
new file mode 100644
index 00000000000..82b3a53aa3d
--- /dev/null
+++ b/cloud/openstack/os_nova_flavor.py
@@ -0,0 +1,237 @@
+#!/usr/bin/python
+
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+#
+# This module 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.
+#
+# This software 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 software. If not, see .
+
+try:
+ import shade
+ HAS_SHADE = True
+except ImportError:
+ HAS_SHADE = False
+
+DOCUMENTATION = '''
+---
+module: os_nova_flavor
+short_description: Manage OpenStack compute flavors
+extends_documentation_fragment: openstack
+version_added: "2.0"
+author: "David Shrewsbury (@Shrews)"
+description:
+ - Add or remove flavors from OpenStack.
+options:
+ state:
+ description:
+ - Indicate desired state of the resource. When I(state) is 'present',
+ then I(ram), I(vcpus), and I(disk) are all required. There are no
+ default values for those parameters.
+ choices: ['present', 'absent']
+ required: false
+ default: present
+ name:
+ description:
+ - Flavor name.
+ required: true
+ ram:
+ description:
+ - Amount of memory, in MB.
+ required: false
+ default: null
+ vcpus:
+ description:
+ - Number of virtual CPUs.
+ required: false
+ default: null
+ disk:
+ description:
+ - Size of local disk, in GB.
+ required: false
+ default: null
+ ephemeral:
+ description:
+ - Ephemeral space size, in GB.
+ required: false
+ default: 0
+ swap:
+ description:
+ - Swap space size, in MB.
+ required: false
+ default: 0
+ rxtx_factor:
+ description:
+ - RX/TX factor.
+ required: false
+ default: 1.0
+ is_public:
+ description:
+ - Make flavor accessible to the public.
+ required: false
+ default: true
+ flavorid:
+ description:
+ - ID for the flavor. This is optional as a unique UUID will be
+ assigned if a value is not specified.
+ required: false
+ default: "auto"
+requirements: ["shade"]
+'''
+
+EXAMPLES = '''
+# Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of
+# local disk, and 10GB of ephemeral.
+- os_nova_flavor:
+ cloud=mycloud
+ state=present
+ name=tiny
+ ram=1024
+ vcpus=1
+ disk=10
+ ephemeral=10
+
+# Delete 'tiny' flavor
+- os_nova_flavor:
+ cloud=mycloud
+ state=absent
+ name=tiny
+'''
+
+RETURN = '''
+flavor:
+ description: Dictionary describing the flavor.
+ returned: On success when I(state) is 'present'
+ type: dictionary
+ contains:
+ id:
+ description: Flavor ID.
+ returned: success
+ type: string
+ sample: "515256b8-7027-4d73-aa54-4e30a4a4a339"
+ name:
+ description: Flavor name.
+ returned: success
+ type: string
+ sample: "tiny"
+ disk:
+ description: Size of local disk, in GB.
+ returned: success
+ type: int
+ sample: 10
+ ephemeral:
+ description: Ephemeral space size, in GB.
+ returned: success
+ type: int
+ sample: 10
+ ram:
+ description: Amount of memory, in MB.
+ returned: success
+ type: int
+ sample: 1024
+ swap:
+ description: Swap space size, in MB.
+ returned: success
+ type: int
+ sample: 100
+ vcpus:
+ description: Number of virtual CPUs.
+ returned: success
+ type: int
+ sample: 2
+ is_public:
+ description: Make flavor accessible to the public.
+ returned: success
+ type: bool
+ sample: true
+'''
+
+
+def _system_state_change(module, flavor):
+ state = module.params['state']
+ if state == 'present' and not flavor:
+ return True
+ if state == 'absent' and flavor:
+ return True
+ return False
+
+
+def main():
+ argument_spec = openstack_full_argument_spec(
+ state = dict(required=False, default='present',
+ choices=['absent', 'present']),
+ name = dict(required=False),
+
+ # required when state is 'present'
+ ram = dict(required=False, type='int'),
+ vcpus = dict(required=False, type='int'),
+ disk = dict(required=False, type='int'),
+
+ ephemeral = dict(required=False, default=0, type='int'),
+ swap = dict(required=False, default=0, type='int'),
+ rxtx_factor = dict(required=False, default=1.0, type='float'),
+ is_public = dict(required=False, default=True, type='bool'),
+ flavorid = dict(required=False, default="auto"),
+ )
+
+ module_kwargs = openstack_module_kwargs()
+ module = AnsibleModule(
+ argument_spec,
+ supports_check_mode=True,
+ required_if=[
+ ('state', 'present', ['ram', 'vcpus', 'disk'])
+ ],
+ **module_kwargs)
+
+ if not HAS_SHADE:
+ module.fail_json(msg='shade is required for this module')
+
+ state = module.params['state']
+ name = module.params['name']
+
+ try:
+ cloud = shade.operator_cloud(**module.params)
+ flavor = cloud.get_flavor(name)
+
+ if module.check_mode:
+ module.exit_json(changed=_system_state_change(module, flavor))
+
+ if state == 'present':
+ if not flavor:
+ flavor = cloud.create_flavor(
+ name=name,
+ ram=module.params['ram'],
+ vcpus=module.params['vcpus'],
+ disk=module.params['disk'],
+ flavorid=module.params['flavorid'],
+ ephemeral=module.params['ephemeral'],
+ swap=module.params['swap'],
+ rxtx_factor=module.params['rxtx_factor'],
+ is_public=module.params['is_public']
+ )
+ module.exit_json(changed=True, flavor=flavor)
+ module.exit_json(changed=False, flavor=flavor)
+
+ elif state == 'absent':
+ if flavor:
+ cloud.delete_flavor(name)
+ module.exit_json(changed=True)
+ module.exit_json(changed=False)
+
+ except shade.OpenStackCloudException as e:
+ module.fail_json(msg=e.message)
+
+
+# this is magic, see lib/ansible/module_common.py
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+if __name__ == '__main__':
+ main()
diff --git a/cloud/openstack/os_security_group_rule.py b/cloud/openstack/os_security_group_rule.py
new file mode 100644
index 00000000000..b2324b097ce
--- /dev/null
+++ b/cloud/openstack/os_security_group_rule.py
@@ -0,0 +1,327 @@
+#!/usr/bin/python
+
+# Copyright (c) 2015 Hewlett-Packard Development Company, L.P.
+# Copyright (c) 2013, Benno Joy
+#
+# This module 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.
+#
+# This software 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 software. If not, see .
+
+try:
+ import shade
+ HAS_SHADE = True
+except ImportError:
+ HAS_SHADE = False
+
+
+DOCUMENTATION = '''
+---
+module: os_security_group_rule
+short_description: Add/Delete rule from an existing security group
+extends_documentation_fragment: openstack
+version_added: "2.0"
+description:
+ - Add or Remove rule from an existing security group
+options:
+ security_group:
+ description:
+ - Name of the security group
+ required: true
+ protocol:
+ description:
+ - IP protocol
+ choices: ['tcp', 'udp', 'icmp', None]
+ default: None
+ port_range_min:
+ description:
+ - Starting port
+ required: false
+ default: None
+ port_range_max:
+ description:
+ - Ending port
+ required: false
+ default: None
+ remote_ip_prefix:
+ description:
+ - Source IP address(es) in CIDR notation (exclusive with remote_group)
+ required: false
+ remote_group:
+ description:
+ - ID of Security group to link (exclusive with remote_ip_prefix)
+ required: false
+ ethertype:
+ description:
+ - Must be IPv4 or IPv6, and addresses represented in CIDR must
+ match the ingress or egress rules. Not all providers support IPv6.
+ choices: ['IPv4', 'IPv6']
+ default: IPv4
+ direction:
+ description:
+ - The direction in which the security group rule is applied. Not
+ all providers support egress.
+ choices: ['egress', 'ingress']
+ default: ingress
+ state:
+ description:
+ - Should the resource be present or absent.
+ choices: [present, absent]
+ default: present
+requirements: ["shade"]
+'''
+
+EXAMPLES = '''
+# Create a security group rule
+- os_security_group_rule:
+ cloud: mordred
+ security_group: foo
+ protocol: tcp
+ port_range_min: 80
+ port_range_max: 80
+ remote_ip_prefix: 0.0.0.0/0
+
+# Create a security group rule for ping
+- os_security_group_rule:
+ cloud: mordred
+ security_group: foo
+ protocol: icmp
+ remote_ip_prefix: 0.0.0.0/0
+
+# Another way to create the ping rule
+- os_security_group_rule:
+ cloud: mordred
+ security_group: foo
+ protocol: icmp
+ port_range_min: -1
+ port_range_max: -1
+ remote_ip_prefix: 0.0.0.0/0
+
+# Create a TCP rule covering all ports
+- os_security_group_rule:
+ cloud: mordred
+ security_group: foo
+ protocol: tcp
+ port_range_min: 1
+ port_range_max: 65535
+ remote_ip_prefix: 0.0.0.0/0
+
+# Another way to create the TCP rule above (defaults to all ports)
+- os_security_group_rule:
+ cloud: mordred
+ security_group: foo
+ protocol: tcp
+ remote_ip_prefix: 0.0.0.0/0
+'''
+
+RETURN = '''
+id:
+ description: Unique rule UUID.
+ type: string
+direction:
+ description: The direction in which the security group rule is applied.
+ type: string
+ sample: 'egress'
+ethertype:
+ description: One of IPv4 or IPv6.
+ type: string
+ sample: 'IPv4'
+port_range_min:
+ description: The minimum port number in the range that is matched by
+ the security group rule.
+ type: int
+ sample: 8000
+port_range_max:
+ description: The maximum port number in the range that is matched by
+ the security group rule.
+ type: int
+ sample: 8000
+protocol:
+ description: The protocol that is matched by the security group rule.
+ type: string
+ sample: 'tcp'
+remote_ip_prefix:
+ description: The remote IP prefix to be associated with this security group rule.
+ type: string
+ sample: '0.0.0.0/0'
+security_group_id:
+ description: The security group ID to associate with this security group rule.
+ type: string
+'''
+
+
+def _ports_match(protocol, module_min, module_max, rule_min, rule_max):
+ """
+ Capture the complex port matching logic.
+
+ The port values coming in for the module might be -1 (for ICMP),
+ which will work only for Nova, but this is handled by shade. Likewise,
+ they might be None, which works for Neutron, but not Nova. This too is
+ handled by shade. Since shade will consistently return these port
+ values as None, we need to convert any -1 values input to the module
+ to None here for comparison.
+
+ For TCP and UDP protocols, None values for both min and max are
+ represented as the range 1-65535 for Nova, but remain None for
+ Neutron. Shade returns the full range when Nova is the backend (since
+ that is how Nova stores them), and None values for Neutron. If None
+ values are input to the module for both values, then we need to adjust
+ for comparison.
+ """
+
+ # Check if the user is supplying -1 for ICMP.
+ if protocol == 'icmp':
+ if module_min and int(module_min) == -1:
+ module_min = None
+ if module_max and int(module_max) == -1:
+ module_max = None
+
+ # Check if user is supplying None values for full TCP/UDP port range.
+ if protocol in ['tcp', 'udp'] and module_min is None and module_max is None:
+ if (rule_min and int(rule_min) == 1
+ and rule_max and int(rule_max) == 65535):
+ # (None, None) == (1, 65535)
+ return True
+
+ # Sanity check to make sure we don't have type comparison issues.
+ if module_min:
+ module_min = int(module_min)
+ if module_max:
+ module_max = int(module_max)
+ if rule_min:
+ rule_min = int(rule_min)
+ if rule_max:
+ rule_max = int(rule_max)
+
+ return module_min == rule_min and module_max == rule_max
+
+
+def _find_matching_rule(module, secgroup):
+ """
+ Find a rule in the group that matches the module parameters.
+ :returns: The matching rule dict, or None if no matches.
+ """
+ protocol = module.params['protocol']
+ remote_ip_prefix = module.params['remote_ip_prefix']
+ ethertype = module.params['ethertype']
+ direction = module.params['direction']
+ remote_group_id = module.params['remote_group']
+
+ for rule in secgroup['security_group_rules']:
+ if (protocol == rule['protocol']
+ and remote_ip_prefix == rule['remote_ip_prefix']
+ and ethertype == rule['ethertype']
+ and direction == rule['direction']
+ and remote_group_id == rule['remote_group_id']
+ and _ports_match(protocol,
+ module.params['port_range_min'],
+ module.params['port_range_max'],
+ rule['port_range_min'],
+ rule['port_range_max'])):
+ return rule
+ return None
+
+
+def _system_state_change(module, secgroup):
+ state = module.params['state']
+ if secgroup:
+ rule_exists = _find_matching_rule(module, secgroup)
+ else:
+ return False
+
+ if state == 'present' and not rule_exists:
+ return True
+ if state == 'absent' and rule_exists:
+ return True
+ return False
+
+
+def main():
+ argument_spec = openstack_full_argument_spec(
+ security_group = dict(required=True),
+ # NOTE(Shrews): None is an acceptable protocol value for
+ # Neutron, but Nova will balk at this.
+ protocol = dict(default=None,
+ choices=[None, 'tcp', 'udp', 'icmp']),
+ port_range_min = dict(required=False, type='int'),
+ port_range_max = dict(required=False, type='int'),
+ remote_ip_prefix = dict(required=False, default=None),
+ # TODO(mordred): Make remote_group handle name and id
+ remote_group = dict(required=False, default=None),
+ ethertype = dict(default='IPv4',
+ choices=['IPv4', 'IPv6']),
+ direction = dict(default='ingress',
+ choices=['egress', 'ingress']),
+ state = dict(default='present',
+ choices=['absent', 'present']),
+ )
+
+ module_kwargs = openstack_module_kwargs(
+ mutually_exclusive=[
+ ['remote_ip_prefix', 'remote_group'],
+ ]
+ )
+
+ module = AnsibleModule(argument_spec,
+ supports_check_mode=True,
+ **module_kwargs)
+
+ if not HAS_SHADE:
+ module.fail_json(msg='shade is required for this module')
+
+ state = module.params['state']
+ security_group = module.params['security_group']
+ changed = False
+
+ try:
+ cloud = shade.openstack_cloud(**module.params)
+ secgroup = cloud.get_security_group(security_group)
+
+ if module.check_mode:
+ module.exit_json(changed=_system_state_change(module, secgroup))
+
+ if state == 'present':
+ if not secgroup:
+ module.fail_json(msg='Could not find security group %s' %
+ security_group)
+
+ rule = _find_matching_rule(module, secgroup)
+ if not rule:
+ rule = cloud.create_security_group_rule(
+ secgroup['id'],
+ port_range_min=module.params['port_range_min'],
+ port_range_max=module.params['port_range_max'],
+ protocol=module.params['protocol'],
+ remote_ip_prefix=module.params['remote_ip_prefix'],
+ remote_group_id=module.params['remote_group'],
+ direction=module.params['direction'],
+ ethertype=module.params['ethertype']
+ )
+ changed = True
+ module.exit_json(changed=changed, rule=rule, id=rule['id'])
+
+ if state == 'absent' and secgroup:
+ rule = _find_matching_rule(module, secgroup)
+ if rule:
+ cloud.delete_security_group_rule(rule['id'])
+ changed = True
+
+ module.exit_json(changed=changed)
+
+ except shade.OpenStackCloudException as e:
+ module.fail_json(msg=e.message)
+
+# this is magic, see lib/ansible/module_common.py
+from ansible.module_utils.basic import *
+from ansible.module_utils.openstack import *
+
+if __name__ == '__main__':
+ main()
diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py
index 78a46f78c04..44481d643f4 100644
--- a/cloud/openstack/os_server.py
+++ b/cloud/openstack/os_server.py
@@ -90,6 +90,11 @@ options:
- Ensure instance has public ip however the cloud wants to do that
required: false
default: 'yes'
+ auto_floating_ip:
+ description:
+ - If the module should automatically assign a floating IP
+ required: false
+ default: 'yes'
floating_ips:
description:
- list of valid floating IPs that pre-exist to assign to this node
@@ -132,7 +137,7 @@ options:
- Boot instance from a volume
required: false
default: None
- terminate_volume:
+ terminate_volume:
description:
- If true, delete volume when deleting instance (if booted from volume)
default: false
@@ -257,6 +262,15 @@ def _network_args(module, cloud):
msg='Could not find network by net-name: %s' %
net['net-name'])
args.append({'net-id': by_name['id']})
+ elif net.get('port-id'):
+ args.append(net)
+ elif net.get('port-name'):
+ by_name = cloud.get_port(net['port-name'])
+ if not by_name:
+ module.fail_json(
+ msg='Could not find port by port-name: %s' %
+ net['port-name'])
+ args.append({'port-id': by_name['id']})
return args
@@ -282,8 +296,12 @@ def _create_server(module, cloud):
if flavor:
flavor_dict = cloud.get_flavor(flavor)
+ if not flavor_dict:
+ module.fail_json(msg="Could not find flavor %s" % flavor)
else:
flavor_dict = cloud.get_flavor_by_ram(flavor_ram, flavor_include)
+ if not flavor_dict:
+ module.fail_json(msg="Could not find any matching flavor")
nics = _network_args(module, cloud)
@@ -387,7 +405,7 @@ def main():
flavor_include = dict(default=None),
key_name = dict(default=None),
security_groups = dict(default='default'),
- nics = dict(default=[]),
+ nics = dict(default=[], type='list'),
meta = dict(default=None),
userdata = dict(default=None),
config_drive = dict(default=False, type='bool'),
diff --git a/cloud/openstack/os_subnet.py b/cloud/openstack/os_subnet.py
index f96ce9fd633..22876c80869 100644
--- a/cloud/openstack/os_subnet.py
+++ b/cloud/openstack/os_subnet.py
@@ -92,6 +92,18 @@ options:
- A list of host route dictionaries for the subnet.
required: false
default: None
+ ipv6_ra_mode:
+ description:
+ - IPv6 router advertisement mode
+ choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
+ required: false
+ default: None
+ ipv6_address_mode:
+ description:
+ - IPv6 address mode
+ choices: ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
+ required: false
+ default: None
requirements:
- "python >= 2.6"
- "shade"
@@ -117,11 +129,53 @@ EXAMPLES = '''
- os_subnet:
state=absent
name=net1subnet
+
+# Create an ipv6 stateless subnet
+- os_subnet:
+ state: present
+ name: intv6
+ network_name: internal
+ ip_version: 6
+ cidr: 2db8:1::/64
+ dns_nameservers:
+ - 2001:4860:4860::8888
+ - 2001:4860:4860::8844
+ ipv6_ra_mode: dhcpv6-stateless
+ ipv6_address_mode: dhcpv6-stateless
'''
+def _can_update(subnet, module, cloud):
+ """Check for differences in non-updatable values"""
+ network_name = module.params['network_name']
+ cidr = module.params['cidr']
+ ip_version = int(module.params['ip_version'])
+ ipv6_ra_mode = module.params['ipv6_ra_mode']
+ ipv6_a_mode = module.params['ipv6_address_mode']
-def _needs_update(subnet, module):
+ if network_name:
+ network = cloud.get_network(network_name)
+ if network:
+ netid = network['id']
+ else:
+ module.fail_json(msg='No network found for %s' % network_name)
+ if netid != subnet['network_id']:
+ module.fail_json(msg='Cannot update network_name in existing \
+ subnet')
+ if ip_version and subnet['ip_version'] != ip_version:
+ module.fail_json(msg='Cannot update ip_version in existing subnet')
+ if ipv6_ra_mode and subnet.get('ipv6_ra_mode', None) != ip_version:
+ module.fail_json(msg='Cannot update ipv6_ra_mode in existing subnet')
+ if ipv6_a_mode and subnet.get('ipv6_address_mode', None) != ipv6_a_mode:
+ module.fail_json(msg='Cannot update ipv6_address_mode in existing \
+ subnet')
+
+def _needs_update(subnet, module, cloud):
"""Check for differences in the updatable values."""
+
+ # First check if we are trying to update something we're not allowed to
+ _can_update(subnet, module, cloud)
+
+ # now check for the things we are allowed to update
enable_dhcp = module.params['enable_dhcp']
subnet_name = module.params['name']
pool_start = module.params['allocation_pool_start']
@@ -151,18 +205,19 @@ def _needs_update(subnet, module):
return False
-def _system_state_change(module, subnet):
+def _system_state_change(module, subnet, cloud):
state = module.params['state']
if state == 'present':
if not subnet:
return True
- return _needs_update(subnet, module)
+ return _needs_update(subnet, module, cloud)
if state == 'absent' and subnet:
return True
return False
def main():
+ ipv6_mode_choices = ['dhcpv6-stateful', 'dhcpv6-stateless', 'slaac']
argument_spec = openstack_full_argument_spec(
name=dict(required=True),
network_name=dict(default=None),
@@ -174,6 +229,8 @@ def main():
allocation_pool_start=dict(default=None),
allocation_pool_end=dict(default=None),
host_routes=dict(default=None, type='list'),
+ ipv6_ra_mode=dict(default=None, choice=ipv6_mode_choices),
+ ipv6_address_mode=dict(default=None, choice=ipv6_mode_choices),
state=dict(default='present', choices=['absent', 'present']),
)
@@ -196,6 +253,8 @@ def main():
pool_start = module.params['allocation_pool_start']
pool_end = module.params['allocation_pool_end']
host_routes = module.params['host_routes']
+ ipv6_ra_mode = module.params['ipv6_ra_mode']
+ ipv6_a_mode = module.params['ipv6_address_mode']
# Check for required parameters when state == 'present'
if state == 'present':
@@ -215,7 +274,8 @@ def main():
subnet = cloud.get_subnet(subnet_name)
if module.check_mode:
- module.exit_json(changed=_system_state_change(module, subnet))
+ module.exit_json(changed=_system_state_change(module, subnet,
+ cloud))
if state == 'present':
if not subnet:
@@ -226,10 +286,12 @@ def main():
gateway_ip=gateway_ip,
dns_nameservers=dns,
allocation_pools=pool,
- host_routes=host_routes)
+ host_routes=host_routes,
+ ipv6_ra_mode=ipv6_ra_mode,
+ ipv6_address_mode=ipv6_a_mode)
changed = True
else:
- if _needs_update(subnet, module):
+ if _needs_update(subnet, module, cloud):
cloud.update_subnet(subnet['id'],
subnet_name=subnet_name,
enable_dhcp=enable_dhcp,
diff --git a/cloud/rackspace/rax_facts.py b/cloud/rackspace/rax_facts.py
index c30df5b9462..481732c0af7 100644
--- a/cloud/rackspace/rax_facts.py
+++ b/cloud/rackspace/rax_facts.py
@@ -97,7 +97,9 @@ def rax_facts(module, address, name, server_id):
servers.append(cs.servers.get(server_id))
except Exception, e:
pass
-
+
+ servers[:] = [server for server in servers if server.status != "DELETED"]
+
if len(servers) > 1:
module.fail_json(msg='Multiple servers found matching provided '
'search parameters')
diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py
index f0239544cec..41da954ac32 100644
--- a/cloud/vmware/vsphere_guest.py
+++ b/cloud/vmware/vsphere_guest.py
@@ -1,6 +1,20 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
+# 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 .
# TODO:
# Ability to set CPU/Memory reservations
@@ -65,13 +79,13 @@ options:
default: null
state:
description:
- - Indicate desired state of the vm.
+ - Indicate desired state of the vm. 'reconfigured' only applies changes to 'memory_mb' and 'num_cpus' in vm_hardware parameter, and only when hot-plugging is enabled for the guest.
default: present
choices: ['present', 'powered_off', 'absent', 'powered_on', 'restarted', 'reconfigured']
from_template:
version_added: "1.9"
description:
- - Specifies if the VM should be deployed from a template (cannot be ran with state)
+ - Specifies if the VM should be deployed from a template (mutually exclusive with 'state' parameter). No guest customization changes to hardware such as CPU, RAM, NICs or Disks can be applied when launching from template.
default: no
choices: ['yes', 'no']
template_src:
@@ -79,6 +93,12 @@ options:
description:
- Name of the source template to deploy from
default: None
+ snapshot_to_clone:
+ description:
+ - A string that when specified, will create a linked clone copy of the VM. Snapshot must already be taken in vCenter.
+ version_added: "2.0"
+ required: false
+ default: none
vm_disk:
description:
- A key, value list of disks and their sizes and which datastore to keep it in.
@@ -132,6 +152,7 @@ EXAMPLES = '''
# Returns changed = True and a adds ansible_facts from the new VM
# State will set the power status of a guest upon creation. Use powered_on to create and boot.
# Options ['state', 'vm_extra_config', 'vm_disk', 'vm_nic', 'vm_hardware', 'esxi'] are required together
+# Note: vm_floppy support added in 2.0
- vsphere_guest:
vcenter_hostname: vcenter.mydomain.local
@@ -165,6 +186,9 @@ EXAMPLES = '''
vm_cdrom:
type: "iso"
iso_path: "DatastoreName/cd-image.iso"
+ vm_floppy:
+ type: "image"
+ image_path: "DatastoreName/floppy-image.flp"
esxi:
datacenter: MyDatacenter
hostname: esx001.mydomain.local
@@ -202,7 +226,6 @@ EXAMPLES = '''
hostname: esx001.mydomain.local
# Deploy a guest from a template
-# No reconfiguration of the destination guest is done at this stage, a reconfigure would be needed to adjust memory/cpu etc..
- vsphere_guest:
vcenter_hostname: vcenter.mydomain.local
username: myuser
@@ -357,6 +380,44 @@ def add_cdrom(module, s, config_target, config, devices, default_devs, type="cli
devices.append(cd_spec)
+def add_floppy(module, s, config_target, config, devices, default_devs, type="image", vm_floppy_image_path=None):
+ # Add a floppy
+ # Make sure the datastore exists.
+ if vm_floppy_image_path:
+ image_location = vm_floppy_image_path.split('/', 1)
+ datastore, ds = find_datastore(
+ module, s, image_location[0], config_target)
+ image_path = image_location[1]
+
+ floppy_spec = config.new_deviceChange()
+ floppy_spec.set_element_operation('add')
+ floppy_ctrl = VI.ns0.VirtualFloppy_Def("floppy_ctrl").pyclass()
+
+ if type == "image":
+ image = VI.ns0.VirtualFloppyImageBackingInfo_Def("image").pyclass()
+ ds_ref = image.new_datastore(ds)
+ ds_ref.set_attribute_type(ds.get_attribute_type())
+ image.set_element_datastore(ds_ref)
+ image.set_element_fileName("%s %s" % (datastore, image_path))
+ floppy_ctrl.set_element_backing(image)
+ floppy_ctrl.set_element_key(3)
+ floppy_spec.set_element_device(floppy_ctrl)
+ elif type == "client":
+ client = VI.ns0.VirtualFloppyRemoteDeviceBackingInfo_Def(
+ "client").pyclass()
+ client.set_element_deviceName("/dev/fd0")
+ floppy_ctrl.set_element_backing(client)
+ floppy_ctrl.set_element_key(3)
+ floppy_spec.set_element_device(floppy_ctrl)
+ else:
+ s.disconnect()
+ module.fail_json(
+ msg="Error adding floppy of type %s to vm spec. "
+ " floppy type can either be image or client" % (type))
+
+ devices.append(floppy_spec)
+
+
def add_nic(module, s, nfmor, config, devices, nic_type="vmxnet3", network_name="VM Network", network_type="standard"):
# add a NIC
# Different network card types are: "VirtualE1000",
@@ -530,7 +591,7 @@ def vmdisk_id(vm, current_datastore_name):
return id_list
-def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, module, cluster_name):
+def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, module, cluster_name, snapshot_to_clone):
vmTemplate = vsphere_client.get_vm_by_name(template_src)
vmTarget = None
@@ -614,9 +675,14 @@ def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, mo
try:
if vmTarget:
changed = False
+ elif snapshot_to_clone is not None:
+ #check if snapshot_to_clone is specified, Create a Linked Clone instead of a full clone.
+ vmTemplate.clone(guest, resourcepool=rpmor, linked=True, snapshot=snapshot_to_clone)
+ changed = True
else:
vmTemplate.clone(guest, resourcepool=rpmor)
changed = True
+
vsphere_client.disconnect()
module.exit_json(changed=changed)
except Exception as e:
@@ -922,6 +988,27 @@ def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest,
# Add a CD-ROM device to the VM.
add_cdrom(module, vsphere_client, config_target, config, devices,
default_devs, cdrom_type, cdrom_iso_path)
+ if 'vm_floppy' in vm_hardware:
+ floppy_image_path = None
+ floppy_type = None
+ try:
+ floppy_type = vm_hardware['vm_floppy']['type']
+ except KeyError:
+ vsphere_client.disconnect()
+ module.fail_json(
+ msg="Error on %s definition. floppy type needs to be"
+ " specified." % vm_hardware['vm_floppy'])
+ if floppy_type == 'image':
+ try:
+ floppy_image_path = vm_hardware['vm_floppy']['image_path']
+ except KeyError:
+ vsphere_client.disconnect()
+ module.fail_json(
+ msg="Error on %s definition. floppy image_path needs"
+ " to be specified." % vm_hardware['vm_floppy'])
+ # Add a floppy to the VM.
+ add_floppy(module, vsphere_client, config_target, config, devices,
+ default_devs, floppy_type, floppy_image_path)
if vm_nic:
for nic in sorted(vm_nic.iterkeys()):
try:
@@ -1218,9 +1305,10 @@ def main():
'reconfigured'
],
default='present'),
- vmware_guest_facts=dict(required=False, choices=BOOLEANS),
- from_template=dict(required=False, choices=BOOLEANS),
+ vmware_guest_facts=dict(required=False, type='bool'),
+ from_template=dict(required=False, type='bool'),
template_src=dict(required=False, type='str'),
+ snapshot_to_clone=dict(required=False, default=None, type='str'),
guest=dict(required=True, type='str'),
vm_disk=dict(required=False, type='dict', default={}),
vm_nic=dict(required=False, type='dict', default={}),
@@ -1229,7 +1317,7 @@ def main():
vm_hw_version=dict(required=False, default=None, type='str'),
resource_pool=dict(required=False, default=None, type='str'),
cluster=dict(required=False, default=None, type='str'),
- force=dict(required=False, choices=BOOLEANS, default=False),
+ force=dict(required=False, type='bool', default=False),
esxi=dict(required=False, type='dict', default={}),
@@ -1245,8 +1333,7 @@ def main():
'vm_hardware',
'esxi'
],
- ['resource_pool', 'cluster'],
- ['from_template', 'resource_pool', 'template_src']
+ ['from_template', 'template_src'],
],
)
@@ -1270,6 +1357,8 @@ def main():
cluster = module.params['cluster']
template_src = module.params['template_src']
from_template = module.params['from_template']
+ snapshot_to_clone = module.params['snapshot_to_clone']
+
# CONNECT TO THE SERVER
viserver = VIServer()
@@ -1349,7 +1438,8 @@ def main():
guest=guest,
template_src=template_src,
module=module,
- cluster_name=cluster
+ cluster_name=cluster,
+ snapshot_to_clone=snapshot_to_clone
)
if state in ['restarted', 'reconfigured']:
module.fail_json(
diff --git a/commands/command.py b/commands/command.py
index b0aa5a7b99f..3fe16882c24 100644
--- a/commands/command.py
+++ b/commands/command.py
@@ -21,6 +21,7 @@
import copy
import sys
import datetime
+import glob
import traceback
import re
import shlex
@@ -29,8 +30,8 @@ import os
DOCUMENTATION = '''
---
module: command
-version_added: historical
short_description: Executes a command on a remote node
+version_added: historical
description:
- The M(command) module takes the command name followed by a list of space-delimited arguments.
- The given command will be executed on all selected nodes. It will not be
@@ -44,15 +45,14 @@ options:
See the examples!
required: true
default: null
- aliases: []
creates:
description:
- - a filename, when it already exists, this step will B(not) be run.
+ - a filename or glob pattern, when it already exists, this step will B(not) be run.
required: no
default: null
removes:
description:
- - a filename, when it does not exist, this step will B(not) be run.
+ - a filename or glob pattern, when it does not exist, this step will B(not) be run.
version_added: "0.8"
required: no
default: null
@@ -143,12 +143,15 @@ def check_command(commandline):
'mount': 'mount', 'rpm': 'yum', 'yum': 'yum', 'apt-get': 'apt-get',
'tar': 'unarchive', 'unzip': 'unarchive', 'sed': 'template or lineinfile',
'rsync': 'synchronize' }
+ become = [ 'sudo', 'su', 'pbrun', 'pfexec', 'runas' ]
warnings = list()
command = os.path.basename(commandline.split()[0])
if command in arguments:
warnings.append("Consider using file module with %s rather than running %s" % (arguments[command], command))
if command in commands:
warnings.append("Consider using %s module rather than running %s" % (commands[command], command))
+ if command in become:
+ warnings.append("Consider using 'become', 'become_method', and 'become_user' rather than running %s" % (command,))
return warnings
@@ -188,7 +191,7 @@ def main():
# and the filename already exists. This allows idempotence
# of command executions.
v = os.path.expanduser(creates)
- if os.path.exists(v):
+ if glob.glob(v):
module.exit_json(
cmd=args,
stdout="skipped, since %s exists" % v,
@@ -202,7 +205,7 @@ def main():
# and the filename does not exist. This allows idempotence
# of command executions.
v = os.path.expanduser(removes)
- if not os.path.exists(v):
+ if not glob.glob(v):
module.exit_json(
cmd=args,
stdout="skipped, since %s does not exist" % v,
diff --git a/commands/raw.py b/commands/raw.py
index 5305c978630..8b9b796a6e7 100644
--- a/commands/raw.py
+++ b/commands/raw.py
@@ -1,10 +1,25 @@
# this is a virtual module that is entirely implemented server side
+# 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 .
+
DOCUMENTATION = '''
---
module: raw
-version_added: historical
short_description: Executes a low-down and dirty SSH command
+version_added: historical
options:
free_form:
description:
@@ -15,7 +30,7 @@ options:
- change the shell used to execute the command. Should be an absolute path to the executable.
required: false
version_added: "1.0"
-description:
+description:
- Executes a low-down and dirty SSH command, not going through the module
subsystem. This is useful and should only be done in two cases. The
first case is installing C(python-simplejson) on older (Python 2.4 and
@@ -34,7 +49,7 @@ notes:
playbooks will follow the trend of using M(command) unless M(shell) is
explicitly required. When running ad-hoc commands, use your best
judgement.
-author:
+author:
- Ansible Core Team
- Michael DeHaan
'''
diff --git a/commands/script.py b/commands/script.py
index ccf15331a6c..9fed7928ce0 100644
--- a/commands/script.py
+++ b/commands/script.py
@@ -1,3 +1,17 @@
+# 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 .
DOCUMENTATION = """
---
diff --git a/commands/shell.py b/commands/shell.py
index cccc90f05ff..23d4962e55f 100644
--- a/commands/shell.py
+++ b/commands/shell.py
@@ -2,6 +2,21 @@
# it runs the 'command' module with special arguments and it behaves differently.
# See the command source and the comment "#USE_SHELL".
+# 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 .
+
DOCUMENTATION = '''
---
module: shell
diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py
index e9a530811d4..33720f5d4f6 100644
--- a/database/mysql/mysql_db.py
+++ b/database/mysql/mysql_db.py
@@ -83,7 +83,8 @@ options:
required: false
notes:
- Requires the MySQLdb Python package on the remote host. For Ubuntu, this
- is as easy as apt-get install python-mysqldb. (See M(apt).)
+ is as easy as apt-get install python-mysqldb. (See M(apt).) For CentOS/Fedora, this
+ is as easy as yum install MySQL-python. (See M(yum).)
- Both I(login_password) and I(login_user) are required when you are
passing credentials. If none are present, the module will attempt to read
the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
@@ -326,7 +327,7 @@ def main():
if state in ['dump','import']:
if target is None:
module.fail_json(msg="with state=%s target is required" % (state))
- if db == 'all':
+ if db == 'all':
connect_to_db = 'mysql'
db = 'mysql'
all_databases = True
diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py
index 763e0e7ebd5..1ea54b41b3a 100644
--- a/database/mysql/mysql_user.py
+++ b/database/mysql/mysql_user.py
@@ -109,7 +109,7 @@ options:
notes:
- Requires the MySQLdb Python package on the remote host. For Ubuntu, this
is as easy as apt-get install python-mysqldb.
- - Both C(login_password) and C(login_username) are required when you are
+ - Both C(login_password) and C(login_user) are required when you are
passing credentials. If none are present, the module will attempt to read
the credentials from C(~/.my.cnf), and finally fall back to using the MySQL
default login of 'root' with no password.
@@ -157,6 +157,7 @@ password=n<_665{vS43y
import getpass
import tempfile
+import re
try:
import MySQLdb
except ImportError:
@@ -291,7 +292,7 @@ def privileges_get(cursor, user,host):
return x
for grant in grants:
- res = re.match("GRANT (.+) ON (.+) TO '.+'@'.+'( IDENTIFIED BY PASSWORD '.+')? ?(.*)", grant[0])
+ res = re.match("GRANT (.+) ON (.+) TO '.*'@'.+'( IDENTIFIED BY PASSWORD '.+')? ?(.*)", grant[0])
if res is None:
raise InvalidPrivsError('unable to parse the MySQL grant string: %s' % grant[0])
privileges = res.group(1).split(", ")
@@ -316,13 +317,22 @@ def privileges_unpack(priv):
not specified in the string, as MySQL will always provide this by default.
"""
output = {}
+ privs = []
for item in priv.strip().split('/'):
pieces = item.strip().split(':')
dbpriv = pieces[0].rsplit(".", 1)
- pieces[0] = "`%s`.%s" % (dbpriv[0].strip('`'), dbpriv[1])
+ # Do not escape if privilege is for database '*' (all databases)
+ if dbpriv[0].strip('`') != '*':
+ pieces[0] = "`%s`.%s" % (dbpriv[0].strip('`'), dbpriv[1])
- output[pieces[0]] = [s.strip() for s in pieces[1].upper().split(',')]
- new_privs = frozenset(output[pieces[0]])
+ if '(' in pieces[1]:
+ output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper())
+ for i in output[pieces[0]]:
+ privs.append(re.sub(r'\(.*\)','',i))
+ else:
+ output[pieces[0]] = pieces[1].upper().split(',')
+ privs = output[pieces[0]]
+ new_privs = frozenset(privs)
if not new_privs.issubset(VALID_PRIVS):
raise InvalidPrivsError('Invalid privileges specified: %s' % new_privs.difference(VALID_PRIVS))
diff --git a/database/mysql/mysql_variables.py b/database/mysql/mysql_variables.py
index 0b0face0328..d7187e85733 100644
--- a/database/mysql/mysql_variables.py
+++ b/database/mysql/mysql_variables.py
@@ -52,6 +52,11 @@ options:
description:
- mysql host to connect
required: False
+ login_port:
+ version_added: "2.0"
+ description:
+ - mysql port to connect
+ required: False
login_unix_socket:
description:
- unix socket to connect mysql server
@@ -68,6 +73,7 @@ EXAMPLES = '''
import ConfigParser
import os
import warnings
+from re import match
try:
import MySQLdb
@@ -104,10 +110,12 @@ def typedvalue(value):
def getvariable(cursor, mysqlvar):
- cursor.execute("SHOW VARIABLES LIKE %s", (mysqlvar,))
+ cursor.execute("SHOW VARIABLES WHERE Variable_name = %s", (mysqlvar,))
mysqlvar_val = cursor.fetchall()
- return mysqlvar_val
-
+ if len(mysqlvar_val) is 1:
+ return mysqlvar_val[0][1]
+ else:
+ return None
def setvariable(cursor, mysqlvar, value):
""" Set a global mysql variable to a given value
@@ -117,11 +125,9 @@ def setvariable(cursor, mysqlvar, value):
should be passed as numeric literals.
"""
- query = ["SET GLOBAL %s" % mysql_quote_identifier(mysqlvar, 'vars') ]
- query.append(" = %s")
- query = ' '.join(query)
+ query = "SET GLOBAL %s = " % mysql_quote_identifier(mysqlvar, 'vars')
try:
- cursor.execute(query, (value,))
+ cursor.execute(query + "%s", (value,))
cursor.fetchall()
result = True
except Exception, e:
@@ -193,7 +199,8 @@ def main():
argument_spec = dict(
login_user=dict(default=None),
login_password=dict(default=None),
- login_host=dict(default="localhost"),
+ login_host=dict(default="127.0.0.1"),
+ login_port=dict(default="3306", type='int'),
login_unix_socket=dict(default=None),
variable=dict(default=None),
value=dict(default=None)
@@ -203,8 +210,13 @@ def main():
user = module.params["login_user"]
password = module.params["login_password"]
host = module.params["login_host"]
+ port = module.params["login_port"]
mysqlvar = module.params["variable"]
value = module.params["value"]
+ if mysqlvar is None:
+ module.fail_json(msg="Cannot run without variable to operate with")
+ if match('^[0-9a-z_]+$', mysqlvar) is None:
+ module.fail_json(msg="invalid variable name \"%s\"" % mysqlvar)
if not mysqldb_found:
module.fail_json(msg="the python mysqldb module is required")
else:
@@ -227,23 +239,21 @@ def main():
module.fail_json(msg="when supplying login arguments, both login_user and login_password must be provided")
try:
if module.params["login_unix_socket"]:
- db_connection = MySQLdb.connect(host=module.params["login_host"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
+ db_connection = MySQLdb.connect(host=module.params["login_host"], port=module.params["login_port"], unix_socket=module.params["login_unix_socket"], user=login_user, passwd=login_password, db="mysql")
else:
- db_connection = MySQLdb.connect(host=module.params["login_host"], user=login_user, passwd=login_password, db="mysql")
+ db_connection = MySQLdb.connect(host=module.params["login_host"], port=module.params["login_port"], user=login_user, passwd=login_password, db="mysql")
cursor = db_connection.cursor()
except Exception, e:
module.fail_json(msg="unable to connect to database, check login_user and login_password are correct or ~/.my.cnf has the credentials")
- if mysqlvar is None:
- module.fail_json(msg="Cannot run without variable to operate with")
mysqlvar_val = getvariable(cursor, mysqlvar)
+ if mysqlvar_val is None:
+ module.fail_json(msg="Variable not available \"%s\"" % mysqlvar, changed=False)
if value is None:
module.exit_json(msg=mysqlvar_val)
else:
- if len(mysqlvar_val) < 1:
- module.fail_json(msg="Variable not available", changed=False)
# Type values before using them
value_wanted = typedvalue(value)
- value_actual = typedvalue(mysqlvar_val[0][1])
+ value_actual = typedvalue(mysqlvar_val)
if value_wanted == value_actual:
module.exit_json(msg="Variable already set to requested value", changed=False)
try:
diff --git a/database/postgresql/postgresql_privs.py b/database/postgresql/postgresql_privs.py
index 10f2361bfb2..8fefd3de648 100644
--- a/database/postgresql/postgresql_privs.py
+++ b/database/postgresql/postgresql_privs.py
@@ -315,7 +315,7 @@ class Connection(object):
query = """SELECT relname
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
- WHERE nspname = %s AND relkind = 'r'"""
+ WHERE nspname = %s AND relkind in ('r', 'v')"""
self.cursor.execute(query, (schema,))
return [t[0] for t in self.cursor.fetchall()]
diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py
index d3f6d81c360..cee5a9ae131 100644
--- a/database/postgresql/postgresql_user.py
+++ b/database/postgresql/postgresql_user.py
@@ -92,7 +92,7 @@ options:
description:
- "PostgreSQL role attributes string in the format: CREATEDB,CREATEROLE,SUPERUSER"
required: false
- default: null
+ default: ""
choices: [ "[NO]SUPERUSER","[NO]CREATEROLE", "[NO]CREATEUSER", "[NO]CREATEDB",
"[NO]INHERIT", "[NO]LOGIN", "[NO]REPLICATION" ]
state:
@@ -233,7 +233,7 @@ def user_alter(cursor, module, user, password, role_attr_flags, encrypted, expir
return False
# Handle passwords.
- if not no_password_changes and (password is not None or role_attr_flags is not None):
+ if not no_password_changes and (password is not None or role_attr_flags != ''):
# Select password and all flag-like columns in order to verify changes.
query_password_data = dict(password=password, expires=expires)
select = "SELECT * FROM pg_authid where rolname=%(user)s"
@@ -490,10 +490,10 @@ def parse_role_attrs(role_attr_flags):
def normalize_privileges(privs, type_):
new_privs = set(privs)
- if 'ALL' in privs:
+ if 'ALL' in new_privs:
new_privs.update(VALID_PRIVS[type_])
new_privs.remove('ALL')
- if 'TEMP' in privs:
+ if 'TEMP' in new_privs:
new_privs.add('TEMPORARY')
new_privs.remove('TEMP')
diff --git a/files/acl.py b/files/acl.py
index 0c924fee94c..ad0f4607609 100644
--- a/files/acl.py
+++ b/files/acl.py
@@ -1,4 +1,5 @@
#!/usr/bin/python
+# -*- coding: utf-8 -*-
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
@@ -20,7 +21,9 @@ module: acl
version_added: "1.4"
short_description: Sets and retrieves file ACL information.
description:
- - Sets and retrieves file ACL information.
+ - Sets and retrieves file ACL information.
+notes:
+ - As of Ansible 2.0, this module only supports Linux distributions.
options:
name:
required: true
@@ -79,7 +82,16 @@ options:
description:
- DEPRECATED. The acl to set or remove. This must always be quoted in the form of '::'. The qualifier may be empty for some types, but the type and perms are always requried. '-' can be used as placeholder when you do not care about permissions. This is now superseded by entity, type and permissions fields.
-author: "Brian Coca (@bcoca)"
+ recursive:
+ version_added: "2.0"
+ required: false
+ default: no
+ choices: [ 'yes', 'no' ]
+ description:
+ - Recursively sets the specified ACL (added in Ansible 2.0). Incompatible with C(state=query).
+author:
+ - "Brian Coca (@bcoca)"
+ - "Jérémie Astori (@astorije)"
notes:
- The "acl" module requires that acls are enabled on the target filesystem and that the setfacl and getfacl binaries are installed.
'''
@@ -110,35 +122,15 @@ acl:
sample: [ "user::rwx", "group::rwx", "other::rwx" ]
'''
-def normalize_permissions(p):
- perms = ['-','-','-']
- for char in p:
- if char == 'r':
- perms[0] = 'r'
- if char == 'w':
- perms[1] = 'w'
- if char == 'x':
- perms[2] = 'x'
- if char == 'X':
- if perms[2] != 'x': # 'x' is more permissive
- perms[2] = 'X'
- return ''.join(perms)
def split_entry(entry):
''' splits entry and ensures normalized return'''
a = entry.split(':')
- a.reverse()
- if len(a) == 3:
- a.append(False)
- try:
- p,e,t,d = a
- except ValueError, e:
- print "wtf?? %s => %s" % (entry,a)
- raise e
+ if len(a) == 2:
+ a.append(None)
- if d:
- d = True
+ t, e, p = a
if t.startswith("u"):
t = "user"
@@ -151,69 +143,98 @@ def split_entry(entry):
else:
t = None
- p = normalize_permissions(p)
+ return [t, e, p]
- return [d,t,e,p]
-def get_acls(module,path,follow):
+def build_entry(etype, entity, permissions=None):
+ '''Builds and returns an entry string. Does not include the permissions bit if they are not provided.'''
+ if permissions:
+ return etype + ':' + entity + ':' + permissions
+ else:
+ return etype + ':' + entity
+
+
+def build_command(module, mode, path, follow, default, recursive, entry=''):
+ '''Builds and returns a getfacl/setfacl command.'''
+ if mode == 'set':
+ cmd = [module.get_bin_path('setfacl', True)]
+ cmd.append('-m "%s"' % entry)
+ elif mode == 'rm':
+ cmd = [module.get_bin_path('setfacl', True)]
+ cmd.append('-x "%s"' % entry)
+ else: # mode == 'get'
+ cmd = [module.get_bin_path('getfacl', True)]
+ # prevents absolute path warnings and removes headers
+ cmd.append('--omit-header')
+ cmd.append('--absolute-names')
+
+ if recursive:
+ cmd.append('--recursive')
- cmd = [ module.get_bin_path('getfacl', True) ]
if not follow:
- cmd.append('-h')
- # prevents absolute path warnings and removes headers
- cmd.append('--omit-header')
- cmd.append('--absolute-names')
- cmd.append(path)
+ cmd.append('--physical')
- return _run_acl(module,cmd)
-
-def set_acl(module,path,entry,follow,default):
-
- cmd = [ module.get_bin_path('setfacl', True) ]
- if not follow:
- cmd.append('-h')
if default:
- cmd.append('-d')
- cmd.append('-m "%s"' % entry)
+ if(mode == 'rm'):
+ cmd.append('-k')
+ else: # mode == 'set' or mode == 'get'
+ cmd.append('-d')
+
cmd.append(path)
+ return cmd
- return _run_acl(module,cmd)
-def rm_acl(module,path,entry,follow,default):
+def acl_changed(module, cmd):
+ '''Returns true if the provided command affects the existing ACLs, false otherwise.'''
+ cmd = cmd[:] # lists are mutables so cmd would be overriden without this
+ cmd.insert(1, '--test')
+ lines = run_acl(module, cmd)
- cmd = [ module.get_bin_path('setfacl', True) ]
- if not follow:
- cmd.append('-h')
- if default:
- cmd.append('-k')
- entry = entry[0:entry.rfind(':')]
- cmd.append('-x "%s"' % entry)
- cmd.append(path)
+ for line in lines:
+ if not line.endswith('*,*'):
+ return True
+ return False
- return _run_acl(module,cmd,False)
-def _run_acl(module,cmd,check_rc=True):
+def run_acl(module, cmd, check_rc=True):
try:
(rc, out, err) = module.run_command(' '.join(cmd), check_rc=check_rc)
except Exception, e:
module.fail_json(msg=e.strerror)
- # trim last line as it is always empty
- ret = out.splitlines()
- return ret[0:len(ret)-1]
+ lines = out.splitlines()
+ if lines and not lines[-1].split():
+ # trim last line only when it is empty
+ return lines[:-1]
+ else:
+ return lines
+
def main():
+ if get_platform().lower() != 'linux':
+ module.fail_json(msg="The acl module is only available for Linux distributions.")
+
module = AnsibleModule(
- argument_spec = dict(
- name = dict(required=True,aliases=['path'], type='str'),
- entry = dict(required=False, etype='str'),
- entity = dict(required=False, type='str', default=''),
- etype = dict(required=False, choices=['other', 'user', 'group', 'mask'], type='str'),
- permissions = dict(required=False, type='str'),
- state = dict(required=False, default='query', choices=[ 'query', 'present', 'absent' ], type='str'),
- follow = dict(required=False, type='bool', default=True),
- default= dict(required=False, type='bool', default=False),
+ argument_spec=dict(
+ name=dict(required=True, aliases=['path'], type='str'),
+ entry=dict(required=False, type='str'),
+ entity=dict(required=False, type='str', default=''),
+ etype=dict(
+ required=False,
+ choices=['other', 'user', 'group', 'mask'],
+ type='str'
+ ),
+ permissions=dict(required=False, type='str'),
+ state=dict(
+ required=False,
+ default='query',
+ choices=['query', 'present', 'absent'],
+ type='str'
+ ),
+ follow=dict(required=False, type='bool', default=True),
+ default=dict(required=False, type='bool', default=False),
+ recursive=dict(required=False, type='bool', default=False),
),
supports_check_mode=True,
)
@@ -226,79 +247,75 @@ def main():
state = module.params.get('state')
follow = module.params.get('follow')
default = module.params.get('default')
-
- if permissions:
- permissions = normalize_permissions(permissions)
+ recursive = module.params.get('recursive')
if not os.path.exists(path):
- module.fail_json(msg="path not found or not accessible!")
+ module.fail_json(msg="Path not found or not accessible.")
- if state in ['present','absent']:
- if not entry and not etype:
- module.fail_json(msg="%s requires either etype and permissions or just entry be set" % state)
+ if state == 'query' and recursive:
+ module.fail_json(msg="'recursive' MUST NOT be set when 'state=query'.")
+
+ if not entry:
+ if state == 'absent' and permissions:
+ module.fail_json(msg="'permissions' MUST NOT be set when 'state=absent'.")
+
+ if state == 'absent' and not entity:
+ module.fail_json(msg="'entity' MUST be set when 'state=absent'.")
+
+ if state in ['present', 'absent'] and not etype:
+ module.fail_json(msg="'etype' MUST be set when 'state=%s'." % state)
if entry:
if etype or entity or permissions:
- module.fail_json(msg="entry and another incompatible field (entity, etype or permissions) are also set")
- if entry.count(":") not in [2,3]:
- module.fail_json(msg="Invalid entry: '%s', it requires 3 or 4 sections divided by ':'" % entry)
+ module.fail_json(msg="'entry' MUST NOT be set when 'entity', 'etype' or 'permissions' are set.")
- default, etype, entity, permissions = split_entry(entry)
+ if state == 'present' and entry.count(":") != 2:
+ module.fail_json(msg="'entry' MUST have 3 sections divided by ':' when 'state=present'.")
- changed=False
+ if state == 'absent' and entry.count(":") != 1:
+ module.fail_json(msg="'entry' MUST have 2 sections divided by ':' when 'state=absent'.")
+
+ if state == 'query':
+ module.fail_json(msg="'entry' MUST NOT be set when 'state=query'.")
+
+ etype, entity, permissions = split_entry(entry)
+
+ changed = False
msg = ""
- currentacls = get_acls(module,path,follow)
- if (state == 'present'):
- matched = False
- for oldentry in currentacls:
- if oldentry.count(":") == 0:
- continue
- old_default, old_type, old_entity, old_permissions = split_entry(oldentry)
- if old_default == default:
- if old_type == etype:
- if etype in ['user', 'group']:
- if old_entity == entity:
- matched = True
- if not old_permissions == permissions:
- changed = True
- break
- else:
- matched = True
- if not old_permissions == permissions:
- changed = True
- break
- if not matched:
- changed=True
+ if state == 'present':
+ entry = build_entry(etype, entity, permissions)
+ command = build_command(
+ module, 'set', path, follow,
+ default, recursive, entry
+ )
+ changed = acl_changed(module, command)
if changed and not module.check_mode:
- set_acl(module,path,':'.join([etype, str(entity), permissions]),follow,default)
- msg="%s is present" % ':'.join([etype, str(entity), permissions])
+ run_acl(module, command)
+ msg = "%s is present" % entry
elif state == 'absent':
- for oldentry in currentacls:
- if oldentry.count(":") == 0:
- continue
- old_default, old_type, old_entity, old_permissions = split_entry(oldentry)
- if old_default == default:
- if old_type == etype:
- if etype in ['user', 'group']:
- if old_entity == entity:
- changed=True
- break
- else:
- changed=True
- break
+ entry = build_entry(etype, entity)
+ command = build_command(
+ module, 'rm', path, follow,
+ default, recursive, entry
+ )
+ changed = acl_changed(module, command)
+
if changed and not module.check_mode:
- rm_acl(module,path,':'.join([etype, entity, '---']),follow,default)
- msg="%s is absent" % ':'.join([etype, entity, '---'])
- else:
- msg="current acl"
+ run_acl(module, command, False)
+ msg = "%s is absent" % entry
- if changed:
- currentacls = get_acls(module,path,follow)
+ elif state == 'query':
+ msg = "current acl"
- module.exit_json(changed=changed, msg=msg, acl=currentacls)
+ acl = run_acl(
+ module,
+ build_command(module, 'get', path, follow, default, recursive)
+ )
+
+ module.exit_json(changed=changed, msg=msg, acl=acl)
# import module snippets
from ansible.module_utils.basic import *
diff --git a/files/assemble.py b/files/assemble.py
index 1f9a952d04a..a996fe44084 100644
--- a/files/assemble.py
+++ b/files/assemble.py
@@ -79,8 +79,23 @@ options:
U(http://docs.python.org/2/library/re.html).
required: false
default: null
+ ignore_hidden:
+ description:
+ - A boolean that controls if files that start with a '.' will be included or not.
+ required: false
+ default: false
+ version_added: "2.0"
+ validate:
+ description:
+ - The validation command to run before copying into place. The path to the file to
+ validate is passed in via '%s' which must be present as in the sshd example below.
+ The command is passed securely so shell features like expansion and pipes won't work.
+ required: false
+ default: null
+ version_added: "2.0"
author: "Stephen Fromm (@sfromm)"
-extends_documentation_fragment: files
+extends_documentation_fragment:
+ - files
'''
EXAMPLES = '''
@@ -89,12 +104,15 @@ EXAMPLES = '''
# When a delimiter is specified, it will be inserted in between each fragment
- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf delimiter='### START FRAGMENT ###'
+
+# Copy a new "sshd_config" file into place, after passing validation with sshd
+- assemble: src=/etc/ssh/conf.d/ dest=/etc/ssh/sshd_config validate='/usr/sbin/sshd -t -f %s'
'''
# ===========================================
# Support method
-def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None):
+def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, ignore_hidden=False):
''' assemble a file from a directory of fragments '''
tmpfd, temp_path = tempfile.mkstemp()
tmp = os.fdopen(tmpfd,'w')
@@ -105,7 +123,7 @@ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None):
if compiled_regexp and not compiled_regexp.search(f):
continue
fragment = "%s/%s" % (src_path, f)
- if not os.path.isfile(fragment):
+ if not os.path.isfile(fragment) or (ignore_hidden and os.path.basename(fragment).startswith('.')):
continue
fragment_content = file(fragment).read()
@@ -148,6 +166,8 @@ def main():
backup=dict(default=False, type='bool'),
remote_src=dict(default=False, type='bool'),
regexp = dict(required=False),
+ ignore_hidden = dict(default=False, type='bool'),
+ validate = dict(required=False, type='str'),
),
add_file_common_args=True
)
@@ -162,6 +182,8 @@ def main():
delimiter = module.params['delimiter']
regexp = module.params['regexp']
compiled_regexp = None
+ ignore_hidden = module.params['ignore_hidden']
+ validate = module.params.get('validate', None)
if not os.path.exists(src):
module.fail_json(msg="Source (%s) does not exist" % src)
@@ -175,7 +197,7 @@ def main():
except re.error, e:
module.fail_json(msg="Invalid Regexp (%s) in \"%s\"" % (e, regexp))
- path = assemble_from_fragments(src, delimiter, compiled_regexp)
+ path = assemble_from_fragments(src, delimiter, compiled_regexp, ignore_hidden)
path_hash = module.sha1(path)
if os.path.exists(dest):
@@ -184,6 +206,13 @@ def main():
if path_hash != dest_hash:
if backup and dest_hash is not None:
module.backup_local(dest)
+ if validate:
+ if "%s" not in validate:
+ module.fail_json(msg="validate must contain %%s: %s" % validate)
+ (rc, out, err) = module.run_command(validate % path)
+ if rc != 0:
+ module.fail_json(msg="failed to validate: rc:%s error:%s" % (rc, err))
+
shutil.copy(path, dest)
changed = True
diff --git a/files/copy.py b/files/copy.py
index b7f333cead6..8f6d3d32f28 100644
--- a/files/copy.py
+++ b/files/copy.py
@@ -63,21 +63,13 @@ options:
force:
description:
- the default is C(yes), which will replace the remote file when contents
- are different than the source. If C(no), the file will only be transferred
+ are different than the source. If C(no), the file will only be transferred
if the destination does not exist.
version_added: "1.1"
required: false
choices: [ "yes", "no" ]
default: "yes"
aliases: [ "thirsty" ]
- validate:
- description:
- - The validation command to run before copying into place. The path to the file to
- validate is passed in via '%s' which must be present as in the visudo example below.
- The command is passed securely so shell features like expansion and pipes won't work.
- required: false
- default: ""
- version_added: "1.2"
directory_mode:
description:
- When doing a recursive copy set the mode for the directories. If this is not set we will use the system
@@ -85,8 +77,10 @@ options:
already existed.
required: false
version_added: "1.5"
-extends_documentation_fragment: files
-author:
+extends_documentation_fragment:
+ - files
+ - validate
+author:
- "Ansible Core Team"
- "Michael DeHaan"
notes:
@@ -168,7 +162,7 @@ size:
type: int
sample: 1220
state:
- description: permissions of the target, after execution
+ description: state of the target, after execution
returned: success
type: string
sample: "file"
@@ -226,6 +220,7 @@ def main():
original_basename = module.params.get('original_basename',None)
validate = module.params.get('validate',None)
follow = module.params['follow']
+ mode = module.params['mode']
if not os.path.exists(src):
module.fail_json(msg="Source %s failed to transfer" % (src))
@@ -295,6 +290,11 @@ def main():
os.unlink(dest)
open(dest, 'w').close()
if validate:
+ # if we have a mode, make sure we set it on the temporary
+ # file source as some validations may require it
+ # FIXME: should we do the same for owner/group here too?
+ if mode is not None:
+ module.set_mode_if_different(src, mode, False)
if "%s" not in validate:
module.fail_json(msg="validate must contain %%s: %s" % (validate))
(rc,out,err) = module.run_command(validate % src)
diff --git a/files/fetch.py b/files/fetch.py
index b8234374976..d0b1371c306 100644
--- a/files/fetch.py
+++ b/files/fetch.py
@@ -1,5 +1,20 @@
# this is a virtual module that is entirely implemented server side
+# 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 .
+
DOCUMENTATION = '''
---
module: fetch
diff --git a/files/file.py b/files/file.py
index 55d3665028e..c3267f7f18b 100644
--- a/files/file.py
+++ b/files/file.py
@@ -18,6 +18,7 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see .
+import errno
import shutil
import stat
import grp
@@ -270,20 +271,30 @@ def main():
module.exit_json(changed=True)
changed = True
curpath = ''
- # Split the path so we can apply filesystem attributes recursively
- # from the root (/) directory for absolute paths or the base path
- # of a relative path. We can then walk the appropriate directory
- # path to apply attributes.
- for dirname in path.strip('/').split('/'):
- curpath = '/'.join([curpath, dirname])
- # Remove leading slash if we're creating a relative path
- if not os.path.isabs(path):
- curpath = curpath.lstrip('/')
- if not os.path.exists(curpath):
- os.mkdir(curpath)
- tmp_file_args = file_args.copy()
- tmp_file_args['path']=curpath
- changed = module.set_fs_attributes_if_different(tmp_file_args, changed)
+
+ try:
+ # Split the path so we can apply filesystem attributes recursively
+ # from the root (/) directory for absolute paths or the base path
+ # of a relative path. We can then walk the appropriate directory
+ # path to apply attributes.
+ for dirname in path.strip('/').split('/'):
+ curpath = '/'.join([curpath, dirname])
+ # Remove leading slash if we're creating a relative path
+ if not os.path.isabs(path):
+ curpath = curpath.lstrip('/')
+ if not os.path.exists(curpath):
+ try:
+ os.mkdir(curpath)
+ except OSError, ex:
+ # Possibly something else created the dir since the os.path.exists
+ # check above. As long as it's a dir, we don't need to error out.
+ if not (ex.errno == errno.EEXISTS and os.isdir(curpath)):
+ raise
+ tmp_file_args = file_args.copy()
+ tmp_file_args['path']=curpath
+ changed = module.set_fs_attributes_if_different(tmp_file_args, changed)
+ except Exception, e:
+ module.fail_json(path=path, msg='There was an issue creating %s as requested: %s' % (curpath, str(e)))
# We already know prev_state is not 'absent', therefore it exists in some form.
elif prev_state != 'directory':
diff --git a/files/ini_file.py b/files/ini_file.py
index 9242821ae9e..fff153af6ad 100644
--- a/files/ini_file.py
+++ b/files/ini_file.py
@@ -120,6 +120,9 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
if cp.get(section, option):
cp.remove_option(section, option)
changed = True
+ except ConfigParser.InterpolationError:
+ cp.remove_option(section, option)
+ changed = True
except:
pass
@@ -143,6 +146,9 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese
except ConfigParser.NoOptionError:
cp.set(section, option, value)
changed = True
+ except ConfigParser.InterpolationError:
+ cp.set(section, option, value)
+ changed = True
if changed and not module.check_mode:
if backup:
diff --git a/files/lineinfile.py b/files/lineinfile.py
index fafb8470b50..45dd12ec135 100644
--- a/files/lineinfile.py
+++ b/files/lineinfile.py
@@ -27,10 +27,12 @@ import tempfile
DOCUMENTATION = """
---
module: lineinfile
-author:
+author:
- "Daniel Hokka Zakrissoni (@dhozac)"
- "Ahti Kitsik (@ahtik)"
-extends_documentation_fragment: files
+extends_documentation_fragment:
+ - files
+ - validate
short_description: Ensure a particular line is in a file, or replace an
existing line using a back-referenced regular expression.
description:
@@ -116,16 +118,6 @@ options:
description:
- Create a backup file including the timestamp information so you can
get the original file back if you somehow clobbered it incorrectly.
- validate:
- required: false
- description:
- - validation to run before copying into place.
- Use %s in the command to indicate the current file to validate.
- The command is passed securely so shell features like
- expansion and pipes won't work.
- required: false
- default: None
- version_added: "1.4"
others:
description:
- All arguments accepted by the M(file) module also work here.
@@ -245,8 +237,11 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create,
# Don't do backref expansion if not asked.
new_line = line
- if lines[index[0]] != new_line + os.linesep:
- lines[index[0]] = new_line + os.linesep
+ if not new_line.endswith(os.linesep):
+ new_line += os.linesep
+
+ if lines[index[0]] != new_line:
+ lines[index[0]] = new_line
msg = 'line replaced'
changed = True
elif backrefs:
diff --git a/files/replace.py b/files/replace.py
index fa0142823ea..765f60f5c8f 100644
--- a/files/replace.py
+++ b/files/replace.py
@@ -26,7 +26,9 @@ DOCUMENTATION = """
---
module: replace
author: "Evan Kaufman (@EvanK)"
-extends_documentation_fragment: files
+extends_documentation_fragment:
+ - files
+ - validate
short_description: Replace all instances of a particular string in a
file using a back-referenced regular expression.
description:
@@ -61,12 +63,6 @@ options:
description:
- Create a backup file including the timestamp information so you can
get the original file back if you somehow clobbered it incorrectly.
- validate:
- required: false
- description:
- - validation to run before copying into place
- required: false
- default: None
others:
description:
- All arguments accepted by the M(file) module also work here.
diff --git a/files/stat.py b/files/stat.py
index 5f79874d9fd..2e088fc8dbd 100644
--- a/files/stat.py
+++ b/files/stat.py
@@ -58,6 +58,23 @@ EXAMPLES = '''
- fail: msg="Whoops! file ownership has changed"
when: st.stat.pw_name != 'root'
+# Determine if a path exists and is a symlink. Note that if the path does
+# not exist, and we test sym.stat.islnk, it will fail with an error. So
+# therefore, we must test whether it is defined.
+# Run this to understand the structure, the skipped ones do not pass the
+# check performed by 'when'
+- stat: path=/path/to/something
+ register: sym
+- debug: msg="islnk isn't defined (path doesn't exist)"
+ when: sym.stat.islnk is not defined
+- debug: msg="islnk is defined (path must exist)"
+ when: sym.stat.islnk is defined
+- debug: msg="Path exists and is a symlink"
+ when: sym.stat.islnk is defined and sym.stat.islnk
+- debug: msg="Path exists and isn't a symlink"
+ when: sym.stat.islnk is defined and sym.stat.islnk == False
+
+
# Determine if a path exists and is a directory. Note that we need to test
# both that p.stat.isdir actually exists, and also that it's set to true.
- stat: path=/path/to/something
diff --git a/files/synchronize.py b/files/synchronize.py
index abad5ad359f..1b9d4326fb5 100644
--- a/files/synchronize.py
+++ b/files/synchronize.py
@@ -34,8 +34,8 @@ options:
required: true
dest_port:
description:
- - Port number for ssh on the destination host. The ansible_ssh_port inventory var takes precedence over this value.
- default: 22
+ - Port number for ssh on the destination host. Prior to ansible 2.0, the ansible_ssh_port inventory var took precedence over this value.
+ default: Value of ansible_ssh_port for this host, remote_port config setting, or 22 if none of those are set
version_added: "1.5"
mode:
description:
@@ -158,6 +158,12 @@ options:
default: no
required: false
version_added: "2.0"
+ verify_host:
+ description:
+ - Verify destination host key.
+ default: no
+ required: false
+ version_added: "2.0"
notes:
- rsync must be installed on both the local and remote machine.
- Inspect the verbose output to validate the destination user/host/path
@@ -227,6 +233,7 @@ def main():
delete = dict(default='no', type='bool'),
private_key = dict(default=None),
rsync_path = dict(default=None),
+ _local_rsync_path = dict(default='rsync', type='path'),
archive = dict(default='yes', type='bool'),
checksum = dict(default='no', type='bool'),
compress = dict(default='yes', type='bool'),
@@ -244,6 +251,8 @@ def main():
rsync_opts = dict(type='list'),
ssh_args = dict(type='str'),
partial = dict(default='no', type='bool'),
+ verify_host = dict(default='no', type='bool'),
+ mode = dict(default='push', choices=['push', 'pull']),
),
supports_check_mode = True
)
@@ -254,7 +263,7 @@ def main():
delete = module.params['delete']
private_key = module.params['private_key']
rsync_path = module.params['rsync_path']
- rsync = module.params.get('local_rsync_path', 'rsync')
+ rsync = module.params.get('_local_rsync_path', 'rsync')
rsync_timeout = module.params.get('rsync_timeout', 'rsync_timeout')
archive = module.params['archive']
checksum = module.params['checksum']
@@ -272,6 +281,7 @@ def main():
group = module.params['group']
rsync_opts = module.params['rsync_opts']
ssh_args = module.params['ssh_args']
+ verify_host = module.params['verify_host']
cmd = '%s --delay-updates -F' % rsync
if compress:
@@ -324,10 +334,13 @@ def main():
else:
private_key = '-i '+ private_key
+ ssh_opts = '-S none'
+
+ if not verify_host:
+ ssh_opts = '%s -o StrictHostKeyChecking=no' % ssh_opts
+
if ssh_args:
- ssh_opts = '-S none -o StrictHostKeyChecking=no %s' % ssh_args
- else:
- ssh_opts = '-S none -o StrictHostKeyChecking=no'
+ ssh_opts = '%s %s' % (ssh_opts, ssh_args)
if dest_port != 22:
cmd += " --rsh 'ssh %s %s -o Port=%s'" % (private_key, ssh_opts, dest_port)
diff --git a/files/template.py b/files/template.py
index 2feb599abdf..808aa13b4ca 100644
--- a/files/template.py
+++ b/files/template.py
@@ -1,5 +1,20 @@
# this is a virtual module that is entirely implemented server side
+# 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 .
+
DOCUMENTATION = '''
---
module: template
@@ -24,13 +39,10 @@ options:
description:
- Path of a Jinja2 formatted template on the local server. This can be a relative or absolute path.
required: true
- default: null
- aliases: []
dest:
description:
- Location to render the template to on the remote machine.
required: true
- default: null
backup:
description:
- Create a backup file including the timestamp information so you can get
@@ -38,22 +50,22 @@ options:
required: false
choices: [ "yes", "no" ]
default: "no"
- validate:
+ force:
description:
- - The validation command to run before copying into place.
- - The path to the file to validate is passed in via '%s' which must be present as in the visudo example below.
- - validation to run before copying into place. The command is passed
- securely so shell features like expansion and pipes won't work.
+ - the default is C(yes), which will replace the remote file when contents
+ are different than the source. If C(no), the file will only be transferred
+ if the destination does not exist.
required: false
- default: ""
- version_added: "1.2"
+ choices: [ "yes", "no" ]
+ default: "yes"
notes:
- "Since Ansible version 0.9, templates are loaded with C(trim_blocks=True)."
-requirements: []
author:
- - Ansible Core Team
+ - Ansible Core Team
- Michael DeHaan
-extends_documentation_fragment: files
+extends_documentation_fragment:
+ - files
+ - validate
'''
EXAMPLES = '''
diff --git a/files/unarchive.py b/files/unarchive.py
index 8053991b63d..2b373a8e7fb 100644
--- a/files/unarchive.py
+++ b/files/unarchive.py
@@ -83,7 +83,7 @@ EXAMPLES = '''
# Unarchive a file that is already on the remote machine
- unarchive: src=/tmp/foo.zip dest=/usr/local/bin copy=no
-# Unarchive a file that needs to be downloaded
+# Unarchive a file that needs to be downloaded (added in 2.0)
- unarchive: src=https://example.com/example.zip dest=/usr/local/bin copy=no
'''
@@ -300,6 +300,13 @@ def main():
if not os.access(src, os.R_OK):
module.fail_json(msg="Source '%s' not readable" % src)
+ # skip working with 0 size archives
+ try:
+ if os.path.getsize(src) == 0:
+ module.fail_json(msg="Invalid archive '%s', the file is 0 bytes" % src)
+ except Exception, e:
+ module.fail_json(msg="Source '%s' not readable" % src)
+
# is dest OK to receive tar file?
if not os.path.isdir(dest):
module.fail_json(msg="Destination '%s' is not a directory" % dest)
diff --git a/inventory/add_host.py b/inventory/add_host.py
index 2ab76b4c16a..ef01ed1051b 100644
--- a/inventory/add_host.py
+++ b/inventory/add_host.py
@@ -1,5 +1,20 @@
# -*- mode: python -*-
+# 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 .
+
DOCUMENTATION = '''
---
module: add_host
diff --git a/inventory/group_by.py b/inventory/group_by.py
index f63bdf5912b..4bfd20206be 100644
--- a/inventory/group_by.py
+++ b/inventory/group_by.py
@@ -1,5 +1,20 @@
# -*- mode: python -*-
+# 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 .
+
DOCUMENTATION = '''
---
module: group_by
diff --git a/network/basics/get_url.py b/network/basics/get_url.py
index 64cd24b6d09..fad0d58f878 100644
--- a/network/basics/get_url.py
+++ b/network/basics/get_url.py
@@ -46,8 +46,6 @@ options:
description:
- HTTP, HTTPS, or FTP URL in the form (http|https|ftp)://[user[:pass]]@host.domain[:port]/path
required: true
- default: null
- aliases: []
dest:
description:
- absolute path of where to download the file to.
@@ -57,7 +55,6 @@ options:
If C(dest) is a directory, the file will always be
downloaded (regardless of the force option), but replaced only if the contents changed.
required: true
- default: null
force:
description:
- If C(yes) and C(dest) is not a directory, will download the file every
@@ -75,9 +72,22 @@ options:
- If a SHA-256 checksum is passed to this parameter, the digest of the
destination file will be calculated after it is downloaded to ensure
its integrity and verify that the transfer completed successfully.
+ This option is deprecated. Use 'checksum'.
version_added: "1.3"
required: false
default: null
+ checksum:
+ description:
+ - 'If a checksum is passed to this parameter, the digest of the
+ destination file will be calculated after it is downloaded to ensure
+ its integrity and verify that the transfer completed successfully.
+ Format: :, e.g.: checksum="sha256:D98291AC[...]B6DC7B97"
+ If you worry about portability, only the sha1 algorithm is available
+ on all platforms and python versions. The third party hashlib
+ library can be installed for access to additional algorithms.'
+ version_added: "2.0"
+ required: false
+ default: null
use_proxy:
description:
- if C(no), it will not use a proxy, even if one is defined in
@@ -98,6 +108,12 @@ options:
required: false
default: 10
version_added: '1.8'
+ headers:
+ description:
+ - 'Add custom HTTP headers to a request in the format "key:value,key:value"'
+ required: false
+ default: null
+ version_added: '2.0'
url_username:
description:
- The username for use in HTTP basic authentication. This parameter can be used
@@ -106,10 +122,20 @@ options:
version_added: '1.6'
url_password:
description:
- - The password for use in HTTP basic authentication. If the C(url_username)
- parameter is not specified, the C(url_password) parameter will not be used.
+ - The password for use in HTTP basic authentication. If the C(url_username)
+ parameter is not specified, the C(url_password) parameter will not be used.
required: false
version_added: '1.6'
+ force_basic_auth:
+ version_added: '2.0'
+ description:
+ - httplib2, the library used by the uri module only sends authentication information when a webservice
+ responds to an initial request with a 401 status. Since some basic auth services do not properly
+ send a 401, logins will fail. This option forces the sending of the Basic authentication header
+ upon initial request.
+ required: false
+ choices: [ "yes", "no" ]
+ default: "no"
others:
description:
- all arguments accepted by the M(file) module also work here
@@ -123,18 +149,19 @@ EXAMPLES='''
- name: download foo.conf
get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf mode=0440
-- name: download file with sha256 check
- get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf sha256sum=b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
+- name: download file and force basic auth
+ get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf force_basic_auth=yes
+
+- name: download file with custom HTTP headers
+ get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf headers='key:value,key:value'
+
+- name: download file with check
+ get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf checksum=sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c
+ get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf checksum=md5:66dffb5228a211e61d6d7ef4a86f5758
'''
import urlparse
-try:
- import hashlib
- HAS_HASHLIB=True
-except ImportError:
- HAS_HASHLIB=False
-
# ==============================================================
# url handling
@@ -144,14 +171,14 @@ def url_filename(url):
return 'index.html'
return fn
-def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10):
+def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, headers=None):
"""
Download data from the url and store in a temporary file.
Return (tempfile, info about the request)
"""
- rsp, info = fetch_url(module, url, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout)
+ rsp, info = fetch_url(module, url, use_proxy=use_proxy, force=force, last_mod_time=last_mod_time, timeout=timeout, headers=headers)
if info['status'] == 304:
module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', ''))
@@ -190,6 +217,7 @@ def extract_filename_from_headers(headers):
return res
+
# ==============================================================
# main
@@ -200,7 +228,9 @@ def main():
url = dict(required=True),
dest = dict(required=True),
sha256sum = dict(default=''),
+ checksum = dict(default=''),
timeout = dict(required=False, type='int', default=10),
+ headers = dict(required=False, default=None),
)
module = AnsibleModule(
@@ -213,14 +243,54 @@ def main():
dest = os.path.expanduser(module.params['dest'])
force = module.params['force']
sha256sum = module.params['sha256sum']
+ checksum = module.params['checksum']
use_proxy = module.params['use_proxy']
timeout = module.params['timeout']
+
+ # Parse headers to dict
+ if module.params['headers']:
+ try:
+ headers = dict(item.split(':') for item in module.params['headers'].split(','))
+ except:
+ module.fail_json(msg="The header parameter requires a key:value,key:value syntax to be properly parsed.")
+ else:
+ headers = None
dest_is_dir = os.path.isdir(dest)
last_mod_time = None
+ # workaround for usage of deprecated sha256sum parameter
+ if sha256sum != '':
+ checksum = 'sha256:%s' % (sha256sum)
+
+ # checksum specified, parse for algorithm and checksum
+ if checksum != '':
+ try:
+ algorithm, checksum = checksum.rsplit(':', 1)
+ # Remove any non-alphanumeric characters, including the infamous
+ # Unicode zero-width space
+ checksum = re.sub(r'\W+', '', checksum).lower()
+ # Ensure the checksum portion is a hexdigest
+ int(checksum, 16)
+ except ValueError:
+ module.fail_json(msg="The checksum parameter has to be in format :")
+
+
if not dest_is_dir and os.path.exists(dest):
- if not force:
+ checksum_mismatch = False
+
+ # If the download is not forced and there is a checksum, allow
+ # checksum match to skip the download.
+ if not force and checksum != '':
+ destination_checksum = module.digest_from_file(dest, algorithm)
+
+ if checksum == destination_checksum:
+ module.exit_json(msg="file already exists", dest=dest, url=url, changed=False)
+
+ checksum_mismatch = True
+
+ # Not forcing redownload, unless checksum does not match
+ if not force and not checksum_mismatch:
module.exit_json(msg="file already exists", dest=dest, url=url, changed=False)
# If the file already exists, prepare the last modified time for the
@@ -229,7 +299,7 @@ def main():
last_mod_time = datetime.datetime.utcfromtimestamp(mtime)
# download to tmpsrc
- tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout)
+ tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout, headers)
# Now the request has completed, we can finally generate the final
# destination file name from the info dict.
@@ -280,22 +350,12 @@ def main():
else:
changed = False
- # Check the digest of the destination file and ensure that it matches the
- # sha256sum parameter if it is present
- if sha256sum != '':
- # Remove any non-alphanumeric characters, including the infamous
- # Unicode zero-width space
- stripped_sha256sum = re.sub(r'\W+', '', sha256sum)
+ if checksum != '':
+ destination_checksum = module.digest_from_file(dest, algorithm)
- if not HAS_HASHLIB:
+ if checksum != destination_checksum:
os.remove(dest)
- module.fail_json(msg="The sha256sum parameter requires hashlib, which is available in Python 2.5 and higher")
- else:
- destination_checksum = module.sha256(dest)
-
- if stripped_sha256sum.lower() != destination_checksum:
- os.remove(dest)
- module.fail_json(msg="The SHA-256 checksum for %s did not match %s; it was %s." % (dest, sha256sum, destination_checksum))
+ module.fail_json(msg="The checksum for %s did not match %s; it was %s." % (dest, checksum, destination_checksum))
os.remove(tmpsrc)
@@ -312,9 +372,8 @@ def main():
md5sum = None
# Mission complete
-
- module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum, checksum=checksum_src,
- sha256sum=sha256sum, changed=changed, msg=info.get('msg', ''))
+ module.exit_json(url=url, dest=dest, src=tmpsrc, md5sum=md5sum, checksum_src=checksum_src,
+ checksum_dest=checksum_dest, changed=changed, msg=info.get('msg', ''))
# import module snippets
from ansible.module_utils.basic import *
diff --git a/network/basics/uri.py b/network/basics/uri.py
index 3de17c12d60..3babba6d609 100644
--- a/network/basics/uri.py
+++ b/network/basics/uri.py
@@ -71,11 +71,12 @@ options:
required: false
choices: [ "raw", "json" ]
default: raw
+ version_added: "2.0"
method:
description:
- The HTTP method of the request or response.
required: false
- choices: [ "GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "PATCH" ]
+ choices: [ "GET", "POST", "PUT", "HEAD", "DELETE", "OPTIONS", "PATCH", "TRACE", "CONNECT", "REFRESH" ]
default: "GET"
return_content:
description:
@@ -269,7 +270,7 @@ def url_filename(url):
def uri(module, url, dest, user, password, body, body_format, method, headers, redirects, socket_timeout, validate_certs):
# To debug
- #httplib2.debug = 4
+ #httplib2.debuglevel = 4
# Handle Redirects
if redirects == "all" or redirects == "yes":
@@ -367,7 +368,7 @@ def main():
password = dict(required=False, default=None),
body = dict(required=False, default=None),
body_format = dict(required=False, default='raw', choices=['raw', 'json']),
- method = dict(required=False, default='GET', choices=['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH']),
+ method = dict(required=False, default='GET', choices=['GET', 'POST', 'PUT', 'HEAD', 'DELETE', 'OPTIONS', 'PATCH', 'TRACE', 'CONNECT', 'REFRESH']),
return_content = dict(required=False, default='no', type='bool'),
force_basic_auth = dict(required=False, default='no', type='bool'),
follow_redirects = dict(required=False, default='safe', choices=['all', 'safe', 'none', 'yes', 'no']),
diff --git a/packaging/language/gem.py b/packaging/language/gem.py
index d058193624a..491402d115f 100644
--- a/packaging/language/gem.py
+++ b/packaging/language/gem.py
@@ -84,7 +84,7 @@ options:
- Allow adding build flags for gem compilation
required: false
version_added: "2.0"
-author:
+author:
- "Ansible Core Team"
- "Johan Wiren"
'''
@@ -196,8 +196,11 @@ def install(module):
if module.params['pre_release']:
cmd.append('--pre')
if not module.params['include_doc']:
- cmd.append('--no-rdoc')
- cmd.append('--no-ri')
+ if major and major < 2:
+ cmd.append('--no-rdoc')
+ cmd.append('--no-ri')
+ else:
+ cmd.append('--no-document')
cmd.append(module.params['gem_source'])
if module.params['build_flags']:
cmd.extend([ '--', module.params['build_flags'] ])
diff --git a/packaging/language/pip.py b/packaging/language/pip.py
index b27e136689d..8bbae35038d 100644
--- a/packaging/language/pip.py
+++ b/packaging/language/pip.py
@@ -63,13 +63,21 @@ options:
default: "no"
choices: [ "yes", "no" ]
virtualenv_command:
- version_aded: "1.1"
+ version_added: "1.1"
description:
- The command or a pathname to the command to create the virtual
environment with. For example C(pyvenv), C(virtualenv),
C(virtualenv2), C(~/bin/virtualenv), C(/usr/local/bin/virtualenv).
required: false
default: virtualenv
+ virtualenv_python:
+ version_added: "2.0"
+ description:
+ - The Python executable used for creating the virtual environment.
+ For example C(python3.4), C(python2.7). When not specified, the
+ system Python version is used.
+ required: false
+ default: null
state:
description:
- The state of module
@@ -147,7 +155,7 @@ def _get_cmd_options(module, cmd):
words = stdout.strip().split()
cmd_options = [ x for x in words if x.startswith('--') ]
return cmd_options
-
+
def _get_full_name(name, version=None):
if version is None:
@@ -228,6 +236,7 @@ def main():
virtualenv=dict(default=None, required=False),
virtualenv_site_packages=dict(default='no', type='bool'),
virtualenv_command=dict(default='virtualenv', required=False),
+ virtualenv_python=dict(default=None, required=False, type='str'),
use_mirrors=dict(default='yes', type='bool'),
extra_args=dict(default=None, required=False),
chdir=dict(default=None, required=False),
@@ -243,6 +252,7 @@ def main():
version = module.params['version']
requirements = module.params['requirements']
extra_args = module.params['extra_args']
+ virtualenv_python = module.params['virtualenv_python']
chdir = module.params['chdir']
if state == 'latest' and version is not None:
@@ -260,18 +270,21 @@ def main():
if module.check_mode:
module.exit_json(changed=True)
- virtualenv = os.path.expanduser(virtualenv_command)
- if os.path.basename(virtualenv) == virtualenv:
- virtualenv = module.get_bin_path(virtualenv_command, True)
+ cmd = os.path.expanduser(virtualenv_command)
+ if os.path.basename(cmd) == cmd:
+ cmd = module.get_bin_path(virtualenv_command, True)
if module.params['virtualenv_site_packages']:
- cmd = '%s --system-site-packages %s' % (virtualenv, env)
+ cmd += ' --system-site-packages'
else:
- cmd_opts = _get_cmd_options(module, virtualenv)
+ cmd_opts = _get_cmd_options(module, cmd)
if '--no-site-packages' in cmd_opts:
- cmd = '%s --no-site-packages %s' % (virtualenv, env)
- else:
- cmd = '%s %s' % (virtualenv, env)
+ cmd += ' --no-site-packages'
+
+ if virtualenv_python:
+ cmd += ' -p%s' % virtualenv_python
+
+ cmd = "%s %s" % (cmd, env)
this_dir = tempfile.gettempdir()
if chdir:
this_dir = os.path.join(this_dir, chdir)
@@ -286,14 +299,14 @@ def main():
cmd = '%s %s' % (pip, state_map[state])
# If there's a virtualenv we want things we install to be able to use other
- # installations that exist as binaries within this virtualenv. Example: we
- # install cython and then gevent -- gevent needs to use the cython binary,
- # not just a python package that will be found by calling the right python.
+ # installations that exist as binaries within this virtualenv. Example: we
+ # install cython and then gevent -- gevent needs to use the cython binary,
+ # not just a python package that will be found by calling the right python.
# So if there's a virtualenv, we add that bin/ to the beginning of the PATH
# in run_command by setting path_prefix here.
path_prefix = None
if env:
- path_prefix="/".join(pip.split('/')[:-1])
+ path_prefix = "/".join(pip.split('/')[:-1])
# Automatically apply -e option to extra_args when source is a VCS url. VCS
# includes those beginning with svn+, git+, hg+ or bzr+
@@ -320,7 +333,7 @@ def main():
this_dir = os.path.join(this_dir, chdir)
if module.check_mode:
- if env or extra_args or requirements or state == 'latest' or not name:
+ if extra_args or requirements or state == 'latest' or not name:
module.exit_json(changed=True)
elif name.startswith('svn+') or name.startswith('git+') or \
name.startswith('hg+') or name.startswith('bzr+'):
@@ -343,7 +356,8 @@ def main():
rc, out_pip, err_pip = module.run_command(cmd, path_prefix=path_prefix, cwd=this_dir)
out += out_pip
err += err_pip
- if rc == 1 and state == 'absent' and 'not installed' in out_pip:
+ if rc == 1 and state == 'absent' and \
+ ('not installed' in out_pip or 'not installed' in err_pip):
pass # rc is 1 when attempting to uninstall non-installed package
elif rc != 0:
_fail(module, cmd, out, err)
@@ -354,7 +368,8 @@ def main():
changed = 'Successfully installed' in out_pip
module.exit_json(changed=changed, cmd=cmd, name=name, version=version,
- state=state, requirements=requirements, virtualenv=env, stdout=out, stderr=err)
+ state=state, requirements=requirements, virtualenv=env,
+ stdout=out, stderr=err)
# import module snippets
from ansible.module_utils.basic import *
diff --git a/packaging/os/apt.py b/packaging/os/apt.py
old mode 100644
new mode 100755
index 09129a73fa5..1fd770f710e
--- a/packaging/os/apt.py
+++ b/packaging/os/apt.py
@@ -80,8 +80,8 @@ options:
- 'Note: This does not upgrade a specific package, use state=latest for that.'
version_added: "1.1"
required: false
- default: "yes"
- choices: [ "yes", "safe", "full", "dist"]
+ default: "no"
+ choices: [ "no", "yes", "safe", "full", "dist"]
dpkg_options:
description:
- Add dpkg options to apt command. Defaults to '-o "Dpkg::Options::=--force-confdef" -o "Dpkg::Options::=--force-confold"'
@@ -179,8 +179,8 @@ APT_ENV_VARS = dict(
)
DPKG_OPTIONS = 'force-confdef,force-confold'
-APT_GET_ZERO = "0 upgraded, 0 newly installed"
-APTITUDE_ZERO = "0 packages upgraded, 0 newly installed"
+APT_GET_ZERO = "\n0 upgraded, 0 newly installed"
+APTITUDE_ZERO = "\n0 packages upgraded, 0 newly installed"
APT_LISTS_PATH = "/var/lib/apt/lists"
APT_UPDATE_SUCCESS_STAMP_PATH = "/var/lib/apt/periodic/update-success-stamp"
@@ -230,10 +230,10 @@ def package_status(m, pkgname, version, cache, state):
try:
provided_packages = cache.get_providing_packages(pkgname)
if provided_packages:
- is_installed = False
+ is_installed = False
# when virtual package providing only one package, look up status of target package
if cache.is_virtual_package(pkgname) and len(provided_packages) == 1:
- package = provided_packages[0]
+ package = provided_packages[0]
installed, upgradable, has_files = package_status(m, package.name, version, cache, state='install')
if installed:
is_installed = True
@@ -403,19 +403,20 @@ def install_deb(m, debs, cache, force, install_recommends, dpkg_options):
for deb_file in debs.split(','):
try:
pkg = apt.debfile.DebPackage(deb_file)
- except SystemError, e:
- m.fail_json(msg="System Error: %s" % str(e))
- # Check if it's already installed
- if pkg.compare_to_version_in_cache() == pkg.VERSION_SAME:
- continue
- # Check if package is installable
- if not pkg.check() and not force:
- m.fail_json(msg=pkg._failure_string)
+ # Check if it's already installed
+ if pkg.compare_to_version_in_cache() == pkg.VERSION_SAME:
+ continue
+ # Check if package is installable
+ if not pkg.check() and not force:
+ m.fail_json(msg=pkg._failure_string)
- # add any missing deps to the list of deps we need
- # to install so they're all done in one shot
- deps_to_install.extend(pkg.missing_deps)
+ # add any missing deps to the list of deps we need
+ # to install so they're all done in one shot
+ deps_to_install.extend(pkg.missing_deps)
+
+ except Exception, e:
+ m.fail_json(msg="Unable to install package: %s" % str(e))
# and add this deb to the list of packages to install
pkgs_to_install.append(deb_file)
@@ -548,7 +549,7 @@ def main():
default_release = dict(default=None, aliases=['default-release']),
install_recommends = dict(default='yes', aliases=['install-recommends'], type='bool'),
force = dict(default='no', type='bool'),
- upgrade = dict(choices=['yes', 'safe', 'full', 'dist']),
+ upgrade = dict(choices=['no', 'yes', 'safe', 'full', 'dist']),
dpkg_options = dict(default=DPKG_OPTIONS)
),
mutually_exclusive = [['package', 'upgrade', 'deb']],
@@ -572,6 +573,10 @@ def main():
APT_GET_CMD = module.get_bin_path("apt-get")
p = module.params
+
+ if p['upgrade'] == 'no':
+ p['upgrade'] = None
+
if not APTITUDE_CMD and p.get('upgrade', None) in [ 'full', 'safe', 'yes' ]:
module.fail_json(msg="Could not find aptitude. Please ensure it is installed.")
diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py
index eee58f77729..750169325e3 100644
--- a/packaging/os/apt_repository.py
+++ b/packaging/os/apt_repository.py
@@ -124,7 +124,8 @@ class InvalidSource(Exception):
# Simple version of aptsources.sourceslist.SourcesList.
# No advanced logic and no backups inside.
class SourcesList(object):
- def __init__(self):
+ def __init__(self, module):
+ self.module = module
self.files = {} # group sources by file
# Repositories that we're adding -- used to implement mode param
self.new_repos = set()
@@ -234,7 +235,7 @@ class SourcesList(object):
group.append((n, valid, enabled, source, comment))
self.files[file] = group
- def save(self, module):
+ def save(self):
for filename, sources in self.files.items():
if sources:
d, fn = os.path.split(filename)
@@ -255,13 +256,13 @@ class SourcesList(object):
try:
f.write(line)
except IOError, err:
- module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, unicode(err)))
- module.atomic_move(tmp_path, filename)
+ self.module.fail_json(msg="Failed to write to file %s: %s" % (tmp_path, unicode(err)))
+ self.module.atomic_move(tmp_path, filename)
# allow the user to override the default mode
if filename in self.new_repos:
- this_mode = module.params['mode']
- module.set_mode_if_different(filename, this_mode, False)
+ this_mode = self.module.params['mode']
+ self.module.set_mode_if_different(filename, this_mode, False)
else:
del self.files[filename]
if os.path.exists(filename):
@@ -329,7 +330,7 @@ class UbuntuSourcesList(SourcesList):
def __init__(self, module, add_ppa_signing_keys_callback=None):
self.module = module
self.add_ppa_signing_keys_callback = add_ppa_signing_keys_callback
- super(UbuntuSourcesList, self).__init__()
+ super(UbuntuSourcesList, self).__init__(module)
def _get_ppa_info(self, owner_name, ppa_name):
lp_api = self.LP_API % (owner_name, ppa_name)
@@ -359,6 +360,10 @@ class UbuntuSourcesList(SourcesList):
if line.startswith('ppa:'):
source, ppa_owner, ppa_name = self._expand_ppa(line)
+ if source in self.repos_urls:
+ # repository already exists
+ return
+
if self.add_ppa_signing_keys_callback is not None:
info = self._get_ppa_info(ppa_owner, ppa_name)
if not self._key_already_exists(info['signing_key_fingerprint']):
@@ -378,6 +383,25 @@ class UbuntuSourcesList(SourcesList):
source = self._parse(line, raise_if_invalid_or_disabled=True)[2]
self._remove_valid_source(source)
+ @property
+ def repos_urls(self):
+ _repositories = []
+ for parsed_repos in self.files.values():
+ for parsed_repo in parsed_repos:
+ enabled = parsed_repo[1]
+ source_line = parsed_repo[3]
+
+ if not enabled:
+ continue
+
+ if source_line.startswith('ppa:'):
+ source, ppa_owner, ppa_name = self._expand_ppa(source_line)
+ _repositories.append(source)
+ else:
+ _repositories.append(source_line)
+
+ return _repositories
+
def get_add_ppa_signing_key_callback(module):
def _run_command(command):
@@ -404,24 +428,24 @@ def main():
)
params = module.params
- if params['install_python_apt'] and not HAVE_PYTHON_APT and not module.check_mode:
- install_python_apt(module)
-
repo = module.params['repo']
state = module.params['state']
update_cache = module.params['update_cache']
sourceslist = None
- if HAVE_PYTHON_APT:
- if isinstance(distro, aptsources_distro.UbuntuDistribution):
- sourceslist = UbuntuSourcesList(module,
- add_ppa_signing_keys_callback=get_add_ppa_signing_key_callback(module))
- elif HAVE_PYTHON_APT and \
- isinstance(distro, aptsources_distro.DebianDistribution) or isinstance(distro, aptsources_distro.Distribution):
- sourceslist = SourcesList()
+ if not HAVE_PYTHON_APT:
+ if params['install_python_apt']:
+ install_python_apt(module)
+ else:
+ module.fail_json(msg='python-apt is not installed, and install_python_apt is False')
+
+ if isinstance(distro, aptsources_distro.UbuntuDistribution):
+ sourceslist = UbuntuSourcesList(module,
+ add_ppa_signing_keys_callback=get_add_ppa_signing_key_callback(module))
+ elif isinstance(distro, aptsources_distro.Distribution):
+ sourceslist = SourcesList(module)
else:
- module.fail_json(msg='Module apt_repository supports only Debian and Ubuntu. ' + \
- 'You may be seeing this because python-apt is not installed, but you requested that it not be auto-installed')
+ module.fail_json(msg='Module apt_repository supports only Debian and Ubuntu.')
sources_before = sourceslist.dump()
@@ -438,7 +462,7 @@ def main():
if not module.check_mode and changed:
try:
- sourceslist.save(module)
+ sourceslist.save()
if update_cache:
cache = apt.Cache()
cache.update()
diff --git a/packaging/os/package.py b/packaging/os/package.py
index 7c94b98a941..f4234b5a472 100644
--- a/packaging/os/package.py
+++ b/packaging/os/package.py
@@ -23,19 +23,29 @@ DOCUMENTATION = '''
---
module: package
version_added: 2.0
-author: Ansible Core Team
+author:
+ - Ansible Inc
+maintainers:
+ - Ansible Core Team
short_description: Generic OS package manager
description:
- Installs, upgrade and removes packages using the underlying OS package manager.
options:
name:
description:
- - "Package name, or package specifier with version, like C(name-1.0). When using state=latest, this can be '*' which means run: yum -y update. You can also pass a url or a local path to a rpm file. To operate on several packages this can accept a comma separated list of packages or (as of 2.0) a list of packages."
+ - "Package name, or package specifier with version, like C(name-1.0)."
+ - "Be aware that packages are not always named the same and this module will not 'translate' them per distro."
required: true
state:
description:
- Whether to install (C(present), C(latest)), or remove (C(absent)) a package.
required: true
+ use:
+ description:
+ - The required package manager module to use (yum, apt, etc). The default 'auto' will use existing facts or try to autodetect it.
+ - You should only use this field if the automatic selection is not working for some reason.
+ required: false
+ default: auto
requirements:
- Whatever is required for the package plugins specific for each system.
notes:
diff --git a/packaging/os/redhat_subscription.py b/packaging/os/redhat_subscription.py
index 1cfd8fc25a6..8e1482a8c4f 100644
--- a/packaging/os/redhat_subscription.py
+++ b/packaging/os/redhat_subscription.py
@@ -76,6 +76,12 @@ EXAMPLES = '''
- redhat_subscription: state=present
activationkey=1-222333444
pool='^(Red Hat Enterprise Server|Red Hat Virtualization)$'
+
+# Update the consumed subscriptions from the previous example (remove the Red
+# Hat Virtualization subscription)
+- redhat_subscription: state=present
+ activationkey=1-222333444
+ pool='^Red Hat Enterprise Server$'
'''
import os
@@ -180,7 +186,7 @@ class Rhsm(RegistrationBase):
for k,v in kwargs.items():
if re.search(r'^(system|rhsm)_', k):
args.append('--%s=%s' % (k.replace('_','.'), v))
-
+
self.module.run_command(args, check_rc=True)
@property
@@ -226,14 +232,26 @@ class Rhsm(RegistrationBase):
rc, stderr, stdout = self.module.run_command(args, check_rc=True)
- def unsubscribe(self):
+ def unsubscribe(self, serials=None):
'''
- Unsubscribe a system from all subscribed channels
+ Unsubscribe a system from subscribed channels
+ Args:
+ serials(list or None): list of serials to unsubscribe. If
+ serials is none or an empty list, then
+ all subscribed channels will be removed.
Raises:
* Exception - if error occurs while running command
'''
- args = ['subscription-manager', 'unsubscribe', '--all']
- rc, stderr, stdout = self.module.run_command(args, check_rc=True)
+ items = []
+ if serials is not None and serials:
+ items = ["--serial=%s" % s for s in serials]
+ if serials is None:
+ items = ["--all"]
+
+ if items:
+ args = ['subscription-manager', 'unsubscribe'] + items
+ rc, stderr, stdout = self.module.run_command(args, check_rc=True)
+ return serials
def unregister(self):
'''
@@ -255,8 +273,27 @@ class Rhsm(RegistrationBase):
# Available pools ready for subscription
available_pools = RhsmPools(self.module)
+ subscribed_pool_ids = []
for pool in available_pools.filter(regexp):
pool.subscribe()
+ subscribed_pool_ids.append(pool.get_pool_id())
+ return subscribed_pool_ids
+
+ def update_subscriptions(self, regexp):
+ changed=False
+ consumed_pools = RhsmPools(self.module, consumed=True)
+ pool_ids_to_keep = [p.get_pool_id() for p in consumed_pools.filter(regexp)]
+
+ serials_to_remove=[p.Serial for p in consumed_pools if p.get_pool_id() not in pool_ids_to_keep]
+ serials = self.unsubscribe(serials=serials_to_remove)
+
+ subscribed_pool_ids = self.subscribe(regexp)
+
+ if subscribed_pool_ids or serials:
+ changed=True
+ return {'changed': changed, 'subscribed_pool_ids': subscribed_pool_ids,
+ 'unsubscribed_serials': serials}
+
class RhsmPool(object):
@@ -272,8 +309,11 @@ class RhsmPool(object):
def __str__(self):
return str(self.__getattribute__('_name'))
+ def get_pool_id(self):
+ return getattr(self, 'PoolId', getattr(self, 'PoolID'))
+
def subscribe(self):
- args = "subscription-manager subscribe --pool %s" % self.PoolId
+ args = "subscription-manager subscribe --pool %s" % self.get_pool_id()
rc, stdout, stderr = self.module.run_command(args, check_rc=True)
if rc == 0:
return True
@@ -285,18 +325,25 @@ class RhsmPools(object):
"""
This class is used for manipulating pools subscriptions with RHSM
"""
- def __init__(self, module):
+ def __init__(self, module, consumed=False):
self.module = module
- self.products = self._load_product_list()
+ self.products = self._load_product_list(consumed)
def __iter__(self):
return self.products.__iter__()
- def _load_product_list(self):
+ def _load_product_list(self, consumed=False):
"""
- Loads list of all available pools for system in data structure
+ Loads list of all available or consumed pools for system in data structure
+
+ Args:
+ consumed(bool): if True list consumed pools, else list available pools (default False)
"""
- args = "subscription-manager list --available"
+ args = "subscription-manager list"
+ if consumed:
+ args += " --consumed"
+ else:
+ args += " --available"
rc, stdout, stderr = self.module.run_command(args, check_rc=True)
products = []
@@ -375,18 +422,27 @@ def main():
# Register system
if rhn.is_registered:
- module.exit_json(changed=False, msg="System already registered.")
+ if pool != '^$':
+ try:
+ result = rhn.update_subscriptions(pool)
+ except Exception, e:
+ module.fail_json(msg="Failed to update subscriptions for '%s': %s" % (server_hostname, e))
+ else:
+ module.exit_json(**result)
+ else:
+ module.exit_json(changed=False, msg="System already registered.")
else:
try:
rhn.enable()
rhn.configure(**module.params)
rhn.register(username, password, autosubscribe, activationkey, org_id)
- rhn.subscribe(pool)
+ subscribed_pool_ids = rhn.subscribe(pool)
except Exception, e:
module.fail_json(msg="Failed to register with '%s': %s" % (server_hostname, e))
else:
- module.exit_json(changed=True, msg="System successfully registered to '%s'." % server_hostname)
-
+ module.exit_json(changed=True,
+ msg="System successfully registered to '%s'." % server_hostname,
+ subscribed_pool_ids=subscribed_pool_ids)
# Ensure system is *not* registered
if state == 'absent':
if not rhn.is_registered:
diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py
index 1e92405c827..b67b442aa22 100644
--- a/packaging/os/rhn_register.py
+++ b/packaging/os/rhn_register.py
@@ -56,6 +56,12 @@ options:
- supply an activation key for use with registration
required: False
default: null
+ profilename:
+ description:
+ - supply an profilename for use with registration
+ required: False
+ default: null
+ version_added: "2.0"
channels:
description:
- Optionally specify a list of comma-separated channels to subscribe to upon successful registration.
@@ -73,6 +79,9 @@ EXAMPLES = '''
# Register with activationkey (1-222333444) and enable extended update support.
- rhn_register: state=present activationkey=1-222333444 enable_eus=true
+# Register with activationkey (1-222333444) and set a profilename which may differ from the hostname.
+- rhn_register: state=present activationkey=1-222333444 profilename=host.example.com.custom
+
# Register as user (joe_user) with password (somepass) against a satellite
# server specified by (server_url).
- rhn_register: >
@@ -209,7 +218,7 @@ class Rhn(RegistrationBase):
self.update_plugin_conf('rhnplugin', True)
self.update_plugin_conf('subscription-manager', False)
- def register(self, enable_eus=False, activationkey=None):
+ def register(self, enable_eus=False, activationkey=None, profilename=None):
'''
Register system to RHN. If enable_eus=True, extended update
support will be requested.
@@ -221,7 +230,8 @@ class Rhn(RegistrationBase):
register_cmd += " --use-eus-channel"
if activationkey is not None:
register_cmd += " --activationkey '%s'" % activationkey
- # FIXME - support --profilename
+ if profilename is not None:
+ register_cmd += " --profilename '%s'" % profilename
# FIXME - support --systemorgid
rc, stdout, stderr = self.module.run_command(register_cmd, check_rc=True, use_unsafe_shell=True)
@@ -285,6 +295,7 @@ def main():
password = dict(default=None, required=False),
server_url = dict(default=rhn.config.get_option('serverURL'), required=False),
activationkey = dict(default=None, required=False),
+ profilename = dict(default=None, required=False),
enable_eus = dict(default=False, type='bool'),
channels = dict(default=[], type='list'),
)
@@ -295,6 +306,7 @@ def main():
rhn.password = module.params['password']
rhn.configure(module.params['server_url'])
activationkey = module.params['activationkey']
+ profilename = module.params['profilename']
channels = module.params['channels']
rhn.module = module
diff --git a/packaging/os/rpm_key.py b/packaging/os/rpm_key.py
index d2d5e684015..1d2d208e4be 100644
--- a/packaging/os/rpm_key.py
+++ b/packaging/os/rpm_key.py
@@ -141,7 +141,14 @@ class RpmKey:
return ret
def getkeyid(self, keyfile):
- gpg = self.module.get_bin_path('gpg', True)
+
+ gpg = self.module.get_bin_path('gpg')
+ if not gpg:
+ gpg = self.module.get_bin_path('gpg2')
+
+ if not gpg:
+ self.json_fail(msg="rpm_key requires a command line gpg or gpg2, none found")
+
stdout, stderr = self.execute_command([gpg, '--no-tty', '--batch', '--with-colons', '--fixed-list-mode', '--list-packets', keyfile])
for line in stdout.splitlines():
line = line.strip()
diff --git a/packaging/os/yum.py b/packaging/os/yum.py
index 14339b4c18b..c66e73ad98b 100644
--- a/packaging/os/yum.py
+++ b/packaging/os/yum.py
@@ -118,10 +118,22 @@ options:
choices: ["yes", "no"]
aliases: []
-notes: []
+notes:
+ - When used with a loop of package names in a playbook, ansible optimizes
+ the call to the yum module. Instead of calling the module with a single
+ package each time through the loop, ansible calls the module once with all
+ of the package names from the loop.
+ - In versions prior to 1.9.2 this module installed and removed each package
+ given to the yum module separately. This caused problems when packages
+ specified by filename or url had to be installed or removed together. In
+ 1.9.2 this was fixed so that packages are installed in one yum
+ transaction. However, if one of the packages adds a new yum repository
+ that the other packages come from (such as epel-release) then that package
+ needs to be installed in a separate task. This mimics yum's command line
+ behaviour.
# informational: requirements for nodes
requirements: [ yum ]
-author:
+author:
- "Ansible Core Team"
- "Seth Vidal"
'''
@@ -212,7 +224,7 @@ def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, di
for rid in en_repos:
my.repos.enableRepo(rid)
- e,m,u = my.rpmdb.matchPackageNames([pkgspec])
+ e, m, u = my.rpmdb.matchPackageNames([pkgspec])
pkgs = e + m
if not pkgs:
pkgs.extend(my.returnInstalledPackagesByDep(pkgspec))
@@ -224,16 +236,16 @@ def is_installed(module, repoq, pkgspec, conf_file, qf=def_qf, en_repos=None, di
else:
cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, pkgspec]
- rc,out,err = module.run_command(cmd)
+ rc, out, err = module.run_command(cmd)
if not is_pkg:
cmd = repoq + ["--disablerepo=*", "--pkgnarrow=installed", "--qf", qf, "--whatprovides", pkgspec]
- rc2,out2,err2 = module.run_command(cmd)
+ rc2, out2, err2 = module.run_command(cmd)
else:
- rc2,out2,err2 = (0, '', '')
+ rc2, out2, err2 = (0, '', '')
if rc == 0 and rc2 == 0:
out += out2
- return [ p for p in out.split('\n') if p.strip() ]
+ return [p for p in out.split('\n') if p.strip()]
else:
module.fail_json(msg='Error from repoquery: %s: %s' % (cmd, err + err2))
@@ -541,7 +553,7 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
module.fail_json(msg="Failure downloading %s, %s" % (spec, e))
#groups :(
- elif spec.startswith('@'):
+ elif spec.startswith('@'):
# complete wild ass guess b/c it's a group
pkg = spec
@@ -608,7 +620,8 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
shutil.rmtree(tempdir)
except Exception, e:
module.fail_json(msg="Failure deleting temp directory %s, %s" % (tempdir, e))
- module.exit_json(changed=True)
+
+ module.exit_json(changed=True, results=res['results'], changes=dict(installed=pkgs))
changed = True
@@ -676,7 +689,7 @@ def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
cmd = yum_basecmd + ["remove"] + pkgs
if module.check_mode:
- module.exit_json(changed=True)
+ module.exit_json(changed=True, results=res['results'], changes=dict(removed=pkgs))
rc, out, err = module.run_command(cmd)
@@ -711,47 +724,69 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
res['msg'] = ''
res['changed'] = False
res['rc'] = 0
+ pkgs = {}
+ pkgs['update'] = []
+ pkgs['install'] = []
+ updates = {}
+ update_all = False
+ cmd = None
- for spec in items:
+ # determine if we're doing an update all
+ if '*' in items:
+ update_all = True
- pkg = None
- basecmd = 'update'
- cmd = ''
- # groups, again
- if spec.startswith('@'):
- pkg = spec
-
- elif spec == '*': #update all
- # use check-update to see if there is any need
- rc,out,err = module.run_command(yum_basecmd + ['check-update'])
- if rc == 100:
- cmd = yum_basecmd + [basecmd]
- else:
- res['results'].append('All packages up to date')
+ # run check-update to see if we have packages pending
+ rc, out, err = module.run_command(yum_basecmd + ['check-update'])
+ if rc == 0 and update_all:
+ res['results'].append('Nothing to do here, all packages are up to date')
+ return res
+ elif rc == 100:
+ available_updates = out.split('\n')
+ # build update dictionary
+ for line in available_updates:
+ line = line.split()
+ # ignore irrelevant lines
+ # FIXME... revisit for something less kludgy
+ if '*' in line or len(line) != 3 or '.' not in line[0]:
continue
-
- # dep/pkgname - find it
- else:
- if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos):
- basecmd = 'update'
else:
- basecmd = 'install'
+ pkg, version, repo = line
+ name, dist = pkg.rsplit('.', 1)
+ updates.update({name: {'version': version, 'dist': dist, 'repo': repo}})
+ elif rc == 1:
+ res['msg'] = err
+ res['rc'] = rc
+ module.fail_json(**res)
+ if update_all:
+ cmd = yum_basecmd + ['update']
+ else:
+ for spec in items:
+ # some guess work involved with groups. update @ will install the group if missing
+ if spec.startswith('@'):
+ pkgs['update'].append(spec)
+ continue
+ # dep/pkgname - find it
+ else:
+ if is_installed(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos):
+ pkgs['update'].append(spec)
+ else:
+ pkgs['install'].append(spec)
pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos)
+ # FIXME..? may not be desirable to throw an exception here if a single package is missing
if not pkglist:
res['msg'] += "No Package matching '%s' found available, installed or updated" % spec
module.fail_json(**res)
-
+
nothing_to_do = True
for this in pkglist:
- if basecmd == 'install' and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
+ if spec in pkgs['install'] and is_available(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
nothing_to_do = False
break
-
- if basecmd == 'update' and is_update(module, repoq, this, conf_file, en_repos=en_repos, dis_repos=dis_repos):
- nothing_to_do = False
- break
-
+
+ if spec in pkgs['update'] and spec in updates.keys():
+ nothing_to_do = False
+
if nothing_to_do:
res['results'].append("All packages providing %s are up to date" % spec)
continue
@@ -763,27 +798,60 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos):
res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts)
module.fail_json(**res)
- pkg = spec
- if not cmd:
- cmd = yum_basecmd + [basecmd, pkg]
+ # list of package updates
+ if update_all:
+ will_update = updates.keys()
+ else:
+ will_update = [u for u in pkgs['update'] if u in updates.keys() or u.startswith('@')]
- if module.check_mode:
- return module.exit_json(changed=True)
+ # check_mode output
+ if module.check_mode:
+ to_update = []
+ for w in will_update:
+ if w.startswith('@'):
+ to_update.append((w, None))
+ msg = '%s will be updated' % w
+ else:
+ to_update.append((w, '%s.%s from %s' % (updates[w]['version'], updates[w]['dist'], updates[w]['repo'])))
- rc, out, err = module.run_command(cmd)
+ res['changes'] = dict(installed=pkgs['install'], updated=to_update)
- res['rc'] += rc
- res['results'].append(out)
- res['msg'] += err
-
- # FIXME if it is - update it and check to see if it applied
- # check to see if there is no longer an update available for the pkgspec
-
- if rc:
- res['failed'] = True
- else:
+ if len(will_update) > 0 or len(pkgs['install']) > 0:
res['changed'] = True
+ return res
+
+ # run commands
+ if cmd: # update all
+ rc, out, err = module.run_command(cmd)
+ res['changed'] = True
+ else:
+ if len(pkgs['install']) > 0: # install missing
+ cmd = yum_basecmd + ['install'] + pkgs['install']
+ rc, out, err = module.run_command(cmd)
+ res['changed'] = True
+ else:
+ rc, out, err = [0, '', '']
+
+ if len(will_update) > 0: # update present
+ cmd = yum_basecmd + ['update'] + pkgs['update']
+ rc2, out2, err2 = module.run_command(cmd)
+ res['changed'] = True
+ else:
+ rc2, out2, err2 = [0, '', '']
+
+ if not update_all:
+ rc += rc2
+ out += out2
+ err += err2
+
+ res['rc'] += rc
+ res['msg'] += err
+ res['results'].append(out)
+
+ if rc:
+ res['failed'] = True
+
return res
def ensure(module, state, pkgs, conf_file, enablerepo, disablerepo,
@@ -904,10 +972,15 @@ def main():
# loaded and plugins are discovered
my.conf
repoquery = None
- if 'rhnplugin' in my.plugins._plugins:
- repoquerybin = ensure_yum_utils(module)
- if repoquerybin:
- repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet']
+ try:
+ yum_plugins = my.plugins._plugins
+ except AttributeError:
+ pass
+ else:
+ if 'rhnplugin' in yum_plugins:
+ repoquerybin = ensure_yum_utils(module)
+ if repoquerybin:
+ repoquery = [repoquerybin, '--show-duplicates', '--plugins', '--quiet']
pkg = [ p.strip() for p in params['name']]
exclude = params['exclude']
@@ -927,4 +1000,3 @@ from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
if __name__ == '__main__':
main()
-
diff --git a/source_control/git.py b/source_control/git.py
index 369430211f3..4b1620392a0 100644
--- a/source_control/git.py
+++ b/source_control/git.py
@@ -173,7 +173,8 @@ options:
to be installed. The commit MUST be signed and the public key MUST
be trusted in the GPG trustdb.
-
+requirements:
+ - git (the command line tool)
notes:
- "If the task seems to be hanging, first verify remote host is in C(known_hosts).
SSH will prompt user to authorize the first contact with a remote host. To avoid this prompt,
@@ -490,10 +491,20 @@ def get_head_branch(git_path, module, dest, remote, bare=False):
f.close()
return branch
-def fetch(git_path, module, repo, dest, version, remote, bare, refspec):
+def set_remote_url(git_path, module, repo, dest, remote):
''' updates repo from remote sources '''
commands = [("set a new url %s for %s" % (repo, remote), [git_path, 'remote', 'set-url', remote, repo])]
+ for (label,command) in commands:
+ (rc,out,err) = module.run_command(command, cwd=dest)
+ if rc != 0:
+ module.fail_json(msg="Failed to %s: %s %s" % (label, out, err))
+
+def fetch(git_path, module, repo, dest, version, remote, bare, refspec):
+ ''' updates repo from remote sources '''
+ set_remote_url(git_path, module, repo, dest, remote)
+ commands = []
+
fetch_str = 'download remote objects and refs'
if bare:
@@ -740,6 +751,7 @@ def main():
if not module.check_mode:
reset(git_path, module, dest)
# exit if already at desired sha version
+ set_remote_url(git_path, module, repo, dest, remote)
remote_head = get_remote_head(git_path, module, dest, version, remote, bare)
if before == remote_head:
if local_mods:
diff --git a/source_control/hg.py b/source_control/hg.py
index 47b23d26fd5..285bc6f1729 100644
--- a/source_control/hg.py
+++ b/source_control/hg.py
@@ -65,6 +65,13 @@ options:
required: false
default: "no"
choices: [ "yes", "no" ]
+ update:
+ required: false
+ default: "yes"
+ choices: [ "yes", "no" ]
+ version_added: "2.0"
+ description:
+ - If C(no), do not retrieve new revisions from the origin repository
executable:
required: false
default: null
@@ -210,6 +217,7 @@ def main():
revision = dict(default=None, aliases=['version']),
force = dict(default='no', type='bool'),
purge = dict(default='no', type='bool'),
+ update = dict(default='yes', type='bool'),
executable = dict(default=None),
),
)
@@ -218,6 +226,7 @@ def main():
revision = module.params['revision']
force = module.params['force']
purge = module.params['purge']
+ update = module.params['update']
hg_path = module.params['executable'] or module.get_bin_path('hg', True)
hgrc = os.path.join(dest, '.hg/hgrc')
@@ -234,6 +243,9 @@ def main():
(rc, out, err) = hg.clone()
if rc != 0:
module.fail_json(msg=err)
+ elif not update:
+ # Just return having found a repo already in the dest path
+ before = hg.get_revision()
elif hg.at_revision:
# no update needed, don't pull
before = hg.get_revision()
diff --git a/source_control/subversion.py b/source_control/subversion.py
index e3ff6dbfba5..24cc065c5a4 100644
--- a/source_control/subversion.py
+++ b/source_control/subversion.py
@@ -78,6 +78,13 @@ options:
version_added: "1.6"
description:
- If C(yes), do export instead of checkout/update.
+ switch:
+ required: false
+ default: "yes"
+ choices: [ "yes", "no" ]
+ version_added: "2.0"
+ description:
+ - If C(no), do not call svn switch before update.
'''
EXAMPLES = '''
@@ -103,7 +110,8 @@ class Subversion(object):
self.password = password
self.svn_path = svn_path
- def _exec(self, args):
+ def _exec(self, args, check_rc=True):
+ '''Execute a subversion command, and return output. If check_rc is False, returns the return code instead of the output.'''
bits = [
self.svn_path,
'--non-interactive',
@@ -115,13 +123,21 @@ class Subversion(object):
if self.password:
bits.extend(["--password", self.password])
bits.extend(args)
- rc, out, err = self.module.run_command(bits, check_rc=True)
- return out.splitlines()
+ rc, out, err = self.module.run_command(bits, check_rc)
+ if check_rc:
+ return out.splitlines()
+ else:
+ return rc
+
+ def is_svn_repo(self):
+ '''Checks if path is a SVN Repo.'''
+ rc = self._exec(["info", self.dest], check_rc=False)
+ return rc == 0
def checkout(self):
'''Creates new svn working directory if it does not already exist.'''
self._exec(["checkout", "-r", self.revision, self.repo, self.dest])
-
+
def export(self, force=False):
'''Export svn repo to directory'''
cmd = ["export"]
@@ -153,8 +169,9 @@ class Subversion(object):
def has_local_mods(self):
'''True if revisioned files have been added or modified. Unrevisioned files are ignored.'''
- lines = self._exec(["status", "--quiet", self.dest])
+ lines = self._exec(["status", "--quiet", "--ignore-externals", self.dest])
# The --quiet option will return only modified files.
+
# Has local mods if more than 0 modifed revisioned files.
return len(filter(len, lines)) > 0
@@ -183,6 +200,7 @@ def main():
password=dict(required=False),
executable=dict(default=None),
export=dict(default=False, required=False, type='bool'),
+ switch=dict(default=True, required=False, type='bool'),
),
supports_check_mode=True
)
@@ -195,6 +213,7 @@ def main():
password = module.params['password']
svn_path = module.params['executable'] or module.get_bin_path('svn', True)
export = module.params['export']
+ switch = module.params['switch']
os.environ['LANG'] = 'C'
svn = Subversion(module, dest, repo, revision, username, password, svn_path)
@@ -208,7 +227,7 @@ def main():
svn.checkout()
else:
svn.export(force=force)
- elif os.path.exists("%s/.svn" % (dest, )):
+ elif svn.is_svn_repo():
# Order matters. Need to get local mods before switch to avoid false
# positives. Need to switch before revert to ensure we are reverting to
# correct repo.
@@ -217,7 +236,8 @@ def main():
module.exit_json(changed=check, before=before, after=after)
before = svn.get_revision()
local_mods = svn.has_local_mods()
- svn.switch()
+ if switch:
+ svn.switch()
if local_mods:
if force:
svn.revert()
diff --git a/system/authorized_key.py b/system/authorized_key.py
index bb223acbe4d..361e68cb009 100644
--- a/system/authorized_key.py
+++ b/system/authorized_key.py
@@ -34,7 +34,6 @@ options:
- The username on the remote host whose authorized_keys file will be modified
required: true
default: null
- aliases: []
key:
description:
- The SSH public key(s), as a string or (since 1.9) url (https://github.com/username.keys)
@@ -72,9 +71,11 @@ options:
version_added: "1.4"
exclusive:
description:
- - Whether to remove all other non-specified keys from the
- authorized_keys file. Multiple keys can be specified in a single
- key= string value by separating them by newlines.
+ - Whether to remove all other non-specified keys from the authorized_keys file. Multiple keys
+ can be specified in a single C(key) string value by separating them by newlines.
+ - This option is not loop aware, so if you use C(with_) , it will be exclusive per iteration
+ of the loop, if you want multiple keys in the file you need to pass them all to C(key) in a
+ single batch as mentioned above.
required: false
choices: [ "yes", "no" ]
default: "no"
@@ -108,11 +109,13 @@ EXAMPLES = '''
# Using key_options:
- authorized_key: user=charlie
key="{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}"
- key_options='no-port-forwarding,host="10.0.1.1"'
+ key_options='no-port-forwarding,from="10.0.1.1"'
# Set up authorized_keys exclusively with one key
-- authorized_key: user=root key=public_keys/doe-jane state=present
+- authorized_key: user=root key="{{ item }}" state=present
exclusive=yes
+ with_file:
+ - public_keys/doe-jane
'''
# Makes sure the public key line is present or absent in the user's .ssh/authorized_keys.
@@ -138,7 +141,7 @@ import shlex
class keydict(dict):
""" a dictionary that maintains the order of keys as they are added """
-
+
# http://stackoverflow.com/questions/2328235/pythonextend-the-dict-class
def __init__(self, *args, **kw):
@@ -146,7 +149,7 @@ class keydict(dict):
self.itemlist = super(keydict,self).keys()
def __setitem__(self, key, value):
self.itemlist.append(key)
- super(keydict,self).__setitem__(key, value)
+ super(keydict,self).__setitem__(key, value)
def __iter__(self):
return iter(self.itemlist)
def keys(self):
@@ -154,7 +157,7 @@ class keydict(dict):
def values(self):
return [self[key] for key in self]
def itervalues(self):
- return (self[key] for key in self)
+ return (self[key] for key in self)
def keyfile(module, user, write=False, path=None, manage_dir=True):
"""
@@ -168,9 +171,15 @@ def keyfile(module, user, write=False, path=None, manage_dir=True):
:return: full path string to authorized_keys for user
"""
+ if module.check_mode and path is not None:
+ keysfile = path
+ return keysfile
+
try:
user_entry = pwd.getpwnam(user)
except KeyError, e:
+ if module.check_mode and path is None:
+ module.fail_json(msg="Either user must exist or you must provide full path to key file in check mode")
module.fail_json(msg="Failed to lookup user %s: %s" % (user, str(e)))
if path is None:
homedir = user_entry.pw_dir
@@ -214,8 +223,8 @@ def keyfile(module, user, write=False, path=None, manage_dir=True):
return keysfile
def parseoptions(module, options):
- '''
- reads a string containing ssh-key options
+ '''
+ reads a string containing ssh-key options
and returns a dictionary of those options
'''
options_dict = keydict() #ordered dict
@@ -246,7 +255,7 @@ def parsekey(module, raw_key):
'ssh-ed25519',
'ecdsa-sha2-nistp256',
'ecdsa-sha2-nistp384',
- 'ecdsa-sha2-nistp521',
+ 'ecdsa-sha2-nistp521',
'ssh-dss',
'ssh-rsa',
]
diff --git a/system/cron.py b/system/cron.py
index b694bab8f20..f2de1bcec0f 100644
--- a/system/cron.py
+++ b/system/cron.py
@@ -4,6 +4,7 @@
# (c) 2012, Dane Summers
# (c) 2013, Mike Grozak
# (c) 2013, Patrick Callahan
+# (c) 2015, Evan Kaufman
#
# This file is part of Ansible
#
@@ -46,7 +47,7 @@ options:
description:
- Description of a crontab entry.
default: null
- required: true
+ required: false
user:
description:
- The specific user whose crontab should be modified.
@@ -116,10 +117,19 @@ options:
required: false
default: null
choices: [ "reboot", "yearly", "annually", "monthly", "weekly", "daily", "hourly" ]
+ disabled:
+ description:
+ - If the job should be disabled (commented out) in the crontab. Only has effect if state=present
+ version_added: "2.0"
+ required: false
+ default: false
requirements:
- cron
-author: "Dane Summers (@dsummersl)"
-updates: [ 'Mike Grozak', 'Patrick Callahan' ]
+author:
+ - "Dane Summers (@dsummersl)"
+ - 'Mike Grozak'
+ - 'Patrick Callahan'
+ - 'Evan Kaufman (@EvanK)'
"""
EXAMPLES = '''
@@ -290,17 +300,22 @@ class CronTab(object):
return []
- def get_cron_job(self,minute,hour,day,month,weekday,job,special):
+ def get_cron_job(self,minute,hour,day,month,weekday,job,special,disabled):
+ if disabled:
+ disable_prefix = '#'
+ else:
+ disable_prefix = ''
+
if special:
if self.cron_file:
- return "@%s %s %s" % (special, self.user, job)
+ return "%s@%s %s %s" % (disable_prefix, special, self.user, job)
else:
- return "@%s %s" % (special, job)
+ return "%s@%s %s" % (disable_prefix, special, job)
else:
if self.cron_file:
- return "%s %s %s %s %s %s %s" % (minute,hour,day,month,weekday,self.user,job)
+ return "%s%s %s %s %s %s %s %s" % (disable_prefix,minute,hour,day,month,weekday,self.user,job)
else:
- return "%s %s %s %s %s %s" % (minute,hour,day,month,weekday,job)
+ return "%s%s %s %s %s %s %s" % (disable_prefix,minute,hour,day,month,weekday,job)
return None
@@ -398,7 +413,7 @@ def main():
module = AnsibleModule(
argument_spec = dict(
- name=dict(required=True),
+ name=dict(required=False),
user=dict(required=False),
job=dict(required=False),
cron_file=dict(required=False),
@@ -413,7 +428,8 @@ def main():
special_time=dict(required=False,
default=None,
choices=["reboot", "yearly", "annually", "monthly", "weekly", "daily", "hourly"],
- type='str')
+ type='str'),
+ disabled=dict(default=False, type='bool')
),
supports_check_mode = False,
)
@@ -431,6 +447,7 @@ def main():
weekday = module.params['weekday']
reboot = module.params['reboot']
special_time = module.params['special_time']
+ disabled = module.params['disabled']
do_install = state == 'present'
changed = False
@@ -481,7 +498,7 @@ def main():
changed = crontab.remove_job_file()
module.exit_json(changed=changed,cron_file=cron_file,state=state)
- job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time)
+ job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled)
old_job = crontab.find_job(name)
if do_install:
diff --git a/system/group.py b/system/group.py
index d952cb5c28c..ab542d9bc47 100644
--- a/system/group.py
+++ b/system/group.py
@@ -121,7 +121,7 @@ class Group(object):
if len(cmd) == 1:
return (None, '', '')
if self.module.check_mode:
- return (0, '', '')
+ return (0, '', '')
cmd.append(self.name)
return self.execute_command(cmd)
@@ -233,7 +233,8 @@ class FreeBsdGroup(Group):
def group_add(self, **kwargs):
cmd = [self.module.get_bin_path('pw', True), 'groupadd', self.name]
if self.gid is not None:
- cmd.append('-g %d' % int(self.gid))
+ cmd.append('-g')
+ cmd.append('%d' % int(self.gid))
return self.execute_command(cmd)
def group_mod(self, **kwargs):
@@ -241,7 +242,8 @@ class FreeBsdGroup(Group):
info = self.group_info()
cmd_len = len(cmd)
if self.gid is not None and int(self.gid) != info[2]:
- cmd.append('-g %d' % int(self.gid))
+ cmd.append('-g')
+ cmd.append('%d' % int(self.gid))
# modify the group if cmd will do anything
if cmd_len != len(cmd):
if self.module.check_mode:
@@ -271,7 +273,8 @@ class DarwinGroup(Group):
def group_add(self, **kwargs):
cmd = [self.module.get_bin_path('dseditgroup', True)]
cmd += [ '-o', 'create' ]
- cmd += [ '-i', self.gid ]
+ if self.gid is not None:
+ cmd += [ '-i', self.gid ]
cmd += [ '-L', self.name ]
(rc, out, err) = self.execute_command(cmd)
return (rc, out, err)
@@ -283,12 +286,13 @@ class DarwinGroup(Group):
(rc, out, err) = self.execute_command(cmd)
return (rc, out, err)
- def group_mod(self):
+ def group_mod(self, gid=None):
info = self.group_info()
if self.gid is not None and int(self.gid) != info[2]:
cmd = [self.module.get_bin_path('dseditgroup', True)]
cmd += [ '-o', 'edit' ]
- cmd += [ '-i', self.gid ]
+ if gid is not None:
+ cmd += [ '-i', gid ]
cmd += [ '-L', self.name ]
(rc, out, err) = self.execute_command(cmd)
return (rc, out, err)
diff --git a/system/hostname.py b/system/hostname.py
index d9193641eb2..9e7f6a4ef70 100644
--- a/system/hostname.py
+++ b/system/hostname.py
@@ -21,7 +21,9 @@
DOCUMENTATION = '''
---
module: hostname
-author: "Hiroaki Nakamura (@hnakamur)"
+author:
+ - "Hiroaki Nakamura (@hnakamur)"
+ - "Hideki Saito (@saito-hideki)"
version_added: "1.4"
short_description: Manage hostname
requirements: [ hostname ]
@@ -116,13 +118,13 @@ class GenericStrategy(object):
- set_current_hostname(name)
- set_permanent_hostname(name)
"""
+
def __init__(self, module):
self.module = module
-
- HOSTNAME_CMD = '/bin/hostname'
+ self.hostname_cmd = self.module.get_bin_path('hostname', True)
def get_current_hostname(self):
- cmd = [self.HOSTNAME_CMD]
+ cmd = [self.hostname_cmd]
rc, out, err = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" %
@@ -130,7 +132,7 @@ class GenericStrategy(object):
return out.strip()
def set_current_hostname(self, name):
- cmd = [self.HOSTNAME_CMD, name]
+ cmd = [self.hostname_cmd, name]
rc, out, err = self.module.run_command(cmd)
if rc != 0:
self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" %
@@ -363,6 +365,39 @@ class OpenBSDStrategy(GenericStrategy):
# ===========================================
+class SolarisStrategy(GenericStrategy):
+ """
+ This is a Solaris11 or later Hostname manipulation strategy class - it
+ execute hostname command.
+ """
+
+ def set_current_hostname(self, name):
+ cmd_option = '-t'
+ cmd = [self.hostname_cmd, cmd_option, name]
+ rc, out, err = self.module.run_command(cmd)
+ if rc != 0:
+ self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" %
+ (rc, out, err))
+
+ def get_permanent_hostname(self):
+ fmri = 'svc:/system/identity:node'
+ pattern = 'config/nodename'
+ cmd = '/usr/sbin/svccfg -s %s listprop -o value %s' % (fmri, pattern)
+ rc, out, err = self.module.run_command(cmd, use_unsafe_shell=True)
+ if rc != 0:
+ self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" %
+ (rc, out, err))
+ return out.strip()
+
+ def set_permanent_hostname(self, name):
+ cmd = [self.hostname_cmd, name]
+ rc, out, err = self.module.run_command(cmd)
+ if rc != 0:
+ self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" %
+ (rc, out, err))
+
+# ===========================================
+
class FedoraHostname(Hostname):
platform = 'Linux'
distribution = 'Fedora'
@@ -456,6 +491,11 @@ class DebianHostname(Hostname):
distribution = 'Debian'
strategy_class = DebianStrategy
+class KaliHostname(Hostname):
+ platform = 'Linux'
+ distribution = 'Kali'
+ strategy_class = DebianStrategy
+
class UbuntuHostname(Hostname):
platform = 'Linux'
distribution = 'Ubuntu'
@@ -486,6 +526,11 @@ class OpenBSDHostname(Hostname):
distribution = None
strategy_class = OpenBSDStrategy
+class SolarisHostname(Hostname):
+ platform = 'SunOS'
+ distribution = None
+ strategy_class = SolarisStrategy
+
# ===========================================
def main():
diff --git a/system/mount.py b/system/mount.py
index 1564d0999f4..ff7094dad3b 100644
--- a/system/mount.py
+++ b/system/mount.py
@@ -104,7 +104,11 @@ def write_fstab(lines, dest):
fs_w.flush()
fs_w.close()
-def set_mount(**kwargs):
+def _escape_fstab(v):
+ """ escape space (040), ampersand (046) and backslash (134) which are invalid in fstab fields """
+ return v.replace('\\', '\\134').replace(' ', '\\040').replace('&', '\\046')
+
+def set_mount(module, **kwargs):
""" set/change a mount point location in fstab """
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
@@ -116,11 +120,17 @@ def set_mount(**kwargs):
)
args.update(kwargs)
+ # save the mount name before space replacement
+ origname = args['name']
+ # replace any space in mount name with '\040' to make it fstab compatible (man fstab)
+ args['name'] = args['name'].replace(' ', r'\040')
+
new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n'
to_write = []
exists = False
changed = False
+ escaped_args = dict([(k, _escape_fstab(v)) for k, v in args.iteritems()])
for line in open(args['fstab'], 'r').readlines():
if not line.strip():
to_write.append(line)
@@ -137,16 +147,16 @@ def set_mount(**kwargs):
ld = {}
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
- if ld['name'] != args['name']:
+ if ld['name'] != escaped_args['name']:
to_write.append(line)
continue
# it exists - now see if what we have is different
exists = True
for t in ('src', 'fstype','opts', 'dump', 'passno'):
- if ld[t] != args[t]:
+ if ld[t] != escaped_args[t]:
changed = True
- ld[t] = args[t]
+ ld[t] = escaped_args[t]
if changed:
to_write.append(new_line % ld)
@@ -157,13 +167,14 @@ def set_mount(**kwargs):
to_write.append(new_line % args)
changed = True
- if changed:
+ if changed and not module.check_mode:
write_fstab(to_write, args['fstab'])
- return (args['name'], changed)
+ # mount function needs origname
+ return (origname, changed)
-def unset_mount(**kwargs):
+def unset_mount(module, **kwargs):
""" remove a mount point from fstab """
# kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
@@ -175,8 +186,14 @@ def unset_mount(**kwargs):
)
args.update(kwargs)
+ # save the mount name before space replacement
+ origname = args['name']
+ # replace any space in mount name with '\040' to make it fstab compatible (man fstab)
+ args['name'] = args['name'].replace(' ', r'\040')
+
to_write = []
changed = False
+ escaped_name = _escape_fstab(args['name'])
for line in open(args['fstab'], 'r').readlines():
if not line.strip():
to_write.append(line)
@@ -193,28 +210,45 @@ def unset_mount(**kwargs):
ld = {}
ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split()
- if ld['name'] != args['name']:
+ if ld['name'] != escaped_name:
to_write.append(line)
continue
# if we got here we found a match - continue and mark changed
changed = True
- if changed:
+ if changed and not module.check_mode:
write_fstab(to_write, args['fstab'])
- return (args['name'], changed)
+ # umount needs origname
+ return (origname, changed)
def mount(module, **kwargs):
""" mount up a path or remount if needed """
+
+ # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab
+ args = dict(
+ opts = 'default',
+ dump = '0',
+ passno = '0',
+ fstab = '/etc/fstab'
+ )
+ args.update(kwargs)
+
mount_bin = module.get_bin_path('mount')
name = kwargs['name']
+
+ cmd = [ mount_bin, ]
+
if os.path.ismount(name):
- cmd = [ mount_bin , '-o', 'remount', name ]
- else:
- cmd = [ mount_bin, name ]
+ cmd += [ '-o', 'remount', ]
+
+ if get_platform().lower() == 'freebsd':
+ cmd += [ '-F', args['fstab'], ]
+
+ cmd += [ name, ]
rc, out, err = module.run_command(cmd)
if rc == 0:
@@ -247,7 +281,8 @@ def main():
src = dict(required=True),
fstype = dict(required=True),
fstab = dict(default='/etc/fstab')
- )
+ ),
+ supports_check_mode=True
)
@@ -262,8 +297,6 @@ def main():
args['passno'] = module.params['passno']
if module.params['opts'] is not None:
args['opts'] = module.params['opts']
- if ' ' in args['opts']:
- module.fail_json(msg="unexpected space in 'opts' parameter")
if module.params['dump'] is not None:
args['dump'] = module.params['dump']
if module.params['fstab'] is not None:
@@ -284,8 +317,8 @@ def main():
state = module.params['state']
name = module.params['name']
if state == 'absent':
- name, changed = unset_mount(**args)
- if changed:
+ name, changed = unset_mount(module, **args)
+ if changed and not module.check_mode:
if os.path.ismount(name):
res,msg = umount(module, **args)
if res:
@@ -301,26 +334,27 @@ def main():
if state == 'unmounted':
if os.path.ismount(name):
- res,msg = umount(module, **args)
- if res:
- module.fail_json(msg="Error unmounting %s: %s" % (name, msg))
+ if not module.check_mode:
+ res,msg = umount(module, **args)
+ if res:
+ module.fail_json(msg="Error unmounting %s: %s" % (name, msg))
changed = True
module.exit_json(changed=changed, **args)
if state in ['mounted', 'present']:
if state == 'mounted':
- if not os.path.exists(name):
+ if not os.path.exists(name) and not module.check_mode:
try:
os.makedirs(name)
except (OSError, IOError), e:
module.fail_json(msg="Error making dir %s: %s" % (name, str(e)))
- name, changed = set_mount(**args)
+ name, changed = set_mount(module, **args)
if state == 'mounted':
res = 0
if os.path.ismount(name):
- if changed:
+ if changed and not module.check_mode:
res,msg = mount(module, **args)
elif 'bind' in args.get('opts', []):
changed = True
@@ -335,7 +369,9 @@ def main():
res,msg = mount(module, **args)
else:
changed = True
- res,msg = mount(module, **args)
+ if not module.check_mode:
+ res,msg = mount(module, **args)
+
if res:
module.fail_json(msg="Error mounting %s: %s" % (name, msg))
diff --git a/system/service.py b/system/service.py
index 763553db124..4255ecb83ab 100644
--- a/system/service.py
+++ b/system/service.py
@@ -21,7 +21,7 @@
DOCUMENTATION = '''
---
module: service
-author:
+author:
- "Ansible Core Team"
- "Michael DeHaan"
version_added: "0.1"
@@ -359,7 +359,7 @@ class Service(object):
self.changed = True
# Add line to the list.
- new_rc_conf.append(rcline)
+ new_rc_conf.append(rcline.strip() + '\n')
# We are done with reading the current rc.conf, close it.
RCFILE.close()
@@ -503,15 +503,31 @@ class LinuxService(Service):
self.svc_initctl = location['initctl']
def get_systemd_service_enabled(self):
- (rc, out, err) = self.execute_command("%s is-enabled %s" % (self.enable_cmd, self.__systemd_unit,))
+ def sysv_exists(name):
+ script = '/etc/init.d/' + name
+ return os.access(script, os.X_OK)
+
+ def sysv_is_enabled(name):
+ return bool(glob.glob('/etc/rc?.d/S??' + name))
+
+ service_name = self.__systemd_unit
+ (rc, out, err) = self.execute_command("%s is-enabled %s" % (self.enable_cmd, service_name,))
if rc == 0:
return True
- return False
+ elif sysv_exists(service_name):
+ return sysv_is_enabled(service_name)
+ else:
+ return False
def get_systemd_status_dict(self):
- (rc, out, err) = self.execute_command("%s show %s" % (self.enable_cmd, self.__systemd_unit,))
+
+ # Check status first as show will not fail if service does not exist
+ (rc, out, err) = self.execute_command("%s show '%s'" % (self.enable_cmd, self.__systemd_unit,))
if rc != 0:
self.module.fail_json(msg='failure %d running systemctl show for %r: %s' % (rc, self.__systemd_unit, err))
+ elif 'LoadState=not-found' in out:
+ self.module.fail_json(msg='systemd could not find the requested service "%r": %s' % (self.__systemd_unit, err))
+
key = None
value_buffer = []
status_dict = {}
@@ -579,6 +595,11 @@ class LinuxService(Service):
self.running = "started" in openrc_status_stdout
self.crashed = "crashed" in openrc_status_stderr
+ # Prefer a non-zero return code. For reference, see:
+ # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
+ if self.running is None and rc in [1, 2, 3, 4, 69]:
+ self.running = False
+
# if the job status is still not known check it by status output keywords
# Only check keywords if there's only one line of output (some init
# scripts will output verbosely in case of error and those can emit
@@ -603,14 +624,10 @@ class LinuxService(Service):
elif 'dead but pid file exists' in cleanout:
self.running = False
- # if the job status is still not known check it by response code
- # For reference, see:
- # http://refspecs.linuxbase.org/LSB_4.1.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
- if self.running is None:
- if rc in [1, 2, 3, 4, 69]:
- self.running = False
- elif rc == 0:
- self.running = True
+ # if the job status is still not known and we got a zero for the
+ # return code, assume here that the service is running
+ if self.running is None and rc == 0:
+ self.running = True
# if the job status is still not known check it by special conditions
if self.running is None:
@@ -885,7 +902,7 @@ class LinuxService(Service):
if self.svc_cmd and self.svc_cmd.endswith('rc-service') and self.action == 'start' and self.crashed:
self.execute_command("%s zap" % svc_cmd, daemonize=True)
- if self.action is not "restart":
+ if self.action != "restart":
if svc_cmd != '':
# upstart or systemd or OpenRC
rc_state, stdout, stderr = self.execute_command("%s %s %s" % (svc_cmd, self.action, arguments), daemonize=True)
@@ -968,7 +985,11 @@ class FreeBsdService(Service):
rc, stdout, stderr = self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments))
cmd = "%s %s %s %s" % (self.svc_cmd, self.name, 'rcvar', self.arguments)
- rcvars = shlex.split(stdout, comments=True)
+ try:
+ rcvars = shlex.split(stdout, comments=True)
+ except:
+ #TODO: add a warning to the output with the failure
+ pass
if not rcvars:
self.module.fail_json(msg="unable to determine rcvar", stdout=stdout, stderr=stderr)
@@ -988,16 +1009,16 @@ class FreeBsdService(Service):
try:
return self.service_enable_rcconf()
- except:
+ except Exception:
self.module.fail_json(msg='unable to set rcvar')
def service_control(self):
- if self.action is "start":
+ if self.action == "start":
self.action = "onestart"
- if self.action is "stop":
+ if self.action == "stop":
self.action = "onestop"
- if self.action is "reload":
+ if self.action == "reload":
self.action = "onereload"
return self.execute_command("%s %s %s %s" % (self.svc_cmd, self.name, self.action, self.arguments))
@@ -1203,9 +1224,9 @@ class NetBsdService(Service):
self.running = True
def service_control(self):
- if self.action is "start":
+ if self.action == "start":
self.action = "onestart"
- if self.action is "stop":
+ if self.action == "stop":
self.action = "onestop"
self.svc_cmd = "%s" % self.svc_initscript
diff --git a/system/setup.py b/system/setup.py
index 2fbe71e260a..a28c037c745 100644
--- a/system/setup.py
+++ b/system/setup.py
@@ -123,7 +123,7 @@ def run_setup(module):
setup_result['ansible_facts'][k] = v
# hack to keep --verbose from showing all the setup module results
- setup_result['verbose_override'] = True
+ setup_result['_ansible_verbose_override'] = True
return setup_result
diff --git a/system/user.py b/system/user.py
old mode 100644
new mode 100755
index 7c3fa4c8594..45ce77381ce
--- a/system/user.py
+++ b/system/user.py
@@ -74,6 +74,10 @@ options:
required: false
description:
- Optionally set the user's home directory.
+ skeleton:
+ required: false
+ description:
+ - Optionally set a home skeleton directory. Requires createhome option!
password:
required: false
description:
@@ -253,13 +257,13 @@ class User(object):
self.group = module.params['group']
self.groups = module.params['groups']
self.comment = module.params['comment']
- self.home = module.params['home']
self.shell = module.params['shell']
self.password = module.params['password']
self.force = module.params['force']
self.remove = module.params['remove']
self.createhome = module.params['createhome']
self.move_home = module.params['move_home']
+ self.skeleton = module.params['skeleton']
self.system = module.params['system']
self.login_class = module.params['login_class']
self.append = module.params['append']
@@ -269,8 +273,12 @@ class User(object):
self.ssh_comment = module.params['ssh_key_comment']
self.ssh_passphrase = module.params['ssh_key_passphrase']
self.update_password = module.params['update_password']
+ self.home = None
self.expires = None
+ if module.params['home'] is not None:
+ self.home = os.path.expanduser(module.params['home'])
+
if module.params['expires']:
try:
self.expires = time.gmtime(module.params['expires'])
@@ -360,6 +368,10 @@ class User(object):
if self.createhome:
cmd.append('-m')
+
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
else:
cmd.append('-M')
@@ -565,11 +577,13 @@ class User(object):
def ssh_key_gen(self):
info = self.user_info()
- if not os.path.exists(info[5]):
+ if not os.path.exists(info[5]) and not self.module.check_mode:
return (1, '', 'User %s home directory does not exist' % self.name)
ssh_key_file = self.get_ssh_key_path()
ssh_dir = os.path.dirname(ssh_key_file)
if not os.path.exists(ssh_dir):
+ if self.module.check_mode:
+ return (0, '', '')
try:
os.mkdir(ssh_dir, 0700)
os.chown(ssh_dir, info[2], info[3])
@@ -577,6 +591,8 @@ class User(object):
return (1, '', 'Failed to create %s: %s' % (ssh_dir, str(e)))
if os.path.exists(ssh_key_file):
return (None, 'Key already exists', '')
+ if self.module.check_mode:
+ return (0, '', '')
cmd = [self.module.get_bin_path('ssh-keygen', True)]
cmd.append('-t')
cmd.append(self.ssh_type)
@@ -635,10 +651,14 @@ class User(object):
def create_homedir(self, path):
if not os.path.exists(path):
- # use /etc/skel if possible
- if os.path.exists('/etc/skel'):
+ if self.skeleton is not None:
+ skeleton = self.skeleton
+ else:
+ skeleton = '/etc/skel'
+
+ if os.path.exists(skeleton):
try:
- shutil.copytree('/etc/skel', path, symlinks=True)
+ shutil.copytree(skeleton, path, symlinks=True)
except OSError, e:
self.module.exit_json(failed=True, msg="%s" % e)
else:
@@ -726,6 +746,10 @@ class FreeBsdUser(User):
if self.createhome:
cmd.append('-m')
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
+
if self.shell is not None:
cmd.append('-s')
cmd.append(self.shell)
@@ -913,13 +937,17 @@ class OpenBSDUser(User):
cmd.append('-L')
cmd.append(self.login_class)
- if self.password is not None:
+ if self.password is not None and self.password != '*':
cmd.append('-p')
cmd.append(self.password)
if self.createhome:
cmd.append('-m')
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
+
cmd.append(self.name)
return self.execute_command(cmd)
@@ -1007,7 +1035,8 @@ class OpenBSDUser(User):
cmd.append('-L')
cmd.append(self.login_class)
- if self.update_password == 'always' and self.password is not None and info[1] != self.password:
+ if self.update_password == 'always' and self.password is not None \
+ and self.password != '*' and info[1] != self.password:
cmd.append('-p')
cmd.append(self.password)
@@ -1087,6 +1116,10 @@ class NetBSDUser(User):
if self.createhome:
cmd.append('-m')
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
+
cmd.append(self.name)
return self.execute_command(cmd)
@@ -1239,6 +1272,10 @@ class SunOS(User):
if self.createhome:
cmd.append('-m')
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
+
cmd.append(self.name)
if self.module.check_mode:
@@ -1747,6 +1784,10 @@ class AIX(User):
if self.createhome:
cmd.append('-m')
+ if self.skeleton is not None:
+ cmd.append('-k')
+ cmd.append(self.skeleton)
+
cmd.append(self.name)
(rc, out, err) = self.execute_command(cmd)
@@ -2018,6 +2059,7 @@ def main():
remove=dict(default='no', type='bool'),
# following options are specific to useradd
createhome=dict(default='yes', type='bool'),
+ skeleton=dict(default=None, type='str'),
system=dict(default='no', type='bool'),
# following options are specific to usermod
move_home=dict(default='no', type='bool'),
@@ -2110,6 +2152,7 @@ def main():
# deal with ssh key
if user.sshkeygen:
+ # generate ssh key (note: this function is check mode aware)
(rc, out, err) = user.ssh_key_gen()
if rc is not None and rc != 0:
module.fail_json(name=user.name, msg=err, rc=rc)
diff --git a/test-docs.sh b/test-docs.sh
new file mode 100755
index 00000000000..76297fbada6
--- /dev/null
+++ b/test-docs.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -x
+
+CHECKOUT_DIR=".ansible-checkout"
+MOD_REPO="$1"
+
+# Hidden file to avoid the module_formatter recursing into the checkout
+git clone https://github.com/ansible/ansible "$CHECKOUT_DIR"
+cd "$CHECKOUT_DIR"
+git submodule update --init
+rm -rf "lib/ansible/modules/$MOD_REPO"
+ln -s "$TRAVIS_BUILD_DIR/" "lib/ansible/modules/$MOD_REPO"
+
+pip install -U Jinja2 PyYAML setuptools six pycrypto sphinx
+
+. ./hacking/env-setup
+PAGER=/bin/cat bin/ansible-doc -l
+if [ $? -ne 0 ] ; then
+ exit $?
+fi
+make -C docsite
diff --git a/utilities/logic/pause.py b/utilities/logic/pause.py
index f1d10bf017f..0fad09ea7bc 100644
--- a/utilities/logic/pause.py
+++ b/utilities/logic/pause.py
@@ -1,5 +1,20 @@
# -*- mode: python -*-
+# 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 .
+
DOCUMENTATION = '''
---
module: pause
diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py
index 95653b56d3e..295155f3028 100644
--- a/utilities/logic/wait_for.py
+++ b/utilities/logic/wait_for.py
@@ -301,6 +301,25 @@ def _little_endian_convert_32bit(block):
# which lets us start at the end of the string block and work to the begining
return "".join([ block[x:x+2] for x in xrange(6, -2, -2) ])
+def _create_connection( (host, port), connect_timeout):
+ """
+ Connect to a 2-tuple (host, port) and return
+ the socket object.
+
+ Args:
+ 2-tuple (host, port) and connection timeout
+ Returns:
+ Socket object
+ """
+ if sys.version_info < (2, 6):
+ (family, _) = _convert_host_to_ip(host)
+ connect_socket = socket.socket(family, socket.SOCK_STREAM)
+ connect_socket.settimeout(connect_timeout)
+ connect_socket.connect( (host, port) )
+ else:
+ connect_socket = socket.create_connection( (host, port), connect_timeout)
+ return connect_socket
+
def main():
module = AnsibleModule(
@@ -362,10 +381,8 @@ def main():
except IOError:
break
elif port:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(connect_timeout)
try:
- s.connect( (host, port) )
+ s = _create_connection( (host, port), connect_timeout)
s.shutdown(socket.SHUT_RDWR)
s.close()
time.sleep(1)
@@ -410,10 +427,8 @@ def main():
elapsed = datetime.datetime.now() - start
module.fail_json(msg="Failed to stat %s, %s" % (path, e.strerror), elapsed=elapsed.seconds)
elif port:
- s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- s.settimeout(connect_timeout)
try:
- s.connect( (host, port) )
+ s = _create_connection( (host, port), connect_timeout)
if search_regex:
data = ''
matched = False
diff --git a/web_infrastructure/apache2_module.py b/web_infrastructure/apache2_module.py
index ec9a8985e60..cb43ba9b0eb 100644
--- a/web_infrastructure/apache2_module.py
+++ b/web_infrastructure/apache2_module.py
@@ -35,6 +35,7 @@ options:
choices: ['present', 'absent']
default: present
+requirements: ["a2enmod","a2dismod"]
'''
EXAMPLES = '''
diff --git a/web_infrastructure/django_manage.py b/web_infrastructure/django_manage.py
index c5ca5a005b0..b3cabfe01b5 100644
--- a/web_infrastructure/django_manage.py
+++ b/web_infrastructure/django_manage.py
@@ -30,7 +30,8 @@ options:
command:
choices: [ 'cleanup', 'collectstatic', 'flush', 'loaddata', 'migrate', 'runfcgi', 'syncdb', 'test', 'validate', ]
description:
- - The name of the Django management command to run. Built in commands are cleanup, collectstatic, flush, loaddata, migrate, runfcgi, syncdb, test, and validate. Other commands can be entered, but will fail if they're unknown to Django.
+ - The name of the Django management command to run. Built in commands are cleanup, collectstatic, flush, loaddata, migrate, runfcgi, syncdb, test, and validate.
+ - Other commands can be entered, but will fail if they're unknown to Django. Other commands that may prompt for user input should be run with the I(--noinput) flag.
required: true
app_path:
description:
@@ -89,7 +90,7 @@ notes:
- I(virtualenv) (U(http://www.virtualenv.org)) must be installed on the remote host if the virtualenv parameter is specified.
- This module will create a virtualenv if the virtualenv parameter is specified and a virtualenv does not already exist at the given location.
- This module assumes English error messages for the 'createcachetable' command to detect table existence, unfortunately.
- - To be able to use the migrate command, you must have south installed and added as an app in your settings
+ - To be able to use the migrate command with django versions < 1.7, you must have south installed and added as an app in your settings
- To be able to use the collectstatic command, you must have enabled staticfiles in your settings
requirements: [ "virtualenv", "django" ]
author: "Scott Anderson (@tastychutney)"
@@ -102,7 +103,7 @@ EXAMPLES = """
# Load the initial_data fixture into the application
- django_manage: command=loaddata app_path={{ django_dir }} fixtures={{ initial_data }}
-#Run syncdb on the application
+# Run syncdb on the application
- django_manage: >
command=syncdb
app_path={{ django_dir }}
@@ -110,8 +111,11 @@ EXAMPLES = """
pythonpath={{ settings_dir }}
virtualenv={{ virtualenv_dir }}
-#Run the SmokeTest test case from the main app. Useful for testing deploys.
-- django_manage: command=test app_path=django_dir apps=main.SmokeTest
+# Run the SmokeTest test case from the main app. Useful for testing deploys.
+- django_manage: command=test app_path={{ django_dir }} apps=main.SmokeTest
+
+# Create an initial superuser.
+- django_manage: command="createsuperuser --noinput --username=admin --email=admin@example.com" app_path={{ django_dir }}
"""
@@ -159,7 +163,10 @@ def syncdb_filter_output(line):
return ("Creating table " in line) or ("Installed" in line and "Installed 0 object" not in line)
def migrate_filter_output(line):
- return ("Migrating forwards " in line) or ("Installed" in line and "Installed 0 object" not in line)
+ return ("Migrating forwards " in line) or ("Installed" in line and "Installed 0 object" not in line) or ("Applying" in line)
+
+def collectstatic_filter_output(line):
+ return "0 static files" not in line
def main():
command_allowed_param_map = dict(
@@ -234,7 +241,7 @@ def main():
_ensure_virtualenv(module)
- cmd = "python manage.py %s" % (command, )
+ cmd = "./manage.py %s" % (command, )
if command in noinput_commands:
cmd = '%s --noinput' % cmd
diff --git a/web_infrastructure/htpasswd.py b/web_infrastructure/htpasswd.py
index bfb525b67eb..361a131ef2d 100644
--- a/web_infrastructure/htpasswd.py
+++ b/web_infrastructure/htpasswd.py
@@ -46,7 +46,10 @@ options:
choices: ["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext"]
default: "apr_md5_crypt"
description:
- - Encryption scheme to be used.
+ - Encryption scheme to be used. As well as the four choices listed
+ here, you can also use any other hash supported by passlib, such as
+ md5_crypt and sha256_crypt, which are linux passwd hashes. If you
+ do so the password file will not be compatible with Apache or Nginx
state:
required: false
choices: [ present, absent ]
@@ -74,6 +77,8 @@ EXAMPLES = """
- htpasswd: path=/etc/nginx/passwdfile name=janedoe password=9s36?;fyNp owner=root group=www-data mode=0640
# Remove a user from a password file
- htpasswd: path=/etc/apache2/passwdfile name=foobar state=absent
+# Add a user to a password file suitable for use by libpam-pwdfile
+- htpasswd: path=/etc/mail/passwords name=alex password=oedu2eGh crypt_scheme=md5_crypt
"""
@@ -82,13 +87,15 @@ import tempfile
from distutils.version import StrictVersion
try:
- from passlib.apache import HtpasswdFile
+ from passlib.apache import HtpasswdFile, htpasswd_context
+ from passlib.context import CryptContext
import passlib
except ImportError:
passlib_installed = False
else:
passlib_installed = True
+apache_hashes = ["apr_md5_crypt", "des_crypt", "ldap_sha1", "plaintext"]
def create_missing_directories(dest):
destpath = os.path.dirname(dest)
@@ -100,6 +107,10 @@ def present(dest, username, password, crypt_scheme, create, check_mode):
""" Ensures user is present
Returns (msg, changed) """
+ if crypt_scheme in apache_hashes:
+ context = htpasswd_context
+ else:
+ context = CryptContext(schemes = [ crypt_scheme ] + apache_hashes)
if not os.path.exists(dest):
if not create:
raise ValueError('Destination %s does not exist' % dest)
@@ -107,9 +118,9 @@ def present(dest, username, password, crypt_scheme, create, check_mode):
return ("Create %s" % dest, True)
create_missing_directories(dest)
if StrictVersion(passlib.__version__) >= StrictVersion('1.6'):
- ht = HtpasswdFile(dest, new=True, default_scheme=crypt_scheme)
+ ht = HtpasswdFile(dest, new=True, default_scheme=crypt_scheme, context=context)
else:
- ht = HtpasswdFile(dest, autoload=False, default=crypt_scheme)
+ ht = HtpasswdFile(dest, autoload=False, default=crypt_scheme, context=context)
if getattr(ht, 'set_password', None):
ht.set_password(username, password)
else:
@@ -118,9 +129,9 @@ def present(dest, username, password, crypt_scheme, create, check_mode):
return ("Created %s and added %s" % (dest, username), True)
else:
if StrictVersion(passlib.__version__) >= StrictVersion('1.6'):
- ht = HtpasswdFile(dest, new=False, default_scheme=crypt_scheme)
+ ht = HtpasswdFile(dest, new=False, default_scheme=crypt_scheme, context=context)
else:
- ht = HtpasswdFile(dest, default=crypt_scheme)
+ ht = HtpasswdFile(dest, default=crypt_scheme, context=context)
found = None
if getattr(ht, 'check_password', None):
@@ -179,7 +190,7 @@ def main():
path=dict(required=True, aliases=["dest", "destfile"]),
name=dict(required=True, aliases=["username"]),
password=dict(required=False, default=None),
- crypt_scheme=dict(required=False, default=None),
+ crypt_scheme=dict(required=False, default="apr_md5_crypt"),
state=dict(required=False, default="present"),
create=dict(type='bool', default='yes'),
diff --git a/web_infrastructure/supervisorctl.py b/web_infrastructure/supervisorctl.py
index 47d341c9e7b..9bc4c0b8afa 100644
--- a/web_infrastructure/supervisorctl.py
+++ b/web_infrastructure/supervisorctl.py
@@ -30,7 +30,7 @@ version_added: "0.7"
options:
name:
description:
- - The name of the supervisord program or group to manage.
+ - The name of the supervisord program or group to manage.
- The name will be taken as group name when it ends with a colon I(:)
- Group support is only available in Ansible version 1.6 or later.
required: true
@@ -64,7 +64,7 @@ options:
- The desired state of program/group.
required: true
default: null
- choices: [ "present", "started", "stopped", "restarted" ]
+ choices: [ "present", "started", "stopped", "restarted", "absent" ]
supervisorctl_path:
description:
- path to supervisorctl executable
@@ -75,8 +75,8 @@ notes:
- When C(state) = I(present), the module will call C(supervisorctl reread) then C(supervisorctl add) if the program/group does not exist.
- When C(state) = I(restarted), the module will call C(supervisorctl update) then call C(supervisorctl restart).
requirements: [ "supervisorctl" ]
-author:
- - "Matt Wright (@mattupstate)"
+author:
+ - "Matt Wright (@mattupstate)"
- "Aaron Wang (@inetfuture) "
'''
@@ -103,7 +103,7 @@ def main():
username=dict(required=False),
password=dict(required=False),
supervisorctl_path=dict(required=False),
- state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped'])
+ state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'absent'])
)
module = AnsibleModule(argument_spec=arg_spec, supports_check_mode=True)
@@ -194,10 +194,26 @@ def main():
if state == 'restarted':
rc, out, err = run_supervisorctl('update', check_rc=True)
processes = get_matched_processes()
+ if len(processes) == 0:
+ module.fail_json(name=name, msg="ERROR (no such process)")
+
take_action_on_processes(processes, lambda s: True, 'restart', 'started')
processes = get_matched_processes()
+ if state == 'absent':
+ if len(processes) == 0:
+ module.exit_json(changed=False, name=name, state=state)
+
+ if module.check_mode:
+ module.exit_json(changed=True)
+ run_supervisorctl('reread', check_rc=True)
+ rc, out, err = run_supervisorctl('remove', name)
+ if '%s: removed process group' % name in out:
+ module.exit_json(changed=True, name=name, state=state)
+ else:
+ module.fail_json(msg=out, name=name, state=state)
+
if state == 'present':
if len(processes) > 0:
module.exit_json(changed=False, name=name, state=state)
@@ -212,9 +228,13 @@ def main():
module.fail_json(msg=out, name=name, state=state)
if state == 'started':
+ if len(processes) == 0:
+ module.fail_json(name=name, msg="ERROR (no such process)")
take_action_on_processes(processes, lambda s: s not in ('RUNNING', 'STARTING'), 'start', 'started')
if state == 'stopped':
+ if len(processes) == 0:
+ module.fail_json(name=name, msg="ERROR (no such process)")
take_action_on_processes(processes, lambda s: s in ('RUNNING', 'STARTING'), 'stop', 'stopped')
# import module snippets
diff --git a/windows/setup.ps1 b/windows/setup.ps1
index 32b4d865263..3e3317d0450 100644
--- a/windows/setup.ps1
+++ b/windows/setup.ps1
@@ -25,7 +25,7 @@ $result = New-Object psobject @{
changed = $false
};
-$win32_os = Get-WmiObject Win32_OperatingSystem
+$win32_os = Get-CimInstance Win32_OperatingSystem
$osversion = [Environment]::OSVersion
$memory = @()
$memory += Get-WmiObject win32_Physicalmemory
@@ -60,12 +60,15 @@ Set-Attr $result.ansible_facts "ansible_hostname" $env:COMPUTERNAME;
Set-Attr $result.ansible_facts "ansible_fqdn" "$([System.Net.Dns]::GetHostByName((hostname)).HostName)"
Set-Attr $result.ansible_facts "ansible_system" $osversion.Platform.ToString()
Set-Attr $result.ansible_facts "ansible_os_family" "Windows"
-Set-Attr $result.ansible_facts "ansible_os_name" $win32_os.Name.Split('|')[0]
+Set-Attr $result.ansible_facts "ansible_os_name" ($win32_os.Name.Split('|')[0]).Trim()
Set-Attr $result.ansible_facts "ansible_distribution" $osversion.VersionString
Set-Attr $result.ansible_facts "ansible_distribution_version" $osversion.Version.ToString()
Set-Attr $result.ansible_facts "ansible_totalmem" $capacity
+Set-Attr $result.ansible_facts "ansible_lastboot" $win32_os.lastbootuptime.ToString("u")
+Set-Attr $result.ansible_facts "ansible_uptime_seconds" $([System.Convert]::ToInt64($(Get-Date).Subtract($win32_os.lastbootuptime).TotalSeconds))
+
$ips = @()
Foreach ($ip in $netcfg.IPAddress) { If ($ip) { $ips += $ip } }
Set-Attr $result.ansible_facts "ansible_ip_addresses" $ips
diff --git a/windows/slurp.ps1 b/windows/slurp.ps1
index edf1da7635f..073acddeade 100644
--- a/windows/slurp.ps1
+++ b/windows/slurp.ps1
@@ -17,7 +17,7 @@
# WANT_JSON
# POWERSHELL_COMMON
-$params = Parse-Args $args;
+$params = Parse-Args $args $true;
$src = Get-Attr $params "src" (Get-Attr $params "path" $FALSE);
If (-not $src)
diff --git a/windows/win_copy.py b/windows/win_copy.py
index efdebc5a4a6..acc6c9ef2e0 100644
--- a/windows/win_copy.py
+++ b/windows/win_copy.py
@@ -18,8 +18,6 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see .
-import os
-import time
DOCUMENTATION = '''
---
diff --git a/windows/win_feature.ps1 b/windows/win_feature.ps1
index a54007b47bf..ec6317fb89b 100644
--- a/windows/win_feature.ps1
+++ b/windows/win_feature.ps1
@@ -27,48 +27,18 @@ $result = New-Object PSObject -Property @{
changed = $false
}
-If ($params.name) {
- $name = $params.name
-}
-Else {
- Fail-Json $result "mising required argument: name"
+$name = Get-Attr $params "name" -failifempty $true
+$name = $name -split ',' | % { $_.Trim() }
+
+$state = Get-Attr $params "state" "present"
+$state = $state.ToString().ToLower()
+If (($state -ne 'present') -and ($state -ne 'absent')) {
+ Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
}
-If ($params.state) {
- $state = $params.state.ToString().ToLower()
- If (($state -ne 'present') -and ($state -ne 'absent')) {
- Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
- }
-}
-Elseif (!$params.state) {
- $state = "present"
-}
-
-If ($params.restart) {
- $restart = $params.restart | ConvertTo-Bool
-}
-Else
-{
- $restart = $false
-}
-
-if ($params.include_sub_features)
-{
- $includesubfeatures = $params.include_sub_features | ConvertTo-Bool
-}
-Else
-{
- $includesubfeatures = $false
-}
-
-if ($params.include_management_tools)
-{
- $includemanagementtools = $params.include_management_tools | ConvertTo-Bool
-}
-Else
-{
- $includemanagementtools = $false
-}
+$restart = Get-Attr $params "restart" $false | ConvertTo-Bool
+$includesubfeatures = Get-Attr $params "include_sub_features" $false | ConvertTo-Bool
+$includemanagementtools = Get-Attr $params "include_management_tools" $false | ConvertTo-Bool
If ($state -eq "present") {
try {
diff --git a/windows/win_feature.py b/windows/win_feature.py
index 2d7a747cea0..6ef4774788c 100644
--- a/windows/win_feature.py
+++ b/windows/win_feature.py
@@ -90,7 +90,7 @@ $ ansible -i hosts -m win_feature -a "name=Web-Server,Web-Common-Http" all
- name: Install IIS
win_feature:
name: "Web-Server"
- state: absent
+ state: present
restart: yes
include_sub_features: yes
include_management_tools: yes
diff --git a/windows/win_file.ps1 b/windows/win_file.ps1
index 0f3c20ec8e3..f8416120abf 100644
--- a/windows/win_file.ps1
+++ b/windows/win_file.ps1
@@ -56,7 +56,7 @@ If ( $state -eq "touch" )
}
Else
{
- echo $null > $file
+ echo $null > $path
}
$result.changed = $TRUE
}
diff --git a/windows/win_file.py b/windows/win_file.py
index 062b4bfe92e..895da567d86 100644
--- a/windows/win_file.py
+++ b/windows/win_file.py
@@ -22,7 +22,7 @@
DOCUMENTATION = '''
---
module: win_file
-version_added: "1.8"
+version_added: "1.9.2"
short_description: Creates, touches or removes files or directories.
description:
- Creates (empty) files, updates file modification stamps of existing files,
diff --git a/windows/win_get_url.ps1 b/windows/win_get_url.ps1
index b555cc7a52c..18977bff1ef 100644
--- a/windows/win_get_url.ps1
+++ b/windows/win_get_url.ps1
@@ -1,7 +1,7 @@
#!powershell
# This file is part of Ansible.
#
-# Copyright 2014, Paul Durivage
+# (c)) 2015, Paul Durivage , Tal Auslander
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
@@ -40,14 +40,57 @@ Else {
Fail-Json $result "missing required argument: dest"
}
-$client = New-Object System.Net.WebClient
+$skip_certificate_validation = Get-Attr $params "skip_certificate_validation" $false | ConvertTo-Bool
+$username = Get-Attr $params "username"
+$password = Get-Attr $params "password"
-Try {
- $client.DownloadFile($url, $dest)
- $result.changed = $true
+if($skip_certificate_validation){
+ [System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}
}
-Catch {
- Fail-Json $result "Error downloading $url to $dest"
+
+$force = Get-Attr -obj $params -name "force" "yes" | ConvertTo-Bool
+
+If ($force -or -not (Test-Path $dest)) {
+ $client = New-Object System.Net.WebClient
+
+ if($username -and $password){
+ $client.Credentials = New-Object System.Net.NetworkCredential($username, $password)
+ }
+
+ Try {
+ $client.DownloadFile($url, $dest)
+ $result.changed = $true
+ }
+ Catch {
+ Fail-Json $result "Error downloading $url to $dest $($_.Exception.Message)"
+ }
+}
+Else {
+ Try {
+ $webRequest = [System.Net.HttpWebRequest]::Create($url)
+
+ if($username -and $password){
+ $webRequest.Credentials = New-Object System.Net.NetworkCredential($username, $password)
+ }
+
+ $webRequest.IfModifiedSince = ([System.IO.FileInfo]$dest).LastWriteTime
+ $webRequest.Method = "GET"
+ [System.Net.HttpWebResponse]$webResponse = $webRequest.GetResponse()
+
+ $stream = New-Object System.IO.StreamReader($response.GetResponseStream())
+
+ $stream.ReadToEnd() | Set-Content -Path $dest -Force -ErrorAction Stop
+
+ $result.changed = $true
+ }
+ Catch [System.Net.WebException] {
+ If ($_.Exception.Response.StatusCode -ne [System.Net.HttpStatusCode]::NotModified) {
+ Fail-Json $result "Error downloading $url to $dest $($_.Exception.Message)"
+ }
+ }
+ Catch {
+ Fail-Json $result "Error downloading $url to $dest $($_.Exception.Message)"
+ }
}
Set-Attr $result.win_get_url "url" $url
diff --git a/windows/win_get_url.py b/windows/win_get_url.py
index 585d3e2aa81..5c3e994d418 100644
--- a/windows/win_get_url.py
+++ b/windows/win_get_url.py
@@ -27,21 +27,44 @@ module: win_get_url
version_added: "1.7"
short_description: Fetches a file from a given URL
description:
- - Fetches a file from a URL and saves to locally
+ - Fetches a file from a URL and saves to locally
+author: "Paul Durivage (@angstwad)"
options:
url:
description:
- The full URL of a file to download
required: true
default: null
- aliases: []
dest:
description:
- - The absolute path of the location to save the file at the URL. Be sure to include a filename and extension as appropriate.
+ - The absolute path of the location to save the file at the URL. Be sure
+ to include a filename and extension as appropriate.
+ required: true
+ default: null
+ force:
+ description:
+ - If C(yes), will always download the file. If C(no), will only
+ download the file if it does not exist or the remote file has been
+ modified more recently than the local file.
+ version_added: "2.0"
required: false
+ choices: [ "yes", "no" ]
default: yes
- aliases: []
-author: "Paul Durivage (@angstwad)"
+ username:
+ description:
+ - Basic authentication username
+ required: false
+ default: null
+ password:
+ description:
+ - Basic authentication password
+ required: false
+ default: null
+ skip_certificate_validation:
+ description:
+ - Skip SSL certificate validation if true
+ required: false
+ default: false
'''
EXAMPLES = '''
@@ -54,4 +77,10 @@ $ ansible -i hosts -c winrm -m win_get_url -a "url=http://www.example.com/earthr
win_get_url:
url: 'http://www.example.com/earthrise.jpg'
dest: 'C:\Users\RandomUser\earthrise.jpg'
+
+- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' only if modified
+ win_get_url:
+ url: 'http://www.example.com/earthrise.jpg'
+ dest: 'C:\Users\RandomUser\earthrise.jpg'
+ force: no
'''
diff --git a/windows/win_group.ps1 b/windows/win_group.ps1
index febaf47d014..c3fc920c916 100644
--- a/windows/win_group.ps1
+++ b/windows/win_group.ps1
@@ -24,35 +24,31 @@ $params = Parse-Args $args;
$result = New-Object PSObject;
Set-Attr $result "changed" $false;
-If (-not $params.name.GetType) {
- Fail-Json $result "missing required arguments: name"
+$name = Get-Attr $params "name" -failifempty $true
+
+$state = Get-Attr $params "state" "present"
+$state = $state.ToString().ToLower()
+If (($state -ne "present") -and ($state -ne "absent")) {
+ Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
}
-If ($params.state) {
- $state = $params.state.ToString().ToLower()
- If (($state -ne "present") -and ($state -ne "absent")) {
- Fail-Json $result "state is '$state'; must be 'present' or 'absent'"
- }
-}
-Elseif (-not $params.state) {
- $state = "present"
-}
+$description = Get-Attr $params "description" $null
$adsi = [ADSI]"WinNT://$env:COMPUTERNAME"
-$group = $adsi.Children | Where-Object {$_.SchemaClassName -eq 'group' -and $_.Name -eq $params.name }
+$group = $adsi.Children | Where-Object {$_.SchemaClassName -eq 'group' -and $_.Name -eq $name }
try {
If ($state -eq "present") {
If (-not $group) {
- $group = $adsi.Create("Group", $params.name)
+ $group = $adsi.Create("Group", $name)
$group.SetInfo()
Set-Attr $result "changed" $true
}
- If ($params.description.GetType) {
- IF (-not $group.description -or $group.description -ne $params.description) {
- $group.description = $params.description
+ If ($null -ne $description) {
+ IF (-not $group.description -or $group.description -ne $description) {
+ $group.description = $description
$group.SetInfo()
Set-Attr $result "changed" $true
}
diff --git a/windows/win_lineinfile.ps1 b/windows/win_lineinfile.ps1
new file mode 100644
index 00000000000..ddf1d4e3000
--- /dev/null
+++ b/windows/win_lineinfile.ps1
@@ -0,0 +1,452 @@
+#!powershell
+# 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
+
+
+# Parse the parameters file dropped by the Ansible machinery
+
+$params = Parse-Args $args;
+
+
+# Initialize defaults for input parameters.
+
+$dest= Get-Attr $params "dest" $FALSE;
+$regexp = Get-Attr $params "regexp" $FALSE;
+$state = Get-Attr $params "state" "present";
+$line = Get-Attr $params "line" $FALSE;
+$backrefs = Get-Attr $params "backrefs" "no";
+$insertafter = Get-Attr $params "insertafter" $FALSE;
+$insertbefore = Get-Attr $params "insertbefore" $FALSE;
+$create = Get-Attr $params "create" "no";
+$backup = Get-Attr $params "backup" "no";
+$validate = Get-Attr $params "validate" $FALSE;
+$encoding = Get-Attr $params "encoding" "auto";
+$newline = Get-Attr $params "newline" "windows";
+
+
+# Parse dest / name /destfile param aliases for compatibility with lineinfile
+# and fail if at least one spelling of the parameter is not provided.
+
+$dest = Get-Attr $params "dest" $FALSE;
+If ($dest -eq $FALSE) {
+ $dest = Get-Attr $params "name" $FALSE;
+ If ($dest -eq $FALSE) {
+ $dest = Get-Attr $params "destfile" $FALSE;
+ If ($dest -eq $FALSE) {
+ Fail-Json (New-Object psobject) "missing required argument: dest";
+ }
+ }
+}
+
+
+# Fail if the destination is not a file
+
+If (Test-Path $dest -pathType container) {
+ Fail-Json (New-Object psobject) "destination is a directory";
+}
+
+
+# Write lines to a file using the specified line separator and encoding,
+# performing validation if a validation command was specified.
+
+function WriteLines($outlines, $dest, $linesep, $encodingobj, $validate) {
+ $temppath = [System.IO.Path]::GetTempFileName();
+ $joined = $outlines -join $linesep;
+ [System.IO.File]::WriteAllText($temppath, $joined, $encodingobj);
+
+ If ($validate -ne $FALSE) {
+
+ If (!($validate -like "*%s*")) {
+ Fail-Json (New-Object psobject) "validate must contain %s: $validate";
+ }
+
+ $validate = $validate.Replace("%s", $temppath);
+
+ $parts = [System.Collections.ArrayList] $validate.Split(" ");
+ $cmdname = $parts[0];
+
+ $cmdargs = $validate.Substring($cmdname.Length + 1);
+
+ $process = [Diagnostics.Process]::Start($cmdname, $cmdargs);
+ $process.WaitForExit();
+
+ If ($process.ExitCode -ne 0) {
+ [string] $output = $process.StandardOutput.ReadToEnd();
+ [string] $error = $process.StandardError.ReadToEnd();
+ Remove-Item $temppath -force;
+ Fail-Json (New-Object psobject) "failed to validate $cmdname $cmdargs with error: $output $error";
+ }
+
+ }
+
+ # Commit changes to the destination file
+ $cleandest = $dest.Replace("/", "\");
+ Move-Item $temppath $cleandest -force;
+}
+
+
+# Backup the file specified with a date/time filename
+
+function BackupFile($path) {
+ $backuppath = $path + "." + [DateTime]::Now.ToString("yyyyMMdd-HHmmss");
+ Copy-Item $path $backuppath;
+ return $backuppath;
+}
+
+
+
+# Implement the functionality for state == 'present'
+
+function Present($dest, $regexp, $line, $insertafter, $insertbefore, $create, $backup, $backrefs, $validate, $encodingobj, $linesep) {
+
+ # Note that we have to clean up the dest path because ansible wants to treat / and \ as
+ # interchangable in windows pathnames, but .NET framework internals do not support that.
+ $cleandest = $dest.Replace("/", "\");
+
+ # Check if destination exists. If it does not exist, either create it if create == "yes"
+ # was specified or fail with a reasonable error message.
+ If (!(Test-Path $dest)) {
+ If ($create -eq "no") {
+ Fail-Json (New-Object psobject) "Destination $dest does not exist !";
+ }
+ # Create new empty file, using the specified encoding to write correct BOM
+ [System.IO.File]::WriteAllLines($cleandest, "", $encodingobj);
+ }
+
+ # Read the dest file lines using the indicated encoding into a mutable ArrayList.
+ $content = [System.IO.File]::ReadAllLines($cleandest, $encodingobj);
+ If ($content -eq $null) {
+ $lines = New-Object System.Collections.ArrayList;
+ }
+ Else {
+ $lines = [System.Collections.ArrayList] $content;
+ }
+
+ # Compile the regex specified, if provided
+ $mre = $FALSE;
+ If ($regexp -ne $FALSE) {
+ $mre = New-Object Regex $regexp, 'Compiled';
+ }
+
+ # Compile the regex for insertafter or insertbefore, if provided
+ $insre = $FALSE;
+
+ If ($insertafter -ne $FALSE -and $insertafter -ne "BOF" -and $insertafter -ne "EOF") {
+ $insre = New-Object Regex $insertafter, 'Compiled';
+ }
+ ElseIf ($insertbefore -ne $FALSE -and $insertbefore -ne "BOF") {
+ $insre = New-Object Regex $insertbefore, 'Compiled';
+ }
+
+ # index[0] is the line num where regexp has been found
+ # index[1] is the line num where insertafter/inserbefore has been found
+ $index = -1, -1;
+ $lineno = 0;
+
+ # The latest match object and matched line
+ $matched_line = "";
+ $m = $FALSE;
+
+ # Iterate through the lines in the file looking for matches
+ Foreach ($cur_line in $lines) {
+ If ($regexp -ne $FALSE) {
+ $m = $mre.Match($cur_line);
+ $match_found = $m.Success;
+ If ($match_found) {
+ $matched_line = $cur_line;
+ }
+ }
+ Else {
+ $match_found = $line -ceq $cur_line;
+ }
+ If ($match_found) {
+ $index[0] = $lineno;
+ }
+ ElseIf ($insre -ne $FALSE -and $insre.Match($cur_line).Success) {
+ If ($insertafter -ne $FALSE) {
+ $index[1] = $lineno + 1;
+ }
+ If ($insertbefore -ne $FALSE) {
+ $index[1] = $lineno;
+ }
+ }
+ $lineno = $lineno + 1;
+ }
+
+ $changed = $FALSE;
+ $msg = "";
+
+ If ($index[0] -ne -1) {
+ If ($backrefs -ne "no") {
+ $new_line = [regex]::Replace($matched_line, $regexp, $line);
+ }
+ Else {
+ $new_line = $line;
+ }
+ If ($lines[$index[0]] -cne $new_line) {
+ $lines[$index[0]] = $new_line;
+ $msg = "line replaced";
+ $changed = $TRUE;
+ }
+ }
+ ElseIf ($backrefs -ne "no") {
+ # No matches - no-op
+ }
+ ElseIf ($insertbefore -eq "BOF" -or $insertafter -eq "BOF") {
+ $lines.Insert(0, $line);
+ $msg = "line added";
+ $changed = $TRUE;
+ }
+ ElseIf ($insertafter -eq "EOF" -or $index[1] -eq -1) {
+ $lines.Add($line);
+ $msg = "line added";
+ $changed = $TRUE;
+ }
+ Else {
+ $lines.Insert($index[1], $line);
+ $msg = "line added";
+ $changed = $TRUE;
+ }
+
+ # Write backup file if backup == "yes"
+ $backupdest = "";
+
+ If ($changed -eq $TRUE -and $backup -eq "yes") {
+ $backupdest = BackupFile $dest;
+ }
+
+ # Write changes to the destination file if changes were made
+ If ($changed) {
+ WriteLines $lines $dest $linesep $encodingobj $validate;
+ }
+
+ $encodingstr = $encodingobj.WebName;
+
+ # Return result information
+ $result = New-Object psobject @{
+ changed = $changed
+ msg = $msg
+ backup = $backupdest
+ encoding = $encodingstr
+ }
+
+ Exit-Json $result;
+}
+
+
+# Implement the functionality for state == 'absent'
+
+function Absent($dest, $regexp, $line, $backup, $validate, $encodingobj, $linesep) {
+
+ # Check if destination exists. If it does not exist, fail with a reasonable error message.
+ If (!(Test-Path $dest)) {
+ Fail-Json (New-Object psobject) "Destination $dest does not exist !";
+ }
+
+ # Read the dest file lines using the indicated encoding into a mutable ArrayList. Note
+ # that we have to clean up the dest path because ansible wants to treat / and \ as
+ # interchangeable in windows pathnames, but .NET framework internals do not support that.
+
+ $cleandest = $dest.Replace("/", "\");
+ $content = [System.IO.File]::ReadAllLines($cleandest, $encodingobj);
+ If ($content -eq $null) {
+ $lines = New-Object System.Collections.ArrayList;
+ }
+ Else {
+ $lines = [System.Collections.ArrayList] $content;
+ }
+
+ # Initialize message to be returned on success
+ $msg = "";
+
+ # Compile the regex specified, if provided
+ $cre = $FALSE;
+ If ($regexp -ne $FALSE) {
+ $cre = New-Object Regex $regexp, 'Compiled';
+ }
+
+ $found = New-Object System.Collections.ArrayList;
+ $left = New-Object System.Collections.ArrayList;
+ $changed = $FALSE;
+
+ Foreach ($cur_line in $lines) {
+ If ($cre -ne $FALSE) {
+ $m = $cre.Match($cur_line);
+ $match_found = $m.Success;
+ }
+ Else {
+ $match_found = $line -ceq $cur_line;
+ }
+ If ($match_found) {
+ $found.Add($cur_line);
+ $changed = $TRUE;
+ }
+ Else {
+ $left.Add($cur_line);
+ }
+ }
+
+ # Write backup file if backup == "yes"
+ $backupdest = "";
+
+ If ($changed -eq $TRUE -and $backup -eq "yes") {
+ $backupdest = BackupFile $dest;
+ }
+
+ # Write changes to the destination file if changes were made
+ If ($changed) {
+ WriteLines $left $dest $linesep $encodingobj $validate;
+ }
+
+ # Return result information
+ $fcount = $found.Count;
+ $msg = "$fcount line(s) removed";
+ $encodingstr = $encodingobj.WebName;
+
+ $result = New-Object psobject @{
+ changed = $changed
+ msg = $msg
+ backup = $backupdest
+ found = $fcount
+ encoding = $encodingstr
+ }
+
+ Exit-Json $result;
+}
+
+
+# Default to windows line separator - probably most common
+
+$linesep = "`r`n";
+
+If ($newline -ne "windows") {
+ $linesep = "`n";
+}
+
+
+# Fix any CR/LF literals in the line argument. PS will not recognize either backslash
+# or backtick literals in the incoming string argument without this bit of black magic.
+
+If ($line -ne $FALSE) {
+ $line = $line.Replace("\r", "`r");
+ $line = $line.Replace("\n", "`n");
+}
+
+
+# Figure out the proper encoding to use for reading / writing the target file.
+
+# The default encoding is UTF-8 without BOM
+$encodingobj = [System.Text.UTF8Encoding] $FALSE;
+
+# If an explicit encoding is specified, use that instead
+If ($encoding -ne "auto") {
+ $encodingobj = [System.Text.Encoding]::GetEncoding($encoding);
+}
+
+# Otherwise see if we can determine the current encoding of the target file.
+# If the file doesn't exist yet (create == 'yes') we use the default or
+# explicitly specified encoding set above.
+Elseif (Test-Path $dest) {
+
+ # Get a sorted list of encodings with preambles, longest first
+
+ $max_preamble_len = 0;
+ $sortedlist = New-Object System.Collections.SortedList;
+ Foreach ($encodinginfo in [System.Text.Encoding]::GetEncodings()) {
+ $encoding = $encodinginfo.GetEncoding();
+ $plen = $encoding.GetPreamble().Length;
+ If ($plen -gt $max_preamble_len) {
+ $max_preamble_len = $plen;
+ }
+ If ($plen -gt 0) {
+ $sortedlist.Add(-($plen * 1000000 + $encoding.CodePage), $encoding);
+ }
+ }
+
+ # Get the first N bytes from the file, where N is the max preamble length we saw
+
+ [Byte[]]$bom = Get-Content -Encoding Byte -ReadCount $max_preamble_len -TotalCount $max_preamble_len -Path $dest;
+
+ # Iterate through the sorted encodings, looking for a full match.
+
+ $found = $FALSE;
+ Foreach ($encoding in $sortedlist.GetValueList()) {
+ $preamble = $encoding.GetPreamble();
+ If ($preamble) {
+ Foreach ($i in 0..$preamble.Length) {
+ If ($preamble[$i] -ne $bom[$i]) {
+ break;
+ }
+ Elseif ($i + 1 -eq $preamble.Length) {
+ $encodingobj = $encoding;
+ $found = $TRUE;
+ }
+ }
+ If ($found) {
+ break;
+ }
+ }
+ }
+}
+
+
+# Main dispatch - based on the value of 'state', perform argument validation and
+# call the appropriate handler function.
+
+If ($state -eq "present") {
+
+ If ( $backrefs -ne "no" -and $regexp -eq $FALSE ) {
+ Fail-Json (New-Object psobject) "regexp= is required with backrefs=true";
+ }
+
+ If ($line -eq $FALSE) {
+ Fail-Json (New-Object psobject) "line= is required with state=present";
+ }
+
+ If ($insertbefore -eq $FALSE -and $insertafter -eq $FALSE) {
+ $insertafter = "EOF";
+ }
+
+ Present $dest $regexp $line $insertafter $insertbefore $create $backup $backrefs $validate $encodingobj $linesep;
+
+}
+Else {
+
+ If ($regex -eq $FALSE -and $line -eq $FALSE) {
+ Fail-Json (New-Object psobject) "one of line= or regexp= is required with state=absent";
+ }
+
+ Absent $dest $regexp $line $backup $validate $encodingobj $linesep;
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/windows/win_lineinfile.py b/windows/win_lineinfile.py
new file mode 100644
index 00000000000..24d0afdef7b
--- /dev/null
+++ b/windows/win_lineinfile.py
@@ -0,0 +1,119 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+#
+# 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 .
+
+DOCUMENTATION = """
+---
+module: win_lineinfile
+author: "Brian Lloyd "
+short_description: Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression.
+description:
+ - This module will search a file for a line, and ensure that it is present or absent.
+ - This is primarily useful when you want to change a single line in a file only.
+version_added: "2.0"
+options:
+ dest:
+ required: true
+ aliases: [ name, destfile ]
+ description:
+ - The path of the file to modify.
+ regexp:
+ required: false
+ description:
+ - "The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found will be replaced. For C(state=absent), the pattern of the line to remove. Uses .NET compatible regular expressions; see U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx)."
+ state:
+ required: false
+ choices: [ present, absent ]
+ default: "present"
+ description:
+ - Whether the line should be there or not.
+ line:
+ required: false
+ description:
+ - Required for C(state=present). The line to insert/replace into the file. If C(backrefs) is set, may contain backreferences that will get expanded with the C(regexp) capture groups if the regexp matches.
+ backrefs:
+ required: false
+ default: "no"
+ choices: [ "yes", "no" ]
+ description:
+ - Used with C(state=present). If set, line can contain backreferences (both positional and named) that will get populated if the C(regexp) matches. This flag changes the operation of the module slightly; C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp) doesn't match anywhere in the file, the file will be left unchanged.
+ - If the C(regexp) does match, the last matching line will be replaced by the expanded line parameter.
+ insertafter:
+ required: false
+ default: EOF
+ description:
+ - Used with C(state=present). If specified, the line will be inserted after the last match of specified regular expression. A special value is available; C(EOF) for inserting the line at the end of the file.
+ - If specified regular expresion has no matches, EOF will be used instead. May not be used with C(backrefs).
+ choices: [ 'EOF', '*regex*' ]
+ insertbefore:
+ required: false
+ description:
+ - Used with C(state=present). If specified, the line will be inserted before the last match of specified regular expression. A value is available; C(BOF) for inserting the line at the beginning of the file.
+ - If specified regular expresion has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs).
+ choices: [ 'BOF', '*regex*' ]
+ create:
+ required: false
+ choices: [ "yes", "no" ]
+ default: "no"
+ description:
+ - Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing.
+ backup:
+ required: false
+ default: "no"
+ choices: [ "yes", "no" ]
+ description:
+ - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly.
+ validate:
+ required: false
+ description:
+ - Validation to run before copying into place. Use %s in the command to indicate the current file to validate.
+ - The command is passed securely so shell features like expansion and pipes won't work.
+ required: false
+ default: None
+ encoding:
+ required: false
+ default: "auto"
+ description:
+ - Specifies the encoding of the source text file to operate on (and thus what the output encoding will be). The default of C(auto) will cause the module to auto-detect the encoding of the source file and ensure that the modified file is written with the same encoding.
+ - "An explicit encoding can be passed as a string that is a valid value to pass to the .NET framework System.Text.Encoding.GetEncoding() method - see U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx)."
+ - This is mostly useful with C(create=yes) if you want to create a new file with a specific encoding. If C(create=yes) is specified without a specific encoding, the default encoding (UTF-8, no BOM) will be used.
+ newline:
+ required: false
+ description:
+ - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (\r\n). Note that the indicated line separator will be used for file output regardless of the original line seperator that appears in the input file."
+ choices: [ "windows", "unix" ]
+ default: "windows"
+"""
+
+EXAMPLES = """
+- win_lineinfile: dest=C:\\temp\\example.conf regexp=^name= line="name=JohnDoe"
+
+- win_lineinfile: dest=C:\\temp\\example.conf state=absent regexp="^name="
+
+- win_lineinfile: dest=C:\\temp\\example.conf regexp='^127\.0\.0\.1' line='127.0.0.1 localhost'
+
+- win_lineinfile: dest=C:\\temp\\httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080"
+
+- win_lineinfile: dest=C:\\temp\\services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default"
+
+# Create file if it doesnt exist with a specific encoding
+- win_lineinfile: dest=C:\\temp\\utf16.txt create="yes" encoding="utf-16" line="This is a utf-16 encoded file"
+
+# Add a line to a file and ensure the resulting file uses unix line separators
+- win_lineinfile: dest=C:\\temp\\testfile.txt line="Line added to file" newline="unix"
+
+"""
diff --git a/windows/win_msi.ps1 b/windows/win_msi.ps1
index 7467aa31daa..479848b4f3c 100644
--- a/windows/win_msi.ps1
+++ b/windows/win_msi.ps1
@@ -21,55 +21,47 @@
$params = Parse-Args $args;
-$result = New-Object psobject;
-Set-Attr $result "changed" $false;
+$path = Get-Attr $params "path" -failifempty $true
+$state = Get-Attr $params "state" "present"
+$creates = Get-Attr $params "creates" $false
+$extra_args = Get-Attr $params "extra_args" ""
$wait = $false
-If (-not $params.path.GetType)
+$result = New-Object psobject @{
+ changed = $false
+};
+
+If (($creates -ne $false) -and ($state -ne "absent") -and (Test-Path $creates))
{
- Fail-Json $result "missing required arguments: path"
+ Exit-Json $result;
}
-If ($params.wait -eq "true" -Or $params.wait -eq "yes")
+If ($params.wait.ToLower() -eq "true" -Or $params.wait.ToLower() -eq "yes")
{
$wait = $true
}
-$extra_args = ""
-If ($params.extra_args.GetType)
-{
- $extra_args = $params.extra_args;
-}
-
-If ($params.creates.GetType -and $params.state.GetType -and $params.state -ne "absent")
-{
- If (Test-File $creates)
- {
- Exit-Json $result;
- }
-}
-
$logfile = [IO.Path]::GetTempFileName();
-If ($params.state.GetType -and $params.state -eq "absent")
+if ($state -eq "absent")
{
If ($wait)
{
- Start-Process -FilePath msiexec.exe -ArgumentList "/x $params.path /qb /l $logfile $extra_args" -Verb Runas -Wait;
+ Start-Process -FilePath msiexec.exe -ArgumentList "/x $path /qn /l $logfile $extra_args" -Verb Runas -Wait;
}
Else
{
- Start-Process -FilePath msiexec.exe -ArgumentList "/x $params.path /qb /l $logfile $extra_args" -Verb Runas;
+ Start-Process -FilePath msiexec.exe -ArgumentList "/x $path /qn /l $logfile $extra_args" -Verb Runas;
}
}
Else
{
If ($wait)
{
- Start-Process -FilePath msiexec.exe -ArgumentList "/i $params.path /qb /l $logfile $extra_args" -Verb Runas -Wait;
+ Start-Process -FilePath msiexec.exe -ArgumentList "/i $path /qn /l $logfile $extra_args" -Verb Runas -Wait;
}
Else
{
- Start-Process -FilePath msiexec.exe -ArgumentList "/i $params.path /qb /l $logfile $extra_args" -Verb Runas;
+ Start-Process -FilePath msiexec.exe -ArgumentList "/i $path /qn /l $logfile $extra_args" -Verb Runas;
}
}
@@ -80,4 +72,4 @@ Remove-Item $logfile;
Set-Attr $result "log" $logcontents;
-Exit-Json $result;
+Exit-Json $result;
\ No newline at end of file
diff --git a/windows/win_ping.ps1 b/windows/win_ping.ps1
index 98f1415e290..a4dd60ef19b 100644
--- a/windows/win_ping.ps1
+++ b/windows/win_ping.ps1
@@ -17,7 +17,7 @@
# WANT_JSON
# POWERSHELL_COMMON
-$params = Parse-Args $args;
+$params = Parse-Args $args $true;
$data = Get-Attr $params "data" "pong";
diff --git a/windows/win_service.ps1 b/windows/win_service.ps1
index a70d82a4ef3..4ea4e2697a1 100644
--- a/windows/win_service.ps1
+++ b/windows/win_service.ps1
@@ -24,26 +24,25 @@ $params = Parse-Args $args;
$result = New-Object PSObject;
Set-Attr $result "changed" $false;
-If (-not $params.name.GetType)
-{
- Fail-Json $result "missing required arguments: name"
-}
+$name = Get-Attr $params "name" -failifempty $true
+$state = Get-Attr $params "state" $false
+$startMode = Get-Attr $params "start_mode" $false
-If ($params.state) {
- $state = $params.state.ToString().ToLower()
+If ($state) {
+ $state = $state.ToString().ToLower()
If (($state -ne 'started') -and ($state -ne 'stopped') -and ($state -ne 'restarted')) {
Fail-Json $result "state is '$state'; must be 'started', 'stopped', or 'restarted'"
}
}
-If ($params.start_mode) {
- $startMode = $params.start_mode.ToString().ToLower()
+If ($startMode) {
+ $startMode = $startMode.ToString().ToLower()
If (($startMode -ne 'auto') -and ($startMode -ne 'manual') -and ($startMode -ne 'disabled')) {
Fail-Json $result "start mode is '$startMode'; must be 'auto', 'manual', or 'disabled'"
}
}
-$svcName = $params.name
+$svcName = $name
$svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue
If (-not $svc) {
Fail-Json $result "Service '$svcName' not installed"
diff --git a/windows/win_stat.ps1 b/windows/win_stat.ps1
index 10101a62b30..af9cbd7eca5 100644
--- a/windows/win_stat.ps1
+++ b/windows/win_stat.ps1
@@ -17,7 +17,12 @@
# WANT_JSON
# POWERSHELL_COMMON
-$params = Parse-Args $args;
+$params = Parse-Args $args $true;
+
+function Date_To_Timestamp($start_date, $end_date)
+{
+ Write-Output (New-TimeSpan -Start $start_date -End $end_date).TotalSeconds
+}
$path = Get-Attr $params "path" $FALSE;
If ($path -eq $FALSE)
@@ -36,15 +41,22 @@ If (Test-Path $path)
{
Set-Attr $result.stat "exists" $TRUE;
$info = Get-Item $path;
- If ($info.Directory) # Only files have the .Directory attribute.
+ $epoch_date = Get-Date -Date "01/01/1970"
+ If ($info.PSIsContainer)
+ {
+ Set-Attr $result.stat "isdir" $TRUE;
+ }
+ Else
{
Set-Attr $result.stat "isdir" $FALSE;
Set-Attr $result.stat "size" $info.Length;
}
- Else
- {
- Set-Attr $result.stat "isdir" $TRUE;
- }
+ Set-Attr $result.stat "extension" $info.Extension;
+ Set-Attr $result.stat "attributes" $info.Attributes.ToString();
+ Set-Attr $result.stat "owner" $info.GetAccessControl().Owner;
+ Set-Attr $result.stat "creationtime" (Date_To_Timestamp $epoch_date $info.CreationTime);
+ Set-Attr $result.stat "lastaccesstime" (Date_To_Timestamp $epoch_date $info.LastAccessTime);
+ Set-Attr $result.stat "lastwritetime" (Date_To_Timestamp $epoch_date $info.LastWriteTime);
}
Else
{
diff --git a/windows/win_template.py b/windows/win_template.py
index c384ad7775f..e8323362dd6 100644
--- a/windows/win_template.py
+++ b/windows/win_template.py
@@ -1,5 +1,20 @@
# this is a virtual module that is entirely implemented server side
+# 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 .
+
DOCUMENTATION = '''
---
module: win_template
diff --git a/windows/win_user.ps1 b/windows/win_user.ps1
index ae4847a8528..ac40ced2cbc 100644
--- a/windows/win_user.ps1
+++ b/windows/win_user.ps1
@@ -16,7 +16,6 @@
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see .
-# WANT_JSON
# POWERSHELL_COMMON
########
@@ -55,33 +54,21 @@ $result = New-Object psobject @{
changed = $false
};
-If (-not $params.name.GetType) {
- Fail-Json $result "missing required arguments: name"
-}
-
-$username = Get-Attr $params "name"
+$username = Get-Attr $params "name" -failifempty $true
$fullname = Get-Attr $params "fullname"
$description = Get-Attr $params "description"
$password = Get-Attr $params "password"
-If ($params.state) {
- $state = $params.state.ToString().ToLower()
- If (($state -ne 'present') -and ($state -ne 'absent') -and ($state -ne 'query')) {
- Fail-Json $result "state is '$state'; must be 'present', 'absent' or 'query'"
- }
-}
-ElseIf (!$params.state) {
- $state = "present"
+$state = Get-Attr $params "state" "present"
+$state = $state.ToString().ToLower()
+If (($state -ne 'present') -and ($state -ne 'absent') -and ($state -ne 'query')) {
+ Fail-Json $result "state is '$state'; must be 'present', 'absent' or 'query'"
}
-If ($params.update_password) {
- $update_password = $params.update_password.ToString().ToLower()
- If (($update_password -ne 'always') -and ($update_password -ne 'on_create')) {
- Fail-Json $result "update_password is '$update_password'; must be 'always' or 'on_create'"
- }
-}
-ElseIf (!$params.update_password) {
- $update_password = "always"
+$update_password = Get-Attr $params "update_password" "always"
+$update_password = $update_password.ToString().ToLower()
+If (($update_password -ne 'always') -and ($update_password -ne 'on_create')) {
+ Fail-Json $result "update_password is '$update_password'; must be 'always' or 'on_create'"
}
$password_expired = Get-Attr $params "password_expired" $null
@@ -126,14 +113,10 @@ If ($groups -ne $null) {
}
}
-If ($params.groups_action) {
- $groups_action = $params.groups_action.ToString().ToLower()
- If (($groups_action -ne 'replace') -and ($groups_action -ne 'add') -and ($groups_action -ne 'remove')) {
- Fail-Json $result "groups_action is '$groups_action'; must be 'replace', 'add' or 'remove'"
- }
-}
-ElseIf (!$params.groups_action) {
- $groups_action = "replace"
+$groups_action = Get-Attr $params "groups_action" "replace"
+$groups_action = $groups_action.ToString().ToLower()
+If (($groups_action -ne 'replace') -and ($groups_action -ne 'add') -and ($groups_action -ne 'remove')) {
+ Fail-Json $result "groups_action is '$groups_action'; must be 'replace', 'add' or 'remove'"
}
$user_obj = Get-User $username
@@ -141,11 +124,12 @@ $user_obj = Get-User $username
If ($state -eq 'present') {
# Add or update user
try {
- If (!$user_obj.GetType) {
+ If (-not $user_obj -or -not $user_obj.GetType) {
$user_obj = $adsi.Create("User", $username)
If ($password -ne $null) {
$user_obj.SetPassword($password)
}
+ $user_obj.SetInfo()
$result.changed = $true
}
ElseIf (($password -ne $null) -and ($update_password -eq 'always')) {
@@ -199,13 +183,13 @@ If ($state -eq 'present') {
If ($result.changed) {
$user_obj.SetInfo()
}
- If ($groups.GetType) {
+ If ($null -ne $groups) {
[string[]]$current_groups = $user_obj.Groups() | ForEach { $_.GetType().InvokeMember("Name", "GetProperty", $null, $_, $null) }
If (($groups_action -eq "remove") -or ($groups_action -eq "replace")) {
ForEach ($grp in $current_groups) {
If ((($groups_action -eq "remove") -and ($groups -contains $grp)) -or (($groups_action -eq "replace") -and ($groups -notcontains $grp))) {
$group_obj = $adsi.Children | where { $_.SchemaClassName -eq 'Group' -and $_.Name -eq $grp }
- If ($group_obj.GetType) {
+ If ($group_obj -and $group_obj.GetType) {
$group_obj.Remove($user_obj.Path)
$result.changed = $true
}
@@ -238,7 +222,7 @@ If ($state -eq 'present') {
ElseIf ($state -eq 'absent') {
# Remove user
try {
- If ($user_obj.GetType) {
+ If ($user_obj -and $user_obj.GetType) {
$username = $user_obj.Name.Value
$adsi.delete("User", $user_obj.Name.Value)
$result.changed = $true
@@ -251,7 +235,7 @@ ElseIf ($state -eq 'absent') {
}
try {
- If ($user_obj.GetType) {
+ If ($user_obj -and $user_obj.GetType) {
$user_obj.RefreshCache()
Set-Attr $result "name" $user_obj.Name[0]
Set-Attr $result "fullname" $user_obj.FullName[0]