mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
Resolving differences in core modules post-merge
This commit is contained in:
parent
95d59b61eb
commit
8afa090417
115 changed files with 2651 additions and 17913 deletions
|
@ -39,35 +39,28 @@ options:
|
|||
description:
|
||||
- name of the cloudformation stack
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
disable_rollback:
|
||||
description:
|
||||
- If a stacks fails to form, rollback will remove the stack
|
||||
required: false
|
||||
default: "false"
|
||||
choices: [ "true", "false" ]
|
||||
aliases: []
|
||||
template_parameters:
|
||||
description:
|
||||
- a list of hashes of all the template variables for the stack
|
||||
required: false
|
||||
default: {}
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- If state is "present", stack will be created. If state is "present" and if stack exists and template has changed, it will be updated.
|
||||
If state is "absent", stack will be removed.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
template:
|
||||
description:
|
||||
- The local path of the cloudformation template. This parameter is mutually exclusive with 'template_url'. Either one of them is required if "state" parameter is "present"
|
||||
Must give full path to the file, relative to the working directory. If using roles this may look like "roles/cloudformation/files/cloudformation-example.json"
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
notification_arns:
|
||||
description:
|
||||
- The Simple Notification Service (SNS) topic ARNs to publish stack related events.
|
||||
|
@ -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)
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.9"
|
||||
tags:
|
||||
description:
|
||||
- Dictionary of tags to associate with stack and its resources during stack creation. Can be updated later, updating tags removes previous entries.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.4"
|
||||
template_url:
|
||||
description:
|
||||
|
|
|
@ -56,7 +56,7 @@ options:
|
|||
region:
|
||||
version_added: "1.2"
|
||||
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
|
||||
default: null
|
||||
aliases: [ 'aws_region', 'ec2_region' ]
|
||||
|
@ -69,16 +69,17 @@ options:
|
|||
aliases: [ 'aws_zone', 'ec2_zone' ]
|
||||
instance_type:
|
||||
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
|
||||
default: null
|
||||
aliases: []
|
||||
tenancy:
|
||||
version_added: "1.9"
|
||||
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
|
||||
default: default
|
||||
choices: [ "default", "dedicated" ]
|
||||
aliases: []
|
||||
spot_price:
|
||||
version_added: "1.5"
|
||||
|
@ -143,6 +144,7 @@ options:
|
|||
- enable detailed monitoring (CloudWatch) for instance
|
||||
required: false
|
||||
default: null
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
user_data:
|
||||
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+
|
||||
required: false
|
||||
default: null
|
||||
choices: [ "yes", "no" ]
|
||||
aliases: []
|
||||
private_ip:
|
||||
version_added: "1.2"
|
||||
|
|
|
@ -51,12 +51,6 @@ options:
|
|||
- create or deregister/delete image
|
||||
required: false
|
||||
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:
|
||||
- An optional human-readable string describing the contents and purpose of the AMI.
|
||||
|
@ -74,10 +68,10 @@ options:
|
|||
required: false
|
||||
default: null
|
||||
device_mapping:
|
||||
version_added: "1.9"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- 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
|
||||
default: null
|
||||
delete_snapshot:
|
||||
|
@ -305,6 +299,7 @@ import time
|
|||
try:
|
||||
import boto
|
||||
import boto.ec2
|
||||
from boto.ec2.blockdevicemapping import BlockDeviceType, BlockDeviceMapping
|
||||
HAS_BOTO = True
|
||||
except ImportError:
|
||||
HAS_BOTO = False
|
||||
|
@ -365,6 +360,7 @@ def create_image(module, ec2):
|
|||
wait_timeout = int(module.params.get('wait_timeout'))
|
||||
description = module.params.get('description')
|
||||
no_reboot = module.params.get('no_reboot')
|
||||
device_mapping = module.params.get('device_mapping')
|
||||
tags = module.params.get('tags')
|
||||
launch_permissions = module.params.get('launch_permissions')
|
||||
|
||||
|
|
|
@ -22,7 +22,7 @@ ANSIBLE_METADATA = {'status': ['preview'],
|
|||
DOCUMENTATION = '''
|
||||
---
|
||||
module: ec2_ami_find
|
||||
version_added: 2.0
|
||||
version_added: '2.0'
|
||||
short_description: Searches for AMIs to obtain the AMI ID and other information
|
||||
description:
|
||||
- Returns list of matching AMIs with AMI ID, along with other useful information
|
||||
|
|
|
@ -69,7 +69,7 @@ options:
|
|||
required: false
|
||||
replace_all_instances:
|
||||
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
|
||||
version_added: "1.8"
|
||||
default: False
|
||||
|
@ -91,11 +91,6 @@ options:
|
|||
required: false
|
||||
version_added: "1.8"
|
||||
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:
|
||||
description:
|
||||
- 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.
|
||||
- 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
|
||||
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']
|
||||
version_added: "2.0"
|
||||
notification_topic:
|
||||
|
|
|
@ -47,12 +47,6 @@ options:
|
|||
required: false
|
||||
choices: ['present', 'absent']
|
||||
default: present
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: [ ec2_region ]
|
||||
in_vpc:
|
||||
description:
|
||||
- allocate an EIP inside a VPC or not
|
||||
|
|
|
@ -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.
|
||||
required: false
|
||||
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:
|
||||
description:
|
||||
- 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
|
||||
default: 0
|
||||
version_added: "1.6"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
|
|
|
@ -82,7 +82,7 @@ options:
|
|||
version_added: "2.0"
|
||||
health_check:
|
||||
description:
|
||||
- An associative array of health check configuration settigs (see example)
|
||||
- An associative array of health check configuration settings (see example)
|
||||
require: false
|
||||
default: None
|
||||
access_logs:
|
||||
|
@ -131,7 +131,7 @@ options:
|
|||
version_added: "2.0"
|
||||
cross_az_load_balancing:
|
||||
description:
|
||||
- Distribute load across all configured Availablity Zones
|
||||
- Distribute load across all configured Availability Zones
|
||||
required: false
|
||||
default: "no"
|
||||
choices: ["yes", "no"]
|
||||
|
@ -163,7 +163,9 @@ options:
|
|||
required: false
|
||||
version_added: "2.1"
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
|
@ -271,7 +273,7 @@ EXAMPLES = """
|
|||
purge_listeners: no
|
||||
|
||||
# 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
|
||||
- local_action:
|
||||
module: ec2_elb_lb
|
||||
|
@ -762,7 +764,7 @@ class ElbManager(object):
|
|||
# Does it match exactly?
|
||||
if listener_as_tuple != existing_listener_found:
|
||||
# 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_add.append(listener_as_tuple)
|
||||
else:
|
||||
|
|
|
@ -33,7 +33,7 @@ options:
|
|||
required: false
|
||||
default: 'yes'
|
||||
choices: ['yes', 'no']
|
||||
version_added: 1.5.1
|
||||
version_added: '1.5.1'
|
||||
description:
|
||||
- This module fetches data from the metadata servers in ec2 (aws) as per
|
||||
http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html.
|
||||
|
|
|
@ -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.
|
||||
required: false
|
||||
version_added: "1.6"
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
version_added: "1.4"
|
||||
description:
|
||||
|
@ -78,7 +72,9 @@ options:
|
|||
default: 'true'
|
||||
aliases: []
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
|
||||
notes:
|
||||
- If a rule declares a group_name and that group doesn't exist, it will be
|
||||
|
|
|
@ -35,12 +35,6 @@ options:
|
|||
description:
|
||||
- Public key material.
|
||||
required: false
|
||||
region:
|
||||
description:
|
||||
- the EC2 region to use
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
state:
|
||||
description:
|
||||
- create or delete keypair
|
||||
|
@ -62,7 +56,9 @@ options:
|
|||
aliases: []
|
||||
version_added: "1.6"
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
author: "Vincent Viallet (@zbal)"
|
||||
'''
|
||||
|
||||
|
|
|
@ -59,11 +59,6 @@ options:
|
|||
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.
|
||||
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:
|
||||
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.
|
||||
|
@ -110,7 +105,7 @@ options:
|
|||
- Id of ClassicLink enabled VPC
|
||||
required: false
|
||||
version_added: "2.0"
|
||||
classic_link_vpc_security_groups"
|
||||
classic_link_vpc_security_groups:
|
||||
description:
|
||||
- A list of security group id's with which to associate the ClassicLink VPC instances.
|
||||
required: false
|
||||
|
|
|
@ -33,7 +33,7 @@ options:
|
|||
required: true
|
||||
choices: ['present', 'absent']
|
||||
name:
|
||||
desciption:
|
||||
description:
|
||||
- Unique name for the alarm
|
||||
required: true
|
||||
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']
|
||||
description:
|
||||
description:
|
||||
- A longer desciption of the alarm
|
||||
- A longer description of the alarm
|
||||
required: false
|
||||
dimensions:
|
||||
description:
|
||||
|
@ -93,7 +93,9 @@ options:
|
|||
description:
|
||||
- A list of the names of action(s) to take when the alarm is in the 'ok' status
|
||||
required: false
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -41,7 +41,7 @@ options:
|
|||
- Name of the associated autoscaling group
|
||||
required: true
|
||||
adjustment_type:
|
||||
desciption:
|
||||
description:
|
||||
- The type of change in capacity of the autoscaling group
|
||||
required: false
|
||||
choices: ['ChangeInCapacity','ExactCapacity','PercentChangeInCapacity']
|
||||
|
@ -57,7 +57,9 @@ options:
|
|||
description:
|
||||
- The minimum period of time between which autoscaling actions can take place
|
||||
required: false
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
"""
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -26,11 +26,6 @@ description:
|
|||
- creates an EC2 snapshot from an existing EBS volume
|
||||
version_added: "1.5"
|
||||
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:
|
||||
description:
|
||||
- volume from which to take the snapshot
|
||||
|
@ -86,7 +81,9 @@ options:
|
|||
version_added: "2.0"
|
||||
|
||||
author: "Will Thames (@willthames)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -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.
|
||||
version_added: "1.3"
|
||||
options:
|
||||
region:
|
||||
description:
|
||||
- region in which the resource exists.
|
||||
required: false
|
||||
default: null
|
||||
aliases: ['aws_region', 'ec2_region']
|
||||
resource:
|
||||
description:
|
||||
- The EC2 resource id.
|
||||
|
@ -53,7 +47,9 @@ options:
|
|||
aliases: []
|
||||
|
||||
author: "Lester Wade (@lwade)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -105,7 +105,9 @@ options:
|
|||
choices: ['absent', 'present', 'list']
|
||||
version_added: "1.6"
|
||||
author: "Lester Wade (@lwade)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -91,7 +91,9 @@ options:
|
|||
required: true
|
||||
choices: [ "present", "absent" ]
|
||||
author: "Carson Gee (@carsongee)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -476,6 +478,7 @@ def create_vpc(module, vpc_conn):
|
|||
|
||||
# Handle Internet gateway (create/delete igw)
|
||||
igw = None
|
||||
igw_id = None
|
||||
igws = vpc_conn.get_all_internet_gateways(filters={'attachment.vpc-id': vpc.id})
|
||||
if len(igws) > 1:
|
||||
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:
|
||||
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
|
||||
# different module but should work fine here. The strategy to stay
|
||||
# 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))
|
||||
|
||||
vpc_dict = get_vpc_info(vpc)
|
||||
|
||||
created_vpc_id = vpc.id
|
||||
returned_subnets = []
|
||||
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)
|
||||
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):
|
||||
"""
|
||||
|
@ -723,6 +730,7 @@ def main():
|
|||
else:
|
||||
module.fail_json(msg="region must be specified")
|
||||
|
||||
igw_id = None
|
||||
if module.params.get('state') == 'absent':
|
||||
vpc_id = module.params.get('vpc_id')
|
||||
cidr = module.params.get('cidr_block')
|
||||
|
@ -730,9 +738,9 @@ def main():
|
|||
subnets_changed = None
|
||||
elif module.params.get('state') == 'present':
|
||||
# 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
|
||||
from ansible.module_utils.basic import *
|
||||
|
|
|
@ -76,7 +76,9 @@ options:
|
|||
default: false
|
||||
required: false
|
||||
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -74,7 +74,7 @@ options:
|
|||
- The subnet group name to associate with. Only use if inside a vpc. Required if inside a vpc
|
||||
required: false
|
||||
default: None
|
||||
version_added: "1.7"
|
||||
version_added: "2.0"
|
||||
security_group_ids:
|
||||
description:
|
||||
- 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
|
||||
default: no
|
||||
choices: [ "yes", "no" ]
|
||||
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']
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
"""
|
||||
|
||||
EXAMPLES = """
|
||||
|
|
|
@ -31,34 +31,25 @@ options:
|
|||
- Specifies whether the subnet should be present or absent.
|
||||
required: true
|
||||
default: present
|
||||
aliases: []
|
||||
choices: [ 'present' , 'absent' ]
|
||||
name:
|
||||
description:
|
||||
- Database subnet group identifier.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
description:
|
||||
description:
|
||||
- Elasticache subnet group description. Only set when a new group is added.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
subnets:
|
||||
description:
|
||||
- List of subnet IDs that make up the Elasticache subnet group.
|
||||
required: false
|
||||
default: null
|
||||
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: Tim Mahoney
|
||||
extends_documentation_fragment: aws
|
||||
author: "Tim Mahoney (@timmahoney)"
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -36,24 +36,22 @@ options:
|
|||
description:
|
||||
- Name of IAM resource to create or identify
|
||||
required: true
|
||||
aliases: []
|
||||
new_name:
|
||||
description:
|
||||
- When state is update, will replace name with new_name on IAM resource
|
||||
required: false
|
||||
aliases: []
|
||||
default: null
|
||||
new_path:
|
||||
description:
|
||||
- When state is update, will replace the path with new_path on the IAM resource
|
||||
required: false
|
||||
aliases: []
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Whether to create, delete or update the IAM resource. Note, roles cannot be updated.
|
||||
required: true
|
||||
default: null
|
||||
choices: [ "present", "absent", "update" ]
|
||||
aliases: []
|
||||
path:
|
||||
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.
|
||||
|
@ -77,13 +75,11 @@ options:
|
|||
required: false
|
||||
default: null
|
||||
choices: [ "create", "remove", "active", "inactive"]
|
||||
aliases: []
|
||||
key_count:
|
||||
description:
|
||||
- When access_key_state is create it will ensure this quantity of keys are present. Defaults to 1.
|
||||
required: false
|
||||
default: '1'
|
||||
aliases: []
|
||||
access_key_ids:
|
||||
description:
|
||||
- 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.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
password:
|
||||
description:
|
||||
- When type is user and state is present, define the users login password. Also works with update. Note that always returns changed.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
update_password:
|
||||
required: false
|
||||
default: always
|
||||
|
|
|
@ -89,7 +89,9 @@ options:
|
|||
|
||||
requirements: [ "boto" ]
|
||||
author: Jonathan I. Davila
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -62,7 +62,9 @@ options:
|
|||
notes:
|
||||
- 'Currently boto does not support the removal of Managed Policies, the module will not work removing/adding managed policies.'
|
||||
author: "Jonathan I. Davila (@defionscode)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -65,7 +65,9 @@ options:
|
|||
default: null
|
||||
aliases: []
|
||||
author: "Scott Anderson (@tastychutney)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -51,14 +51,10 @@ options:
|
|||
required: false
|
||||
default: null
|
||||
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)"
|
||||
extends_documentation_fragment: aws
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -30,33 +30,26 @@ options:
|
|||
description:
|
||||
- Specifies the action to take.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'get', 'create', 'delete' ]
|
||||
zone:
|
||||
description:
|
||||
- The DNS zone to modify
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
hosted_zone_id:
|
||||
description:
|
||||
- The Hosted Zone ID of the DNS zone to modify
|
||||
required: false
|
||||
version_added: 2.0
|
||||
version_added: "2.0"
|
||||
default: null
|
||||
record:
|
||||
description:
|
||||
- The full DNS record to create or delete
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
ttl:
|
||||
description:
|
||||
- The TTL to give the new record
|
||||
required: false
|
||||
default: 3600 (one hour)
|
||||
aliases: []
|
||||
type:
|
||||
description:
|
||||
- The type of DNS record to create
|
||||
|
@ -66,40 +59,36 @@ options:
|
|||
description:
|
||||
- Indicates if this is an alias record.
|
||||
required: false
|
||||
version_added: 1.9
|
||||
version_added: "1.9"
|
||||
default: False
|
||||
aliases: []
|
||||
choices: [ 'True', 'False' ]
|
||||
alias_hosted_zone_id:
|
||||
description:
|
||||
- The hosted zone identifier.
|
||||
required: false
|
||||
version_added: 1.9
|
||||
version_added: "1.9"
|
||||
default: null
|
||||
alias_evaluate_target_health:
|
||||
description:
|
||||
- Whether or not to evaluate an alias target health. Useful for aliases to Elastic Load Balancers.
|
||||
required: false
|
||||
version_added: "2.0"
|
||||
version_added: "2.1"
|
||||
default: false
|
||||
value:
|
||||
description:
|
||||
- The new value when creating a DNS record. Multiple comma-spaced values are allowed for non-alias records. When deleting a record all values for the record must be specified or Route53 will not delete it.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
overwrite:
|
||||
description:
|
||||
- Whether an existing record should be overwritten on create if values do not match
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
retry_interval:
|
||||
description:
|
||||
- In the case that route53 is still servicing a prior request, this module will wait and try again after this many seconds. If you have many domain names, the default of 500 seconds may be too long.
|
||||
required: false
|
||||
default: 500
|
||||
aliases: []
|
||||
private_zone:
|
||||
description:
|
||||
- If set to true, the private zone matching the requested name within the domain will be used if there are both public and private zones. The default is to use the public zone.
|
||||
|
@ -316,6 +305,7 @@ import distutils.version
|
|||
|
||||
try:
|
||||
import boto
|
||||
import boto.ec2
|
||||
from boto import route53
|
||||
from boto.route53 import Route53Connection
|
||||
from boto.route53.record import Record, ResourceRecordSets
|
||||
|
@ -496,26 +486,11 @@ def main():
|
|||
except boto.exception.BotoServerError as e:
|
||||
module.fail_json(msg = e.error_message)
|
||||
|
||||
# Get all the existing hosted zones and save their ID's
|
||||
zones = {}
|
||||
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
|
||||
# Find the named zone ID
|
||||
zone = get_zone_by_name(conn, module, zone_in, private_zone_in, hosted_zone_id_in, vpc_id_in)
|
||||
|
||||
# Verify that the requested zone is already defined in Route53
|
||||
if not zone_in in zones and hosted_zone_id_in:
|
||||
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:
|
||||
if zone is None:
|
||||
errmsg = "Zone %s does not exist in Route53" % zone_in
|
||||
module.fail_json(msg = errmsg)
|
||||
|
||||
|
@ -551,6 +526,8 @@ def main():
|
|||
record['ttl'] = rset.ttl
|
||||
record['value'] = ','.join(sorted(rset.resource_records))
|
||||
record['values'] = sorted(rset.resource_records)
|
||||
if hosted_zone_id_in:
|
||||
record['hosted_zone_id'] = hosted_zone_id_in
|
||||
record['identifier'] = rset.identifier
|
||||
record['weight'] = rset.weight
|
||||
record['region'] = rset.region
|
||||
|
|
|
@ -584,7 +584,7 @@ def main():
|
|||
if overwrite == 'always':
|
||||
download_s3file(module, s3, bucket, obj, dest, retries, version=version)
|
||||
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:
|
||||
sum_matches = False
|
||||
|
||||
|
@ -594,7 +594,7 @@ def main():
|
|||
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.
|
||||
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)
|
||||
|
||||
# if our mode is a PUT operation (upload), go through the procedure as appropriate ...
|
||||
|
|
|
@ -38,12 +38,12 @@ options:
|
|||
default: null
|
||||
subscription_id:
|
||||
description:
|
||||
- azure subscription id. Overrides the AZURE_SUBSCRIPTION_ID environement variable.
|
||||
- azure subscription id. Overrides the AZURE_SUBSCRIPTION_ID environment variable.
|
||||
required: false
|
||||
default: null
|
||||
management_cert_path:
|
||||
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
|
||||
default: null
|
||||
storage_account:
|
||||
|
@ -114,13 +114,6 @@ options:
|
|||
required: false
|
||||
default: 'present'
|
||||
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:
|
||||
description:
|
||||
- Enable Auto Updates on Windows Machines
|
||||
|
@ -258,7 +251,7 @@ AZURE_ROLE_SIZES = ['ExtraSmall',
|
|||
'Standard_DS14',
|
||||
'Standard_G1',
|
||||
'Standard_G2',
|
||||
'Sandard_G3',
|
||||
'Standard_G3',
|
||||
'Standard_G4',
|
||||
'Standard_G5']
|
||||
|
||||
|
@ -516,7 +509,7 @@ def terminate_virtual_machine(module, azure):
|
|||
|
||||
|
||||
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')
|
||||
if not subscription_id:
|
||||
subscription_id = os.environ.get('AZURE_SUBSCRIPTION_ID', None)
|
||||
|
@ -553,7 +546,6 @@ def main():
|
|||
wait=dict(type='bool', default=False),
|
||||
wait_timeout=dict(default=600),
|
||||
wait_timeout_redirects=dict(default=300),
|
||||
reset_pass_atlogon=dict(type='bool', default=False),
|
||||
auto_updates=dict(type='bool', default=False),
|
||||
enable_winrm=dict(type='bool', default=True),
|
||||
)
|
||||
|
|
|
@ -264,17 +264,17 @@ class AzureRMPublicIPAddress(AzureRMModuleBase):
|
|||
def create_or_update_pip(self, pip):
|
||||
try:
|
||||
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:
|
||||
self.fail("Error creating or updating {0} - {1}".format(self.name, str(exc)))
|
||||
pip = self.get_poller_result(poller)
|
||||
return pip_to_dict(pip)
|
||||
|
||||
def delete_pip(self):
|
||||
try:
|
||||
poller = self.network_client.public_ip_addresses.delete(self.resource_group, self.name)
|
||||
self.get_poller_result(poller)
|
||||
except Exception as 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.
|
||||
self.results['state']['status'] = 'Deleted'
|
||||
return True
|
||||
|
|
|
@ -74,11 +74,10 @@ options:
|
|||
version_added: "1.5"
|
||||
ports:
|
||||
description:
|
||||
- List containing private to public port mapping specification. 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
|
||||
- a host interface. The container ports need to be exposed either in the
|
||||
- Dockerfile or via the next option.
|
||||
- "List containing private to public port mapping specification.
|
||||
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 - a host interface.
|
||||
The container ports need to be exposed either in the Dockerfile or via the C(expose) option."
|
||||
default: null
|
||||
version_added: "1.5"
|
||||
expose:
|
||||
|
@ -154,10 +153,7 @@ options:
|
|||
description:
|
||||
- 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.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
default: 256MB
|
||||
default: 0
|
||||
docker_url:
|
||||
description:
|
||||
- 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.
|
||||
"""
|
||||
if not self._capabilities:
|
||||
self._check_capabilties()
|
||||
self._check_capabilities()
|
||||
|
||||
if capability in self._capabilities:
|
||||
return True
|
||||
|
@ -1054,7 +1050,7 @@ class DockerManager(object):
|
|||
elif p_len == 3:
|
||||
# Bind `container_port` of the container to port `parts[1]` on
|
||||
# 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],)
|
||||
|
||||
if container_port in binds:
|
||||
|
@ -1643,23 +1639,10 @@ class DockerManager(object):
|
|||
'name': self.module.params.get('name'),
|
||||
'stdin_open': self.module.params.get('stdin_open'),
|
||||
'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'),
|
||||
'cpu_shares': self.module.params.get('cpu_shares'),
|
||||
'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):
|
||||
params['host_config'] = self.create_host_config()
|
||||
|
||||
|
|
|
@ -31,25 +31,20 @@ options:
|
|||
description:
|
||||
- Bucket name.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
object:
|
||||
description:
|
||||
- Keyname of the object inside the bucket. Can be also be used to create "virtual directories" (see examples).
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
src:
|
||||
description:
|
||||
- The source file path when performing a PUT operation.
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
dest:
|
||||
description:
|
||||
- The destination file path when downloading an object/key with a GET operation.
|
||||
required: false
|
||||
aliases: []
|
||||
force:
|
||||
description:
|
||||
- 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
|
||||
default: private
|
||||
headers:
|
||||
version_added: 2.0
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Headers to attach to object.
|
||||
required: false
|
||||
default: {}
|
||||
default: '{}'
|
||||
expiration:
|
||||
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
|
||||
default: null
|
||||
aliases: []
|
||||
mode:
|
||||
description:
|
||||
- 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
|
||||
default: null
|
||||
aliases: []
|
||||
choices: [ 'get', 'put', 'get_url', 'get_str', 'delete', 'create' ]
|
||||
gs_secret_key:
|
||||
description:
|
||||
|
|
|
@ -36,35 +36,30 @@ options:
|
|||
- image string to use for the instance
|
||||
required: false
|
||||
default: "debian-7"
|
||||
aliases: []
|
||||
instance_names:
|
||||
description:
|
||||
- a comma-separated list of instance names to create or destroy
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
machine_type:
|
||||
description:
|
||||
- machine type to use for the instance, use 'n1-standard-1' by default
|
||||
required: false
|
||||
default: "n1-standard-1"
|
||||
aliases: []
|
||||
metadata:
|
||||
description:
|
||||
- a hash/dictionary of custom data for the instance;
|
||||
'{"key":"value", ...}'
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
service_account_email:
|
||||
version_added: 1.5.1
|
||||
version_added: "1.5.1"
|
||||
description:
|
||||
- service account email
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
service_account_permissions:
|
||||
version_added: 2.0
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- service account permissions (see
|
||||
U(https://cloud.google.com/sdk/gcloud/reference/compute/instances/create),
|
||||
|
@ -78,7 +73,7 @@ options:
|
|||
"storage-rw", "taskqueue", "userinfo-email"
|
||||
]
|
||||
pem_file:
|
||||
version_added: 1.5.1
|
||||
version_added: "1.5.1"
|
||||
description:
|
||||
- path to the pem file associated with the service account email
|
||||
This option is deprecated. Use 'credentials_file'.
|
||||
|
@ -91,12 +86,11 @@ options:
|
|||
default: null
|
||||
required: false
|
||||
project_id:
|
||||
version_added: 1.5.1
|
||||
version_added: "1.5.1"
|
||||
description:
|
||||
- your GCE project ID
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
name:
|
||||
description:
|
||||
- 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
|
||||
required: false
|
||||
default: "false"
|
||||
aliases: []
|
||||
disks:
|
||||
description:
|
||||
- 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).
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
version_added: "1.7"
|
||||
state:
|
||||
description:
|
||||
|
@ -148,13 +140,11 @@ options:
|
|||
- a comma-separated list of tags to associate with the instance
|
||||
required: false
|
||||
default: null
|
||||
aliases: []
|
||||
zone:
|
||||
description:
|
||||
- the GCE zone to use
|
||||
required: true
|
||||
default: "us-central1-a"
|
||||
aliases: []
|
||||
ip_forward:
|
||||
version_added: "1.9"
|
||||
description:
|
||||
|
@ -162,14 +152,12 @@ options:
|
|||
gateways)
|
||||
required: false
|
||||
default: "false"
|
||||
aliases: []
|
||||
external_ip:
|
||||
version_added: "1.9"
|
||||
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.
|
||||
required: false
|
||||
default: "ephemeral"
|
||||
aliases: []
|
||||
disk_auto_delete:
|
||||
version_added: "1.9"
|
||||
description:
|
||||
|
|
|
@ -26,7 +26,7 @@ module: gce_net
|
|||
version_added: "1.5"
|
||||
short_description: create/destroy GCE networks and firewall rules
|
||||
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).
|
||||
The I(name) parameter is reserved for referencing a network while the
|
||||
I(fwname) parameter is used to reference firewall rules.
|
||||
|
|
|
@ -116,7 +116,10 @@ options:
|
|||
required: false
|
||||
default: publicURL
|
||||
version_added: "1.7"
|
||||
requirements: ["glanceclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-glanceclient"
|
||||
- "python-keystoneclient"
|
||||
|
||||
'''
|
||||
|
||||
|
@ -136,9 +139,14 @@ EXAMPLES = '''
|
|||
import time
|
||||
try:
|
||||
import glanceclient
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
HAS_GLANCECLIENT = True
|
||||
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):
|
||||
|
@ -269,4 +277,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -19,15 +19,16 @@
|
|||
|
||||
import operator
|
||||
import os
|
||||
import time
|
||||
|
||||
try:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from novaclient.v1_1 import floating_ips
|
||||
from novaclient import exceptions
|
||||
from novaclient import utils
|
||||
import time
|
||||
HAS_NOVACLIENT = True
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient is required for this module'")
|
||||
HAS_NOVACLIENT = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -37,7 +38,7 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nova_compute
|
||||
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
|
||||
description:
|
||||
- Create or Remove virtual machines from Openstack.
|
||||
|
@ -179,7 +180,9 @@ options:
|
|||
required: false
|
||||
default: None
|
||||
version_added: "1.9"
|
||||
requirements: ["novaclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-novaclient"
|
||||
'''
|
||||
|
||||
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'],
|
||||
module.params['login_password'],
|
||||
module.params['login_tenant_name'],
|
||||
|
@ -593,4 +599,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -17,12 +17,13 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
try:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
from novaclient import exceptions as exc
|
||||
import time
|
||||
HAS_NOVACLIENT = True
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient is required for this module to work'")
|
||||
HAS_NOVACLIENT = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -81,7 +82,9 @@ options:
|
|||
required: false
|
||||
default: None
|
||||
|
||||
requirements: ["novaclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-novaclient"
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- name: Create a key pair with the running users public key
|
||||
|
@ -110,6 +113,8 @@ def main():
|
|||
state = dict(default='present', choices=['absent', 'present'])
|
||||
))
|
||||
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'],
|
||||
module.params['login_password'],
|
||||
|
@ -151,5 +156,6 @@ def main():
|
|||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
|
||||
try:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
try:
|
||||
|
@ -23,9 +25,9 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
import time
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
print("failed=True msg='novaclient,keystoneclient and quantumclient (or neutronclient) are required'")
|
||||
HAVE_DEPS = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -89,7 +91,11 @@ options:
|
|||
required: false
|
||||
default: None
|
||||
version_added: "1.5"
|
||||
requirements: ["novaclient", "quantumclient", "neutronclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-novaclient"
|
||||
- "python-neutronclient or python-quantumclient"
|
||||
- "python-keystoneclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -252,6 +258,9 @@ def main():
|
|||
))
|
||||
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:
|
||||
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')
|
||||
|
@ -285,5 +294,6 @@ def main():
|
|||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
# You should have received a copy of the GNU General Public License
|
||||
# along with this software. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import time
|
||||
try:
|
||||
from novaclient.v1_1 import client as nova_client
|
||||
try:
|
||||
|
@ -23,9 +24,9 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
import time
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
print "failed=True msg='novaclient, keystone, and quantumclient (or neutronclient) client are required'"
|
||||
HAVE_DEPS = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -81,7 +82,11 @@ options:
|
|||
- floating ip that should be assigned to the instance
|
||||
required: true
|
||||
default: None
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-novaclient"
|
||||
- "python-neutronclient or python-quantumclient"
|
||||
- "python-keystoneclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -192,6 +197,9 @@ def main():
|
|||
))
|
||||
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:
|
||||
nova = nova_client.Client(module.params['login_username'], module.params['login_password'],
|
||||
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
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
HAVE_DEPS = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -108,7 +109,10 @@ options:
|
|||
- Whether the state should be marked as up or down
|
||||
required: false
|
||||
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)
|
||||
|
||||
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 not module.params['provider_physical_network']:
|
||||
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
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -22,8 +22,9 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
print("failed=True msg='quantumclient (or neutronclient) and keystone client are required'")
|
||||
HAVE_DEPS = False
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['deprecated'],
|
||||
'supported_by': 'community',
|
||||
|
@ -84,7 +85,10 @@ options:
|
|||
- desired admin state of the created router .
|
||||
required: false
|
||||
default: true
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-neutronclient or python-quantumclient"
|
||||
- "python-keystoneclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -189,6 +193,8 @@ def main():
|
|||
admin_state_up = dict(type='bool', default=True),
|
||||
))
|
||||
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)
|
||||
_set_tenant_id(module)
|
||||
|
@ -212,5 +218,6 @@ def main():
|
|||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
HAVE_DEPS = False
|
||||
|
||||
|
@ -79,7 +80,10 @@ options:
|
|||
- Name of the external network which should be attached to the router.
|
||||
required: true
|
||||
default: None
|
||||
requirements: ["quantumclient", "neutronclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-neutronclient or python-quantumclient"
|
||||
- "python-keystoneclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -192,6 +196,8 @@ def main():
|
|||
state = dict(default='present', choices=['absent', 'present']),
|
||||
))
|
||||
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)
|
||||
router_id = _get_router_id(module, neutron)
|
||||
|
@ -220,5 +226,6 @@ def main():
|
|||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ try:
|
|||
except ImportError:
|
||||
from quantumclient.quantum import client
|
||||
from keystoneclient.v2_0 import client as ksclient
|
||||
HAVE_DEPS = True
|
||||
except ImportError:
|
||||
HAVE_DEPS = False
|
||||
|
||||
|
@ -84,7 +85,10 @@ options:
|
|||
- Name of the tenant whose subnet has to be attached.
|
||||
required: false
|
||||
default: None
|
||||
requirements: ["quantumclient", "keystoneclient"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "python-neutronclient or python-quantumclient"
|
||||
- "python-keystoneclient"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -224,6 +228,8 @@ def main():
|
|||
state = dict(default='present', choices=['absent', 'present']),
|
||||
))
|
||||
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)
|
||||
_set_tenant_id(module)
|
||||
|
@ -253,5 +259,6 @@ def main():
|
|||
# this is magic, see lib/ansible/module.params['common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
|
|
|
@ -34,6 +34,9 @@ version_added: "2.0"
|
|||
author: "Monty Taylor (@emonty)"
|
||||
description:
|
||||
- Retrieve an auth token from an OpenStack Cloud
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
extends_documentation_fragment: openstack
|
||||
'''
|
||||
|
||||
|
@ -69,4 +72,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -97,6 +97,7 @@ options:
|
|||
IP completely, or only detach it from the server. Default is to detach only.
|
||||
required: false
|
||||
default: false
|
||||
version_added: "2.1"
|
||||
requirements: ["shade"]
|
||||
'''
|
||||
|
||||
|
|
|
@ -32,6 +32,7 @@ DOCUMENTATION = '''
|
|||
module: os_ironic
|
||||
short_description: Create/Delete Bare Metal Resources from OpenStack
|
||||
extends_documentation_fragment: openstack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Create or Remove Ironic nodes from OpenStack.
|
||||
|
@ -75,28 +76,30 @@ options:
|
|||
- Information for this server's driver. Will vary based on which
|
||||
driver is in use. Any sub-field which is populated will be validated
|
||||
during creation.
|
||||
suboptions:
|
||||
power:
|
||||
- Information necessary to turn this server on / off. This often
|
||||
includes such things as IPMI username, password, and IP address.
|
||||
description:
|
||||
- Information necessary to turn this server on / off.
|
||||
This often includes such things as IPMI username, password, and IP address.
|
||||
required: true
|
||||
deploy:
|
||||
- Information necessary to deploy this server directly, without
|
||||
using Nova. THIS IS NOT RECOMMENDED.
|
||||
description:
|
||||
- Information necessary to deploy this server directly, without using Nova. THIS IS NOT RECOMMENDED.
|
||||
console:
|
||||
- Information necessary to connect to this server's serial console.
|
||||
Not all drivers support this.
|
||||
description:
|
||||
- Information necessary to connect to this server's serial console. Not all drivers support this.
|
||||
management:
|
||||
- Information necessary to interact with this server's management
|
||||
interface. May be shared by power_info in some cases.
|
||||
description:
|
||||
- Information necessary to interact with this server's management interface. May be shared by power_info in some cases.
|
||||
required: true
|
||||
nics:
|
||||
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
|
||||
properties:
|
||||
description:
|
||||
- Definition of the physical characteristics of this server, used for
|
||||
scheduling purposes
|
||||
- Definition of the physical characteristics of this server, used for scheduling purposes
|
||||
suboptions:
|
||||
cpu_arch:
|
||||
description:
|
||||
- CPU architecture (x86_64, i686, ...)
|
||||
|
@ -111,8 +114,7 @@ options:
|
|||
default: 1
|
||||
disk_size:
|
||||
description:
|
||||
- size of first storage device in this machine (typically
|
||||
/dev/sda), in GB
|
||||
- size of first storage device in this machine (typically /dev/sda), in GB
|
||||
default: 1
|
||||
skip_update_of_driver_password:
|
||||
description:
|
||||
|
|
|
@ -32,7 +32,9 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: os_ironic_node
|
||||
short_description: Activate/Deactivate Bare Metal Resources from OpenStack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Deploy to nodes controlled by Ironic.
|
||||
options:
|
||||
|
@ -71,6 +73,7 @@ options:
|
|||
- Definition of the instance information which is used to deploy
|
||||
the node. This information is only required when an instance is
|
||||
set to present.
|
||||
suboptions:
|
||||
image_source:
|
||||
description:
|
||||
- An HTTP(S) URL where the image can be retrieved from.
|
||||
|
|
|
@ -31,7 +31,8 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: os_object
|
||||
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
|
||||
description:
|
||||
- Create or Delete objects and containers from OpenStack
|
||||
|
@ -60,7 +61,6 @@ options:
|
|||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements: ["shade"]
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -32,6 +32,7 @@ DOCUMENTATION = '''
|
|||
module: os_security_group
|
||||
short_description: Add/Delete security groups from an OpenStack cloud.
|
||||
extends_documentation_fragment: openstack
|
||||
author: "Monty Taylor (@emonty)"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Add or Remove security groups from an OpenStack cloud.
|
||||
|
|
|
@ -32,7 +32,7 @@ DOCUMENTATION = '''
|
|||
module: os_security_group_rule
|
||||
short_description: Add/Delete rule from an existing security group
|
||||
extends_documentation_fragment: openstack
|
||||
version_added: "1.10"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- Add or Remove rule from an existing security group
|
||||
options:
|
||||
|
@ -81,7 +81,6 @@ options:
|
|||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
|
||||
requirements: ["shade"]
|
||||
'''
|
||||
|
||||
|
@ -257,7 +256,6 @@ def _system_state_change(module, secgroup, remotegroup):
|
|||
|
||||
|
||||
def main():
|
||||
|
||||
argument_spec = openstack_full_argument_spec(
|
||||
security_group = dict(required=True),
|
||||
# NOTE(Shrews): None is an acceptable protocol value for
|
||||
|
|
|
@ -45,12 +45,10 @@ options:
|
|||
description:
|
||||
- Name that has to be given to the instance
|
||||
required: true
|
||||
default: None
|
||||
image:
|
||||
description:
|
||||
- The name or id of the base image to boot.
|
||||
required: true
|
||||
default: None
|
||||
image_exclude:
|
||||
description:
|
||||
- Text to use to filter image names, for the case, such as HP, where
|
||||
|
@ -110,7 +108,7 @@ options:
|
|||
default: 'yes'
|
||||
aliases: ['auto_floating_ip', 'public_ip']
|
||||
floating_ips:
|
||||
decription:
|
||||
description:
|
||||
- list of valid floating IPs that pre-exist to assign to this node
|
||||
required: false
|
||||
default: None
|
||||
|
@ -648,4 +646,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -44,11 +44,11 @@ options:
|
|||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
required: false
|
||||
server:
|
||||
description:
|
||||
- Name or ID of server you want to attach a volume to
|
||||
required: true
|
||||
default: None
|
||||
volume:
|
||||
description:
|
||||
- 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.
|
||||
required: false
|
||||
default: None
|
||||
requirements: ["shade"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -153,4 +155,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_utils/common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -47,7 +47,6 @@ options:
|
|||
description:
|
||||
- Name of volume
|
||||
required: true
|
||||
default: None
|
||||
display_description:
|
||||
description:
|
||||
- String describing the volume
|
||||
|
@ -59,7 +58,7 @@ options:
|
|||
required: false
|
||||
default: None
|
||||
image:
|
||||
descritpion:
|
||||
description:
|
||||
- Image name or id for boot from volume
|
||||
required: false
|
||||
default: None
|
||||
|
@ -73,7 +72,9 @@ options:
|
|||
- Should the resource be present or absent.
|
||||
choices: [present, absent]
|
||||
default: present
|
||||
requirements: ["shade"]
|
||||
requirements:
|
||||
- "python >= 2.6"
|
||||
- "shade"
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -162,4 +163,5 @@ def main():
|
|||
# this is magic, see lib/ansible/module_common.py
|
||||
from ansible.module_utils.basic import *
|
||||
from ansible.module_utils.openstack import *
|
||||
main()
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
|
|
@ -109,7 +109,7 @@ options:
|
|||
- Data to be uploaded to the servers config drive. This option implies
|
||||
I(config_drive). Can be a file path or a string
|
||||
version_added: 1.8
|
||||
wait
|
||||
wait:
|
||||
description:
|
||||
- wait for the scaling group to finish provisioning the minimum amount of
|
||||
servers
|
||||
|
@ -121,7 +121,7 @@ options:
|
|||
description:
|
||||
- how long before wait gives up, in seconds
|
||||
default: 300
|
||||
author: Matt Martz
|
||||
author: "Matt Martz (@sivel)"
|
||||
extends_documentation_fragment: rackspace
|
||||
'''
|
||||
|
||||
|
|
|
@ -203,6 +203,10 @@ EXAMPLES = '''
|
|||
type: vmxnet3
|
||||
network: VM Network
|
||||
network_type: standard
|
||||
nic2:
|
||||
type: vmxnet3
|
||||
network: dvSwitch Network
|
||||
network_type: dvs
|
||||
vm_hardware:
|
||||
memory_mb: 2048
|
||||
num_cpus: 2
|
||||
|
@ -251,7 +255,6 @@ EXAMPLES = '''
|
|||
hostname: esx001.mydomain.local
|
||||
|
||||
# Deploy a guest from a template
|
||||
# No reconfiguration of the destination guest is done at this stage, a reconfigure would be needed to adjust memory/cpu etc..
|
||||
- vsphere_guest:
|
||||
vcenter_hostname: vcenter.mydomain.local
|
||||
username: myuser
|
||||
|
|
|
@ -26,7 +26,6 @@ ANSIBLE_METADATA = {'status': ['stableinterface'],
|
|||
DOCUMENTATION = '''
|
||||
---
|
||||
module: command
|
||||
version_added: historical
|
||||
short_description: Executes a command on a remote node
|
||||
version_added: historical
|
||||
description:
|
||||
|
@ -42,7 +41,6 @@ options:
|
|||
See the examples!
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
creates:
|
||||
description:
|
||||
- a filename or (since 2.0) glob pattern, when it already exists, this step will B(not) be run.
|
||||
|
|
|
@ -22,7 +22,6 @@ ANSIBLE_METADATA = {'status': ['stableinterface'],
|
|||
DOCUMENTATION = '''
|
||||
---
|
||||
module: raw
|
||||
version_added: historical
|
||||
short_description: Executes a low-down and dirty SSH command
|
||||
version_added: historical
|
||||
options:
|
||||
|
|
|
@ -98,7 +98,7 @@ options:
|
|||
required: false
|
||||
default: always
|
||||
choices: ['always', 'on_create']
|
||||
version_added: "1.9"
|
||||
version_added: "2.0"
|
||||
description:
|
||||
- C(always) will update passwords if they differ. C(on_create) will only set the password for newly created users.
|
||||
notes:
|
||||
|
@ -301,13 +301,6 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append
|
|||
changed = 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:
|
||||
hostnames = user_get_hostnames(cursor, [user])
|
||||
else:
|
||||
|
@ -348,7 +341,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append
|
|||
if module.check_mode:
|
||||
return True
|
||||
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:
|
||||
cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password BY %s", (user, host, password))
|
||||
changed = True
|
||||
|
@ -614,7 +607,7 @@ def main():
|
|||
module.fail_json(msg="invalid privileges string: %s" % str(e))
|
||||
|
||||
if state == "present":
|
||||
if user_exists(cursor, user, host):
|
||||
if user_exists(cursor, user, host, host_all):
|
||||
try:
|
||||
if update_password == 'always':
|
||||
changed = user_mod(cursor, user, host, host_all, password, encrypted, priv, append_privs, module)
|
||||
|
|
|
@ -349,12 +349,21 @@ def user_delete(cursor, user):
|
|||
cursor.execute("RELEASE SAVEPOINT ansible_pgsql_user_delete")
|
||||
return True
|
||||
|
||||
def has_table_privilege(cursor, user, table, priv):
|
||||
if priv == 'ALL':
|
||||
priv = ','.join([ p for p in VALID_PRIVS['table'] if p != 'ALL' ])
|
||||
query = 'SELECT has_table_privilege(%s, %s, %s)'
|
||||
cursor.execute(query, (user, table, priv))
|
||||
return cursor.fetchone()[0]
|
||||
def has_table_privileges(cursor, user, table, privs):
|
||||
"""
|
||||
Return the difference between the privileges that a user already has and
|
||||
the privileges that they desire to have.
|
||||
|
||||
: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):
|
||||
if '.' in table:
|
||||
|
@ -364,26 +373,21 @@ def get_table_privileges(cursor, user, table):
|
|||
query = '''SELECT privilege_type FROM information_schema.role_table_grants
|
||||
WHERE grantee=%s AND table_name=%s AND table_schema=%s'''
|
||||
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
|
||||
prev_priv = get_table_privileges(cursor, user, table)
|
||||
privs = ', '.join(privs)
|
||||
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)
|
||||
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
|
||||
prev_priv = get_table_privileges(cursor, user, table)
|
||||
privs = ', '.join(privs)
|
||||
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)
|
||||
curr_priv = get_table_privileges(cursor, user, table)
|
||||
return len(curr_priv) < len(prev_priv)
|
||||
|
||||
|
||||
def get_database_privileges(cursor, user, db):
|
||||
priv_map = {
|
||||
|
@ -395,54 +399,62 @@ def get_database_privileges(cursor, user, db):
|
|||
cursor.execute(query, (db,))
|
||||
datacl = cursor.fetchone()[0]
|
||||
if datacl is None:
|
||||
return []
|
||||
return set()
|
||||
r = re.search('%s=(C?T?c?)/[a-z]+\,?' % user, datacl)
|
||||
if r is None:
|
||||
return []
|
||||
o = []
|
||||
return set()
|
||||
o = set()
|
||||
for v in r.group(1):
|
||||
o.append(priv_map[v])
|
||||
return o
|
||||
o.add(priv_map[v])
|
||||
return normalize_privileges(o, 'database')
|
||||
|
||||
def has_database_privilege(cursor, user, db, priv):
|
||||
if priv == 'ALL':
|
||||
priv = ','.join([ p for p in VALID_PRIVS['database'] if p != 'ALL' ])
|
||||
query = 'SELECT has_database_privilege(%s, %s, %s)'
|
||||
cursor.execute(query, (user, db, priv))
|
||||
return cursor.fetchone()[0]
|
||||
def has_database_privileges(cursor, user, db, privs):
|
||||
"""
|
||||
Return the difference between the privileges that a user already has and
|
||||
the privileges that they desire to have.
|
||||
|
||||
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
|
||||
prev_priv = get_database_privileges(cursor, user, db)
|
||||
privs =', '.join(privs)
|
||||
if user == "PUBLIC":
|
||||
query = 'GRANT %s ON DATABASE %s TO PUBLIC' % (
|
||||
priv, pg_quote_identifier(db, 'database'))
|
||||
privs, pg_quote_identifier(db, 'database'))
|
||||
else:
|
||||
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'))
|
||||
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
|
||||
prev_priv = get_database_privileges(cursor, user, db)
|
||||
privs = ', '.join(privs)
|
||||
if user == "PUBLIC":
|
||||
query = 'REVOKE %s ON DATABASE %s FROM PUBLIC' % (
|
||||
priv, pg_quote_identifier(db, 'database'))
|
||||
privs, pg_quote_identifier(db, 'database'))
|
||||
else:
|
||||
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'))
|
||||
cursor.execute(query)
|
||||
curr_priv = get_database_privileges(cursor, user, db)
|
||||
return len(curr_priv) < len(prev_priv)
|
||||
|
||||
def revoke_privileges(cursor, user, privs):
|
||||
if privs is None:
|
||||
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
|
||||
for type_ in privs:
|
||||
for name, privileges in iteritems(privs[type_]):
|
||||
|
@ -498,6 +510,17 @@ def parse_role_attrs(role_attr_flags):
|
|||
o_flags = ' '.join(flag_set)
|
||||
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):
|
||||
"""
|
||||
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_]):
|
||||
raise InvalidPrivsError('Invalid privs specified for %s: %s' %
|
||||
(type_, ' '.join(priv_set.difference(VALID_PRIVS[type_]))))
|
||||
|
||||
priv_set = normalize_privileges(priv_set, type_)
|
||||
o_privs[type_][name] = priv_set
|
||||
|
||||
return o_privs
|
||||
|
|
|
@ -90,9 +90,11 @@ options:
|
|||
validate is passed in via '%s' which must be present as in the sshd example below.
|
||||
The command is passed securely so shell features like expansion and pipes won't work.
|
||||
required: false
|
||||
default: ""
|
||||
default: null
|
||||
version_added: "2.0"
|
||||
author: "Stephen Fromm (@sfromm)"
|
||||
extends_documentation_fragment: files
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -33,8 +33,7 @@ description:
|
|||
- 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
|
||||
sections if they don't exist.
|
||||
- Comments are discarded when the source file is read, and therefore will not
|
||||
show up in the destination file.
|
||||
- Before version 2.0, comments are discarded when the source file is read, and therefore will not show up in the destination file.
|
||||
version_added: "0.9"
|
||||
options:
|
||||
dest:
|
||||
|
|
|
@ -29,8 +29,9 @@ module: lineinfile
|
|||
author:
|
||||
- "Daniel Hokka Zakrissoni (@dhozac)"
|
||||
- "Ahti Kitsik (@ahtik)"
|
||||
extends_documentation_fragment: files
|
||||
extends_documentation_fragment: validate
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
- validate
|
||||
short_description: Ensure a particular line is in a file, or replace an
|
||||
existing line using a back-referenced regular expression.
|
||||
description:
|
||||
|
|
|
@ -30,8 +30,9 @@ DOCUMENTATION = """
|
|||
---
|
||||
module: replace
|
||||
author: "Evan Kaufman (@EvanK)"
|
||||
extends_documentation_fragment: files
|
||||
extends_documentation_fragment: validate
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
- validate
|
||||
short_description: Replace all instances of a particular string in a
|
||||
file using a back-referenced regular expression.
|
||||
description:
|
||||
|
|
|
@ -43,13 +43,10 @@ options:
|
|||
description:
|
||||
- Path of a Jinja2 formatted template on the Ansible controller. This can be a relative or absolute path.
|
||||
required: true
|
||||
default: null
|
||||
aliases: []
|
||||
dest:
|
||||
description:
|
||||
- Location to render the template to on the remote machine.
|
||||
required: true
|
||||
default: null
|
||||
backup:
|
||||
description:
|
||||
- Create a backup file including the timestamp information so you can get
|
||||
|
@ -77,8 +74,9 @@ notes:
|
|||
author:
|
||||
- Ansible Core Team
|
||||
- Michael DeHaan
|
||||
extends_documentation_fragment: files
|
||||
extends_documentation_fragment: validate
|
||||
extends_documentation_fragment:
|
||||
- files
|
||||
- validate
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
|
|
@ -86,6 +86,7 @@ options:
|
|||
required: false
|
||||
choices: [ "yes", "no" ]
|
||||
default: "no"
|
||||
version_added: '2.1'
|
||||
sha256sum:
|
||||
description:
|
||||
- If a SHA-256 checksum is passed to this parameter, the digest of the
|
||||
|
|
|
@ -66,6 +66,7 @@ options:
|
|||
- The serialization format of the body. When set to json, encodes the
|
||||
body argument, if needed, and automatically sets the Content-Type header accordingly.
|
||||
required: false
|
||||
choices: [ "raw", "json" ]
|
||||
default: raw
|
||||
version_added: "2.0"
|
||||
method:
|
||||
|
|
|
@ -72,14 +72,11 @@ options:
|
|||
default: null
|
||||
zeroize:
|
||||
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
|
||||
argument will effectively remove all current configuration
|
||||
statements on the remote device.
|
||||
required: false
|
||||
choices:
|
||||
- yes
|
||||
- no
|
||||
default: null
|
||||
confirm:
|
||||
description:
|
||||
|
|
|
@ -32,14 +32,14 @@ author:
|
|||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- I(state)=absent removes the ACE if it exists
|
||||
- I(state)=delete_acl deleted the ACL if it exists
|
||||
- for idempotency, use port numbers for the src/dest port
|
||||
- C(state=absent) removes the ACE if it exists.
|
||||
- C(state=delete_acl) deleted the ACL if it exists.
|
||||
- For idempotency, use port numbers for the src/dest port
|
||||
params like I(src_port1) and names for the well defined protocols
|
||||
for the I(proto) param.
|
||||
- while this module is idempotent in that if the ace as presented in the
|
||||
task is identical to the one on the switch, no changes will be made. If
|
||||
there is any difference, what is in Ansible will be pushed (configured
|
||||
- Although this module is idempotent in that if the ace as presented in
|
||||
the task is identical to the one on the switch, no changes will be made.
|
||||
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
|
||||
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
|
||||
|
@ -47,151 +47,152 @@ notes:
|
|||
options:
|
||||
seq:
|
||||
description:
|
||||
- sequence number of the entry (ACE)
|
||||
- Sequence number of the entry (ACE).
|
||||
required: false
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- Case sensitive name of the access list (ACL)
|
||||
- Case sensitive name of the access list (ACL).
|
||||
required: true
|
||||
action:
|
||||
description:
|
||||
- action of the ACE
|
||||
- Action of the ACE.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['permit', 'deny', 'remark']
|
||||
remark:
|
||||
description:
|
||||
- If action is set to remark, this is the description
|
||||
- If action is set to remark, this is the description.
|
||||
required: false
|
||||
default: null
|
||||
proto:
|
||||
description:
|
||||
- port number or protocol (as supported by the switch)
|
||||
- Port number or protocol (as supported by the switch).
|
||||
required: false
|
||||
default: null
|
||||
src:
|
||||
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
|
||||
default: null
|
||||
src_port_op:
|
||||
description:
|
||||
- src port operands such as eq, neq, gt, lt, range
|
||||
- Source port operands such as eq, neq, gt, lt, range.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
|
||||
src_port1:
|
||||
description:
|
||||
- port/protocol and also first (lower) port when using range
|
||||
operand
|
||||
- Port/protocol and also first (lower) port when using range
|
||||
operand.
|
||||
required: false
|
||||
default: null
|
||||
src_port2:
|
||||
description:
|
||||
- second (end) port when using range operand
|
||||
- Second (end) port when using range operand.
|
||||
required: false
|
||||
default: null
|
||||
dest:
|
||||
description:
|
||||
- dest ip and mask using IP/MASK notation and supports the
|
||||
keyword 'any'
|
||||
- Destination ip and mask using IP/MASK notation and supports the
|
||||
keyword 'any'.
|
||||
required: false
|
||||
default: null
|
||||
dest_port_op:
|
||||
description:
|
||||
- dest port operands such as eq, neq, gt, lt, range
|
||||
- Destination port operands such as eq, neq, gt, lt, range.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range']
|
||||
dest_port1:
|
||||
description:
|
||||
- port/protocol and also first (lower) port when using range
|
||||
operand
|
||||
- Port/protocol and also first (lower) port when using range
|
||||
operand.
|
||||
required: false
|
||||
default: null
|
||||
dest_port2:
|
||||
description:
|
||||
- second (end) port when using range operand
|
||||
- Second (end) port when using range operand.
|
||||
required: false
|
||||
default: null
|
||||
log:
|
||||
description:
|
||||
- Log matches against this entry
|
||||
- Log matches against this entry.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
urg:
|
||||
description:
|
||||
- Match on the URG bit
|
||||
- Match on the URG bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
ack:
|
||||
description:
|
||||
- Match on the ACK bit
|
||||
- Match on the ACK bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
psh:
|
||||
description:
|
||||
- Match on the PSH bit
|
||||
- Match on the PSH bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
rst:
|
||||
description:
|
||||
- Match on the RST bit
|
||||
- Match on the RST bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
syn:
|
||||
description:
|
||||
- Match on the SYN bit
|
||||
- Match on the SYN bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
fin:
|
||||
description:
|
||||
- Match on the FIN bit
|
||||
- Match on the FIN bit.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
established:
|
||||
description:
|
||||
- Match established connections
|
||||
- Match established connections.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
fragments:
|
||||
description:
|
||||
- Check non-initial fragments
|
||||
- Check non-initial fragments.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['enable']
|
||||
time-range:
|
||||
description:
|
||||
- Name of time-range to apply
|
||||
- Name of time-range to apply.
|
||||
required: false
|
||||
default: null
|
||||
precedence:
|
||||
description:
|
||||
- Match packets with given precedence
|
||||
- Match packets with given precedence.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['critical', 'flash', 'flash-override', 'immediate',
|
||||
'internet', 'network', 'priority', 'routine']
|
||||
dscp:
|
||||
description:
|
||||
- Match packets with given dscp value
|
||||
- Match packets with given dscp value.
|
||||
required: false
|
||||
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',
|
||||
'cs5', 'cs6', 'cs7', 'default', 'ef']
|
||||
state:
|
||||
description:
|
||||
- Specify desired state of the resource
|
||||
- Specify desired state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present','absent','delete_acl']
|
||||
|
@ -220,6 +221,7 @@ proposed:
|
|||
"proto": "tcp", "seq": "10", "src": "1.1.1.1/24"}
|
||||
existing:
|
||||
description: k/v pairs of existing ACL entries.
|
||||
returned: always
|
||||
type: dict
|
||||
sample: {}
|
||||
end_state:
|
||||
|
@ -240,217 +242,32 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -462,14 +279,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -477,93 +286,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -615,303 +354,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -919,6 +399,7 @@ def load_config(module, candidate):
|
|||
return result
|
||||
# END OF COMMON CODE
|
||||
|
||||
|
||||
def get_cli_body_ssh(command, response, module):
|
||||
"""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
|
||||
|
@ -941,6 +422,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -1195,9 +681,11 @@ def main():
|
|||
host=dict(required=True),
|
||||
username=dict(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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
|
|
@ -88,11 +88,6 @@ acl_applied_to:
|
|||
type: list
|
||||
sample: [{"acl_type": "Router ACL", "direction": "egress",
|
||||
"interface": "Ethernet1/41", "name": "ANSIBLE"}]
|
||||
state:
|
||||
description: state as sent in from the playbook
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
updates:
|
||||
description: commands sent to the device
|
||||
returned: always
|
||||
|
@ -105,211 +100,32 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import itertools
|
||||
import json
|
||||
|
||||
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 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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -321,14 +137,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -336,93 +144,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -474,18 +212,20 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
return dict(
|
||||
# config options
|
||||
running_config=dict(aliases=['config']),
|
||||
save_config=dict(type='bool', default=False, aliases=['save'])
|
||||
)
|
||||
nxos_argument_spec = argument_spec()
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def get_config(module):
|
||||
config = module.params['running_config']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
|
@ -494,15 +234,22 @@ def load_config(module, candidate):
|
|||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -511,6 +258,7 @@ def load_config(module, candidate):
|
|||
# END OF COMMON CODE
|
||||
|
||||
|
||||
|
||||
def get_cli_body_ssh(command, response, module):
|
||||
"""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
|
||||
|
@ -533,6 +281,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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()
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
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
|
||||
|
||||
|
||||
|
@ -705,9 +471,11 @@ def main():
|
|||
direction=dict(required=True, choices=['egress', 'ingress']),
|
||||
state=dict(choices=['absent', 'present'],
|
||||
default='present'),
|
||||
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)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -763,7 +531,6 @@ def main():
|
|||
results = {}
|
||||
results['proposed'] = proposed
|
||||
results['existing'] = existing
|
||||
results['state'] = state
|
||||
results['updates'] = cmds
|
||||
results['changed'] = changed
|
||||
results['end_state'] = end_state
|
||||
|
@ -771,10 +538,6 @@ def main():
|
|||
|
||||
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__':
|
||||
main()
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -24,14 +24,14 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_bgp_af
|
||||
version_added: "2.2"
|
||||
short_description: Manages BGP Address-family configuration
|
||||
short_description: Manages BGP Address-family configuration.
|
||||
description:
|
||||
- Manages BGP Address-family configurations on NX-OS switches
|
||||
- Manages BGP Address-family configurations on NX-OS switches.
|
||||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- State 'absent' removes the whole BGP ASN configuration
|
||||
- default, where supported, restores params default value
|
||||
- C(state=absent) removes the whole BGP ASN configuration
|
||||
- Default, where supported, restores params default value.
|
||||
options:
|
||||
asn:
|
||||
description:
|
||||
|
@ -45,7 +45,7 @@ options:
|
|||
required: true
|
||||
afi:
|
||||
description:
|
||||
- Address Family Identifie.
|
||||
- Address Family Identifier.
|
||||
required: true
|
||||
choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn']
|
||||
safi:
|
||||
|
@ -56,7 +56,7 @@ options:
|
|||
additional_paths_install:
|
||||
description:
|
||||
- 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.
|
||||
required: false
|
||||
choices: ['true','false']
|
||||
|
@ -175,7 +175,7 @@ options:
|
|||
keyword which indicates that attributes should be copied from
|
||||
the aggregate. For example [['lax_inject_map', 'lax_exist_map'],
|
||||
['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'],
|
||||
['fsd_inject_map', 'fsd_exist_map']]
|
||||
['fsd_inject_map', 'fsd_exist_map']].
|
||||
required: false
|
||||
default: null
|
||||
maximum_paths:
|
||||
|
@ -194,9 +194,9 @@ options:
|
|||
- Networks to configure. Valid value is a list of network
|
||||
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
|
||||
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.3.0/24', 'routemap_NYC']]
|
||||
['192.168.3.0/24', 'routemap_NYC']].
|
||||
required: false
|
||||
default: null
|
||||
next_hop_route_map:
|
||||
|
@ -213,7 +213,7 @@ options:
|
|||
redistribute from; the second entry defines a route-map name.
|
||||
A route-map is highly advised but may be optional on some
|
||||
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
|
||||
default: null
|
||||
suppress_inactive:
|
||||
|
@ -237,16 +237,11 @@ options:
|
|||
default: null
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# configure a simple address-family
|
||||
|
@ -262,17 +257,18 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4",
|
||||
"asn": "65535", "safi": "unicast", "vrf": "TESTING"}
|
||||
existing:
|
||||
description: k/v pairs of existing BGP AF configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {}
|
||||
end_state:
|
||||
description: k/v pairs of BGP AF configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"additional_paths_install": false,
|
||||
"additional_paths_receive": false,
|
||||
|
@ -305,12 +301,7 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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
|
||||
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -524,14 +332,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -539,93 +339,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -677,303 +407,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -1213,7 +684,7 @@ def get_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
|
||||
try:
|
||||
asn_regex = '.*router\sbgp\s(?P<existing_asn>\d+).*'
|
||||
|
@ -1515,13 +986,13 @@ def main():
|
|||
suppress_inactive=dict(required=False, type='bool'),
|
||||
table_map=dict(required=False, type='str'),
|
||||
table_map_filter=dict(required=False, type='bool'),
|
||||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
required_together=[DAMPENING_PARAMS,
|
||||
['distance_ibgp',
|
||||
'distance_ebgp',
|
||||
|
@ -1624,7 +1095,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,18 +24,18 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_bgp_neighbor
|
||||
version_added: "2.2"
|
||||
short_description: Manages BGP neighbors configurations
|
||||
short_description: Manages BGP neighbors configurations.
|
||||
description:
|
||||
- Manages BGP neighbors configurations on NX-OS switches
|
||||
- Manages BGP neighbors configurations on NX-OS switches.
|
||||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- State 'absent' removes the whole BGP neighbor configuration
|
||||
- default, where supported, restores params default value
|
||||
- C(state=absent) removes the whole BGP neighbor configuration.
|
||||
- Default, where supported, restores params default value.
|
||||
options:
|
||||
asn:
|
||||
description:
|
||||
- BGP autonomous system number. Valid values are String,
|
||||
- BGP autonomous system number. Valid values are string,
|
||||
Integer in ASPLAIN or ASDOT notation.
|
||||
required: true
|
||||
vrf:
|
||||
|
@ -180,16 +180,11 @@ options:
|
|||
default: null
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
# create a new neighbor
|
||||
|
@ -210,7 +205,7 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"asn": "65535", "description": "just a description",
|
||||
"local_as": "20", "neighbor": "3.3.3.3",
|
||||
|
@ -218,11 +213,12 @@ proposed:
|
|||
"update_source": "Ethernet1/3", "vrf": "default"}
|
||||
existing:
|
||||
description: k/v pairs of existing BGP neighbor configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {}
|
||||
end_state:
|
||||
description: k/v pairs of BGP neighbor configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"asn": "65535", "capability_negotiation": false,
|
||||
"connected_check": false, "description": "just a description",
|
||||
|
@ -250,12 +246,7 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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.shell import ShellError
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -468,14 +276,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -483,93 +283,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -621,303 +351,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -1045,7 +516,7 @@ def get_custom_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
custom = [
|
||||
'log_neighbor_changes',
|
||||
'pwd',
|
||||
|
@ -1207,12 +678,13 @@ def main():
|
|||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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_module(argument_spec=argument_spec,
|
||||
required_together=[['pwd', 'pwd_type'],
|
||||
['timers_holdtime', 'timers_keepalive']],
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
required_together=[['timer_bgp_hold',
|
||||
'timer_bgp_keepalive']],
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -1281,7 +753,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,17 +24,17 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_bgp_neighbor_af
|
||||
version_added: "2.2"
|
||||
short_description: Manages BGP address-family's neighbors configuration
|
||||
short_description: Manages BGP address-family's neighbors configuration.
|
||||
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)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- State 'absent' removes the whole BGP address-family's
|
||||
neighbor configuration
|
||||
- default, when supported, removes properties
|
||||
- to defaults maximum-prefix configuration, only
|
||||
max_prefix_limit=default is needed
|
||||
- C(state=absent) removes the whole BGP address-family's
|
||||
neighbor configuration.
|
||||
- Default, when supported, removes properties
|
||||
- In order to default maximum-prefix configuration, only
|
||||
C(max_prefix_limit=default) is needed.
|
||||
options:
|
||||
asn:
|
||||
description:
|
||||
|
@ -65,7 +65,7 @@ options:
|
|||
additional_paths_receive:
|
||||
description:
|
||||
- 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
|
||||
to remove the command at this level (the command value is
|
||||
inherited from a higher BGP layer).
|
||||
|
@ -75,7 +75,7 @@ options:
|
|||
additional_paths_send:
|
||||
description:
|
||||
- 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
|
||||
to remove the command at this level (the command value is
|
||||
inherited from a higher BGP layer).
|
||||
|
@ -129,7 +129,7 @@ options:
|
|||
default_originate_route_map:
|
||||
description:
|
||||
- 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,
|
||||
or 'default'.
|
||||
required: false
|
||||
|
@ -248,21 +248,16 @@ options:
|
|||
default: null
|
||||
weight:
|
||||
description:
|
||||
- weight value. Valid values are an integer value or 'default'.
|
||||
- Weight value. Valid values are an integer value or 'default'.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
configure RR client
|
||||
|
@ -281,18 +276,19 @@ configure RR client
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"afi": "ipv4", "asn": "65535",
|
||||
"neighbor": "3.3.3.3", "route_reflector_client": true,
|
||||
"safi": "unicast", "vrf": "default"}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {}
|
||||
end_state:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"additional_paths_receive": "inherit",
|
||||
"additional_paths_send": "inherit",
|
||||
|
@ -327,12 +323,7 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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.shell import ShellError
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -545,14 +353,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -560,93 +360,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -698,303 +428,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -1176,7 +647,7 @@ def get_custom_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
|
||||
custom = [
|
||||
'allowas_in_max',
|
||||
|
@ -1527,13 +998,13 @@ def main():
|
|||
suppress_inactive=dict(required=False, type='bool'),
|
||||
unsuppress_map=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',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
mutually_exclusive=[['advertise_map_exist',
|
||||
'advertise_map_non_exist']],
|
||||
supports_check_mode=True)
|
||||
|
@ -1635,7 +1106,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -35,12 +35,6 @@ options:
|
|||
- EVPN control plane.
|
||||
required: true
|
||||
choices: ['true', 'false']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- nxos_evpn_global:
|
||||
|
@ -53,17 +47,17 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"nv_overlay_evpn": true}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"nv_overlay_evpn": false}
|
||||
end_state:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"nv_overlay_evpn": true}
|
||||
updates:
|
||||
|
@ -80,213 +74,25 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import itertools
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
import ansible.module_utils.nxos
|
||||
from ansible.module_utils.basic import get_exception
|
||||
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
|
||||
from ansible.module_utils.network import NetworkModule
|
||||
from ansible.module_utils.shell import ShellError
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -298,14 +104,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -313,93 +111,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -451,18 +179,20 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
return dict(
|
||||
# config options
|
||||
running_config=dict(aliases=['config']),
|
||||
save_config=dict(type='bool', default=False, aliases=['save'])
|
||||
)
|
||||
nxos_argument_spec = argument_spec()
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def get_config(module):
|
||||
config = module.params['running_config']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
|
@ -471,15 +201,22 @@ def load_config(module, candidate):
|
|||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -545,11 +282,11 @@ def get_commands(module, existing, proposed, candidate):
|
|||
def main():
|
||||
argument_spec = dict(
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
existing = invoke('get_existing', module)
|
||||
|
@ -571,7 +308,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
@ -580,10 +317,5 @@ def main():
|
|||
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__':
|
||||
main()
|
||||
|
|
|
@ -24,7 +24,7 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_evpn_vni
|
||||
version_added: "2.2"
|
||||
short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI)
|
||||
short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI).
|
||||
description:
|
||||
- Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network
|
||||
Identifier (VNI) configurations of a Nexus device.
|
||||
|
@ -34,14 +34,14 @@ notes:
|
|||
- default, where supported, restores params default value.
|
||||
- RD override is not permitted. You should set it to the default values
|
||||
first and then reconfigure it.
|
||||
- route_target_both, route_target_import and route_target_export valid
|
||||
values are a list of extended communities, (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
|
||||
- C(route_target_both), C(route_target_import) and
|
||||
C(route_target_export valid) values are a list of extended communities,
|
||||
(i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'.
|
||||
- The C(route_target_both) property is discouraged due to the inconsistent
|
||||
behavior of the property across Nexus platforms and image versions.
|
||||
For this reason it is recommended to use explicit 'route_target_export'
|
||||
and 'route_target_import' properties instead of route_target_both.
|
||||
- RD valid values are a String in one of the route-distinguisher formats,
|
||||
For this reason it is recommended to use explicit C(route_target_export)
|
||||
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,
|
||||
the keyword 'auto', or the keyword 'default'.
|
||||
options:
|
||||
vni:
|
||||
|
@ -74,16 +74,11 @@ options:
|
|||
default: null
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- nxos_evpn_vni:
|
||||
|
@ -102,19 +97,20 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"route_target_import": ["5000:10", "4100:100",
|
||||
"5001:10"],"vni": "6000"}
|
||||
existing:
|
||||
description: k/v pairs of existing EVPN VNI configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"route_distinguisher": "70:10", "route_target_both": [],
|
||||
"route_target_export": [], "route_target_import": [
|
||||
"4100:100", "5000:10"], "vni": "6000"}
|
||||
end_state:
|
||||
description: k/v pairs of EVPN VNI configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"route_distinguisher": "70:10", "route_target_both": [],
|
||||
"route_target_export": [], "route_target_import": [
|
||||
|
@ -132,12 +128,7 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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.network import NetworkModule
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -350,14 +158,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -365,93 +165,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -503,303 +233,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -847,7 +318,7 @@ def get_route_target_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])]
|
||||
config = netcfg.get_section(parents)
|
||||
|
||||
|
@ -943,13 +414,13 @@ def main():
|
|||
route_target_both=dict(required=False, type='list'),
|
||||
route_target_import=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',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -996,7 +467,7 @@ def main():
|
|||
|
||||
candidate.add(remove_commands, parents=parents)
|
||||
result = execute_config(module, candidate)
|
||||
time.sleep(20)
|
||||
time.sleep(30)
|
||||
|
||||
candidate = CustomNetworkConfig(indent=3)
|
||||
candidate.add(commands, parents=parents)
|
||||
|
@ -1005,7 +476,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,9 +24,9 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_feature
|
||||
version_added: "2.1"
|
||||
short_description: Manage features in NX-OS switches
|
||||
short_description: Manage features in NX-OS switches.
|
||||
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
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
|
@ -81,11 +81,6 @@ end_state:
|
|||
returned: always
|
||||
type: dict
|
||||
sample: {"state": "disabled"}
|
||||
state:
|
||||
description: state as sent in from the playbook
|
||||
returned: always
|
||||
type: string
|
||||
sample: "disabled"
|
||||
updates:
|
||||
description: commands sent to the device
|
||||
returned: always
|
||||
|
@ -103,217 +98,32 @@ feature:
|
|||
sample: "vpc"
|
||||
'''
|
||||
|
||||
import json
|
||||
import collections
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -325,14 +135,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -340,93 +142,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -478,303 +210,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -820,6 +293,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -970,9 +448,11 @@ def main():
|
|||
feature=dict(type='str', required=True),
|
||||
state=dict(choices=['enabled', 'disabled'], default='enabled',
|
||||
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)
|
||||
|
||||
feature = validate_feature(module)
|
||||
|
|
|
@ -26,14 +26,16 @@ module: nxos_file_copy
|
|||
version_added: "2.2"
|
||||
short_description: Copy a file to a remote NXOS device over SCP.
|
||||
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:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- 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.
|
||||
options:
|
||||
local_file:
|
||||
|
@ -49,7 +51,8 @@ options:
|
|||
file_system:
|
||||
description:
|
||||
- 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
|
||||
default: null
|
||||
'''
|
||||
|
@ -87,214 +90,28 @@ import paramiko
|
|||
import time
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -306,14 +123,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -321,93 +130,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -459,303 +198,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -764,6 +244,11 @@ def load_config(module, candidate):
|
|||
# END OF COMMON CODE
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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()
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
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
|
||||
|
||||
|
||||
|
@ -850,7 +348,7 @@ def transfer_file(module, dest):
|
|||
scp = SCPClient(ssh.get_transport())
|
||||
try:
|
||||
scp.put(module.params['local_file'], full_remote_path)
|
||||
except Exception as e:
|
||||
except:
|
||||
time.sleep(10)
|
||||
temp_size = verify_remote_file_exists(
|
||||
module, dest, file_system=module.params['file_system'])
|
||||
|
@ -870,8 +368,11 @@ def main():
|
|||
local_file=dict(required=True),
|
||||
remote_file=dict(required=False),
|
||||
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)
|
||||
|
||||
local_file = module.params['local_file']
|
||||
|
|
|
@ -25,60 +25,60 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_hsrp
|
||||
version_added: "2.2"
|
||||
short_description: Manages HSRP configuration on NX-OS switches
|
||||
short_description: Manages HSRP configuration on NX-OS switches.
|
||||
description:
|
||||
- Manages HSRP configuration on NX-OS switches
|
||||
- Manages HSRP configuration on NX-OS switches.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- HSRP feature needs to be enabled first on the system
|
||||
- SVIs must exist before using this module
|
||||
- Interface must be a L3 port before using this module
|
||||
- HSRP cannot be configured on loopback interfaces
|
||||
- HSRP feature needs to be enabled first on the system.
|
||||
- SVIs must exist before using this module.
|
||||
- Interface must be a L3 port before using this module.
|
||||
- HSRP cannot be configured on loopback interfaces.
|
||||
- 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
|
||||
MD5 authentication only with HSRPv2 in order to enforce better practice.
|
||||
options:
|
||||
group:
|
||||
description:
|
||||
- HSRP group number
|
||||
- HSRP group number.
|
||||
required: true
|
||||
interface:
|
||||
description:
|
||||
- Full name of interface that is being managed for HSRP
|
||||
- Full name of interface that is being managed for HSRP.
|
||||
required: true
|
||||
version:
|
||||
description:
|
||||
- HSRP version
|
||||
- HSRP version.
|
||||
required: false
|
||||
default: 2
|
||||
choices: ['1','2']
|
||||
priority:
|
||||
description:
|
||||
- HSRP priority
|
||||
- HSRP priority.
|
||||
required: false
|
||||
default: null
|
||||
vip:
|
||||
description:
|
||||
- HSRP virtual IP address
|
||||
- HSRP virtual IP address.
|
||||
required: false
|
||||
default: null
|
||||
auth_string:
|
||||
description:
|
||||
- Authentication string
|
||||
- Authentication string.
|
||||
required: false
|
||||
default: null
|
||||
auth_type:
|
||||
description:
|
||||
- Authentication type
|
||||
- Authentication type.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['text','md5']
|
||||
state:
|
||||
description:
|
||||
- Specify desired state of the resource
|
||||
- Specify desired state of the resource.
|
||||
required: false
|
||||
choices: ['present','absent']
|
||||
default: 'present'
|
||||
|
@ -143,7 +143,6 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
import json
|
||||
|
||||
# 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.network import NetworkModule
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -357,14 +175,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -372,93 +182,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -510,303 +250,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -856,6 +337,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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)
|
||||
except ShellError:
|
||||
clie = get_exception()
|
||||
module.fail_json(msg='Error sending {0}'.format(command),
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
error=str(clie))
|
||||
except AttributeError:
|
||||
try:
|
||||
|
@ -1129,8 +615,11 @@ def main():
|
|||
auth_string=dict(type='str', required=False),
|
||||
state=dict(choices=['absent', 'present'], required=False,
|
||||
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)
|
||||
|
||||
interface = module.params['interface'].lower()
|
||||
|
|
|
@ -24,17 +24,18 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_igmp
|
||||
version_added: "2.2"
|
||||
short_description: Manages IGMP global configuration
|
||||
short_description: Manages IGMP global configuration.
|
||||
description:
|
||||
- Manages IGMP global configuration configuration settings
|
||||
- Manages IGMP global configuration configuration settings.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
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
|
||||
last, i.e. after the configuration takes place
|
||||
last, i.e. after the configuration takes place.
|
||||
options:
|
||||
flush_routes:
|
||||
description:
|
||||
|
@ -46,19 +47,19 @@ options:
|
|||
enforce_rtr_alert:
|
||||
description:
|
||||
- Enables or disables the enforce router alert option check for
|
||||
IGMPv2 and IGMPv3 packets
|
||||
IGMPv2 and IGMPv3 packets.
|
||||
required: false
|
||||
default: null
|
||||
choices: ['true', 'false']
|
||||
restart:
|
||||
description:
|
||||
- restarts the igmp process (using an exec config command)
|
||||
- Restarts the igmp process (using an exec config command).
|
||||
required: false
|
||||
default: null
|
||||
choices: ['true', 'false']
|
||||
state:
|
||||
description:
|
||||
- Manages desired state of the resource
|
||||
- Manages desired state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'default']
|
||||
|
@ -84,17 +85,17 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: when C(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"enforce_rtr_alert": true, "flush_routes": true}
|
||||
existing:
|
||||
description: k/v pairs of existing IGMP configuration
|
||||
returned: when C(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"enforce_rtr_alert": true, "flush_routes": false}
|
||||
end_state:
|
||||
description: k/v pairs of IGMP configuration after module execution
|
||||
returned: when C(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"enforce_rtr_alert": true, "flush_routes": true}
|
||||
updates:
|
||||
|
@ -110,215 +111,28 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -330,14 +144,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -345,93 +151,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -483,303 +219,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -806,7 +283,7 @@ def get_value(arg, config):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
config = str(custom_get_config(module))
|
||||
config = str(get_config(module))
|
||||
|
||||
for arg in args:
|
||||
existing[arg] = get_value(arg, config)
|
||||
|
@ -862,11 +339,11 @@ def main():
|
|||
enforce_rtr_alert=dict(type='bool'),
|
||||
restart=dict(type='bool', default=False),
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -909,7 +386,7 @@ def main():
|
|||
if restart:
|
||||
proposed['restart'] = restart
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,35 +24,32 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_igmp_interface
|
||||
version_added: "2.2"
|
||||
short_description: Manages IGMP interface configuration
|
||||
short_description: Manages IGMP interface configuration.
|
||||
description:
|
||||
- Manages IGMP interface configuration settings
|
||||
- Manages IGMP interface configuration settings.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- When state=default, supported params will be reset to a default state.
|
||||
These include: version, startup_query_interval, startup_query_count,
|
||||
robustness, querier_timeout, query_mrt, query_interval, last_member_qrt,
|
||||
last_member_query_count, group_timeout, report_llg, and immediate_leave
|
||||
- When state=absent, all configs for oif_prefix, oif_source, and
|
||||
oif_routemap will be removed.
|
||||
- PIM must be enabled to use this module
|
||||
- This module is for Layer 3 interfaces
|
||||
- When C(state=default), supported params will be reset to a default state.
|
||||
These include C(version), C(startup_query_interval),
|
||||
C(startup_query_count), C(robustness), C(querier_timeout), C(query_mrt),
|
||||
C(query_interval), C(last_member_qrt), C(last_member_query_count),
|
||||
C(group_timeout), C(report_llg), and C(immediate_leave).
|
||||
- When C(state=absent), all configs for C(oif_prefix), C(oif_source), and
|
||||
C(oif_routemap) will be removed.
|
||||
- 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 with 'static-oif'
|
||||
- If restart is set to true with other params set, the restart will happen
|
||||
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
|
||||
last, i.e. after the configuration takes place.
|
||||
options:
|
||||
interface:
|
||||
description:
|
||||
- The FULL interface name for IGMP configuration.
|
||||
- The full interface name for IGMP configuration.
|
||||
e.g. I(Ethernet1/2).
|
||||
required: true
|
||||
version:
|
||||
description:
|
||||
|
@ -102,7 +99,7 @@ options:
|
|||
description:
|
||||
- Sets the query interval waited after sending membership reports
|
||||
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
|
||||
default: null
|
||||
last_member_query_count:
|
||||
|
@ -156,13 +153,13 @@ options:
|
|||
default: null
|
||||
restart:
|
||||
description:
|
||||
- Restart IGMP
|
||||
- Restart IGMP.
|
||||
required: false
|
||||
choices: ['true', 'false']
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Manages desired state of the resource
|
||||
- Manages desired state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present', 'default']
|
||||
|
@ -236,217 +233,32 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
import json
|
||||
import collections
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -458,14 +270,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -473,93 +277,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -611,303 +345,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -915,7 +390,6 @@ def load_config(module, candidate):
|
|||
return result
|
||||
# END OF COMMON CODE
|
||||
|
||||
|
||||
def get_cli_body_ssh(command, response, module):
|
||||
"""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
|
||||
|
@ -939,6 +413,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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)
|
||||
except ShellError:
|
||||
clie = get_exception()
|
||||
module.fail_json(msg='Error sending {0}'.format(command),
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
error=str(clie))
|
||||
except AttributeError:
|
||||
try:
|
||||
|
@ -1272,10 +751,11 @@ def main():
|
|||
restart=dict(type='bool', default=False),
|
||||
state=dict(choices=['present', 'absent', 'default'],
|
||||
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)
|
||||
|
||||
state = module.params['state']
|
||||
|
|
|
@ -30,11 +30,11 @@ description:
|
|||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- default, where supported, restores params default value
|
||||
- Default, where supported, restores params default value.
|
||||
- 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.
|
||||
- State absent remove the whole OSPF interface configuration
|
||||
- C(state=absent) removes the whole OSPF interface configuration.
|
||||
options:
|
||||
interface:
|
||||
description:
|
||||
|
@ -84,7 +84,7 @@ options:
|
|||
default: null
|
||||
message_digest_key_id:
|
||||
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,
|
||||
message_digest_algorithm_type and message_digest_password are
|
||||
mandatory. Valid value is an integer and 'default'.
|
||||
|
@ -111,16 +111,11 @@ options:
|
|||
default: null
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- nxos_interface_ospf:
|
||||
|
@ -136,11 +131,12 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"}
|
||||
existing:
|
||||
description: k/v pairs of existing OSPF configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"area": "", "cost": "", "dead_interval": "",
|
||||
"hello_interval": "", "interface": "ethernet1/32",
|
||||
|
@ -150,7 +146,7 @@ existing:
|
|||
"ospf": "", "passive_interface": false}
|
||||
end_state:
|
||||
description: k/v pairs of OSPF configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "",
|
||||
"hello_interval": "", "interface": "ethernet1/32",
|
||||
|
@ -172,14 +168,8 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
import ansible.module_utils.nxos
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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
|
||||
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -390,14 +198,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -405,93 +205,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -543,303 +273,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -954,7 +425,7 @@ def get_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
parents = ['interface {0}'.format(module.params['interface'].capitalize())]
|
||||
config = netcfg.get_section(parents)
|
||||
if 'ospf' in config:
|
||||
|
@ -1123,13 +594,13 @@ def main():
|
|||
message_digest_encryption_type=dict(required=False, type='str',
|
||||
choices=['cisco_type_7','3des']),
|
||||
message_digest_password=dict(required=False, type='str'),
|
||||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
required_together=[['message_digest_key_id',
|
||||
'message_digest_algorithm_type',
|
||||
'message_digest_encryption_type',
|
||||
|
@ -1197,7 +668,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,37 +24,37 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_ip_interface
|
||||
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:
|
||||
- Manages Layer 3 attributes for IPv4 and IPv6 interfaces
|
||||
- Manages Layer 3 attributes for IPv4 and IPv6 interfaces.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- Interface must already be a L3 port when using this module
|
||||
- Logical interfaces (po, loop, svi) must be created first
|
||||
- I(mask) must be inserted in decimal format (i.e. 24) for
|
||||
- Interface must already be a L3 port when using this module.
|
||||
- Logical interfaces (po, loop, svi) must be created first.
|
||||
- C(mask) must be inserted in decimal format (i.e. 24) for
|
||||
both IPv6 and IPv4.
|
||||
- A single interface can have multiple IPv6 configured.
|
||||
options:
|
||||
interface:
|
||||
description:
|
||||
- Full name of interface, i.e. Ethernet1/1, vlan10
|
||||
- Full name of interface, i.e. Ethernet1/1, vlan10.
|
||||
required: true
|
||||
addr:
|
||||
description:
|
||||
- IPv4 or IPv6 Address
|
||||
- IPv4 or IPv6 Address.
|
||||
required: false
|
||||
default: null
|
||||
mask:
|
||||
description:
|
||||
- Subnet mask for IPv4 or IPv6 Address in decimal format
|
||||
- Subnet mask for IPv4 or IPv6 Address in decimal format.
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Specify desired state of the resource
|
||||
- Specify desired state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
|
@ -99,11 +99,6 @@ end_state:
|
|||
sample: {"addresses": [{"addr": "20.20.20.20", "mask": 24}],
|
||||
"interface": "ethernet1/32", "prefix": "20.20.20.0",
|
||||
"type": "ethernet", "vrf": "default"}
|
||||
state:
|
||||
description: state as sent in from the playbook
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
updates:
|
||||
description: commands sent to the device
|
||||
returned: always
|
||||
|
@ -116,216 +111,32 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import json
|
||||
import collections
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
|
||||
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -337,14 +148,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -352,93 +155,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -490,303 +223,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -834,6 +308,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -1157,9 +636,11 @@ def main():
|
|||
mask=dict(type='str', required=False),
|
||||
state=dict(required=False, default='present',
|
||||
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)
|
||||
|
||||
addr = module.params['addr']
|
||||
|
|
|
@ -24,6 +24,7 @@ DOCUMENTATION = '''
|
|||
---
|
||||
|
||||
module: nxos_ntp_options
|
||||
version_added: "2.2"
|
||||
short_description: Manages NTP options.
|
||||
description:
|
||||
- Manages NTP options, e.g. authoritative server and logging.
|
||||
|
|
|
@ -36,16 +36,11 @@ options:
|
|||
required: true
|
||||
state:
|
||||
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
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -60,22 +55,22 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ospf": "1"}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ospf": ["2"]}
|
||||
end_state:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ospf": ["1", "2"]}
|
||||
updates:
|
||||
description: commands sent to the device
|
||||
returned: when I(m_facts)=true
|
||||
returned: always
|
||||
type: list
|
||||
sample: ["router ospf 1"]
|
||||
changed:
|
||||
|
@ -87,213 +82,25 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import itertools
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
import ansible.module_utils.nxos
|
||||
from ansible.module_utils.basic import get_exception
|
||||
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine
|
||||
from ansible.module_utils.network import NetworkModule
|
||||
from ansible.module_utils.shell import ShellError
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -305,14 +112,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -320,93 +119,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -458,18 +187,20 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
return dict(
|
||||
# config options
|
||||
running_config=dict(aliases=['config']),
|
||||
save_config=dict(type='bool', default=False, aliases=['save'])
|
||||
)
|
||||
nxos_argument_spec = argument_spec()
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def get_config(module):
|
||||
config = module.params['running_config']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
|
@ -478,15 +209,22 @@ def load_config(module, candidate):
|
|||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -560,13 +298,13 @@ def state_absent(module, proposed, candidate):
|
|||
def main():
|
||||
argument_spec = dict(
|
||||
ospf=dict(required=True, type='str'),
|
||||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -596,7 +334,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
@ -605,11 +343,5 @@ def main():
|
|||
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__':
|
||||
main()
|
||||
|
|
|
@ -111,12 +111,6 @@ options:
|
|||
Valid values are an integer, in Mbps, or the keyword 'default'.
|
||||
required: false
|
||||
default: null
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -129,7 +123,6 @@ EXAMPLES = '''
|
|||
timer_throttle_lsa_hold: 1100
|
||||
timer_throttle_lsa_max: 3000
|
||||
vrf: test
|
||||
m_facts: true
|
||||
state: present
|
||||
username: "{{ un }}"
|
||||
password: "{{ pwd }}"
|
||||
|
@ -139,7 +132,7 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100",
|
||||
"timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60",
|
||||
|
@ -148,7 +141,7 @@ proposed:
|
|||
"vrf": "test"}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
|
||||
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000",
|
||||
|
@ -158,7 +151,7 @@ existing:
|
|||
"timer_throttle_spf_start": "200", "vrf": "test"}
|
||||
end_state:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: when I(m_facts)=true
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "",
|
||||
"ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100",
|
||||
|
@ -180,12 +173,7 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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.shell import ShellError
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -398,14 +203,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -413,93 +210,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -551,303 +278,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -855,6 +323,7 @@ def load_config(module, candidate):
|
|||
return result
|
||||
# END OF COMMON CODE
|
||||
|
||||
|
||||
PARAM_TO_COMMAND_KEYMAP = {
|
||||
'router_id': 'router-id',
|
||||
'default_metric': 'default-metric',
|
||||
|
@ -912,7 +381,7 @@ def get_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
parents = ['router ospf {0}'.format(module.params['ospf'])]
|
||||
|
||||
if module.params['vrf'] != 'default':
|
||||
|
@ -1052,13 +521,13 @@ def main():
|
|||
timer_throttle_spf_hold=dict(required=False, type='str'),
|
||||
timer_throttle_spf_max=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',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
state = module.params['state']
|
||||
|
@ -1111,7 +580,7 @@ def main():
|
|||
result['updates'] = []
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -30,7 +30,7 @@ description:
|
|||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
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",
|
||||
"EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE"
|
||||
options:
|
||||
|
@ -39,13 +39,8 @@ options:
|
|||
- Anycast gateway mac of the switch.
|
||||
required: true
|
||||
default: null
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
- nxos_overlay_global:
|
||||
anycast_gateway_mac: "b.b.b"
|
||||
|
@ -57,23 +52,56 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"12:34:56:78:9a:bc"}
|
||||
sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
description: k/v pairs of existing BGP configuration
|
||||
returned: verbose mode
|
||||
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:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: always
|
||||
description: k/v pairs of BGP configuration after module execution
|
||||
returned: verbose mode
|
||||
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:
|
||||
description: commands sent to the device
|
||||
returned: always
|
||||
type: list
|
||||
sample: ["fabric forwarding anycast-gateway-mac 1234.5678.9ABC"]
|
||||
sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"]
|
||||
changed:
|
||||
description: check to see if a change was made on the device
|
||||
returned: always
|
||||
|
@ -82,214 +110,28 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -301,14 +143,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -316,93 +150,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -454,303 +218,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -779,7 +284,7 @@ def get_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
config = str(custom_get_config(module))
|
||||
config = str(get_config(module))
|
||||
|
||||
for arg in args:
|
||||
existing[arg] = get_value(arg, config, module)
|
||||
|
@ -867,10 +372,11 @@ def main():
|
|||
argument_spec = dict(
|
||||
anycast_gateway_mac=dict(required=True, type='str'),
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
args = [
|
||||
|
@ -894,7 +400,7 @@ def main():
|
|||
module.fail_json(msg=str(exc))
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,9 +24,9 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_pim
|
||||
version_added: "2.2"
|
||||
short_description: Manages configuration of an Protocol Independent Multicast (PIM) instance.
|
||||
short_description: Manages configuration of a PIM instance.
|
||||
description:
|
||||
- Manages configuration of an Protocol Independent Multicast (PIM) instance.
|
||||
- Manages configuration of a Protocol Independent Multicast (PIM) instance.
|
||||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
options:
|
||||
|
@ -35,12 +35,6 @@ options:
|
|||
- Configure group ranges for Source Specific Multicast (SSM).
|
||||
Valid values are multicast addresses or the keyword 'none'.
|
||||
required: true
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- nxos_pim:
|
||||
|
@ -53,16 +47,17 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ssm_range": "232.0.0.0/8"}
|
||||
existing:
|
||||
description: k/v pairs of existing PIM configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ssm_range": none}
|
||||
end_state:
|
||||
description: k/v pairs of BGP configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"ssm_range": "232.0.0.0/8"}
|
||||
updates:
|
||||
|
@ -79,214 +74,28 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -298,14 +107,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -313,93 +114,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -451,303 +182,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -779,7 +251,7 @@ def get_value(arg, config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
config = str(custom_get_config(module))
|
||||
config = str(get_config(module))
|
||||
for arg in args:
|
||||
existing[arg] = get_value(arg, config, module)
|
||||
return existing
|
||||
|
@ -815,10 +287,11 @@ def main():
|
|||
argument_spec = dict(
|
||||
ssm_range=dict(required=True, type='str'),
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
splitted_ssm_range = module.params['ssm_range'].split('.')
|
||||
|
@ -847,7 +320,7 @@ def main():
|
|||
module.fail_json(msg=str(exc))
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,15 +24,14 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_pim_rp_address
|
||||
version_added: "2.2"
|
||||
short_description: Manages configuration of an Protocol Independent Multicast
|
||||
(PIM) static rendezvous point (RP) address instance.
|
||||
short_description: Manages configuration of an PIM static RP address instance.
|
||||
description:
|
||||
- Manages configuration of an Protocol Independent Multicast (PIM) static
|
||||
rendezvous point (RP) address instance.
|
||||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- state=absent remove the whole rp-address configuration, if existing.
|
||||
- C(state=absent) remove the whole rp-address configuration, if existing.
|
||||
options:
|
||||
rp_address:
|
||||
description:
|
||||
|
@ -63,12 +62,6 @@ options:
|
|||
required: false
|
||||
choices: ['true','false']
|
||||
default: null
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
EXAMPLES = '''
|
||||
- nxos_pim_rp_address:
|
||||
|
@ -82,16 +75,17 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"rp_address": "10.1.1.21"}
|
||||
existing:
|
||||
description: list of existing pim rp-address configuration entries
|
||||
returned: verbose mode
|
||||
type: list
|
||||
sample: []
|
||||
end_state:
|
||||
description: pim rp-address configuration entries after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: list
|
||||
sample: [{"bidir": false, "group_list": "224.0.0.0/4",
|
||||
"rp_address": "10.1.1.21"}]
|
||||
|
@ -110,214 +104,28 @@ changed:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -329,14 +137,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -344,93 +144,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -482,303 +212,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -825,7 +296,7 @@ def get_value(config, module):
|
|||
|
||||
def get_existing(module, args):
|
||||
existing = {}
|
||||
config = str(custom_get_config(module))
|
||||
config = str(get_config(module))
|
||||
existing = get_value(config, module)
|
||||
return existing
|
||||
|
||||
|
@ -880,13 +351,13 @@ def main():
|
|||
prefix_list=dict(required=False, type='str'),
|
||||
route_map=dict(required=False, type='str'),
|
||||
bidir=dict(required=False, type='bool'),
|
||||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
mutually_exclusive=[['group_list', 'route_map'],
|
||||
['group_list', 'prefix_list'],
|
||||
['route_map', 'prefix_list']],
|
||||
|
@ -929,7 +400,7 @@ def main():
|
|||
module.fail_json(msg=str(exc))
|
||||
|
||||
result['connected'] = module.connected
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, args)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -24,29 +24,31 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_ping
|
||||
version_added: "2.1"
|
||||
short_description: Tests reachability using ping from Nexus switch
|
||||
short_description: Tests reachability using ping from Nexus switch.
|
||||
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
|
||||
author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele)
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
options:
|
||||
dest:
|
||||
description:
|
||||
- IP address or hostname (resolvable by switch) of remote node
|
||||
- IP address or hostname (resolvable by switch) of remote node.
|
||||
required: true
|
||||
count:
|
||||
description:
|
||||
- Number of packets to send
|
||||
- Number of packets to send.
|
||||
required: false
|
||||
default: 2
|
||||
source:
|
||||
description:
|
||||
- Source IP Address
|
||||
- Source IP Address.
|
||||
required: false
|
||||
default: null
|
||||
vrf:
|
||||
description:
|
||||
- Outgoing VRF
|
||||
- Outgoing VRF.
|
||||
required: false
|
||||
default: null
|
||||
'''
|
||||
|
@ -114,216 +116,32 @@ packet_loss:
|
|||
sample: "0.00%"
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import json
|
||||
import collections
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
|
||||
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -335,14 +153,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -350,93 +160,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -488,303 +228,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -833,6 +314,11 @@ def get_statistics_summary_line(response_as_list):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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()
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
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
|
||||
|
||||
|
||||
|
@ -885,8 +384,11 @@ def main():
|
|||
source=dict(required=False),
|
||||
state=dict(required=False, choices=['present', 'absent'],
|
||||
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)
|
||||
|
||||
destination = module.params['dest']
|
||||
|
|
|
@ -26,7 +26,7 @@ module: nxos_reboot
|
|||
version_added: 2.2
|
||||
short_description: Reboot a network device.
|
||||
description:
|
||||
- Reboot a network device
|
||||
- Reboot a network device.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
|
@ -58,215 +58,32 @@ rebooted:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
|
||||
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -278,14 +95,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -293,93 +102,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -431,303 +170,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -743,6 +223,11 @@ def reboot(module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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()
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
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 execute_show_command(command, module, command_type='cli_show'):
|
||||
if module.params['transport'] == 'cli':
|
||||
body = execute_show(command, module, reboot=reboot)
|
||||
body = execute_show(command, module)
|
||||
elif module.params['transport'] == 'nxapi':
|
||||
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):
|
||||
command = 'terminal dont-ask'
|
||||
command = ['terminal dont-ask']
|
||||
body = execute_show_command(command, module, command_type='cli_show_ascii')[0]
|
||||
|
||||
|
||||
def main():
|
||||
argument_spec = dict(
|
||||
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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
supports_check_mode=True)
|
||||
|
||||
confirm = module.params['confirm']
|
||||
|
|
|
@ -23,24 +23,29 @@ ANSIBLE_METADATA = {'status': ['preview'],
|
|||
DOCUMENTATION = '''
|
||||
---
|
||||
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:
|
||||
- This module offers the ability to set a configuration checkpoint file or rollback
|
||||
to a configuration checkpoint file on Cisco NXOS switches-
|
||||
- This module offers the ability to set a configuration checkpoint
|
||||
file or rollback to a configuration checkpoint file on Cisco NXOS
|
||||
switches.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- Sometimes C(transport)=nxapi may cause a timeout error.
|
||||
- Sometimes C(transport=nxapi) may cause a timeout error.
|
||||
options:
|
||||
checkpoint_file:
|
||||
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
|
||||
default: null
|
||||
rollback_to:
|
||||
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
|
||||
default: null
|
||||
'''
|
||||
|
@ -73,214 +78,28 @@ status:
|
|||
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -292,14 +111,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -307,93 +118,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -445,303 +186,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -751,15 +233,34 @@ def load_config(module, candidate):
|
|||
|
||||
|
||||
def execute_commands(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
module.execute(cmds, command_type=command_type)
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
else:
|
||||
module.execute(cmds)
|
||||
response = module.execute(cmds)
|
||||
except ShellError:
|
||||
clie = get_exception()
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
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):
|
||||
|
@ -776,16 +277,27 @@ def checkpoint(filename, module):
|
|||
|
||||
def rollback(filename, module):
|
||||
commands = ['rollback running-config file %s' % filename]
|
||||
try:
|
||||
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():
|
||||
argument_spec = dict(
|
||||
checkpoint_file=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_module(argument_spec=argument_spec,
|
||||
module = get_network_module(argument_spec=argument_spec,
|
||||
mutually_exclusive=[['checkpoint_file',
|
||||
'rollback_to']],
|
||||
supports_check_mode=False)
|
||||
|
|
|
@ -32,11 +32,11 @@ author: Gabriele Gerbino (@GGabriele)
|
|||
notes:
|
||||
- The module can only activate and commit a package,
|
||||
not remove or deactivate it.
|
||||
- Use I(transport)=nxapi to avoid connection timeout
|
||||
- Use C(transport=nxapi) to avoid connection timeout
|
||||
options:
|
||||
pkg:
|
||||
description:
|
||||
- Name of the remote package
|
||||
- Name of the remote package.
|
||||
required: true
|
||||
file_system:
|
||||
description:
|
||||
|
@ -81,215 +81,32 @@ changed:
|
|||
'''
|
||||
|
||||
import time
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
|
||||
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -301,14 +118,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -316,93 +125,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -454,303 +193,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -759,6 +239,11 @@ def load_config(module, candidate):
|
|||
# END OF COMMON CODE
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -861,8 +346,11 @@ def main():
|
|||
argument_spec = dict(
|
||||
pkg=dict(required=True),
|
||||
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)
|
||||
|
||||
pkg = module.params['pkg']
|
||||
|
|
|
@ -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'],
|
||||
'supported_by': 'community',
|
||||
|
@ -26,7 +29,9 @@ short_description: Manages SNMP host configuration.
|
|||
description:
|
||||
- Manages SNMP host configuration parameters.
|
||||
extends_documentation_fragment: nxos
|
||||
author: Jason Edelman (@jedelman8)
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
notes:
|
||||
- C(state=absent) removes the host configuration if it is configured.
|
||||
options:
|
||||
|
|
|
@ -24,9 +24,11 @@ ANSIBLE_METADATA = {'status': ['preview'],
|
|||
DOCUMENTATION = '''
|
||||
---
|
||||
module: nxos_snmp_location
|
||||
version_added: "2.2"
|
||||
short_description: Manages SNMP location information.
|
||||
description:
|
||||
- Manages SNMP location configuration.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
- Gabriele Gerbino (@GGabriele)
|
||||
|
|
|
@ -29,12 +29,13 @@ description:
|
|||
author: Gabriele Gerbino (@GGabriele)
|
||||
extends_documentation_fragment: nxos
|
||||
notes:
|
||||
- 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 no vrf is supplied, vrf is set to default.
|
||||
- If C(state=absent), the route will be removed, regardless of the
|
||||
non-required parameters.
|
||||
options:
|
||||
prefix:
|
||||
description:
|
||||
- Destination prefix of static route
|
||||
- Destination prefix of static route.
|
||||
required: true
|
||||
next_hop:
|
||||
description:
|
||||
|
@ -43,7 +44,7 @@ options:
|
|||
required: true
|
||||
vrf:
|
||||
description:
|
||||
- VRF for static route
|
||||
- VRF for static route.
|
||||
required: false
|
||||
default: default
|
||||
tag:
|
||||
|
@ -58,20 +59,14 @@ options:
|
|||
default: null
|
||||
pref:
|
||||
description:
|
||||
- Preference or administrative difference of route (range 1-255)
|
||||
- Preference or administrative difference of route (range 1-255).
|
||||
required: false
|
||||
default: null
|
||||
state:
|
||||
description:
|
||||
- Manage the state of the resource
|
||||
- Manage the state of the resource.
|
||||
required: true
|
||||
choices: ['present','absent']
|
||||
m_facts:
|
||||
description:
|
||||
- Used to print module facts
|
||||
required: false
|
||||
default: false
|
||||
choices: ['true','false']
|
||||
'''
|
||||
|
||||
EXAMPLES = '''
|
||||
|
@ -88,18 +83,19 @@ EXAMPLES = '''
|
|||
RETURN = '''
|
||||
proposed:
|
||||
description: k/v pairs of parameters passed into module
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"next_hop": "3.3.3.3", "pref": "100",
|
||||
"prefix": "192.168.20.64/24", "route_name": "testing",
|
||||
"vrf": "default"}
|
||||
existing:
|
||||
description: k/v pairs of existing configuration
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {}
|
||||
end_state:
|
||||
description: k/v pairs of configuration after module execution
|
||||
returned: always
|
||||
returned: verbose mode
|
||||
type: dict
|
||||
sample: {"next_hop": "3.3.3.3", "pref": "100",
|
||||
"prefix": "192.168.20.0/24", "route_name": "testing",
|
||||
|
@ -117,193 +113,42 @@ changed:
|
|||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
from ansible.module_utils.basic import get_exception
|
||||
from ansible.module_utils.netcfg import NetworkConfig, ConfigLine, dumps
|
||||
from ansible.module_utils.network import NetworkModule
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
else:
|
||||
match = childline.match(line)
|
||||
line_indent = match.start(1)
|
||||
level = int(line_indent / indent)
|
||||
parent_level = level - 1
|
||||
return list()
|
||||
|
||||
cfg.parents = ancestors[:level]
|
||||
|
||||
if level > len(ancestors):
|
||||
config.append(cfg)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
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
|
||||
|
||||
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 = ''
|
||||
def get_object(self, path):
|
||||
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])
|
||||
|
||||
|
@ -324,119 +169,6 @@ class CustomNetworkConfig(object):
|
|||
raise ValueError('path does not exist in config')
|
||||
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):
|
||||
"""Adds one or lines of configuration
|
||||
|
@ -487,303 +219,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -809,7 +282,7 @@ def state_present(module, candidate, prefix):
|
|||
|
||||
|
||||
def state_absent(module, candidate, prefix):
|
||||
netcfg = custom_get_config(module)
|
||||
netcfg = get_config(module)
|
||||
commands = list()
|
||||
parents = 'vrf context {0}'.format(module.params['vrf'])
|
||||
invoke('set_route', module, commands, prefix)
|
||||
|
@ -828,17 +301,13 @@ def state_absent(module, candidate, prefix):
|
|||
|
||||
|
||||
def fix_prefix_to_regex(prefix):
|
||||
prefix = prefix.split('.')
|
||||
prefix = '\.'.join(prefix)
|
||||
prefix = prefix.split('/')
|
||||
prefix = '\/'.join(prefix)
|
||||
|
||||
prefix = prefix.replace('.', '\.').replace('/', '\/')
|
||||
return prefix
|
||||
|
||||
|
||||
def get_existing(module, prefix, warnings):
|
||||
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'])
|
||||
prefix_to_regex = fix_prefix_to_regex(prefix)
|
||||
|
||||
|
@ -952,16 +421,17 @@ def main():
|
|||
tag=dict(type='str'),
|
||||
route_name=dict(type='str'),
|
||||
pref=dict(type='str'),
|
||||
m_facts=dict(required=False, default=False, type='bool'),
|
||||
state=dict(choices=['absent', '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)
|
||||
|
||||
m_facts = module.params['m_facts']
|
||||
state = module.params['state']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
@ -981,16 +451,15 @@ def main():
|
|||
try:
|
||||
response = load_config(module, candidate)
|
||||
result.update(response)
|
||||
except ShellError:
|
||||
except Exception:
|
||||
exc = get_exception()
|
||||
module.fail_json(msg=str(exc))
|
||||
else:
|
||||
result['updates'] = []
|
||||
|
||||
result['warnings'] = warnings
|
||||
result['connected'] = module.connected
|
||||
|
||||
if module.params['m_facts']:
|
||||
if module._verbosity > 0:
|
||||
end_state = invoke('get_existing', module, prefix, warnings)
|
||||
result['end_state'] = end_state
|
||||
result['existing'] = existing
|
||||
|
|
|
@ -145,8 +145,8 @@ end_state:
|
|||
updates:
|
||||
description: command string sent to the device
|
||||
returned: always
|
||||
type: string
|
||||
sample: "interface eth1/5 ; switchport access vlan 20 ;"
|
||||
type: list
|
||||
sample: ["interface eth1/5", "switchport access vlan 20"]
|
||||
changed:
|
||||
description: check to see if a change was made on the device
|
||||
returned: always
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# 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/> .
|
||||
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
ANSIBLE_METADATA = {'status': ['preview'],
|
||||
|
@ -24,51 +24,51 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_vlan
|
||||
version_added: "2.1"
|
||||
short_description: Manages VLAN resources and attributes
|
||||
short_description: Manages VLAN resources and attributes.
|
||||
description:
|
||||
- Manages VLAN configurations on NX-OS switches
|
||||
- Manages VLAN configurations on NX-OS switches.
|
||||
author: Jason Edelman (@jedelman8)
|
||||
extends_documentation_fragment: nxos
|
||||
options:
|
||||
vlan_id:
|
||||
description:
|
||||
- single vlan id
|
||||
- Single VLAN ID.
|
||||
required: false
|
||||
default: null
|
||||
vlan_range:
|
||||
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
|
||||
default: null
|
||||
name:
|
||||
description:
|
||||
- name of VLAN
|
||||
- Name of VLAN.
|
||||
required: false
|
||||
default: null
|
||||
vlan_state:
|
||||
description:
|
||||
- Manage the vlan operational state of the VLAN
|
||||
(equivalent to state {active | suspend} command
|
||||
(equivalent to state {active | suspend} command.
|
||||
required: false
|
||||
default: active
|
||||
choices: ['active','suspend']
|
||||
admin_state:
|
||||
description:
|
||||
- Manage the vlan admin state of the VLAN equivalent
|
||||
to shut/no shut in vlan config mode
|
||||
- Manage the VLAN administrative state of the VLAN equivalent
|
||||
to shut/no shut in VLAN config mode.
|
||||
required: false
|
||||
default: up
|
||||
choices: ['up','down']
|
||||
mapped_vni:
|
||||
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'.
|
||||
required: false
|
||||
default: null
|
||||
version_added: "2.2"
|
||||
state:
|
||||
description:
|
||||
- Manage the state of the resource
|
||||
- Manage the state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
|
@ -141,11 +141,6 @@ end_state:
|
|||
type: dict or null
|
||||
sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20",
|
||||
"vlan_state": "suspend", "mapped_vni": "5000"}
|
||||
state:
|
||||
description: state as sent in from the playbook
|
||||
returned: always
|
||||
type: string
|
||||
sample: "present"
|
||||
updates:
|
||||
description: command string sent to the device
|
||||
returned: always
|
||||
|
@ -159,216 +154,32 @@ changed:
|
|||
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import json
|
||||
import collections
|
||||
|
||||
from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception
|
||||
from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -380,14 +191,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -395,93 +198,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -533,303 +266,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -1035,6 +509,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if 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)
|
||||
except ShellError:
|
||||
clie = get_exception()
|
||||
module.fail_json(msg='Error sending {0}'.format(command),
|
||||
module.fail_json(msg='Error sending {0}'.format(cmds),
|
||||
error=str(clie))
|
||||
except AttributeError:
|
||||
try:
|
||||
|
@ -1084,8 +563,11 @@ def main():
|
|||
state=dict(choices=['present', 'absent'], default='present',
|
||||
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'],
|
||||
['vlan_id', 'vlan_range']],
|
||||
supports_check_mode=True)
|
||||
|
|
|
@ -149,215 +149,28 @@ import json
|
|||
import collections
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -369,14 +182,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -384,93 +189,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -522,303 +257,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -865,6 +341,11 @@ def get_cli_body_ssh(command, response, module):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -1092,9 +573,11 @@ def main():
|
|||
auto_recovery=dict(required=True, type='bool'),
|
||||
delay_restore=dict(required=False, type='str'),
|
||||
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)
|
||||
|
||||
domain = module.params['domain']
|
||||
|
|
|
@ -41,11 +41,11 @@ notes:
|
|||
options:
|
||||
portchannel:
|
||||
description:
|
||||
- group number of the portchannel that will be configured
|
||||
- Group number of the portchannel that will be configured.
|
||||
required: true
|
||||
vpc:
|
||||
description:
|
||||
- vpc group/id that will be configured on associated portchannel
|
||||
- VPC group/id that will be configured on associated portchannel.
|
||||
required: false
|
||||
default: null
|
||||
peer_link:
|
||||
|
@ -55,7 +55,7 @@ options:
|
|||
default: null
|
||||
state:
|
||||
description:
|
||||
- Manages desired state of the resource
|
||||
- Manages desired state of the resource.
|
||||
required: true
|
||||
choices: ['present','absent']
|
||||
'''
|
||||
|
@ -96,216 +96,33 @@ changed:
|
|||
sample: true
|
||||
'''
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
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 BOOLEANS_TRUE, BOOLEANS_FALSE
|
||||
from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO
|
||||
from ansible.module_utils.netcfg import parse
|
||||
from ansible.module_utils.urls import fetch_url
|
||||
# COMMON CODE FOR MIGRATION
|
||||
import re
|
||||
|
||||
from ansible.module_utils.basic import get_exception
|
||||
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 = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -317,14 +134,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -332,93 +141,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -470,303 +209,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -776,7 +256,7 @@ def load_config(module, candidate):
|
|||
|
||||
def execute_config_command(commands, module):
|
||||
try:
|
||||
output = module.configure(commands)
|
||||
response = module.configure(commands)
|
||||
except ShellError:
|
||||
clie = get_exception()
|
||||
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):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -996,8 +481,11 @@ def main():
|
|||
vpc=dict(required=False, type='str'),
|
||||
peer_link=dict(required=False, type='bool'),
|
||||
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']],
|
||||
supports_check_mode=True)
|
||||
|
||||
|
|
|
@ -24,9 +24,9 @@ DOCUMENTATION = '''
|
|||
---
|
||||
module: nxos_vrf
|
||||
version_added: "2.1"
|
||||
short_description: Manages global VRF configuration
|
||||
short_description: Manages global VRF configuration.
|
||||
description:
|
||||
- Manages global VRF configuration
|
||||
- Manages global VRF configuration.
|
||||
extends_documentation_fragment: nxos
|
||||
author:
|
||||
- Jason Edelman (@jedelman8)
|
||||
|
@ -34,20 +34,20 @@ author:
|
|||
notes:
|
||||
- Cisco NX-OS creates the default VRF by itself. Therefore,
|
||||
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
|
||||
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
|
||||
a VRF named NTC, but running it again with I(vrf=ntc) will not cause
|
||||
unless the VRF is removed and re-created. i.e. C(vrf=NTC) will create
|
||||
a VRF named NTC, but running it again with C(vrf=ntc) will not cause
|
||||
a configuration change.
|
||||
options:
|
||||
vrf:
|
||||
description:
|
||||
- Name of VRF to be managed
|
||||
- Name of VRF to be managed.
|
||||
required: true
|
||||
admin_state:
|
||||
description:
|
||||
- Administrative state of the VRF
|
||||
- Administrative state of the VRF.
|
||||
required: false
|
||||
default: up
|
||||
choices: ['up','down']
|
||||
|
@ -68,13 +68,13 @@ options:
|
|||
version_added: "2.2"
|
||||
state:
|
||||
description:
|
||||
- Manages desired state of the resource
|
||||
- Manages desired state of the resource.
|
||||
required: false
|
||||
default: present
|
||||
choices: ['present','absent']
|
||||
description:
|
||||
description:
|
||||
- Description of the VRF
|
||||
- Description of the VRF.
|
||||
required: false
|
||||
default: null
|
||||
'''
|
||||
|
@ -121,13 +121,7 @@ changed:
|
|||
import json
|
||||
|
||||
# COMMON CODE FOR MIGRATION
|
||||
|
||||
import re
|
||||
import time
|
||||
import collections
|
||||
import itertools
|
||||
import shlex
|
||||
import json
|
||||
|
||||
import ansible.module_utils.nxos
|
||||
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.network import NetworkModule
|
||||
|
||||
DEFAULT_COMMENT_TOKENS = ['#', '!']
|
||||
|
||||
class ConfigLine(object):
|
||||
|
||||
def __init__(self, text):
|
||||
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
|
||||
def to_list(val):
|
||||
if isinstance(val, (list, tuple)):
|
||||
return list(val)
|
||||
elif val is not None:
|
||||
return [val]
|
||||
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
|
||||
return list()
|
||||
|
||||
|
||||
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()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [path]
|
||||
obj = self.get_object(path)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
class CustomNetworkConfig(NetworkConfig):
|
||||
|
||||
def expand_section(self, configobj, S=None):
|
||||
if S is None:
|
||||
|
@ -340,14 +151,6 @@ class CustomNetworkConfig(object):
|
|||
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]:
|
||||
|
@ -355,93 +158,23 @@ class CustomNetworkConfig(object):
|
|||
if parents == path[:-1]:
|
||||
return item
|
||||
|
||||
def get_children(self, path):
|
||||
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)
|
||||
return self.to_block(section)
|
||||
except ValueError:
|
||||
return list()
|
||||
|
||||
def get_section_objects(self, path):
|
||||
if not isinstance(path, list):
|
||||
path = [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)
|
||||
if not obj:
|
||||
raise ValueError('path does not exist in config')
|
||||
return self.expand_section(obj)
|
||||
|
||||
|
||||
def add(self, lines, parents=None):
|
||||
|
@ -493,303 +226,44 @@ class CustomNetworkConfig(object):
|
|||
self.items.append(item)
|
||||
|
||||
|
||||
def argument_spec():
|
||||
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)
|
||||
|
||||
def get_network_module(**kwargs):
|
||||
try:
|
||||
self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE,
|
||||
errors_re=CLI_ERRORS_RE)
|
||||
self.shell.open(host, port=port, username=username,
|
||||
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)
|
||||
return get_module(**kwargs)
|
||||
except NameError:
|
||||
return NetworkModule(**kwargs)
|
||||
|
||||
def send(self, commands, encoding='text'):
|
||||
try:
|
||||
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']
|
||||
def get_config(module, include_defaults=False):
|
||||
config = module.params['config']
|
||||
if not config:
|
||||
cmd = 'show running-config'
|
||||
if module.params['include_defaults']:
|
||||
cmd += ' all'
|
||||
if module.params['transport'] == 'nxapi':
|
||||
config = module.execute([cmd], command_type='cli_show_ascii')[0]
|
||||
else:
|
||||
config = module.execute([cmd])[0]
|
||||
|
||||
try:
|
||||
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)
|
||||
|
||||
def load_config(module, candidate):
|
||||
config = custom_get_config(module)
|
||||
config = get_config(module)
|
||||
|
||||
commands = candidate.difference(config)
|
||||
commands = [str(c).strip() for c in commands]
|
||||
|
||||
save_config = module.params['save_config']
|
||||
save_config = module.params['save']
|
||||
|
||||
result = dict(changed=False)
|
||||
|
||||
if commands:
|
||||
if not module.check_mode:
|
||||
try:
|
||||
module.configure(commands)
|
||||
except AttributeError:
|
||||
module.config(commands)
|
||||
|
||||
if save_config:
|
||||
try:
|
||||
module.config.save_config()
|
||||
except AttributeError:
|
||||
module.execute(['copy running-config startup-config'])
|
||||
|
||||
result['changed'] = True
|
||||
result['updates'] = commands
|
||||
|
@ -834,6 +308,11 @@ def get_cli_body_ssh_vrf(module, command, response):
|
|||
|
||||
|
||||
def execute_show(cmds, module, command_type=None):
|
||||
command_type_map = {
|
||||
'cli_show': 'json',
|
||||
'cli_show_ascii': 'text'
|
||||
}
|
||||
|
||||
try:
|
||||
if command_type:
|
||||
response = module.execute(cmds, command_type=command_type)
|
||||
|
@ -974,8 +453,11 @@ def main():
|
|||
required=False),
|
||||
state=dict(default='present', choices=['present', 'absent'],
|
||||
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)
|
||||
|
||||
vrf = module.params['vrf']
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue