1#!/usr/local/bin/python3.8 2from __future__ import (absolute_import, division, print_function) 3# Copyright 2019-2020 Fortinet, Inc. 4# 5# This program 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# This program 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 this program. If not, see <https://www.gnu.org/licenses/>. 17 18__metaclass__ = type 19 20ANSIBLE_METADATA = {'status': ['preview'], 21 'supported_by': 'community', 22 'metadata_version': '1.1'} 23 24DOCUMENTATION = ''' 25--- 26module: fortios_user_fortitoken 27short_description: Configure FortiToken in Fortinet's FortiOS and FortiGate. 28description: 29 - This module is able to configure a FortiGate or FortiOS (FOS) device by allowing the 30 user to set and modify user feature and fortitoken category. 31 Examples include all parameters and values need to be adjusted to datasources before usage. 32 Tested with FOS v6.0.0 33version_added: "2.10" 34author: 35 - Link Zheng (@chillancezen) 36 - Jie Xue (@JieX19) 37 - Hongbin Lu (@fgtdev-hblu) 38 - Frank Shen (@frankshen01) 39 - Miguel Angel Munoz (@mamunozgonzalez) 40 - Nicolas Thomas (@thomnico) 41notes: 42 - Legacy fortiosapi has been deprecated, httpapi is the preferred way to run playbooks 43 44requirements: 45 - ansible>=2.9.0 46options: 47 access_token: 48 description: 49 - Token-based authentication. 50 Generated from GUI of Fortigate. 51 type: str 52 required: false 53 enable_log: 54 description: 55 - Enable/Disable logging for task. 56 type: bool 57 required: false 58 default: false 59 vdom: 60 description: 61 - Virtual domain, among those defined previously. A vdom is a 62 virtual instance of the FortiGate that can be configured and 63 used as a different unit. 64 type: str 65 default: root 66 67 state: 68 description: 69 - Indicates whether to create or remove the object. 70 type: str 71 required: true 72 choices: 73 - present 74 - absent 75 user_fortitoken: 76 description: 77 - Configure FortiToken. 78 default: null 79 type: dict 80 suboptions: 81 activation_code: 82 description: 83 - Mobile token user activation-code. 84 type: str 85 activation_expire: 86 description: 87 - Mobile token user activation-code expire time. 88 type: int 89 comments: 90 description: 91 - Comment. 92 type: str 93 license: 94 description: 95 - Mobile token license. 96 type: str 97 os_ver: 98 description: 99 - Device Mobile Version. 100 type: str 101 reg_id: 102 description: 103 - Device Reg ID. 104 type: str 105 seed: 106 description: 107 - Token seed. 108 type: str 109 serial_number: 110 description: 111 - Serial number. 112 type: str 113 status: 114 description: 115 - Status 116 type: str 117 choices: 118 - active 119 - lock 120''' 121 122EXAMPLES = ''' 123- hosts: fortigates 124 collections: 125 - fortinet.fortios 126 connection: httpapi 127 vars: 128 vdom: "root" 129 ansible_httpapi_use_ssl: yes 130 ansible_httpapi_validate_certs: no 131 ansible_httpapi_port: 443 132 tasks: 133 - name: Configure FortiToken. 134 fortios_user_fortitoken: 135 vdom: "{{ vdom }}" 136 state: "present" 137 access_token: "<your_own_value>" 138 user_fortitoken: 139 activation_code: "<your_own_value>" 140 activation_expire: "4" 141 comments: "<your_own_value>" 142 license: "<your_own_value>" 143 os_ver: "<your_own_value>" 144 reg_id: "<your_own_value>" 145 seed: "<your_own_value>" 146 serial_number: "<your_own_value>" 147 status: "active" 148 149''' 150 151RETURN = ''' 152build: 153 description: Build number of the fortigate image 154 returned: always 155 type: str 156 sample: '1547' 157http_method: 158 description: Last method used to provision the content into FortiGate 159 returned: always 160 type: str 161 sample: 'PUT' 162http_status: 163 description: Last result given by FortiGate on last operation applied 164 returned: always 165 type: str 166 sample: "200" 167mkey: 168 description: Master key (id) used in the last call to FortiGate 169 returned: success 170 type: str 171 sample: "id" 172name: 173 description: Name of the table used to fulfill the request 174 returned: always 175 type: str 176 sample: "urlfilter" 177path: 178 description: Path of the table used to fulfill the request 179 returned: always 180 type: str 181 sample: "webfilter" 182revision: 183 description: Internal revision number 184 returned: always 185 type: str 186 sample: "17.0.2.10658" 187serial: 188 description: Serial number of the unit 189 returned: always 190 type: str 191 sample: "FGVMEVYYQT3AB5352" 192status: 193 description: Indication of the operation's result 194 returned: always 195 type: str 196 sample: "success" 197vdom: 198 description: Virtual domain used 199 returned: always 200 type: str 201 sample: "root" 202version: 203 description: Version of the FortiGate 204 returned: always 205 type: str 206 sample: "v5.6.3" 207 208''' 209from ansible.module_utils.basic import AnsibleModule 210from ansible.module_utils.connection import Connection 211from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import FortiOSHandler 212from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_legacy_fortiosapi 213from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import schema_to_module_spec 214from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.fortios import check_schema_versioning 215from ansible_collections.fortinet.fortios.plugins.module_utils.fortimanager.common import FAIL_SOCKET_MSG 216from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import is_same_comparison 217from ansible_collections.fortinet.fortios.plugins.module_utils.fortios.comparison import serialize 218 219 220def filter_user_fortitoken_data(json): 221 option_list = ['activation_code', 'activation_expire', 'comments', 222 'license', 'os_ver', 'reg_id', 223 'seed', 'serial_number', 'status'] 224 dictionary = {} 225 226 for attribute in option_list: 227 if attribute in json and json[attribute] is not None: 228 dictionary[attribute] = json[attribute] 229 230 return dictionary 231 232 233def underscore_to_hyphen(data): 234 if isinstance(data, list): 235 for i, elem in enumerate(data): 236 data[i] = underscore_to_hyphen(elem) 237 elif isinstance(data, dict): 238 new_data = {} 239 for k, v in data.items(): 240 new_data[k.replace('_', '-')] = underscore_to_hyphen(v) 241 data = new_data 242 243 return data 244 245 246def user_fortitoken(data, fos, check_mode=False): 247 248 vdom = data['vdom'] 249 250 state = data['state'] 251 252 user_fortitoken_data = data['user_fortitoken'] 253 filtered_data = underscore_to_hyphen(filter_user_fortitoken_data(user_fortitoken_data)) 254 255 # check_mode starts from here 256 if check_mode: 257 mkey = fos.get_mkey('system', 'interface', filtered_data, vdom=vdom) 258 current_data = fos.get('system', 'interface', vdom=vdom, mkey=mkey) 259 is_existed = current_data and current_data.get('http_status') == 200 \ 260 and isinstance(current_data.get('results'), list) \ 261 and len(current_data['results']) > 0 262 263 # 2. if it exists and the state is 'present' then compare current settings with desired 264 if state == 'present' or state is True: 265 if mkey is None: 266 return False, True, filtered_data 267 268 # if mkey exists then compare each other 269 # record exits and they're matched or not 270 if is_existed: 271 is_same = is_same_comparison( 272 serialize(current_data['results'][0]), serialize(filtered_data)) 273 return False, not is_same, filtered_data 274 275 # record does not exist 276 return False, True, filtered_data 277 278 if state == 'absent': 279 if mkey is None: 280 return False, False, filtered_data 281 282 if is_existed: 283 return False, True, filtered_data 284 return False, False, filtered_data 285 286 return True, False, {'reason: ': 'Must provide state parameter'} 287 288 if state == "present" or state is True: 289 return fos.set('user', 290 'fortitoken', 291 data=filtered_data, 292 vdom=vdom) 293 294 elif state == "absent": 295 return fos.delete('user', 296 'fortitoken', 297 mkey=filtered_data['serial-number'], 298 vdom=vdom) 299 else: 300 fos._module.fail_json(msg='state must be present or absent!') 301 302 303def is_successful_status(status): 304 return status['status'] == "success" or \ 305 status['http_method'] == "DELETE" and status['http_status'] == 404 306 307 308def fortios_user(data, fos, check_mode): 309 310 if data['user_fortitoken']: 311 resp = user_fortitoken(data, fos, check_mode) 312 else: 313 fos._module.fail_json(msg='missing task body: %s' % ('user_fortitoken')) 314 if check_mode: 315 return resp 316 return not is_successful_status(resp), \ 317 resp['status'] == "success" and \ 318 (resp['revision_changed'] if 'revision_changed' in resp else True), \ 319 resp 320 321 322versioned_schema = { 323 "type": "list", 324 "children": { 325 "status": { 326 "type": "string", 327 "options": [ 328 { 329 "value": "active", 330 "revisions": { 331 "v6.0.0": True, 332 "v7.0.0": True, 333 "v6.0.5": True, 334 "v6.4.4": True, 335 "v6.4.0": True, 336 "v6.4.1": True, 337 "v6.2.0": True, 338 "v6.2.3": True, 339 "v6.2.5": True, 340 "v6.2.7": True, 341 "v6.0.11": True 342 } 343 }, 344 { 345 "value": "lock", 346 "revisions": { 347 "v6.0.0": True, 348 "v7.0.0": True, 349 "v6.0.5": True, 350 "v6.4.4": True, 351 "v6.4.0": True, 352 "v6.4.1": True, 353 "v6.2.0": True, 354 "v6.2.3": True, 355 "v6.2.5": True, 356 "v6.2.7": True, 357 "v6.0.11": True 358 } 359 } 360 ], 361 "revisions": { 362 "v6.0.0": True, 363 "v7.0.0": True, 364 "v6.0.5": True, 365 "v6.4.4": True, 366 "v6.4.0": True, 367 "v6.4.1": True, 368 "v6.2.0": True, 369 "v6.2.3": True, 370 "v6.2.5": True, 371 "v6.2.7": True, 372 "v6.0.11": True 373 } 374 }, 375 "license": { 376 "type": "string", 377 "revisions": { 378 "v6.0.0": True, 379 "v7.0.0": True, 380 "v6.0.5": True, 381 "v6.4.4": True, 382 "v6.4.0": True, 383 "v6.4.1": True, 384 "v6.2.0": True, 385 "v6.2.3": True, 386 "v6.2.5": True, 387 "v6.2.7": True, 388 "v6.0.11": True 389 } 390 }, 391 "comments": { 392 "type": "string", 393 "revisions": { 394 "v6.0.0": True, 395 "v7.0.0": True, 396 "v6.0.5": True, 397 "v6.4.4": True, 398 "v6.4.0": True, 399 "v6.4.1": True, 400 "v6.2.0": True, 401 "v6.2.3": True, 402 "v6.2.5": True, 403 "v6.2.7": True, 404 "v6.0.11": True 405 } 406 }, 407 "activation_expire": { 408 "type": "integer", 409 "revisions": { 410 "v6.0.0": True, 411 "v7.0.0": True, 412 "v6.0.5": True, 413 "v6.4.4": True, 414 "v6.4.0": True, 415 "v6.4.1": True, 416 "v6.2.0": True, 417 "v6.2.3": True, 418 "v6.2.5": True, 419 "v6.2.7": True, 420 "v6.0.11": True 421 } 422 }, 423 "reg_id": { 424 "type": "string", 425 "revisions": { 426 "v6.0.0": True, 427 "v7.0.0": True, 428 "v6.0.5": True, 429 "v6.4.4": True, 430 "v6.4.0": True, 431 "v6.4.1": True, 432 "v6.2.0": True, 433 "v6.2.3": True, 434 "v6.2.5": True, 435 "v6.2.7": True, 436 "v6.0.11": True 437 } 438 }, 439 "seed": { 440 "type": "string", 441 "revisions": { 442 "v6.0.0": True, 443 "v7.0.0": False, 444 "v6.0.5": True, 445 "v6.4.4": False, 446 "v6.4.0": False, 447 "v6.4.1": False, 448 "v6.2.0": False, 449 "v6.2.3": True, 450 "v6.2.5": False, 451 "v6.2.7": False, 452 "v6.0.11": True 453 } 454 }, 455 "serial_number": { 456 "type": "string", 457 "revisions": { 458 "v6.0.0": True, 459 "v7.0.0": True, 460 "v6.0.5": True, 461 "v6.4.4": True, 462 "v6.4.0": True, 463 "v6.4.1": True, 464 "v6.2.0": True, 465 "v6.2.3": True, 466 "v6.2.5": True, 467 "v6.2.7": True, 468 "v6.0.11": True 469 } 470 }, 471 "activation_code": { 472 "type": "string", 473 "revisions": { 474 "v6.0.0": True, 475 "v7.0.0": True, 476 "v6.0.5": True, 477 "v6.4.4": True, 478 "v6.4.0": True, 479 "v6.4.1": True, 480 "v6.2.0": True, 481 "v6.2.3": True, 482 "v6.2.5": True, 483 "v6.2.7": True, 484 "v6.0.11": True 485 } 486 }, 487 "os_ver": { 488 "type": "string", 489 "revisions": { 490 "v6.0.0": True, 491 "v7.0.0": True, 492 "v6.0.5": True, 493 "v6.4.4": True, 494 "v6.4.0": True, 495 "v6.4.1": True, 496 "v6.2.0": True, 497 "v6.2.3": True, 498 "v6.2.5": True, 499 "v6.2.7": True, 500 "v6.0.11": True 501 } 502 } 503 }, 504 "revisions": { 505 "v6.0.0": True, 506 "v7.0.0": True, 507 "v6.0.5": True, 508 "v6.4.4": True, 509 "v6.4.0": True, 510 "v6.4.1": True, 511 "v6.2.0": True, 512 "v6.2.3": True, 513 "v6.2.5": True, 514 "v6.2.7": True, 515 "v6.0.11": True 516 } 517} 518 519 520def main(): 521 module_spec = schema_to_module_spec(versioned_schema) 522 mkeyname = 'serial-number' 523 fields = { 524 "access_token": {"required": False, "type": "str", "no_log": True}, 525 "enable_log": {"required": False, "type": bool}, 526 "vdom": {"required": False, "type": "str", "default": "root"}, 527 "state": {"required": True, "type": "str", 528 "choices": ["present", "absent"]}, 529 "user_fortitoken": { 530 "required": False, "type": "dict", "default": None, 531 "options": { 532 } 533 } 534 } 535 for attribute_name in module_spec['options']: 536 fields["user_fortitoken"]['options'][attribute_name] = module_spec['options'][attribute_name] 537 if mkeyname and mkeyname == attribute_name: 538 fields["user_fortitoken"]['options'][attribute_name]['required'] = True 539 540 check_legacy_fortiosapi() 541 module = AnsibleModule(argument_spec=fields, 542 supports_check_mode=True) 543 544 versions_check_result = None 545 if module._socket_path: 546 connection = Connection(module._socket_path) 547 if 'access_token' in module.params: 548 connection.set_option('access_token', module.params['access_token']) 549 550 if 'enable_log' in module.params: 551 connection.set_option('enable_log', module.params['enable_log']) 552 else: 553 connection.set_option('enable_log', False) 554 fos = FortiOSHandler(connection, module, mkeyname) 555 versions_check_result = check_schema_versioning(fos, versioned_schema, "user_fortitoken") 556 557 is_error, has_changed, result = fortios_user(module.params, fos, module.check_mode) 558 559 else: 560 module.fail_json(**FAIL_SOCKET_MSG) 561 562 if versions_check_result and versions_check_result['matched'] is False: 563 module.warn("Ansible has detected version mismatch between FortOS system and your playbook, see more details by specifying option -vvv") 564 565 if not is_error: 566 if versions_check_result and versions_check_result['matched'] is False: 567 module.exit_json(changed=has_changed, version_check_warning=versions_check_result, meta=result) 568 else: 569 module.exit_json(changed=has_changed, meta=result) 570 else: 571 if versions_check_result and versions_check_result['matched'] is False: 572 module.fail_json(msg="Error in repo", version_check_warning=versions_check_result, meta=result) 573 else: 574 module.fail_json(msg="Error in repo", meta=result) 575 576 577if __name__ == '__main__': 578 main() 579