1""" 2Cisco IOS configuration manipulation helpers 3 4.. versionadded:: 2019.2.0 5 6This module provides a collection of helper functions for Cisco IOS style 7configuration manipulation. This module does not have external dependencies 8and can be used from any Proxy or regular Minion. 9""" 10 11import difflib 12 13import salt.utils.dictdiffer 14import salt.utils.dictupdate 15from salt.exceptions import SaltException 16 17# Import Salt modules 18from salt.utils.odict import OrderedDict 19 20# ------------------------------------------------------------------------------ 21# module properties 22# ------------------------------------------------------------------------------ 23 24__virtualname__ = "iosconfig" 25__proxyenabled__ = ["*"] 26 27# ------------------------------------------------------------------------------ 28# helper functions -- will not be exported 29# ------------------------------------------------------------------------------ 30 31 32def _attach_data_to_path(obj, ele, data): 33 if ele not in obj: 34 obj[ele] = OrderedDict() 35 obj[ele] = data 36 else: 37 obj[ele].update(data) 38 39 40def _attach_data_to_path_tags(obj, path, data, list_=False): 41 if "#list" not in obj: 42 obj["#list"] = [] 43 path = [path] 44 obj_tmp = obj 45 first = True 46 while True: 47 obj_tmp["#text"] = " ".join(path) 48 path_item = path.pop(0) 49 if not path: 50 break 51 else: 52 if path_item not in obj_tmp: 53 obj_tmp[path_item] = OrderedDict() 54 obj_tmp = obj_tmp[path_item] 55 56 if first and list_: 57 obj["#list"].append({path_item: obj_tmp}) 58 first = False 59 if path_item in obj_tmp: 60 obj_tmp[path_item].update(data) 61 else: 62 obj_tmp[path_item] = data 63 obj_tmp[path_item]["#standalone"] = True 64 65 66def _parse_text_config(config_lines, with_tags=False, current_indent=0, nested=False): 67 struct_cfg = OrderedDict() 68 while config_lines: 69 line = config_lines.pop(0) 70 if not line.strip() or line.lstrip().startswith("!"): 71 # empty or comment 72 continue 73 current_line = line.lstrip() 74 leading_spaces = len(line) - len(current_line) 75 if leading_spaces > current_indent: 76 current_block = _parse_text_config( 77 config_lines, 78 current_indent=leading_spaces, 79 with_tags=with_tags, 80 nested=True, 81 ) 82 if with_tags: 83 _attach_data_to_path_tags( 84 struct_cfg, current_line, current_block, nested 85 ) 86 else: 87 _attach_data_to_path(struct_cfg, current_line, current_block) 88 elif leading_spaces < current_indent: 89 config_lines.insert(0, line) 90 break 91 else: 92 if not nested: 93 current_block = _parse_text_config( 94 config_lines, 95 current_indent=leading_spaces, 96 with_tags=with_tags, 97 nested=True, 98 ) 99 if with_tags: 100 _attach_data_to_path_tags( 101 struct_cfg, current_line, current_block, nested 102 ) 103 else: 104 _attach_data_to_path(struct_cfg, current_line, current_block) 105 else: 106 config_lines.insert(0, line) 107 break 108 return struct_cfg 109 110 111def _get_diff_text(old, new): 112 """ 113 Returns the diff of two text blobs. 114 """ 115 diff = difflib.unified_diff(old.splitlines(1), new.splitlines(1)) 116 return "".join([x.replace("\r", "") for x in diff]) 117 118 119def _print_config_text(tree, indentation=0): 120 """ 121 Return the config as text from a config tree. 122 """ 123 config = "" 124 for key, value in tree.items(): 125 config += "{indent}{line}\n".format(indent=" " * indentation, line=key) 126 if value: 127 config += _print_config_text(value, indentation=indentation + 1) 128 return config 129 130 131# ------------------------------------------------------------------------------ 132# callable functions 133# ------------------------------------------------------------------------------ 134 135 136def tree(config=None, path=None, with_tags=False, saltenv="base"): 137 """ 138 Transform Cisco IOS style configuration to structured Python dictionary. 139 Depending on the value of the ``with_tags`` argument, this function may 140 provide different views, valuable in different situations. 141 142 config 143 The configuration sent as text. This argument is ignored when ``path`` 144 is configured. 145 146 path 147 Absolute or remote path from where to load the configuration text. This 148 argument allows any URI supported by 149 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 150 ``https://``, ``s3://``, ``ftp:/``, etc. 151 152 with_tags: ``False`` 153 Whether this function should return a detailed view, with tags. 154 155 saltenv: ``base`` 156 Salt fileserver environment from which to retrieve the file. 157 Ignored if ``path`` is not a ``salt://`` URL. 158 159 CLI Example: 160 161 .. code-block:: bash 162 163 salt '*' iosconfig.tree path=salt://path/to/my/config.txt 164 salt '*' iosconfig.tree path=https://bit.ly/2mAdq7z 165 """ 166 if path: 167 config = __salt__["cp.get_file_str"](path, saltenv=saltenv) 168 if config is False: 169 raise SaltException("{} is not available".format(path)) 170 config_lines = config.splitlines() 171 return _parse_text_config(config_lines, with_tags=with_tags) 172 173 174def clean(config=None, path=None, saltenv="base"): 175 """ 176 Return a clean version of the config, without any special signs (such as 177 ``!`` as an individual line) or empty lines, but just lines with significant 178 value in the configuration of the network device. 179 180 config 181 The configuration sent as text. This argument is ignored when ``path`` 182 is configured. 183 184 path 185 Absolute or remote path from where to load the configuration text. This 186 argument allows any URI supported by 187 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 188 ``https://``, ``s3://``, ``ftp:/``, etc. 189 190 saltenv: ``base`` 191 Salt fileserver environment from which to retrieve the file. 192 Ignored if ``path`` is not a ``salt://`` URL. 193 194 CLI Example: 195 196 .. code-block:: bash 197 198 salt '*' iosconfig.clean path=salt://path/to/my/config.txt 199 salt '*' iosconfig.clean path=https://bit.ly/2mAdq7z 200 """ 201 config_tree = tree(config=config, path=path, saltenv=saltenv) 202 return _print_config_text(config_tree) 203 204 205def merge_tree( 206 initial_config=None, 207 initial_path=None, 208 merge_config=None, 209 merge_path=None, 210 saltenv="base", 211): 212 """ 213 Return the merge tree of the ``initial_config`` with the ``merge_config``, 214 as a Python dictionary. 215 216 initial_config 217 The initial configuration sent as text. This argument is ignored when 218 ``initial_path`` is set. 219 220 initial_path 221 Absolute or remote path from where to load the initial configuration 222 text. This argument allows any URI supported by 223 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 224 ``https://``, ``s3://``, ``ftp:/``, etc. 225 226 merge_config 227 The config to be merged into the initial config, sent as text. This 228 argument is ignored when ``merge_path`` is set. 229 230 merge_path 231 Absolute or remote path from where to load the merge configuration 232 text. This argument allows any URI supported by 233 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 234 ``https://``, ``s3://``, ``ftp:/``, etc. 235 236 saltenv: ``base`` 237 Salt fileserver environment from which to retrieve the file. 238 Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL. 239 240 CLI Example: 241 242 .. code-block:: bash 243 244 salt '*' iosconfig.merge_tree initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg 245 """ 246 merge_tree = tree(config=merge_config, path=merge_path, saltenv=saltenv) 247 initial_tree = tree(config=initial_config, path=initial_path, saltenv=saltenv) 248 return salt.utils.dictupdate.merge(initial_tree, merge_tree) 249 250 251def merge_text( 252 initial_config=None, 253 initial_path=None, 254 merge_config=None, 255 merge_path=None, 256 saltenv="base", 257): 258 """ 259 Return the merge result of the ``initial_config`` with the ``merge_config``, 260 as plain text. 261 262 initial_config 263 The initial configuration sent as text. This argument is ignored when 264 ``initial_path`` is set. 265 266 initial_path 267 Absolute or remote path from where to load the initial configuration 268 text. This argument allows any URI supported by 269 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 270 ``https://``, ``s3://``, ``ftp:/``, etc. 271 272 merge_config 273 The config to be merged into the initial config, sent as text. This 274 argument is ignored when ``merge_path`` is set. 275 276 merge_path 277 Absolute or remote path from where to load the merge configuration 278 text. This argument allows any URI supported by 279 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 280 ``https://``, ``s3://``, ``ftp:/``, etc. 281 282 saltenv: ``base`` 283 Salt fileserver environment from which to retrieve the file. 284 Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL. 285 286 CLI Example: 287 288 .. code-block:: bash 289 290 salt '*' iosconfig.merge_text initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg 291 """ 292 candidate_tree = merge_tree( 293 initial_config=initial_config, 294 initial_path=initial_path, 295 merge_config=merge_config, 296 merge_path=merge_path, 297 saltenv=saltenv, 298 ) 299 return _print_config_text(candidate_tree) 300 301 302def merge_diff( 303 initial_config=None, 304 initial_path=None, 305 merge_config=None, 306 merge_path=None, 307 saltenv="base", 308): 309 """ 310 Return the merge diff, as text, after merging the merge config into the 311 initial config. 312 313 initial_config 314 The initial configuration sent as text. This argument is ignored when 315 ``initial_path`` is set. 316 317 initial_path 318 Absolute or remote path from where to load the initial configuration 319 text. This argument allows any URI supported by 320 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 321 ``https://``, ``s3://``, ``ftp:/``, etc. 322 323 merge_config 324 The config to be merged into the initial config, sent as text. This 325 argument is ignored when ``merge_path`` is set. 326 327 merge_path 328 Absolute or remote path from where to load the merge configuration 329 text. This argument allows any URI supported by 330 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 331 ``https://``, ``s3://``, ``ftp:/``, etc. 332 333 saltenv: ``base`` 334 Salt fileserver environment from which to retrieve the file. 335 Ignored if ``initial_path`` or ``merge_path`` is not a ``salt://`` URL. 336 337 CLI Example: 338 339 .. code-block:: bash 340 341 salt '*' iosconfig.merge_diff initial_path=salt://path/to/running.cfg merge_path=salt://path/to/merge.cfg 342 """ 343 if initial_path: 344 initial_config = __salt__["cp.get_file_str"](initial_path, saltenv=saltenv) 345 candidate_config = merge_text( 346 initial_config=initial_config, 347 merge_config=merge_config, 348 merge_path=merge_path, 349 saltenv=saltenv, 350 ) 351 clean_running_dict = tree(config=initial_config) 352 clean_running = _print_config_text(clean_running_dict) 353 return _get_diff_text(clean_running, candidate_config) 354 355 356def diff_tree( 357 candidate_config=None, 358 candidate_path=None, 359 running_config=None, 360 running_path=None, 361 saltenv="base", 362): 363 """ 364 Return the diff, as Python dictionary, between the candidate and the running 365 configuration. 366 367 candidate_config 368 The candidate configuration sent as text. This argument is ignored when 369 ``candidate_path`` is set. 370 371 candidate_path 372 Absolute or remote path from where to load the candidate configuration 373 text. This argument allows any URI supported by 374 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 375 ``https://``, ``s3://``, ``ftp:/``, etc. 376 377 running_config 378 The running configuration sent as text. This argument is ignored when 379 ``running_path`` is set. 380 381 running_path 382 Absolute or remote path from where to load the running configuration 383 text. This argument allows any URI supported by 384 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 385 ``https://``, ``s3://``, ``ftp:/``, etc. 386 387 saltenv: ``base`` 388 Salt fileserver environment from which to retrieve the file. 389 Ignored if ``candidate_path`` or ``running_path`` is not a 390 ``salt://`` URL. 391 392 CLI Example: 393 394 .. code-block:: bash 395 396 salt '*' iosconfig.diff_tree candidate_path=salt://path/to/candidate.cfg running_path=salt://path/to/running.cfg 397 """ 398 candidate_tree = tree(config=candidate_config, path=candidate_path, saltenv=saltenv) 399 running_tree = tree(config=running_config, path=running_path, saltenv=saltenv) 400 return salt.utils.dictdiffer.deep_diff(running_tree, candidate_tree) 401 402 403def diff_text( 404 candidate_config=None, 405 candidate_path=None, 406 running_config=None, 407 running_path=None, 408 saltenv="base", 409): 410 """ 411 Return the diff, as text, between the candidate and the running config. 412 413 candidate_config 414 The candidate configuration sent as text. This argument is ignored when 415 ``candidate_path`` is set. 416 417 candidate_path 418 Absolute or remote path from where to load the candidate configuration 419 text. This argument allows any URI supported by 420 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 421 ``https://``, ``s3://``, ``ftp:/``, etc. 422 423 running_config 424 The running configuration sent as text. This argument is ignored when 425 ``running_path`` is set. 426 427 running_path 428 Absolute or remote path from where to load the running configuration 429 text. This argument allows any URI supported by 430 :py:func:`cp.get_url <salt.modules.cp.get_url>`), e.g., ``salt://``, 431 ``https://``, ``s3://``, ``ftp:/``, etc. 432 433 saltenv: ``base`` 434 Salt fileserver environment from which to retrieve the file. 435 Ignored if ``candidate_path`` or ``running_path`` is not a 436 ``salt://`` URL. 437 438 CLI Example: 439 440 .. code-block:: bash 441 442 salt '*' iosconfig.diff_text candidate_path=salt://path/to/candidate.cfg running_path=salt://path/to/running.cfg 443 """ 444 candidate_text = clean( 445 config=candidate_config, path=candidate_path, saltenv=saltenv 446 ) 447 running_text = clean(config=running_config, path=running_path, saltenv=saltenv) 448 return _get_diff_text(running_text, candidate_text) 449