Browse code

iosxr_config crash if config has route-policy with multiple levels of… (#41490)

* iosxr_config crash if config has route-policy with multiple levels of 'ifelseif' and other caveats (#41091)

* diff in as-path-set or prefix-set

* fix caveat diff can not have last line with comma in prefix-set/as-path/community-set

* Simplify fix to include indentation before parse

* remove debugger

* route-policy diffs

* fix iosxr_config crash issue

* new changes in iosxr_config after git add

* end-policy-map and end-class-map are properly indented so match misplaced children only when end-* is at the beigining also fix pep8

* Remaining config blocks of route-policy which needs exclusion from diff. added new tests

* pylint/pep8 warnings

* Review comments , sanity test fix

* shbang warning

* remove unused import

(cherry picked from commit 2db6a8c26ac39f91e1c8e08abea53581fae2b957)

* changelog entry

* sanity fix

Deepak Agrawal authored on 2018/06/14 06:43:18
Showing 6 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1,5 @@
0
+---
1
+bugfixes:
2
+- Fix iosxr_config module to handle route-policy, community-set, prefix-set,
3
+  as-path-set and rd-set blocks. All these blocks are part of route-policy
4
+  language of iosxr.
... ...
@@ -186,7 +186,38 @@ from ansible.module_utils.network.common.config import NetworkConfig, dumps
186 186
 DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config'
187 187
 
188 188
 CONFIG_MISPLACED_CHILDREN = [
189
-    re.compile(r'end-\s*(.+)$')
189
+    re.compile(r'^end-\s*(.+)$')
190
+]
191
+
192
+# Objects defined in Route-policy Language guide of IOS_XR.
193
+# Reconfiguring these objects replace existing configurations.
194
+# Hence these objects should be played direcly from candidate
195
+# configurations
196
+CONFIG_BLOCKS_FORCED_IN_DIFF = [
197
+    {
198
+        'start': re.compile(r'route-policy'),
199
+        'end': re.compile(r'end-policy')
200
+    },
201
+    {
202
+        'start': re.compile(r'prefix-set'),
203
+        'end': re.compile(r'end-set')
204
+    },
205
+    {
206
+        'start': re.compile(r'as-path-set'),
207
+        'end': re.compile(r'end-set')
208
+    },
209
+    {
210
+        'start': re.compile(r'community-set'),
211
+        'end': re.compile(r'end-set')
212
+    },
213
+    {
214
+        'start': re.compile(r'rd-set'),
215
+        'end': re.compile(r'end-set')
216
+    },
217
+    {
218
+        'start': re.compile(r'extcommunity-set'),
219
+        'end': re.compile(r'end-set')
220
+    }
190 221
 ]
191 222
 
192 223
 
... ...
@@ -214,46 +245,93 @@ def check_args(module, warnings):
214 214
                         'removed in the future')
215 215
 
216 216
 
217
+# A list of commands like {end-set, end-policy, ...} are part of configuration
218
+# block like { prefix-set, as-path-set , ... } but they are not indented properly
219
+# to be included with their parent. sanitize_config will add indentation to
220
+# end-* commands so they are included with their parents
221
+def sanitize_config(config, force_diff_prefix=None):
222
+    conf_lines = config.split('\n')
223
+    for regex in CONFIG_MISPLACED_CHILDREN:
224
+        for index, line in enumerate(conf_lines):
225
+            m = regex.search(line)
226
+            if m and m.group(0):
227
+                if force_diff_prefix:
228
+                    conf_lines[index] = '  ' + m.group(0) + force_diff_prefix
229
+                else:
230
+                    conf_lines[index] = '  ' + m.group(0)
231
+    conf = ('\n').join(conf_lines)
232
+    return conf
233
+
234
+
235
+def mask_config_blocks_from_diff(config, candidate, force_diff_prefix):
236
+    conf_lines = config.split('\n')
237
+    candidate_lines = candidate.split('\n')
238
+
239
+    for regex in CONFIG_BLOCKS_FORCED_IN_DIFF:
240
+        block_index_start_end = []
241
+        for index, line in enumerate(candidate_lines):
242
+            startre = regex['start'].search(line)
243
+            if startre and startre.group(0):
244
+                start_index = index
245
+            else:
246
+                endre = regex['end'].search(line)
247
+                if endre and endre.group(0):
248
+                    end_index = index
249
+                    new_block = True
250
+                    for prev_start, prev_end in block_index_start_end:
251
+                        if start_index == prev_start:
252
+                            # This might be end-set of another regex
253
+                            # otherwise we would be having new start
254
+                            new_block = False
255
+                            break
256
+                    if new_block:
257
+                        block_index_start_end.append((start_index, end_index))
258
+
259
+        for start, end in block_index_start_end:
260
+            diff = False
261
+            if candidate_lines[start] in conf_lines:
262
+                run_conf_start_index = conf_lines.index(candidate_lines[start])
263
+            else:
264
+                diff = False
265
+                continue
266
+            for i in range(start, end + 1):
267
+                if conf_lines[run_conf_start_index] == candidate_lines[i]:
268
+                    run_conf_start_index = run_conf_start_index + 1
269
+                else:
270
+                    diff = True
271
+                    break
272
+            if diff:
273
+                run_conf_start_index = conf_lines.index(candidate_lines[start])
274
+                for i in range(start, end + 1):
275
+                    conf_lines[run_conf_start_index] = conf_lines[run_conf_start_index] + force_diff_prefix
276
+                    run_conf_start_index = run_conf_start_index + 1
277
+
278
+    conf = ('\n').join(conf_lines)
279
+    return conf
280
+
281
+
217 282
 def get_running_config(module):
218 283
     contents = module.params['config']
219 284
     if not contents:
220 285
         contents = get_config(module)
286
+    if module.params['src']:
287
+        contents = mask_config_blocks_from_diff(contents, module.params['src'], "ansible")
288
+        contents = sanitize_config(contents)
221 289
     return NetworkConfig(indent=1, contents=contents)
222 290
 
223 291
 
224 292
 def get_candidate(module):
225 293
     candidate = NetworkConfig(indent=1)
226 294
     if module.params['src']:
227
-        candidate.load(module.params['src'])
295
+        config = module.params['src']
296
+        config = sanitize_config(config)
297
+        candidate.load(config)
228 298
     elif module.params['lines']:
229 299
         parents = module.params['parents'] or list()
230 300
         candidate.add(module.params['lines'], parents=parents)
231 301
     return candidate
232 302
 
233 303
 
234
-def sanitize_candidate_config(config):
235
-    last_parents = None
236
-    for regex in CONFIG_MISPLACED_CHILDREN:
237
-        for index, line in enumerate(config):
238
-            if line._parents:
239
-                last_parents = line._parents
240
-            m = regex.search(line.text)
241
-            if m and m.group(0):
242
-                config[index]._parents = last_parents
243
-
244
-
245
-def sanitize_running_config(config):
246
-    last_parents = None
247
-    for regex in CONFIG_MISPLACED_CHILDREN:
248
-        for index, line in enumerate(config):
249
-            if line._parents:
250
-                last_parents = line._parents
251
-            m = regex.search(line.text)
252
-            if m and m.group(0):
253
-                config[index].text = ' ' + m.group(0)
254
-                config[index]._parents = last_parents
255
-
256
-
257 304
 def run(module, result):
258 305
     match = module.params['match']
259 306
     replace = module.params['replace']
... ...
@@ -266,9 +344,6 @@ def run(module, result):
266 266
     candidate_config = get_candidate(module)
267 267
     running_config = get_running_config(module)
268 268
 
269
-    sanitize_candidate_config(candidate_config.items)
270
-    sanitize_running_config(running_config.items)
271
-
272 269
     commands = None
273 270
     if match != 'none' and replace != 'config':
274 271
         commands = candidate_config.difference(running_config, path=path, match=match, replace=replace)
275 272
new file mode 100644
... ...
@@ -0,0 +1,121 @@
0
+router ospf 1
1
+  area 0
2
+!
3
+prefix-set EBGP-PEER-BOGONS
4
+  0.0.0.0/0,
5
+  0.0.0.0/8 le 32,
6
+  10.0.0.0/8 le 32,
7
+  127.0.0.0/8 le 32,
8
+  169.254.0.0/16 le 32,
9
+  172.16.0.0/12 le 32,
10
+  192.0.0.0/24 le 32,
11
+  192.0.2.0/24 le 32,
12
+  192.168.0.0/16 le 32,
13
+  198.18.0.0/15 le 32,
14
+  224.0.0.0/4 le 32,
15
+  240.0.0.0/4 le 32
16
+end-set
17
+!
18
+prefix-set cust-ddos-DDOS
19
+end-set
20
+!
21
+prefix-set cust-no-export
22
+end-set
23
+!
24
+prefix-set acme_DC_Internal
25
+  137.1.0.0/16,
26
+  137.1.16.0/24,
27
+  137.1.18.0/24,
28
+  137.1.20.0/24,
29
+  137.1.22.0/24,
30
+  137.1.23.0/24,
31
+  137.1.24.0/24,
32
+  137.1.29.0/24,
33
+  137.1.30.0/24,
34
+  137.1.31.0/24,
35
+  137.1.32.0/21,
36
+  137.1.40.0/22,
37
+  209.1.0.0/16
38
+end-set
39
+!
40
+as-path-set EBGP-PEER-AS16509-403-PERMIT-PATHS
41
+  ios-regex '^11164_8075_',
42
+  ios-regex '^11164_16509$',
43
+  ios-regex '^1116_16509_[0-9]+$',
44
+  ios-regex '^8075_',
45
+  ios-regex '^16509$',
46
+  ios-regex '^16509_[0-9]+$'
47
+end-set
48
+!
49
+community-set cust-announce
50
+  1525:65298,
51
+  1525:65436,
52
+  1525:65438,
53
+  1525:65439,
54
+  1525:65498,
55
+  1525:65511,
56
+  1523:65418,
57
+  1523:65436,
58
+  1523:65438
59
+end-set
60
+!
61
+community-set cust-no-export
62
+  1525:65439,
63
+  1525:65511,
64
+  1525:65535
65
+end-set
66
+!
67
+
68
+route-policy POLICY2
69
+end-policy
70
+!
71
+route-policy cust2bgp
72
+  set origin igp
73
+  set next-hop 137.1.16.12
74
+end-policy
75
+!
76
+rd-set ebpg-1
77
+end-set
78
+!
79
+rd-set EBGP_INCOMING_RD_SET
80
+  172.16.0.0/16:*,
81
+  172.17.0.0/16:100,
82
+  192:*,
83
+  192:100
84
+end-set
85
+!
86
+extcommunity-set rt EBGP_INCOMIG_RT_SET
87
+  10:615,
88
+  10:6150,
89
+  15.15.15.15:15
90
+end-set
91
+!
92
+extcommunity-set rt ebpg-1
93
+end-set
94
+!
95
+route-policy static-to-bgp
96
+  if destination in cust-no-export then
97
+    apply cust2bgp
98
+    set community cust-no-export additive
99
+  elseif destination in cust-announce then
100
+    apply cust2bgp
101
+    set community cust-announce additive
102
+  elseif destination in cust-announce-backup then
103
+    apply cust2bgp
104
+    set local-preference 100
105
+    set weight 0
106
+    set community cust-announce additive
107
+  elseif destination in cust-no-export-backup then
108
+    apply cust2bgp
109
+    set local-preference 98
110
+    set weight 0
111
+    set community cust-no-export additive
112
+  else
113
+    drop
114
+  endif
115
+end-policy
116
+!
117
+class-map match-any data
118
+ match precedence ipv4 0 1
119
+ end-class-map
120
+!
0 121
new file mode 100644
... ...
@@ -0,0 +1,65 @@
0
+prefix-set EBGP-PEER-BOGONS
1
+  192.0.2.0/24 le 32,
2
+  192.168.0.0/16 le 32,
3
+  198.18.0.0/16 le 32,
4
+  224.0.0.0/4 le 32,
5
+  240.0.0.0/4 le 32
6
+end-set
7
+!
8
+as-path-set EBGP-PEER-AS16509-403-PERMIT-PATHS
9
+  ios-regex '^11164_8075_',
10
+  ios-regex '^1164_16509$',
11
+  ios-regex '^1116_16409_[0-9]+$',
12
+  ios-regex '^8075_'
13
+end-set
14
+!
15
+community-set cust-announce
16
+  1525:65298,
17
+  1525:6546,
18
+  1525:6438,
19
+  1525:65439,
20
+  1525:65498
21
+end-set
22
+!
23
+rd-set EBGP_INCOMING_RD_SET
24
+  172.16.0.0/16:*,
25
+  172.14.0.0/16:100,
26
+  192:*,
27
+  192:100
28
+end-set
29
+!
30
+extcommunity-set rt EBGP_INCOMIG_RT_SET
31
+  10:615,
32
+  10:6120,
33
+  15.15.15.15:15
34
+end-set
35
+!
36
+route-policy POLICY2
37
+end-policy
38
+!
39
+route-policy static-to-bgp
40
+  if destination in cust-no-export then
41
+    apply cust2bgp
42
+    set community cust-no-export additive
43
+  elseif destination in cust-announce then
44
+    apply cust2bgp
45
+    set community cust-announce additive
46
+  elseif destination in cust-announce-backup then
47
+    apply cust2bgp
48
+    set local-preference 100
49
+    set weight 23
50
+    set community cust-announce additive
51
+  elseif destination in cust-no-export-backup then
52
+    apply cust2bgp
53
+    set local-preference 98
54
+    set weight 0
55
+    set community cust-no-export additive
56
+  else
57
+    drop
58
+  endif
59
+end-policy
60
+!
61
+class-map match-any data
62
+ match precedence ipv4 0 1 2
63
+ end-class-map
64
+!
0 65
new file mode 100644
... ...
@@ -0,0 +1,32 @@
0
+no router ospf 1
1
+!
2
+no prefix-set EBGP-PEER-BOGONS
3
+!
4
+no prefix-set cust-ddos-DDOS
5
+!
6
+no prefix-set cust-no-export
7
+!          
8
+no prefix-set acme_DC_Internal
9
+!
10
+no as-path-set EBGP-PEER-AS16509-403-PERMIT-PATHS
11
+!
12
+no community-set cust-announce
13
+!
14
+no community-set cust-no-export
15
+!
16
+no rd-set ebpg-1
17
+!
18
+no rd-set EBGP_INCOMING_RD_SET
19
+!
20
+no extcommunity-set rt EBGP_INCOMIG_RT_SET
21
+!
22
+no extcommunity-set rt ebpg-1
23
+!
24
+no route-policy POLICY2
25
+!
26
+no route-policy cust2bgp
27
+!
28
+no route-policy static-to-bgp
29
+!
30
+no class-map match-any data
31
+!
0 32
new file mode 100644
... ...
@@ -0,0 +1,53 @@
0
+---
1
+- debug: msg="START cli/route_policy.yaml on connection={{ ansible_connection }}"
2
+
3
+- name: Cleanup
4
+  iosxr_config:
5
+    src: basic/route_policy_clean.j2
6
+
7
+- name: config setup route-policy/prefix-set/as-path-set/community-set
8
+  iosxr_config:
9
+    src: basic/route_policy.j2
10
+  register: result
11
+
12
+- assert:
13
+    that:
14
+      - "result.changed == true"
15
+
16
+- name: Configure same route-policy/prefix-set ... verify change=0
17
+  iosxr_config:
18
+    src: basic/route_policy.j2
19
+  register: result
20
+
21
+- assert:
22
+    that:
23
+      - "result.changed == false"
24
+
25
+- name: Do a change in multi-sublevel route-policy/prefix-set/community-set
26
+  iosxr_config:
27
+    src: basic/route_policy_change.j2
28
+  register: result
29
+
30
+- assert:
31
+    that:
32
+      - "result.changed == true"
33
+
34
+- name: Configure same route-policy/prefix-set ... verify change=0
35
+  iosxr_config:
36
+    src: basic/route_policy_change.j2
37
+  register: result
38
+
39
+- assert:
40
+    that:
41
+      - "result.changed == false"
42
+
43
+- name: Cleanup
44
+  iosxr_config:
45
+    src: basic/route_policy_clean.j2
46
+  register: result
47
+
48
+- assert:
49
+    that:
50
+      - "result.changed == true"
51
+
52
+- debug: msg="END cli/route_policy.yaml on connection={{ ansible_connection }}"