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