Resolving differences in core modules post-merge

This commit is contained in:
James Cammarata 2016-12-07 21:33:38 -05:00 committed by Matt Clay
commit 8afa090417
115 changed files with 2651 additions and 17913 deletions

View file

@ -39,35 +39,28 @@ options:
description: description:
- name of the cloudformation stack - name of the cloudformation stack
required: true required: true
default: null
aliases: []
disable_rollback: disable_rollback:
description: description:
- If a stacks fails to form, rollback will remove the stack - If a stacks fails to form, rollback will remove the stack
required: false required: false
default: "false" default: "false"
choices: [ "true", "false" ] choices: [ "true", "false" ]
aliases: []
template_parameters: template_parameters:
description: description:
- a list of hashes of all the template variables for the stack - a list of hashes of all the template variables for the stack
required: false required: false
default: {} default: {}
aliases: []
state: state:
description: 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 "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. If state is "absent", stack will be removed.
required: true required: true
default: null
aliases: []
template: template:
description: 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" - 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" 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 required: false
default: null default: null
aliases: []
notification_arns: notification_arns:
description: description:
- The Simple Notification Service (SNS) topic ARNs to publish stack related events. - The Simple Notification Service (SNS) topic ARNs to publish stack related events.
@ -79,14 +72,12 @@ options:
- the path of the cloudformation stack policy. A policy cannot be removed once placed, but it can be modified. (for instance, [allow all updates](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051) - the path of the cloudformation stack policy. A policy cannot be removed once placed, but it can be modified. (for instance, [allow all updates](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051)
required: false required: false
default: null default: null
aliases: []
version_added: "1.9" version_added: "1.9"
tags: tags:
description: description:
- Dictionary of tags to associate with stack and its resources during stack creation. Can be updated later, updating tags removes previous entries. - Dictionary of tags to associate with stack and its resources during stack creation. Can be updated later, updating tags removes previous entries.
required: false required: false
default: null default: null
aliases: []
version_added: "1.4" version_added: "1.4"
template_url: template_url:
description: description:

View file

@ -56,7 +56,7 @@ options:
region: region:
version_added: "1.2" version_added: "1.2"
description: description:
- The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. - The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used. See U(http://docs.aws.amazon.com/general/latest/gr/rande.html#ec2_region)
required: false required: false
default: null default: null
aliases: [ 'aws_region', 'ec2_region' ] aliases: [ 'aws_region', 'ec2_region' ]
@ -69,16 +69,17 @@ options:
aliases: [ 'aws_zone', 'ec2_zone' ] aliases: [ 'aws_zone', 'ec2_zone' ]
instance_type: instance_type:
description: description:
- instance type to use for the instance - instance type to use for the instance, see U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-types.html)
required: true required: true
default: null default: null
aliases: [] aliases: []
tenancy: tenancy:
version_added: "1.9" version_added: "1.9"
description: description:
- An instance with a tenancy of "dedicated" runs on single-tenant hardware and can only be launched into a VPC. Valid values are "default" or "dedicated". Note that to use dedicated tenancy you MUST specify a vpc_subnet_id as well. Dedicated tenancy is not available for EC2 "micro" instances. - An instance with a tenancy of "dedicated" runs on single-tenant hardware and can only be launched into a VPC. Note that to use dedicated tenancy you MUST specify a vpc_subnet_id as well. Dedicated tenancy is not available for EC2 "micro" instances.
required: false required: false
default: default default: default
choices: [ "default", "dedicated" ]
aliases: [] aliases: []
spot_price: spot_price:
version_added: "1.5" version_added: "1.5"
@ -143,6 +144,7 @@ options:
- enable detailed monitoring (CloudWatch) for instance - enable detailed monitoring (CloudWatch) for instance
required: false required: false
default: null default: null
choices: [ "yes", "no" ]
aliases: [] aliases: []
user_data: user_data:
version_added: "0.9" version_added: "0.9"
@ -178,6 +180,7 @@ options:
- when provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+ - when provisioning within vpc, assign a public IP address. Boto library must be 2.13.0+
required: false required: false
default: null default: null
choices: [ "yes", "no" ]
aliases: [] aliases: []
private_ip: private_ip:
version_added: "1.2" version_added: "1.2"

View file

@ -51,12 +51,6 @@ options:
- create or deregister/delete image - create or deregister/delete image
required: false required: false
default: 'present' default: 'present'
region:
description:
- The AWS region to use. Must be specified if ec2_url is not used. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
default: null
aliases: [ 'aws_region', 'ec2_region' ]
description: description:
description: description:
- An optional human-readable string describing the contents and purpose of the AMI. - An optional human-readable string describing the contents and purpose of the AMI.
@ -74,10 +68,10 @@ options:
required: false required: false
default: null default: null
device_mapping: device_mapping:
version_added: "1.9" version_added: "2.0"
description: description:
- An optional list of device hashes/dictionaries with custom configurations (same block-device-mapping parameters) - An optional list of device hashes/dictionaries with custom configurations (same block-device-mapping parameters)
- Valid properties include: device_name, volume_type, size (in GB), delete_on_termination (boolean), no_device (boolean), snapshot_id, iops (for io1 volume_type) - "Valid properties include: device_name, volume_type, size (in GB), delete_on_termination (boolean), no_device (boolean), snapshot_id, iops (for io1 volume_type)"
required: false required: false
default: null default: null
delete_snapshot: delete_snapshot:
@ -305,6 +299,7 @@ import time
try: try:
import boto import boto
import boto.ec2 import boto.ec2
from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
HAS_BOTO = True HAS_BOTO = True
except ImportError: except ImportError:
HAS_BOTO = False HAS_BOTO = False
@ -365,6 +360,7 @@ def create_image(module, ec2):
wait_timeout = int(module.params.get('wait_timeout')) wait_timeout = int(module.params.get('wait_timeout'))
description = module.params.get('description') description = module.params.get('description')
no_reboot = module.params.get('no_reboot') no_reboot = module.params.get('no_reboot')
device_mapping = module.params.get('device_mapping')
tags = module.params.get('tags') tags = module.params.get('tags')
launch_permissions = module.params.get('launch_permissions') launch_permissions = module.params.get('launch_permissions')

View file

@ -22,7 +22,7 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: ec2_ami_find 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 short_description: Searches for AMIs to obtain the AMI ID and other information
description: description:
- Returns list of matching AMIs with AMI ID, along with other useful information - Returns list of matching AMIs with AMI ID, along with other useful information

View file

@ -69,7 +69,7 @@ options:
required: false required: false
replace_all_instances: replace_all_instances:
description: description:
- In a rolling fashion, replace all instances with an old launch configuration with one from the current launch configuraiton. - In a rolling fashion, replace all instances with an old launch configuration with one from the current launch configuration.
required: false required: false
version_added: "1.8" version_added: "1.8"
default: False default: False
@ -91,11 +91,6 @@ options:
required: false required: false
version_added: "1.8" version_added: "1.8"
default: True default: True
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
aliases: ['aws_region', 'ec2_region']
vpc_zone_identifier: vpc_zone_identifier:
description: description:
- List of VPC subnets to use - List of VPC subnets to use
@ -142,7 +137,7 @@ options:
- An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity. - 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"i value is used. When used to change an existent autoscaling group, the current termination policies are maintained. - For 'Default', when used to create a new autoscaling group, the "Default"i value is used. When used to change an existent autoscaling group, the current termination policies are maintained.
required: false required: false
default: Default. Eg, 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 default: Default
choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default'] choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default']
version_added: "2.0" version_added: "2.0"
notification_topic: notification_topic:

View file

@ -47,12 +47,6 @@ options:
required: false required: false
choices: ['present', 'absent'] choices: ['present', 'absent']
default: present default: present
region:
description:
- the EC2 region to use
required: false
default: null
aliases: [ ec2_region ]
in_vpc: in_vpc:
description: description:
- allocate an EIP inside a VPC or not - allocate an EIP inside a VPC or not

View file

@ -45,11 +45,6 @@ options:
- List of ELB names, required for registration. The ec2_elbs fact should be used if there was a previous de-register. - List of ELB names, required for registration. The ec2_elbs fact should be used if there was a previous de-register.
required: false required: false
default: None default: None
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
aliases: ['aws_region', 'ec2_region']
enable_availability_zone: enable_availability_zone:
description: description:
- Whether to enable the availability zone of the instance on the target ELB if the availability zone has not already - Whether to enable the availability zone of the instance on the target ELB if the availability zone has not already
@ -77,7 +72,9 @@ options:
required: false required: false
default: 0 default: 0
version_added: "1.6" version_added: "1.6"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
""" """
EXAMPLES = """ EXAMPLES = """

View file

@ -82,7 +82,7 @@ options:
version_added: "2.0" version_added: "2.0"
health_check: health_check:
description: description:
- An associative array of health check configuration settigs (see example) - An associative array of health check configuration settings (see example)
require: false require: false
default: None default: None
access_logs: access_logs:
@ -131,7 +131,7 @@ options:
version_added: "2.0" version_added: "2.0"
cross_az_load_balancing: cross_az_load_balancing:
description: description:
- Distribute load across all configured Availablity Zones - Distribute load across all configured Availability Zones
required: false required: false
default: "no" default: "no"
choices: ["yes", "no"] choices: ["yes", "no"]
@ -163,7 +163,9 @@ options:
required: false required: false
version_added: "2.1" version_added: "2.1"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
""" """
EXAMPLES = """ EXAMPLES = """
@ -271,7 +273,7 @@ EXAMPLES = """
purge_listeners: no purge_listeners: no
# Normally, this module will leave availability zones that are enabled # Normally, this module will leave availability zones that are enabled
# on the ELB alone. If purge_zones is true, then any extreneous zones # on the ELB alone. If purge_zones is true, then any extraneous zones
# will be removed # will be removed
- local_action: - local_action:
module: ec2_elb_lb module: ec2_elb_lb
@ -762,7 +764,7 @@ class ElbManager(object):
# Does it match exactly? # Does it match exactly?
if listener_as_tuple != existing_listener_found: if listener_as_tuple != existing_listener_found:
# The ports are the same but something else is different, # The ports are the same but something else is different,
# so we'll remove the exsiting one and add the new one # so we'll remove the existing one and add the new one
listeners_to_remove.append(existing_listener_found) listeners_to_remove.append(existing_listener_found)
listeners_to_add.append(listener_as_tuple) listeners_to_add.append(listener_as_tuple)
else: else:

View file

@ -33,7 +33,7 @@ options:
required: false required: false
default: 'yes' default: 'yes'
choices: ['yes', 'no'] choices: ['yes', 'no']
version_added: 1.5.1 version_added: '1.5.1'
description: description:
- This module fetches data from the metadata servers in ec2 (aws) as per - 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. http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html.

View file

@ -49,12 +49,6 @@ options:
- List of firewall outbound rules to enforce in this group (see example). If none are supplied, a default all-out rule is assumed. If an empty list is supplied, no outbound rules will be enabled. - List of firewall outbound rules to enforce in this group (see example). If none are supplied, a default all-out rule is assumed. If an empty list is supplied, no outbound rules will be enabled.
required: false required: false
version_added: "1.6" version_added: "1.6"
region:
description:
- the EC2 region to use
required: false
default: null
aliases: []
state: state:
version_added: "1.4" version_added: "1.4"
description: description:
@ -78,7 +72,9 @@ options:
default: 'true' default: 'true'
aliases: [] aliases: []
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
notes: notes:
- If a rule declares a group_name and that group doesn't exist, it will be - If a rule declares a group_name and that group doesn't exist, it will be

View file

@ -35,12 +35,6 @@ options:
description: description:
- Public key material. - Public key material.
required: false required: false
region:
description:
- the EC2 region to use
required: false
default: null
aliases: []
state: state:
description: description:
- create or delete keypair - create or delete keypair
@ -62,7 +56,9 @@ options:
aliases: [] aliases: []
version_added: "1.6" version_added: "1.6"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
author: "Vincent Viallet (@zbal)" author: "Vincent Viallet (@zbal)"
''' '''

View file

@ -59,11 +59,6 @@ options:
description: description:
- A list of security groups to apply to the instances. For VPC instances, specify security group IDs. For EC2-Classic, specify either security group names or IDs. - A list of security groups to apply to the instances. For VPC instances, specify security group IDs. For EC2-Classic, specify either security group names or IDs.
required: false required: false
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
aliases: ['aws_region', 'ec2_region']
volumes: volumes:
description: description:
- a list of volume dicts, each containing device name and optionally ephemeral id or snapshot id. Size and type (and number of iops for io device type) must be specified for a new volume or a root volume, and may be passed for a snapshot volume. For any volume, a volume size less than 1 will be interpreted as a request not to create the volume. - a list of volume dicts, each containing device name and optionally ephemeral id or snapshot id. Size and type (and number of iops for io device type) must be specified for a new volume or a root volume, and may be passed for a snapshot volume. For any volume, a volume size less than 1 will be interpreted as a request not to create the volume.
@ -110,7 +105,7 @@ options:
- Id of ClassicLink enabled VPC - Id of ClassicLink enabled VPC
required: false required: false
version_added: "2.0" version_added: "2.0"
classic_link_vpc_security_groups" classic_link_vpc_security_groups:
description: description:
- A list of security group id's with which to associate the ClassicLink VPC instances. - A list of security group id's with which to associate the ClassicLink VPC instances.
required: false required: false

View file

@ -33,7 +33,7 @@ options:
required: true required: true
choices: ['present', 'absent'] choices: ['present', 'absent']
name: name:
desciption: description:
- Unique name for the alarm - Unique name for the alarm
required: true required: true
metric: metric:
@ -75,7 +75,7 @@ options:
options: ['Seconds','Microseconds','Milliseconds','Bytes','Kilobytes','Megabytes','Gigabytes','Terabytes','Bits','Kilobits','Megabits','Gigabits','Terabits','Percent','Count','Bytes/Second','Kilobytes/Second','Megabytes/Second','Gigabytes/Second','Terabytes/Second','Bits/Second','Kilobits/Second','Megabits/Second','Gigabits/Second','Terabits/Second','Count/Second','None'] options: ['Seconds','Microseconds','Milliseconds','Bytes','Kilobytes','Megabytes','Gigabytes','Terabytes','Bits','Kilobits','Megabits','Gigabits','Terabits','Percent','Count','Bytes/Second','Kilobytes/Second','Megabytes/Second','Gigabytes/Second','Terabytes/Second','Bits/Second','Kilobits/Second','Megabits/Second','Gigabits/Second','Terabits/Second','Count/Second','None']
description: description:
description: description:
- A longer desciption of the alarm - A longer description of the alarm
required: false required: false
dimensions: dimensions:
description: description:
@ -93,7 +93,9 @@ options:
description: description:
- A list of the names of action(s) to take when the alarm is in the 'ok' status - A list of the names of action(s) to take when the alarm is in the 'ok' status
required: false required: false
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
""" """
EXAMPLES = ''' EXAMPLES = '''

View file

@ -41,7 +41,7 @@ options:
- Name of the associated autoscaling group - Name of the associated autoscaling group
required: true required: true
adjustment_type: adjustment_type:
desciption: description:
- The type of change in capacity of the autoscaling group - The type of change in capacity of the autoscaling group
required: false required: false
choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity'] choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity']
@ -57,7 +57,9 @@ options:
description: description:
- The minimum period of time between which autoscaling actions can take place - The minimum period of time between which autoscaling actions can take place
required: false required: false
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
""" """
EXAMPLES = ''' EXAMPLES = '''

View file

@ -26,11 +26,6 @@ description:
- creates an EC2 snapshot from an existing EBS volume - creates an EC2 snapshot from an existing EBS volume
version_added: "1.5" version_added: "1.5"
options: options:
region:
description:
- The AWS region to use. If not specified then the value of the EC2_REGION environment variable, if any, is used.
required: false
aliases: ['aws_region', 'ec2_region']
volume_id: volume_id:
description: description:
- volume from which to take the snapshot - volume from which to take the snapshot
@ -86,7 +81,9 @@ options:
version_added: "2.0" version_added: "2.0"
author: "Will Thames (@willthames)" author: "Will Thames (@willthames)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -26,12 +26,6 @@ description:
- Creates, removes and lists tags from any EC2 resource. The resource is referenced by its resource id (e.g. an instance being i-XXXXXXX). It is designed to be used with complex args (tags), see the examples. This module has a dependency on python-boto. - Creates, removes and lists tags from any EC2 resource. The resource is referenced by its resource id (e.g. an instance being i-XXXXXXX). It is designed to be used with complex args (tags), see the examples. This module has a dependency on python-boto.
version_added: "1.3" version_added: "1.3"
options: options:
region:
description:
- region in which the resource exists.
required: false
default: null
aliases: ['aws_region', 'ec2_region']
resource: resource:
description: description:
- The EC2 resource id. - The EC2 resource id.
@ -53,7 +47,9 @@ options:
aliases: [] aliases: []
author: "Lester Wade (@lwade)" author: "Lester Wade (@lwade)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -105,7 +105,9 @@ options:
choices: ['absent', 'present', 'list'] choices: ['absent', 'present', 'list']
version_added: "1.6" version_added: "1.6"
author: "Lester Wade (@lwade)" author: "Lester Wade (@lwade)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -91,7 +91,9 @@ options:
required: true required: true
choices: [ "present", "absent" ] choices: [ "present", "absent" ]
author: "Carson Gee (@carsongee)" author: "Carson Gee (@carsongee)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -476,6 +478,7 @@ def create_vpc(module, vpc_conn):
# Handle Internet gateway (create/delete igw) # Handle Internet gateway (create/delete igw)
igw = None igw = None
igw_id = None
igws = vpc_conn.get_all_internet_gateways(filters={'attachment.vpc-id': vpc.id}) igws = vpc_conn.get_all_internet_gateways(filters={'attachment.vpc-id': vpc.id})
if len(igws) > 1: if len(igws) > 1:
module.fail_json(msg='EC2 returned more than one Internet Gateway for id %s, aborting' % vpc.id) module.fail_json(msg='EC2 returned more than one Internet Gateway for id %s, aborting' % vpc.id)
@ -499,6 +502,9 @@ def create_vpc(module, vpc_conn):
except EC2ResponseError as e: except EC2ResponseError as e:
module.fail_json(msg='Unable to delete Internet Gateway, error: {0}'.format(e)) module.fail_json(msg='Unable to delete Internet Gateway, error: {0}'.format(e))
if igw is not None:
igw_id = igw.id
# Handle route tables - this may be worth splitting into a # Handle route tables - this may be worth splitting into a
# different module but should work fine here. The strategy to stay # different module but should work fine here. The strategy to stay
# idempotent is to basically build all the route tables as # idempotent is to basically build all the route tables as
@ -602,6 +608,7 @@ def create_vpc(module, vpc_conn):
module.fail_json(msg='Unable to delete old route table {0}, error: {1}'.format(rt.id, e)) module.fail_json(msg='Unable to delete old route table {0}, error: {1}'.format(rt.id, e))
vpc_dict = get_vpc_info(vpc) vpc_dict = get_vpc_info(vpc)
created_vpc_id = vpc.id created_vpc_id = vpc.id
returned_subnets = [] returned_subnets = []
current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id }) current_subnets = vpc_conn.get_all_subnets(filters={ 'vpc_id': vpc.id })
@ -624,7 +631,7 @@ def create_vpc(module, vpc_conn):
subnets_in_play = len(subnets) subnets_in_play = len(subnets)
returned_subnets.sort(key=lambda x: order.get(x['cidr'], subnets_in_play)) returned_subnets.sort(key=lambda x: order.get(x['cidr'], subnets_in_play))
return (vpc_dict, created_vpc_id, returned_subnets, changed) return (vpc_dict, created_vpc_id, returned_subnets, igw_id, changed)
def terminate_vpc(module, vpc_conn, vpc_id=None, cidr=None): def terminate_vpc(module, vpc_conn, vpc_id=None, cidr=None):
""" """
@ -723,6 +730,7 @@ def main():
else: else:
module.fail_json(msg="region must be specified") module.fail_json(msg="region must be specified")
igw_id = None
if module.params.get('state') == 'absent': if module.params.get('state') == 'absent':
vpc_id = module.params.get('vpc_id') vpc_id = module.params.get('vpc_id')
cidr = module.params.get('cidr_block') cidr = module.params.get('cidr_block')
@ -730,9 +738,9 @@ def main():
subnets_changed = None subnets_changed = None
elif module.params.get('state') == 'present': elif module.params.get('state') == 'present':
# Changed is always set to true when provisioning a new VPC # Changed is always set to true when provisioning a new VPC
(vpc_dict, new_vpc_id, subnets_changed, changed) = create_vpc(module, vpc_conn) (vpc_dict, new_vpc_id, subnets_changed, igw_id, changed) = create_vpc(module, vpc_conn)
module.exit_json(changed=changed, vpc_id=new_vpc_id, vpc=vpc_dict, subnets=subnets_changed) module.exit_json(changed=changed, vpc_id=new_vpc_id, vpc=vpc_dict, igw_id=igw_id, subnets=subnets_changed)
# import module snippets # import module snippets
from ansible.module_utils.basic import * from ansible.module_utils.basic import *

View file

@ -76,7 +76,9 @@ options:
default: false default: false
required: false required: false
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -74,7 +74,7 @@ options:
- The subnet group name to associate with. Only use if inside a vpc. Required if inside a vpc - The subnet group name to associate with. Only use if inside a vpc. Required if inside a vpc
required: false required: false
default: None default: None
version_added: "1.7" version_added: "2.0"
security_group_ids: security_group_ids:
description: description:
- A list of vpc security group names to associate with this cache cluster. Only use if inside a vpc - A list of vpc security group names to associate with this cache cluster. Only use if inside a vpc
@ -103,13 +103,9 @@ options:
required: false required: false
default: no default: no
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
region: extends_documentation_fragment:
description: - aws
- The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used. - ec2
required: true
default: null
aliases: ['aws_region', 'ec2_region']
extends_documentation_fragment: aws
""" """
EXAMPLES = """ EXAMPLES = """

View file

@ -31,34 +31,25 @@ options:
- Specifies whether the subnet should be present or absent. - Specifies whether the subnet should be present or absent.
required: true required: true
default: present default: present
aliases: []
choices: [ 'present' , 'absent' ] choices: [ 'present' , 'absent' ]
name: name:
description: description:
- Database subnet group identifier. - Database subnet group identifier.
required: true required: true
default: null
aliases: []
description: description:
description: description:
- Elasticache subnet group description. Only set when a new group is added. - Elasticache subnet group description. Only set when a new group is added.
required: false required: false
default: null default: null
aliases: []
subnets: subnets:
description: description:
- List of subnet IDs that make up the Elasticache subnet group. - List of subnet IDs that make up the Elasticache subnet group.
required: false required: false
default: null default: null
aliases: [] author: "Tim Mahoney (@timmahoney)"
region: extends_documentation_fragment:
description: - aws
- The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used. - ec2
required: true
default: null
aliases: ['aws_region', 'ec2_region']
author: Tim Mahoney
extends_documentation_fragment: aws
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -36,24 +36,22 @@ options:
description: description:
- Name of IAM resource to create or identify - Name of IAM resource to create or identify
required: true required: true
aliases: []
new_name: new_name:
description: description:
- When state is update, will replace name with new_name on IAM resource - When state is update, will replace name with new_name on IAM resource
required: false required: false
aliases: [] default: null
new_path: new_path:
description: description:
- When state is update, will replace the path with new_path on the IAM resource - When state is update, will replace the path with new_path on the IAM resource
required: false required: false
aliases: [] default: null
state: state:
description: description:
- Whether to create, delete or update the IAM resource. Note, roles cannot be updated. - Whether to create, delete or update the IAM resource. Note, roles cannot be updated.
required: true required: true
default: null default: null
choices: [ "present", "absent", "update" ] choices: [ "present", "absent", "update" ]
aliases: []
path: path:
description: description:
- When creating or updating, specify the desired path of the resource. If state is present, it will replace the current path to match what is passed in when they do not match. - When creating or updating, specify the desired path of the resource. If state is present, it will replace the current path to match what is passed in when they do not match.
@ -77,13 +75,11 @@ options:
required: false required: false
default: null default: null
choices: [ "create", "remove", "active", "inactive"] choices: [ "create", "remove", "active", "inactive"]
aliases: []
key_count: key_count:
description: description:
- When access_key_state is create it will ensure this quantity of keys are present. Defaults to 1. - When access_key_state is create it will ensure this quantity of keys are present. Defaults to 1.
required: false required: false
default: '1' default: '1'
aliases: []
access_key_ids: access_key_ids:
description: description:
- A list of the keys that you want impacted by the access_key_state parameter. - A list of the keys that you want impacted by the access_key_state parameter.
@ -92,13 +88,11 @@ options:
- A list of groups the user should belong to. When update, will gracefully remove groups not listed. - A list of groups the user should belong to. When update, will gracefully remove groups not listed.
required: false required: false
default: null default: null
aliases: []
password: password:
description: description:
- When type is user and state is present, define the users login password. Also works with update. Note that always returns changed. - When type is user and state is present, define the users login password. Also works with update. Note that always returns changed.
required: false required: false
default: null default: null
aliases: []
update_password: update_password:
required: false required: false
default: always default: always

View file

@ -89,7 +89,9 @@ options:
requirements: [ "boto" ] requirements: [ "boto" ]
author: Jonathan I. Davila author: Jonathan I. Davila
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -62,7 +62,9 @@ options:
notes: notes:
- 'Currently boto does not support the removal of Managed Policies, the module will not work removing/adding managed policies.' - 'Currently boto does not support the removal of Managed Policies, the module will not work removing/adding managed policies.'
author: "Jonathan I. Davila (@defionscode)" author: "Jonathan I. Davila (@defionscode)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -65,7 +65,9 @@ options:
default: null default: null
aliases: [] aliases: []
author: "Scott Anderson (@tastychutney)" author: "Scott Anderson (@tastychutney)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -51,14 +51,10 @@ options:
required: false required: false
default: null default: null
aliases: [] aliases: []
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']
author: "Scott Anderson (@tastychutney)" author: "Scott Anderson (@tastychutney)"
extends_documentation_fragment: aws extends_documentation_fragment:
- aws
- ec2
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -30,33 +30,26 @@ options:
description: description:
- Specifies the action to take. - Specifies the action to take.
required: true required: true
default: null
aliases: []
choices: [ 'get', 'create', 'delete' ] choices: [ 'get', 'create', 'delete' ]
zone: zone:
description: description:
- The DNS zone to modify - The DNS zone to modify
required: true required: true
default: null
aliases: []
hosted_zone_id: hosted_zone_id:
description: description:
- The Hosted Zone ID of the DNS zone to modify - The Hosted Zone ID of the DNS zone to modify
required: false required: false
version_added: 2.0 version_added: "2.0"
default: null default: null
record: record:
description: description:
- The full DNS record to create or delete - The full DNS record to create or delete
required: true required: true
default: null
aliases: []
ttl: ttl:
description: description:
- The TTL to give the new record - The TTL to give the new record
required: false required: false
default: 3600 (one hour) default: 3600 (one hour)
aliases: []
type: type:
description: description:
- The type of DNS record to create - The type of DNS record to create
@ -66,40 +59,36 @@ options:
description: description:
- Indicates if this is an alias record. - Indicates if this is an alias record.
required: false required: false
version_added: 1.9 version_added: "1.9"
default: False default: False
aliases: []
choices: [ 'True', 'False' ] choices: [ 'True', 'False' ]
alias_hosted_zone_id: alias_hosted_zone_id:
description: description:
- The hosted zone identifier. - The hosted zone identifier.
required: false required: false
version_added: 1.9 version_added: "1.9"
default: null default: null
alias_evaluate_target_health: alias_evaluate_target_health:
description: description:
- Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers. - Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers.
required: false required: false
version_added: "2.0" version_added: "2.1"
default: false default: false
value: value:
description: 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. - 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 required: false
default: null default: null
aliases: []
overwrite: overwrite:
description: description:
- Whether an existing record should be overwritten on create if values do not match - Whether an existing record should be overwritten on create if values do not match
required: false required: false
default: null default: null
aliases: []
retry_interval: retry_interval:
description: 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. - 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 required: false
default: 500 default: 500
aliases: []
private_zone: private_zone:
description: 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. - 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.
@ -316,6 +305,7 @@ import distutils.version
try: try:
import boto import boto
import boto.ec2
from boto import route53 from boto import route53
from boto.route53 import Route53Connection from boto.route53 import Route53Connection
from boto.route53.record import Record, ResourceRecordSets from boto.route53.record import Record, ResourceRecordSets
@ -496,26 +486,11 @@ def main():
except boto.exception.BotoServerError as e: except boto.exception.BotoServerError as e:
module.fail_json(msg = e.error_message) module.fail_json(msg = e.error_message)
# Get all the existing hosted zones and save their ID's # Find the named zone ID
zones = {} zone = get_zone_by_name(conn, module, zone_in, private_zone_in, hosted_zone_id_in, vpc_id_in)
results = conn.get_all_hosted_zones()
for r53zone in results['ListHostedZonesResponse']['HostedZones']:
# only save this zone id if the private status of the zone matches
# the private_zone_in boolean specified in the params
if module.boolean(r53zone['Config'].get('PrivateZone', False)) == private_zone_in:
zone_id = r53zone['Id'].replace('/hostedzone/', '')
# only save when unique hosted_zone_id is given and is equal
# hosted_zone_id_in is specified in the params
if hosted_zone_id_in and zone_id == hosted_zone_id_in:
zones[r53zone['Name']] = zone_id
elif not hosted_zone_id_in:
zones[r53zone['Name']] = zone_id
# Verify that the requested zone is already defined in Route53 # Verify that the requested zone is already defined in Route53
if not zone_in in zones and hosted_zone_id_in: if zone is None:
errmsg = "Hosted_zone_id %s does not exist in Route53" % hosted_zone_id_in
module.fail_json(msg = errmsg)
if not zone_in in zones:
errmsg = "Zone %s does not exist in Route53" % zone_in errmsg = "Zone %s does not exist in Route53" % zone_in
module.fail_json(msg = errmsg) module.fail_json(msg = errmsg)
@ -551,6 +526,8 @@ def main():
record['ttl'] = rset.ttl record['ttl'] = rset.ttl
record['value'] = ','.join(sorted(rset.resource_records)) record['value'] = ','.join(sorted(rset.resource_records))
record['values'] = 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['identifier'] = rset.identifier
record['weight'] = rset.weight record['weight'] = rset.weight
record['region'] = rset.region record['region'] = rset.region

View file

@ -584,7 +584,7 @@ def main():
if overwrite == 'always': if overwrite == 'always':
download_s3file(module, s3, bucket, obj, dest, retries, version=version) download_s3file(module, s3, bucket, obj, dest, retries, version=version)
else: else:
module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False) module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite=always parameter to force.", changed=False)
else: else:
sum_matches = False sum_matches = False
@ -594,7 +594,7 @@ def main():
module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.") module.exit_json(msg="WARNING: Checksums do not match. Use overwrite parameter to force download.")
# Firstly, if key_matches is TRUE and overwrite is not enabled, we EXIT with a helpful message. # Firstly, if key_matches is TRUE and overwrite is not enabled, we EXIT with a helpful message.
if sum_matches is True and overwrite is False: 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) module.exit_json(msg="Local and remote object are identical, ignoring. Use overwrite parameter to force.", changed=False)
# if our mode is a PUT operation (upload), go through the procedure as appropriate ... # if our mode is a PUT operation (upload), go through the procedure as appropriate ...

View file

@ -38,12 +38,12 @@ options:
default: null default: null
subscription_id: subscription_id:
description: description:
- azure subscription id. Overrides the AZURE_SUBSCRIPTION_ID environement variable. - azure subscription id. Overrides the AZURE_SUBSCRIPTION_ID environment variable.
required: false required: false
default: null default: null
management_cert_path: management_cert_path:
description: description:
- path to an azure management certificate associated with the subscription id. Overrides the AZURE_CERT_PATH environement variable. - path to an azure management certificate associated with the subscription id. Overrides the AZURE_CERT_PATH environment variable.
required: false required: false
default: null default: null
storage_account: storage_account:
@ -114,13 +114,6 @@ options:
required: false required: false
default: 'present' default: 'present'
aliases: [] aliases: []
reset_pass_atlogon:
description:
- Reset the admin password on first logon for windows hosts
required: false
default: "no"
version_added: "2.0"
choices: [ "yes", "no" ]
auto_updates: auto_updates:
description: description:
- Enable Auto Updates on Windows Machines - Enable Auto Updates on Windows Machines
@ -258,7 +251,7 @@ AZURE_ROLE_SIZES = ['ExtraSmall',
'Standard_DS14', 'Standard_DS14',
'Standard_G1', 'Standard_G1',
'Standard_G2', 'Standard_G2',
'Sandard_G3', 'Standard_G3',
'Standard_G4', 'Standard_G4',
'Standard_G5'] 'Standard_G5']
@ -516,7 +509,7 @@ def terminate_virtual_machine(module, azure):
def get_azure_creds(module): def get_azure_creds(module):
# Check modul args for credentials, then check environment vars # Check module args for credentials, then check environment vars
subscription_id = module.params.get('subscription_id') subscription_id = module.params.get('subscription_id')
if not subscription_id: if not subscription_id:
subscription_id = os.environ.get('AZURE_SUBSCRIPTION_ID', None) subscription_id = os.environ.get('AZURE_SUBSCRIPTION_ID', None)
@ -553,7 +546,6 @@ def main():
wait=dict(type='bool', default=False), wait=dict(type='bool', default=False),
wait_timeout=dict(default=600), wait_timeout=dict(default=600),
wait_timeout_redirects=dict(default=300), wait_timeout_redirects=dict(default=300),
reset_pass_atlogon=dict(type='bool', default=False),
auto_updates=dict(type='bool', default=False), auto_updates=dict(type='bool', default=False),
enable_winrm=dict(type='bool', default=True), enable_winrm=dict(type='bool', default=True),
) )

View file

@ -264,17 +264,17 @@ class AzureRMPublicIPAddress(AzureRMModuleBase):
def create_or_update_pip(self, pip): def create_or_update_pip(self, pip):
try: try:
poller = self.network_client.public_ip_addresses.create_or_update(self.resource_group, self.name, pip) poller = self.network_client.public_ip_addresses.create_or_update(self.resource_group, self.name, pip)
pip = self.get_poller_result(poller)
except Exception as exc: except Exception as exc:
self.fail("Error creating or updating {0} - {1}".format(self.name, str(exc))) self.fail("Error creating or updating {0} - {1}".format(self.name, str(exc)))
pip = self.get_poller_result(poller)
return pip_to_dict(pip) return pip_to_dict(pip)
def delete_pip(self): def delete_pip(self):
try: try:
poller = self.network_client.public_ip_addresses.delete(self.resource_group, self.name) poller = self.network_client.public_ip_addresses.delete(self.resource_group, self.name)
self.get_poller_result(poller)
except Exception as exc: except Exception as exc:
self.fail("Error deleting {0} - {1}".format(self.name, str(exc))) self.fail("Error deleting {0} - {1}".format(self.name, str(exc)))
self.get_poller_result(poller)
# Delete returns nada. If we get here, assume that all is well. # Delete returns nada. If we get here, assume that all is well.
self.results['state']['status'] = 'Deleted' self.results['state']['status'] = 'Deleted'
return True return True

View file

@ -74,11 +74,10 @@ options:
version_added: "1.5" version_added: "1.5"
ports: ports:
description: description:
- List containing private to public port mapping specification. Use docker - "List containing private to public port mapping specification.
- 'CLI-style syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000)' Use docker 'CLI-style syntax: C(8000), C(9000:8000), or C(0.0.0.0:9000:8000)'
- where 8000 is a container port, 9000 is a host port, and 0.0.0.0 is where 8000 is a container port, 9000 is a host port, and 0.0.0.0 is - a host interface.
- a host interface. The container ports need to be exposed either in the The container ports need to be exposed either in the Dockerfile or via the C(expose) option."
- Dockerfile or via the next option.
default: null default: null
version_added: "1.5" version_added: "1.5"
expose: expose:
@ -154,10 +153,7 @@ options:
description: description:
- RAM allocated to the container as a number of bytes or as a human-readable - RAM allocated to the container as a number of bytes or as a human-readable
string like "512MB". Leave as "0" to specify no limit. string like "512MB". Leave as "0" to specify no limit.
required: false default: 0
default: null
aliases: []
default: 256MB
docker_url: docker_url:
description: description:
- URL of the host running the docker daemon. This will default to the env - URL of the host running the docker daemon. This will default to the env
@ -878,7 +874,7 @@ class DockerManager(object):
we lack the capability. we lack the capability.
""" """
if not self._capabilities: if not self._capabilities:
self._check_capabilties() self._check_capabilities()
if capability in self._capabilities: if capability in self._capabilities:
return True return True
@ -1054,7 +1050,7 @@ class DockerManager(object):
elif p_len == 3: elif p_len == 3:
# Bind `container_port` of the container to port `parts[1]` on # Bind `container_port` of the container to port `parts[1]` on
# IP `parts[0]` of the host machine. If `parts[1]` empty bind # IP `parts[0]` of the host machine. If `parts[1]` empty bind
# to a dynamically allocacted port of IP `parts[0]`. # to a dynamically allocated port of IP `parts[0]`.
bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],) bind = (parts[0], int(parts[1])) if parts[1] else (parts[0],)
if container_port in binds: if container_port in binds:
@ -1643,23 +1639,10 @@ class DockerManager(object):
'name': self.module.params.get('name'), 'name': self.module.params.get('name'),
'stdin_open': self.module.params.get('stdin_open'), 'stdin_open': self.module.params.get('stdin_open'),
'tty': self.module.params.get('tty'), 'tty': self.module.params.get('tty'),
'volumes_from': self.module.params.get('volumes_from'),
'dns': self.module.params.get('dns'),
'cpuset': self.module.params.get('cpu_set'), 'cpuset': self.module.params.get('cpu_set'),
'cpu_shares': self.module.params.get('cpu_shares'), 'cpu_shares': self.module.params.get('cpu_shares'),
'user': self.module.params.get('docker_user'), 'user': self.module.params.get('docker_user'),
} }
if docker.utils.compare_version('1.10', self.client.version()['ApiVersion']) >= 0:
params['volumes_from'] = ""
if params['volumes_from'] is not None:
self.ensure_capability('volumes_from')
extra_params = {}
if self.module.params.get('insecure_registry'):
if self.ensure_capability('insecure_registry', fail=False):
extra_params['insecure_registry'] = self.module.params.get('insecure_registry')
if self.ensure_capability('host_config', fail=False): if self.ensure_capability('host_config', fail=False):
params['host_config'] = self.create_host_config() params['host_config'] = self.create_host_config()

View file

@ -31,25 +31,20 @@ options:
description: description:
- Bucket name. - Bucket name.
required: true required: true
default: null
aliases: []
object: object:
description: description:
- Keyname of the object inside the bucket. Can be also be used to create "virtual directories" (see examples). - Keyname of the object inside the bucket. Can be also be used to create "virtual directories" (see examples).
required: false required: false
default: null default: null
aliases: []
src: src:
description: description:
- The source file path when performing a PUT operation. - The source file path when performing a PUT operation.
required: false required: false
default: null default: null
aliases: []
dest: dest:
description: description:
- The destination file path when downloading an object/key with a GET operation. - The destination file path when downloading an object/key with a GET operation.
required: false required: false
aliases: []
force: force:
description: description:
- Forces an overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations. - Forces an overwrite either locally on the filesystem or remotely with the object/key. Used with PUT and GET operations.
@ -62,23 +57,21 @@ options:
required: false required: false
default: private default: private
headers: headers:
version_added: 2.0 version_added: "2.0"
description: description:
- Headers to attach to object. - Headers to attach to object.
required: false required: false
default: {} default: '{}'
expiration: expiration:
description: description:
- Time limit (in seconds) for the URL generated and returned by GCA when performing a mode=put or mode=get_url operation. This url is only avaialbe when public-read is the acl for the object. - Time limit (in seconds) for the URL generated and returned by GCA when performing a mode=put or mode=get_url operation. This url is only available when public-read is the acl for the object.
required: false required: false
default: null default: null
aliases: []
mode: mode:
description: description:
- Switches the module behaviour between upload, download, get_url (return download url) , get_str (download object as string), create (bucket) and delete (bucket). - Switches the module behaviour between upload, download, get_url (return download url) , get_str (download object as string), create (bucket) and delete (bucket).
required: true required: true
default: null default: null
aliases: []
choices: [ 'get', 'put', 'get_url', 'get_str', 'delete', 'create' ] choices: [ 'get', 'put', 'get_url', 'get_str', 'delete', 'create' ]
gs_secret_key: gs_secret_key:
description: description:

View file

@ -36,35 +36,30 @@ options:
- image string to use for the instance - image string to use for the instance
required: false required: false
default: "debian-7" default: "debian-7"
aliases: []
instance_names: instance_names:
description: description:
- a comma-separated list of instance names to create or destroy - a comma-separated list of instance names to create or destroy
required: false required: false
default: null default: null
aliases: []
machine_type: machine_type:
description: description:
- machine type to use for the instance, use 'n1-standard-1' by default - machine type to use for the instance, use 'n1-standard-1' by default
required: false required: false
default: "n1-standard-1" default: "n1-standard-1"
aliases: []
metadata: metadata:
description: description:
- a hash/dictionary of custom data for the instance; - a hash/dictionary of custom data for the instance;
'{"key":"value", ...}' '{"key":"value", ...}'
required: false required: false
default: null default: null
aliases: []
service_account_email: service_account_email:
version_added: 1.5.1 version_added: "1.5.1"
description: description:
- service account email - service account email
required: false required: false
default: null default: null
aliases: []
service_account_permissions: service_account_permissions:
version_added: 2.0 version_added: "2.0"
description: description:
- service account permissions (see - service account permissions (see
U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create), U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create),
@ -78,7 +73,7 @@ options:
"storage-rw", "taskqueue", "userinfo-email" "storage-rw", "taskqueue", "userinfo-email"
] ]
pem_file: pem_file:
version_added: 1.5.1 version_added: "1.5.1"
description: description:
- path to the pem file associated with the service account email - path to the pem file associated with the service account email
This option is deprecated. Use 'credentials_file'. This option is deprecated. Use 'credentials_file'.
@ -91,12 +86,11 @@ options:
default: null default: null
required: false required: false
project_id: project_id:
version_added: 1.5.1 version_added: "1.5.1"
description: description:
- your GCE project ID - your GCE project ID
required: false required: false
default: null default: null
aliases: []
name: name:
description: description:
- either a name of a single instance or when used with 'num_instances', - either a name of a single instance or when used with 'num_instances',
@ -126,7 +120,6 @@ options:
- if set, create the instance with a persistent boot disk - if set, create the instance with a persistent boot disk
required: false required: false
default: "false" default: "false"
aliases: []
disks: disks:
description: description:
- a list of persistent disks to attach to the instance; a string value - a list of persistent disks to attach to the instance; a string value
@ -135,7 +128,6 @@ options:
will be the boot disk (which must be READ_WRITE). will be the boot disk (which must be READ_WRITE).
required: false required: false
default: null default: null
aliases: []
version_added: "1.7" version_added: "1.7"
state: state:
description: description:
@ -148,13 +140,11 @@ options:
- a comma-separated list of tags to associate with the instance - a comma-separated list of tags to associate with the instance
required: false required: false
default: null default: null
aliases: []
zone: zone:
description: description:
- the GCE zone to use - the GCE zone to use
required: true required: true
default: "us-central1-a" default: "us-central1-a"
aliases: []
ip_forward: ip_forward:
version_added: "1.9" version_added: "1.9"
description: description:
@ -162,14 +152,12 @@ options:
gateways) gateways)
required: false required: false
default: "false" default: "false"
aliases: []
external_ip: external_ip:
version_added: "1.9" version_added: "1.9"
description: description:
- type of external ip, ephemeral by default; alternatively, a list of fixed gce ips or ip names can be given (if there is not enough specified ip, 'ephemeral' will be used). Specify 'none' if no external ip is desired. - type of external ip, ephemeral by default; alternatively, a list of fixed gce ips or ip names can be given (if there is not enough specified ip, 'ephemeral' will be used). Specify 'none' if no external ip is desired.
required: false required: false
default: "ephemeral" default: "ephemeral"
aliases: []
disk_auto_delete: disk_auto_delete:
version_added: "1.9" version_added: "1.9"
description: description:

View file

@ -26,7 +26,7 @@ module: gce_net
version_added: "1.5" version_added: "1.5"
short_description: create/destroy GCE networks and firewall rules short_description: create/destroy GCE networks and firewall rules
description: description:
- This module can create and destroy Google Compue Engine networks and - This module can create and destroy Google Compute Engine networks and
firewall rules U(https://developers.google.com/compute/docs/networking). firewall rules U(https://developers.google.com/compute/docs/networking).
The I(name) parameter is reserved for referencing a network while the The I(name) parameter is reserved for referencing a network while the
I(fwname) parameter is used to reference firewall rules. I(fwname) parameter is used to reference firewall rules.

View file

@ -116,7 +116,10 @@ options:
required: false required: false
default: publicURL default: publicURL
version_added: "1.7" version_added: "1.7"
requirements: ["glanceclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-glanceclient"
- "python-keystoneclient"
''' '''
@ -136,9 +139,14 @@ EXAMPLES = '''
import time import time
try: try:
import glanceclient import glanceclient
from keystoneclient.v2_0 import client as ksclient HAS_GLANCECLIENT = True
except ImportError: except ImportError:
print("failed=True msg='glanceclient and keystone client are required'") HAS_GLANCECLIENT = False
try:
from keystoneclient.v2_0 import client as ksclient
HAS_KEYSTONECLIENT = True
except ImportError:
HAS_KEYSTONECLIENT= False
def _get_ksclient(module, kwargs): def _get_ksclient(module, kwargs):
@ -269,4 +277,5 @@ def main():
# this is magic, see lib/ansible/module_common.py # this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -19,15 +19,16 @@
import operator import operator
import os import os
import time
try: try:
from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import client as nova_client
from novaclient.v1_1 import floating_ips from novaclient.v1_1 import floating_ips
from novaclient import exceptions from novaclient import exceptions
from novaclient import utils from novaclient import utils
import time HAS_NOVACLIENT = True
except ImportError: except ImportError:
print("failed=True msg='novaclient is required for this module'") HAS_NOVACLIENT = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -37,7 +38,7 @@ DOCUMENTATION = '''
--- ---
module: nova_compute module: nova_compute
version_added: "1.2" version_added: "1.2"
deprecated: Deprecated in 1.10. Use os_server instead deprecated: Deprecated in 2.0. Use os_server instead
short_description: Create/Delete VMs from OpenStack short_description: Create/Delete VMs from OpenStack
description: description:
- Create or Remove virtual machines from Openstack. - Create or Remove virtual machines from Openstack.
@ -179,7 +180,9 @@ options:
required: false required: false
default: None default: None
version_added: "1.9" version_added: "1.9"
requirements: ["novaclient"] requirements:
- "python >= 2.6"
- "python-novaclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -567,6 +570,9 @@ def main():
], ],
) )
if not HAS_NOVACLIENT:
module.fail_json(msg='python-novaclient is required for this module')
nova = nova_client.Client(module.params['login_username'], nova = nova_client.Client(module.params['login_username'],
module.params['login_password'], module.params['login_password'],
module.params['login_tenant_name'], module.params['login_tenant_name'],
@ -593,4 +599,5 @@ def main():
# this is magic, see lib/ansible/module_common.py # this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -17,12 +17,13 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>. # along with this software. If not, see <http://www.gnu.org/licenses/>.
import time
try: try:
from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import client as nova_client
from novaclient import exceptions as exc from novaclient import exceptions as exc
import time HAS_NOVACLIENT = True
except ImportError: except ImportError:
print("failed=True msg='novaclient is required for this module to work'") HAS_NOVACLIENT = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -81,7 +82,9 @@ options:
required: false required: false
default: None default: None
requirements: ["novaclient"] requirements:
- "python >= 2.6"
- "python-novaclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
- name: Create a key pair with the running users public key - name: Create a key pair with the running users public key
@ -110,6 +113,8 @@ def main():
state = dict(default='present', choices=['absent', 'present']) state = dict(default='present', choices=['absent', 'present'])
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAS_NOVACLIENT:
module.fail_json(msg='python-novaclient is required for this module to work')
nova = nova_client.Client(module.params['login_username'], nova = nova_client.Client(module.params['login_username'],
module.params['login_password'], module.params['login_password'],
@ -151,5 +156,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -16,6 +16,8 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>. # along with this software. If not, see <http://www.gnu.org/licenses/>.
import time
try: try:
from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import client as nova_client
try: try:
@ -23,9 +25,9 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
import time HAVE_DEPS = True
except ImportError: except ImportError:
print("failed=True msg='novaclient,keystoneclient and quantumclient (or neutronclient) are required'") HAVE_DEPS = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -89,7 +91,11 @@ options:
required: false required: false
default: None default: None
version_added: "1.5" version_added: "1.5"
requirements: ["novaclient", "quantumclient", "neutronclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-novaclient"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -252,6 +258,9 @@ def main():
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-novaclient, python-keystoneclient, and either python-neutronclient or python-quantumclient are required')
try: try:
nova = nova_client.Client(module.params['login_username'], module.params['login_password'], nova = nova_client.Client(module.params['login_username'], module.params['login_password'],
module.params['login_tenant_name'], module.params['auth_url'], region_name=module.params['region_name'], service_type='compute') module.params['login_tenant_name'], module.params['auth_url'], region_name=module.params['region_name'], service_type='compute')
@ -285,5 +294,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -16,6 +16,7 @@
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with this software. If not, see <http://www.gnu.org/licenses/>. # along with this software. If not, see <http://www.gnu.org/licenses/>.
import time
try: try:
from novaclient.v1_1 import client as nova_client from novaclient.v1_1 import client as nova_client
try: try:
@ -23,9 +24,9 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
import time HAVE_DEPS = True
except ImportError: except ImportError:
print "failed=True msg='novaclient, keystone, and quantumclient (or neutronclient) client are required'" HAVE_DEPS = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -81,7 +82,11 @@ options:
- floating ip that should be assigned to the instance - floating ip that should be assigned to the instance
required: true required: true
default: None default: None
requirements: ["quantumclient", "neutronclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-novaclient"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -192,6 +197,9 @@ def main():
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-novaclient, python-keystoneclient, and either python-neutronclient or python-quantumclient are required')
try: try:
nova = nova_client.Client(module.params['login_username'], module.params['login_password'], nova = nova_client.Client(module.params['login_username'], module.params['login_password'],
module.params['login_tenant_name'], module.params['auth_url'], service_type='compute') module.params['login_tenant_name'], module.params['auth_url'], service_type='compute')
@ -220,5 +228,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -22,8 +22,9 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
HAVE_DEPS = True
except ImportError: except ImportError:
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'") HAVE_DEPS = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -108,7 +109,10 @@ options:
- Whether the state should be marked as up or down - Whether the state should be marked as up or down
required: false required: false
default: true default: true
requirements: ["quantumclient", "neutronclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
@ -259,6 +263,9 @@ def main():
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-keystoneclient and either python-neutronclient or python-quantumclient are required')
if module.params['provider_network_type'] in ['vlan' , 'flat']: if module.params['provider_network_type'] in ['vlan' , 'flat']:
if not module.params['provider_physical_network']: if not module.params['provider_physical_network']:
module.fail_json(msg = " for vlan and flat networks, variable provider_physical_network should be set.") module.fail_json(msg = " for vlan and flat networks, variable provider_physical_network should be set.")
@ -290,5 +297,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -22,8 +22,9 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
HAVE_DEPS = True
except ImportError: except ImportError:
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'") HAVE_DEPS = False
ANSIBLE_METADATA = {'status': ['deprecated'], ANSIBLE_METADATA = {'status': ['deprecated'],
'supported_by': 'community', 'supported_by': 'community',
@ -84,7 +85,10 @@ options:
- desired admin state of the created router . - desired admin state of the created router .
required: false required: false
default: true default: true
requirements: ["quantumclient", "neutronclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -189,6 +193,8 @@ def main():
admin_state_up = dict(type='bool', default=True), admin_state_up = dict(type='bool', default=True),
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-keystoneclient and either python-neutronclient or python-quantumclient are required')
neutron = _get_neutron_client(module, module.params) neutron = _get_neutron_client(module, module.params)
_set_tenant_id(module) _set_tenant_id(module)
@ -212,5 +218,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -22,6 +22,7 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
HAVE_DEPS = True
except ImportError: except ImportError:
HAVE_DEPS = False HAVE_DEPS = False
@ -79,7 +80,10 @@ options:
- Name of the external network which should be attached to the router. - Name of the external network which should be attached to the router.
required: true required: true
default: None default: None
requirements: ["quantumclient", "neutronclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -192,6 +196,8 @@ def main():
state = dict(default='present', choices=['absent', 'present']), state = dict(default='present', choices=['absent', 'present']),
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-keystoneclient and either python-neutronclient or python-quantumclient are required')
neutron = _get_neutron_client(module, module.params) neutron = _get_neutron_client(module, module.params)
router_id = _get_router_id(module, neutron) router_id = _get_router_id(module, neutron)
@ -220,5 +226,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -22,6 +22,7 @@ try:
except ImportError: except ImportError:
from quantumclient.quantum import client from quantumclient.quantum import client
from keystoneclient.v2_0 import client as ksclient from keystoneclient.v2_0 import client as ksclient
HAVE_DEPS = True
except ImportError: except ImportError:
HAVE_DEPS = False HAVE_DEPS = False
@ -84,7 +85,10 @@ options:
- Name of the tenant whose subnet has to be attached. - Name of the tenant whose subnet has to be attached.
required: false required: false
default: None default: None
requirements: ["quantumclient", "keystoneclient"] requirements:
- "python >= 2.6"
- "python-neutronclient or python-quantumclient"
- "python-keystoneclient"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -224,6 +228,8 @@ def main():
state = dict(default='present', choices=['absent', 'present']), state = dict(default='present', choices=['absent', 'present']),
)) ))
module = AnsibleModule(argument_spec=argument_spec) module = AnsibleModule(argument_spec=argument_spec)
if not HAVE_DEPS:
module.fail_json(msg='python-keystoneclient and either python-neutronclient or python-quantumclient are required')
neutron = _get_neutron_client(module, module.params) neutron = _get_neutron_client(module, module.params)
_set_tenant_id(module) _set_tenant_id(module)
@ -253,5 +259,6 @@ def main():
# this is magic, see lib/ansible/module.params['common.py # this is magic, see lib/ansible/module.params['common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -34,6 +34,9 @@ version_added: "2.0"
author: "Monty Taylor (@emonty)" author: "Monty Taylor (@emonty)"
description: description:
- Retrieve an auth token from an OpenStack Cloud - Retrieve an auth token from an OpenStack Cloud
requirements:
- "python >= 2.6"
- "shade"
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
''' '''
@ -69,4 +72,5 @@ def main():
# this is magic, see lib/ansible/module_common.py # this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -97,6 +97,7 @@ options:
IP completely, or only detach it from the server. Default is to detach only. IP completely, or only detach it from the server. Default is to detach only.
required: false required: false
default: false default: false
version_added: "2.1"
requirements: ["shade"] requirements: ["shade"]
''' '''

View file

@ -32,6 +32,7 @@ DOCUMENTATION = '''
module: os_ironic module: os_ironic
short_description: Create/Delete Bare Metal Resources from OpenStack short_description: Create/Delete Bare Metal Resources from OpenStack
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
author: "Monty Taylor (@emonty)"
version_added: "2.0" version_added: "2.0"
description: description:
- Create or Remove Ironic nodes from OpenStack. - Create or Remove Ironic nodes from OpenStack.
@ -75,28 +76,30 @@ options:
- Information for this server's driver. Will vary based on which - Information for this server's driver. Will vary based on which
driver is in use. Any sub-field which is populated will be validated driver is in use. Any sub-field which is populated will be validated
during creation. during creation.
suboptions:
power: power:
- Information necessary to turn this server on / off. This often description:
includes such things as IPMI username, password, and IP address. - Information necessary to turn this server on / off.
This often includes such things as IPMI username, password, and IP address.
required: true required: true
deploy: deploy:
- Information necessary to deploy this server directly, without description:
using Nova. THIS IS NOT RECOMMENDED. - Information necessary to deploy this server directly, without using Nova. THIS IS NOT RECOMMENDED.
console: console:
- Information necessary to connect to this server's serial console. description:
Not all drivers support this. - Information necessary to connect to this server's serial console. Not all drivers support this.
management: management:
- Information necessary to interact with this server's management description:
interface. May be shared by power_info in some cases. - Information necessary to interact with this server's management interface. May be shared by power_info in some cases.
required: true required: true
nics: nics:
description: description:
- A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc" - 'A list of network interface cards, eg, " - mac: aa:bb:cc:aa:bb:cc"'
required: true required: true
properties: properties:
description: description:
- Definition of the physical characteristics of this server, used for - Definition of the physical characteristics of this server, used for scheduling purposes
scheduling purposes suboptions:
cpu_arch: cpu_arch:
description: description:
- CPU architecture (x86_64, i686, ...) - CPU architecture (x86_64, i686, ...)
@ -111,8 +114,7 @@ options:
default: 1 default: 1
disk_size: disk_size:
description: description:
- size of first storage device in this machine (typically - size of first storage device in this machine (typically /dev/sda), in GB
/dev/sda), in GB
default: 1 default: 1
skip_update_of_driver_password: skip_update_of_driver_password:
description: description:

View file

@ -32,7 +32,9 @@ DOCUMENTATION = '''
--- ---
module: os_ironic_node module: os_ironic_node
short_description: Activate/Deactivate Bare Metal Resources from OpenStack short_description: Activate/Deactivate Bare Metal Resources from OpenStack
author: "Monty Taylor (@emonty)"
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
version_added: "2.0"
description: description:
- Deploy to nodes controlled by Ironic. - Deploy to nodes controlled by Ironic.
options: options:
@ -71,6 +73,7 @@ options:
- Definition of the instance information which is used to deploy - Definition of the instance information which is used to deploy
the node. This information is only required when an instance is the node. This information is only required when an instance is
set to present. set to present.
suboptions:
image_source: image_source:
description: description:
- An HTTP(S) URL where the image can be retrieved from. - An HTTP(S) URL where the image can be retrieved from.

View file

@ -31,7 +31,8 @@ DOCUMENTATION = '''
--- ---
module: os_object module: os_object
short_description: Create or Delete objects and containers from OpenStack short_description: Create or Delete objects and containers from OpenStack
version_added: "1.10" version_added: "2.0"
author: "Monty Taylor (@emonty)"
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
description: description:
- Create or Delete objects and containers from OpenStack - Create or Delete objects and containers from OpenStack
@ -60,7 +61,6 @@ options:
- Should the resource be present or absent. - Should the resource be present or absent.
choices: [present, absent] choices: [present, absent]
default: present default: present
requirements: ["shade"]
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -32,6 +32,7 @@ DOCUMENTATION = '''
module: os_security_group module: os_security_group
short_description: Add/Delete security groups from an OpenStack cloud. short_description: Add/Delete security groups from an OpenStack cloud.
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
author: "Monty Taylor (@emonty)"
version_added: "2.0" version_added: "2.0"
description: description:
- Add or Remove security groups from an OpenStack cloud. - Add or Remove security groups from an OpenStack cloud.

View file

@ -32,7 +32,7 @@ DOCUMENTATION = '''
module: os_security_group_rule module: os_security_group_rule
short_description: Add/Delete rule from an existing security group short_description: Add/Delete rule from an existing security group
extends_documentation_fragment: openstack extends_documentation_fragment: openstack
version_added: "1.10" version_added: "2.0"
description: description:
- Add or Remove rule from an existing security group - Add or Remove rule from an existing security group
options: options:
@ -81,7 +81,6 @@ options:
- Should the resource be present or absent. - Should the resource be present or absent.
choices: [present, absent] choices: [present, absent]
default: present default: present
requirements: ["shade"] requirements: ["shade"]
''' '''
@ -257,7 +256,6 @@ def _system_state_change(module, secgroup, remotegroup):
def main(): def main():
argument_spec = openstack_full_argument_spec( argument_spec = openstack_full_argument_spec(
security_group = dict(required=True), security_group = dict(required=True),
# NOTE(Shrews): None is an acceptable protocol value for # NOTE(Shrews): None is an acceptable protocol value for

View file

@ -45,12 +45,10 @@ options:
description: description:
- Name that has to be given to the instance - Name that has to be given to the instance
required: true required: true
default: None
image: image:
description: description:
- The name or id of the base image to boot. - The name or id of the base image to boot.
required: true required: true
default: None
image_exclude: image_exclude:
description: description:
- Text to use to filter image names, for the case, such as HP, where - Text to use to filter image names, for the case, such as HP, where
@ -110,7 +108,7 @@ options:
default: 'yes' default: 'yes'
aliases: ['auto_floating_ip', 'public_ip'] aliases: ['auto_floating_ip', 'public_ip']
floating_ips: floating_ips:
decription: description:
- list of valid floating IPs that pre-exist to assign to this node - list of valid floating IPs that pre-exist to assign to this node
required: false required: false
default: None default: None
@ -648,4 +646,5 @@ def main():
# this is magic, see lib/ansible/module_common.py # this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -44,11 +44,11 @@ options:
- Should the resource be present or absent. - Should the resource be present or absent.
choices: [present, absent] choices: [present, absent]
default: present default: present
required: false
server: server:
description: description:
- Name or ID of server you want to attach a volume to - Name or ID of server you want to attach a volume to
required: true required: true
default: None
volume: volume:
description: description:
- Name or id of volume you want to attach to a server - Name or id of volume you want to attach to a server
@ -58,7 +58,9 @@ options:
- Device you want to attach. Defaults to auto finding a device name. - Device you want to attach. Defaults to auto finding a device name.
required: false required: false
default: None default: None
requirements: ["shade"] requirements:
- "python >= 2.6"
- "shade"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -153,4 +155,5 @@ def main():
# this is magic, see lib/ansible/module_utils/common.py # this is magic, see lib/ansible/module_utils/common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -47,7 +47,6 @@ options:
description: description:
- Name of volume - Name of volume
required: true required: true
default: None
display_description: display_description:
description: description:
- String describing the volume - String describing the volume
@ -59,7 +58,7 @@ options:
required: false required: false
default: None default: None
image: image:
descritpion: description:
- Image name or id for boot from volume - Image name or id for boot from volume
required: false required: false
default: None default: None
@ -73,7 +72,9 @@ options:
- Should the resource be present or absent. - Should the resource be present or absent.
choices: [present, absent] choices: [present, absent]
default: present default: present
requirements: ["shade"] requirements:
- "python >= 2.6"
- "shade"
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -162,4 +163,5 @@ def main():
# this is magic, see lib/ansible/module_common.py # this is magic, see lib/ansible/module_common.py
from ansible.module_utils.basic import * from ansible.module_utils.basic import *
from ansible.module_utils.openstack import * from ansible.module_utils.openstack import *
main() if __name__ == '__main__':
main()

View file

@ -109,7 +109,7 @@ options:
- Data to be uploaded to the servers config drive. This option implies - Data to be uploaded to the servers config drive. This option implies
I(config_drive). Can be a file path or a string I(config_drive). Can be a file path or a string
version_added: 1.8 version_added: 1.8
wait wait:
description: description:
- wait for the scaling group to finish provisioning the minimum amount of - wait for the scaling group to finish provisioning the minimum amount of
servers servers
@ -121,7 +121,7 @@ options:
description: description:
- how long before wait gives up, in seconds - how long before wait gives up, in seconds
default: 300 default: 300
author: Matt Martz author: "Matt Martz (@sivel)"
extends_documentation_fragment: rackspace extends_documentation_fragment: rackspace
''' '''

View file

@ -203,6 +203,10 @@ EXAMPLES = '''
type: vmxnet3 type: vmxnet3
network: VM Network network: VM Network
network_type: standard network_type: standard
nic2:
type: vmxnet3
network: dvSwitch Network
network_type: dvs
vm_hardware: vm_hardware:
memory_mb: 2048 memory_mb: 2048
num_cpus: 2 num_cpus: 2
@ -251,7 +255,6 @@ EXAMPLES = '''
hostname: esx001.mydomain.local hostname: esx001.mydomain.local
# Deploy a guest from a template # 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: - vsphere_guest:
vcenter_hostname: vcenter.mydomain.local vcenter_hostname: vcenter.mydomain.local
username: myuser username: myuser

View file

@ -26,7 +26,6 @@ ANSIBLE_METADATA = {'status': ['stableinterface'],
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: command module: command
version_added: historical
short_description: Executes a command on a remote node short_description: Executes a command on a remote node
version_added: historical version_added: historical
description: description:
@ -42,7 +41,6 @@ options:
See the examples! See the examples!
required: true required: true
default: null default: null
aliases: []
creates: creates:
description: description:
- a filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run. - a filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run.

View file

@ -22,7 +22,6 @@ ANSIBLE_METADATA = {'status': ['stableinterface'],
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: raw module: raw
version_added: historical
short_description: Executes a low-down and dirty SSH command short_description: Executes a low-down and dirty SSH command
version_added: historical version_added: historical
options: options:

View file

@ -98,7 +98,7 @@ options:
required: false required: false
default: always default: always
choices: ['always', 'on_create'] choices: ['always', 'on_create']
version_added: "1.9" version_added: "2.0"
description: description:
- C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users. - C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users.
notes: notes:
@ -301,13 +301,6 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append
changed = False changed = False
grant_option = False grant_option = False
# Handle clear text and hashed passwords.
if bool(password):
# Determine what user management method server uses
old_user_mgmt = server_version_check(cursor)
# to simplify code, if we have a specific host and no host_all, we create
# a list with just host and loop over that
if host_all: if host_all:
hostnames = user_get_hostnames(cursor, [user]) hostnames = user_get_hostnames(cursor, [user])
else: else:
@ -348,7 +341,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append
if module.check_mode: if module.check_mode:
return True return True
if old_user_mgmt: if old_user_mgmt:
cursor.execute("SET PASSWORD FOR %s@%s = %s", (user, host, password)) cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user, host, password))
else: else:
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password BY %s", (user, host, password)) cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password BY %s", (user, host, password))
changed = True changed = True
@ -614,7 +607,7 @@ def main():
module.fail_json(msg="invalid privileges string: %s" % str(e)) module.fail_json(msg="invalid privileges string: %s" % str(e))
if state == "present": if state == "present":
if user_exists(cursor, user, host): if user_exists(cursor, user, host, host_all):
try: try:
if update_password == 'always': if update_password == 'always':
changed = user_mod(cursor, user, host, host_all, password, encrypted, priv, append_privs, module) changed = user_mod(cursor, user, host, host_all, password, encrypted, priv, append_privs, module)

View file

@ -349,12 +349,21 @@ def user_delete(cursor, user):
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete") cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
return True return True
def has_table_privilege(cursor, user, table, priv): def has_table_privileges(cursor, user, table, privs):
if priv == 'ALL': """
priv = ','.join([ p for p in VALID_PRIVS['table'] if p != 'ALL' ]) Return the difference between the privileges that a user already has and
query = 'SELECT has_table_privilege(%s, %s, %s)' the privileges that they desire to have.
cursor.execute(query, (user, table, priv))
return cursor.fetchone()[0] :returns: tuple of:
* privileges that they have and were requested
* privileges they currently hold but were not requested
* privileges requested that they do not hold
"""
cur_privs = get_table_privileges(cursor, user, table)
have_currently = cur_privs.intersection(privs)
other_current = cur_privs.difference(privs)
desired = privs.difference(cur_privs)
return (have_currently, other_current, desired)
def get_table_privileges(cursor, user, table): def get_table_privileges(cursor, user, table):
if '.' in table: if '.' in table:
@ -364,26 +373,21 @@ def get_table_privileges(cursor, user, table):
query = '''SELECT privilege_type FROM information_schema.role_table_grants query = '''SELECT privilege_type FROM information_schema.role_table_grants
WHERE grantee=%s AND table_name=%s AND table_schema=%s''' WHERE grantee=%s AND table_name=%s AND table_schema=%s'''
cursor.execute(query, (user, table, schema)) cursor.execute(query, (user, table, schema))
return set([x[0] for x in cursor.fetchall()]) return frozenset([x[0] for x in cursor.fetchall()])
def grant_table_privilege(cursor, user, table, priv): def grant_table_privileges(cursor, user, table, privs):
# Note: priv escaped by parse_privs # Note: priv escaped by parse_privs
prev_priv = get_table_privileges(cursor, user, table) privs = ', '.join(privs)
query = 'GRANT %s ON TABLE %s TO %s' % ( query = 'GRANT %s ON TABLE %s TO %s' % (
priv, pg_quote_identifier(table, 'table'), pg_quote_identifier(user, 'role') ) privs, pg_quote_identifier(table, 'table'), pg_quote_identifier(user, 'role') )
cursor.execute(query) cursor.execute(query)
curr_priv = get_table_privileges(cursor, user, table)
return len(curr_priv) > len(prev_priv)
def revoke_table_privilege(cursor, user, table, priv): def revoke_table_privileges(cursor, user, table, privs):
# Note: priv escaped by parse_privs # Note: priv escaped by parse_privs
prev_priv = get_table_privileges(cursor, user, table) privs = ', '.join(privs)
query = 'REVOKE %s ON TABLE %s FROM %s' % ( query = 'REVOKE %s ON TABLE %s FROM %s' % (
priv, pg_quote_identifier(table, 'table'), pg_quote_identifier(user, 'role') ) privs, pg_quote_identifier(table, 'table'), pg_quote_identifier(user, 'role') )
cursor.execute(query) cursor.execute(query)
curr_priv = get_table_privileges(cursor, user, table)
return len(curr_priv) < len(prev_priv)
def get_database_privileges(cursor, user, db): def get_database_privileges(cursor, user, db):
priv_map = { priv_map = {
@ -395,54 +399,62 @@ def get_database_privileges(cursor, user, db):
cursor.execute(query, (db,)) cursor.execute(query, (db,))
datacl = cursor.fetchone()[0] datacl = cursor.fetchone()[0]
if datacl is None: if datacl is None:
return [] return set()
r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl) r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl)
if r is None: if r is None:
return [] return set()
o = [] o = set()
for v in r.group(1): for v in r.group(1):
o.append(priv_map[v]) o.add(priv_map[v])
return o return normalize_privileges(o, 'database')
def has_database_privilege(cursor, user, db, priv): def has_database_privileges(cursor, user, db, privs):
if priv == 'ALL': """
priv = ','.join([ p for p in VALID_PRIVS['database'] if p != 'ALL' ]) Return the difference between the privileges that a user already has and
query = 'SELECT has_database_privilege(%s, %s, %s)' the privileges that they desire to have.
cursor.execute(query, (user, db, priv))
return cursor.fetchone()[0]
def grant_database_privilege(cursor, user, db, priv): :returns: tuple of:
* privileges that they have and were requested
* privileges they currently hold but were not requested
* privileges requested that they do not hold
"""
cur_privs = get_database_privileges(cursor, user, db)
have_currently = cur_privs.intersection(privs)
other_current = cur_privs.difference(privs)
desired = privs.difference(cur_privs)
return (have_currently, other_current, desired)
def grant_database_privileges(cursor, user, db, privs):
# Note: priv escaped by parse_privs # Note: priv escaped by parse_privs
prev_priv = get_database_privileges(cursor, user, db) privs =', '.join(privs)
if user == "PUBLIC": if user == "PUBLIC":
query = 'GRANT %s ON DATABASE %s TO PUBLIC' % ( query = 'GRANT %s ON DATABASE %s TO PUBLIC' % (
priv, pg_quote_identifier(db, 'database')) privs, pg_quote_identifier(db, 'database'))
else: else:
query = 'GRANT %s ON DATABASE %s TO %s' % ( query = 'GRANT %s ON DATABASE %s TO %s' % (
priv, pg_quote_identifier(db, 'database'), privs, pg_quote_identifier(db, 'database'),
pg_quote_identifier(user, 'role')) pg_quote_identifier(user, 'role'))
cursor.execute(query) cursor.execute(query)
curr_priv = get_database_privileges(cursor, user, db)
return len(curr_priv) > len(prev_priv)
def revoke_database_privilege(cursor, user, db, priv): def revoke_database_privileges(cursor, user, db, privs):
# Note: priv escaped by parse_privs # Note: priv escaped by parse_privs
prev_priv = get_database_privileges(cursor, user, db) privs = ', '.join(privs)
if user == "PUBLIC": if user == "PUBLIC":
query = 'REVOKE %s ON DATABASE %s FROM PUBLIC' % ( query = 'REVOKE %s ON DATABASE %s FROM PUBLIC' % (
priv, pg_quote_identifier(db, 'database')) privs, pg_quote_identifier(db, 'database'))
else: else:
query = 'REVOKE %s ON DATABASE %s FROM %s' % ( query = 'REVOKE %s ON DATABASE %s FROM %s' % (
priv, pg_quote_identifier(db, 'database'), privs, pg_quote_identifier(db, 'database'),
pg_quote_identifier(user, 'role')) pg_quote_identifier(user, 'role'))
cursor.execute(query) cursor.execute(query)
curr_priv = get_database_privileges(cursor, user, db)
return len(curr_priv) < len(prev_priv)
def revoke_privileges(cursor, user, privs): def revoke_privileges(cursor, user, privs):
if privs is None: if privs is None:
return False return False
revoke_funcs = dict(table=revoke_table_privileges, database=revoke_database_privileges)
check_funcs = dict(table=has_table_privileges, database=has_database_privileges)
changed = False changed = False
for type_ in privs: for type_ in privs:
for name, privileges in iteritems(privs[type_]): for name, privileges in iteritems(privs[type_]):
@ -498,6 +510,17 @@ def parse_role_attrs(role_attr_flags):
o_flags = ' '.join(flag_set) o_flags = ' '.join(flag_set)
return o_flags return o_flags
def normalize_privileges(privs, type_):
new_privs = set(privs)
if 'ALL' in new_privs:
new_privs.update(VALID_PRIVS[type_])
new_privs.remove('ALL')
if 'TEMP' in new_privs:
new_privs.add('TEMPORARY')
new_privs.remove('TEMP')
return new_privs
def parse_privs(privs, db): def parse_privs(privs, db):
""" """
Parse privilege string to determine permissions for database db. Parse privilege string to determine permissions for database db.
@ -530,6 +553,8 @@ def parse_privs(privs, db):
if not priv_set.issubset(VALID_PRIVS[type_]): if not priv_set.issubset(VALID_PRIVS[type_]):
raise InvalidPrivsError('Invalid privs specified for %s: %s' % raise InvalidPrivsError('Invalid privs specified for %s: %s' %
(type_, ' '.join(priv_set.difference(VALID_PRIVS[type_])))) (type_, ' '.join(priv_set.difference(VALID_PRIVS[type_]))))
priv_set = normalize_privileges(priv_set, type_)
o_privs[type_][name] = priv_set o_privs[type_][name] = priv_set
return o_privs return o_privs

View file

@ -90,9 +90,11 @@ options:
validate is passed in via '%s' which must be present as in the sshd example below. 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. The command is passed securely so shell features like expansion and pipes won't work.
required: false required: false
default: "" default: null
version_added: "2.0"
author: "Stephen Fromm (@sfromm)" author: "Stephen Fromm (@sfromm)"
extends_documentation_fragment: files extends_documentation_fragment:
- files
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -33,8 +33,7 @@ description:
- Manage (add, remove, change) individual settings in an INI-style file without having - Manage (add, remove, change) individual settings in an INI-style file without having
to manage the file as a whole with, say, M(template) or M(assemble). Adds missing to manage the file as a whole with, say, M(template) or M(assemble). Adds missing
sections if they don't exist. sections if they don't exist.
- Comments are discarded when the source file is read, and therefore will not - Before version 2.0, comments are discarded when the source file is read, and therefore will not show up in the destination file.
show up in the destination file.
version_added: "0.9" version_added: "0.9"
options: options:
dest: dest:

View file

@ -29,8 +29,9 @@ module: lineinfile
author: author:
- "Daniel Hokka Zakrissoni (@dhozac)" - "Daniel Hokka Zakrissoni (@dhozac)"
- "Ahti Kitsik (@ahtik)" - "Ahti Kitsik (@ahtik)"
extends_documentation_fragment: files extends_documentation_fragment:
extends_documentation_fragment: validate - files
- validate
short_description: Ensure a particular line is in a file, or replace an short_description: Ensure a particular line is in a file, or replace an
existing line using a back-referenced regular expression. existing line using a back-referenced regular expression.
description: description:

View file

@ -30,8 +30,9 @@ DOCUMENTATION = """
--- ---
module: replace module: replace
author: "Evan Kaufman (@EvanK)" author: "Evan Kaufman (@EvanK)"
extends_documentation_fragment: files extends_documentation_fragment:
extends_documentation_fragment: validate - files
- validate
short_description: Replace all instances of a particular string in a short_description: Replace all instances of a particular string in a
file using a back-referenced regular expression. file using a back-referenced regular expression.
description: description:

View file

@ -43,13 +43,10 @@ options:
description: description:
- Path of a Jinja2 formatted template on the Ansible controller. This can be a relative or absolute path. - Path of a Jinja2 formatted template on the Ansible controller. This can be a relative or absolute path.
required: true required: true
default: null
aliases: []
dest: dest:
description: description:
- Location to render the template to on the remote machine. - Location to render the template to on the remote machine.
required: true required: true
default: null
backup: backup:
description: description:
- Create a backup file including the timestamp information so you can get - Create a backup file including the timestamp information so you can get
@ -77,8 +74,9 @@ notes:
author: author:
- Ansible Core Team - Ansible Core Team
- Michael DeHaan - Michael DeHaan
extends_documentation_fragment: files extends_documentation_fragment:
extends_documentation_fragment: validate - files
- validate
''' '''
EXAMPLES = ''' EXAMPLES = '''

View file

@ -86,6 +86,7 @@ options:
required: false required: false
choices: [ "yes", "no" ] choices: [ "yes", "no" ]
default: "no" default: "no"
version_added: '2.1'
sha256sum: sha256sum:
description: description:
- If a SHA-256 checksum is passed to this parameter, the digest of the - If a SHA-256 checksum is passed to this parameter, the digest of the

View file

@ -66,6 +66,7 @@ options:
- The serialization format of the body. When set to json, encodes the - The serialization format of the body. When set to json, encodes the
body argument, if needed, and automatically sets the Content-Type header accordingly. body argument, if needed, and automatically sets the Content-Type header accordingly.
required: false required: false
choices: [ "raw", "json" ]
default: raw default: raw
version_added: "2.0" version_added: "2.0"
method: method:

View file

@ -72,14 +72,11 @@ options:
default: null default: null
zeroize: zeroize:
description: description:
- The C(zeroize) argument is used to completely santaize the - The C(zeroize) argument is used to completely sanitize the
remote device configuration back to initial defaults. This remote device configuration back to initial defaults. This
argument will effectively remove all current configuration argument will effectively remove all current configuration
statements on the remote device. statements on the remote device.
required: false required: false
choices:
- yes
- no
default: null default: null
confirm: confirm:
description: description:

View file

@ -32,14 +32,14 @@ author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- I(state)=absent removes the ACE if it exists - C(state=absent) removes the ACE if it exists.
- I(state)=delete_acl deleted the ACL if it exists - C(state=delete_acl) deleted the ACL if it exists.
- for idempotency, use port numbers for the src/dest port - For idempotency, use port numbers for the src/dest port
params like I(src_port1) and names for the well defined protocols params like I(src_port1) and names for the well defined protocols
for the I(proto) param. for the I(proto) param.
- while this module is idempotent in that if the ace as presented in the - Although this module is idempotent in that if the ace as presented in
task is identical to the one on the switch, no changes will be made. If the task is identical to the one on the switch, no changes will be made.
there is any difference, what is in Ansible will be pushed (configured If there is any difference, what is in Ansible will be pushed (configured
options will be overridden). This is to improve security, but at the options will be overridden). This is to improve security, but at the
same time remember an ACE is removed, then re-added, so if there is a same time remember an ACE is removed, then re-added, so if there is a
change, the new ACE will be exactly what parameters you are sending to change, the new ACE will be exactly what parameters you are sending to
@ -47,151 +47,152 @@ notes:
options: options:
seq: seq:
description: description:
- sequence number of the entry (ACE) - Sequence number of the entry (ACE).
required: false required: false
default: null default: null
name: name:
description: description:
- Case sensitive name of the access list (ACL) - Case sensitive name of the access list (ACL).
required: true required: true
action: action:
description: description:
- action of the ACE - Action of the ACE.
required: false required: false
default: null default: null
choices: ['permit', 'deny', 'remark'] choices: ['permit', 'deny', 'remark']
remark: remark:
description: description:
- If action is set to remark, this is the description - If action is set to remark, this is the description.
required: false required: false
default: null default: null
proto: proto:
description: description:
- port number or protocol (as supported by the switch) - Port number or protocol (as supported by the switch).
required: false required: false
default: null default: null
src: src:
description: description:
- src ip and mask using IP/MASK notation and supports keyword 'any' - Source ip and mask using IP/MASK notation and
supports keyword 'any'.
required: false required: false
default: null default: null
src_port_op: src_port_op:
description: description:
- src port operands such as eq, neq, gt, lt, range - Source port operands such as eq, neq, gt, lt, range.
required: false required: false
default: null default: null
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
src_port1: src_port1:
description: description:
- port/protocol and also first (lower) port when using range - Port/protocol and also first (lower) port when using range
operand operand.
required: false required: false
default: null default: null
src_port2: src_port2:
description: description:
- second (end) port when using range operand - Second (end) port when using range operand.
required: false required: false
default: null default: null
dest: dest:
description: description:
- dest ip and mask using IP/MASK notation and supports the - Destination ip and mask using IP/MASK notation and supports the
keyword 'any' keyword 'any'.
required: false required: false
default: null default: null
dest_port_op: dest_port_op:
description: description:
- dest port operands such as eq, neq, gt, lt, range - Destination port operands such as eq, neq, gt, lt, range.
required: false required: false
default: null default: null
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
dest_port1: dest_port1:
description: description:
- port/protocol and also first (lower) port when using range - Port/protocol and also first (lower) port when using range
operand operand.
required: false required: false
default: null default: null
dest_port2: dest_port2:
description: description:
- second (end) port when using range operand - Second (end) port when using range operand.
required: false required: false
default: null default: null
log: log:
description: description:
- Log matches against this entry - Log matches against this entry.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
urg: urg:
description: description:
- Match on the URG bit - Match on the URG bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
ack: ack:
description: description:
- Match on the ACK bit - Match on the ACK bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
psh: psh:
description: description:
- Match on the PSH bit - Match on the PSH bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
rst: rst:
description: description:
- Match on the RST bit - Match on the RST bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
syn: syn:
description: description:
- Match on the SYN bit - Match on the SYN bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
fin: fin:
description: description:
- Match on the FIN bit - Match on the FIN bit.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
established: established:
description: description:
- Match established connections - Match established connections.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
fragments: fragments:
description: description:
- Check non-initial fragments - Check non-initial fragments.
required: false required: false
default: null default: null
choices: ['enable'] choices: ['enable']
time-range: time-range:
description: description:
- Name of time-range to apply - Name of time-range to apply.
required: false required: false
default: null default: null
precedence: precedence:
description: description:
- Match packets with given precedence - Match packets with given precedence.
required: false required: false
default: null default: null
choices: ['critical', 'flash', 'flash-override', 'immediate', choices: ['critical', 'flash', 'flash-override', 'immediate',
'internet', 'network', 'priority', 'routine'] 'internet', 'network', 'priority', 'routine']
dscp: dscp:
description: description:
- Match packets with given dscp value - Match packets with given dscp value.
required: false required: false
default: null default: null
choices: ['af11, 'af12, 'af13, 'af21', 'af22', 'af23','af31','af32', choices: ['af11', 'af12', 'af13', 'af21', 'af22', 'af23','af31','af32',
'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4',
'cs5', 'cs6', 'cs7', 'default', 'ef'] 'cs5', 'cs6', 'cs7', 'default', 'ef']
state: state:
description: description:
- Specify desired state of the resource - Specify desired state of the resource.
required: false required: false
default: present default: present
choices: ['present','absent','delete_acl'] choices: ['present','absent','delete_acl']
@ -220,6 +221,7 @@ proposed:
"proto": "tcp", "seq": "10", "src": "1.1.1.1/24"} "proto": "tcp", "seq": "10", "src": "1.1.1.1/24"}
existing: existing:
description: k/v pairs of existing ACL entries. description: k/v pairs of existing ACL entries.
returned: always
type: dict type: dict
sample: {} sample: {}
end_state: end_state:
@ -240,217 +242,32 @@ changed:
sample: true sample: true
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections import collections
import itertools
import shlex
import json import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -462,14 +279,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -477,93 +286,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -615,303 +354,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -919,6 +399,7 @@ def load_config(module, candidate):
return result return result
# END OF COMMON CODE # END OF COMMON CODE
def get_cli_body_ssh(command, response, module): def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly """Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And needed because these modules were originally written for NX-API. And
@ -941,6 +422,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -1195,9 +681,11 @@ def main():
host=dict(required=True), host=dict(required=True),
username=dict(type='str'), username=dict(type='str'),
password=dict(no_log=True, type='str'), password=dict(no_log=True, type='str'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']

View file

@ -88,11 +88,6 @@ acl_applied_to:
type: list type: list
sample: [{"acl_type": "Router ACL", "direction": "egress", sample: [{"acl_type": "Router ACL", "direction": "egress",
"interface": "Ethernet1/41", "name": "ANSIBLE"}] "interface": "Ethernet1/41", "name": "ANSIBLE"}]
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates: updates:
description: commands sent to the device description: commands sent to the device
returned: always returned: always
@ -105,211 +100,32 @@ changed:
sample: true sample: true
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections import collections
import itertools import json
import shlex
import itertools
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE # COMMON CODE FOR MIGRATION
import re
DEFAULT_COMMENT_TOKENS = ['#', '!'] from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
class ConfigLine(object): try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
def __init__(self, text):
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property def to_list(val):
def line(self): if isinstance(val, (list, tuple)):
line = ['set'] return list(val)
line.extend([p.text for p in self.parents]) elif val is not None:
line.append(self.text) return [val]
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -321,14 +137,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -336,93 +144,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -474,18 +212,20 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict( try:
# config options return get_module(**kwargs)
running_config=dict(aliases=['config']), except NameError:
save_config=dict(type='bool', default=False, aliases=['save']) return NetworkModule(**kwargs)
)
nxos_argument_spec = argument_spec()
def get_config(module): def get_config(module, include_defaults=False):
config = module.params['running_config'] config = module.params['config']
if not config: if not config:
try:
config = module.get_config() config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
@ -494,15 +234,22 @@ def load_config(module, candidate):
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -511,6 +258,7 @@ def load_config(module, candidate):
# END OF COMMON CODE # END OF COMMON CODE
def get_cli_body_ssh(command, response, module): def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly """Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And needed because these modules were originally written for NX-API. And
@ -533,6 +281,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -542,6 +295,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response return response
@ -705,9 +471,11 @@ def main():
direction=dict(required=True, choices=['egress', 'ingress']), direction=dict(required=True, choices=['egress', 'ingress']),
state=dict(choices=['absent', 'present'], state=dict(choices=['absent', 'present'],
default='present'), default='present'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -763,7 +531,6 @@ def main():
results = {} results = {}
results['proposed'] = proposed results['proposed'] = proposed
results['existing'] = existing results['existing'] = existing
results['state'] = state
results['updates'] = cmds results['updates'] = cmds
results['changed'] = changed results['changed'] = changed
results['end_state'] = end_state results['end_state'] = end_state
@ -771,10 +538,6 @@ def main():
module.exit_json(**results) module.exit_json(**results)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__': if __name__ == '__main__':
main() main()

File diff suppressed because it is too large Load diff

View file

@ -24,14 +24,14 @@ DOCUMENTATION = '''
--- ---
module: nxos_bgp_af module: nxos_bgp_af
version_added: "2.2" version_added: "2.2"
short_description: Manages BGP Address-family configuration short_description: Manages BGP Address-family configuration.
description: description:
- Manages BGP Address-family configurations on NX-OS switches - Manages BGP Address-family configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- State 'absent' removes the whole BGP ASN configuration - C(state=absent) removes the whole BGP ASN configuration
- default, where supported, restores params default value - Default, where supported, restores params default value.
options: options:
asn: asn:
description: description:
@ -45,7 +45,7 @@ options:
required: true required: true
afi: afi:
description: description:
- Address Family Identifie. - Address Family Identifier.
required: true required: true
choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn'] choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn']
safi: safi:
@ -56,7 +56,7 @@ options:
additional_paths_install: additional_paths_install:
description: description:
- Install a backup path into the forwarding table and provide - Install a backup path into the forwarding table and provide
prefix 'independent convergence (PIC) in case of a PE-CE link prefix independent convergence (PIC) in case of a PE-CE link
failure. failure.
required: false required: false
choices: ['true','false'] choices: ['true','false']
@ -175,7 +175,7 @@ options:
keyword which indicates that attributes should be copied from keyword which indicates that attributes should be copied from
the aggregate. For example [['lax_inject_map', 'lax_exist_map'], the aggregate. For example [['lax_inject_map', 'lax_exist_map'],
['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'], ['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'],
['fsd_inject_map', 'fsd_exist_map']] ['fsd_inject_map', 'fsd_exist_map']].
required: false required: false
default: null default: null
maximum_paths: maximum_paths:
@ -194,9 +194,9 @@ options:
- Networks to configure. Valid value is a list of network - Networks to configure. Valid value is a list of network
prefixes to advertise. The list must be in the form of an array. prefixes to advertise. The list must be in the form of an array.
Each entry in the array must include a prefix address and an Each entry in the array must include a prefix address and an
optional route-map. Example: [['10.0.0.0/16', 'routemap_LA'], optional route-map. For example [['10.0.0.0/16', 'routemap_LA'],
['192.168.1.1', 'Chicago'], ['192.168.2.0/24], ['192.168.1.1', 'Chicago'], ['192.168.2.0/24],
['192.168.3.0/24', 'routemap_NYC']] ['192.168.3.0/24', 'routemap_NYC']].
required: false required: false
default: null default: null
next_hop_route_map: next_hop_route_map:
@ -213,7 +213,7 @@ options:
redistribute from; the second entry defines a route-map name. redistribute from; the second entry defines a route-map name.
A route-map is highly advised but may be optional on some A route-map is highly advised but may be optional on some
platforms, in which case it may be omitted from the array list. platforms, in which case it may be omitted from the array list.
Example: [['direct', 'rm_direct'], ['lisp', 'rm_lisp']] For example [['direct', 'rm_direct'], ['lisp', 'rm_lisp']].
required: false required: false
default: null default: null
suppress_inactive: suppress_inactive:
@ -237,16 +237,11 @@ options:
default: null default: null
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
# configure a simple address-family # configure a simple address-family
@ -262,17 +257,18 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4", sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4",
"asn": "65535", "safi": "unicast", "vrf": "TESTING"} "asn": "65535", "safi": "unicast", "vrf": "TESTING"}
existing: existing:
description: k/v pairs of existing BGP AF configuration description: k/v pairs of existing BGP AF configuration
returned: verbose mode
type: dict type: dict
sample: {} sample: {}
end_state: end_state:
description: k/v pairs of BGP AF configuration after module execution description: k/v pairs of BGP AF configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"additional_paths_install": false, sample: {"additional_paths_install": false,
"additional_paths_receive": false, "additional_paths_receive": false,
@ -305,12 +301,7 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -319,200 +310,17 @@ from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -524,14 +332,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -539,93 +339,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -677,303 +407,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -1213,7 +684,7 @@ def get_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
try: try:
asn_regex = '.*router\sbgp\s(?P<existing_asn>\d+).*' asn_regex = '.*router\sbgp\s(?P<existing_asn>\d+).*'
@ -1515,13 +986,13 @@ def main():
suppress_inactive=dict(required=False, type='bool'), suppress_inactive=dict(required=False, type='bool'),
table_map=dict(required=False, type='str'), table_map=dict(required=False, type='str'),
table_map_filter=dict(required=False, type='bool'), table_map_filter=dict(required=False, type='bool'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
required_together=[DAMPENING_PARAMS, required_together=[DAMPENING_PARAMS,
['distance_ibgp', ['distance_ibgp',
'distance_ebgp', 'distance_ebgp',
@ -1624,7 +1095,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,18 +24,18 @@ DOCUMENTATION = '''
--- ---
module: nxos_bgp_neighbor module: nxos_bgp_neighbor
version_added: "2.2" version_added: "2.2"
short_description: Manages BGP neighbors configurations short_description: Manages BGP neighbors configurations.
description: description:
- Manages BGP neighbors configurations on NX-OS switches - Manages BGP neighbors configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- State 'absent' removes the whole BGP neighbor configuration - C(state=absent) removes the whole BGP neighbor configuration.
- default, where supported, restores params default value - Default, where supported, restores params default value.
options: options:
asn: asn:
description: description:
- BGP autonomous system number. Valid values are String, - BGP autonomous system number. Valid values are string,
Integer in ASPLAIN or ASDOT notation. Integer in ASPLAIN or ASDOT notation.
required: true required: true
vrf: vrf:
@ -180,16 +180,11 @@ options:
default: null default: null
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
# create a new neighbor # create a new neighbor
@ -210,7 +205,7 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"asn": "65535", "description": "just a description", sample: {"asn": "65535", "description": "just a description",
"local_as": "20", "neighbor": "3.3.3.3", "local_as": "20", "neighbor": "3.3.3.3",
@ -218,11 +213,12 @@ proposed:
"update_source": "Ethernet1/3", "vrf": "default"} "update_source": "Ethernet1/3", "vrf": "default"}
existing: existing:
description: k/v pairs of existing BGP neighbor configuration description: k/v pairs of existing BGP neighbor configuration
returned: verbose mode
type: dict type: dict
sample: {} sample: {}
end_state: end_state:
description: k/v pairs of BGP neighbor configuration after module execution description: k/v pairs of BGP neighbor configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"asn": "65535", "capability_negotiation": false, sample: {"asn": "65535", "capability_negotiation": false,
"connected_check": false, "description": "just a description", "connected_check": false, "description": "just a description",
@ -250,12 +246,7 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -263,200 +254,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -468,14 +276,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -483,93 +283,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -621,303 +351,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -1045,7 +516,7 @@ def get_custom_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
custom = [ custom = [
'log_neighbor_changes', 'log_neighbor_changes',
'pwd', 'pwd',
@ -1207,12 +678,13 @@ def main():
m_facts=dict(required=False, default=False, type='bool'), m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec, required_together=[['timer_bgp_hold',
required_together=[['pwd', 'pwd_type'], 'timer_bgp_keepalive']],
['timers_holdtime', 'timers_keepalive']],
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -1281,7 +753,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,17 +24,17 @@ DOCUMENTATION = '''
--- ---
module: nxos_bgp_neighbor_af module: nxos_bgp_neighbor_af
version_added: "2.2" version_added: "2.2"
short_description: Manages BGP address-family's neighbors configuration short_description: Manages BGP address-family's neighbors configuration.
description: description:
- Manages BGP address-family's neighbors configurations on NX-OS switches - Manages BGP address-family's neighbors configurations on NX-OS switches.
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- State 'absent' removes the whole BGP address-family's - C(state=absent) removes the whole BGP address-family's
neighbor configuration neighbor configuration.
- default, when supported, removes properties - Default, when supported, removes properties
- to defaults maximum-prefix configuration, only - In order to default maximum-prefix configuration, only
max_prefix_limit=default is needed C(max_prefix_limit=default) is needed.
options: options:
asn: asn:
description: description:
@ -65,7 +65,7 @@ options:
additional_paths_receive: additional_paths_receive:
description: description:
- Valid values are enable for basic command enablement; disable - Valid values are enable for basic command enablement; disable
for disabling the command at the neighbor_af level for disabling the command at the neighbor af level
(it adds the disable keyword to the basic command); and inherit (it adds the disable keyword to the basic command); and inherit
to remove the command at this level (the command value is to remove the command at this level (the command value is
inherited from a higher BGP layer). inherited from a higher BGP layer).
@ -75,7 +75,7 @@ options:
additional_paths_send: additional_paths_send:
description: description:
- Valid values are enable for basic command enablement; disable - Valid values are enable for basic command enablement; disable
for disabling the command at the neighbor_af level for disabling the command at the neighbor af level
(it adds the disable keyword to the basic command); and inherit (it adds the disable keyword to the basic command); and inherit
to remove the command at this level (the command value is to remove the command at this level (the command value is
inherited from a higher BGP layer). inherited from a higher BGP layer).
@ -129,7 +129,7 @@ options:
default_originate_route_map: default_originate_route_map:
description: description:
- Optional route-map for the default_originate property. Can be - Optional route-map for the default_originate property. Can be
used independently or in conjunction with default_originate. used independently or in conjunction with C(default_originate).
Valid values are a string defining a route-map name, Valid values are a string defining a route-map name,
or 'default'. or 'default'.
required: false required: false
@ -248,21 +248,16 @@ options:
default: null default: null
weight: weight:
description: description:
- weight value. Valid values are an integer value or 'default'. - Weight value. Valid values are an integer value or 'default'.
required: false required: false
default: null default: null
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
configure RR client configure RR client
@ -281,18 +276,19 @@ configure RR client
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"afi": "ipv4", "asn": "65535", sample: {"afi": "ipv4", "asn": "65535",
"neighbor": "3.3.3.3", "route_reflector_client": true, "neighbor": "3.3.3.3", "route_reflector_client": true,
"safi": "unicast", "vrf": "default"} "safi": "unicast", "vrf": "default"}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing configuration
returned: verbose mode
type: dict type: dict
sample: {} sample: {}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"additional_paths_receive": "inherit", sample: {"additional_paths_receive": "inherit",
"additional_paths_send": "inherit", "additional_paths_send": "inherit",
@ -327,12 +323,7 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -340,200 +331,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -545,14 +353,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -560,93 +360,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -698,303 +428,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -1176,7 +647,7 @@ def get_custom_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
custom = [ custom = [
'allowas_in_max', 'allowas_in_max',
@ -1527,13 +998,13 @@ def main():
suppress_inactive=dict(required=False, type='bool'), suppress_inactive=dict(required=False, type='bool'),
unsuppress_map=dict(required=False, type='str'), unsuppress_map=dict(required=False, type='str'),
weight=dict(required=False, type='str'), weight=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
mutually_exclusive=[['advertise_map_exist', mutually_exclusive=[['advertise_map_exist',
'advertise_map_non_exist']], 'advertise_map_non_exist']],
supports_check_mode=True) supports_check_mode=True)
@ -1635,7 +1106,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -35,12 +35,6 @@ options:
- EVPN control plane. - EVPN control plane.
required: true required: true
choices: ['true', 'false'] choices: ['true', 'false']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_evpn_global: - nxos_evpn_global:
@ -53,17 +47,17 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"nv_overlay_evpn": true} sample: {"nv_overlay_evpn": true}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing configuration
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"nv_overlay_evpn": false} sample: {"nv_overlay_evpn": false}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"nv_overlay_evpn": true} sample: {"nv_overlay_evpn": true}
updates: updates:
@ -80,213 +74,25 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import itertools
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
class ConfigLine(object):
def __init__(self, text): def to_list(val):
self.text = text if isinstance(val, (list, tuple)):
self.children = list() return list(val)
self.parents = list() elif val is not None:
self.raw = None return [val]
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -298,14 +104,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -313,93 +111,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -451,18 +179,20 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict( try:
# config options return get_module(**kwargs)
running_config=dict(aliases=['config']), except NameError:
save_config=dict(type='bool', default=False, aliases=['save']) return NetworkModule(**kwargs)
)
nxos_argument_spec = argument_spec()
def get_config(module): def get_config(module, include_defaults=False):
config = module.params['running_config'] config = module.params['config']
if not config: if not config:
try:
config = module.get_config() config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
@ -471,15 +201,22 @@ def load_config(module, candidate):
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -545,11 +282,11 @@ def get_commands(module, existing, proposed, candidate):
def main(): def main():
argument_spec = dict( argument_spec = dict(
nv_overlay_evpn=dict(required=True, type='bool'), nv_overlay_evpn=dict(required=True, type='bool'),
m_facts=dict(required=False, default=False, type='bool'), include_defaults=dict(default=True),
include_defaults=dict(default=True) config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
existing = invoke('get_existing', module) existing = invoke('get_existing', module)
@ -571,7 +308,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module) end_state = invoke('get_existing', module)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing
@ -580,10 +317,5 @@ def main():
module.exit_json(**result) module.exit_json(**result)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -24,7 +24,7 @@ DOCUMENTATION = '''
--- ---
module: nxos_evpn_vni module: nxos_evpn_vni
version_added: "2.2" version_added: "2.2"
short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI) short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI).
description: description:
- Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network - Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network
Identifier (VNI) configurations of a Nexus device. Identifier (VNI) configurations of a Nexus device.
@ -34,14 +34,14 @@ notes:
- default, where supported, restores params default value. - default, where supported, restores params default value.
- RD override is not permitted. You should set it to the default values - RD override is not permitted. You should set it to the default values
first and then reconfigure it. first and then reconfigure it.
- route_target_both, route_target_import and route_target_export valid - C(route_target_both), C(route_target_import) and
values are a list of extended communities, (i.e. ['1.2.3.4:5', '33:55']) C(route_target_export valid) values are a list of extended communities,
or the keywords 'auto' or 'default'. (i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'.
- The route_target_both property is discouraged due to the inconsistent - The C(route_target_both) property is discouraged due to the inconsistent
behavior of the property across Nexus platforms and image versions. behavior of the property across Nexus platforms and image versions.
For this reason it is recommended to use explicit 'route_target_export' For this reason it is recommended to use explicit C(route_target_export)
and 'route_target_import' properties instead of route_target_both. and C(route_target_import) properties instead of C(route_target_both).
- RD valid values are a String in one of the route-distinguisher formats, - RD valid values are a string in one of the route-distinguisher formats,
the keyword 'auto', or the keyword 'default'. the keyword 'auto', or the keyword 'default'.
options: options:
vni: vni:
@ -74,16 +74,11 @@ options:
default: null default: null
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_evpn_vni: - nxos_evpn_vni:
@ -102,19 +97,20 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"route_target_import": ["5000:10", "4100:100", sample: {"route_target_import": ["5000:10", "4100:100",
"5001:10"],"vni": "6000"} "5001:10"],"vni": "6000"}
existing: existing:
description: k/v pairs of existing EVPN VNI configuration description: k/v pairs of existing EVPN VNI configuration
returned: verbose mode
type: dict type: dict
sample: {"route_distinguisher": "70:10", "route_target_both": [], sample: {"route_distinguisher": "70:10", "route_target_both": [],
"route_target_export": [], "route_target_import": [ "route_target_export": [], "route_target_import": [
"4100:100", "5000:10"], "vni": "6000"} "4100:100", "5000:10"], "vni": "6000"}
end_state: end_state:
description: k/v pairs of EVPN VNI configuration after module execution description: k/v pairs of EVPN VNI configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"route_distinguisher": "70:10", "route_target_both": [], sample: {"route_distinguisher": "70:10", "route_target_both": [],
"route_target_export": [], "route_target_import": [ "route_target_export": [], "route_target_import": [
@ -132,12 +128,7 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -145,200 +136,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -350,14 +158,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -365,93 +165,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -503,303 +233,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -847,7 +318,7 @@ def get_route_target_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])] parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])]
config = netcfg.get_section(parents) config = netcfg.get_section(parents)
@ -943,13 +414,13 @@ def main():
route_target_both=dict(required=False, type='list'), route_target_both=dict(required=False, type='list'),
route_target_import=dict(required=False, type='list'), route_target_import=dict(required=False, type='list'),
route_target_export=dict(required=False, type='list'), route_target_export=dict(required=False, type='list'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -996,7 +467,7 @@ def main():
candidate.add(remove_commands, parents=parents) candidate.add(remove_commands, parents=parents)
result = execute_config(module, candidate) result = execute_config(module, candidate)
time.sleep(20) time.sleep(30)
candidate = CustomNetworkConfig(indent=3) candidate = CustomNetworkConfig(indent=3)
candidate.add(commands, parents=parents) candidate.add(commands, parents=parents)
@ -1005,7 +476,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
--- ---
module: nxos_feature module: nxos_feature
version_added: "2.1" version_added: "2.1"
short_description: Manage features in NX-OS switches short_description: Manage features in NX-OS switches.
description: description:
- Offers ability to enable and disable features in NX-OS - Offers ability to enable and disable features in NX-OS.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
@ -81,11 +81,6 @@ end_state:
returned: always returned: always
type: dict type: dict
sample: {"state": "disabled"} sample: {"state": "disabled"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "disabled"
updates: updates:
description: commands sent to the device description: commands sent to the device
returned: always returned: always
@ -103,217 +98,32 @@ feature:
sample: "vpc" sample: "vpc"
''' '''
import json
import collections
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -325,14 +135,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -340,93 +142,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -478,303 +210,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -820,6 +293,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -970,9 +448,11 @@ def main():
feature=dict(type='str', required=True), feature=dict(type='str', required=True),
state=dict(choices=['enabled', 'disabled'], default='enabled', state=dict(choices=['enabled', 'disabled'], default='enabled',
required=False), required=False),
include_defaults=dict(default=False) include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
feature = validate_feature(module) feature = validate_feature(module)

View file

@ -26,14 +26,16 @@ module: nxos_file_copy
version_added: "2.2" version_added: "2.2"
short_description: Copy a file to a remote NXOS device over SCP. short_description: Copy a file to a remote NXOS device over SCP.
description: description:
- Copy a file to the flash (or bootflash) remote network device on NXOS devices - Copy a file to the flash (or bootflash) remote network device
on NXOS devices.
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- The feature must be enabled with feature scp-server. - The feature must be enabled with feature scp-server.
- If the file is already present (md5 sums match), no transfer will take place. - If the file is already present (md5 sums match), no transfer will
take place.
- Check mode will tell you if the file would be copied. - Check mode will tell you if the file would be copied.
options: options:
local_file: local_file:
@ -49,7 +51,8 @@ options:
file_system: file_system:
description: description:
- The remote file system of the device. If omitted, - The remote file system of the device. If omitted,
devices that support a file_system parameter will use their default values. devices that support a file_system parameter will use
their default values.
required: false required: false
default: null default: null
''' '''
@ -87,214 +90,28 @@ import paramiko
import time import time
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -306,14 +123,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -321,93 +130,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -459,303 +198,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -764,6 +244,11 @@ def load_config(module, candidate):
# END OF COMMON CODE # END OF COMMON CODE
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -773,6 +258,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response return response
@ -850,7 +348,7 @@ def transfer_file(module, dest):
scp = SCPClient(ssh.get_transport()) scp = SCPClient(ssh.get_transport())
try: try:
scp.put(module.params['local_file'], full_remote_path) scp.put(module.params['local_file'], full_remote_path)
except Exception as e: except:
time.sleep(10) time.sleep(10)
temp_size = verify_remote_file_exists( temp_size = verify_remote_file_exists(
module, dest, file_system=module.params['file_system']) module, dest, file_system=module.params['file_system'])
@ -870,8 +368,11 @@ def main():
local_file=dict(required=True), local_file=dict(required=True),
remote_file=dict(required=False), remote_file=dict(required=False),
file_system=dict(required=False, default='bootflash:'), file_system=dict(required=False, default='bootflash:'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
local_file = module.params['local_file'] local_file = module.params['local_file']

View file

@ -25,60 +25,60 @@ DOCUMENTATION = '''
--- ---
module: nxos_hsrp module: nxos_hsrp
version_added: "2.2" version_added: "2.2"
short_description: Manages HSRP configuration on NX-OS switches short_description: Manages HSRP configuration on NX-OS switches.
description: description:
- Manages HSRP configuration on NX-OS switches - Manages HSRP configuration on NX-OS switches.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- HSRP feature needs to be enabled first on the system - HSRP feature needs to be enabled first on the system.
- SVIs must exist before using this module - SVIs must exist before using this module.
- Interface must be a L3 port before using this module - Interface must be a L3 port before using this module.
- HSRP cannot be configured on loopback interfaces - HSRP cannot be configured on loopback interfaces.
- MD5 authentication is only possible with HSRPv2 while it is ignored if - MD5 authentication is only possible with HSRPv2 while it is ignored if
HSRPv1 is used instead, while it will not raise any error. Here we allow HSRPv1 is used instead, while it will not raise any error. Here we allow
MD5 authentication only with HSRPv2 in order to enforce better practice. MD5 authentication only with HSRPv2 in order to enforce better practice.
options: options:
group: group:
description: description:
- HSRP group number - HSRP group number.
required: true required: true
interface: interface:
description: description:
- Full name of interface that is being managed for HSRP - Full name of interface that is being managed for HSRP.
required: true required: true
version: version:
description: description:
- HSRP version - HSRP version.
required: false required: false
default: 2 default: 2
choices: ['1','2'] choices: ['1','2']
priority: priority:
description: description:
- HSRP priority - HSRP priority.
required: false required: false
default: null default: null
vip: vip:
description: description:
- HSRP virtual IP address - HSRP virtual IP address.
required: false required: false
default: null default: null
auth_string: auth_string:
description: description:
- Authentication string - Authentication string.
required: false required: false
default: null default: null
auth_type: auth_type:
description: description:
- Authentication type - Authentication type.
required: false required: false
default: null default: null
choices: ['text','md5'] choices: ['text','md5']
state: state:
description: description:
- Specify desired state of the resource - Specify desired state of the resource.
required: false required: false
choices: ['present','absent'] choices: ['present','absent']
default: 'present' default: 'present'
@ -143,7 +143,6 @@ changed:
sample: true sample: true
''' '''
DEFAULT_COMMENT_TOKENS = ['#', '!']
import json import json
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
@ -154,198 +153,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
class ConfigLine(object):
def __init__(self, text): def to_list(val):
self.text = text if isinstance(val, (list, tuple)):
self.children = list() return list(val)
self.parents = list() elif val is not None:
self.raw = None return [val]
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -357,14 +175,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -372,93 +182,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -510,303 +250,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -856,6 +337,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -863,7 +349,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds) response = module.execute(cmds)
except ShellError: except ShellError:
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError: except AttributeError:
try: try:
@ -1129,8 +615,11 @@ def main():
auth_string=dict(type='str', required=False), auth_string=dict(type='str', required=False),
state=dict(choices=['absent', 'present'], required=False, state=dict(choices=['absent', 'present'], required=False,
default='present'), default='present'),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
interface = module.params['interface'].lower() interface = module.params['interface'].lower()

View file

@ -24,17 +24,18 @@ DOCUMENTATION = '''
--- ---
module: nxos_igmp module: nxos_igmp
version_added: "2.2" version_added: "2.2"
short_description: Manages IGMP global configuration short_description: Manages IGMP global configuration.
description: description:
- Manages IGMP global configuration configuration settings - Manages IGMP global configuration configuration settings.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- When state=default, all supported params will be reset to a default state - When C(state=default), all supported params will be reset to a
default state.
- If restart is set to true with other params set, the restart will happen - If restart is set to true with other params set, the restart will happen
last, i.e. after the configuration takes place last, i.e. after the configuration takes place.
options: options:
flush_routes: flush_routes:
description: description:
@ -46,19 +47,19 @@ options:
enforce_rtr_alert: enforce_rtr_alert:
description: description:
- Enables or disables the enforce router alert option check for - Enables or disables the enforce router alert option check for
IGMPv2 and IGMPv3 packets IGMPv2 and IGMPv3 packets.
required: false required: false
default: null default: null
choices: ['true', 'false'] choices: ['true', 'false']
restart: restart:
description: description:
- restarts the igmp process (using an exec config command) - Restarts the igmp process (using an exec config command).
required: false required: false
default: null default: null
choices: ['true', 'false'] choices: ['true', 'false']
state: state:
description: description:
- Manages desired state of the resource - Manages desired state of the resource.
required: false required: false
default: present default: present
choices: ['present', 'default'] choices: ['present', 'default']
@ -84,17 +85,17 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: when C(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": true} sample: {"enforce_rtr_alert": true, "flush_routes": true}
existing: existing:
description: k/v pairs of existing IGMP configuration description: k/v pairs of existing IGMP configuration
returned: when C(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": false} sample: {"enforce_rtr_alert": true, "flush_routes": false}
end_state: end_state:
description: k/v pairs of IGMP configuration after module execution description: k/v pairs of IGMP configuration after module execution
returned: when C(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"enforce_rtr_alert": true, "flush_routes": true} sample: {"enforce_rtr_alert": true, "flush_routes": true}
updates: updates:
@ -110,215 +111,28 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -330,14 +144,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -345,93 +151,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -483,303 +219,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -806,7 +283,7 @@ def get_value(arg, config):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
config = str(custom_get_config(module)) config = str(get_config(module))
for arg in args: for arg in args:
existing[arg] = get_value(arg, config) existing[arg] = get_value(arg, config)
@ -862,11 +339,11 @@ def main():
enforce_rtr_alert=dict(type='bool'), enforce_rtr_alert=dict(type='bool'),
restart=dict(type='bool', default=False), restart=dict(type='bool', default=False),
state=dict(choices=['present', 'default'], default='present'), state=dict(choices=['present', 'default'], default='present'),
m_facts=dict(required=False, default=False, type='bool'), include_defaults=dict(default=False),
include_defaults=dict(default=False) config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -909,7 +386,7 @@ def main():
if restart: if restart:
proposed['restart'] = restart proposed['restart'] = restart
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,35 +24,32 @@ DOCUMENTATION = '''
--- ---
module: nxos_igmp_interface module: nxos_igmp_interface
version_added: "2.2" version_added: "2.2"
short_description: Manages IGMP interface configuration short_description: Manages IGMP interface configuration.
description: description:
- Manages IGMP interface configuration settings - Manages IGMP interface configuration settings.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- When state=default, supported params will be reset to a default state. - When C(state=default), supported params will be reset to a default state.
These include: version, startup_query_interval, startup_query_count, These include C(version), C(startup_query_interval),
robustness, querier_timeout, query_mrt, query_interval, last_member_qrt, C(startup_query_count), C(robustness), C(querier_timeout), C(query_mrt),
last_member_query_count, group_timeout, report_llg, and immediate_leave C(query_interval), C(last_member_qrt), C(last_member_query_count),
- When state=absent, all configs for oif_prefix, oif_source, and C(group_timeout), C(report_llg), and C(immediate_leave).
oif_routemap will be removed. - When C(state=absent), all configs for C(oif_prefix), C(oif_source), and
- PIM must be enabled to use this module C(oif_routemap) will be removed.
- This module is for Layer 3 interfaces - PIM must be enabled to use this module.
- This module is for Layer 3 interfaces.
- Route-map check not performed (same as CLI) check when configuring - Route-map check not performed (same as CLI) check when configuring
route-map with 'static-oif' route-map with 'static-oif'
- If restart is set to true with other params set, the restart will happen - If restart is set to true with other params set, the restart will happen
last, i.e. after the configuration takes place last, i.e. after the configuration takes place.
- While username and password are not required params, they are
if you are not using the .netauth file. .netauth file is recommended
as it will clean up the each task in the playbook by not requiring
the username and password params for every tasks.
- Using the username and password params will override the .netauth file
options: options:
interface: interface:
description: description:
- The FULL interface name for IGMP configuration. - The full interface name for IGMP configuration.
e.g. I(Ethernet1/2).
required: true required: true
version: version:
description: description:
@ -102,7 +99,7 @@ options:
description: description:
- Sets the query interval waited after sending membership reports - Sets the query interval waited after sending membership reports
before the software deletes the group state. Values can range before the software deletes the group state. Values can range
from 1 to 25 seconds. The default is 1 second from 1 to 25 seconds. The default is 1 second.
required: false required: false
default: null default: null
last_member_query_count: last_member_query_count:
@ -156,13 +153,13 @@ options:
default: null default: null
restart: restart:
description: description:
- Restart IGMP - Restart IGMP.
required: false required: false
choices: ['true', 'false'] choices: ['true', 'false']
default: null default: null
state: state:
description: description:
- Manages desired state of the resource - Manages desired state of the resource.
required: false required: false
default: present default: present
choices: ['present', 'default'] choices: ['present', 'default']
@ -236,217 +233,32 @@ changed:
sample: true sample: true
''' '''
import json
import collections
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -458,14 +270,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -473,93 +277,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -611,303 +345,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -915,7 +390,6 @@ def load_config(module, candidate):
return result return result
# END OF COMMON CODE # END OF COMMON CODE
def get_cli_body_ssh(command, response, module): def get_cli_body_ssh(command, response, module):
"""Get response for when transport=cli. This is kind of a hack and mainly """Get response for when transport=cli. This is kind of a hack and mainly
needed because these modules were originally written for NX-API. And needed because these modules were originally written for NX-API. And
@ -939,6 +413,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -946,7 +425,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds) response = module.execute(cmds)
except ShellError: except ShellError:
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError: except AttributeError:
try: try:
@ -1272,10 +751,11 @@ def main():
restart=dict(type='bool', default=False), restart=dict(type='bool', default=False),
state=dict(choices=['present', 'absent', 'default'], state=dict(choices=['present', 'absent', 'default'],
default='present'), default='present'),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']

View file

@ -30,11 +30,11 @@ description:
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- default, where supported, restores params default value - Default, where supported, restores params default value.
- To remove an existing authentication configuration you should use - To remove an existing authentication configuration you should use
message_digest_key_id=default plus all other options matching their C(message_digest_key_id=default) plus all other options matching their
existing values. existing values.
- State absent remove the whole OSPF interface configuration - C(state=absent) removes the whole OSPF interface configuration.
options: options:
interface: interface:
description: description:
@ -84,7 +84,7 @@ options:
default: null default: null
message_digest_key_id: message_digest_key_id:
description: description:
- md5 authentication key-id associated with the ospf instance. - Md5 authentication key-id associated with the ospf instance.
If this is present, message_digest_encryption_type, If this is present, message_digest_encryption_type,
message_digest_algorithm_type and message_digest_password are message_digest_algorithm_type and message_digest_password are
mandatory. Valid value is an integer and 'default'. mandatory. Valid value is an integer and 'default'.
@ -111,16 +111,11 @@ options:
default: null default: null
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_interface_ospf: - nxos_interface_ospf:
@ -136,11 +131,12 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"} sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"}
existing: existing:
description: k/v pairs of existing OSPF configuration description: k/v pairs of existing OSPF configuration
returned: verbose mode
type: dict type: dict
sample: {"area": "", "cost": "", "dead_interval": "", sample: {"area": "", "cost": "", "dead_interval": "",
"hello_interval": "", "interface": "ethernet1/32", "hello_interval": "", "interface": "ethernet1/32",
@ -150,7 +146,7 @@ existing:
"ospf": "", "passive_interface": false} "ospf": "", "passive_interface": false}
end_state: end_state:
description: k/v pairs of OSPF configuration after module execution description: k/v pairs of OSPF configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "", sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "",
"hello_interval": "", "interface": "ethernet1/32", "hello_interval": "", "interface": "ethernet1/32",
@ -172,14 +168,8 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
@ -187,198 +177,16 @@ from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -390,14 +198,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -405,93 +205,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -543,303 +273,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -954,7 +425,7 @@ def get_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
parents = ['interface {0}'.format(module.params['interface'].capitalize())] parents = ['interface {0}'.format(module.params['interface'].capitalize())]
config = netcfg.get_section(parents) config = netcfg.get_section(parents)
if 'ospf' in config: if 'ospf' in config:
@ -1123,13 +594,13 @@ def main():
message_digest_encryption_type=dict(required=False, type='str', message_digest_encryption_type=dict(required=False, type='str',
choices=['cisco_type_7','3des']), choices=['cisco_type_7','3des']),
message_digest_password=dict(required=False, type='str'), message_digest_password=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
required_together=[['message_digest_key_id', required_together=[['message_digest_key_id',
'message_digest_algorithm_type', 'message_digest_algorithm_type',
'message_digest_encryption_type', 'message_digest_encryption_type',
@ -1197,7 +668,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,37 +24,37 @@ DOCUMENTATION = '''
--- ---
module: nxos_ip_interface module: nxos_ip_interface
version_added: "2.1" version_added: "2.1"
short_description: Manages L3 attributes for IPv4 and IPv6 interfaces short_description: Manages L3 attributes for IPv4 and IPv6 interfaces.
description: description:
- Manages Layer 3 attributes for IPv4 and IPv6 interfaces - Manages Layer 3 attributes for IPv4 and IPv6 interfaces.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- Interface must already be a L3 port when using this module - Interface must already be a L3 port when using this module.
- Logical interfaces (po, loop, svi) must be created first - Logical interfaces (po, loop, svi) must be created first.
- I(mask) must be inserted in decimal format (i.e. 24) for - C(mask) must be inserted in decimal format (i.e. 24) for
both IPv6 and IPv4. both IPv6 and IPv4.
- A single interface can have multiple IPv6 configured. - A single interface can have multiple IPv6 configured.
options: options:
interface: interface:
description: description:
- Full name of interface, i.e. Ethernet1/1, vlan10 - Full name of interface, i.e. Ethernet1/1, vlan10.
required: true required: true
addr: addr:
description: description:
- IPv4 or IPv6 Address - IPv4 or IPv6 Address.
required: false required: false
default: null default: null
mask: mask:
description: description:
- Subnet mask for IPv4 or IPv6 Address in decimal format - Subnet mask for IPv4 or IPv6 Address in decimal format.
required: false required: false
default: null default: null
state: state:
description: description:
- Specify desired state of the resource - Specify desired state of the resource.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
@ -99,11 +99,6 @@ end_state:
sample: {"addresses": [{"addr": "20.20.20.20", "mask": 24}], sample: {"addresses": [{"addr": "20.20.20.20", "mask": 24}],
"interface": "ethernet1/32", "prefix": "20.20.20.0", "interface": "ethernet1/32", "prefix": "20.20.20.0",
"type": "ethernet", "vrf": "default"} "type": "ethernet", "vrf": "default"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates: updates:
description: commands sent to the device description: commands sent to the device
returned: always returned: always
@ -116,216 +111,32 @@ changed:
sample: true sample: true
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -337,14 +148,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -352,93 +155,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -490,303 +223,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -834,6 +308,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -1157,9 +636,11 @@ def main():
mask=dict(type='str', required=False), mask=dict(type='str', required=False),
state=dict(required=False, default='present', state=dict(required=False, default='present',
choices=['present', 'absent']), choices=['present', 'absent']),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
addr = module.params['addr'] addr = module.params['addr']

View file

@ -24,6 +24,7 @@ DOCUMENTATION = '''
--- ---
module: nxos_ntp_options module: nxos_ntp_options
version_added: "2.2"
short_description: Manages NTP options. short_description: Manages NTP options.
description: description:
- Manages NTP options, e.g. authoritative server and logging. - Manages NTP options, e.g. authoritative server and logging.

View file

@ -36,16 +36,11 @@ options:
required: true required: true
state: state:
description: description:
- Determines whether the config should be present or not on the device. - Determines whether the config should be present or not
on the device.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -60,22 +55,22 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"ospf": "1"} sample: {"ospf": "1"}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing configuration
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"ospf": ["2"]} sample: {"ospf": ["2"]}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"ospf": ["1", "2"]} sample: {"ospf": ["1", "2"]}
updates: updates:
description: commands sent to the device description: commands sent to the device
returned: when I(m_facts)=true returned: always
type: list type: list
sample: ["router ospf 1"] sample: ["router ospf 1"]
changed: changed:
@ -87,213 +82,25 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import itertools
DEFAULT_COMMENT_TOKENS = ['#', '!']
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
class ConfigLine(object):
def __init__(self, text): def to_list(val):
self.text = text if isinstance(val, (list, tuple)):
self.children = list() return list(val)
self.parents = list() elif val is not None:
self.raw = None return [val]
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -305,14 +112,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -320,93 +119,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -458,18 +187,20 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict( try:
# config options return get_module(**kwargs)
running_config=dict(aliases=['config']), except NameError:
save_config=dict(type='bool', default=False, aliases=['save']) return NetworkModule(**kwargs)
)
nxos_argument_spec = argument_spec()
def get_config(module): def get_config(module, include_defaults=False):
config = module.params['running_config'] config = module.params['config']
if not config: if not config:
try:
config = module.get_config() config = module.get_config()
except AttributeError:
defaults = module.params['include_defaults']
config = module.config.get_config(include_defaults=defaults)
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
@ -478,15 +209,22 @@ def load_config(module, candidate):
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -560,13 +298,13 @@ def state_absent(module, proposed, candidate):
def main(): def main():
argument_spec = dict( argument_spec = dict(
ospf=dict(required=True, type='str'), ospf=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -596,7 +334,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module) end_state = invoke('get_existing', module)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing
@ -605,11 +343,5 @@ def main():
module.exit_json(**result) module.exit_json(**result)
from ansible.module_utils.basic import *
from ansible.module_utils.urls import *
from ansible.module_utils.shell import *
from ansible.module_utils.netcfg import *
from ansible.module_utils.nxos import *
if __name__ == '__main__': if __name__ == '__main__':
main() main()

View file

@ -111,12 +111,6 @@ options:
Valid values are an integer, in Mbps, or the keyword 'default'. Valid values are an integer, in Mbps, or the keyword 'default'.
required: false required: false
default: null default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -129,7 +123,6 @@ EXAMPLES = '''
timer_throttle_lsa_hold: 1100 timer_throttle_lsa_hold: 1100
timer_throttle_lsa_max: 3000 timer_throttle_lsa_max: 3000
vrf: test vrf: test
m_facts: true
state: present state: present
username: "{{ un }}" username: "{{ un }}"
password: "{{ pwd }}" password: "{{ pwd }}"
@ -139,7 +132,7 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100", sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100",
"timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60", "timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60",
@ -148,7 +141,7 @@ proposed:
"vrf": "test"} "vrf": "test"}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing configuration
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000", "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000",
@ -158,7 +151,7 @@ existing:
"timer_throttle_spf_start": "200", "vrf": "test"} "timer_throttle_spf_start": "200", "vrf": "test"}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of configuration after module execution
returned: when I(m_facts)=true returned: verbose mode
type: dict type: dict
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100", "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100",
@ -180,12 +173,7 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -193,200 +181,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -398,14 +203,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -413,93 +210,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -551,303 +278,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -855,6 +323,7 @@ def load_config(module, candidate):
return result return result
# END OF COMMON CODE # END OF COMMON CODE
PARAM_TO_COMMAND_KEYMAP = { PARAM_TO_COMMAND_KEYMAP = {
'router_id': 'router-id', 'router_id': 'router-id',
'default_metric': 'default-metric', 'default_metric': 'default-metric',
@ -912,7 +381,7 @@ def get_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
netcfg = custom_get_config(module) netcfg = get_config(module)
parents = ['router ospf {0}'.format(module.params['ospf'])] parents = ['router ospf {0}'.format(module.params['ospf'])]
if module.params['vrf'] != 'default': if module.params['vrf'] != 'default':
@ -1052,13 +521,13 @@ def main():
timer_throttle_spf_hold=dict(required=False, type='str'), timer_throttle_spf_hold=dict(required=False, type='str'),
timer_throttle_spf_max=dict(required=False, type='str'), timer_throttle_spf_max=dict(required=False, type='str'),
auto_cost=dict(required=False, type='str'), auto_cost=dict(required=False, type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
state = module.params['state'] state = module.params['state']
@ -1111,7 +580,7 @@ def main():
result['updates'] = [] result['updates'] = []
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -30,7 +30,7 @@ description:
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- default, where supported, restores params default value - Default restores params default value
- Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE", - Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE",
"EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE" "EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE"
options: options:
@ -39,13 +39,8 @@ options:
- Anycast gateway mac of the switch. - Anycast gateway mac of the switch.
required: true required: true
default: null default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_overlay_global: - nxos_overlay_global:
anycast_gateway_mac: "b.b.b" anycast_gateway_mac: "b.b.b"
@ -57,23 +52,56 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"12:34:56:78:9a:bc"} sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing BGP configuration
returned: verbose mode
type: dict type: dict
sample: {"anycast_gateway_mac": "000E.000E.000E"} sample: {"asn": "65535", "bestpath_always_compare_med": false,
"bestpath_aspath_multipath_relax": false,
"bestpath_compare_neighborid": false,
"bestpath_compare_routerid": false,
"bestpath_cost_community_ignore": false,
"bestpath_med_confed": false,
"bestpath_med_missing_as_worst": false,
"bestpath_med_non_deterministic": false, "cluster_id": "",
"confederation_id": "", "confederation_peers": "",
"graceful_restart": true, "graceful_restart_helper": false,
"graceful_restart_timers_restart": "120",
"graceful_restart_timers_stalepath_time": "300", "local_as": "",
"log_neighbor_changes": false, "maxas_limit": "",
"neighbor_down_fib_accelerate": false, "reconnect_interval": "60",
"router_id": "11.11.11.11", "suppress_fib_pending": false,
"timer_bestpath_limit": "", "timer_bgp_hold": "180",
"timer_bgp_keepalive": "60", "vrf": "test"}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of BGP configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"anycast_gateway_mac": "1234.5678.9ABC"} sample: {"asn": "65535", "bestpath_always_compare_med": false,
"bestpath_aspath_multipath_relax": false,
"bestpath_compare_neighborid": false,
"bestpath_compare_routerid": false,
"bestpath_cost_community_ignore": false,
"bestpath_med_confed": false,
"bestpath_med_missing_as_worst": false,
"bestpath_med_non_deterministic": false, "cluster_id": "",
"confederation_id": "", "confederation_peers": "",
"graceful_restart": true, "graceful_restart_helper": false,
"graceful_restart_timers_restart": "120",
"graceful_restart_timers_stalepath_time": "300", "local_as": "",
"log_neighbor_changes": false, "maxas_limit": "",
"neighbor_down_fib_accelerate": false, "reconnect_interval": "60",
"router_id": "1.1.1.1", "suppress_fib_pending": false,
"timer_bestpath_limit": "", "timer_bgp_hold": "180",
"timer_bgp_keepalive": "60", "vrf": "test"}
updates: updates:
description: commands sent to the device description: commands sent to the device
returned: always returned: always
type: list type: list
sample: ["fabric forwarding anycast-gateway-mac 1234.5678.9ABC"] sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"]
changed: changed:
description: check to see if a change was made on the device description: check to see if a change was made on the device
returned: always returned: always
@ -82,214 +110,28 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -301,14 +143,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -316,93 +150,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -454,303 +218,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -779,7 +284,7 @@ def get_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
config = str(custom_get_config(module)) config = str(get_config(module))
for arg in args: for arg in args:
existing[arg] = get_value(arg, config, module) existing[arg] = get_value(arg, config, module)
@ -867,10 +372,11 @@ def main():
argument_spec = dict( argument_spec = dict(
anycast_gateway_mac=dict(required=True, type='str'), anycast_gateway_mac=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'), m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
args = [ args = [
@ -894,7 +400,7 @@ def main():
module.fail_json(msg=str(exc)) module.fail_json(msg=str(exc))
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
--- ---
module: nxos_pim module: nxos_pim
version_added: "2.2" version_added: "2.2"
short_description: Manages configuration of an Protocol Independent Multicast (PIM) instance. short_description: Manages configuration of a PIM instance.
description: description:
- Manages configuration of an Protocol Independent Multicast (PIM) instance. - Manages configuration of a Protocol Independent Multicast (PIM) instance.
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
options: options:
@ -35,12 +35,6 @@ options:
- Configure group ranges for Source Specific Multicast (SSM). - Configure group ranges for Source Specific Multicast (SSM).
Valid values are multicast addresses or the keyword 'none'. Valid values are multicast addresses or the keyword 'none'.
required: true required: true
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_pim: - nxos_pim:
@ -53,16 +47,17 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"ssm_range": "232.0.0.0/8"} sample: {"ssm_range": "232.0.0.0/8"}
existing: existing:
description: k/v pairs of existing PIM configuration description: k/v pairs of existing PIM configuration
returned: verbose mode
type: dict type: dict
sample: {"ssm_range": none} sample: {"ssm_range": none}
end_state: end_state:
description: k/v pairs of BGP configuration after module execution description: k/v pairs of BGP configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"ssm_range": "232.0.0.0/8"} sample: {"ssm_range": "232.0.0.0/8"}
updates: updates:
@ -79,214 +74,28 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -298,14 +107,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -313,93 +114,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -451,303 +182,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -779,7 +251,7 @@ def get_value(arg, config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
config = str(custom_get_config(module)) config = str(get_config(module))
for arg in args: for arg in args:
existing[arg] = get_value(arg, config, module) existing[arg] = get_value(arg, config, module)
return existing return existing
@ -815,10 +287,11 @@ def main():
argument_spec = dict( argument_spec = dict(
ssm_range=dict(required=True, type='str'), ssm_range=dict(required=True, type='str'),
m_facts=dict(required=False, default=False, type='bool'), m_facts=dict(required=False, default=False, type='bool'),
include_defaults=dict(default=False) include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
splitted_ssm_range = module.params['ssm_range'].split('.') splitted_ssm_range = module.params['ssm_range'].split('.')
@ -847,7 +320,7 @@ def main():
module.fail_json(msg=str(exc)) module.fail_json(msg=str(exc))
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,15 +24,14 @@ DOCUMENTATION = '''
--- ---
module: nxos_pim_rp_address module: nxos_pim_rp_address
version_added: "2.2" version_added: "2.2"
short_description: Manages configuration of an Protocol Independent Multicast short_description: Manages configuration of an PIM static RP address instance.
(PIM) static rendezvous point (RP) address instance.
description: description:
- Manages configuration of an Protocol Independent Multicast (PIM) static - Manages configuration of an Protocol Independent Multicast (PIM) static
rendezvous point (RP) address instance. rendezvous point (RP) address instance.
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- state=absent remove the whole rp-address configuration, if existing. - C(state=absent) remove the whole rp-address configuration, if existing.
options: options:
rp_address: rp_address:
description: description:
@ -63,12 +62,6 @@ options:
required: false required: false
choices: ['true','false'] choices: ['true','false']
default: null default: null
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
- nxos_pim_rp_address: - nxos_pim_rp_address:
@ -82,16 +75,17 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"rp_address": "10.1.1.21"} sample: {"rp_address": "10.1.1.21"}
existing: existing:
description: list of existing pim rp-address configuration entries description: list of existing pim rp-address configuration entries
returned: verbose mode
type: list type: list
sample: [] sample: []
end_state: end_state:
description: pim rp-address configuration entries after module execution description: pim rp-address configuration entries after module execution
returned: always returned: verbose mode
type: list type: list
sample: [{"bidir": false, "group_list": "224.0.0.0/4", sample: [{"bidir": false, "group_list": "224.0.0.0/4",
"rp_address": "10.1.1.21"}] "rp_address": "10.1.1.21"}]
@ -110,214 +104,28 @@ changed:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -329,14 +137,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -344,93 +144,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -482,303 +212,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -825,7 +296,7 @@ def get_value(config, module):
def get_existing(module, args): def get_existing(module, args):
existing = {} existing = {}
config = str(custom_get_config(module)) config = str(get_config(module))
existing = get_value(config, module) existing = get_value(config, module)
return existing return existing
@ -880,13 +351,13 @@ def main():
prefix_list=dict(required=False, type='str'), prefix_list=dict(required=False, type='str'),
route_map=dict(required=False, type='str'), route_map=dict(required=False, type='str'),
bidir=dict(required=False, type='bool'), bidir=dict(required=False, type='bool'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
include_defaults=dict(default=False) include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
mutually_exclusive=[['group_list', 'route_map'], mutually_exclusive=[['group_list', 'route_map'],
['group_list', 'prefix_list'], ['group_list', 'prefix_list'],
['route_map', 'prefix_list']], ['route_map', 'prefix_list']],
@ -929,7 +400,7 @@ def main():
module.fail_json(msg=str(exc)) module.fail_json(msg=str(exc))
result['connected'] = module.connected result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, args) end_state = invoke('get_existing', module, args)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -24,29 +24,31 @@ DOCUMENTATION = '''
--- ---
module: nxos_ping module: nxos_ping
version_added: "2.1" version_added: "2.1"
short_description: Tests reachability using ping from Nexus switch short_description: Tests reachability using ping from Nexus switch.
description: description:
- Tests reachability using ping from switch to a remote destination - Tests reachability using ping from switch to a remote destination.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
options: options:
dest: dest:
description: description:
- IP address or hostname (resolvable by switch) of remote node - IP address or hostname (resolvable by switch) of remote node.
required: true required: true
count: count:
description: description:
- Number of packets to send - Number of packets to send.
required: false required: false
default: 2 default: 2
source: source:
description: description:
- Source IP Address - Source IP Address.
required: false required: false
default: null default: null
vrf: vrf:
description: description:
- Outgoing VRF - Outgoing VRF.
required: false required: false
default: null default: null
''' '''
@ -114,216 +116,32 @@ packet_loss:
sample: "0.00%" sample: "0.00%"
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -335,14 +153,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -350,93 +160,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -488,303 +228,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -833,6 +314,11 @@ def get_statistics_summary_line(response_as_list):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -842,6 +328,19 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response return response
@ -885,8 +384,11 @@ def main():
source=dict(required=False), source=dict(required=False),
state=dict(required=False, choices=['present', 'absent'], state=dict(required=False, choices=['present', 'absent'],
default='present'), default='present'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
destination = module.params['dest'] destination = module.params['dest']

View file

@ -26,7 +26,7 @@ module: nxos_reboot
version_added: 2.2 version_added: 2.2
short_description: Reboot a network device. short_description: Reboot a network device.
description: description:
- Reboot a network device - Reboot a network device.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
@ -58,215 +58,32 @@ rebooted:
sample: true sample: true
''' '''
# COMMON CODE FOR MIGRATION import json
import re
import time
import collections import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -278,14 +95,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -293,93 +102,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -431,303 +170,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -743,6 +223,11 @@ def reboot(module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -752,12 +237,25 @@ def execute_show(cmds, module, command_type=None):
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response return response
def execute_show_command(command, module, command_type='cli_show'): def execute_show_command(command, module, command_type='cli_show'):
if module.params['transport'] == 'cli': if module.params['transport'] == 'cli':
body = execute_show(command, module, reboot=reboot) body = execute_show(command, module)
elif module.params['transport'] == 'nxapi': elif module.params['transport'] == 'nxapi':
body = execute_show(command, module, command_type=command_type) body = execute_show(command, module, command_type=command_type)
@ -765,16 +263,18 @@ def execute_show_command(command, module, command_type='cli_show'):
def disable_confirmation(module): def disable_confirmation(module):
command = 'terminal dont-ask' command = ['terminal dont-ask']
body = execute_show_command(command, module, command_type='cli_show_ascii')[0] body = execute_show_command(command, module, command_type='cli_show_ascii')[0]
def main(): def main():
argument_spec = dict( argument_spec = dict(
confirm=dict(required=True, type='bool'), confirm=dict(required=True, type='bool'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
confirm = module.params['confirm'] confirm = module.params['confirm']

View file

@ -23,24 +23,29 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: nxos_rollback module: nxos_rollback
short_description: Set a checkpoint or rollback to a checkpoint version_added: "2.2"
short_description: Set a checkpoint or rollback to a checkpoint.
description: description:
- This module offers the ability to set a configuration checkpoint file or rollback - This module offers the ability to set a configuration checkpoint
to a configuration checkpoint file on Cisco NXOS switches- file or rollback to a configuration checkpoint file on Cisco NXOS
switches.
extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)
notes: notes:
- Sometimes C(transport)=nxapi may cause a timeout error. - Sometimes C(transport=nxapi) may cause a timeout error.
options: options:
checkpoint_file: checkpoint_file:
description: description:
- Name of checkpoint file to create. Mutually exclusive with rollback_to. - Name of checkpoint file to create. Mutually exclusive
with rollback_to.
required: false required: false
default: null default: null
rollback_to: rollback_to:
description: description:
- Name of checkpoint file to rollback to. Mutually exclusive with checkpoint_file. - Name of checkpoint file to rollback to. Mutually exclusive
with checkpoint_file.
required: false required: false
default: null default: null
''' '''
@ -73,214 +78,28 @@ status:
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -292,14 +111,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -307,93 +118,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -445,303 +186,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST', timeout=20)
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -751,15 +233,34 @@ def load_config(module, candidate):
def execute_commands(cmds, module, command_type=None): def execute_commands(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
else: else:
module.execute(cmds) response = module.execute(cmds)
except ShellError: except ShellError:
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError:
try:
if command_type:
command_type = command_type_map.get(command_type)
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
else:
module.cli.add_commands(cmds, output=command_type)
response = module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie))
return response
def prepare_show_command(command, module): def prepare_show_command(command, module):
@ -776,16 +277,27 @@ def checkpoint(filename, module):
def rollback(filename, module): def rollback(filename, module):
commands = ['rollback running-config file %s' % filename] commands = ['rollback running-config file %s' % filename]
try:
module.configure(commands) module.configure(commands)
except AttributeError:
try:
module.cli.add_commands(commands, output='config')
module.cli.run_commands()
except ShellError:
clie = get_exception()
module.fail_json(msg='Error sending CLI commands',
error=str(clie), commands=commands)
def main(): def main():
argument_spec = dict( argument_spec = dict(
checkpoint_file=dict(required=False), checkpoint_file=dict(required=False),
rollback_to=dict(required=False), rollback_to=dict(required=False),
include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec) module = get_network_module(argument_spec=argument_spec,
module = get_module(argument_spec=argument_spec,
mutually_exclusive=[['checkpoint_file', mutually_exclusive=[['checkpoint_file',
'rollback_to']], 'rollback_to']],
supports_check_mode=False) supports_check_mode=False)

View file

@ -32,11 +32,11 @@ author: Gabriele Gerbino (@GGabriele)
notes: notes:
- The module can only activate and commit a package, - The module can only activate and commit a package,
not remove or deactivate it. not remove or deactivate it.
- Use I(transport)=nxapi to avoid connection timeout - Use C(transport=nxapi) to avoid connection timeout
options: options:
pkg: pkg:
description: description:
- Name of the remote package - Name of the remote package.
required: true required: true
file_system: file_system:
description: description:
@ -81,215 +81,32 @@ changed:
''' '''
import time import time
# COMMON CODE FOR MIGRATION import json
import re
import time
import collections import collections
import itertools
import shlex
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -301,14 +118,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -316,93 +125,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -454,303 +193,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -759,6 +239,11 @@ def load_config(module, candidate):
# END OF COMMON CODE # END OF COMMON CODE
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -861,8 +346,11 @@ def main():
argument_spec = dict( argument_spec = dict(
pkg=dict(required=True), pkg=dict(required=True),
file_system=dict(required=False, default='bootflash:'), file_system=dict(required=False, default='bootflash:'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
pkg = module.params['pkg'] pkg = module.params['pkg']

View file

@ -1,18 +1,21 @@
#!/usr/bin/env python #!/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 <http://www.gnu.org/licenses/>.
#
# Copyright 2015 Jason Edelman <jedelman8@gmail.com>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
ANSIBLE_METADATA = {'status': ['preview'], ANSIBLE_METADATA = {'status': ['preview'],
'supported_by': 'community', 'supported_by': 'community',
@ -26,7 +29,9 @@ short_description: Manages SNMP host configuration.
description: description:
- Manages SNMP host configuration parameters. - Manages SNMP host configuration parameters.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: Jason Edelman (@jedelman8) author:
- Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele)
notes: notes:
- C(state=absent) removes the host configuration if it is configured. - C(state=absent) removes the host configuration if it is configured.
options: options:

View file

@ -24,9 +24,11 @@ ANSIBLE_METADATA = {'status': ['preview'],
DOCUMENTATION = ''' DOCUMENTATION = '''
--- ---
module: nxos_snmp_location module: nxos_snmp_location
version_added: "2.2"
short_description: Manages SNMP location information. short_description: Manages SNMP location information.
description: description:
- Manages SNMP location configuration. - Manages SNMP location configuration.
extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
- Gabriele Gerbino (@GGabriele) - Gabriele Gerbino (@GGabriele)

View file

@ -29,12 +29,13 @@ description:
author: Gabriele Gerbino (@GGabriele) author: Gabriele Gerbino (@GGabriele)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
notes: notes:
- If no vrf is supplied, vrf is set to default - If no vrf is supplied, vrf is set to default.
- If state=absent, the route will be removed, regardless of the non-required parameters. - If C(state=absent), the route will be removed, regardless of the
non-required parameters.
options: options:
prefix: prefix:
description: description:
- Destination prefix of static route - Destination prefix of static route.
required: true required: true
next_hop: next_hop:
description: description:
@ -43,7 +44,7 @@ options:
required: true required: true
vrf: vrf:
description: description:
- VRF for static route - VRF for static route.
required: false required: false
default: default default: default
tag: tag:
@ -58,20 +59,14 @@ options:
default: null default: null
pref: pref:
description: description:
- Preference or administrative difference of route (range 1-255) - Preference or administrative difference of route (range 1-255).
required: false required: false
default: null default: null
state: state:
description: description:
- Manage the state of the resource - Manage the state of the resource.
required: true required: true
choices: ['present','absent'] choices: ['present','absent']
m_facts:
description:
- Used to print module facts
required: false
default: false
choices: ['true','false']
''' '''
EXAMPLES = ''' EXAMPLES = '''
@ -88,18 +83,19 @@ EXAMPLES = '''
RETURN = ''' RETURN = '''
proposed: proposed:
description: k/v pairs of parameters passed into module description: k/v pairs of parameters passed into module
returned: always returned: verbose mode
type: dict type: dict
sample: {"next_hop": "3.3.3.3", "pref": "100", sample: {"next_hop": "3.3.3.3", "pref": "100",
"prefix": "192.168.20.64/24", "route_name": "testing", "prefix": "192.168.20.64/24", "route_name": "testing",
"vrf": "default"} "vrf": "default"}
existing: existing:
description: k/v pairs of existing configuration description: k/v pairs of existing configuration
returned: verbose mode
type: dict type: dict
sample: {} sample: {}
end_state: end_state:
description: k/v pairs of configuration after module execution description: k/v pairs of configuration after module execution
returned: always returned: verbose mode
type: dict type: dict
sample: {"next_hop": "3.3.3.3", "pref": "100", sample: {"next_hop": "3.3.3.3", "pref": "100",
"prefix": "192.168.20.0/24", "route_name": "testing", "prefix": "192.168.20.0/24", "route_name": "testing",
@ -117,193 +113,42 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine, dumps from ansible.module_utils.netcfg import NetworkConfig, ConfigLine, dumps
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line) return list()
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors): class CustomNetworkConfig(NetworkConfig):
config.append(cfg)
def expand_section(self, configobj, S=None):
if S is None:
S = list()
S.append(configobj)
for child in configobj.children:
if child in S:
continue continue
self.expand_section(child, S)
return S
for i in range(level, len(ancestors)): def get_object(self, path):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items: for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]: if item.text == path[-1]:
parents = [p.text for p in item.parents] parents = [p.text for p in item.parents]
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section): def to_block(self, section):
return '\n'.join([item.raw for item in section]) return '\n'.join([item.raw for item in section])
@ -324,119 +169,6 @@ class CustomNetworkConfig(object):
raise ValueError('path does not exist in config') raise ValueError('path does not exist in config')
return self.expand_section(obj) return self.expand_section(obj)
def expand_section(self, configobj, S=None):
if S is None:
S = list()
S.append(configobj)
for child in configobj.children:
if child in S:
continue
self.expand_section(child, S)
return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path):
for item in self.items:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def get_children(self, path):
obj = self.get_object(path)
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try:
if item != current[index]:
updates.append(item)
except IndexError:
updates.append(item)
elif match == 'exact':
if path:
current = other.get_children(path) or list()
else:
current = other.items
if len(current) != len(config):
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
"""Adds one or lines of configuration """Adds one or lines of configuration
@ -487,303 +219,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -809,7 +282,7 @@ def state_present(module, candidate, prefix):
def state_absent(module, candidate, prefix): def state_absent(module, candidate, prefix):
netcfg = custom_get_config(module) netcfg = get_config(module)
commands = list() commands = list()
parents = 'vrf context {0}'.format(module.params['vrf']) parents = 'vrf context {0}'.format(module.params['vrf'])
invoke('set_route', module, commands, prefix) invoke('set_route', module, commands, prefix)
@ -828,17 +301,13 @@ def state_absent(module, candidate, prefix):
def fix_prefix_to_regex(prefix): def fix_prefix_to_regex(prefix):
prefix = prefix.split('.') prefix = prefix.replace('.', '\.').replace('/', '\/')
prefix = '\.'.join(prefix)
prefix = prefix.split('/')
prefix = '\/'.join(prefix)
return prefix return prefix
def get_existing(module, prefix, warnings): def get_existing(module, prefix, warnings):
key_map = ['tag', 'pref', 'route_name', 'next_hop'] key_map = ['tag', 'pref', 'route_name', 'next_hop']
netcfg = custom_get_config(module) netcfg = get_config(module)
parents = 'vrf context {0}'.format(module.params['vrf']) parents = 'vrf context {0}'.format(module.params['vrf'])
prefix_to_regex = fix_prefix_to_regex(prefix) prefix_to_regex = fix_prefix_to_regex(prefix)
@ -952,16 +421,17 @@ def main():
tag=dict(type='str'), tag=dict(type='str'),
route_name=dict(type='str'), route_name=dict(type='str'),
pref=dict(type='str'), pref=dict(type='str'),
m_facts=dict(required=False, default=False, type='bool'),
state=dict(choices=['absent', 'present'], state=dict(choices=['absent', 'present'],
default='present'), default='present'),
include_defaults=dict(default=True) include_defaults=dict(default=True),
config=dict(),
save=dict(type='bool', default=False)
) )
argument_spec.update(nxos_argument_spec)
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
m_facts = module.params['m_facts']
state = module.params['state'] state = module.params['state']
result = dict(changed=False) result = dict(changed=False)
@ -981,16 +451,15 @@ def main():
try: try:
response = load_config(module, candidate) response = load_config(module, candidate)
result.update(response) result.update(response)
except ShellError: except Exception:
exc = get_exception() exc = get_exception()
module.fail_json(msg=str(exc)) module.fail_json(msg=str(exc))
else: else:
result['updates'] = [] result['updates'] = []
result['warnings'] = warnings result['warnings'] = warnings
result['connected'] = module.connected
if module.params['m_facts']: if module._verbosity > 0:
end_state = invoke('get_existing', module, prefix, warnings) end_state = invoke('get_existing', module, prefix, warnings)
result['end_state'] = end_state result['end_state'] = end_state
result['existing'] = existing result['existing'] = existing

View file

@ -145,8 +145,8 @@ end_state:
updates: updates:
description: command string sent to the device description: command string sent to the device
returned: always returned: always
type: string type: list
sample: "interface eth1/5 ; switchport access vlan 20 ;" sample: ["interface eth1/5", "switchport access vlan 20"]
changed: changed:
description: check to see if a change was made on the device description: check to see if a change was made on the device
returned: always returned: always

View file

@ -13,7 +13,7 @@
# GNU General Public License for more details. # GNU General Public License for more details.
# #
# You should have received a copy of the GNU General Public License # You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/> . # along with Ansible. If not, see <http://www.gnu.org/licenses/>.
# #
ANSIBLE_METADATA = {'status': ['preview'], ANSIBLE_METADATA = {'status': ['preview'],
@ -24,51 +24,51 @@ DOCUMENTATION = '''
--- ---
module: nxos_vlan module: nxos_vlan
version_added: "2.1" version_added: "2.1"
short_description: Manages VLAN resources and attributes short_description: Manages VLAN resources and attributes.
description: description:
- Manages VLAN configurations on NX-OS switches - Manages VLAN configurations on NX-OS switches.
author: Jason Edelman (@jedelman8) author: Jason Edelman (@jedelman8)
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
options: options:
vlan_id: vlan_id:
description: description:
- single vlan id - Single VLAN ID.
required: false required: false
default: null default: null
vlan_range: vlan_range:
description: description:
- range of VLANs such as 2-10 or 2,5,10-15, etc. - Range of VLANs such as 2-10 or 2,5,10-15, etc.
required: false required: false
default: null default: null
name: name:
description: description:
- name of VLAN - Name of VLAN.
required: false required: false
default: null default: null
vlan_state: vlan_state:
description: description:
- Manage the vlan operational state of the VLAN - Manage the vlan operational state of the VLAN
(equivalent to state {active | suspend} command (equivalent to state {active | suspend} command.
required: false required: false
default: active default: active
choices: ['active','suspend'] choices: ['active','suspend']
admin_state: admin_state:
description: description:
- Manage the vlan admin state of the VLAN equivalent - Manage the VLAN administrative state of the VLAN equivalent
to shut/no shut in vlan config mode to shut/no shut in VLAN config mode.
required: false required: false
default: up default: up
choices: ['up','down'] choices: ['up','down']
mapped_vni: mapped_vni:
description: description:
- The Virtual Network Identifier (VNI) id that is mapped to the - The Virtual Network Identifier (VNI) ID that is mapped to the
VLAN. Valid values are integer and keyword 'default'. VLAN. Valid values are integer and keyword 'default'.
required: false required: false
default: null default: null
version_added: "2.2" version_added: "2.2"
state: state:
description: description:
- Manage the state of the resource - Manage the state of the resource.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
@ -141,11 +141,6 @@ end_state:
type: dict or null type: dict or null
sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20", sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20",
"vlan_state": "suspend", "mapped_vni": "5000"} "vlan_state": "suspend", "mapped_vni": "5000"}
state:
description: state as sent in from the playbook
returned: always
type: string
sample: "present"
updates: updates:
description: command string sent to the device description: command string sent to the device
returned: always returned: always
@ -159,216 +154,32 @@ changed:
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections
import itertools
import shlex
import json import json
import collections
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -380,14 +191,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -395,93 +198,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -533,303 +266,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -1035,6 +509,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -1042,7 +521,7 @@ def execute_show(cmds, module, command_type=None):
response = module.execute(cmds) response = module.execute(cmds)
except ShellError: except ShellError:
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending {0}'.format(command), module.fail_json(msg='Error sending {0}'.format(cmds),
error=str(clie)) error=str(clie))
except AttributeError: except AttributeError:
try: try:
@ -1084,8 +563,11 @@ def main():
state=dict(choices=['present', 'absent'], default='present', state=dict(choices=['present', 'absent'], default='present',
required=False), required=False),
admin_state=dict(choices=['up', 'down'], required=False), admin_state=dict(choices=['up', 'down'], required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['vlan_range', 'name'], mutually_exclusive=[['vlan_range', 'name'],
['vlan_id', 'vlan_range']], ['vlan_id', 'vlan_range']],
supports_check_mode=True) supports_check_mode=True)

View file

@ -149,215 +149,28 @@ import json
import collections import collections
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import get_exception
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO from ansible.module_utils.shell import ShellError
from ansible.module_utils.netcfg import parse
from ansible.module_utils.urls import fetch_url try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -369,14 +182,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -384,93 +189,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -522,303 +257,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -865,6 +341,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -1092,9 +573,11 @@ def main():
auto_recovery=dict(required=True, type='bool'), auto_recovery=dict(required=True, type='bool'),
delay_restore=dict(required=False, type='str'), delay_restore=dict(required=False, type='str'),
state=dict(choices=['absent', 'present'], default='present'), state=dict(choices=['absent', 'present'], default='present'),
include_defaults=dict(default=False) include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
domain = module.params['domain'] domain = module.params['domain']

View file

@ -41,11 +41,11 @@ notes:
options: options:
portchannel: portchannel:
description: description:
- group number of the portchannel that will be configured - Group number of the portchannel that will be configured.
required: true required: true
vpc: vpc:
description: description:
- vpc group/id that will be configured on associated portchannel - VPC group/id that will be configured on associated portchannel.
required: false required: false
default: null default: null
peer_link: peer_link:
@ -55,7 +55,7 @@ options:
default: null default: null
state: state:
description: description:
- Manages desired state of the resource - Manages desired state of the resource.
required: true required: true
choices: ['present','absent'] choices: ['present','absent']
''' '''
@ -96,216 +96,33 @@ changed:
sample: true sample: true
''' '''
# COMMON CODE FOR MIGRATION
import re
import time
import collections import collections
import itertools
import shlex
import json import json
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception # COMMON CODE FOR MIGRATION
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE import re
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
from ansible.module_utils.netcfg import parse from ansible.module_utils.basic import get_exception
from ansible.module_utils.urls import fetch_url from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError
try:
from ansible.module_utils.nxos import get_module
except ImportError:
from ansible.module_utils.nxos import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!'] def to_list(val):
if isinstance(val, (list, tuple)):
class ConfigLine(object): return list(val)
elif val is not None:
def __init__(self, text): return [val]
self.text = text
self.children = list()
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -317,14 +134,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -332,93 +141,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -470,303 +209,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -776,7 +256,7 @@ def load_config(module, candidate):
def execute_config_command(commands, module): def execute_config_command(commands, module):
try: try:
output = module.configure(commands) response = module.configure(commands)
except ShellError: except ShellError:
clie = get_exception() clie = get_exception()
module.fail_json(msg='Error sending CLI commands', module.fail_json(msg='Error sending CLI commands',
@ -812,6 +292,11 @@ def get_cli_body_ssh(command, response, module):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -996,8 +481,11 @@ def main():
vpc=dict(required=False, type='str'), vpc=dict(required=False, type='str'),
peer_link=dict(required=False, type='bool'), peer_link=dict(required=False, type='bool'),
state=dict(choices=['absent', 'present'], default='present'), state=dict(choices=['absent', 'present'], default='present'),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
mutually_exclusive=[['vpc', 'peer_link']], mutually_exclusive=[['vpc', 'peer_link']],
supports_check_mode=True) supports_check_mode=True)

View file

@ -24,9 +24,9 @@ DOCUMENTATION = '''
--- ---
module: nxos_vrf module: nxos_vrf
version_added: "2.1" version_added: "2.1"
short_description: Manages global VRF configuration short_description: Manages global VRF configuration.
description: description:
- Manages global VRF configuration - Manages global VRF configuration.
extends_documentation_fragment: nxos extends_documentation_fragment: nxos
author: author:
- Jason Edelman (@jedelman8) - Jason Edelman (@jedelman8)
@ -34,20 +34,20 @@ author:
notes: notes:
- Cisco NX-OS creates the default VRF by itself. Therefore, - Cisco NX-OS creates the default VRF by itself. Therefore,
you're not allowed to use default as I(vrf) name in this module. you're not allowed to use default as I(vrf) name in this module.
- I(vrf) name must be shorter than 32 chars. - C(vrf) name must be shorter than 32 chars.
- VRF names are not case sensible in NX-OS. Anyway, the name is stored - VRF names are not case sensible in NX-OS. Anyway, the name is stored
just like it's inserted by the user and it'll not be changed again just like it's inserted by the user and it'll not be changed again
unless the VRF is removed and re-created. i.e. I(vrf=NTC) will create unless the VRF is removed and re-created. i.e. C(vrf=NTC) will create
a VRF named NTC, but running it again with I(vrf=ntc) will not cause a VRF named NTC, but running it again with C(vrf=ntc) will not cause
a configuration change. a configuration change.
options: options:
vrf: vrf:
description: description:
- Name of VRF to be managed - Name of VRF to be managed.
required: true required: true
admin_state: admin_state:
description: description:
- Administrative state of the VRF - Administrative state of the VRF.
required: false required: false
default: up default: up
choices: ['up','down'] choices: ['up','down']
@ -68,13 +68,13 @@ options:
version_added: "2.2" version_added: "2.2"
state: state:
description: description:
- Manages desired state of the resource - Manages desired state of the resource.
required: false required: false
default: present default: present
choices: ['present','absent'] choices: ['present','absent']
description: description:
description: description:
- Description of the VRF - Description of the VRF.
required: false required: false
default: null default: null
''' '''
@ -121,13 +121,7 @@ changed:
import json import json
# COMMON CODE FOR MIGRATION # COMMON CODE FOR MIGRATION
import re import re
import time
import collections
import itertools
import shlex
import json
import ansible.module_utils.nxos import ansible.module_utils.nxos
from ansible.module_utils.basic import get_exception from ansible.module_utils.basic import get_exception
@ -135,200 +129,17 @@ from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
from ansible.module_utils.shell import ShellError from ansible.module_utils.shell import ShellError
from ansible.module_utils.network import NetworkModule from ansible.module_utils.network import NetworkModule
DEFAULT_COMMENT_TOKENS = ['#', '!']
class ConfigLine(object): def to_list(val):
if isinstance(val, (list, tuple)):
def __init__(self, text): return list(val)
self.text = text elif val is not None:
self.children = list() return [val]
self.parents = list()
self.raw = None
@property
def line(self):
line = ['set']
line.extend([p.text for p in self.parents])
line.append(self.text)
return ' '.join(line)
def __str__(self):
return self.raw
def __eq__(self, other):
if self.text == other.text:
return self.parents == other.parents
def __ne__(self, other):
return not self.__eq__(other)
def ignore_line(text, tokens=None):
for item in (tokens or DEFAULT_COMMENT_TOKENS):
if text.startswith(item):
return True
def get_next(iterable):
item, next_item = itertools.tee(iterable, 2)
next_item = itertools.islice(next_item, 1, None)
return itertools.izip_longest(item, next_item)
def parse(lines, indent, comment_tokens=None):
toplevel = re.compile(r'\S')
childline = re.compile(r'^\s*(.+)$')
ancestors = list()
config = list()
for line in str(lines).split('\n'):
text = str(re.sub(r'([{};])', '', line)).strip()
cfg = ConfigLine(text)
cfg.raw = line
if not text or ignore_line(text, comment_tokens):
continue
# handle top level commands
if toplevel.match(line):
ancestors = [cfg]
# handle sub level commands
else: else:
match = childline.match(line)
line_indent = match.start(1)
level = int(line_indent / indent)
parent_level = level - 1
cfg.parents = ancestors[:level]
if level > len(ancestors):
config.append(cfg)
continue
for i in range(level, len(ancestors)):
ancestors.pop()
ancestors.append(cfg)
ancestors[parent_level].children.append(cfg)
config.append(cfg)
return config
class CustomNetworkConfig(object):
def __init__(self, indent=None, contents=None, device_os=None):
self.indent = indent or 1
self._config = list()
self._device_os = device_os
if contents:
self.load(contents)
@property
def items(self):
return self._config
@property
def lines(self):
lines = list()
for item, next_item in get_next(self.items):
if next_item is None:
lines.append(item.line)
elif not next_item.line.startswith(item.line):
lines.append(item.line)
return lines
def __str__(self):
text = ''
for item in self.items:
if not item.parents:
expand = self.get_section(item.text)
text += '%s\n' % self.get_section(item.text)
return str(text).strip()
def load(self, contents):
self._config = parse(contents, indent=self.indent)
def load_from_file(self, filename):
self.load(open(filename).read())
def get(self, path):
if isinstance(path, basestring):
path = [path]
for item in self._config:
if item.text == path[-1]:
parents = [p.text for p in item.parents]
if parents == path[:-1]:
return item
def search(self, regexp, path=None):
regex = re.compile(r'^%s' % regexp, re.M)
if path:
parent = self.get(path)
if not parent or not parent.children:
return
children = [c.text for c in parent.children]
data = '\n'.join(children)
else:
data = str(self)
match = regex.search(data)
if match:
if match.groups():
values = match.groupdict().values()
groups = list(set(match.groups()).difference(values))
return (groups, match.groupdict())
else:
return match.group()
def findall(self, regexp):
regexp = r'%s' % regexp
return re.findall(regexp, str(self))
def expand(self, obj, items):
block = [item.raw for item in obj.parents]
block.append(obj.raw)
current_level = items
for b in block:
if b not in current_level:
current_level[b] = collections.OrderedDict()
current_level = current_level[b]
for c in obj.children:
if c.raw not in current_level:
current_level[c.raw] = collections.OrderedDict()
def to_lines(self, section):
lines = list()
for entry in section[1:]:
line = ['set']
line.extend([p.text for p in entry.parents])
line.append(entry.text)
lines.append(' '.join(line))
return lines
def to_block(self, section):
return '\n'.join([item.raw for item in section])
def get_section(self, path):
try:
section = self.get_section_objects(path)
if self._device_os == 'junos':
return self.to_lines(section)
return self.to_block(section)
except ValueError:
return list() return list()
def get_section_objects(self, path):
if not isinstance(path, list): class CustomNetworkConfig(NetworkConfig):
path = [path]
obj = self.get_object(path)
if not obj:
raise ValueError('path does not exist in config')
return self.expand_section(obj)
def expand_section(self, configobj, S=None): def expand_section(self, configobj, S=None):
if S is None: if S is None:
@ -340,14 +151,6 @@ class CustomNetworkConfig(object):
self.expand_section(child, S) self.expand_section(child, S)
return S return S
def flatten(self, data, obj=None):
if obj is None:
obj = list()
for k, v in data.items():
obj.append(k)
self.flatten(v, obj)
return obj
def get_object(self, path): def get_object(self, path):
for item in self.items: for item in self.items:
if item.text == path[-1]: if item.text == path[-1]:
@ -355,93 +158,23 @@ class CustomNetworkConfig(object):
if parents == path[:-1]: if parents == path[:-1]:
return item return item
def get_children(self, path): def to_block(self, section):
obj = self.get_object(path) return '\n'.join([item.raw for item in section])
if obj:
return obj.children
def difference(self, other, path=None, match='line', replace='line'): def get_section(self, path):
updates = list()
config = self.items
if path:
config = self.get_children(path) or list()
if match == 'line':
for item in config:
if item not in other.items:
updates.append(item)
elif match == 'strict':
if path:
current = other.get_children(path) or list()
else:
current = other.items
for index, item in enumerate(config):
try: try:
if item != current[index]: section = self.get_section_objects(path)
updates.append(item) return self.to_block(section)
except IndexError: except ValueError:
updates.append(item) return list()
elif match == 'exact': def get_section_objects(self, path):
if path: if not isinstance(path, list):
current = other.get_children(path) or list() path = [path]
else: obj = self.get_object(path)
current = other.items if not obj:
raise ValueError('path does not exist in config')
if len(current) != len(config): return self.expand_section(obj)
updates.extend(config)
else:
for ours, theirs in itertools.izip(config, current):
if ours != theirs:
updates.extend(config)
break
if self._device_os == 'junos':
return updates
diffs = collections.OrderedDict()
for update in updates:
if replace == 'block' and update.parents:
update = update.parents[-1]
self.expand(update, diffs)
return self.flatten(diffs)
def replace(self, replace, text=None, regex=None, parents=None,
add_if_missing=False, ignore_whitespace=False):
match = None
parents = parents or list()
if text is None and regex is None:
raise ValueError('missing required arguments')
if not regex:
regex = ['^%s$' % text]
patterns = [re.compile(r, re.I) for r in to_list(regex)]
for item in self.items:
for regexp in patterns:
if ignore_whitespace is True:
string = item.text
else:
string = item.raw
if regexp.search(item.text):
if item.text != replace:
if parents == [p.text for p in item.parents]:
match = item
break
if match:
match.text = replace
indent = len(match.raw) - len(match.raw.lstrip())
match.raw = replace.rjust(len(replace) + indent)
elif add_if_missing:
self.add(replace, parents=parents)
def add(self, lines, parents=None): def add(self, lines, parents=None):
@ -493,303 +226,44 @@ class CustomNetworkConfig(object):
self.items.append(item) self.items.append(item)
def argument_spec(): def get_network_module(**kwargs):
return dict(
# config options
running_config=dict(aliases=['config']),
save_config=dict(type='bool', default=False, aliases=['save'])
)
nxos_argument_spec = argument_spec()
NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I)
NET_COMMON_ARGS = dict(
host=dict(required=True),
port=dict(type='int'),
username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])),
ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
transport=dict(default='cli', choices=['cli', 'nxapi']),
use_ssl=dict(default=False, type='bool'),
validate_certs=dict(default=True, type='bool'),
provider=dict(type='dict'),
timeout=dict(default=10, type='int')
)
NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash']
NXAPI_ENCODINGS = ['json', 'xml']
CLI_PROMPTS_RE = [
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'),
re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$')
]
CLI_ERRORS_RE = [
re.compile(r"% ?Error"),
re.compile(r"^% \w+", re.M),
re.compile(r"% ?Bad secret"),
re.compile(r"invalid input", re.I),
re.compile(r"(?:incomplete|ambiguous) command", re.I),
re.compile(r"connection timed out", re.I),
re.compile(r"[^\r\n]+ not found", re.I),
re.compile(r"'[^']' +returned error code: ?\d+"),
re.compile(r"syntax error"),
re.compile(r"unknown command")
]
def to_list(val):
if isinstance(val, (list, tuple)):
return list(val)
elif val is not None:
return [val]
else:
return list()
class Nxapi(object):
def __init__(self, module):
self.module = module
# sets the module_utils/urls.py req parameters
self.module.params['url_username'] = module.params['username']
self.module.params['url_password'] = module.params['password']
self.url = None
self._nxapi_auth = None
def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None):
"""Encodes a NXAPI JSON request message
"""
if isinstance(commands, (list, set, tuple)):
commands = ' ;'.join(commands)
if encoding not in NXAPI_ENCODINGS:
msg = 'invalid encoding, received %s, exceped one of %s' % \
(encoding, ','.join(NXAPI_ENCODINGS))
self.module_fail_json(msg=msg)
msg = {
'version': version,
'type': command_type,
'chunk': chunk,
'sid': sid,
'input': commands,
'output_format': encoding
}
return dict(ins_api=msg)
def connect(self):
host = self.module.params['host']
port = self.module.params['port']
if self.module.params['use_ssl']:
proto = 'https'
if not port:
port = 443
else:
proto = 'http'
if not port:
port = 80
self.url = '%s://%s:%s/ins' % (proto, host, port)
def send(self, commands, command_type='cli_show_ascii', encoding='json'):
"""Send commands to the device.
"""
clist = to_list(commands)
if command_type not in NXAPI_COMMAND_TYPES:
msg = 'invalid command_type, received %s, exceped one of %s' % \
(command_type, ','.join(NXAPI_COMMAND_TYPES))
self.module_fail_json(msg=msg)
data = self._get_body(clist, command_type, encoding)
data = self.module.jsonify(data)
headers = {'Content-Type': 'application/json'}
if self._nxapi_auth:
headers['Cookie'] = self._nxapi_auth
response, headers = fetch_url(self.module, self.url, data=data,
headers=headers, method='POST')
self._nxapi_auth = headers.get('set-cookie')
if headers['status'] != 200:
self.module.fail_json(**headers)
response = self.module.from_json(response.read())
result = list()
output = response['ins_api']['outputs']['output']
for item in to_list(output):
if item['code'] != '200':
self.module.fail_json(**item)
else:
result.append(item['body'])
return result
class Cli(object):
def __init__(self, module):
self.module = module
self.shell = None
def connect(self, **kwargs):
host = self.module.params['host']
port = self.module.params['port'] or 22
username = self.module.params['username']
password = self.module.params['password']
timeout = self.module.params['timeout']
key_filename = self.module.params['ssh_keyfile']
allow_agent = (key_filename is not None) or (key_filename is None and password is None)
try: try:
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, return get_module(**kwargs)
errors_re=CLI_ERRORS_RE) except NameError:
self.shell.open(host, port=port, username=username, return NetworkModule(**kwargs)
password=password, key_filename=key_filename,
allow_agent=allow_agent, timeout=timeout)
except ShellError:
e = get_exception()
msg = 'failed to connect to %s:%s - %s' % (host, port, str(e))
self.module.fail_json(msg=msg)
def send(self, commands, encoding='text'): def get_config(module, include_defaults=False):
try: config = module.params['config']
return self.shell.send(commands)
except ShellError:
e = get_exception()
self.module.fail_json(msg=e.message, commands=commands)
class NetworkModule(AnsibleModule):
def __init__(self, *args, **kwargs):
super(NetworkModule, self).__init__(*args, **kwargs)
self.connection = None
self._config = None
self._connected = False
@property
def connected(self):
return self._connected
@property
def config(self):
if not self._config:
self._config = self.get_config()
return self._config
def _load_params(self):
super(NetworkModule, self)._load_params()
provider = self.params.get('provider') or dict()
for key, value in provider.items():
if key in NET_COMMON_ARGS:
if self.params.get(key) is None and value is not None:
self.params[key] = value
def connect(self):
cls = globals().get(str(self.params['transport']).capitalize())
try:
self.connection = cls(self)
except TypeError:
e = get_exception()
self.fail_json(msg=e.message)
self.connection.connect()
if self.params['transport'] == 'cli':
self.connection.send('terminal length 0')
self._connected = True
def configure(self, commands):
commands = to_list(commands)
if self.params['transport'] == 'cli':
return self.configure_cli(commands)
else:
return self.execute(commands, command_type='cli_conf')
def configure_cli(self, commands):
commands = to_list(commands)
commands.insert(0, 'configure')
responses = self.execute(commands)
responses.pop(0)
return responses
def execute(self, commands, **kwargs):
if not self.connected:
self.connect()
return self.connection.send(commands, **kwargs)
def disconnect(self):
self.connection.close()
self._connected = False
def parse_config(self, cfg):
return parse(cfg, indent=2)
def get_config(self):
cmd = 'show running-config'
if self.params.get('include_defaults'):
cmd += ' all'
response = self.execute(cmd)
return response[0]
def get_module(**kwargs):
"""Return instance of NetworkModule
"""
argument_spec = NET_COMMON_ARGS.copy()
if kwargs.get('argument_spec'):
argument_spec.update(kwargs['argument_spec'])
kwargs['argument_spec'] = argument_spec
module = NetworkModule(**kwargs)
if module.params['transport'] == 'cli' and not HAS_PARAMIKO:
module.fail_json(msg='paramiko is required but does not appear to be installed')
return module
def custom_get_config(module, include_defaults=False):
config = module.params['running_config']
if not config: if not config:
cmd = 'show running-config' try:
if module.params['include_defaults']: config = module.get_config()
cmd += ' all' except AttributeError:
if module.params['transport'] == 'nxapi': defaults = module.params['include_defaults']
config = module.execute([cmd], command_type='cli_show_ascii')[0] config = module.config.get_config(include_defaults=defaults)
else:
config = module.execute([cmd])[0]
return CustomNetworkConfig(indent=2, contents=config) return CustomNetworkConfig(indent=2, contents=config)
def load_config(module, candidate): def load_config(module, candidate):
config = custom_get_config(module) config = get_config(module)
commands = candidate.difference(config) commands = candidate.difference(config)
commands = [str(c).strip() for c in commands] commands = [str(c).strip() for c in commands]
save_config = module.params['save_config'] save_config = module.params['save']
result = dict(changed=False) result = dict(changed=False)
if commands: if commands:
if not module.check_mode: if not module.check_mode:
try:
module.configure(commands) module.configure(commands)
except AttributeError:
module.config(commands)
if save_config: if save_config:
try:
module.config.save_config() module.config.save_config()
except AttributeError:
module.execute(['copy running-config startup-config'])
result['changed'] = True result['changed'] = True
result['updates'] = commands result['updates'] = commands
@ -834,6 +308,11 @@ def get_cli_body_ssh_vrf(module, command, response):
def execute_show(cmds, module, command_type=None): def execute_show(cmds, module, command_type=None):
command_type_map = {
'cli_show': 'json',
'cli_show_ascii': 'text'
}
try: try:
if command_type: if command_type:
response = module.execute(cmds, command_type=command_type) response = module.execute(cmds, command_type=command_type)
@ -974,8 +453,11 @@ def main():
required=False), required=False),
state=dict(default='present', choices=['present', 'absent'], state=dict(default='present', choices=['present', 'absent'],
required=False), required=False),
include_defaults=dict(default=False),
config=dict(),
save=dict(type='bool', default=False)
) )
module = get_module(argument_spec=argument_spec, module = get_network_module(argument_spec=argument_spec,
supports_check_mode=True) supports_check_mode=True)
vrf = module.params['vrf'] vrf = module.params['vrf']

Some files were not shown because too many files have changed in this diff Show more