Skip to content

Commit

Permalink
Merge pull request #99 from aedwardstx/future_config
Browse files Browse the repository at this point in the history
Future config
  • Loading branch information
aedwardstx authored Nov 16, 2021
2 parents 94d10a7 + e16a096 commit dc554c3
Show file tree
Hide file tree
Showing 6 changed files with 210 additions and 4 deletions.
120 changes: 120 additions & 0 deletions docs/experimental-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,124 @@ Out[1]:
' + ip access-group TEST in',
' + no shutdown']
```
## Future Config
Starting in version 2.2.0, a featured called future config was introduced. It attempts to predict the running config after a change is applied.
This feature is useful in cases where you need to determine what the configuration state will be after a change is applied. Such as:
- Ensuring that a configuration change was applied successfully to a device.
- i.e. Does the post-change config match the predicted future config?
- Providing a future state config that can be fed into batfish, or similar, to predict if a change will cause an impact.
- Building rollback configs. If you have the future config state, then generating a rollback config can be done by simply building the remediation config in the reverse direction `rollback = future.config_to_get_to(running)`.
- If you are building rollbacks for a series of config changes, you can feed the post-change-1 future config into the process for determining the post-change-2 future config e.g.
```shell
post_change_1_config = running_config.future(change_1_config)
change_1_rollback_config = post_change_1_config.config_to_get_to(running_config)
post_change_2_config = post_change_1_config.future(change_2_config)
change_2_rollback_config = post_change_2_config.config_to_get_to(post_change_1_config)
...
```
In its current state, this algorithm does not consider:
- negate a numbered ACL when removing an item
- sectional exiting
- negate with
- idempotent command blacklist
- idempotent_acl_check
- and likely others
```bash
In [1]: from hier_config import HConfig, Host
...:
...:
...: host = Host("test.dfw1", "ios")
...: running_config = HConfig(host)
...: running_config.load_from_file("./tests/fixtures/running_config.conf")
...: remediation_config = HConfig(host)
...: remediation_config.load_from_file("./tests/fixtures/remediation_config_without_tags.conf")
...: future_config = running_config.future(remediation_config)
...:
...: print("\n##### running config")
...: for line in running_config.all_children():
...: print(line.cisco_style_text())
...:
...: print("\n##### remediation config")
...: for line in remediation_config.all_children():
...: print(line.cisco_style_text())
...:
...: print("\n##### future config")
...: for line in future_config.all_children():
...: print(line.cisco_style_text())
...:
##### running config
hostname aggr-example.rtr
ip access-list extended TEST
10 permit ip 10.0.0.0 0.0.0.7 any
vlan 2
name switch_mgmt_10.0.2.0/24
vlan 3
name switch_mgmt_10.0.4.0/24
interface Vlan2
descripton switch_10.0.2.0/24
ip address 10.0.2.1 255.255.255.0
shutdown
interface Vlan3
mtu 9000
description switch_mgmt_10.0.4.0/24
ip address 10.0.4.1 255.255.0.0
ip access-group TEST in
no shutdown
##### remediation config
vlan 3
name switch_mgmt_10.0.3.0/24
vlan 4
name switch_mgmt_10.0.4.0/24
interface Vlan2
mtu 9000
ip access-group TEST in
no shutdown
interface Vlan3
description switch_mgmt_10.0.3.0/24
ip address 10.0.3.1 255.255.0.0
interface Vlan4
mtu 9000
description switch_mgmt_10.0.4.0/24
ip address 10.0.4.1 255.255.0.0
ip access-group TEST in
no shutdown
##### future config
vlan 3
name switch_mgmt_10.0.3.0/24
vlan 4
name switch_mgmt_10.0.4.0/24
interface Vlan2
mtu 9000
ip access-group TEST in
descripton switch_10.0.2.0/24
ip address 10.0.2.1 255.255.255.0
interface Vlan3
description switch_mgmt_10.0.3.0/24
ip address 10.0.3.1 255.255.0.0
mtu 9000
ip address 10.0.4.1 255.255.0.0
ip access-group TEST in
no shutdown
interface Vlan4
mtu 9000
description switch_mgmt_10.0.4.0/24
ip address 10.0.4.1 255.255.0.0
ip access-group TEST in
no shutdown
hostname aggr-example.rtr
ip access-list extended TEST
10 permit ip 10.0.0.0 0.0.0.7 any
vlan 2
name switch_mgmt_10.0.2.0/24
```
51 changes: 51 additions & 0 deletions hier_config/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,57 @@ def unified_diff(self, target: Union[HConfig, HConfigChild]) -> Iterator[str]:
for c in target_child.all_children_sorted()
)

def _future(
self,
config: Union[HConfig, HConfigChild],
future_config: Union[HConfig, HConfigChild],
) -> None:
"""
The below cases still need to be accounted for:
- negate a numbered ACL when removing an item
- sectional exiting
- negate with
- idempotent command blacklist
- idempotent_acl_check
- and likely others
"""
negated_or_recursed = set()
for config_child in config.children:
# sectional_overwrite
if config_child.sectional_overwrite_check():
future_config.add_deep_copy_of(config_child)
# sectional_overwrite_no_negate
elif config_child.sectional_overwrite_no_negate_check():
future_config.add_deep_copy_of(config_child)
# Idempotent commands
elif self_child := config_child.idempotent_for(self.children):
future_config.add_deep_copy_of(config_child)
negated_or_recursed.add(self_child.text)
# config_child is already in self
elif self_child := self.get_child("equals", config_child.text):
future_child = future_config.add_shallow_copy_of(self_child)
# pylint: disable=protected-access
self_child._future(config_child, future_child)
negated_or_recursed.add(config_child.text)
# config_child is being negated
elif config_child.text.startswith(self._negation_prefix):
unnegated_command = config_child.text[len(self._negation_prefix) :]
if self.get_child("equals", unnegated_command):
negated_or_recursed.add(unnegated_command)
# Account for "no ..." commands in the running config
else:
future_config.add_shallow_copy_of(config_child)
# config_child is not in self and doesn't match a special case
else:
future_config.add_deep_copy_of(config_child)

for self_child in self.children:
# self_child matched an above special case and should be ignored
if self_child.text in negated_or_recursed:
continue
# self_child was not modified above and should be present in the future config
future_config.add_deep_copy_of(self_child)

def _with_tags(
self, tags: Set[str], new_instance: Union[HConfig, HConfigChild]
) -> Union[HConfig, HConfigChild]:
Expand Down
10 changes: 7 additions & 3 deletions hier_config/child.py
Original file line number Diff line number Diff line change
Expand Up @@ -272,13 +272,17 @@ def is_idempotent_command(self, other_children: Iterable[HConfigChild]) -> bool:
return True

# Idempotent command identification
return bool(self.idempotent_for(other_children))

def idempotent_for(
self, other_children: Iterable[HConfigChild]
) -> Optional[HConfigChild]:
for rule in self.options["idempotent_commands"]:
if self.lineage_test(rule, True):
for other_child in other_children:
if other_child.lineage_test(rule, True):
return True

return False
return other_child
return None

def sectional_overwrite_no_negate_check(self) -> bool:
"""
Expand Down
12 changes: 12 additions & 0 deletions hier_config/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,18 @@ def add_sectional_exiting(self) -> None:
exit_line.tags = child.tags
exit_line.order_weight = 999

def future(self, config: HConfig) -> HConfig:
"""
EXPERIMENTAL - predict the future config after config is applied to self
The quality of the this method's output will in part depend on how well
the OS options are tuned. Ensuring that idempotency rules are accurate is
especially important.
"""
future_config = HConfig(host=self.host)
self._future(config, future_config)
return future_config

def with_tags(self, tags: Set[str]) -> HConfig:
"""
Returns a new instance containing only sub-objects
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "hier-config"
version = "2.1.0"
version = "2.2.0"
description = "A network configuration comparison tool, used to build remediation configurations."
packages = [
{ include="hier_config", from="."},
Expand Down
19 changes: 19 additions & 0 deletions tests/test_hier_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,25 @@ def test_line_inclusion_test(self):
def test_lineage_test(self):
pass

def test_future_config(self):
running_config = HConfig(host=self.host_a)
running_config.add_children_deep(["a", "aa", "aaa", "aaaa"])
running_config.add_children_deep(["a", "ab", "aba", "abaa"])
config = HConfig(host=self.host_a)
config.add_children_deep(["a", "ac"])
config.add_children_deep(["a", "no ab"])
config.add_children_deep(["a", "no az"])

future_config = running_config.future(config)
assert list(c.cisco_style_text() for c in future_config.all_children()) == [
"a",
" ac", # config lines are added first
" no az",
" aa", # self lines not in config are added last
" aaa",
" aaaa",
]

def test_difference1(self):
rc = ["a", " a1", " a2", " a3", "b"]
step = ["a", " a1", " a2", " a3", " a4", " a5", "b", "c", "d", " d1"]
Expand Down

0 comments on commit dc554c3

Please sign in to comment.