Ticket Please – A ServiceNow Automation Post

Standard

Awhile back I did a post on why screen scraping is annoying compared to APIs because you have to spend some effort cleaning up the output before you can use it.

Today we are going to have some fun with Cisco’s PyAts toolset and their Parse_genie Ansible plugin. Ultimately we will parse some output from a Cisco router and use it to create a trouble ticket in ServiceNow!

The topology for this is pretty simple, I have 4 x CSR1000v routers that I will be testing against but I’ll be focused on just one to try to make things a bit more clean. I’ll also be doing all my Ansible stuff from a RHEL 8 box.

snow-006.png

The scripting part of our lab will have three parts:

PyAts – This is Cisco’s internal testing environment that they recently released to the public, it allows for automated testing and capturing the results. I’ll probably end up making another post on this on its own soon.

Genie – For our purposes this is a collection of parsers that will format our output the way we want it.

Parse_genie – This is a Ansible plugin that lets us use genie in our playbooks.

Since the solution needs at least Python 3.4 and a pretty recent version of Ansible, we’ll go ahead and create a virtual environment to keep our work space clean.

The first thing I will do is create a requirements file so we can install the things we need with pip. We’ll also need paramiko for ansible to work so I’ll install netmiko to get it on my box since that is what the cool kids use. Also if you haven’t seen the EOF treat before, it is a quick way to make files with multiple lines.

[root@rhel01 PyAts]# cat <<EOF > requirements.txt
ansible
netmiko
genie
EOF

Next I’ll create a venv called .Lab, switch into it, then install our requirements.txt file.

[root@rhel01 PyAts]# python3 -m venv .Lab
[root@rhel01 PyAts]# source .Lab/bin/activate
(.Lab) [root@rhel01 PyAts]# pip install -r requirements.txt 
Collecting ansible (from -r requirements.txt (line 1))
  Using cached https://files.pythonhosted.org/packages/f3/0d/ee54843308769f2c78be849d9e9f65dc8d63781941dc880f68507aae33ba/ansible-2.9.2.tar.gz
Collecting netmiko (from -r requirements.txt (line 2))
  Cache entry deserialization failed, entry ignored
  Cache entry deserialization failed, entry ignored
  Downloading https://files.pythonhosted.org/packages/26/05/dbe9c97c39f126e7b8dc70cf897dcad557dbd579703f2e3acfd3606d0cee/netmiko-2.4.2-py2.py3-none-any.whl (144kB)
    100% |████████████████████████████████| 153kB 3.1MB/s 
Collecting genie (from -r requirements.txt (line 3))

Lastly we’ll install the parse_genie plugin using ansible-galaxy, we can find it on clay584’s Github.

(.Lab) [root@rhel01 PyAts]# ansible-galaxy install clay584.parse_genie
- downloading role 'parse_genie', owned by clay584
- downloading role from https://github.com/clay584/parse_genie/archive/1.4.0.tar.gz
- extracting clay584.parse_genie to /root/.ansible/roles/clay584.parse_genie
- clay584.parse_genie (1.4.0) was installed successfully

Setting up Ansible

When we install things through pip it generally doesn’t create folders for you like it would if you installed through yum or apt. This means that we need to create the standard Ansible files we need for our lab.

The ansible.cfg has a lot of settings we can adjust, but for our purposes we are just going to turn off host key checking so we can connect to our routers.

(.Lab) [root@rhel01 PyAts]# cat ansible.cfg 
[defaults]
host_key_checking = False

Next we need an inventory file which I’ll call hosts, for the most part I’ll stick with the router group to keep the output clean since there is just one router in it.

(.Lab) [root@rhel01 PyAts]# cat hosts 
[switch]
sw0[1:2].testlab.com

[router]
csr31.testlab.com

[cisco:children]
switch
router

[cisco:vars]
ansible_connection=network_cli
ansible_network_os=ios
ansible_user=ansible
ansible_password=ansible

 

Our Standard Playbook

Before we dive into the fun stuff lets remind ourselves what Ansible output looks like, since I mentioned CDP in the screen scraping example earlier, we’ll use that for fun.

Here is a simple playbook that will print out some CDP output from our router group.

---
- name: Standard CDP Output 
hosts: router
gather_facts: no
connection: network_cli

tasks:

- name: Run the show cdp neighbor command
ios_command:
commands:
- show cdp neighbor 
register: showcdp

- name: Print Output - CDP
debug:
var: showcdp

When we run the playbook we can see get the CDP output printed out but we would need to work on the output before we can use this in another script since it just captured the standard output of the command.

If I wanted to grab particular data out of the output I would need to work on it more by doing some regex or maybe converting it to JSON first.

(.Lab) [root@rhel01 PyAts]# ansible-playbook -i hosts get-cdp-standard.yml

PLAY [Standard CDP Output] **********************************************************************************************************************************************

TASK [Run the show cdp neighbor command] ********************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Print Output - CDP] ***********************************************************************************************************************************************
ok: [csr31.testlab.com] => {
"showcdp": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/libexec/platform-python"
},
"changed": false,
"failed": false,
"stdout": [
"Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge\n S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, \n D - Remote, C - CVTA, M - Two-port Mac Relay \n\nDevice ID Local Intrfce Holdtme Capability Platform Port ID\n88154402f38c Gig 1 156 S MS220-8P Port 1\nCSR33.testlab.com\n Gig 1 147 R I CSR1000V Gig 1\nCSR32.testlab.com\n Gig 1 154 R I CSR1000V Gig 1\nCSR32.testlab.com\n Gig 2 158 R I CSR1000V Gig 2\nCSR34.testlab.com\n Gig 1 175 R I CSR1000V Gig 1\n\nTotal cdp entries displayed : 5"
],
"stdout_lines": [
[
"Capability Codes: R - Router, T - Trans Bridge, B - Source Route Bridge",
" S - Switch, H - Host, I - IGMP, r - Repeater, P - Phone, ",
" D - Remote, C - CVTA, M - Two-port Mac Relay ",
"",
"Device ID Local Intrfce Holdtme Capability Platform Port ID",
"88154402f38c Gig 1 156 S MS220-8P Port 1",
"CSR33.testlab.com",
" Gig 1 147 R I CSR1000V Gig 1",
"CSR32.testlab.com",
" Gig 1 154 R I CSR1000V Gig 1",
"CSR32.testlab.com",
" Gig 2 158 R I CSR1000V Gig 2",
"CSR34.testlab.com",
" Gig 1 175 R I CSR1000V Gig 1",
"",
"Total cdp entries displayed : 5"
]
]
}
}

PLAY RECAP *********************************************************************************************************

For example if I just wanted the interface name out of the CDP output, I could use Ansible’s regex features to  match it.

Regex looks scary but basically I’m matching a line that starts with letters and then numbers which matches my hostnames. Though if you don’t number your router names then you need to adjust it a bit.

- set_fact:
    fact_showcdp: |
      {{showcdp.stdout_lines[0] | map('regex_findall','^([A-Za-z]+[0-9/]+\.testlab\.com)') | list }}

- name: Print Output - CDP
debug:
var: fact_showcdp

When I run it we get just the hostnames in the CDP output, but also empty lines that don’t match that. This is because Ansible is still seeing the full results but only showing the matched regex output.

TASK [Print Output - CDP] ***********************************************************************************************************************************************
ok: [csr31.testlab.com] => {
    "msg": [
        "[]",
        "[]",
        "[]",
        "[]",
        "[]",
        "[]",
        "['CSR33.testlab.com']",
        "[]",
        "['CSR32.testlab.com']",
        "[]",
        "['CSR32.testlab.com']",
        "[]",
        "['CSR34.testlab.com']",
        "[]",
        "[]",
        "[]"
    ]
}

We could spend more time cleaning it up but I think you get the idea!

The Parse Genie Playbook!

Next we’ll do the same thing but with the parse_genie doing the heavy lifting.

---
- name: Fun with CDP 
hosts: router
gather_facts: no
connection: network_cli
roles:
- clay584.parse_genie

tasks:

- name: Run the show cdp neighbor command
ios_command:
commands:
- show cdp neighbor 
register: showcdp

- name: Set fact - Show CDP
set_fact:
pyats_showcdp: "{{ showcdp['stdout'][0] | parse_genie(command='show cdp neighbors', os=ansible_network_os) }}"

- name: Debug Facts - CDP
debug:
var: pyats_showcdp.cdp

Let’s take a minute to break down what we are doing here:

First we need to tell the playbook to use the clay584.parse_genie role.

 roles:
- clay584.parse_genie

Then we run the show cdp neighbor command and store the result in a variable called showcdp.

 - name: Run the show cdp neighbor command
ios_command:
commands:
- show cdp neighbor 
register: showcdp

This is where the magic happens! We create a fact called pyats_showcdp  (though we can call it whatever we want), then we talk the standard output from the showcdp variable above then pipe it into parse_genie.

Lastly We tell it what parser to apply, and also what device type to use, I’m using the ansible_network_os variable to set it to ios, this is so I can reuse playbooks with different device types this way.

- name: Set fact - Show CDP
set_fact:
pyats_showcdp: "{{ showcdp['stdout'][0] | parse_genie(command='show cdp neighbors', os=ansible_network_os) }}"

All that is left is to print the output

 - name: Debug Facts - CDP
debug:
var: pyats_showcdp.cdp

After all that effort, let’s see what the difference is! Everything is now nicely formatted in JSON so we can easily access the output as we need to.

(.Lab) [root@rhel01 PyAts]# ansible-playbook -i hosts get-cdp-genie.yml

PLAY [Fun with CDP] *****************************************************************************************************************************************************

TASK [Run the show cdp neighbor command] ********************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Set fact - Show CDP] **********************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Debug Facts - CDP] ************************************************************************************************************************************************
ok: [csr31.testlab.com] => {
"pyats_showcdp.cdp": {
"index": {
"2": {
"capability": "R I",
"device_id": "CSR33.testlab.com",
"hold_time": 136,
"local_interface": "GigabitEthernet1",
"platform": "CSR1000V",
"port_id": "GigabitEthernet1"
},
"3": {
"capability": "R I",
"device_id": "CSR32.testlab.com",
"hold_time": 173,
"local_interface": "GigabitEthernet1",
"platform": "CSR1000V",
"port_id": "GigabitEthernet1"
},
"4": {
"capability": "R I",
"device_id": "CSR32.testlab.com",
"hold_time": 143,
"local_interface": "GigabitEthernet2",
"platform": "CSR1000V",
"port_id": "GigabitEthernet2"
},
"5": {
"capability": "R I",
"device_id": "CSR34.testlab.com",
"hold_time": 157,
"local_interface": "GigabitEthernet1",
"platform": "CSR1000V",
"port_id": "GigabitEthernet1"
}
}
}
}

For example, if I just wanted to see device_id from index 2, I could do this:

 - name: Debug Facts - CDP
debug:
var: pyats_showcdp.cdp.index.2.device_id

Now we get just that output when I run it

TASK [Debug Facts - CDP] ************************************************************************************************************************************************
ok: [csr31.testlab.com] => {
"pyats_showcdp.cdp.index.2.device_id": "CSR33.testlab.com"
}

This is going to be very handy latter when we are trying to make a trouble ticket.

Another example!

You might be wondering where you get information about the parsers you can use, to  see what is available, simply go to: https://pubhub.devnetcloud.com/media/genie-feature-browser/docs/#/parsers

The site gives us a reference of all the parsers that are currently supported and breaks it down by platform type. This lets us know what commands we can reference when we make our parse_genie pipe and also lets us know what the output will look like. This is useful when we want just particular fields to come back from the playbook. Though it might be just as easy to do a dry run with the full command and then see what fields you get in the results. Either way works!

snow-005.png

To give the parsers a try, here is the output of the show ip interface brief command ran through a parse_genie playbook.

- name: Set fact - Show IP Int Brief
set_fact:
pyats_showipintbrief: "{{ showipintbrief['stdout'][0] | parse_genie(command='show ip interface brief', os=ansible_network_os) }}"

- name: Debug Facts - Show ip int brief
debug:
var: pyats_showipintbrief.interface

Once again we get a easy to work with output!

TASK [Debug Facts - Show ip int brief] **********************************************************************************************************************************
ok: [csr31.testlab.com] => {
"pyats_showipintbrief.interface": {
"GigabitEthernet1": {
"interface_is_ok": "YES",
"ip_address": "10.30.10.81",
"method": "NVRAM",
"protocol": "up",
"status": "up"
},
"GigabitEthernet2": {
"interface_is_ok": "YES",
"ip_address": "10.1.1.1",
"method": "manual",
"protocol": "up",
"status": "up"
},
"Loopback0": {
"interface_is_ok": "YES",
"ip_address": "10.255.255.31",
"method": "manual",
"protocol": "up",
"status": "up"
},
"Loopback10": {
"interface_is_ok": "YES",
"ip_address": "192.168.10.1",
"method": "manual",
"protocol": "up",
"status": "up"
},
"Loopback11": {
"interface_is_ok": "YES",
"ip_address": "192.168.11.1",
"method": "manual",
"protocol": "up",
"status": "up"
},
"Loopback12": {
"interface_is_ok": "YES",
"ip_address": "192.168.12.1",
"method": "manual",
"protocol": "up",
"status": "up"
},
"Loopback13": {
"interface_is_ok": "YES",
"ip_address": "192.168.13.1",
"method": "manual",
"protocol": "up",
"status": "up"
},
"VirtualPortGroup1": {
"interface_is_ok": "YES",
"ip_address": "192.168.123.1",
"method": "manual",
"protocol": "up",
"status": "up"
}
}
}

Take your ticket!

To recap we have setup a simple Ansible environment and played with the genie parser.  To wrap this up I’ll put my ServiceNow environment to good use and create a incident that includes some router info!

What we’ll do is include some fields from show version and also the router IP which we’ll get from show ip int brief, we just looked at that output so here is show version for reference:

ok: [csr31.testlab.com] => {
"pyats_showipintbrief.version": {
"chassis": "CSR1000V",
"chassis_sn": "9CA8RUAJXPB",
"compiled_by": "mcpre",
"compiled_date": "Fri 22-Nov-19 03:39",
"curr_config_register": "0x2102",
"disks": {
"bootflash:.": {
"disk_size": "102657024",
"type_of_disk": "virtual hard disk"
}
},
"hostname": "CSR31",
"image_id": "X86_64_LINUX_IOSD-UNIVERSALK9-M",
"image_type": "production image",
"last_reload_reason": "reload",
"license_level": "ax",
"license_type": "N/A(Smart License Enabled)",
"main_mem": "2074934",
"mem_size": {
"non-volatile configuration": "32768",
"physical": "3975800"
},
"next_reload_license_level": "ax",
"number_of_intfs": {
"Gigabit Ethernet": "8"
},
"os": "IOS-XE",
"platform": "Virtual XE",
"processor_type": "VXE",
"returned_to_rom_by": "reload",
"rom": "IOS-XE ROMMON",
"rtr_type": "CSR1000V",
"system_image": "bootflash:packages.conf",
"uptime": "2 days, 6 hours, 41 minutes",
"uptime_this_cp": "2 days, 6 hours, 51 minutes",
"version": "17.1.1",
"version_short": "17.1"
}
}

From here we’ll take note of the following fields for later later on:

uptime
version
hostname
chassis_sn
rtr_type

Fortunately actually making the ticket is easily done because Ansible has a ServiceNow module called snow_record that we can use.

First I need to create a file to store the ServiceNow info called snow_vars.yml, sorry guys I’ve changed the login info 🙂

---
#snow_record variables

snow_username: admin
snow_password: Meowcat123 
snow_instance: dev56688

Since ServiceNow doesn’t have a field for our Cisco stuff out of the box we need to job over there and create some new ones. To get there log into ServiceNow, go to Incidents, and edit the module.

Then in the module menu select Configure -> Form Layout

snow-001

In the form page, we simply add each new field we want to appear in the in incident page. When you append the name with u_ ServiceNow will display the name without it on the actual page but makes it easier to reference the right field in our playbook since its easy to identify the new fields in SN.

snow-002

Save when your done and we can now see the fields in the incident page.

snow-003

With all that done we can now focus on our playbook.

Here is the whole thing and I’ll break down the new stuff in a sec.

---
- name: Create a ServiceNow Incident 
hosts: router 
gather_facts: no
connection: network_cli
roles:
- clay584.parse_genie

tasks:
- name: Include vars
include_vars: snow_vars.yml

# Show version
- name: show version
ios_command:
commands:
- show version
register: showversion

- name: Set Fact - Show version
set_fact:
pyats_showversion: "{{ showversion['stdout'][0] | parse_genie(command='show version', os=ansible_network_os) }}"

# Show IP Int Brief
- name: show ip interface brief
ios_command:
commands:
- show ip interface brief
register: showipBrief

- name: Set Fact - Show IP Brief
set_fact:
pyats_showipBrief: "{{ showipBrief['stdout'][0] | parse_genie(command='show ip interface brief', os=ansible_network_os) }}"

# Create Incident
- name: Create an incident
snow_record:
state: present
table: incident
username: "{{ snow_username }}"
password: "{{ snow_password }}"
instance: "{{ snow_instance }}"
data:
priority: "3"
u_device_up_time: "{{ pyats_showversion.version.uptime }}"
u_ios_version: "{{ pyats_showversion.version.version }}"
u_hostname: "{{ pyats_showversion.version.hostname }}"
u_platform: "{{ pyats_showversion.version.platform }}"
u_device_type: "{{ pyats_showversion.version.rtr_type }}"
u_serial_number: "{{ pyats_showversion.version.chassis_sn }}"
u_last_reload_reason: "{{ pyats_showversion.version.last_reload_reason }}"
u_ipaddress: "{{ pyats_showipBrief.interface.GigabitEthernet1.ip_address }}"
short_description: "This ticket was created by Ansible"
description: "Meow Meow Meow Meow Meow Meow Meow Meow"
category: "Network"
assignment_group: "Network"
subcategory: "DHCP"
caller_id: "Ansible Network"
register: new_incident

- name: Show Incident number
debug: 
var=new_incident.record.number

After we get all the genie’d output we can make the ticket with the snow_record module.

The first part tells ServiceNow what type of ticket it is, we’re making an incident, and also grabs the login info from our variables.

# Create Incident
- name: Create an incident
snow_record:
state: present
table: incident
username: "{{ snow_username }}"
password: "{{ snow_password }}"
instance: "{{ snow_instance }}"

Then we start working on data in the ticket, I assign a priority for the incident and then map each new ServiceNow field to our output we got.  For example, u_device_uptime will be mapped to the output in pyats_showversion.version.uptime

data:
priority: "3"
u_device_uptime: "{{ pyats_showversion.version.uptime }}"
u_ios_version: "{{ pyats_showversion.version.version }}"
u_hostname: "{{ pyats_showversion.version.hostname }}"
u_platform: "{{ pyats_showversion.version.platform }}"
u_device_type: "{{ pyats_showversion.version.rtr_type }}"
u_serial_number: "{{ pyats_showversion.version.chassis_sn }}"
u_last_reload_reason: "{{ pyats_showversion.version.last_reload_reason }}"
u_mgmt_ipaddress: "{{ pyats_showipBrief.interface.GigabitEthernet1.ip_address }}"

The rest of the section is pretty understandable, its just things like description for the ticket and who called.

short_description: "This ticket was created by Ansible"
description: "Meow Meow Meow Meow Meow Meow Meow Meow"
category: "Network"
assignment_group: "Network"
subcategory: "DHCP"
caller_id: "Ansible Network"

Just to make the playbook a bit nicer to use, I also added a section to have the ticket number printed out when we run the it.

register: new_incident

- name: Show Incident number
debug:
var=new_incident.record.number

Now all that is left to do is run the playbook!

(.Lab) [root@rhel01 PyAts]# ansible-playbook -i hosts create-incident.yml

PLAY [Create a ServiceNow Incident] *************************************************************************************************************************************

TASK [Include vars] *****************************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [show version] *****************************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Set Fact - Show version] ******************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [show ip interface brief] ******************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Set Fact - Show IP Brief] *****************************************************************************************************************************************
ok: [csr31.testlab.com]

TASK [Create an incident] ***********************************************************************************************************************************************
changed: [csr31.testlab.com]

TASK [Show Incident number] *********************************************************************************************************************************************
ok: [csr31.testlab.com] => {
"new_incident.record.number": "INC0010005"
}

PLAY RECAP **************************************************************************************************************************************************************
csr31.testlab.com : ok=7 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

If I check ServiceNow, we suddenly have a INC0010005 incident from “Ansible Network” our description, and it also includes the router information that we added.

snow-004.png

You might be wondering what happens if there are more devices in the Ansible group, it simply would create an incident per device like so:

TASK [Show Incident number] *********************************************************************************************************************************************
ok: [csr33.testlab.com] => {
"new_incident.record.number": "INC0010009"
}
ok: [csr32.testlab.com] => {
"new_incident.record.number": "INC0010008"
}
ok: [csr34.testlab.com] => {
"new_incident.record.number": "INC0010010"
}
ok: [csr31.testlab.com] => {
"new_incident.record.number": "INC0010007"

A Step Further

That was fun! But since we did the bulk of everything in Ansible, some of you might be wondering if we can automate the field creation in ServiceNow. Yes! But we have to go about it differently. At the time of this blog there are two modules available:

snow_record – This is what we used to make our record.

snow_ record_find – This is used for searching for things.

Fortunately ServiceNow has a full API so we can use REST to add a record!

We can build out the request using ServiceNow’s Rest API Explorer

snow-007.png

Then generate the curl command that will look something like this, I’ve also added json_pp to make the response code more pretty.

[root@rhel01 PyAts]# curl "https://dev56615.service-now.com/api/now/table/sys_dictionary" \
> --request POST \
> --header "Accept:application/json" \
> --header "Content-Type:application/json" \
> --data "{\"name\":\"incident\",\"max_length\":\"100\",\"internal_type\":\"747127c1bf3320001875647fcf0739e0\",\"column_label\":\"test101\"}" \
> --user 'admin':'Meowcat1234' | json_pp
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100  2077    0  1963  100   114   1932    112  0:00:01  0:00:01 --:--:--  2046
{
   "result" : {
      "function_field" : "false",
      "element_reference" : "false",
      "reference_floats" : "false",
      "sys_updated_on" : "2020-01-06 23:42:20",
      "primary" : "false",
      "dynamic_default_value" : "",
      "function_definition" : "",
      "mandatory" : "false",
      "delete_roles" : "",
      "virtual" : "false",
      "column_label" : "test101",
      "table_reference" : "false",
      "sys_scope" : {
         "value" : "global",
         "link" : "https://dev56615.service-now.com/api/now/table/sys_scope/global"
trimmed for brevity, it goes on for quite awhile

Now all that is left is to convert this over to Ansible’s URI module which would look something like this:

- name: Create SNOW Fields
uri:
url: https://dev56615.service-now.com/api/now/table/sys_dictionary
user: "{{ snow_username }}"
password: "{{ snow_password }}"
method: POST
body: "{\"name\":\"incident\",\"max_length\":\"100\",\"internal_type\":\"747127c1bf3320001875647fcf0739e0\",\"column_label\":\"{{ item }}\"}" 
force_basic_auth: yes
status_code: 201
body_format: json

with_items:
- testloop101
- testloop102
- testloop103

The logic is the same as the curl command except I am looping through a list of items in column_label field which is the field name. For now I’m using testloop101 through 103 for testing.

Now if we run it it will create my three testloop fields for me!

[root@rhel01 PyAts]# ansible-playbook -i hosts create-snow-fields.yml

PLAY [Create SNOW Fields with API] **************************************************************************************************************************************

TASK [Create SNOW Fields] ***********************************************************************************************************************************************
ok: [localhost] => (item=testloop101)
ok: [localhost] => (item=testloop102)
ok: [localhost] => (item=testloop103)

All that would be left is to add the code into our main playbook so it can create the proper fields for our show version output.  Our full playbook will look like this:

---
- name: Create a ServiceNow Incident 
hosts: router 
gather_facts: no
connection: network_cli
roles:
- clay584.parse_genie

tasks:
- name: Include vars
include_vars: snow_vars.yml

# Show version
- name: show version
ios_command:
commands:
- show version
register: showversion

- name: Set Fact - Show version
set_fact:
pyats_showversion: "{{ showversion['stdout'][0] | parse_genie(command='show version', os=ansible_network_os) }}"

# Show IP Int Brief
- name: show ip interface brief
ios_command:
commands:
- show ip interface brief
register: showipBrief

- name: Set Fact - Show IP Brief
set_fact:
pyats_showipBrief: "{{ showipBrief['stdout'][0] | parse_genie(command='show ip interface brief', os=ansible_network_os) }}"

# Create ServiceNOW Fields with API
- name: Create SNOW Fields
uri:
url: https://{{ snow_instance }}.service-now.com/api/now/table/sys_dictionary
user: "{{ snow_username }}"
password: "{{ snow_password }}"
method: POST
body: "{\"name\":\"incident\",\"max_length\":\"100\",\"internal_type\":\"747127c1bf3320001875647fcf0739e0\",\"column_label\":\"{{ item }}\"}" 
force_basic_auth: yes
status_code: 201
body_format: json

with_items:
- u_device_uptime
- u_ios_version
- u_hostname
- u_platform
- u_device_type
- u_serial_number
- u_last_reload_reason
- u_mgmt_ipaddress

# Create Incident
- name: Create an incident
snow_record:
state: present
table: incident
username: "{{ snow_username }}"
password: "{{ snow_password }}"
instance: "{{ snow_instance }}"
data:
priority: "3"
u_device_uptime: "{{ pyats_showversion.version.uptime }}"
u_ios_version: "{{ pyats_showversion.version.version }}"
u_hostname: "{{ pyats_showversion.version.hostname }}"
u_platform: "{{ pyats_showversion.version.platform }}"
u_device_type: "{{ pyats_showversion.version.rtr_type }}"
u_serial_number: "{{ pyats_showversion.version.chassis_sn }}"
u_last_reload_reason: "{{ pyats_showversion.version.last_reload_reason }}"
u_mgmt_ipaddress: "{{ pyats_showipBrief.interface.GigabitEthernet1.ip_address }}"
short_description: "This ticket was created by Ansible"
description: "Meow Meow Meow Meow Meow Meow Meow Meow"
category: "Network"
assignment_group: "Network"
subcategory: "DHCP"
caller_id: "Ansible Network"
register: new_incident

- name: Show Incident number
debug: 
var=new_incident.record.number

Now this isn’t perfect because we are using the POST method so this will fail if the records already exist in ServiceNow so we would need to either rework things to use the PUT method or add in some if/then logic into the ServiceNow field part of the playbook but I think you get the point about how you can work with APIs with Ansible.

Ansible Tower

I was initially going to follow this up with a Ansible Tower version, but as it turns out Ansible’s Colin McCarthy already beat me too it, you can check it out here.

Final Thoughts

Hopefully you found this to be a neat early year post, the main point is that we can more easily work with output using the PyAts + Genie combo, and I put my ServiceNow developer instance to good use so win/win.

Next time I might go a bit more into PyAts for automated testing with VIRL but honestly who knows with me, I like to keep everyone guessing.

Happy New Year!

 

One thought on “Ticket Please – A ServiceNow Automation Post

  1. Plagiarism Police

    This looks really similar to a blog written already by Ansible. Maybe give them credit or refer to their blog from them?

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.