1# 2# -*- coding: utf-8 -*- 3# Copyright 2019 Red Hat 4# GNU General Public License v3.0+ 5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6""" 7The iosxr_lag_interfaces class 8It is in this file where the current configuration (as dict) 9is compared to the provided configuration (as dict) and the command set 10necessary to bring the current configuration to it's desired end-state is 11created 12""" 13 14from __future__ import absolute_import, division, print_function 15 16__metaclass__ = type 17 18from copy import deepcopy 19from distutils.version import LooseVersion 20from ansible.module_utils.six import iteritems 21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( 22 ConfigBase, 23) 24from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.facts import ( 25 Facts, 26) 27from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 28 to_list, 29 dict_diff, 30 remove_empties, 31 search_obj_in_list, 32 param_list_to_dict, 33) 34from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.utils.utils import ( 35 diff_list_of_dicts, 36 pad_commands, 37 flatten_dict, 38 dict_delete, 39 normalize_interface, 40) 41from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr import ( 42 get_os_version, 43) 44 45 46class Lag_interfaces(ConfigBase): 47 """ 48 The iosxr_lag_interfaces class 49 """ 50 51 gather_subset = ["!all", "!min"] 52 53 gather_network_resources = ["lag_interfaces"] 54 55 def __init__(self, module): 56 super(Lag_interfaces, self).__init__(module) 57 58 def get_lag_interfaces_facts(self, data=None): 59 """ Get the 'facts' (the current configuration) 60 61 :rtype: A dictionary 62 :returns: The current configuration as a dictionary 63 """ 64 facts, _warnings = Facts(self._module).get_facts( 65 self.gather_subset, self.gather_network_resources, data=data 66 ) 67 lag_interfaces_facts = facts["ansible_network_resources"].get( 68 "lag_interfaces" 69 ) 70 if not lag_interfaces_facts: 71 return [] 72 return lag_interfaces_facts 73 74 def execute_module(self): 75 """ Execute the module 76 77 :rtype: A dictionary 78 :returns: The result from module execution 79 """ 80 result = {"changed": False} 81 warnings = list() 82 commands = list() 83 if self.state in self.ACTION_STATES: 84 existing_lag_interfaces_facts = self.get_lag_interfaces_facts() 85 else: 86 existing_lag_interfaces_facts = {} 87 88 if self.state in self.ACTION_STATES or self.state == "rendered": 89 commands.extend(self.set_config(existing_lag_interfaces_facts)) 90 91 if commands and self.state in self.ACTION_STATES: 92 if not self._module.check_mode: 93 self._connection.edit_config(commands) 94 result["changed"] = True 95 96 if self.state in self.ACTION_STATES: 97 result["commands"] = commands 98 99 if self.state in self.ACTION_STATES or self.state == "gathered": 100 changed_lag_interfaces_facts = self.get_lag_interfaces_facts() 101 102 elif self.state == "rendered": 103 result["rendered"] = commands 104 105 elif self.state == "parsed": 106 running_config = self._module.params["running_config"] 107 if not running_config: 108 self._module.fail_json( 109 msg="value of running_config parameter must not be empty for state parsed" 110 ) 111 result["parsed"] = self.get_lag_interfaces_facts( 112 data=running_config 113 ) 114 115 if self.state in self.ACTION_STATES: 116 result["before"] = existing_lag_interfaces_facts 117 if result["changed"]: 118 result["after"] = changed_lag_interfaces_facts 119 120 elif self.state == "gathered": 121 result["gathered"] = changed_lag_interfaces_facts 122 123 result["warnings"] = warnings 124 return result 125 126 def set_config(self, existing_lag_interfaces_facts): 127 """ Collect the configuration from the args passed to the module, 128 collect the current configuration (as a dict from facts) 129 130 :rtype: A list 131 :returns: the commands necessary to migrate the current configuration 132 to the desired configuration 133 """ 134 want = self._module.params["config"] 135 if want: 136 for item in want: 137 item["name"] = normalize_interface(item["name"]) 138 if "members" in want and want["members"]: 139 for item in want["members"]: 140 item.update( 141 { 142 "member": normalize_interface(item["member"]), 143 "mode": item["mode"], 144 } 145 ) 146 have = existing_lag_interfaces_facts 147 resp = self.set_state(want, have) 148 return to_list(resp) 149 150 def set_state(self, want, have): 151 """ Select the appropriate function based on the state provided 152 153 :param want: the desired configuration as a dictionary 154 :param have: the current configuration as a dictionary 155 :rtype: A list 156 :returns: the commands necessary to migrate the current configuration 157 to the desired configuration 158 """ 159 state = self._module.params["state"] 160 commands = [] 161 162 if ( 163 self.state in ("merged", "replaced", "overridden", "rendered") 164 and not want 165 ): 166 self._module.fail_json( 167 msg="value of config parameter must not be empty for state {0}".format( 168 state 169 ) 170 ) 171 172 if state == "overridden": 173 commands.extend(self._state_overridden(want, have)) 174 175 elif state == "deleted": 176 commands.extend(self._state_deleted(want, have)) 177 178 else: 179 # Instead of passing entire want and have 180 # list of dictionaries to the respective 181 # _state_* methods we are passing the want 182 # and have dictionaries per interface 183 for item in want: 184 name = item["name"] 185 obj_in_have = search_obj_in_list(name, have) 186 187 if state in ("merged", "rendered"): 188 commands.extend(self._state_merged(item, obj_in_have)) 189 190 elif state == "replaced": 191 commands.extend(self._state_replaced(item, obj_in_have)) 192 193 return commands 194 195 def _state_replaced(self, want, have): 196 """ The command generator when state is replaced 197 198 :rtype: A list 199 :returns: the commands necessary to migrate the current configuration 200 to the desired configuration 201 """ 202 commands = [] 203 if have: 204 commands.extend(self._render_bundle_del_commands(want, have)) 205 commands.extend(self._render_bundle_updates(want, have)) 206 207 if commands or have == {}: 208 pad_commands(commands, want["name"]) 209 210 if have: 211 commands.extend(self._render_interface_del_commands(want, have)) 212 commands.extend(self._render_interface_updates(want, have)) 213 214 return commands 215 216 def _state_overridden(self, want, have): 217 """ The command generator when state is overridden 218 219 :rtype: A list 220 :returns: the commands necessary to migrate the current configuration 221 to the desired configuration 222 """ 223 commands = [] 224 for have_intf in have: 225 intf_in_want = search_obj_in_list(have_intf["name"], want) 226 if not intf_in_want: 227 commands.extend(self._purge_attribs(have_intf)) 228 229 for intf in want: 230 intf_in_have = search_obj_in_list(intf["name"], have) 231 commands.extend(self._state_replaced(intf, intf_in_have)) 232 233 return commands 234 235 def _state_merged(self, want, have): 236 """ The command generator when state is merged 237 238 :rtype: A list 239 :returns: the commands necessary to merge the provided into 240 the current configuration 241 """ 242 commands = [] 243 commands.extend(self._render_bundle_updates(want, have)) 244 245 if commands or have == {}: 246 pad_commands(commands, want["name"]) 247 248 commands.extend(self._render_interface_updates(want, have)) 249 return commands 250 251 def _state_deleted(self, want, have): 252 """ The command generator when state is deleted 253 254 :rtype: A list 255 :returns: the commands necessary to remove the current configuration 256 of the provided objects 257 """ 258 commands = [] 259 260 if not want: 261 for item in have: 262 commands.extend(self._purge_attribs(intf=item)) 263 else: 264 for item in want: 265 name = item["name"] 266 obj_in_have = search_obj_in_list(name, have) 267 if not obj_in_have: 268 self._module.fail_json( 269 msg=("interface {0} does not exist".format(name)) 270 ) 271 commands.extend(self._purge_attribs(intf=obj_in_have)) 272 273 return commands 274 275 def _render_bundle_updates(self, want, have): 276 """ The command generator for updates to bundles 277 :rtype: A list 278 :returns: the commands necessary to update bundles 279 """ 280 commands = [] 281 if not have: 282 have = {"name": want["name"]} 283 284 want_copy = deepcopy(want) 285 have_copy = deepcopy(have) 286 287 want_copy.pop("members", []) 288 have_copy.pop("members", []) 289 290 bundle_updates = dict_diff(have_copy, want_copy) 291 292 if bundle_updates: 293 for key, value in iteritems( 294 flatten_dict(remove_empties(bundle_updates)) 295 ): 296 commands.append(self._compute_commands(key=key, value=value)) 297 298 return commands 299 300 def _render_interface_updates(self, want, have): 301 """ The command generator for updates to member 302 interfaces 303 :rtype: A list 304 :returns: the commands necessary to update member 305 interfaces 306 """ 307 commands = [] 308 309 if not have: 310 have = {"name": want["name"]} 311 312 member_diff = diff_list_of_dicts( 313 want["members"], have.get("members", []) 314 ) 315 316 for diff in member_diff: 317 diff_cmd = [] 318 bundle_cmd = "bundle id {0}".format( 319 want["name"].split("Bundle-Ether")[1] 320 ) 321 if diff.get("mode"): 322 bundle_cmd += " mode {0}".format(diff.get("mode")) 323 diff_cmd.append(bundle_cmd) 324 pad_commands(diff_cmd, diff["member"]) 325 commands.extend(diff_cmd) 326 327 return commands 328 329 def _render_bundle_del_commands(self, want, have): 330 """ The command generator for delete commands 331 w.r.t bundles 332 :rtype: A list 333 :returns: the commands necessary to update member 334 interfaces 335 """ 336 commands = [] 337 if not want: 338 want = {"name": have["name"]} 339 340 want_copy = deepcopy(want) 341 have_copy = deepcopy(have) 342 want_copy.pop("members", []) 343 have_copy.pop("members", []) 344 345 to_delete = dict_delete(have_copy, remove_empties(want_copy)) 346 if to_delete: 347 for key, value in iteritems( 348 flatten_dict(remove_empties(to_delete)) 349 ): 350 commands.append( 351 self._compute_commands(key=key, value=value, remove=True) 352 ) 353 354 return commands 355 356 def _render_interface_del_commands(self, want, have): 357 """ The command generator for delete commands 358 w.r.t member interfaces 359 :rtype: A list 360 :returns: the commands necessary to update member 361 interfaces 362 """ 363 commands = [] 364 if not want: 365 want = {} 366 have_members = have.get("members") 367 368 if have_members: 369 have_members = param_list_to_dict( 370 deepcopy(have_members), unique_key="member" 371 ) 372 want_members = param_list_to_dict( 373 deepcopy(want).get("members", []), unique_key="member" 374 ) 375 376 for key in have_members: 377 if key not in want_members: 378 member_cmd = ["no bundle id"] 379 pad_commands(member_cmd, key) 380 commands.extend(member_cmd) 381 382 return commands 383 384 def _purge_attribs(self, intf): 385 """ The command generator for purging attributes 386 :rtype: A list 387 :returns: the commands necessary to purge attributes 388 """ 389 commands = [] 390 have_copy = deepcopy(intf) 391 members = have_copy.pop("members", []) 392 393 to_delete = dict_delete( 394 have_copy, remove_empties({"name": have_copy["name"]}) 395 ) 396 if to_delete: 397 for key, value in iteritems( 398 flatten_dict(remove_empties(to_delete)) 399 ): 400 commands.append( 401 self._compute_commands(key=key, value=value, remove=True) 402 ) 403 404 if commands: 405 pad_commands(commands, intf["name"]) 406 407 if members: 408 members = param_list_to_dict( 409 deepcopy(members), unique_key="member" 410 ) 411 for key in members: 412 member_cmd = ["no bundle id"] 413 pad_commands(member_cmd, key) 414 commands.extend(member_cmd) 415 416 return commands 417 418 def _compute_commands(self, key, value, remove=False): 419 """ The method generates LAG commands based on the 420 key, value passed. When remove is set to True, 421 the command is negated. 422 :rtype: str 423 :returns: a command based on the `key`, `value` pair 424 passed and the value of `remove` 425 """ 426 if key == "mode": 427 cmd = "lacp mode {0}".format(value) 428 429 elif key == "load_balancing_hash": 430 os_version = get_os_version(self._module) 431 if os_version and LooseVersion(os_version) < LooseVersion("7.0.0"): 432 cmd = "bundle load-balancing hash {0}".format(value) 433 434 elif key == "max_active": 435 cmd = "bundle maximum-active links {0}".format(value) 436 437 elif key == "min_active": 438 cmd = "bundle minimum-active links {0}".format(value) 439 440 if remove: 441 cmd = "no {0}".format(cmd) 442 443 return cmd 444