1#!/usr/bin/python 2# -*- coding: utf-8 -*- 3# 4# Copyright: (c) 2018, F5 Networks Inc. 5# GNU General Public License v3.0 (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 6 7from __future__ import absolute_import, division, print_function 8__metaclass__ = type 9 10 11ANSIBLE_METADATA = {'metadata_version': '1.1', 12 'status': ['preview'], 13 'supported_by': 'certified'} 14 15DOCUMENTATION = r''' 16--- 17module: bigip_device_auth_ldap 18short_description: Manage LDAP device authentication settings on BIG-IP 19description: 20 - Manage LDAP device authentication settings on BIG-IP. 21version_added: 2.8 22options: 23 servers: 24 description: 25 - Specifies the LDAP servers that the system must use to obtain 26 authentication information. You must specify a server when you 27 create an LDAP configuration object. 28 type: list 29 port: 30 description: 31 - Specifies the port that the system uses for access to the remote host server. 32 - When configuring LDAP device authentication for the first time, if this parameter 33 is not specified, the default port is C(389). 34 type: int 35 remote_directory_tree: 36 description: 37 - Specifies the file location (tree) of the user authentication database on the 38 server. 39 type: str 40 scope: 41 description: 42 - Specifies the level of the remote Active Directory or LDAP directory that the 43 system should search for the user authentication. 44 type: str 45 choices: 46 - sub 47 - one 48 - base 49 bind_dn: 50 description: 51 - Specifies the distinguished name for the Active Directory or LDAP server user 52 ID. 53 - The BIG-IP client authentication module does not support Active Directory or 54 LDAP servers that do not perform bind referral when authenticating referred 55 accounts. 56 - Therefore, if you plan to use Active Directory or LDAP as your authentication 57 source and want to use referred accounts, make sure your servers perform bind 58 referral. 59 type: str 60 bind_password: 61 description: 62 - Specifies a password for the Active Directory or LDAP server user ID. 63 type: str 64 user_template: 65 description: 66 - Specifies the distinguished name of the user who is logging on. 67 - You specify the template as a variable that the system replaces with user-specific 68 information during the logon attempt. 69 - For example, you could specify a user template such as C(%s@siterequest.com) or 70 C(uxml:id=%s,ou=people,dc=siterequest,dc=com). 71 - When a user attempts to log on, the system replaces C(%s) with the name the user 72 specified in the Basic Authentication dialog box, and passes that as the 73 distinguished name for the bind operation. 74 - The system passes the associated password as the password for the bind operation. 75 - This field can contain only one C(%s) and cannot contain any other format 76 specifiers. 77 type: str 78 check_member_attr: 79 description: 80 - Checks the user's member attribute in the remote LDAP or AD group. 81 type: bool 82 ssl: 83 description: 84 - Specifies whether the system uses an SSL port to communicate with the LDAP server. 85 type: str 86 choices: 87 - "yes" 88 - "no" 89 - start-tls 90 ca_cert: 91 description: 92 - Specifies the name of an SSL certificate from a certificate authority (CA). 93 - To remove this value, use the reserved value C(none). 94 type: str 95 aliases: [ ssl_ca_cert ] 96 client_key: 97 description: 98 - Specifies the name of an SSL client key. 99 - To remove this value, use the reserved value C(none). 100 type: str 101 aliases: [ ssl_client_key ] 102 client_cert: 103 description: 104 - Specifies the name of an SSL client certificate. 105 - To remove this value, use the reserved value C(none). 106 type: str 107 aliases: [ ssl_client_cert ] 108 validate_certs: 109 description: 110 - Specifies whether the system checks an SSL peer, as a result of which the 111 system requires and verifies the server certificate. 112 type: bool 113 aliases: [ ssl_check_peer ] 114 login_ldap_attr: 115 description: 116 - Specifies the LDAP directory attribute containing the local user name that is 117 associated with the selected directory entry. 118 - When configuring LDAP device authentication for the first time, if this parameter 119 is not specified, the default port is C(samaccountname). 120 type: str 121 fallback_to_local: 122 description: 123 - Specifies that the system uses the Local authentication method if the remote 124 authentication method is not available. 125 type: bool 126 state: 127 description: 128 - When C(present), ensures the device authentication method exists. 129 - When C(absent), ensures the device authentication method does not exist. 130 type: str 131 choices: 132 - present 133 - absent 134 default: present 135 update_password: 136 description: 137 - C(always) will always update the C(bind_password). 138 - C(on_create) will only set the C(bind_password) for newly created authentication 139 mechanisms. 140 type: str 141 choices: 142 - always 143 - on_create 144 default: always 145extends_documentation_fragment: f5 146author: 147 - Tim Rupp (@caphrim007) 148 - Wojciech Wypior (@wojtek0806) 149''' 150 151EXAMPLES = r''' 152- name: Create an LDAP authentication object 153 bigip_device_auth_ldap: 154 name: foo 155 provider: 156 password: secret 157 server: lb.mydomain.com 158 user: admin 159 delegate_to: localhost 160''' 161 162RETURN = r''' 163servers: 164 description: LDAP servers used by the system to obtain authentication information. 165 returned: changed 166 type: list 167 sample: ['192.168.1.1', '192.168.1.2'] 168port: 169 description: The port that the system uses for access to the remote LDAP server. 170 returned: changed 171 type: int 172 sample: 389 173remote_directory_tree: 174 description: File location (tree) of the user authentication database on the server. 175 returned: changed 176 type: str 177 sample: "CN=Users,DC=FOOBAR,DC=LOCAL" 178scope: 179 description: The level of the remote Active Directory or LDAP directory searched for user authentication. 180 returned: changed 181 type: str 182 sample: base 183bind_dn: 184 description: The distinguished name for the Active Directory or LDAP server user ID. 185 returned: changed 186 type: str 187 sample: "user@foobar.local" 188user_template: 189 description: The distinguished name of the user who is logging on. 190 returned: changed 191 type: str 192 sample: "uid=%s,ou=people,dc=foobar,dc=local" 193check_member_attr: 194 description: The user's member attribute in the remote LDAP or AD group. 195 returned: changed 196 type: bool 197 sample: yes 198ssl: 199 description: Specifies whether the system uses an SSL port to communicate with the LDAP server. 200 returned: changed 201 type: str 202 sample: start-tls 203ca_cert: 204 description: The name of an SSL certificate from a certificate authority. 205 returned: changed 206 type: str 207 sample: My-Trusted-CA-Bundle.crt 208client_key: 209 description: The name of an SSL client key. 210 returned: changed 211 type: str 212 sample: MyKey.key 213client_cert: 214 description: The name of an SSL client certificate. 215 returned: changed 216 type: str 217 sample: MyCert.crt 218validate_certs: 219 description: Indicates if the system checks an SSL peer. 220 returned: changed 221 type: bool 222 sample: yes 223login_ldap_attr: 224 description: The LDAP directory attribute containing the local user name associated with the selected directory entry. 225 returned: changed 226 type: str 227 sample: samaccountname 228fallback_to_local: 229 description: Specifies that the system uses the Local authentication method as fallback 230 returned: changed 231 type: bool 232 sample: yes 233''' 234 235from ansible.module_utils.basic import AnsibleModule 236 237try: 238 from library.module_utils.network.f5.bigip import F5RestClient 239 from library.module_utils.network.f5.common import F5ModuleError 240 from library.module_utils.network.f5.common import AnsibleF5Parameters 241 from library.module_utils.network.f5.common import fq_name 242 from library.module_utils.network.f5.common import transform_name 243 from library.module_utils.network.f5.common import f5_argument_spec 244 from library.module_utils.network.f5.common import flatten_boolean 245 from library.module_utils.network.f5.compare import cmp_str_with_none 246except ImportError: 247 from ansible.module_utils.network.f5.bigip import F5RestClient 248 from ansible.module_utils.network.f5.common import F5ModuleError 249 from ansible.module_utils.network.f5.common import AnsibleF5Parameters 250 from ansible.module_utils.network.f5.common import fq_name 251 from ansible.module_utils.network.f5.common import transform_name 252 from ansible.module_utils.network.f5.common import f5_argument_spec 253 from ansible.module_utils.network.f5.common import flatten_boolean 254 from ansible.module_utils.network.f5.compare import cmp_str_with_none 255 256 257class Parameters(AnsibleF5Parameters): 258 api_map = { 259 'bindDn': 'bind_dn', 260 'bindPw': 'bind_password', 261 'userTemplate': 'user_template', 262 'fallback': 'fallback_to_local', 263 'loginAttribute': 'login_ldap_attr', 264 'sslCheckPeer': 'validate_certs', 265 'sslClientCert': 'client_cert', 266 'sslClientKey': 'client_key', 267 'sslCaCertFile': 'ca_cert', 268 'checkRolesGroup': 'check_member_attr', 269 'searchBaseDn': 'remote_directory_tree', 270 } 271 272 api_attributes = [ 273 'bindDn', 274 'bindPw', 275 'checkRolesGroup', 276 'loginAttribute', 277 'port', 278 'scope', 279 'searchBaseDn', 280 'servers', 281 'ssl', 282 'sslCaCertFile', 283 'sslCheckPeer', 284 'sslClientCert', 285 'sslClientKey', 286 'userTemplate', 287 ] 288 289 returnables = [ 290 'bind_dn', 291 'bind_password', 292 'check_member_attr', 293 'fallback_to_local', 294 'login_ldap_attr', 295 'port', 296 'remote_directory_tree', 297 'scope', 298 'servers', 299 'ssl', 300 'ca_cert', 301 'validate_certs', 302 'client_cert', 303 'client_key', 304 'user_template', 305 ] 306 307 updatables = [ 308 'bind_dn', 309 'bind_password', 310 'check_member_attr', 311 'fallback_to_local', 312 'login_ldap_attr', 313 'port', 314 'remote_directory_tree', 315 'scope', 316 'servers', 317 'ssl', 318 'ssl_ca_cert', 319 'ssl_check_peer', 320 'ssl_client_cert', 321 'ssl_client_key', 322 'user_template', 323 ] 324 325 @property 326 def ssl_ca_cert(self): 327 if self._values['ssl_ca_cert'] is None: 328 return None 329 elif self._values['ssl_ca_cert'] in ['none', '']: 330 return '' 331 return fq_name(self.partition, self._values['ssl_ca_cert']) 332 333 @property 334 def ssl_client_key(self): 335 if self._values['ssl_client_key'] is None: 336 return None 337 elif self._values['ssl_client_key'] in ['none', '']: 338 return '' 339 return fq_name(self.partition, self._values['ssl_client_key']) 340 341 @property 342 def ssl_client_cert(self): 343 if self._values['ssl_client_cert'] is None: 344 return None 345 elif self._values['ssl_client_cert'] in ['none', '']: 346 return '' 347 return fq_name(self.partition, self._values['ssl_client_cert']) 348 349 @property 350 def ssl_check_peer(self): 351 return flatten_boolean(self._values['ssl_check_peer']) 352 353 @property 354 def fallback_to_local(self): 355 return flatten_boolean(self._values['fallback_to_local']) 356 357 @property 358 def check_member_attr(self): 359 return flatten_boolean(self._values['check_member_attr']) 360 361 @property 362 def login_ldap_attr(self): 363 if self._values['login_ldap_attr'] is None: 364 return None 365 elif self._values['login_ldap_attr'] in ['none', '']: 366 return '' 367 return self._values['login_ldap_attr'] 368 369 @property 370 def user_template(self): 371 if self._values['user_template'] is None: 372 return None 373 elif self._values['user_template'] in ['none', '']: 374 return '' 375 return self._values['user_template'] 376 377 @property 378 def ssl(self): 379 if self._values['ssl'] is None: 380 return None 381 elif self._values['ssl'] == 'start-tls': 382 return 'start-tls' 383 return flatten_boolean(self._values['ssl']) 384 385 386class ApiParameters(Parameters): 387 pass 388 389 390class ModuleParameters(Parameters): 391 pass 392 393 394class Changes(Parameters): 395 def to_return(self): 396 result = {} 397 try: 398 for returnable in self.returnables: 399 result[returnable] = getattr(self, returnable) 400 result = self._filter_params(result) 401 except Exception: 402 pass 403 return result 404 405 406class UsableChanges(Changes): 407 @property 408 def ssl_check_peer(self): 409 if self._values['ssl_check_peer'] is None: 410 return None 411 elif self._values['ssl_check_peer'] == 'yes': 412 return 'enabled' 413 return 'disabled' 414 415 @property 416 def fallback_to_local(self): 417 if self._values['fallback_to_local'] is None: 418 return None 419 elif self._values['fallback_to_local'] == 'yes': 420 return 'true' 421 return 'false' 422 423 @property 424 def check_member_attr(self): 425 if self._values['check_member_attr'] is None: 426 return None 427 elif self._values['check_member_attr'] == 'yes': 428 return 'enabled' 429 return 'disabled' 430 431 @property 432 def ssl(self): 433 if self._values['ssl'] is None: 434 return None 435 elif self._values['ssl'] == 'start-tls': 436 return 'start-tls' 437 elif self._values['ssl'] == 'yes': 438 return 'enabled' 439 return 'disabled' 440 441 442class ReportableChanges(Changes): 443 @property 444 def bind_password(self): 445 return None 446 447 @property 448 def ssl_check_peer(self): 449 return flatten_boolean(self._values['ssl_check_peer']) 450 451 @property 452 def check_member_attr(self): 453 return flatten_boolean(self._values['check_member_attr']) 454 455 @property 456 def ssl(self): 457 if self._values['ssl'] is None: 458 return None 459 elif self._values['ssl'] == 'start-tls': 460 return 'start-tls' 461 return flatten_boolean(self._values['ssl']) 462 463 464class Difference(object): 465 def __init__(self, want, have=None): 466 self.want = want 467 self.have = have 468 469 def compare(self, param): 470 try: 471 result = getattr(self, param) 472 return result 473 except AttributeError: 474 return self.__default(param) 475 476 def __default(self, param): 477 attr1 = getattr(self.want, param) 478 try: 479 attr2 = getattr(self.have, param) 480 if attr1 != attr2: 481 return attr1 482 except AttributeError: 483 return attr1 484 485 @property 486 def login_ldap_attr(self): 487 return cmp_str_with_none(self.want.login_ldap_attr, self.have.login_ldap_attr) 488 489 @property 490 def user_template(self): 491 return cmp_str_with_none(self.want.user_template, self.have.user_template) 492 493 @property 494 def ssl_ca_cert(self): 495 return cmp_str_with_none(self.want.ssl_ca_cert, self.have.ssl_ca_cert) 496 497 @property 498 def ssl_client_key(self): 499 return cmp_str_with_none(self.want.ssl_client_key, self.have.ssl_client_key) 500 501 @property 502 def ssl_client_cert(self): 503 return cmp_str_with_none(self.want.ssl_client_cert, self.have.ssl_client_cert) 504 505 @property 506 def bind_password(self): 507 if self.want.bind_password != self.have.bind_password and self.want.update_password == 'always': 508 return self.want.bind_password 509 510 511class ModuleManager(object): 512 def __init__(self, *args, **kwargs): 513 self.module = kwargs.get('module', None) 514 self.client = F5RestClient(**self.module.params) 515 self.want = ModuleParameters(params=self.module.params) 516 self.have = ApiParameters() 517 self.changes = UsableChanges() 518 519 def _set_changed_options(self): 520 changed = {} 521 for key in Parameters.returnables: 522 if getattr(self.want, key) is not None: 523 changed[key] = getattr(self.want, key) 524 if changed: 525 self.changes = UsableChanges(params=changed) 526 527 def _update_changed_options(self): 528 diff = Difference(self.want, self.have) 529 updatables = Parameters.updatables 530 changed = dict() 531 for k in updatables: 532 change = diff.compare(k) 533 if change is None: 534 continue 535 else: 536 if isinstance(change, dict): 537 changed.update(change) 538 else: 539 changed[k] = change 540 if changed: 541 self.changes = UsableChanges(params=changed) 542 return True 543 return False 544 545 def _announce_deprecations(self, result): 546 warnings = result.pop('__warnings', []) 547 for warning in warnings: 548 self.client.module.deprecate( 549 msg=warning['msg'], 550 version=warning['version'] 551 ) 552 553 def update_auth_source_on_device(self, source): 554 """Set the system auth source. 555 556 Configuring the authentication source is only one step in the process of setting 557 up an auth source. The other step is to inform the system of the auth source 558 you want to use. 559 560 This method is used for situations where 561 562 * The ``use_for_auth`` parameter is set to ``yes`` 563 * The ``use_for_auth`` parameter is set to ``no`` 564 * The ``state`` parameter is set to ``absent`` 565 566 When ``state`` equal to ``absent``, before you can delete the TACACS+ configuration, 567 you must set the system auth to "something else". The system ships with a system 568 auth called "local", so this is the logical "something else" to use. 569 570 When ``use_for_auth`` is no, the same situation applies as when ``state`` equal 571 to ``absent`` is done above. 572 573 When ``use_for_auth`` is ``yes``, this method will set the current system auth 574 state to TACACS+. 575 576 Arguments: 577 source (string): The source that you want to set on the device. 578 """ 579 params = dict( 580 type=source 581 ) 582 uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( 583 self.client.provider['server'], 584 self.client.provider['server_port'] 585 ) 586 587 resp = self.client.api.patch(uri, json=params) 588 try: 589 response = resp.json() 590 except ValueError as ex: 591 raise F5ModuleError(str(ex)) 592 593 if 'code' in response and response['code'] == 400: 594 if 'message' in response: 595 raise F5ModuleError(response['message']) 596 else: 597 raise F5ModuleError(resp.content) 598 599 def update_fallback_on_device(self, fallback): 600 params = dict( 601 fallback=fallback 602 ) 603 uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( 604 self.client.provider['server'], 605 self.client.provider['server_port'] 606 ) 607 608 resp = self.client.api.patch(uri, json=params) 609 try: 610 response = resp.json() 611 except ValueError as ex: 612 raise F5ModuleError(str(ex)) 613 614 if 'code' in response and response['code'] == 400: 615 if 'message' in response: 616 raise F5ModuleError(response['message']) 617 else: 618 raise F5ModuleError(resp.content) 619 620 def exec_module(self): 621 changed = False 622 result = dict() 623 state = self.want.state 624 625 if state == "present": 626 changed = self.present() 627 elif state == "absent": 628 changed = self.absent() 629 630 reportable = ReportableChanges(params=self.changes.to_return()) 631 changes = reportable.to_return() 632 result.update(**changes) 633 result.update(dict(changed=changed)) 634 self._announce_deprecations(result) 635 return result 636 637 def present(self): 638 if self.exists(): 639 return self.update() 640 else: 641 return self.create() 642 643 def absent(self): 644 if self.exists(): 645 return self.remove() 646 return False 647 648 def should_update(self): 649 result = self._update_changed_options() 650 if result: 651 return True 652 return False 653 654 def update(self): 655 self.have = self.read_current_from_device() 656 if not self.should_update(): 657 return False 658 if self.module.check_mode: 659 return True 660 self.update_on_device() 661 if self.want.fallback_to_local == 'yes': 662 self.update_fallback_on_device('true') 663 elif self.want.fallback_to_local == 'no': 664 self.update_fallback_on_device('false') 665 return True 666 667 def remove(self): 668 if self.module.check_mode: 669 return True 670 self.update_auth_source_on_device('local') 671 self.remove_from_device() 672 if self.exists(): 673 raise F5ModuleError("Failed to delete the resource.") 674 return True 675 676 def create(self): 677 self._set_changed_options() 678 if self.module.check_mode: 679 return True 680 self.create_on_device() 681 if self.want.fallback_to_local == 'yes': 682 self.update_fallback_on_device('true') 683 elif self.want.fallback_to_local == 'no': 684 self.update_fallback_on_device('false') 685 return True 686 687 def exists(self): 688 uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( 689 self.client.provider['server'], 690 self.client.provider['server_port'], 691 transform_name('Common', 'system-auth') 692 ) 693 resp = self.client.api.get(uri) 694 try: 695 response = resp.json() 696 except ValueError: 697 return False 698 if resp.status == 404 or 'code' in response and response['code'] == 404: 699 return False 700 return True 701 702 def create_on_device(self): 703 params = self.changes.api_params() 704 params['name'] = 'system-auth' 705 params['partition'] = 'Common' 706 uri = "https://{0}:{1}/mgmt/tm/auth/ldap/".format( 707 self.client.provider['server'], 708 self.client.provider['server_port'], 709 ) 710 resp = self.client.api.post(uri, json=params) 711 try: 712 response = resp.json() 713 except ValueError as ex: 714 raise F5ModuleError(str(ex)) 715 716 if 'code' in response and response['code'] in [400, 409]: 717 if 'message' in response: 718 raise F5ModuleError(response['message']) 719 else: 720 raise F5ModuleError(resp.content) 721 return True 722 723 def update_on_device(self): 724 params = self.changes.api_params() 725 if not params: 726 return 727 uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( 728 self.client.provider['server'], 729 self.client.provider['server_port'], 730 transform_name('Common', 'system-auth') 731 ) 732 resp = self.client.api.patch(uri, json=params) 733 try: 734 response = resp.json() 735 except ValueError as ex: 736 raise F5ModuleError(str(ex)) 737 738 if 'code' in response and response['code'] == 400: 739 if 'message' in response: 740 raise F5ModuleError(response['message']) 741 else: 742 raise F5ModuleError(resp.content) 743 744 def remove_from_device(self): 745 uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( 746 self.client.provider['server'], 747 self.client.provider['server_port'], 748 transform_name('Common', 'system-auth') 749 ) 750 response = self.client.api.delete(uri) 751 if response.status == 200: 752 return True 753 raise F5ModuleError(response.content) 754 755 def read_current_from_device(self): 756 uri = "https://{0}:{1}/mgmt/tm/auth/ldap/{2}".format( 757 self.client.provider['server'], 758 self.client.provider['server_port'], 759 transform_name('Common', 'system-auth') 760 ) 761 resp = self.client.api.get(uri) 762 try: 763 response = resp.json() 764 except ValueError as ex: 765 raise F5ModuleError(str(ex)) 766 767 if 'code' in response and response['code'] == 400: 768 if 'message' in response: 769 raise F5ModuleError(response['message']) 770 else: 771 raise F5ModuleError(resp.content) 772 result = ApiParameters(params=response) 773 774 uri = 'https://{0}:{1}/mgmt/tm/auth/source/'.format( 775 self.client.provider['server'], 776 self.client.provider['server_port'] 777 ) 778 resp = self.client.api.get(uri) 779 try: 780 response = resp.json() 781 except ValueError as ex: 782 raise F5ModuleError(str(ex)) 783 784 if 'code' in response and response['code'] == 400: 785 if 'message' in response: 786 raise F5ModuleError(response['message']) 787 else: 788 raise F5ModuleError(resp.content) 789 result.update({'fallback': response['fallback']}) 790 return result 791 792 793class ArgumentSpec(object): 794 def __init__(self): 795 self.supports_check_mode = True 796 argument_spec = dict( 797 servers=dict(type='list'), 798 port=dict(type='int'), 799 remote_directory_tree=dict(), 800 scope=dict( 801 choices=['sub', 'one', 'base'] 802 ), 803 bind_dn=dict(), 804 bind_password=dict(no_log=True), 805 user_template=dict(), 806 check_member_attr=dict(type='bool'), 807 ssl=dict( 808 choices=['yes', 'no', 'start-tls'] 809 ), 810 ca_cert=dict(aliases=['ssl_ca_cert']), 811 client_key=dict(aliases=['ssl_client_key']), 812 client_cert=dict(aliases=['ssl_client_cert']), 813 validate_certs=dict(type='bool', aliases=['ssl_check_peer']), 814 login_ldap_attr=dict(), 815 fallback_to_local=dict(type='bool'), 816 update_password=dict( 817 default='always', 818 choices=['always', 'on_create'] 819 ), 820 state=dict(default='present', choices=['absent', 'present']), 821 ) 822 self.argument_spec = {} 823 self.argument_spec.update(f5_argument_spec) 824 self.argument_spec.update(argument_spec) 825 826 827def main(): 828 spec = ArgumentSpec() 829 830 module = AnsibleModule( 831 argument_spec=spec.argument_spec, 832 supports_check_mode=spec.supports_check_mode, 833 ) 834 835 try: 836 mm = ModuleManager(module=module) 837 results = mm.exec_module() 838 module.exit_json(**results) 839 except F5ModuleError as ex: 840 module.fail_json(msg=str(ex)) 841 842 843if __name__ == '__main__': 844 main() 845