diff options
author | James Cammarata <jimi@sngx.net> | 2016-12-07 21:33:38 -0500 |
---|---|---|
committer | Matt Clay <matt@mystile.com> | 2016-12-08 11:25:36 -0500 |
commit | 8afa090417bcb6b7007ab6feb1ce3281cdd37001 (patch) | |
tree | 47ac2b7cef9572b90cddb866222e4a969dbebda9 | |
parent | 95d59b61ebaeaeec53a3860c590bb95a38f96e45 (diff) | |
download | ansible-8afa090417bcb6b7007ab6feb1ce3281cdd37001.tar.gz |
Resolving differences in core modules post-merge
115 files changed, 2442 insertions, 17704 deletions
diff --git a/lib/ansible/modules/cloud/amazon/cloudformation.py b/lib/ansible/modules/cloud/amazon/cloudformation.py index a2cbf2c99c..c05ae2df28 100644 --- a/lib/ansible/modules/cloud/amazon/cloudformation.py +++ b/lib/ansible/modules/cloud/amazon/cloudformation.py @@ -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: diff --git a/lib/ansible/modules/cloud/amazon/ec2.py b/lib/ansible/modules/cloud/amazon/ec2.py index 84f2687617..d19f06f369 100755 --- a/lib/ansible/modules/cloud/amazon/ec2.py +++ b/lib/ansible/modules/cloud/amazon/ec2.py @@ -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" diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami.py b/lib/ansible/modules/cloud/amazon/ec2_ami.py index 075fe15734..de3a31c4a7 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_ami.py +++ b/lib/ansible/modules/cloud/amazon/ec2_ami.py @@ -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') diff --git a/lib/ansible/modules/cloud/amazon/ec2_ami_find.py b/lib/ansible/modules/cloud/amazon/ec2_ami_find.py index c7947c9638..c6b986bdd8 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_ami_find.py +++ b/lib/ansible/modules/cloud/amazon/ec2_ami_find.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/ec2_asg.py b/lib/ansible/modules/cloud/amazon/ec2_asg.py index f7ced3b957..e295d0008b 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_asg.py +++ b/lib/ansible/modules/cloud/amazon/ec2_asg.py @@ -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: diff --git a/lib/ansible/modules/cloud/amazon/ec2_eip.py b/lib/ansible/modules/cloud/amazon/ec2_eip.py index 6e6fa5dbfd..22d950f9fb 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_eip.py +++ b/lib/ansible/modules/cloud/amazon/ec2_eip.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/ec2_elb.py b/lib/ansible/modules/cloud/amazon/ec2_elb.py index fee8ff22bb..cd2cf5fbae 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_elb.py +++ b/lib/ansible/modules/cloud/amazon/ec2_elb.py @@ -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 = """ diff --git a/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py b/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py index 8bd02a698e..ca87a1cb3a 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py +++ b/lib/ansible/modules/cloud/amazon/ec2_elb_lb.py @@ -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: diff --git a/lib/ansible/modules/cloud/amazon/ec2_facts.py b/lib/ansible/modules/cloud/amazon/ec2_facts.py index 463cef8cae..498cf9c2df 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_facts.py +++ b/lib/ansible/modules/cloud/amazon/ec2_facts.py @@ -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. diff --git a/lib/ansible/modules/cloud/amazon/ec2_group.py b/lib/ansible/modules/cloud/amazon/ec2_group.py index 5a85bcab25..b381218f49 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_group.py +++ b/lib/ansible/modules/cloud/amazon/ec2_group.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/ec2_key.py b/lib/ansible/modules/cloud/amazon/ec2_key.py index 3ebf563f15..69d96fed97 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_key.py +++ b/lib/ansible/modules/cloud/amazon/ec2_key.py @@ -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)" ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_lc.py b/lib/ansible/modules/cloud/amazon/ec2_lc.py index 54bb209974..7a8754b899 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_lc.py +++ b/lib/ansible/modules/cloud/amazon/ec2_lc.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py b/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py index 017f238571..984211bc27 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py +++ b/lib/ansible/modules/cloud/amazon/ec2_metric_alarm.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py b/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py index 16287387b5..bea3bfbca8 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py +++ b/lib/ansible/modules/cloud/amazon/ec2_scaling_policy.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_snapshot.py b/lib/ansible/modules/cloud/amazon/ec2_snapshot.py index b8911fbb7e..b962e18760 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_snapshot.py +++ b/lib/ansible/modules/cloud/amazon/ec2_snapshot.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_tag.py b/lib/ansible/modules/cloud/amazon/ec2_tag.py index d05f988e2a..0fe20e1786 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_tag.py +++ b/lib/ansible/modules/cloud/amazon/ec2_tag.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_vol.py b/lib/ansible/modules/cloud/amazon/ec2_vol.py index 9e7d5d02ea..cd76703f43 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vol.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vol.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc.py b/lib/ansible/modules/cloud/amazon/ec2_vpc.py index 669aa56fa6..5b0cfc51b0 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc.py @@ -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 * diff --git a/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py b/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py index fe3e99adf4..7b7e0e4647 100644 --- a/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py +++ b/lib/ansible/modules/cloud/amazon/ec2_vpc_net.py @@ -76,7 +76,9 @@ options: default: false required: false -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/lib/ansible/modules/cloud/amazon/elasticache.py b/lib/ansible/modules/cloud/amazon/elasticache.py index 80372643d0..00098b171e 100644 --- a/lib/ansible/modules/cloud/amazon/elasticache.py +++ b/lib/ansible/modules/cloud/amazon/elasticache.py @@ -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 = """ diff --git a/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py b/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py index b2927cf33e..1e5708c03e 100644 --- a/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py +++ b/lib/ansible/modules/cloud/amazon/elasticache_subnet_group.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/iam.py b/lib/ansible/modules/cloud/amazon/iam.py index 4fb56dd160..70b9da0f5e 100644 --- a/lib/ansible/modules/cloud/amazon/iam.py +++ b/lib/ansible/modules/cloud/amazon/iam.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/iam_cert.py b/lib/ansible/modules/cloud/amazon/iam_cert.py index 5f88798efd..6e71b5552f 100644 --- a/lib/ansible/modules/cloud/amazon/iam_cert.py +++ b/lib/ansible/modules/cloud/amazon/iam_cert.py @@ -89,7 +89,9 @@ options: requirements: [ "boto" ] author: Jonathan I. Davila -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/lib/ansible/modules/cloud/amazon/iam_policy.py b/lib/ansible/modules/cloud/amazon/iam_policy.py index 1f601860b6..97be3f4051 100644 --- a/lib/ansible/modules/cloud/amazon/iam_policy.py +++ b/lib/ansible/modules/cloud/amazon/iam_policy.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/rds_param_group.py b/lib/ansible/modules/cloud/amazon/rds_param_group.py index 6a0518a06e..154fed391a 100644 --- a/lib/ansible/modules/cloud/amazon/rds_param_group.py +++ b/lib/ansible/modules/cloud/amazon/rds_param_group.py @@ -65,7 +65,9 @@ options: default: null aliases: [] author: "Scott Anderson (@tastychutney)" -extends_documentation_fragment: aws +extends_documentation_fragment: + - aws + - ec2 ''' EXAMPLES = ''' diff --git a/lib/ansible/modules/cloud/amazon/rds_subnet_group.py b/lib/ansible/modules/cloud/amazon/rds_subnet_group.py index 5c08a9bb39..bec08cf61d 100644 --- a/lib/ansible/modules/cloud/amazon/rds_subnet_group.py +++ b/lib/ansible/modules/cloud/amazon/rds_subnet_group.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/amazon/route53.py b/lib/ansible/modules/cloud/amazon/route53.py index cf16c1a4fe..dfcd325fd8 100644 --- a/lib/ansible/modules/cloud/amazon/route53.py +++ b/lib/ansible/modules/cloud/amazon/route53.py @@ -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 diff --git a/lib/ansible/modules/cloud/amazon/s3.py b/lib/ansible/modules/cloud/amazon/s3.py index 045f40dd01..9974a4f467 100755 --- a/lib/ansible/modules/cloud/amazon/s3.py +++ b/lib/ansible/modules/cloud/amazon/s3.py @@ -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 ... diff --git a/lib/ansible/modules/cloud/azure/azure.py b/lib/ansible/modules/cloud/azure/azure.py index 6892e54b7a..60cdbbe047 100644 --- a/lib/ansible/modules/cloud/azure/azure.py +++ b/lib/ansible/modules/cloud/azure/azure.py @@ -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), ) diff --git a/lib/ansible/modules/cloud/azure/azure_rm_publicipaddress.py b/lib/ansible/modules/cloud/azure/azure_rm_publicipaddress.py index 70ee6c5e88..7aa05d4ed5 100644 --- a/lib/ansible/modules/cloud/azure/azure_rm_publicipaddress.py +++ b/lib/ansible/modules/cloud/azure/azure_rm_publicipaddress.py @@ -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 diff --git a/lib/ansible/modules/cloud/docker/_docker.py b/lib/ansible/modules/cloud/docker/_docker.py index f1bfff1d73..08adf3b907 100644 --- a/lib/ansible/modules/cloud/docker/_docker.py +++ b/lib/ansible/modules/cloud/docker/_docker.py @@ -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() diff --git a/lib/ansible/modules/cloud/google/gc_storage.py b/lib/ansible/modules/cloud/google/gc_storage.py index 4d4004c0f8..001de7c700 100644 --- a/lib/ansible/modules/cloud/google/gc_storage.py +++ b/lib/ansible/modules/cloud/google/gc_storage.py @@ -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: diff --git a/lib/ansible/modules/cloud/google/gce.py b/lib/ansible/modules/cloud/google/gce.py index a6dba47ecb..802a7a1393 100644 --- a/lib/ansible/modules/cloud/google/gce.py +++ b/lib/ansible/modules/cloud/google/gce.py @@ -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: diff --git a/lib/ansible/modules/cloud/google/gce_net.py b/lib/ansible/modules/cloud/google/gce_net.py index b33a81113c..aec0a29427 100644 --- a/lib/ansible/modules/cloud/google/gce_net.py +++ b/lib/ansible/modules/cloud/google/gce_net.py @@ -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. diff --git a/lib/ansible/modules/cloud/openstack/_glance_image.py b/lib/ansible/modules/cloud/openstack/_glance_image.py index a589e7d0bc..a97255241a 100644 --- a/lib/ansible/modules/cloud/openstack/_glance_image.py +++ b/lib/ansible/modules/cloud/openstack/_glance_image.py @@ -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 + HAS_GLANCECLIENT = True +except ImportError: + HAS_GLANCECLIENT = False +try: from keystoneclient.v2_0 import client as ksclient + HAS_KEYSTONECLIENT = True except ImportError: - print("failed=True msg='glanceclient and keystone client are required'") + 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() diff --git a/lib/ansible/modules/cloud/openstack/_nova_compute.py b/lib/ansible/modules/cloud/openstack/_nova_compute.py index e14ca3407d..0bea21048f 100644 --- a/lib/ansible/modules/cloud/openstack/_nova_compute.py +++ b/lib/ansible/modules/cloud/openstack/_nova_compute.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_nova_keypair.py b/lib/ansible/modules/cloud/openstack/_nova_keypair.py index 045235b099..914db91bf2 100644 --- a/lib/ansible/modules/cloud/openstack/_nova_keypair.py +++ b/lib/ansible/modules/cloud/openstack/_nova_keypair.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_floating_ip.py b/lib/ansible/modules/cloud/openstack/_quantum_floating_ip.py index 362e56487e..9c72c431d0 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_floating_ip.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_floating_ip.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_floating_ip_associate.py b/lib/ansible/modules/cloud/openstack/_quantum_floating_ip_associate.py index ef86e4b594..f7eed5fe86 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_floating_ip_associate.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_floating_ip_associate.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_network.py b/lib/ansible/modules/cloud/openstack/_quantum_network.py index d31fcd961d..db82e90d33 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_network.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_network.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_router.py b/lib/ansible/modules/cloud/openstack/_quantum_router.py index 37b812a5e5..c65f916d6b 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_router.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_router.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_router_gateway.py b/lib/ansible/modules/cloud/openstack/_quantum_router_gateway.py index c67b90f62c..af6179bc62 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_router_gateway.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_router_gateway.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/_quantum_router_interface.py b/lib/ansible/modules/cloud/openstack/_quantum_router_interface.py index 6405ac6182..b2a1784d99 100644 --- a/lib/ansible/modules/cloud/openstack/_quantum_router_interface.py +++ b/lib/ansible/modules/cloud/openstack/_quantum_router_interface.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/os_auth.py b/lib/ansible/modules/cloud/openstack/os_auth.py index a780ae1c5c..c46945a3d6 100644 --- a/lib/ansible/modules/cloud/openstack/os_auth.py +++ b/lib/ansible/modules/cloud/openstack/os_auth.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/os_floating_ip.py b/lib/ansible/modules/cloud/openstack/os_floating_ip.py index 04db985f06..16c217c2d8 100644 --- a/lib/ansible/modules/cloud/openstack/os_floating_ip.py +++ b/lib/ansible/modules/cloud/openstack/os_floating_ip.py @@ -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"] ''' diff --git a/lib/ansible/modules/cloud/openstack/os_ironic.py b/lib/ansible/modules/cloud/openstack/os_ironic.py index 0954868a7b..2658203646 100644 --- a/lib/ansible/modules/cloud/openstack/os_ironic.py +++ b/lib/ansible/modules/cloud/openstack/os_ironic.py @@ -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: diff --git a/lib/ansible/modules/cloud/openstack/os_ironic_node.py b/lib/ansible/modules/cloud/openstack/os_ironic_node.py index 34e1c98a2b..fa41d6fcbc 100644 --- a/lib/ansible/modules/cloud/openstack/os_ironic_node.py +++ b/lib/ansible/modules/cloud/openstack/os_ironic_node.py @@ -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. diff --git a/lib/ansible/modules/cloud/openstack/os_object.py b/lib/ansible/modules/cloud/openstack/os_object.py index d1a9b9d9da..9e67ab39df 100644 --- a/lib/ansible/modules/cloud/openstack/os_object.py +++ b/lib/ansible/modules/cloud/openstack/os_object.py @@ -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 = ''' diff --git a/lib/ansible/modules/cloud/openstack/os_security_group.py b/lib/ansible/modules/cloud/openstack/os_security_group.py index 3fb3d05f16..3ed5dfceb7 100644 --- a/lib/ansible/modules/cloud/openstack/os_security_group.py +++ b/lib/ansible/modules/cloud/openstack/os_security_group.py @@ -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. diff --git a/lib/ansible/modules/cloud/openstack/os_security_group_rule.py b/lib/ansible/modules/cloud/openstack/os_security_group_rule.py index 86258255b4..3379d16040 100644 --- a/lib/ansible/modules/cloud/openstack/os_security_group_rule.py +++ b/lib/ansible/modules/cloud/openstack/os_security_group_rule.py @@ -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 diff --git a/lib/ansible/modules/cloud/openstack/os_server.py b/lib/ansible/modules/cloud/openstack/os_server.py index c732189e18..0bb7dbcfbc 100644 --- a/lib/ansible/modules/cloud/openstack/os_server.py +++ b/lib/ansible/modules/cloud/openstack/os_server.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/os_server_volume.py b/lib/ansible/modules/cloud/openstack/os_server_volume.py index 4fb4dde2ce..a6549649d8 100644 --- a/lib/ansible/modules/cloud/openstack/os_server_volume.py +++ b/lib/ansible/modules/cloud/openstack/os_server_volume.py @@ -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() diff --git a/lib/ansible/modules/cloud/openstack/os_volume.py b/lib/ansible/modules/cloud/openstack/os_volume.py index 024a8d9826..6d6cc08d74 100644 --- a/lib/ansible/modules/cloud/openstack/os_volume.py +++ b/lib/ansible/modules/cloud/openstack/os_volume.py @@ -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() diff --git a/lib/ansible/modules/cloud/rackspace/rax_scaling_group.py b/lib/ansible/modules/cloud/rackspace/rax_scaling_group.py index d1f3eeaca0..95aef91cc5 100644 --- a/lib/ansible/modules/cloud/rackspace/rax_scaling_group.py +++ b/lib/ansible/modules/cloud/rackspace/rax_scaling_group.py @@ -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 ''' diff --git a/lib/ansible/modules/cloud/vmware/vsphere_guest.py b/lib/ansible/modules/cloud/vmware/vsphere_guest.py index e54465e891..5425db6f89 100644 --- a/lib/ansible/modules/cloud/vmware/vsphere_guest.py +++ b/lib/ansible/modules/cloud/vmware/vsphere_guest.py @@ -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 diff --git a/lib/ansible/modules/commands/command.py b/lib/ansible/modules/commands/command.py index 2c04a3b21e..9b8afe3ef5 100644 --- a/lib/ansible/modules/commands/command.py +++ b/lib/ansible/modules/commands/command.py @@ -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. diff --git a/lib/ansible/modules/commands/raw.py b/lib/ansible/modules/commands/raw.py index 41d677df68..e76cf42bc7 100644 --- a/lib/ansible/modules/commands/raw.py +++ b/lib/ansible/modules/commands/raw.py @@ -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: diff --git a/lib/ansible/modules/database/mysql/mysql_user.py b/lib/ansible/modules/database/mysql/mysql_user.py index d3c3c5f3f1..286106fe71 100644 --- a/lib/ansible/modules/database/mysql/mysql_user.py +++ b/lib/ansible/modules/database/mysql/mysql_user.py @@ -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) diff --git a/lib/ansible/modules/database/postgresql/postgresql_user.py b/lib/ansible/modules/database/postgresql/postgresql_user.py index dfe7a790f3..95c19caaba 100644 --- a/lib/ansible/modules/database/postgresql/postgresql_user.py +++ b/lib/ansible/modules/database/postgresql/postgresql_user.py @@ -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_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 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] + :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_privilege(cursor, user, db, priv): +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 diff --git a/lib/ansible/modules/files/assemble.py b/lib/ansible/modules/files/assemble.py index 2895b00700..41e7530e44 100644 --- a/lib/ansible/modules/files/assemble.py +++ b/lib/ansible/modules/files/assemble.py @@ -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 = ''' diff --git a/lib/ansible/modules/files/ini_file.py b/lib/ansible/modules/files/ini_file.py index e04b79e9bf..f9f08ac569 100644 --- a/lib/ansible/modules/files/ini_file.py +++ b/lib/ansible/modules/files/ini_file.py @@ -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: diff --git a/lib/ansible/modules/files/lineinfile.py b/lib/ansible/modules/files/lineinfile.py index 28c3d8e90a..0e55bc9da5 100644 --- a/lib/ansible/modules/files/lineinfile.py +++ b/lib/ansible/modules/files/lineinfile.py @@ -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: diff --git a/lib/ansible/modules/files/replace.py b/lib/ansible/modules/files/replace.py index 7e8d5e45d1..85d6d91e1f 100644 --- a/lib/ansible/modules/files/replace.py +++ b/lib/ansible/modules/files/replace.py @@ -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: diff --git a/lib/ansible/modules/files/template.py b/lib/ansible/modules/files/template.py index 8152e0cb3a..4727eff774 100644 --- a/lib/ansible/modules/files/template.py +++ b/lib/ansible/modules/files/template.py @@ -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 = ''' diff --git a/lib/ansible/modules/network/basics/get_url.py b/lib/ansible/modules/network/basics/get_url.py index 0d9d81fd36..a15b78df4f 100644 --- a/lib/ansible/modules/network/basics/get_url.py +++ b/lib/ansible/modules/network/basics/get_url.py @@ -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 diff --git a/lib/ansible/modules/network/basics/uri.py b/lib/ansible/modules/network/basics/uri.py index 889cc4f151..24257dc356 100644 --- a/lib/ansible/modules/network/basics/uri.py +++ b/lib/ansible/modules/network/basics/uri.py @@ -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: diff --git a/lib/ansible/modules/network/junos/junos_config.py b/lib/ansible/modules/network/junos/junos_config.py index b0c0252c7b..a1212e974c 100644 --- a/lib/ansible/modules/network/junos/junos_config.py +++ b/lib/ansible/modules/network/junos/junos_config.py @@ -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: diff --git a/lib/ansible/modules/network/nxos/nxos_acl.py b/lib/ansible/modules/network/nxos/nxos_acl.py index 7683d1aada..ce1a19e018 100644 --- a/lib/ansible/modules/network/nxos/nxos_acl.py +++ b/lib/ansible/modules/network/nxos/nxos_acl.py @@ -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,206 +242,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 - - -class CustomNetworkConfig(object): + return list() - 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) +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -452,119 +304,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 @@ -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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_acl_interface.py b/lib/ansible/modules/network/nxos/nxos_acl_interface.py index 837ea61317..739f55c044 100644 --- a/lib/ansible/modules/network/nxos/nxos_acl_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_acl_interface.py @@ -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,200 +100,56 @@ changed: sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time import collections -import itertools -import shlex -import itertools - -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE - -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) +import json -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 - - -class CustomNetworkConfig(object): + return list() - 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) +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -311,119 +162,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 @@ -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() diff --git a/lib/ansible/modules/network/nxos/nxos_bgp.py b/lib/ansible/modules/network/nxos/nxos_bgp.py index ce018fbc40..785e112253 100644 --- a/lib/ansible/modules/network/nxos/nxos_bgp.py +++ b/lib/ansible/modules/network/nxos/nxos_bgp.py @@ -24,17 +24,19 @@ DOCUMENTATION = ''' --- module: nxos_bgp version_added: "2.2" -short_description: Manages BGP configuration +short_description: Manages BGP configuration. description: - - Manages BGP configurations on NX-OS switches -author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) + - Manages BGP configurations on NX-OS switches. +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - State 'absent' removes the whole BGP ASN configuration when VRF is - 'default' or the whole VRF instance within the BGP process when using - a different VRF. - - 'default', when supported, restores params default value. - - Configuring global parmas is only permitted if VRF is 'default'. + - C(state=absent) removes the whole BGP ASN configuration when + C(vrf=default) or the whole VRF instance within the BGP process when + using a different VRF. + - Default when supported restores params default value. + - Configuring global parmas is only permitted if C(vrf=default). options: asn: description: @@ -43,55 +45,58 @@ options: required: true vrf: description: - - Name of the VRF. The name 'default' is a valid VRF representing the global BGP. + - Name of the VRF. The name 'default' is a valid VRF representing + the global BGP. required: false default: null bestpath_always_compare_med: description: - - Enable/Disable MED comparison on paths from different autonomous systems. + - Enable/Disable MED comparison on paths from different + autonomous systems. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_aspath_multipath_relax: description: - Enable/Disable load sharing across the providers with different (but equal-length) AS paths. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_compare_routerid: description: - Enable/Disable comparison of router IDs for identical eBGP paths. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_cost_community_ignore: description: - Enable/Disable Ignores the cost community for BGP best-path calculations. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_confed: description: - Enable/Disable enforcement of bestpath to do a MED comparison only between paths originated within a confederation. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_missing_as_worst: description: - - Enable/Disable assigns the value of infinity to received routes that - do not carry the MED attribute, making these routes the least desirable. + - Enable/Disable assigns the value of infinity to received + routes that do not carry the MED attribute, making these routes + the least desirable. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_non_deterministic: description: - - Enable/Disable deterministic selection of the best MED path from among - the paths from the same autonomous system. + - Enable/Disable deterministic selection of the best MED pat + from among the paths from the same autonomous system. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null cluster_id: description: @@ -110,27 +115,30 @@ options: default: null disable_policy_batching: description: - - Enable/Disable the batching evaluation of prefix advertisements to all peers. + - Enable/Disable the batching evaluation of prefix advertisement + to all peers. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null disable_policy_batching_ipv4_prefix_list: description: - - Enable/Disable the batching evaluation of prefix advertisements to all - peers with prefix list. + - Enable/Disable the batching evaluation of prefix advertisements + to all peers with prefix list. required: false default: null disable_policy_batching_ipv6_prefix_list: description: - - Enable/Disable the batching evaluation of prefix advertisements to all peers with prefix list. + - Enable/Disable the batching evaluation of prefix advertisements + to all peers with prefix list. required: false enforce_first_as: description: - - Enable/Disable enforces the neighbor autonomous system to be the first AS number - listed in the AS path attribute for eBGP. On NX-OS, this property is only supported - in the global BGP context. + - Enable/Disable enforces the neighbor autonomous system to be + the first AS number listed in the AS path attribute for eBGP. + On NX-OS, this property is only supported in the + global BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null event_history_cli: description: @@ -158,45 +166,48 @@ options: fast_external_fallover: description: - Enable/Disable immediately reset the session if the link to a - directly connected BGP peer goes down. Only supported in the global BGP context. + directly connected BGP peer goes down. Only supported in the + global BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null flush_routes: description: - Enable/Disable flush routes in RIB upon controlled restart. - On NX-OS, this property is only supported in the global BGP context. + On NX-OS, this property is only supported in the global + BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart: description: - Enable/Disable graceful restart. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_helper: description: - Enable/Disable graceful restart helper mode. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_timers_restart: description: - Set maximum time for a restart sent to the BGP peer. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_timers_stalepath_time: description: - - Set maximum time that BGP keeps the stale routes from the restarting BGP peer. - choices: ['true','false', 'default'] + - Set maximum time that BGP keeps the stale routes from the + restarting BGP peer. + choices: ['true','false'] default: null isolate: description: - Enable/Disable isolate this router from BGP perspective. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null local_as: description: @@ -207,23 +218,25 @@ options: description: - Enable/Disable message logging for neighbor up/down event. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null maxas_limit: description: - - Specify Maximum number of AS numbers allowed in the AS-path attribute - Valid values are between 1 and 512. + - Specify Maximum number of AS numbers allowed in the AS-path + attribute. Valid values are between 1 and 512. required: false default: null neighbor_down_fib_accelerate: description: - - Enable/Disable handle BGP neighbor down event, due to various reasons. + - Enable/Disable handle BGP neighbor down event, due to + various reasons. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null reconnect_interval: description: - - The BGP reconnection interval for dropped sessions. 1 - 60. + - The BGP reconnection interval for dropped sessions. + Valid values are between 1 and 60. required: false default: null router_id: @@ -235,47 +248,44 @@ options: description: - Administratively shutdown the BGP protocol. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null suppress_fib_pending: description: - - Enable/Disable advertise only routes programmed in hardware to peers. + - Enable/Disable advertise only routes programmed in hardware + to peers. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null timer_bestpath_limit: description: - - Specify timeout for the first best path after a restart, in seconds. + - Specify timeout for the first best path after a restart, + in seconds. required: false default: null timer_bestpath_limit_always: description: - Enable/Disable update-delay-always option. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null timer_bgp_hold: description: - - Set bgp hold timer + - Set BGP hold timer. required: false default: null timer_bgp_keepalive: description: - - Set bgp keepalive timer. + - Set BGP keepalive timer. 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'] ''' @@ -294,11 +304,12 @@ EXAMPLES = ''' RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"} existing: description: k/v pairs of existing BGP configuration + returned: verbose mode type: dict sample: {"asn": "65535", "bestpath_always_compare_med": false, "bestpath_aspath_multipath_relax": false, @@ -319,7 +330,7 @@ existing: "timer_bgp_keepalive": "60", "vrf": "test"} end_state: description: k/v pairs of BGP configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "bestpath_always_compare_med": false, "bestpath_aspath_multipath_relax": false, @@ -351,12 +362,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 @@ -364,189 +370,41 @@ 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() + return list() - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - config.append(cfg) +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -559,116 +417,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: - string = item.text if ignore_whitespace is True else 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 @@ -719,303 +467,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) - - 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() + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 @@ -1204,7 +693,7 @@ def get_value(arg, config): 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+).*' @@ -1219,7 +708,7 @@ def get_existing(module, args): if module.params['vrf'] != 'default': parents = [bgp_parent, 'vrf {0}'.format(module.params['vrf'])] else: - parents = bgp_parent + parents = [bgp_parent] config = netcfg.get_section(parents) if config: @@ -1407,13 +896,13 @@ def main(): timer_bestpath_limit=dict(required=False, type='str'), timer_bgp_hold=dict(required=False, type='str'), timer_bgp_keepalive=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=[['timer_bgp_hold', 'timer_bgp_keepalive']], supports_check_mode=True) @@ -1507,7 +996,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 diff --git a/lib/ansible/modules/network/nxos/nxos_bgp_af.py b/lib/ansible/modules/network/nxos/nxos_bgp_af.py index a24f8ee7e7..0181591bd5 100644 --- a/lib/ansible/modules/network/nxos/nxos_bgp_af.py +++ b/lib/ansible/modules/network/nxos/nxos_bgp_af.py @@ -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,189 +310,41 @@ 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 + return list() - cfg.parents = ancestors[:level] - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -514,119 +357,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 @@ -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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_bgp_neighbor.py b/lib/ansible/modules/network/nxos/nxos_bgp_neighbor.py index 650258d92e..6d117b7c50 100644 --- a/lib/ansible/modules/network/nxos/nxos_bgp_neighbor.py +++ b/lib/ansible/modules/network/nxos/nxos_bgp_neighbor.py @@ -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,189 +254,41 @@ 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 + return list() - cfg.parents = ancestors[:level] - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -458,119 +301,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 @@ -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) - - 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) - - 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()) +def get_network_module(**kwargs): try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_bgp_neighbor_af.py b/lib/ansible/modules/network/nxos/nxos_bgp_neighbor_af.py index 5cd7413375..36db7ec8e2 100644 --- a/lib/ansible/modules/network/nxos/nxos_bgp_neighbor_af.py +++ b/lib/ansible/modules/network/nxos/nxos_bgp_neighbor_af.py @@ -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,189 +331,41 @@ 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 + return list() - cfg.parents = ancestors[:level] - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -535,119 +378,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 @@ -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) - - 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) - - 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()) +def get_network_module(**kwargs): try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_evpn_global.py b/lib/ansible/modules/network/nxos/nxos_evpn_global.py index 4a91165e9a..e2389ab526 100644 --- a/lib/ansible/modules/network/nxos/nxos_evpn_global.py +++ b/lib/ansible/modules/network/nxos/nxos_evpn_global.py @@ -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,202 +74,49 @@ 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -288,119 +129,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 @@ -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() diff --git a/lib/ansible/modules/network/nxos/nxos_evpn_vni.py b/lib/ansible/modules/network/nxos/nxos_evpn_vni.py index eaa4e628ec..15d620a6d1 100644 --- a/lib/ansible/modules/network/nxos/nxos_evpn_vni.py +++ b/lib/ansible/modules/network/nxos/nxos_evpn_vni.py @@ -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,189 +136,41 @@ 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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -340,119 +183,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_feature.py b/lib/ansible/modules/network/nxos/nxos_feature.py index f70d90b8eb..f959035172 100644 --- a/lib/ansible/modules/network/nxos/nxos_feature.py +++ b/lib/ansible/modules/network/nxos/nxos_feature.py @@ -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,206 +98,56 @@ 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 - - -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() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -315,119 +160,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 @@ -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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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) diff --git a/lib/ansible/modules/network/nxos/nxos_file_copy.py b/lib/ansible/modules/network/nxos/nxos_file_copy.py index e4f9734c2e..08601b0fb4 100644 --- a/lib/ansible/modules/network/nxos/nxos_file_copy.py +++ b/lib/ansible/modules/network/nxos/nxos_file_copy.py @@ -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,203 +90,52 @@ 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 - - -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) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -296,119 +148,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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) + return get_module(**kwargs) + except NameError: + return NetworkModule(**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'] diff --git a/lib/ansible/modules/network/nxos/nxos_hsrp.py b/lib/ansible/modules/network/nxos/nxos_hsrp.py index e885a4605c..36d6f6a2f3 100644 --- a/lib/ansible/modules/network/nxos/nxos_hsrp.py +++ b/lib/ansible/modules/network/nxos/nxos_hsrp.py @@ -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,187 +153,41 @@ 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) + return list() - config.append(cfg) - return config +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 -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]) 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() @@ -347,119 +200,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 @@ -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) - - 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) - - 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()) +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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() diff --git a/lib/ansible/modules/network/nxos/nxos_igmp.py b/lib/ansible/modules/network/nxos/nxos_igmp.py index cc15fbca55..216922f226 100644 --- a/lib/ansible/modules/network/nxos/nxos_igmp.py +++ b/lib/ansible/modules/network/nxos/nxos_igmp.py @@ -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,204 +111,52 @@ 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 - - -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 +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -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) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -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 + return list() - for i in range(level, len(ancestors)): - ancestors.pop() - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -320,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 @@ -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) - - 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_igmp_interface.py b/lib/ansible/modules/network/nxos/nxos_igmp_interface.py index 586a6fed53..7294455a48 100644 --- a/lib/ansible/modules/network/nxos/nxos_igmp_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_igmp_interface.py @@ -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,206 +233,56 @@ 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 - - -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) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - 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) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -448,119 +295,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_interface_ospf.py b/lib/ansible/modules/network/nxos/nxos_interface_ospf.py index c04b2eeafb..c4a2ee6be4 100644 --- a/lib/ansible/modules/network/nxos/nxos_interface_ospf.py +++ b/lib/ansible/modules/network/nxos/nxos_interface_ospf.py @@ -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,187 +177,40 @@ 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 + return list() - cfg.parents = ancestors[:level] - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -380,119 +223,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 @@ -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) - - 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) - - 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()) +def get_network_module(**kwargs): try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_ip_interface.py b/lib/ansible/modules/network/nxos/nxos_ip_interface.py index 29b6326eb4..09fd478044 100644 --- a/lib/ansible/modules/network/nxos/nxos_ip_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_ip_interface.py @@ -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,205 +111,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -327,119 +173,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_ntp_options.py b/lib/ansible/modules/network/nxos/nxos_ntp_options.py index 9d4f02d203..010e67c886 100644 --- a/lib/ansible/modules/network/nxos/nxos_ntp_options.py +++ b/lib/ansible/modules/network/nxos/nxos_ntp_options.py @@ -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. diff --git a/lib/ansible/modules/network/nxos/nxos_ospf.py b/lib/ansible/modules/network/nxos/nxos_ospf.py index 0aa1620b5a..d6f47bbc35 100644 --- a/lib/ansible/modules/network/nxos/nxos_ospf.py +++ b/lib/ansible/modules/network/nxos/nxos_ospf.py @@ -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,202 +82,49 @@ 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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -295,119 +137,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 @@ -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() diff --git a/lib/ansible/modules/network/nxos/nxos_ospf_vrf.py b/lib/ansible/modules/network/nxos/nxos_ospf_vrf.py index 0071504b89..3d049edcad 100644 --- a/lib/ansible/modules/network/nxos/nxos_ospf_vrf.py +++ b/lib/ansible/modules/network/nxos/nxos_ospf_vrf.py @@ -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,189 +181,41 @@ 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] + return list() - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config +class CustomNetworkConfig(NetworkConfig): -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 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 __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]) 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() @@ -388,119 +228,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 @@ -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) - - 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) + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_overlay_global.py b/lib/ansible/modules/network/nxos/nxos_overlay_global.py index 3864eb0a93..dda0d48413 100644 --- a/lib/ansible/modules/network/nxos/nxos_overlay_global.py +++ b/lib/ansible/modules/network/nxos/nxos_overlay_global.py @@ -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,203 +110,52 @@ 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 - - -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 +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -291,119 +168,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_pim.py b/lib/ansible/modules/network/nxos/nxos_pim.py index 2ee67ae954..7fc4080def 100644 --- a/lib/ansible/modules/network/nxos/nxos_pim.py +++ b/lib/ansible/modules/network/nxos/nxos_pim.py @@ -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,203 +74,52 @@ 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 - - -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 +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - 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) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -288,119 +132,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_pim_rp_address.py b/lib/ansible/modules/network/nxos/nxos_pim_rp_address.py index 2b1fc484d2..5149d4d3fb 100644 --- a/lib/ansible/modules/network/nxos/nxos_pim_rp_address.py +++ b/lib/ansible/modules/network/nxos/nxos_pim_rp_address.py @@ -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,203 +104,52 @@ 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 - - -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() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - 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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -319,119 +162,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_ping.py b/lib/ansible/modules/network/nxos/nxos_ping.py index b29c30910d..1bb7b044d7 100644 --- a/lib/ansible/modules/network/nxos/nxos_ping.py +++ b/lib/ansible/modules/network/nxos/nxos_ping.py @@ -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,205 +116,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 - - -class CustomNetworkConfig(object): + return list() - 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) +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -325,119 +178,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 @@ -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) - - 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) - - def send(self, commands, encoding='text'): +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_reboot.py b/lib/ansible/modules/network/nxos/nxos_reboot.py index 78d5810074..ce31a9b8f9 100644 --- a/lib/ansible/modules/network/nxos/nxos_reboot.py +++ b/lib/ansible/modules/network/nxos/nxos_reboot.py @@ -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,204 +58,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -268,119 +120,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 @@ -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) - - 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_rollback.py b/lib/ansible/modules/network/nxos/nxos_rollback.py index 731e096146..bff47e5512 100644 --- a/lib/ansible/modules/network/nxos/nxos_rollback.py +++ b/lib/ansible/modules/network/nxos/nxos_rollback.py @@ -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,203 +78,52 @@ 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 - - -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 +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -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) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -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 + return list() - for i in range(level, len(ancestors)): - ancestors.pop() - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -282,119 +136,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 @@ -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) - - 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 get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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: + response = module.execute(cmds, command_type=command_type) + else: + 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: - module.execute(cmds, command_type=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.execute(cmds) + 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) diff --git a/lib/ansible/modules/network/nxos/nxos_smu.py b/lib/ansible/modules/network/nxos/nxos_smu.py index 4698bf6452..d487aaf64b 100644 --- a/lib/ansible/modules/network/nxos/nxos_smu.py +++ b/lib/ansible/modules/network/nxos/nxos_smu.py @@ -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,204 +81,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -291,119 +143,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 @@ -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) - - 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) - - 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()) +def get_network_module(**kwargs): 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_snmp_host.py b/lib/ansible/modules/network/nxos/nxos_snmp_host.py index 99ef9944f1..366da22a91 100644 --- a/lib/ansible/modules/network/nxos/nxos_snmp_host.py +++ b/lib/ansible/modules/network/nxos/nxos_snmp_host.py @@ -1,18 +1,21 @@ -#!/usr/bin/env python - -# Copyright 2015 Jason Edelman <jedelman8@gmail.com> +#!/usr/bin/python +# +# This file is part of Ansible # -# 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 +# 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. # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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. # -# 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. +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. +# + 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: diff --git a/lib/ansible/modules/network/nxos/nxos_snmp_location.py b/lib/ansible/modules/network/nxos/nxos_snmp_location.py index e5939bbfb8..e3b90973e7 100644 --- a/lib/ansible/modules/network/nxos/nxos_snmp_location.py +++ b/lib/ansible/modules/network/nxos/nxos_snmp_location.py @@ -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) diff --git a/lib/ansible/modules/network/nxos/nxos_static_route.py b/lib/ansible/modules/network/nxos/nxos_static_route.py index 361347c392..5433c0ef5e 100644 --- a/lib/ansible/modules/network/nxos/nxos_static_route.py +++ b/lib/ansible/modules/network/nxos/nxos_static_route.py @@ -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 - - 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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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) - - 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) - - 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()) +def get_network_module(**kwargs): 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 + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 diff --git a/lib/ansible/modules/network/nxos/nxos_switchport.py b/lib/ansible/modules/network/nxos/nxos_switchport.py index 2034a8f1ff..f0e06163ce 100644 --- a/lib/ansible/modules/network/nxos/nxos_switchport.py +++ b/lib/ansible/modules/network/nxos/nxos_switchport.py @@ -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 diff --git a/lib/ansible/modules/network/nxos/nxos_vlan.py b/lib/ansible/modules/network/nxos/nxos_vlan.py index 81fdfe9140..b81bc98295 100644 --- a/lib/ansible/modules/network/nxos/nxos_vlan.py +++ b/lib/ansible/modules/network/nxos/nxos_vlan.py @@ -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,205 +154,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -370,119 +216,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 @@ -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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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) diff --git a/lib/ansible/modules/network/nxos/nxos_vpc.py b/lib/ansible/modules/network/nxos/nxos_vpc.py index 30b9969f6d..849018cf92 100644 --- a/lib/ansible/modules/network/nxos/nxos_vpc.py +++ b/lib/ansible/modules/network/nxos/nxos_vpc.py @@ -149,204 +149,52 @@ 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 - - -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 +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __ne__(self, other): - return not self.__eq__(other) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -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) - continue - for i in range(level, len(ancestors)): - ancestors.pop() +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -359,119 +207,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 @@ -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) - - 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) + return get_module(**kwargs) + except NameError: + return 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_vpc_interface.py b/lib/ansible/modules/network/nxos/nxos_vpc_interface.py index 2ead2ca3f3..de5a660cf0 100644 --- a/lib/ansible/modules/network/nxos/nxos_vpc_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_vpc_interface.py @@ -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,205 +96,57 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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] + return list() - if level > len(ancestors): - config.append(cfg) - continue - - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - @property - def items(self): - return self._config +class CustomNetworkConfig(NetworkConfig): - @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 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 __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]) 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() @@ -307,119 +159,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 @@ -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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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) diff --git a/lib/ansible/modules/network/nxos/nxos_vrf.py b/lib/ansible/modules/network/nxos/nxos_vrf.py index 5f88f1b8d1..217007b54f 100644 --- a/lib/ansible/modules/network/nxos/nxos_vrf.py +++ b/lib/ansible/modules/network/nxos/nxos_vrf.py @@ -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,189 +129,41 @@ 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): +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -330,119 +176,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 @@ -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) - - 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) + return get_module(**kwargs) + except NameError: + return 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_vrf_af.py b/lib/ansible/modules/network/nxos/nxos_vrf_af.py index 754fc962d3..f0b008476f 100644 --- a/lib/ansible/modules/network/nxos/nxos_vrf_af.py +++ b/lib/ansible/modules/network/nxos/nxos_vrf_af.py @@ -22,16 +22,15 @@ ANSIBLE_METADATA = {'status': ['preview'], DOCUMENTATION = ''' --- -module: nxos_vxlan_vtep_vni +module: nxos_vrf_af version_added: "2.2" -short_description: Creates a Virtual Network Identifier member (VNI) +short_description: Manages VRF AF. description: - - Creates a Virtual Network Identifier member (VNI) for an NVE - overlay interface. + - Manages VRF AF author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - Default, where supported, restores params default value. options: vrf: description: @@ -58,16 +57,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_vrf_af: @@ -78,22 +72,22 @@ EXAMPLES = ''' password: "{{ pwd }}" host: "{{ inventory_hostname }}" ''' - RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, "safi": "unicast", "vrf": "test"} existing: description: k/v pairs of existing configuration + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": false, "safi": "unicast", "vrf": "test"} end_state: description: k/v pairs of configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, "safi": "unicast", "vrf": "test"} @@ -111,203 +105,52 @@ 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 - - -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() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -320,119 +163,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 @@ -483,303 +213,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) - - 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') + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - 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 @@ -819,7 +290,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['vrf context {0}'.format(module.params['vrf'])] parents.append('address-family {0} {1}'.format(module.params['afi'], @@ -903,10 +374,11 @@ def main(): 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, supports_check_mode=True) state = module.params['state'] @@ -948,7 +420,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 diff --git a/lib/ansible/modules/network/nxos/nxos_vrf_interface.py b/lib/ansible/modules/network/nxos/nxos_vrf_interface.py index 11a90a8091..ca04148496 100644 --- a/lib/ansible/modules/network/nxos/nxos_vrf_interface.py +++ b/lib/ansible/modules/network/nxos/nxos_vrf_interface.py @@ -24,30 +24,32 @@ DOCUMENTATION = ''' --- module: nxos_vrf_interface version_added: "2.1" -short_description: Manages interface specific VRF configuration +short_description: Manages interface specific VRF configuration. description: - - Manages interface specific VRF configuration + - Manages interface specific VRF configuration. extends_documentation_fragment: nxos -author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) notes: - VRF needs to be added globally with M(nxos_vrf) before - adding a VRF to an interface + adding a VRF to an interface. - Remove a VRF from an interface will still remove - all L3 attributes just as it does from CLI + all L3 attributes just as it does from CLI. - VRF is not read from an interface until IP address is - configured on that interface + configured on that interface. options: vrf: description: - - Name of VRF to be managed + - Name of VRF to be managed. required: true interface: description: - - Full name of interface to be managed, i.e. Ethernet1/1 + - Full name of interface to be managed, i.e. Ethernet1/1. required: true state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: false default: present choices: ['present','absent'] @@ -96,205 +98,56 @@ 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 - - -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() +# COMMON CODE FOR MIGRATION +import re - cfg = ConfigLine(text) - cfg.raw = line +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - if not text or ignore_line(text, comment_tokens): - continue +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - # 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 +class CustomNetworkConfig(NetworkConfig): - 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 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 __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]) 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() @@ -307,119 +160,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 @@ -470,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) - - 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) + return get_module(**kwargs) + except NameError: + return 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 @@ -810,6 +291,11 @@ def get_cli_body_ssh_vrf_interface(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) @@ -817,7 +303,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: @@ -936,8 +422,11 @@ def main(): interface=dict(type='str', required=True), 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_vrrp.py b/lib/ansible/modules/network/nxos/nxos_vrrp.py index bf820ed9d8..58c04a8367 100644 --- a/lib/ansible/modules/network/nxos/nxos_vrrp.py +++ b/lib/ansible/modules/network/nxos/nxos_vrrp.py @@ -25,53 +25,53 @@ DOCUMENTATION = ''' --- module: nxos_vrrp version_added: "2.1" -short_description: Manages VRRP configuration on NX-OS switches +short_description: Manages VRRP configuration on NX-OS switches. description: - - Manages VRRP configuration on NX-OS switches + - Manages VRRP configuration on NX-OS switches. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - VRRP 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 - - I(state)=absent removes the vrrp group if it exists on the device - - VRRP cannot be configured on loopback interfaces + - VRRP 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. + - C(state=absent) removes the VRRP group if it exists on the device. + - VRRP cannot be configured on loopback interfaces. options: group: description: - - VRRP group number + - VRRP group number. required: true interface: description: - - Full name of interface that is being managed for VRRP + - Full name of interface that is being managed for VRRP. required: true priority: description: - - VRRP priority + - VRRP priority. required: false default: null vip: description: - - VRRP virtual IP address + - VRRP virtual IP address. required: false default: null authentication: description: - - clear text authentication string + - Clear text authentication string. required: false default: null admin_state: description: - - Used to enable or disable the VRRP process + - Used to enable or disable the VRRP process. required: false choices: ['shutdown', 'no shutdown'] default: no shutdown version_added: "2.2" state: description: - - Specify desired state of the resource + - Specify desired state of the resource. required: false default: present choices: ['present','absent'] diff --git a/lib/ansible/modules/network/nxos/nxos_vtp_password.py b/lib/ansible/modules/network/nxos/nxos_vtp_password.py index 1051f64bc9..12c142c2fc 100644 --- a/lib/ansible/modules/network/nxos/nxos_vtp_password.py +++ b/lib/ansible/modules/network/nxos/nxos_vtp_password.py @@ -25,9 +25,9 @@ DOCUMENTATION = ''' module: nxos_vtp version_added: "2.2" -short_description: Manages VTP configuration. +short_description: Manages VTP password configuration. description: - - Manages VTP configuration + - Manages VTP password configuration. extends_documentation_fragment: nxos author: - Gabriele Gerbino (@GGabriele) diff --git a/lib/ansible/modules/network/nxos/nxos_vxlan_vtep.py b/lib/ansible/modules/network/nxos/nxos_vxlan_vtep.py index 973c175a04..6d29597cd2 100644 --- a/lib/ansible/modules/network/nxos/nxos_vxlan_vtep.py +++ b/lib/ansible/modules/network/nxos/nxos_vxlan_vtep.py @@ -24,7 +24,7 @@ DOCUMENTATION = ''' --- module: nxos_vxlan_vtep version_added: "2.2" -short_description: Manages VXLAN Network Virtualization Endpoint (NVE) +short_description: Manages VXLAN Network Virtualization Endpoint (NVE). description: - Manages VXLAN Network Virtualization Endpoint (NVE) overlay interface that terminates VXLAN tunnels. @@ -32,13 +32,13 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - The module is used to manage NVE properties, not to create NVE - interfaces. Use nxos_interface if you wish to do so. - - State 'absent' removes the interface - - default, where supported, restores params default value + interfaces. Use M(nxos_interface) if you wish to do so. + - C(state=absent) removes the interface. + - Default, where supported, restores params default value. options: interface: description: - - Interface name for the VXLAN Network Virtualization Endpoint + - Interface name for the VXLAN Network Virtualization Endpoint. required: true description: description: @@ -71,7 +71,8 @@ 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'] diff --git a/lib/ansible/modules/network/nxos/nxos_vxlan_vtep_vni.py b/lib/ansible/modules/network/nxos/nxos_vxlan_vtep_vni.py index 545b62f3b8..cf354d59c4 100644 --- a/lib/ansible/modules/network/nxos/nxos_vxlan_vtep_vni.py +++ b/lib/ansible/modules/network/nxos/nxos_vxlan_vtep_vni.py @@ -31,11 +31,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. options: interface: description: - - Interface name for the VXLAN Network Virtualization Endpoint + - Interface name for the VXLAN Network Virtualization Endpoint. required: true vni: description: @@ -77,7 +77,8 @@ 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'] @@ -489,7 +490,6 @@ def main(): suppress_arp=dict(required=False, type='bool'), ingress_replication=dict(required=False, type='str', choices=['bgp', 'static', 'default']), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), include_defaults=dict(default=True), diff --git a/lib/ansible/modules/packaging/os/rhn_register.py b/lib/ansible/modules/packaging/os/rhn_register.py index e5fc77147d..c228f0b1b7 100644 --- a/lib/ansible/modules/packaging/os/rhn_register.py +++ b/lib/ansible/modules/packaging/os/rhn_register.py @@ -71,13 +71,13 @@ options: - supply a custom ssl CA certificate file for use with registration required: False default: None - version_added: "2.0" + version_added: "2.1" systemorgid: description: - supply an organizational id for use with registration required: False default: None - version_added: "2.0" + version_added: "2.1" channels: description: - Optionally specify a list of comma-separated channels to subscribe to upon successful registration. diff --git a/lib/ansible/modules/packaging/os/rpm_key.py b/lib/ansible/modules/packaging/os/rpm_key.py index 83d5e967f8..9cb058c56a 100644 --- a/lib/ansible/modules/packaging/os/rpm_key.py +++ b/lib/ansible/modules/packaging/os/rpm_key.py @@ -154,7 +154,7 @@ class RpmKey: gpg = self.module.get_bin_path('gpg2') if not gpg: - self.json_fail(msg="rpm_key requires a command lne gpg or gpg2, none found") + self.json_fail(msg="rpm_key requires a command line gpg or gpg2, none found") stdout, stderr = self.execute_command([gpg, '--no-tty', '--batch', '--with-colons', '--fixed-list-mode', '--list-packets', keyfile]) for line in stdout.splitlines(): diff --git a/lib/ansible/modules/packaging/os/yum.py b/lib/ansible/modules/packaging/os/yum.py index 53f4c24804..18e7171390 100644 --- a/lib/ansible/modules/packaging/os/yum.py +++ b/lib/ansible/modules/packaging/os/yum.py @@ -56,7 +56,7 @@ options: aliases: [ 'pkg' ] exclude: description: - - "Package name(s) to exlude when state=present, or latest + - "Package name(s) to exclude when state=present, or latest" required: false version_added: "2.0" default: null diff --git a/lib/ansible/modules/system/cron.py b/lib/ansible/modules/system/cron.py index 4306ea12bf..6e87147f39 100644 --- a/lib/ansible/modules/system/cron.py +++ b/lib/ansible/modules/system/cron.py @@ -137,7 +137,7 @@ options: disabled: description: - If the job should be disabled (commented out) in the crontab. Only has effect if state=present - version_added: "1.9" + version_added: "2.0" required: false default: false env: @@ -152,14 +152,14 @@ options: description: - Used with C(state=present) and C(env). If specified, the environment variable will be inserted after the declaration of specified environment variable. - version_added: "2" + version_added: "2.1" required: false default: null insertbefore: description: - Used with C(state=present) and C(env). If specified, the environment variable will be inserted before the declaration of specified environment variable. - version_added: "2" + version_added: "2.1" required: false default: null requirements: diff --git a/lib/ansible/modules/system/setup.py b/lib/ansible/modules/system/setup.py index 39186bfa35..81bbf43ddb 100644 --- a/lib/ansible/modules/system/setup.py +++ b/lib/ansible/modules/system/setup.py @@ -80,7 +80,7 @@ notes: - If the target host is Windows, you will not currently have the ability to use C(filter) as this is provided by a simpler implementation of the module. - If the target host is Windows you can now use C(fact_path). Make sure that this path - exists on the target host. Files in this path MUST be PowerShell scripts (*.ps1) and + exists on the target host. Files in this path MUST be PowerShell scripts (``*.ps1``) and their output must be formattable in JSON (Ansible will take care of this). Test the output of your scripts. This option was added in Ansible 2.1. diff --git a/lib/ansible/modules/system/user.py b/lib/ansible/modules/system/user.py index f812e4eb66..ed5503583a 100644 --- a/lib/ansible/modules/system/user.py +++ b/lib/ansible/modules/system/user.py @@ -57,6 +57,7 @@ options: required: false description: - Optionally sets the seuser type (user_u) on selinux enabled systems. + version_added: "2.1" group: required: false description: diff --git a/lib/ansible/modules/windows/win_lineinfile.py b/lib/ansible/modules/windows/win_lineinfile.py index a657a64d60..df250d6d41 100644 --- a/lib/ansible/modules/windows/win_lineinfile.py +++ b/lib/ansible/modules/windows/win_lineinfile.py @@ -23,13 +23,11 @@ ANSIBLE_METADATA = {'status': ['preview'], DOCUMENTATION = """ --- module: win_lineinfile -author: Brian Lloyd (brian.d.lloyd@gmail.com) -short_description: Ensure a particular line is in a file, or replace an - existing line using a back-referenced regular expression. +author: "Brian Lloyd <brian.d.lloyd@gmail.com>" +short_description: Ensure a particular line is in a file, or replace an existing line using a back-referenced regular expression. description: - This module will search a file for a line, and ensure that it is present or absent. - - This is primarily useful when you want to change a single line in - a file only. + - This is primarily useful when you want to change a single line in a file only. version_added: "2.0" options: dest: @@ -51,21 +49,14 @@ options: line: required: false description: - - Required for C(state=present). The line to insert/replace into the - file. If C(backrefs) is set, may contain backreferences that will get - expanded with the C(regexp) capture groups if the regexp matches. + - Required for C(state=present). The line to insert/replace into the file. If C(backrefs) is set, may contain backreferences that will get expanded with the C(regexp) capture groups if the regexp matches. backrefs: required: false default: "no" choices: [ "yes", "no" ] description: - - Used with C(state=present). If set, line can contain backreferences - (both positional and named) that will get populated if the C(regexp) - matches. This flag changes the operation of the module slightly; - C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp) - doesn't match anywhere in the file, the file will be left unchanged. - If the C(regexp) does match, the last matching line will be replaced by - the expanded line parameter. + - Used with C(state=present). If set, line can contain backreferences (both positional and named) that will get populated if the C(regexp) matches. This flag changes the operation of the module slightly; C(insertbefore) and C(insertafter) will be ignored, and if the C(regexp) doesn't match anywhere in the file, the file will be left unchanged. + - If the C(regexp) does match, the last matching line will be replaced by the expanded line parameter. insertafter: required: false default: EOF @@ -75,7 +66,6 @@ options: choices: [ 'EOF', '*regex*' ] insertbefore: required: false - version_added: "1.1" description: - Used with C(state=present). If specified, the line will be inserted before the last match of specified regular expression. A value is available; C(BOF) for inserting the line at the beginning of the file. - If specified regular expression has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs). @@ -85,16 +75,13 @@ options: choices: [ "yes", "no" ] default: "no" description: - - Used with C(state=present). If specified, the file will be created - if it does not already exist. By default it will fail if the file - is missing. + - Used with C(state=present). If specified, the file will be created if it does not already exist. By default it will fail if the file is missing. backup: required: false default: "no" choices: [ "yes", "no" ] description: - - Create a backup file including the timestamp information so you can - get the original file back if you somehow clobbered it incorrectly. + - Create a backup file including the timestamp information so you can get the original file back if you somehow clobbered it incorrectly. validate: required: false description: @@ -105,23 +92,15 @@ options: required: false default: "auto" description: - - Specifies the encoding of the source text file to operate on (and thus what the - output encoding will be). The default of C(auto) will cause the module to auto-detect - the encoding of the source file and ensure that the modified file is written with the - same encoding. - An explicit encoding can be passed as a string that is a valid value to pass to - the .NET framework System.Text.Encoding.GetEncoding() method - see - U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx). - This is mostly useful with C(create=yes) if you want to create a new file with a specific - encoding. If C(create=yes) is specified without a specific encoding, the default encoding - (UTF-8, no BOM) will be used. + - Specifies the encoding of the source text file to operate on (and thus what the output encoding will be). The default of C(auto) will cause the module to auto-detect the encoding of the source file and ensure that the modified file is written with the same encoding. + - "An explicit encoding can be passed as a string that is a valid value to pass to the .NET framework System.Text.Encoding.GetEncoding() method - see U(https://msdn.microsoft.com/en-us/library/system.text.encoding%28v=vs.110%29.aspx)." + - This is mostly useful with C(create=yes) if you want to create a new file with a specific encoding. If C(create=yes) is specified without a specific encoding, the default encoding (UTF-8, no BOM) will be used. newline: required: false description: - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (C(\r\n)). Note that the indicated line separator will be used for file output regardless of the original line separator that appears in the input file." choices: [ "windows", "unix" ] default: "windows" - """ EXAMPLES = r""" diff --git a/lib/ansible/modules/windows/win_msi.ps1 b/lib/ansible/modules/windows/win_msi.ps1 index a5b933b569..a3d455beb0 100644 --- a/lib/ansible/modules/windows/win_msi.ps1 +++ b/lib/ansible/modules/windows/win_msi.ps1 @@ -27,13 +27,17 @@ $creates = Get-Attr $params "creates" $false $extra_args = Get-Attr $params "extra_args" "" $wait = Get-Attr $params "wait" $false | ConvertTo-Bool -If (-not $params.path.GetType) +$result = New-Object psobject @{ + changed = $false +}; + +If (($creates -ne $false) -and ($state -ne "absent") -and (Test-Path $creates)) { - Fail-Json $result "missing required arguments: path" + Exit-Json $result; } $logfile = [IO.Path]::GetTempFileName(); -If ($params.state.GetType -and $params.state -eq "absent") +if ($state -eq "absent") { If ($wait) { diff --git a/lib/ansible/modules/windows/win_msi.py b/lib/ansible/modules/windows/win_msi.py index d365f5bdcf..cfc7e08982 100644 --- a/lib/ansible/modules/windows/win_msi.py +++ b/lib/ansible/modules/windows/win_msi.py @@ -61,7 +61,7 @@ options: - true - false default: false -author: Matt Martz +author: "Matt Martz (@sivel)" ''' EXAMPLES = ''' |