1# Copyright 2019 Red Hat 2# GNU General Public License v3.0+ 3# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 4""" 5The vyos_interfaces class 6It is in this file where the current configuration (as dict) 7is compared to the provided configuration (as dict) and the command set 8necessary to bring the current configuration to it's desired end-state is 9created 10""" 11 12from __future__ import absolute_import, division, print_function 13 14__metaclass__ = type 15 16from copy import deepcopy 17from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( 18 ConfigBase, 19) 20from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 21 to_list, 22 dict_diff, 23 remove_empties, 24) 25from ansible.module_utils.six import iteritems 26from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import ( 27 Facts, 28) 29from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import ( 30 search_obj_in_list, 31 get_interface_type, 32 dict_delete, 33) 34 35 36class Interfaces(ConfigBase): 37 """ 38 The vyos_interfaces class 39 """ 40 41 gather_subset = [ 42 "!all", 43 "!min", 44 ] 45 46 gather_network_resources = ["interfaces"] 47 48 def __init__(self, module): 49 super(Interfaces, self).__init__(module) 50 51 def get_interfaces_facts(self, data=None): 52 """Get the 'facts' (the current configuration) 53 54 :rtype: A dictionary 55 :returns: The current configuration as a dictionary 56 """ 57 facts, _warnings = Facts(self._module).get_facts( 58 self.gather_subset, self.gather_network_resources, data=data 59 ) 60 interfaces_facts = facts["ansible_network_resources"].get("interfaces") 61 if not interfaces_facts: 62 return [] 63 return interfaces_facts 64 65 def execute_module(self): 66 """Execute the module 67 :rtype: A dictionary 68 :returns: The result from module execution 69 """ 70 result = {"changed": False} 71 commands = list() 72 warnings = list() 73 74 if self.state in self.ACTION_STATES: 75 existing_interfaces_facts = self.get_interfaces_facts() 76 else: 77 existing_interfaces_facts = [] 78 79 if self.state in self.ACTION_STATES or self.state == "rendered": 80 commands.extend(self.set_config(existing_interfaces_facts)) 81 82 if commands and self.state in self.ACTION_STATES: 83 if not self._module.check_mode: 84 self._connection.edit_config(commands) 85 result["changed"] = True 86 87 if self.state in self.ACTION_STATES: 88 result["commands"] = commands 89 90 if self.state in self.ACTION_STATES or self.state == "gathered": 91 changed_interfaces_facts = self.get_interfaces_facts() 92 elif self.state == "rendered": 93 result["rendered"] = commands 94 elif self.state == "parsed": 95 running_config = self._module.params["running_config"] 96 if not running_config: 97 self._module.fail_json( 98 msg="value of running_config parameter must not be empty for state parsed" 99 ) 100 result["parsed"] = self.get_interfaces_facts(data=running_config) 101 else: 102 changed_interfaces_facts = [] 103 104 if self.state in self.ACTION_STATES: 105 result["before"] = existing_interfaces_facts 106 if result["changed"]: 107 result["after"] = changed_interfaces_facts 108 elif self.state == "gathered": 109 result["gathered"] = changed_interfaces_facts 110 111 result["warnings"] = warnings 112 return result 113 114 def set_config(self, existing_interfaces_facts): 115 """Collect the configuration from the args passed to the module, 116 collect the current configuration (as a dict from facts) 117 118 :rtype: A list 119 :returns: the commands necessary to migrate the current configuration 120 to the desired configuration 121 """ 122 want = self._module.params["config"] 123 have = existing_interfaces_facts 124 resp = self.set_state(want, have) 125 return to_list(resp) 126 127 def set_state(self, want, have): 128 """Select the appropriate function based on the state provided 129 130 :param want: the desired configuration as a dictionary 131 :param have: the current configuration as a dictionary 132 :rtype: A list 133 :returns: the commands necessary to migrate the current configuration 134 to the desired configuration 135 """ 136 commands = [] 137 138 if ( 139 self.state in ("merged", "replaced", "overridden", "rendered") 140 and not want 141 ): 142 self._module.fail_json( 143 msg="value of config parameter must not be empty for state {0}".format( 144 self.state 145 ) 146 ) 147 148 if self.state == "overridden": 149 commands.extend(self._state_overridden(want=want, have=have)) 150 151 elif self.state == "deleted": 152 if not want: 153 for intf in have: 154 commands.extend( 155 self._state_deleted({"name": intf["name"]}, intf) 156 ) 157 else: 158 for item in want: 159 obj_in_have = search_obj_in_list(item["name"], have) 160 commands.extend(self._state_deleted(item, obj_in_have)) 161 else: 162 for item in want: 163 name = item["name"] 164 enable_state = item["enabled"] 165 obj_in_have = search_obj_in_list(name, have) 166 if not obj_in_have: 167 obj_in_have = {"name": name, "enabled": enable_state} 168 169 if self.state in ("merged", "rendered"): 170 commands.extend(self._state_merged(item, obj_in_have)) 171 172 elif self.state == "replaced": 173 commands.extend(self._state_replaced(item, obj_in_have)) 174 175 return commands 176 177 def _state_replaced(self, want, have): 178 """The command generator when state is replaced 179 180 :rtype: A list 181 :returns: the commands necessary to migrate the current configuration 182 to the desired configuration 183 """ 184 commands = [] 185 if have: 186 commands.extend(self._state_deleted(want, have)) 187 188 commands.extend(self._state_merged(want, have)) 189 190 return commands 191 192 def _state_overridden(self, want, have): 193 """The command generator when state is overridden 194 195 :rtype: A list 196 :returns: the commands necessary to migrate the current configuration 197 to the desired configuration 198 """ 199 commands = [] 200 201 for intf in have: 202 intf_in_want = search_obj_in_list(intf["name"], want) 203 if not intf_in_want: 204 commands.extend( 205 self._state_deleted({"name": intf["name"]}, intf) 206 ) 207 208 for intf in want: 209 intf_in_have = search_obj_in_list(intf["name"], have) 210 if not intf_in_have: 211 intf_in_have = { 212 "name": intf["name"], 213 "enabled": intf["enabled"], 214 } 215 commands.extend(self._state_replaced(intf, intf_in_have)) 216 217 return commands 218 219 def _state_merged(self, want, have): 220 """The command generator when state is merged 221 222 :rtype: A list 223 :returns: the commands necessary to merge the provided into 224 the current configuration 225 """ 226 commands = [] 227 want_copy = deepcopy(remove_empties(want)) 228 have_copy = deepcopy(have) 229 230 want_vifs = want_copy.pop("vifs", []) 231 have_vifs = have_copy.pop("vifs", []) 232 233 updates = dict_diff(have_copy, want_copy) 234 235 if updates: 236 for key, value in iteritems(updates): 237 commands.append( 238 self._compute_commands( 239 key=key, value=value, interface=want_copy["name"] 240 ) 241 ) 242 243 if want_vifs: 244 for want_vif in want_vifs: 245 have_vif = search_obj_in_list( 246 want_vif["vlan_id"], have_vifs, key="vlan_id" 247 ) 248 if not have_vif: 249 have_vif = { 250 "vlan_id": want_vif["vlan_id"], 251 "enabled": True, 252 } 253 254 vif_updates = dict_diff(have_vif, want_vif) 255 if vif_updates: 256 for key, value in iteritems(vif_updates): 257 commands.append( 258 self._compute_commands( 259 key=key, 260 value=value, 261 interface=want_copy["name"], 262 vif=want_vif["vlan_id"], 263 ) 264 ) 265 266 return commands 267 268 def _state_deleted(self, want, have): 269 """The command generator when state is deleted 270 271 :rtype: A list 272 :returns: the commands necessary to remove the current configuration 273 of the provided objects 274 """ 275 commands = [] 276 277 want_copy = deepcopy(remove_empties(want)) 278 have_copy = deepcopy(have) 279 want_vifs = want_copy.pop("vifs", []) 280 have_vifs = have_copy.pop("vifs", []) 281 282 for key in dict_delete(have_copy, want_copy).keys(): 283 if key == "enabled": 284 continue 285 commands.append( 286 self._compute_commands( 287 key=key, interface=want_copy["name"], remove=True 288 ) 289 ) 290 if have_copy["enabled"] is False: 291 commands.append( 292 self._compute_commands( 293 key="enabled", value=True, interface=want_copy["name"] 294 ) 295 ) 296 297 if have_vifs: 298 for have_vif in have_vifs: 299 want_vif = search_obj_in_list( 300 have_vif["vlan_id"], want_vifs, key="vlan_id" 301 ) 302 if not want_vif: 303 want_vif = { 304 "vlan_id": have_vif["vlan_id"], 305 "enabled": True, 306 } 307 308 for key in dict_delete(have_vif, want_vif).keys(): 309 if key == "enabled": 310 continue 311 commands.append( 312 self._compute_commands( 313 key=key, 314 interface=want_copy["name"], 315 vif=want_vif["vlan_id"], 316 remove=True, 317 ) 318 ) 319 if have_vif["enabled"] is False: 320 commands.append( 321 self._compute_commands( 322 key="enabled", 323 value=True, 324 interface=want_copy["name"], 325 vif=want_vif["vlan_id"], 326 ) 327 ) 328 return commands 329 330 def _compute_commands( 331 self, interface, key, vif=None, value=None, remove=False 332 ): 333 intf_context = "interfaces {0} {1}".format( 334 get_interface_type(interface), interface 335 ) 336 set_cmd = "set {0}".format(intf_context) 337 del_cmd = "delete {0}".format(intf_context) 338 339 if vif: 340 set_cmd = set_cmd + (" vif {0}".format(vif)) 341 del_cmd = del_cmd + (" vif {0}".format(vif)) 342 343 if key == "enabled": 344 if not value: 345 command = "{0} disable".format(set_cmd) 346 else: 347 command = "{0} disable".format(del_cmd) 348 else: 349 if not remove: 350 command = "{0} {1} '{2}'".format(set_cmd, key, value) 351 else: 352 command = "{0} {1}".format(del_cmd, key) 353 354 return command 355