├── .gitignore └── orion_node.py /.gitignore: -------------------------------------------------------------------------------- 1 | ansible/ 2 | args.json 3 | -------------------------------------------------------------------------------- /orion_node.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | 4 | # Copyright: (c) 2021, Ashley Hooper 5 | # Copyright: (c) 2019, Jarett D. Chaiken 6 | # GNU General Public License v3.0+ 7 | # (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 8 | 9 | ANSIBLE_METADATA = { 10 | 'metadata_version': '2.1.1', 11 | 'status': ['preview'], 12 | 'supported_by': 'community', 13 | } 14 | 15 | DOCUMENTATION = r''' 16 | --- 17 | module: orion_node 18 | short_description: Creates/Removes/Edits Nodes in Solarwinds Orion NPM 19 | description: 20 | - Create/Remove/Edit Nodes in SolarWinds Orion NPM. 21 | version_added: "2.7" 22 | author: "Jarett D Chaiken (@jdchaiken)" 23 | options: 24 | orion_hostname: 25 | description: 26 | - Name of Orion host running SWIS service 27 | required: true 28 | orion_username: 29 | description: 30 | - Orion Username 31 | - Active Directory users may use DOMAIN\\username or username@DOMAIN format 32 | required: true 33 | orion_password: 34 | description: 35 | - Password for Orion user 36 | required: true 37 | state: 38 | description: 39 | - The desired state of the node 40 | required: false 41 | choices: 42 | - present 43 | - absent 44 | - remanaged 45 | - unmanaged 46 | - muted 47 | - unmuted 48 | default: 49 | - remanaged 50 | node_id: 51 | description: 52 | - node_id of the node 53 | - Must provide either an IP address, node_id, or exact node name 54 | required: false 55 | name: 56 | description: 57 | - Hostname of the node 58 | - For Adding a node this field is required 59 | - For All other states field is optional and partial names are acceptable 60 | required: false 61 | ip_address: 62 | description: 63 | - IP Address of the node 64 | - Must provide either an IP address, node_id, or exact node name 65 | required: false 66 | unmanage_from: 67 | description: 68 | - "The date and time (in ISO 8601 UTC format) to begin the unmanage period." 69 | - If this is in the past, the node will be unmanaged effective immediately. 70 | - If not provided, module defaults to now. 71 | - "ex: 2017-02-21T12:00:00Z" 72 | required: false 73 | unmanage_until: 74 | description: 75 | - "The date and time (in ISO 8601 UTC format) to end the unmanage period." 76 | - You can set this as far in the future as you like. 77 | - If not provided, module defaults to 24 hours from now. 78 | - "ex: 2017-02-21T12:00:00Z" 79 | required: false 80 | polling_method: 81 | description: 82 | - Polling method to use 83 | choices: 84 | - External 85 | - icmp 86 | - snmp 87 | - WMI 88 | - agent 89 | - vcenter 90 | - meraki 91 | default: snmp 92 | required: false 93 | polling_engine_name: 94 | description: 95 | - Name of polling engine that NPM will use to poll this device 96 | required: false 97 | type: str 98 | polling_engine_id: 99 | description: 100 | - ID of polling engine that NPM will use to poll this device 101 | required: false 102 | type: int 103 | snmp_version: 104 | description: 105 | - SNMPv2c is used by default 106 | - SNMPv3 requires use of existing, named SNMPv3 credentials within Orion 107 | choices: 108 | - 2 109 | - 3 110 | default: 2 111 | required: false 112 | type: int 113 | snmp_port: 114 | description: 115 | - port that SNMP server listens on 116 | required: false 117 | default: 161 118 | type: int 119 | snmp_allow_64: 120 | description: 121 | - Set true if device supports 64-bit counters 122 | type: bool 123 | default: true 124 | required: false 125 | credential_name: 126 | description: 127 | - The named, existing credential to use to manage this device 128 | required: true 129 | type: str 130 | interface_filters: 131 | description: 132 | - List of SolarWinds Orion interface discovery filters 133 | required: false 134 | type: list 135 | volume_filters: 136 | description: 137 | - List of regular expressions by which to exclude volumes from monitoring 138 | required: false 139 | type: list 140 | custom_properties: 141 | description: 142 | - A dictionary containing custom properties and their values 143 | required: false 144 | type: dict 145 | requirements: 146 | - orionsdk 147 | - datetime 148 | - dateutil 149 | - requests 150 | - traceback 151 | ''' 152 | 153 | EXAMPLES = r''' 154 | - name: Remove a node from Orion 155 | orion_node: 156 | orion_hostname: "" 157 | orion_username: Orion Username 158 | orion_password: Orion Password 159 | name: servername 160 | state: absent 161 | 162 | - name: Mute hosts 163 | hosts: all 164 | gather_facts: no 165 | tasks: 166 | - orion_node: 167 | orion_hostname: "{{solarwinds_host}}" 168 | orion_username: "{{solarwinds_username}}" 169 | orion_password: "{{solarwinds_password}}" 170 | name: "{{inventory_hostname}}" 171 | state: muted 172 | unmanage_from: "2020-03-13T20:58:22.033" 173 | unmanage_until: "2020-03-14T20:58:22.033" 174 | delegate_to: localhost 175 | ''' 176 | 177 | import traceback 178 | from datetime import datetime, timedelta, timezone 179 | from dateutil.parser import parse 180 | import re 181 | import requests 182 | import time 183 | from ansible.module_utils.basic import AnsibleModule 184 | from ansible.module_utils._text import to_native 185 | 186 | try: 187 | from orionsdk import SwisClient 188 | HAS_ORION = True 189 | except Exception as e: 190 | HAS_ORION = False 191 | 192 | __SWIS__ = None 193 | 194 | # These constants control how many times and at what interval this module 195 | # will check the status of the Orion discovery job to see if it has completed. 196 | # Total time will be retries multiplied by sleep seconds. 197 | DISCOVERY_STATUS_CHECK_RETRIES = 60 198 | DISCOVERY_RETRY_SLEEP_SECS = 3 199 | # These control the discovery timeouts within Orion itself. 200 | ORION_DISCOVERY_JOB_TIMEOUT_SECS=300 201 | ORION_DISCOVERY_SEARCH_TIMEOUT_MS=20000 202 | ORION_DISCOVERY_SNMP_TIMEOUT_MS=30000 203 | ORION_DISCOVERY_REPEAT_INTERVAL_MS=3000 204 | 205 | requests.urllib3.disable_warnings() 206 | 207 | 208 | def run_module(): 209 | ''' 210 | Module Main Function 211 | ''' 212 | global __SWIS__ 213 | 214 | module_args = { 215 | 'orion_hostname': {'required': True}, 216 | 'orion_username': {'required': True, 'no_log': True}, 217 | 'orion_password': {'required': True, 'no_log': True}, 218 | 'state': { 219 | 'required': False, 220 | 'choices': [ 221 | 'present', 222 | 'absent', 223 | 'remanaged', 224 | 'unmanaged', 225 | 'muted', 226 | 'unmuted', 227 | ], 228 | 'default': 'managed' 229 | }, 230 | 'node_id': {'required': False}, 231 | 'ip_address': {'required': False}, 232 | 'name': {'required': False}, 233 | 'unmanage_from': {'required': False, 'default': "None"}, 234 | 'unmanage_until': {'required': False, 'default': "None"}, 235 | 'polling_method': { 236 | 'required': False, 237 | 'choices': [ 238 | 'external', 239 | 'icmp', 240 | 'snmp', 241 | 'wmi', 242 | 'agent'], 243 | 'default': 'snmp' 244 | }, 245 | 'polling_engine_name': {'required': False}, 246 | 'polling_engine_id': {'required': False}, 247 | 'snmp_version': {'required': False, 'default': 2}, 248 | 'snmp_port': {'required': False, 'default': 161}, 249 | 'snmp_allow_64': {'required': False, 'default': True}, 250 | 'credential_name': {'required': False}, 251 | 'interface_filters': {'required': False, 'type': 'list', 'default': []}, 252 | 'volume_filters': {'required': False, 'type': 'list', 'default': []}, 253 | 'custom_properties': {'required': False, 'type': 'dict', 'default': {}} 254 | } 255 | 256 | module = AnsibleModule(argument_spec=module_args, supports_check_mode=True) 257 | 258 | if not HAS_ORION: 259 | module.fail_json(msg='orionsdk required for this module') 260 | 261 | options = { 262 | 'hostname': module.params['orion_hostname'], 263 | 'username': module.params['orion_username'], 264 | 'password': module.params['orion_password'], 265 | } 266 | 267 | __SWIS__ = SwisClient(**options) 268 | 269 | try: 270 | __SWIS__.query('SELECT uri FROM Orion.Environment') 271 | except Exception as e: 272 | module.fail_json( 273 | msg='Failed to query Orion. ' 274 | 'Check Hostname, Username, and/or Password: {0}'.format(str(e)) 275 | ) 276 | 277 | if module.params['state'] == 'present': 278 | add_node(module) 279 | elif module.params['state'] == 'absent': 280 | remove_node(module) 281 | elif module.params['state'] == 'remanaged': 282 | remanage_node(module) 283 | elif module.params['state'] == 'unmanaged': 284 | unmanage_node(module) 285 | elif module.params['state'] == 'muted': 286 | mute_node(module) 287 | elif module.params['state'] == 'unmuted': 288 | unmute_node(module) 289 | 290 | def _get_node(module): 291 | node = {} 292 | if module.params['node_id'] is not None: 293 | results = __SWIS__.query( 294 | 'SELECT NodeID, Caption, Unmanaged, UnManageFrom, UnManageUntil, Uri FROM Orion.Nodes WHERE NodeID = @node_id', 295 | node_id=module.params['node_id'] 296 | ) 297 | elif module.params['ip_address'] is not None: 298 | results = __SWIS__.query( 299 | 'SELECT NodeID, Caption, Unmanaged, UnManageFrom, UnManageUntil, Uri FROM Orion.Nodes WHERE IPAddress = @ip_address', 300 | ip_address=module.params['ip_address'] 301 | ) 302 | elif module.params['name'] is not None: 303 | results = __SWIS__.query( 304 | 'SELECT NodeID, Caption, Unmanaged, UnManageFrom, UnManageUntil, Uri FROM Orion.Nodes WHERE Caption = @name', 305 | name=module.params['name'] 306 | ) 307 | else: 308 | # No Id provided 309 | module.fail_json(msg='You must provide either a node_id, ip_address, or name') 310 | 311 | if results['results']: 312 | node['nodeid'] = results['results'][0]['NodeID'] 313 | node['caption'] = results['results'][0]['Caption'] 314 | node['netobjectid'] = 'N:{}'.format(node['nodeid']) 315 | node['unmanaged'] = results['results'][0]['Unmanaged'] 316 | node['unmanagefrom'] = parse(results['results'][0]['UnManageFrom']).isoformat() 317 | node['unmanageuntil'] = parse(results['results'][0]['UnManageUntil']).isoformat() 318 | node['uri'] = results['results'][0]['Uri'] 319 | return node 320 | 321 | def _get_credential_id(module): 322 | credential_name = module.params['credential_name'] 323 | try: 324 | credentials_res = __SWIS__.query("SELECT ID FROM Orion.Credential WHERE Name = @name", name = credential_name) 325 | try: 326 | return next(c for c in credentials_res['results'])['ID'] 327 | except Exception as e: 328 | module.fail_json(msg='Failed to query credential {}'.format(str(e))) 329 | except Exception as e: 330 | module.fail_json(msg='Failed to query credentials {}'.format(str(e))) 331 | 332 | def _get_polling_engine_id(module): 333 | polling_engine_name = module.params['polling_engine_name'] 334 | try: 335 | engines_res = __SWIS__.query("SELECT EngineID, ServerName, PollingCompletion FROM Orion.Engines WHERE ServerName = @engine_name", engine_name = polling_engine_name) 336 | return next(e for e in engines_res['results'])['EngineID'] 337 | except Exception as e: 338 | module.fail_json(msg='Failed to query polling engines {}'.format(str(e))) 339 | 340 | def _validate_fields(module): 341 | params = module.params 342 | # Setup properties for new node 343 | # module.fail_json(msg='FAIL NOW', **params) 344 | props = { 345 | 'IPAddress': params['ip_address'], 346 | 'Caption': params['name'], 347 | 'ObjectSubType': params['polling_method'].upper(), 348 | 'SNMPVersion': params['snmp_version'], 349 | 'AgentPort': params['snmp_port'], 350 | 'Allow64BitCounters': params['snmp_allow_64'], 351 | 'External': True if params['polling_method'] == 'external' else False, 352 | } 353 | 354 | # Validate required fields 355 | if not props['IPAddress']: 356 | module.fail_json(msg='IP Address is required') 357 | 358 | if not props['External']: 359 | if not props['Caption']: 360 | module.fail_json(msg='Node name is required') 361 | 362 | if not props['ObjectSubType']: 363 | module.fail_json(msg='Polling Method is required [external, snmp, icmp, wmi, agent]') 364 | elif props['ObjectSubType'] == 'SNMP': 365 | if not props['SNMPVersion']: 366 | print("Defaulting to SNMPv2") 367 | props['SNMPVersion'] = '2' 368 | if not props['AgentPort']: 369 | print("Using default SNMP port") 370 | props['AgentPort'] = '161' 371 | elif props['ObjectSubType'] == 'EXTERNAL': 372 | props['ObjectSubType'] = 'ICMP' 373 | 374 | if not params['credential_name']: 375 | module.fail_json(msg='A credential name is required') 376 | 377 | if not props['Allow64BitCounters']: 378 | props['Allow64BitCounters'] = True 379 | 380 | if params['polling_engine_name']: 381 | props['EngineID'] =_get_polling_engine_id(module) 382 | else: 383 | print("Using default polling engine") 384 | props['EngineID'] = 1 385 | 386 | if params['state'] == 'present': 387 | if not props['Caption']: 388 | module.fail_json(msg='Node name is required') 389 | 390 | return props 391 | 392 | def add_node(module): 393 | changed = False 394 | # Check if node already exists and exit if found 395 | node = _get_node(module) 396 | if node: 397 | module.exit_json(changed=False, ansible_facts=node) 398 | 399 | # Validate Fields 400 | props = _validate_fields(module) 401 | 402 | # Start to prepare our discovery profile 403 | core_plugin_context = { 404 | 'BulkList': [{'Address': module.params['ip_address']}], 405 | 'Credentials': [ 406 | { 407 | 'CredentialID': _get_credential_id(module), 408 | 'Order': 1 409 | } 410 | ], 411 | 'WmiRetriesCount': 0, 412 | 'WmiRetryIntervalMiliseconds': 1000 413 | } 414 | 415 | try: 416 | core_plugin_config = __SWIS__.invoke('Orion.Discovery', 'CreateCorePluginConfiguration', core_plugin_context) 417 | except Exception as e: 418 | module.fail_json(msg='Failed to create core plugin configuration {}'.format(str(e)), **props) 419 | 420 | expression_filters = [ 421 | {"Prop": "Descr", "Op": "!Any", "Val": "null"}, 422 | {"Prop": "Descr", "Op": "!Any", "Val": "vlan"}, 423 | {"Prop": "Descr", "Op": "!Any", "Val": "loopback"}, 424 | {"Prop": "Descr", "Op": "!Regex", "Val": "^$"}, 425 | ] 426 | expression_filters += module.params['interface_filters'] 427 | 428 | interfaces_plugin_context = { 429 | "AutoImportStatus": ['Up'], 430 | "AutoImportVlanPortTypes": ['Trunk', 'Access', 'Unknown'], 431 | "AutoImportVirtualTypes": ['Physical', 'Virtual'], 432 | "AutoImportExpressionFilter": expression_filters 433 | } 434 | 435 | try: 436 | interfaces_plugin_config = __SWIS__.invoke('Orion.NPM.Interfaces', 'CreateInterfacesPluginConfiguration', interfaces_plugin_context) 437 | except Exception as e: 438 | module.fail_json(msg='Failed to create interfaces plugin configuration {}'.format(str(e)), **props) 439 | 440 | discovery_name = "orion_node.py.{}".format(datetime.now().isoformat()) 441 | discovery_profile = { 442 | 'Name': discovery_name, 443 | 'EngineID': props['EngineID'], 444 | 'JobTimeoutSeconds': ORION_DISCOVERY_JOB_TIMEOUT_SECS, 445 | 'SearchTimeoutMiliseconds': ORION_DISCOVERY_SEARCH_TIMEOUT_MS, 446 | 'SnmpTimeoutMiliseconds': ORION_DISCOVERY_SNMP_TIMEOUT_MS, 447 | 'RepeatIntervalMiliseconds': ORION_DISCOVERY_REPEAT_INTERVAL_MS, 448 | 'SnmpRetries': 2, 449 | 'SnmpPort': module.params['snmp_port'], 450 | 'HopCount': 0, 451 | 'PreferredSnmpVersion': 'SNMP' + str(module.params['snmp_version']), 452 | 'DisableIcmp': False, 453 | 'AllowDuplicateNodes': False, 454 | 'IsAutoImport': True, 455 | 'IsHidden': False, 456 | 'PluginConfigurations': [ 457 | {'PluginConfigurationItem': core_plugin_config}, 458 | {'PluginConfigurationItem': interfaces_plugin_config} 459 | ] 460 | } 461 | 462 | # Initiate discovery job with above discovery profile 463 | try: 464 | discovery_res = __SWIS__.invoke('Orion.Discovery', 'StartDiscovery', discovery_profile) 465 | changed = True 466 | except Exception as e: 467 | module.fail_json(msg='Failed to start node discovery: {}'.format(str(e)), **props) 468 | discovery_profile_id = int(discovery_res) 469 | 470 | # Loop until discovery job finished 471 | # Discovery job statuses are: 472 | # 0 {"Unknown"} 1 {"InProgress"} 2 {"Finished"} 3 {"Error"} 4 {"NotScheduled"} 5 {"Scheduled"} 6 {"NotCompleted"} 7 {"Canceling"} 8 {"ReadyForImport"} 473 | # https://github.com/solarwinds/OrionSDK/blob/master/Samples/PowerShell/DiscoverSnmpV3Node.ps1 474 | discovery_active = True 475 | discovery_iter = 0 476 | while discovery_active: 477 | try: 478 | status_res = __SWIS__.query("SELECT Status FROM Orion.DiscoveryProfiles WHERE ProfileID = @profile_id", profile_id=discovery_profile_id) 479 | except Exception as e: 480 | module.fail_json(msg='Failed to query node discovery status: {}'.format(str(e)), **props) 481 | if len(status_res['results']) > 0: 482 | if next(s for s in status_res['results'])['Status'] == 2: 483 | discovery_active = False 484 | else: 485 | discovery_active = False 486 | discovery_iter += 1 487 | if discovery_iter >= DISCOVERY_STATUS_CHECK_RETRIES: 488 | module.fail_json(msg='Timeout while waiting for node discovery job to terminate', **props) 489 | time.sleep(DISCOVERY_RETRY_SLEEP_SECS) 490 | 491 | # Retrieve Result and BatchID to find items added to new node by discovery 492 | try: 493 | discovery_log_res = __SWIS__.query("SELECT Result, ResultDescription, ErrorMessage, BatchID FROM Orion.DiscoveryLogs WHERE ProfileID = @profile_id", profile_id=discovery_profile_id) 494 | except Exception as e: 495 | module.fail_json(msg='Failed to query discovery logs: {}'.format(str(e)), **props) 496 | discovery_log = discovery_log_res['results'][0] 497 | 498 | # Any of the below values for Result indicate a failure, so we'll abort 499 | if int(discovery_log['Result']) in [0, 3, 6, 7]: 500 | module.fail_json(msg='Node discovery did not complete successfully: {}'.format(str(discovery_log_res))) 501 | 502 | # Look up NodeID of node we discovered. We have to do all of these joins 503 | # because mysteriously, the NodeID in the DiscoveredNodes table has no 504 | # bearing on the actual NodeID of the host(s) discovered. 505 | try: 506 | discovered_nodes_res = __SWIS__.query("SELECT n.NodeID, Caption, n.Uri FROM Orion.DiscoveryProfiles dp JOIN Orion.DiscoveredNodes dn ON dn.ProfileID = dp.ProfileID JOIN Orion.Nodes n ON n.DNS = dn.DNS OR n.Caption = dn.SysName WHERE dp.Name = @discovery_name", discovery_name=discovery_name) 507 | except Exception as e: 508 | module.fail_json(msg='Failed to query discovered nodes: {}'.format(str(e)), **props) 509 | 510 | try: 511 | discovered_node = discovered_nodes_res['results'][0] 512 | except Exception as e: 513 | module.fail_json(msg="Node '{}' not found in discovery results: {}".format(module.params['name'], str(e)), **props) 514 | 515 | discovered_node_id = discovered_node['NodeID'] 516 | # Check if we need to re-set the caption for the discovered node 517 | if discovered_node['Caption'] != module.params['name']: 518 | try: 519 | __SWIS__.update(discovered_node['Uri'], caption=module.params['name']) 520 | except Exception as e: 521 | module.fail_json(msg="Failed to update node Caption from '{}' to '{}': {}".format(discovered_node['Caption'], module.params['name'], str(e)), **props) 522 | 523 | # Retrieve all items added by discovery profile 524 | try: 525 | discovered_objects_res = __SWIS__.query("SELECT EntityType, DisplayName, NetObjectID FROM Orion.DiscoveryLogItems WHERE BatchID = @batch_id", batch_id=discovery_log['BatchID']) 526 | except Exception as e: 527 | module.fail_json(msg='Failed to query discovered objects: {}'.format(str(e)), **props) 528 | 529 | volumes_to_remove = [] 530 | for entry in discovered_objects_res['results']: 531 | if entry['EntityType'] == 'Orion.Volumes': 532 | for vol_filter in module.params['volume_filters']: 533 | vol_filter_regex = "^{} - ".format(module.params['name']) + vol_filter['regex'] 534 | if re.search(vol_filter_regex, entry['DisplayName']): 535 | volumes_to_remove.append(entry) 536 | if len(volumes_to_remove) > 50: 537 | module.fail_json(msg='Too many volumes to remove ({}) - aborting for safety'.format(str(len(volumes_to_remove))), **props) 538 | 539 | volume_removal_uris = [] 540 | for volume in volumes_to_remove: 541 | try: 542 | volume_lookup_res = __SWIS__.query("SELECT Uri FROM Orion.Volumes WHERE NodeID = @node_id AND Concat('V:', ToString(VolumeID)) = @net_object_id", node_id=discovered_node_id, net_object_id=volume['NetObjectID']) 543 | except Exception as e: 544 | module.fail_json(msg='Failed to query Uri for volume to remove: {}'.format(str(e)), **props) 545 | 546 | volume_uri = volume_lookup_res['results'][0]['Uri'] 547 | if volume_uri: 548 | try: 549 | __SWIS__.delete(volume_uri) 550 | except Exception as e: 551 | module.fail_json(msg='Failed to delete volume: {}'.format(str(e)), **props) 552 | 553 | # Add Custom Properties 554 | custom_properties = module.params['custom_properties'] if 'custom_properties' in module.params else {} 555 | 556 | if not props['External']: 557 | try: 558 | node = _get_node(module) 559 | except Exception as e: 560 | module.fail_json(msg='Failed to look up node details {}'.format(str(e))) 561 | 562 | if type(custom_properties) is dict: 563 | for k in custom_properties.keys(): 564 | custom_property = { k: custom_properties[k] } 565 | try: 566 | __SWIS__.update(node['uri'] + '/CustomProperties', **custom_property) 567 | except Exception as e: 568 | module.fail_json(msg='Failed to add custom properties',**node) 569 | 570 | node['changed'] = changed 571 | module.exit_json(**node) 572 | 573 | def remove_node(module): 574 | node = _get_node(module) 575 | if not node: 576 | module.exit_json(changed=False) 577 | 578 | try: 579 | __SWIS__.delete(node['uri']) 580 | node['changed'] = True 581 | module.exit_json(**node) 582 | except Exception as e: 583 | module.fail_json(msg='Error removing node {}'.format(str(e)), **node) 584 | 585 | def remanage_node(module): 586 | node = _get_node(module) 587 | if not node: 588 | module.fail_json(skipped=True, msg='Node not found') 589 | elif not node['unmanaged']: 590 | module.fail_json(changed=False, msg='Node is not currently unmanaged') 591 | try: 592 | __SWIS__.invoke('Orion.Nodes', 'Remanage', node['netobjectid']) 593 | module.exit_json(changed=True, msg="{0} has been remanaged".format(node['caption'])) 594 | except Exception as e: 595 | module.fail_json(msg=to_native(e), exception=traceback.format_exc()) 596 | 597 | def unmanage_node(module): 598 | node = _get_node(module) 599 | if not node: 600 | module.fail_json(skipped=True, msg='Node not found') 601 | 602 | now_dt = datetime.now(timezone.utc) 603 | unmanage_from = module.params['unmanage_from'] 604 | unmanage_until = module.params['unmanage_until'] 605 | 606 | if unmanage_from != "None": 607 | unmanage_from_dt = datetime.fromisoformat(unmanage_from) 608 | else: 609 | unmanage_from_dt = now_dt 610 | if unmanage_until != "None": 611 | unmanage_until_dt = datetime.fromisoformat(unmanage_until) 612 | else: 613 | tomorrow_dt = now_dt + timedelta(days=1) 614 | unmanage_until_dt = tomorrow_dt 615 | 616 | if node['unmanaged']: 617 | if unmanage_from_dt.isoformat() == node['unmanagefrom'] and unmanage_until_dt.isoformat() == node['unmanageuntil']: 618 | module.exit_json(changed=False) 619 | 620 | try: 621 | __SWIS__.invoke( 622 | 'Orion.Nodes', 623 | 'Unmanage', 624 | node['netobjectid'], 625 | str(unmanage_from_dt.astimezone(timezone.utc)).replace('+00:00', 'Z'), 626 | str(unmanage_until_dt.astimezone(timezone.utc)).replace('+00:00', 'Z'), 627 | False # use Absolute Time 628 | ) 629 | msg = "Node {0} will be unmanaged from {1} until {2}".format( 630 | node['caption'], 631 | unmanage_from_dt.astimezone().isoformat("T", "minutes"), 632 | unmanage_until_dt.astimezone().isoformat("T", "minutes") 633 | ) 634 | module.exit_json(changed=True, msg=msg, ansible_facts=node) 635 | except Exception as e: 636 | module.fail_json(msg="Failed to unmanage {0}".format(node['caption']), ansible_facts=node) 637 | 638 | def mute_node(module): 639 | node = _get_node(module) 640 | if not node: 641 | module.exit_json(skipped=True, msg='Node not found') 642 | 643 | now_dt = datetime.now(timezone.utc) 644 | unmanage_from = module.params['unmanage_from'] 645 | unmanage_until = module.params['unmanage_until'] 646 | 647 | if unmanage_from != "None": 648 | unmanage_from_dt = datetime.fromisoformat(unmanage_from) 649 | else: 650 | unmanage_from_dt = now_dt 651 | if unmanage_until != "None": 652 | unmanage_until_dt = datetime.fromisoformat(unmanage_until) 653 | else: 654 | tomorrow_dt = now_dt + timedelta(days=1) 655 | unmanage_until_dt = tomorrow_dt 656 | 657 | unmanage_from_dt = unmanage_from_dt.astimezone() 658 | unmanage_until_dt = unmanage_until_dt.astimezone() 659 | 660 | # Check if already muted 661 | suppressed = __SWIS__.invoke('Orion.AlertSuppression','GetAlertSuppressionState',[node['uri']])[0] 662 | 663 | # If already muted, exit 664 | if suppressed['SuppressedFrom'] == unmanage_from and suppressed['SuppressedUntil'] == unmanage_until: 665 | node['changed']=False 666 | module.exit_json(changed=False, ansible_facts=node) 667 | 668 | # Otherwise Mute Node with given parameters 669 | try: 670 | __SWIS__.invoke( 671 | 'Orion.AlertSuppression', 672 | 'SuppressAlerts', 673 | [node['uri']], 674 | str(unmanage_from_dt.astimezone(timezone.utc)).replace('+00:00', 'Z'), 675 | str(unmanage_until_dt.astimezone(timezone.utc)).replace('+00:00', 'Z') 676 | ) 677 | msg = "Node {0} will be muted from {1} until {2}".format( 678 | node['caption'], 679 | unmanage_from_dt.astimezone().isoformat("T", "minutes"), 680 | unmanage_until_dt.astimezone().isoformat("T", "minutes") 681 | ) 682 | module.exit_json(changed=True, msg=msg, ansible_facts=node) 683 | except Exception as e: 684 | module.fail_json(msg="Failed to mute {0}".format(node['caption']), ansible_facts=node) 685 | 686 | def unmute_node(module): 687 | node = _get_node(module) 688 | if not node: 689 | module.exit_json(skipped=True, msg='Node not found') 690 | 691 | # Check if already muted 692 | suppressed = __SWIS__.invoke('Orion.AlertSuppression','GetAlertSuppressionState',[node['uri']])[0] 693 | 694 | if suppressed['SuppressionMode'] == 0: 695 | node['changed'] = False 696 | module.exit_json(changed=False, ansible_facts=node) 697 | else: 698 | __SWIS__.invoke('Orion.AlertSuppression', 'ResumeAlerts',[node['uri']]) 699 | module.exit_json(changed=True, msg="{0} has been unmuted".format(node['caption']), ansible_facts=node) 700 | 701 | 702 | def main(): 703 | run_module() 704 | 705 | if __name__ == "__main__": 706 | main() 707 | --------------------------------------------------------------------------------