diff --git a/test/arista-ansible-role-test/README.md b/test/arista-ansible-role-test/README.md index f9583c1..3078ab0 100644 --- a/test/arista-ansible-role-test/README.md +++ b/test/arista-ansible-role-test/README.md @@ -8,9 +8,15 @@ Arista Roles for Ansible - Development Guidelines * [Overview] (#overview) * [Details] (#details) 2. [Developing Arista Roles For Ansible] (#developing-arista-roles-for-ansible) - * [Role Development Guidelines] (#role-development-guidelines) - * [Role Test Development] (#role-test-development) - * [Development for arista-ansible-role-test] (#development-for-arista-ansible-role-test) + * [Preparing the Role Development Workspace] (#preparing-the-role-development-workspace) + * [Existing Role Development] (#existing-role-development) + * [New Arista Ansible role development] (#new-arista-ansible-role-development) + * [Role Development Guidelines] (#role-development-guidelines) + * [Define the role's task list] (#define-the-roles-task-list) + * [Implement jinja2 templates for the role's tasks] (#implement-jinja2-templates-for-the-roles-tasks) + * [Include supporting files and documentation] (#include-supporting-files-and-documentation) + * [Role Test Development] (#role-test-development) + * [Development for arista-ansible-role-test] (#development-for-arista-ansible-role-test) @@ -83,31 +89,262 @@ used for the backups, as well as how to restore the device and delete the backup Developing Arista roles for Ansible ----------------------------------- +#### Preparing the Role Development Workspace + +##### Existing role development + +To begin development on an existing Arista Ansible role, clone/fork +the role repository to your working environment, create a working +branch, and proceed with development. + +##### New Arista Ansible role development + +To begin development on a new Arista role for Ansible, initialize a +role directory using the `ansible-galaxy init` command. + ``` + ansible-galaxy init ansible-eos-newrole + ``` + +This will create a directory named ansible-eos-newrole with the following +directory structure: + ``` + README.md + .travis.yml + defaults/ + main.yml + files/ + handlers/ + main.yml + meta/ + main.yml + templates/ + tests/ + inventory + test.yml + vars/ + main.yml + ``` + +Remove the .travis.yml file and the tests/ directory. + +From an existing Arista role, copy the following files into the new +role's path, creating any missing directories as needed, and updating +file information to match the new role: + + - .gitignore + - Makefile* + - files/README.md + - filter_plugins/config_block.py + - handlers/main.yml + - meta/main.yml* + - test/fixtures/hosts + + +``` +Note: An asterisk (*) indicates copied file should be reviewed for changes +specific to the new role, such as updating the role name. +``` + #### Role development guidelines -* Copy the following files from an existing Ansible EOS role into the - current role: - - * .gitignore - * Makefile* - * defaults/main.yml* - * files/README.md - * filter_plugins/config_block.py - * handlers/main.yml* - * meta/main.yml* - * tasks/main.yml* - * templates/README.md - * vars/main.yml* - * test/fixtures/hosts +An Arista role for Ansible consists of a list of tasks (tasks/main.yml) and +associated jinja2 templates (templates/*.j2) that will process a set of host +variables defined in an Ansible host_vars file. + +The development of an Arista role for Ansible includes defining the set of +tasks for the role, implementing the jinja2 templates for each of the +role-specific tasks, and providing any supporting files and documentation for +the role. + +##### Define the role's task list + +The task list for the role is defined in tasks/main.yml. + +Every Arista role should contain two tasks at the top of the list which will +gather the current running-config from the device and store it in the variable +`_eos_config`. + + ``` + - name: Gather EOS configuration + eos_command: + commands: 'show running-config all | exclude \.\*' + provider: "{{ provider | default(omit) }}" + auth_pass: "{{ auth_pass | default(omit) }}" + authorize: "{{ authorize | default(omit) }}" + host: "{{ host | default(omit) }}" + password: "{{ password | default(omit) }}" + port: "{{ port | default(omit) }}" + transport: "cli" + use_ssl: "{{ use_ssl | default(omit) }}" + username: "{{ username | default(omit) }}" + register: output + no_log: "{{ no_log | default(true) }}" + when: _eos_config is not defined + + - name: Save EOS configuration + set_fact: + _eos_config: "{{ output.stdout[0] }}" + no_log: "{{ no_log | default(true) }}" + when: _eos_config is not defined + ``` + +Then call each task that will be defined for the role itself, using the +following format: + + ``` + - name: Arista EOS < XXX task description > + eos_template: + src: XXXtemplatenameXXX.j2 + include_defaults: true + config: "{{ _eos_config | default(omit) }}" + auth_pass: "{{ auth_pass | default(omit) }}" + authorize: "{{ authorize | default(omit) }}" + host: "{{ host | default(omit) }}" + password: "{{ password | default(omit) }}" + port: "{{ port | default(omit) }}" + provider: "{{ provider | default(omit) }}" + transport: 'cli' + use_ssl: "{{ use_ssl | default(omit) }}" + username: "{{ username | default(omit) }}" + notify: save running config + ``` + +Refer to existing Arista roles and the Ansible documentation for examples on +using when, with_items, and other conditional statements to refine the +execution of the tasks in the role. + +Occasionally, a task may result in a change that needs to be propagated to the +stored _eos_config (tasks affect the running-config, not the stored _eos_config). +This may occur when an individual task produces results that are necessary may +not be in the initial configuration, but have updated the running-config. In +this instance, a block statement may be used to update the stored configuration +if a change has occurred. Refer to an existing Arista role, such as +ansible-eos-vxlan, for an example of how this might be handled. + +##### Implement jinja2 templates for the role's tasks + +Each task in the role's task list (other than those tasks for retrieving and +storing the running-config) should have a jinja2 file listed as the src entry +(XXXtemplatenameXXX.j2 in the example above). The jinja2 template files +take information from the passed in host_var definition and convert those to +a set of configuration entries that match those lines as they would be +returned from a `show running-config all` command on EOS. + +So, for example, a template that would set the hostname on the device would +return the following line. ``` - Note: Asterisk (*) indicates file should be reviewed for changes specific - to the new role, such as updating the role name. + hostname ``` + +A template that would configure elements of a BGP setup would require indented +information in addition to the initial `router bgp` call. + ``` + router bgp 113 + no shutdown + router-id 13.13.13.13 + maximum-paths 3 ecmp 14 + ``` + +Note that indented lines must match the three-space indentation exactly as is +returned by the EOS configuration output. The information returned from the +template is matched to the stored configuration to determine what calls will +be sent to the device. + +To maintain consistency, please follow the following formatting guidelines +for jinja2 templates included with Arista roles. + + - Each template should contain the file name marker, and set trim_blocks and + lstrip_blocks to false at the top of the file + - Indentation should be 3 spaces for all lines in the file + - Jinja filter pipes should be surrounded by a single space + - Output lines from the template (the lines that will be returned and + compared with the stored config) should be preceeded and followed by + a single blank line + - Multiple sequential output lines may be grouped and the entire + group enclosed by the single blank lines + - Comments that span multiple lines should have each line begin with + the Jinja2 comment delimiter + - The closing comment delimiter is only required on the last line + of the multi-line comment + - Comments should be used liberally to provide clarification to + the process taking place in the template + - Mark the end of for loops and if blocks where useful + - Explain the purpose of filters being used or sections of code + +Below is a short example of the formatting for a template file. Refer to +existing Arista roles template files for additional examples. -*XXX File structure, formatting guidelines, and other info goes here* + ``` + ! templates/XXXtest.j2 + #jinja2: trim_blocks: False + #jinja2: lstrip_blocks: False + + {% set state = item.state | default(eos_XXX_default_state) %} + {% set name = item.name %} + + {# add a comment that spans several lines to keep the formatting + {# and indentation across the multiple lines, making sure to close + {# the comment after the final line #} + + {# set the netaddr by filtering through ipsubnet. note the space around the pipe #} + {% set netaddr = srcaddr | ipsubnet(srcprefixlen) %} + + {% if not netaddr %} + {# if srcprefixlen is 32, netaddr is False, so define the netaddr as a host IP #} + {% set netaddr = "host %s" % srcaddr %} + {% endif %} + + {# set the seqno and log strings to be used in the actual rule #} + {% set seqno_rule = "%s " % seqno if seqno else '' %} + {% set log_rule = ' log' if log else '' %} + + {# build the rule command line #} + {% set rule = "%s%s %s%s" % (seqno_rule, action, netaddr, log_rule) %} + + {# send the rule - surround the output by blank lines #} + + {{ rule }} + + {# the comments around the output lines are not required, but are + {# for the example only #} + + {# make sure the indentation is correct for an EOS configuration #} + + {% if something %} + {% if x == y %} + + hostname {{ x }} <-- note the indentation + + {% else %} + + hostname {{ abc }} + + {% endif %} {# x == y #} + {% endif %} {# something #} + ``` +##### Include supporting files and documentation + +Occasionally, a default value is desired for a variable within an Arista role, +as in the `set state` line in the example above. These defaults may be set +in the defaults/main.yml file, and should be explanatory in their naming. +Default values do not fill in missing host_vars values automatically, but must +be pulled in through a default filter as shown in the example. + +There may also arise the need to implement a specific filter to be used by the +role. Filters are written in python and located in the filter_plugins +directory. The config_block filter is included in all roles, and contains +filters to return a block of configuration from the _eos_config file, as well +as regular expression search and findall filters for matching lines in the +config. Additional filters may be implemented as necessary. See the range filter +in ansible-eos-mlag for an example. + +Finally, make sure the README.md is properly updated for the role. Include a +brief description of the purpose of the role, role variable information, any +dependencies, and an example playbook. Refer to existing Arista roles for +content examples, and sections that may be copied and pasted for ease. #### Role test development diff --git a/test/arista-ansible-role-test/test_module.py b/test/arista-ansible-role-test/test_module.py index 09a9d8d..19f7419 100644 --- a/test/arista-ansible-role-test/test_module.py +++ b/test/arista-ansible-role-test/test_module.py @@ -18,7 +18,8 @@ HERE = os.path.abspath(os.path.dirname(__file__)) ROLE = re.match( r'^.*\/ansible-eos-([^/\s]+)\/test/arista-ansible-role-test$', HERE).group(1) -CONFIG_BACKUP = '_eos_role_test_{}'.format(ROLE) +RUN_CONFIG_BACKUP = '_eos_role_test_{}_running'.format(ROLE) +START_CONFIG_BACKUP = '_eos_role_test_{}_startup'.format(ROLE) EOS_ROLE_PLAYBOOK = 'test/arista-ansible-role-test/eos_role.yml' EOS_MODULE_PLAYBOOK = 'test/arista-ansible-role-test/eos_module.yml' @@ -275,7 +276,29 @@ def setup(): 'description': 'Back up running-config on node', 'cmds': [ 'configure terminal', - 'copy running-config {}'.format(CONFIG_BACKUP) + 'copy running-config {}'.format(RUN_CONFIG_BACKUP) + ], + } + arguments = [json.dumps(args)] + + ret_code, out, err = ansible_playbook(EOS_MODULE_PLAYBOOK, + arguments=arguments) + + if ret_code != 0: + LOG.write(str(">> ansible-playbook {} stdout:\n".format(EOS_MODULE_PLAYBOOK), out)) + LOG.write(str(">> ansible-playbook {} stddrr:\n".format(EOS_MODULE_PLAYBOOK), err)) + teardown() + raise RuntimeError("Error in Test Suite Setup") + + run_backup = " Backing up startup-config on nodes ..." + print >> sys.stderr, run_backup + LOG.write('++ ' + run_backup.strip()) + args = { + 'module': 'eos_command', + 'description': 'Back up startup-config on node', + 'cmds': [ + 'configure terminal', + 'copy startup-config {}'.format(START_CONFIG_BACKUP) ], } arguments = [json.dumps(args)] @@ -333,9 +356,15 @@ def teardown(): " - configure terminal\n" " - configure replace {}\n" " - delete {}\n" - "{}".format(SEPARATOR, CONFIG_BACKUP, - CONFIG_BACKUP, SEPARATOR)) + " - copy {} startup-config\n" + " - delete {}\n" + "{}".format(SEPARATOR, RUN_CONFIG_BACKUP, + RUN_CONFIG_BACKUP, + START_CONFIG_BACKUP, + START_CONFIG_BACKUP, SEPARATOR)) else: + # Restore the running-config on the nodes + # --------------------------------------- restore_backup = " Restoring running-config on nodes ..." print >> sys.stderr, restore_backup LOG.write('++ ' + restore_backup.strip()) @@ -344,7 +373,8 @@ def teardown(): 'description': 'Restore running-config from backup', 'cmds': [ 'configure terminal', - 'configure replace {}'.format(CONFIG_BACKUP), + 'configure replace {}'.format(RUN_CONFIG_BACKUP), + 'delete {}'.format(RUN_CONFIG_BACKUP), ], } arguments = [json.dumps(args)] @@ -354,34 +384,40 @@ def teardown(): arguments=arguments) if ret_code != 0: - msg = "Error replacing running-config on nodes\n" \ + msg = "Error restoring running-config on nodes\n" \ "Running ansible-playbook {} -e {}\n" \ ">> stdout: {}\n" \ ">> stderr: {}\n".format(EOS_MODULE_PLAYBOOK, arguments, out, err) warnings.warn(msg) - delete_backup = " Deleting backup config from nodes ..." - print >> sys.stderr, delete_backup - LOG.write('++ ' + delete_backup.strip()) + # Restore the startup-config on the nodes + # --------------------------------------- + restore_backup = " Restoring startup-config on nodes ..." + print >> sys.stderr, restore_backup + LOG.write('++ ' + restore_backup.strip()) args = { 'module': 'eos_command', - 'description': 'Delete backup config file from node', + 'description': 'Restore startup-config from backup', 'cmds': [ - 'configure terminal', 'delete {}'.format(CONFIG_BACKUP) + 'configure terminal', + 'copy {} startup-config'.format(START_CONFIG_BACKUP), + 'delete {}'.format(START_CONFIG_BACKUP), ], } arguments = [json.dumps(args)] + # ret_code, out, err = ansible_playbook(CMD_PLAY, arguments=arguments) ret_code, out, err = ansible_playbook(EOS_MODULE_PLAYBOOK, arguments=arguments) if ret_code != 0: - msg = "Error deleting backup config on nodes\n" \ + msg = "Error restoring startup-config on nodes\n" \ "Running ansible-playbook {} -e {}\n" \ ">> stdout: {}\n" \ ">> stderr: {}\n".format(EOS_MODULE_PLAYBOOK, arguments, out, err) warnings.warn(msg) + print >> sys.stderr, " Teardown complete"