1#!/usr/local/bin/python3.8 2# 3# This file is part of Ansible 4# 5# Ansible is free software: you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation, either version 3 of the License, or 8# (at your option) any later version. 9# 10# Ansible is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13# GNU General Public License for more details. 14# 15# You should have received a copy of the GNU General Public License 16# along with Ansible. If not, see <http://www.gnu.org/licenses/>. 17# 18from __future__ import absolute_import, division, print_function 19 20__metaclass__ = type 21 22 23DOCUMENTATION = """ 24module: nxos_aaa_server 25extends_documentation_fragment: 26- cisco.nxos.nxos 27short_description: Manages AAA server global configuration. 28description: 29- Manages AAA server global configuration 30version_added: 1.0.0 31author: 32- Jason Edelman (@jedelman8) 33notes: 34- Tested against NXOSv 7.3.(0)D1(1) on VIRL 35- Limited Support for Cisco MDS 36- The server_type parameter is always required. 37- If encrypt_type is not supplied, the global AAA server key will be stored as encrypted 38 (type 7). 39- Changes to the global AAA server key with encrypt_type=0 are not idempotent. 40- state=default will set the supplied parameters to their default values. The parameters 41 that you want to default must also be set to default. If global_key=default, the 42 global key will be removed. 43options: 44 server_type: 45 description: 46 - The server type is either radius or tacacs. 47 required: true 48 choices: 49 - radius 50 - tacacs 51 type: str 52 global_key: 53 description: 54 - Global AAA shared secret or keyword 'default'. 55 type: str 56 encrypt_type: 57 description: 58 - The state of encryption applied to the entered global key. O clear text, 7 encrypted. 59 Type-6 encryption is not supported. 60 choices: 61 - '0' 62 - '7' 63 type: str 64 deadtime: 65 description: 66 - Duration for which a non-reachable AAA server is skipped, in minutes or keyword 67 'default. Range is 1-1440. Device default is 0. 68 type: str 69 server_timeout: 70 description: 71 - Global AAA server timeout period, in seconds or keyword 'default. Range is 1-60. 72 Device default is 5. 73 type: str 74 directed_request: 75 description: 76 - Enables direct authentication requests to AAA server or keyword 'default' Device 77 default is disabled. 78 choices: 79 - enabled 80 - disabled 81 - default 82 type: str 83 state: 84 description: 85 - Manage the state of the resource. 86 default: present 87 choices: 88 - present 89 - default 90 type: str 91""" 92 93EXAMPLES = """ 94# Radius Server Basic settings 95- name: Radius Server Basic settings 96 cisco.nxos.nxos_aaa_server: 97 server_type: radius 98 server_timeout: 9 99 deadtime: 20 100 directed_request: enabled 101 102# Tacacs Server Basic settings 103- name: Tacacs Server Basic settings 104 cisco.nxos.nxos_aaa_server: 105 server_type: tacacs 106 server_timeout: 8 107 deadtime: 19 108 directed_request: disabled 109 110# Setting Global Key 111- name: AAA Server Global Key 112 cisco.nxos.nxos_aaa_server: 113 server_type: radius 114 global_key: test_key 115""" 116 117RETURN = """ 118commands: 119 description: command sent to the device 120 returned: always 121 type: list 122 sample: ["radius-server deadtime 22", "radius-server timeout 11", 123 "radius-server directed-request"] 124""" 125import re 126 127from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( 128 load_config, 129 run_commands, 130) 131from ansible_collections.cisco.nxos.plugins.module_utils.network.nxos.nxos import ( 132 nxos_argument_spec, 133) 134from ansible.module_utils.basic import AnsibleModule 135 136 137PARAM_TO_DEFAULT_KEYMAP = { 138 "server_timeout": "5", 139 "deadtime": "0", 140 "directed_request": "disabled", 141} 142 143 144def execute_show_command(command, module): 145 command = {"command": command, "output": "text"} 146 147 return run_commands(module, command) 148 149 150def flatten_list(command_lists): 151 flat_command_list = [] 152 for command in command_lists: 153 if isinstance(command, list): 154 flat_command_list.extend(command) 155 else: 156 flat_command_list.append(command) 157 return flat_command_list 158 159 160def get_aaa_server_info(server_type, module): 161 aaa_server_info = {} 162 server_command = "show {0}-server".format(server_type) 163 request_command = "show {0}-server directed-request".format(server_type) 164 global_key_command = "show run | sec {0}".format(server_type) 165 aaa_regex = r".*{0}-server\skey\s\d\s+(?P<key>\S+).*".format(server_type) 166 167 server_body = execute_show_command(server_command, module)[0] 168 169 split_server = server_body.splitlines() 170 171 for line in split_server: 172 if line.startswith("timeout"): 173 aaa_server_info["server_timeout"] = line.split(":")[1] 174 175 elif line.startswith("deadtime"): 176 aaa_server_info["deadtime"] = line.split(":")[1] 177 178 request_body = execute_show_command(request_command, module)[0] 179 180 if bool(request_body): 181 aaa_server_info["directed_request"] = request_body.replace("\n", "") 182 else: 183 aaa_server_info["directed_request"] = "disabled" 184 185 key_body = execute_show_command(global_key_command, module)[0] 186 187 try: 188 match_global_key = re.match(aaa_regex, key_body, re.DOTALL) 189 group_key = match_global_key.groupdict() 190 aaa_server_info["global_key"] = group_key["key"].replace('"', "") 191 except (AttributeError, TypeError): 192 aaa_server_info["global_key"] = None 193 194 return aaa_server_info 195 196 197def config_aaa_server(params, server_type): 198 cmds = [] 199 200 deadtime = params.get("deadtime") 201 server_timeout = params.get("server_timeout") 202 directed_request = params.get("directed_request") 203 encrypt_type = params.get("encrypt_type", "7") 204 global_key = params.get("global_key") 205 206 if deadtime is not None: 207 cmds.append("{0}-server deadtime {1}".format(server_type, deadtime)) 208 209 if server_timeout is not None: 210 cmds.append( 211 "{0}-server timeout {1}".format(server_type, server_timeout) 212 ) 213 214 if directed_request is not None: 215 if directed_request == "enabled": 216 cmds.append("{0}-server directed-request".format(server_type)) 217 elif directed_request == "disabled": 218 cmds.append("no {0}-server directed-request".format(server_type)) 219 220 if global_key is not None: 221 cmds.append( 222 "{0}-server key {1} {2}".format( 223 server_type, encrypt_type, global_key 224 ) 225 ) 226 227 return cmds 228 229 230def default_aaa_server(existing, params, server_type): 231 cmds = [] 232 233 deadtime = params.get("deadtime") 234 server_timeout = params.get("server_timeout") 235 directed_request = params.get("directed_request") 236 global_key = params.get("global_key") 237 existing_key = existing.get("global_key") 238 239 if ( 240 deadtime is not None 241 and existing.get("deadtime") != PARAM_TO_DEFAULT_KEYMAP["deadtime"] 242 ): 243 cmds.append("no {0}-server deadtime 1".format(server_type)) 244 245 if ( 246 server_timeout is not None 247 and existing.get("server_timeout") 248 != PARAM_TO_DEFAULT_KEYMAP["server_timeout"] 249 ): 250 cmds.append("no {0}-server timeout 1".format(server_type)) 251 252 if ( 253 directed_request is not None 254 and existing.get("directed_request") 255 != PARAM_TO_DEFAULT_KEYMAP["directed_request"] 256 ): 257 cmds.append("no {0}-server directed-request".format(server_type)) 258 259 if global_key is not None and existing_key is not None: 260 cmds.append( 261 "no {0}-server key 7 {1}".format(server_type, existing_key) 262 ) 263 264 return cmds 265 266 267def main(): 268 argument_spec = dict( 269 server_type=dict( 270 type="str", choices=["radius", "tacacs"], required=True 271 ), 272 global_key=dict(type="str", no_log=True), 273 encrypt_type=dict(type="str", choices=["0", "7"]), 274 deadtime=dict(type="str"), 275 server_timeout=dict(type="str"), 276 directed_request=dict( 277 type="str", choices=["enabled", "disabled", "default"] 278 ), 279 state=dict(choices=["default", "present"], default="present"), 280 ) 281 282 argument_spec.update(nxos_argument_spec) 283 284 module = AnsibleModule( 285 argument_spec=argument_spec, supports_check_mode=True 286 ) 287 288 warnings = list() 289 results = {"changed": False, "commands": [], "warnings": warnings} 290 291 server_type = module.params["server_type"] 292 global_key = module.params["global_key"] 293 encrypt_type = module.params["encrypt_type"] 294 deadtime = module.params["deadtime"] 295 server_timeout = module.params["server_timeout"] 296 directed_request = module.params["directed_request"] 297 state = module.params["state"] 298 299 if encrypt_type and not global_key: 300 module.fail_json(msg="encrypt_type must be used with global_key.") 301 302 args = dict( 303 server_type=server_type, 304 global_key=global_key, 305 encrypt_type=encrypt_type, 306 deadtime=deadtime, 307 server_timeout=server_timeout, 308 directed_request=directed_request, 309 ) 310 311 proposed = dict((k, v) for k, v in args.items() if v is not None) 312 313 existing = get_aaa_server_info(server_type, module) 314 315 commands = [] 316 if state == "present": 317 if deadtime: 318 try: 319 if int(deadtime) < 0 or int(deadtime) > 1440: 320 raise ValueError 321 except ValueError: 322 module.fail_json( 323 msg="deadtime must be an integer between 0 and 1440" 324 ) 325 326 if server_timeout: 327 try: 328 if int(server_timeout) < 1 or int(server_timeout) > 60: 329 raise ValueError 330 except ValueError: 331 module.fail_json( 332 msg="server_timeout must be an integer between 1 and 60" 333 ) 334 335 delta = dict(set(proposed.items()).difference(existing.items())) 336 if delta: 337 command = config_aaa_server(delta, server_type) 338 if command: 339 commands.append(command) 340 341 elif state == "default": 342 for key, value in proposed.items(): 343 if key != "server_type" and value != "default": 344 module.fail_json( 345 msg='Parameters must be set to "default"' 346 "when state=default" 347 ) 348 command = default_aaa_server(existing, proposed, server_type) 349 if command: 350 commands.append(command) 351 352 cmds = flatten_list(commands) 353 if cmds: 354 results["changed"] = True 355 if not module.check_mode: 356 load_config(module, cmds) 357 if "configure" in cmds: 358 cmds.pop(0) 359 results["commands"] = cmds 360 361 module.exit_json(**results) 362 363 364if __name__ == "__main__": 365 main() 366