pod01

Lab Task 4.3 - Scenario 1 (Better together)

Step 1: Catalyst Center

Create a new file named 03_deploy-template.yml in the dnac/playbooks directory.

yaml
POD01
1---
2- hosts: dnac_servers
3  gather_facts: false
4  connection: local
5  tasks:
6    - name: Get timestamp from the system
7      shell: "date +%Y-%m-%d%H-%M-%S"
8      register: tstamp
9
10    - debug: var=tstamp.stdout
11
12    - name: Set sentitive variables
13      ansible.builtin.set_fact:
14        api_token: "{{ lookup('env', 'api_token') }}"
15
16    - name: Get device details from NetBox
17      uri:
18          url: "http://198.18.134.22:9000/api/dcim/devices/?name={{ device_name }}"
19          method: GET
20          return_content: yes
21          headers:
22              accept: "application/json"
23              Authorization: "Token {{ api_token }}"
24      register: device
25
26    - debug: var=device
27
28    - name: Extract primary IP address without netmask
29      set_fact:
30        primary_ip: "{{ device.json.results[0].primary_ip4.address.split('/')[0] }}"
31
32    - name: Debug primary IP address
33      debug:
34        msg: "Primary IP address: {{ primary_ip }}"
35
36    - name: Get intended state from NetBox based on device ID
37      uri:
38          url: "http://198.18.134.22:9000/api/dcim/devices/{{ device.json.results.0['id'] }}/render-config/"
39          method: POST
40          return_content: yes
41          headers:
42              accept: "application/json"
43              Authorization: "Token {{ api_token }}"
44      register: intended_config
45
46    - debug: var=intended_config
47
48    - name: Extract and format the config content
49      ansible.builtin.set_fact:
50        formatted_config: "{{ intended_config.json.content | replace('\\n', '\n') }}"
51
52    - name: Debug formatted config
53      debug:
54        var: formatted_config
55
56  #
57  # Project Creation
58  #
59    - name: Create dayN-Templates project 
60      cisco.dnac.configuration_template_project:
61        state: present
62        createTime: 0
63        description: string
64        name: Templates-by-Ansible
65      register: configuration_template_project_result
66
67    - name: Set Task ID
68      ansible.builtin.set_fact:
69        project_task_id: "{{ configuration_template_project_result.dnac_response.response.taskId }}"
70
71    - debug: var=project_task_id
72
73    - name: Pause
74      pause:
75        seconds: 5
76
77    - name: Get project task by id
78      cisco.dnac.task_info:
79        taskId: "{{ project_task_id }}"
80      register: result_project_task_id
81
82    - debug: var=result_project_task_id
83
84    - name: Set project ID
85      ansible.builtin.set_fact:
86        project_id: "{{ result_project_task_id.dnac_response.response.data }}"
87
88    - debug: var=project_id
89  #
90  # Template Info / Config Section
91  #
92    - name: Create an configuration_template_project
93      cisco.dnac.configuration_template_create:
94        name: "{{ device_name }}_{{ tstamp.stdout }}"
95        templateContent: '{{ formatted_config }}'
96        language: "VELOCITY"
97        projectName: "Onboarding Configuration"
98        deviceTypes:
99          - productFamily: "Routers"
100        projectId: "{{ project_id }}"
101        softwareType: "IOS-XE"
102        softwareVariant: "XE"
103      register: configuration_template_project_result
104
105    - debug: var=configuration_template_project_result
106
107    - name: Set Task ID
108      ansible.builtin.set_fact:
109        task_id: "{{ configuration_template_project_result.dnac_response.response.taskId }}"
110
111    - debug: var=task_id
112
113    - name: Pause
114      pause:
115        seconds: 30
116
117    - name: Get Task by id
118      cisco.dnac.task_info:
119        taskId: "{{ task_id }}"
120      register: result_task_id
121
122    - name: Set Template ID
123      ansible.builtin.set_fact:
124        template_id: "{{ (result_task_id.dnac_response.response.data | from_json).templateId }}"
125
126    - debug: var=template_id
127     
128    - name: Create Versioning
129      cisco.dnac.configuration_template_version_create:
130        comments: "COMMITTED"
131        templateId: "{{ template_id }}"
132      register: template_version_result
133
134  #
135  # Get the device infos
136  #
137    - name: Get Network Device information
138      cisco.dnac.network_device_info:
139        managementIpAddress: [ "{{ primary_ip }}" ]
140      register: network_device_info_result
141
142    - debug: var=network_device_info_result
143
144    - name: Set Device ID
145      ansible.builtin.set_fact:
146        device_id: "{{ network_device_info_result.dnac_response.response[0].id }}"
147
148    - debug: var=device_id
149
150  #
151  # Deploy template to device
152  #
153    - name: Deploy dayN template on device
154      cisco.dnac.configuration_template_deploy:
155        forcePushTemplate: true
156        mainTemplateId: "{{ template_id }}"
157        targetInfo:
158        - id: "{{ device_id }}"
159          type: "MANAGED_DEVICE_UUID"
160        templateId: "{{ template_id }}"
161      register: template_deploy_result
162
163    - debug: var=template_deploy_result
03_deploy-template.yml

Here is a breakdown of the playbook:

  • Hosts and Execution Scope: Runs on dnac_servers with a local connection and no fact gathering.
  • Get Timestamp: Retrieves the current timestamp to use in template naming.
  • Set API Token: Stores api_token by retrieving it from environment variables.
  • Fetch Device Details: Queries NetBox for details of the catalyst_switch.
  • Debug Device Data: Outputs the retrieved device details.
  • Retrieve Intended Configuration: Requests NetBox to generate the intended configuration using the device ID.
  • Debug Intended Configuration: Displays the retrieved intended state.
  • Format Configuration: Replaces newline escape sequences in the intended configuration for proper formatting.
  • Debug Formatted Configuration: Prints the formatted configuration.

Project Creation

  • Create Project: Creates a Cisco DNA Center (DNAC) configuration template project named Templates-by-Ansible.
  • Set Task ID: Extracts and stores the project creation task ID.
  • Pause Execution: Waits 5 seconds for the project creation process to complete.
  • Retrieve Project Task Status: Checks the status of the project creation task using the stored task ID.
  • Set Project ID: Extracts and stores the project ID from the task result.

Template Creation

  • Create Configuration Template: Defines a DNAC configuration template using formatted_config, assigns it to the Onboarding Configuration project, and associates it with IOS-XE devices.
  • Set Task ID: Stores the task ID for tracking the template creation process.
  • Pause Execution: Waits 30 seconds for template creation.
  • Retrieve Task Status: Checks the status of the template creation task.
  • Set Template ID: Extracts and stores the created template ID.
  • Create Template Versioning: Commits the created template in DNAC.

Device Information Retrieval

  • Get Device Information: Queries DNAC for details of the network device with IP 10.100.80.11.
  • Set Device ID: Extracts and stores the device ID from the DNAC response.

Template Deployment

  • Deploy Configuration Template: Applies the created DNAC template to the device, forcing the push to the specified device_id.
  • Debug Deployment Status: Outputs the result of the template deployment process.

Step 2: Nexus Dashboard

Create the file 02_deploy-template.yml in ndfc/playbooks:

02_deploy-template.yml
yaml
POD01
1---
2- hosts: ndfc_servers
3  gather_facts: false
4  tasks:
5    - name: Set sentitive variables
6      ansible.builtin.set_fact:
7        api_token: "{{ lookup('env', 'api_token') }}"
8
9    - name: Get device details from NetBox
10      uri:
11          url: "http://198.18.134.22:9000/api/dcim/devices/?name={{ nexus_switch }}"
12          method: GET
13          return_content: yes
14          headers:
15              accept: "application/json"
16              Authorization: "Token {{ api_token }}"
17      register: device
18
19    - debug: var=device
20
21    - name: Extract primary IP address without netmask
22      set_fact:
23        primary_ip: "{{ device.json.results[0].primary_ip4.address.split('/')[0] }}"
24
25    - name: Debug primary IP address
26      debug:
27        msg: "Primary IP address: {{ primary_ip }}"
28
29    - name: Get intended state from NetBox based on device ID
30      uri:
31          url: "http://198.18.134.22:9000/api/dcim/devices/{{ device.json.results.0['id'] }}/render-config/"
32          method: POST
33          return_content: yes
34          headers:
35              accept: "application/json"
36              Authorization: "Token {{ api_token }}"
37      register: intended_config
38
39    - debug: var=intended_config
40
41    - name: Extract and format the config content
42      ansible.builtin.set_fact:
43        formatted_config: "{{ intended_config.json.content | replace('\\n', '\n') }}"
44
45    - name: Debug formatted config
46      debug:
47        var: formatted_config
48
49    - name: Create policy including required variables
50      cisco.dcnm.dcnm_policy:
51        fabric: "{{ fabric_name }}"
52        state: merged
53        deploy: true
54        config:
55          - name: switch_freeform
56            create_additional_policy: true
57            priority: 400
58            description: ANSIBLE_DEPLOYMENT
59            policy_vars: 
60              CONF: "{{ formatted_config }}"
61          - switch:
62              - ip: "{{ primary_ip }}"

Here is a breakdown of what each part does:

  • Hosts and Execution Scope: Runs on ndfc_servers without gathering system facts.
  • Set API Token: Stores api_token by retrieving it from environment variables.
  • Fetch Device Details from NetBox: Queries the NetBox API to get device details based on the nexus_switch variable.
  • Debug Device Data: Prints the retrieved device details for verification.
  • Retrieve Intended Configuration: Requests NetBox to generate the intended device configuration using the device ID.
  • Debug Intended Configuration: Displays the retrieved intended state for validation.
  • Format Configuration: Processes the intended configuration by replacing escaped newlines with actual newlines.
  • Debug Formatted Configuration: Outputs the formatted configuration for troubleshooting.
  • Create and Deploy Policy: Uses the cisco.dcnm.dcnm_policy module to apply the formatted configuration to the target switch (10.100.80.10) within the specified fabric_name. The policy is merged and deployed automatically.

Step 3: SCC deployment

Create the file 01_configure-ftd.yml in scc/playbooks:

01_configure-ftd.yml
yaml
POD01
1- name: Retrieve Device and update physical interfaces configuration, add static routes, and deploy configuration
2  hosts: localhost
3  gather_facts: no
4  vars:
5    fmc_url: "https://cisco-cbeye--sc6nui.app.eu.cdo.cisco.com"
6    domain_uuid: "e276abec-e0f2-11e3-8169-6d9ed49b625f"
7    api_token: "{{ lookup('env', 'api_token') }}"
8  tasks:
9    - name: Get list of devices from FMC
10      uri:
11        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/devices/devicerecords?offset=0&limit=50"
12        method: GET
13        headers:
14          Authorization: "Bearer {{ api_token }}"
15          Accept: "application/json"
16        return_content: yes
17        validate_certs: no
18      register: fmc_devices
19
20    - name: Extract device id using jq
21      shell: |
22        echo '{{ fmc_devices.content }}' | jq -r '.items | select(.name=="{{ target_pod }}") | .id'
23      register: device_id_output
24      changed_when: false
25
26    - name: Debug device id
27      debug:
28        msg: "Device id for {{ target_pod }} is {{ device_id_output.stdout }}"
29
30    - name: Get physical interfaces for the device (container UUID)
31
32      uri:
33        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/devices/devicerecords/{{ device_id_output.stdout }}/physicalinterfaces"
34        method: GET
35        headers:
36          Authorization: "Bearer {{ api_token }}"
37          Accept: "application/json"
38        return_content: yes
39        validate_certs: no
40      register: phys_int_resp
41
42    - name: Extract GigabitEthernet0/0 id using jq
43      shell: |
44        echo '{{ phys_int_resp.content }}' | jq -r '.items | select(.name=="GigabitEthernet0/0") | .id'
45      register: gig0_0_id
46      changed_when: false
47
48    - name: Extract GigabitEthernet0/1 id using jq
49      shell: |
50        echo '{{ phys_int_resp.content }}' | jq -r '.items | select(.name=="GigabitEthernet0/1") | .id'
51      register: gig0_1_id
52      changed_when: false
53
54    - name: Debug physical interface ids
55      debug:
56        msg:
57          - "GigabitEthernet0/0 id: {{ gig0_0_id.stdout }}"
58          - "GigabitEthernet0/1 id: {{ gig0_1_id.stdout }}"
59
60    - name: Build interface list for update
61      set_fact:
62        interfaces_to_update:
63          - name: "GigabitEthernet0/0"
64            ifname: "Datacenter"
65            id: "{{ gig0_0_id.stdout }}"
66            ip_address: "172.16.1.2"
67            ip_netmask: "30"
68          - name: "GigabitEthernet0/1"
69            ifname: "Branch"
70            id: "{{ gig0_1_id.stdout }}"
71            ip_address: "172.16.0.2"
72            ip_netmask: "30"
73
74    - name: Update configuration of each physical interface via PUT
75      uri:
76        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/devices/devicerecords/{{ device_id_output.stdout }}/physicalinterfaces/{{ item.id }}"
77        method: PUT
78        headers:
79          Authorization: "Bearer {{ api_token }}"
80          Accept: "application/json"
81          Content-Type: "application/json"
82        body: "{{ {
83          'type': 'PhysicalInterface',
84          'nveOnly': false,
85          'hardware': { 'speed': 'AUTO', 'duplex': 'AUTO' },
86          'mode': 'NONE',
87          'id': item.id,
88          'MTU': 1500,
89          'enabled': true,
90          'name': item.name,
91          'ifname': item.ifname,
92          'managementOnly': false,
93          'ipv4': { 'static': { 'address': item.ip_address, 'netmask': item.ip_netmask } }
94        } | to_json }}"
95        status_code: 200
96        return_content: yes
97        validate_certs: no
98      loop: "{{ interfaces_to_update }}"
99      register: interface_update_results
100
101    - name: Show update results for all interfaces
102      debug:
103        var: interface_update_results
104
105    - name: Add static route for interface "Branch"
106      uri:
107        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/devices/devicerecords/{{ device_id_output.stdout }}/routing/ipv4staticroutes"
108        method: POST
109        headers:
110          Authorization: "Bearer {{ api_token }}"
111          Accept: "application/json"
112          Content-Type: "application/json"
113        body: |
114          {
115            "interfaceName": "Branch",
116            "selectedNetworks": [
117              {
118                "type": "Network",
119                "overridable": false,
120                "id": "06E6F43C-465D-0ed3-0000-004294985130",
121                "name": "Branch"
122              }
123            ],
124            "gateway": {
125              "object": {
126                "type": "Host",
127                "overridable": false,
128                "id": "06E6F43C-465D-0ed3-0000-004294985170",
129                "name": "branch-catalyst-host"
130              }
131            },
132            "metricValue": 1,
133            "type": "IPv4StaticRoute",
134            "isTunneled": false
135          }
136        status_code: 201
137        return_content: yes
138        validate_certs: no
139      register: static_route_branch
140      ignore_errors: true
141
142    - name: Show static route creation result for Branch
143      debug:
144        msg: "Static route for Branch created: {{ static_route_branch.content }}"
145
146    - name: Add static route for interface "Datacenter"
147      uri:
148        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/devices/devicerecords/{{ device_id_output.stdout }}/routing/ipv4staticroutes"
149        method: POST
150        headers:
151          Authorization: "Bearer {{ api_token }}"
152          Accept: "application/json"
153          Content-Type: "application/json"
154        body: |
155          {
156            "interfaceName": "Datacenter",
157            "selectedNetworks": [
158              {
159                "type": "Network",
160                "overridable": false,
161                "id": "06E6F43C-465D-0ed3-0000-004294985039",
162                "name": "Datacenter"
163              }
164            ],
165            "gateway": {
166              "object": {
167                "type": "Host",
168                "overridable": false,
169                "id": "06E6F43C-465D-0ed3-0000-004294985085",
170                "name": "datacenter-nexus-host"
171              }
172            },
173            "metricValue": 1,
174            "type": "IPv4StaticRoute",
175            "isTunneled": false,
176            "id": "06E6F43C-465D-0ed3-0000-004294985195"
177          }
178        status_code: 201
179        return_content: yes
180        validate_certs: no
181      register: static_route_datacenter
182      ignore_errors: true
183
184    - name: Show static route creation result for Datacenter
185      debug:
186        msg: "Static route for Datacenter created: {{ static_route_datacenter.content }}"
187
188    - name: Get current UTC timestamp in epoch (milliseconds)
189
190      shell: "date -u +%s%3N"
191      register: epoch_timestamp_ms
192      changed_when: false
193
194    - name: Store UTC epoch timestamp as a variable
195      set_fact:
196        epoch_timestamp_ms: "{{ epoch_timestamp_ms.stdout | int }}"
197
198    - name: Sleep for 20 seconds before deployment
199      pause:
200        seconds: 20
201
202    - name: Deploy configuration
203      uri:
204        url: "{{ fmc_url }}/api/fmc_config/v1/domain/{{ domain_uuid }}/deployment/deploymentrequests"
205        method: POST
206        headers:
207          Authorization: "Bearer {{ api_token }}"
208          Accept: "application/json"
209          Content-Type: "application/json"
210        body: "{{ {
211          'type': 'DeploymentRequest',
212          'version': epoch_timestamp_ms,
213          'forceDeploy': true,
214          'ignoreWarning': true,
215          'deviceList': [ device_id_output.stdout ],
216          'deploymentNote': 'GITLAB_PUSH'
217        } | to_json }}"
218        status_code: 202
219        return_content: yes
220        validate_certs: no
221      register: deployment_result
222
223    - name: Show deployment result
224      debug:
225        msg: "Deployment result: {{ deployment_result.content }}"

This Ansible playbook interacts with the Cloud-Delivered Firepower Management Center (cdFMC) to configure a security device by retrieving its details, updating physical interfaces, adding static routes, and deploying the configuration.

Retrieve Device Information:

  • Queries FMC to get a list of managed devices.
  • Extracts the device ID of the target device (pod01).
  • Retrieve and Configure Physical Interfaces:
  • Fetches the physical interfaces of the target device.
  • Extracts interface IDs for GigabitEthernet0/0 and GigabitEthernet0/1.
  • Updates the interfaces with new IP addresses, netmasks, and settings.

Add Static Routes:

  • Adds a static route for the Branch interface to reach a specific network via a predefined gateway.
  • Adds a static route for the Datacenter interface to ensure traffic is properly routed.

Deploy Configuration:

  • Sends a deployment request to FMC to apply all the changes to the target device.

Debugging and Validation:

  • Prints retrieved device details, interface updates, static route creation responses, and the final deployment result.

Step 4: Lets put everything together

As usual 😉, update the pipeline file:

.gitlab-ci.yml

ATTENTION! If you are assigned to group 2, please make sure that you name your firewall target_pod=pod01-sgn This needs to be replaced in the pipeline file as an extra variable when you call the playbook.

yaml
POD01
1stages:
2  - deploy_config_dnac
3  - deploy_config_ndfc
4  - deploy_config_scc
5
6deploy_config_dnac:
7  stage: deploy_config_dnac
8  tags:
9    - docker-runner
10  image: cbeye592/ltrato-2600:dnac
11  id_tokens:
12    VAULT_ID_TOKEN:
13      aud: https://198.18.133.99:8200
14  secrets:
15    DNAC_HOST:
16      vault: DNAC/DNAC_HOST@pod01
17      file: false
18      token: $VAULT_ID_TOKEN
19    DNAC_VERIFY:
20      vault: DNAC/DNAC_VERIFY@pod01
21      file: false
22      token: $VAULT_ID_TOKEN
23    DNAC_USERNAME:
24      vault: DNAC/DNAC_USERNAME@pod01
25      file: false
26      token: $VAULT_ID_TOKEN
27    DNAC_PASSWORD:
28      vault: DNAC/DNAC_PASSWORD@pod01
29      file: false
30      token: $VAULT_ID_TOKEN
31    api_token:
32      vault: NETBOX/api_token@pod01
33      file: false
34      token: $VAULT_ID_TOKEN
35  before_script:
36    - source /root/ansible/bin/activate
37    - chmod -R 700 dnac
38    - cd dnac
39  script:
40    - ansible-playbook -i hosts playbooks/03_deploy-template.yml --extra-vars "device_name=POD01-CAT8KV-01"
41
42deploy_config_ndfc:
43  stage: deploy_config_ndfc
44  tags:
45    - docker-runner
46  image: cbeye592/ltrato-2600:ndfc
47  id_tokens:
48    VAULT_ID_TOKEN:
49      aud: https://198.18.133.99:8200
50  secrets:
51    ansible_user:
52      vault: NDFC/ansible_user@pod01
53      file: false
54      token: $VAULT_ID_TOKEN
55    ansible_password:
56      vault: NDFC/ansible_password@pod01
57      file: false
58      token: $VAULT_ID_TOKEN
59    DEVICE_USER:
60      vault: NDFC/DEVICE_USER@pod01
61      file: false
62      token: $VAULT_ID_TOKEN
63    DEVICE_PASSWORD:
64      vault: NDFC/DEVICE_PASSWORD@pod01
65      file: false
66      token: $VAULT_ID_TOKEN
67    api_token:
68      vault: NETBOX/api_token@pod01
69      file: false
70      token: $VAULT_ID_TOKEN
71  before_script:
72    - source /root/ansible/bin/activate
73    - chmod -R 700 ndfc
74    - cd ndfc
75    - echo "" >> hosts
76    - echo "ansible_user=$ansible_user" >> hosts
77    - echo "ansible_password=$ansible_password" >> hosts
78  script:
79    - "sed -i 's/\"username\": \"\",/\"username\": \"'${DEVICE_USER}'\",/g' data/01_add-devices.yaml"
80    - "sed -i 's/\"password\": \"\",/\"password\": \"'${DEVICE_PASSWORD}'\",/g' data/01_add-devices.yaml"
81    - ansible-playbook playbooks/02_deploy-template.yml --extra-vars "nexus_switch=POD01-N9KV-01" --extra-vars "fabric_name=pod01"
82
83configure_ftd_devices:
84  stage: deploy_config_scc
85  tags:
86    - docker-runner
87  image: cbeye592/ltrato-2600:scc
88  id_tokens:
89    VAULT_ID_TOKEN:
90      aud: https://198.18.133.99:8200
91  secrets:
92    api_token:
93      vault: SCC/api_token@pod01
94      file: false
95      token: $VAULT_ID_TOKEN
96    cdo_host:
97      vault: SCC/cdo_host@pod01
98      file: false
99      token: $VAULT_ID_TOKEN
100  before_script:
101    - source /root/ansible/bin/activate
102    - chmod -R 700 ndfc
103    - cd scc
104  script:
105    - ansible-playbook playbooks/01_configure-ftd.yml --extra-vars "target_pod=pod01-apjc"

Validation

Catalyst Center

Check the created templates in Catalyst Center:

NDFC

Validate the create templates:

SCC

Check the configuration in SCC:

Until the config is completely pushed to the devices, it can take up to 5-10 minutes. 

CML validation

Login to CML, open the console of datacenter-client-01, and run the following commands:

bash
POD01
ping 10.0.0.10
traceroute 10.0.0.10