1""" 2Manage chassis via Salt Proxies. 3 4.. versionadded:: 2015.8.2 5 6Below is an example state that sets basic parameters: 7 8.. code-block:: yaml 9 10 my-dell-chassis: 11 dellchassis.chassis: 12 - chassis_name: my-dell-chassis 13 - datacenter: dc-1-us 14 - location: my-location 15 - mode: 2 16 - idrac_launch: 1 17 - slot_names: 18 - server-1: my-slot-name 19 - server-2: my-other-slot-name 20 - blade_power_states: 21 - server-1: on 22 - server-2: off 23 - server-3: powercycle 24 25However, it is possible to place the entire set of chassis configuration 26data in pillar. Here's an example pillar structure: 27 28.. code-block:: yaml 29 30 proxy: 31 host: 10.27.20.18 32 admin_username: root 33 fallback_admin_username: root 34 passwords: 35 - super-secret 36 - old-secret 37 proxytype: fx2 38 39 chassis: 40 name: fx2-1 41 username: root 42 password: saltstack1 43 datacenter: london 44 location: rack-1-shelf-3 45 management_mode: 2 46 idrac_launch: 0 47 slot_names: 48 - 'server-1': blade1 49 - 'server-2': blade2 50 51 servers: 52 server-1: 53 idrac_password: saltstack1 54 ipmi_over_lan: True 55 ip: 172.17.17.132 56 netmask: 255.255.0.0 57 gateway: 172.17.17.1 58 server-2: 59 idrac_password: saltstack1 60 ipmi_over_lan: True 61 ip: 172.17.17.2 62 netmask: 255.255.0.0 63 gateway: 172.17.17.1 64 server-3: 65 idrac_password: saltstack1 66 ipmi_over_lan: True 67 ip: 172.17.17.20 68 netmask: 255.255.0.0 69 gateway: 172.17.17.1 70 server-4: 71 idrac_password: saltstack1 72 ipmi_over_lan: True 73 ip: 172.17.17.2 74 netmask: 255.255.0.0 75 gateway: 172.17.17.1 76 77 switches: 78 switch-1: 79 ip: 192.168.1.2 80 netmask: 255.255.255.0 81 gateway: 192.168.1.1 82 snmp: nonpublic 83 password: saltstack1 84 switch-2: 85 ip: 192.168.1.3 86 netmask: 255.255.255.0 87 gateway: 192.168.1.1 88 snmp: nonpublic 89 password: saltstack1 90 91And to go with it, here's an example state that pulls the data from the 92pillar stated above: 93 94.. code-block:: jinja 95 96 {% set details = pillar.get('proxy:chassis', {}) %} 97 standup-step1: 98 dellchassis.chassis: 99 - name: {{ details['name'] }} 100 - location: {{ details['location'] }} 101 - mode: {{ details['management_mode'] }} 102 - idrac_launch: {{ details['idrac_launch'] }} 103 - slot_names: 104 {% for entry details['slot_names'] %} 105 - {{ next(iter(entry)) }}: {{ entry[next(iter(entry))] }} 106 {% endfor %} 107 108 blade_powercycle: 109 dellchassis.chassis: 110 - blade_power_states: 111 - server-1: powercycle 112 - server-2: powercycle 113 - server-3: powercycle 114 - server-4: powercycle 115 116 # Set idrac_passwords for blades. racadm needs them to be called 'server-x' 117 {% for k, v in details['servers'].iteritems() %} 118 {{ k }}: 119 dellchassis.blade_idrac: 120 - idrac_password: {{ v['idrac_password'] }} 121 {% endfor %} 122 123 # Set management ip addresses, passwords, and snmp strings for switches 124 {% for k, v in details['switches'].iteritems() %} 125 {{ k }}-switch-setup: 126 dellchassis.switch: 127 - name: {{ k }} 128 - ip: {{ v['ip'] }} 129 - netmask: {{ v['netmask'] }} 130 - gateway: {{ v['gateway'] }} 131 - password: {{ v['password'] }} 132 - snmp: {{ v['snmp'] }} 133 {% endfor %} 134 135.. note:: 136 137 This state module relies on the dracr.py execution module, which runs racadm commands on 138 the chassis, blades, etc. The racadm command runs very slowly and, depending on your state, 139 the proxy minion return might timeout before the racadm commands have completed. If you 140 are repeatedly seeing minions timeout after state calls, please use the ``-t`` CLI argument 141 to increase the timeout variable. 142 143 For example: 144 145 .. code-block:: bash 146 147 salt '*' state.sls my-dell-chasis-state-name -t 60 148 149.. note:: 150 151 The Dell CMC units perform adequately but many iDRACs are **excruciatingly** 152 slow. Some functions can take minutes to execute. 153 154""" 155 156 157import logging 158import os 159 160from salt.exceptions import CommandExecutionError 161 162# Import Salt lobs 163 164# Get logging started 165log = logging.getLogger(__name__) 166 167 168def __virtual__(): 169 if "chassis.cmd" in __salt__: 170 return True 171 return (False, "chassis module could not be loaded") 172 173 174def blade_idrac( 175 name, 176 idrac_password=None, 177 idrac_ipmi=None, 178 idrac_ip=None, 179 idrac_netmask=None, 180 idrac_gateway=None, 181 idrac_dnsname=None, 182 idrac_dhcp=None, 183): 184 """ 185 Set parameters for iDRAC in a blade. 186 187 :param idrac_password: Password to use to connect to the iDRACs directly 188 (idrac_ipmi and idrac_dnsname must be set directly on the iDRAC. They 189 can't be set through the CMC. If this password is present, use it 190 instead of the CMC password) 191 :param idrac_ipmi: Enable/Disable IPMI over LAN 192 :param idrac_ip: Set IP address for iDRAC 193 :param idrac_netmask: Set netmask for iDRAC 194 :param idrac_gateway: Set gateway for iDRAC 195 :param idrac_dhcp: Turn on DHCP for iDRAC (True turns on, False does 196 nothing becaause setting a static IP will disable DHCP). 197 198 :return: A standard Salt changes dictionary 199 200 NOTE: If any of the IP address settings is configured, all of ip, netmask, 201 and gateway must be present 202 """ 203 204 ret = {"name": name, "result": True, "changes": {}, "comment": ""} 205 206 if not idrac_password: 207 (username, password) = __salt__["chassis.chassis_credentials"]() 208 else: 209 password = idrac_password 210 211 module_network = __salt__["chassis.cmd"]("network_info", module=name) 212 current_idrac_ip = module_network["Network"]["IP Address"] 213 214 if idrac_ipmi is not None: 215 if idrac_ipmi is True or idrac_ipmi == 1: 216 idrac_ipmi = "1" 217 if idrac_ipmi is False or idrac_ipmi == 0: 218 idrac_ipmi = "0" 219 current_ipmi = __salt__["dracr.get_general"]( 220 "cfgIpmiLan", 221 "cfgIpmiLanEnable", 222 host=current_idrac_ip, 223 admin_username="root", 224 admin_password=password, 225 ) 226 227 if current_ipmi != idrac_ipmi: 228 ch = {"Old": current_ipmi, "New": idrac_ipmi} 229 ret["changes"]["IPMI"] = ch 230 231 if idrac_dnsname is not None: 232 dnsret = __salt__["dracr.get_dns_dracname"]( 233 host=current_idrac_ip, admin_username="root", admin_password=password 234 ) 235 current_dnsname = dnsret["[Key=iDRAC.Embedded.1#NIC.1]"]["DNSRacName"] 236 if current_dnsname != idrac_dnsname: 237 ch = {"Old": current_dnsname, "New": idrac_dnsname} 238 ret["changes"]["DNSRacName"] = ch 239 240 if idrac_dhcp is not None or idrac_ip or idrac_netmask or idrac_gateway: 241 if idrac_dhcp is True or idrac_dhcp == 1: 242 idrac_dhcp = 1 243 else: 244 idrac_dhcp = 0 245 if str(module_network["Network"]["DHCP Enabled"]) == "0" and idrac_dhcp == 1: 246 ch = {"Old": module_network["Network"]["DHCP Enabled"], "New": idrac_dhcp} 247 ret["changes"]["DRAC DHCP"] = ch 248 249 if idrac_dhcp == 0 and all([idrac_ip, idrac_netmask, idrac_netmask]): 250 current_network = __salt__["chassis.cmd"]("network_info", module=name) 251 old_ipv4 = {} 252 new_ipv4 = {} 253 if current_network["Network"]["IP Address"] != idrac_ip: 254 old_ipv4["ip"] = current_network["Network"]["IP Address"] 255 new_ipv4["ip"] = idrac_ip 256 if current_network["Network"]["Subnet Mask"] != idrac_netmask: 257 old_ipv4["netmask"] = current_network["Network"]["Subnet Mask"] 258 new_ipv4["netmask"] = idrac_netmask 259 if current_network["Network"]["Gateway"] != idrac_gateway: 260 old_ipv4["gateway"] = current_network["Network"]["Gateway"] 261 new_ipv4["gateway"] = idrac_gateway 262 263 if new_ipv4 != {}: 264 ret["changes"]["Network"] = {} 265 ret["changes"]["Network"]["Old"] = old_ipv4 266 ret["changes"]["Network"]["New"] = new_ipv4 267 268 if ret["changes"] == {}: 269 ret["comment"] = "iDRAC on blade is already in the desired state." 270 return ret 271 272 if __opts__["test"] and ret["changes"] != {}: 273 ret["result"] = None 274 ret["comment"] = "iDRAC on blade will change." 275 return ret 276 277 if "IPMI" in ret["changes"]: 278 ipmi_result = __salt__["dracr.set_general"]( 279 "cfgIpmiLan", 280 "cfgIpmiLanEnable", 281 idrac_ipmi, 282 host=current_idrac_ip, 283 admin_username="root", 284 admin_password=password, 285 ) 286 if not ipmi_result: 287 ret["result"] = False 288 ret["changes"]["IPMI"]["success"] = False 289 290 if "DNSRacName" in ret["changes"]: 291 dnsracname_result = __salt__["dracr.set_dns_dracname"]( 292 idrac_dnsname, 293 host=current_idrac_ip, 294 admin_username="root", 295 admin_password=password, 296 ) 297 if dnsracname_result["retcode"] == 0: 298 ret["changes"]["DNSRacName"]["success"] = True 299 else: 300 ret["result"] = False 301 ret["changes"]["DNSRacName"]["success"] = False 302 ret["changes"]["DNSRacName"]["return"] = dnsracname_result 303 304 if "DRAC DHCP" in ret["changes"]: 305 dhcp_result = __salt__["chassis.cmd"]("set_niccfg", dhcp=idrac_dhcp) 306 if dhcp_result["retcode"]: 307 ret["changes"]["DRAC DHCP"]["success"] = True 308 else: 309 ret["result"] = False 310 ret["changes"]["DRAC DHCP"]["success"] = False 311 ret["changes"]["DRAC DHCP"]["return"] = dhcp_result 312 313 if "Network" in ret["changes"]: 314 network_result = __salt__["chassis.cmd"]( 315 "set_niccfg", 316 ip=idrac_ip, 317 netmask=idrac_netmask, 318 gateway=idrac_gateway, 319 module=name, 320 ) 321 if network_result["retcode"] == 0: 322 ret["changes"]["Network"]["success"] = True 323 else: 324 ret["result"] = False 325 ret["changes"]["Network"]["success"] = False 326 ret["changes"]["Network"]["return"] = network_result 327 328 return ret 329 330 331def chassis( 332 name, 333 chassis_name=None, 334 password=None, 335 datacenter=None, 336 location=None, 337 mode=None, 338 idrac_launch=None, 339 slot_names=None, 340 blade_power_states=None, 341): 342 """ 343 Manage a Dell Chassis. 344 345 chassis_name 346 The name of the chassis. 347 348 datacenter 349 The datacenter in which the chassis is located 350 351 location 352 The location of the chassis. 353 354 password 355 Password for the chassis. Note: If this password is set for the chassis, 356 the current implementation of this state will set this password both on 357 the chassis and the iDrac passwords on any configured blades. If the 358 password for the blades should be distinct, they should be set separately 359 with the blade_idrac function. 360 361 mode 362 The management mode of the chassis. Viable options are: 363 364 - 0: None 365 - 1: Monitor 366 - 2: Manage and Monitor 367 368 idrac_launch 369 The iDRAC launch method of the chassis. Viable options are: 370 371 - 0: Disabled (launch iDRAC using IP address) 372 - 1: Enabled (launch iDRAC using DNS name) 373 374 slot_names 375 The names of the slots, provided as a list identified by 376 their slot numbers. 377 378 blade_power_states 379 The power states of a blade server, provided as a list and 380 identified by their server numbers. Viable options are: 381 382 - on: Ensure the blade server is powered on. 383 - off: Ensure the blade server is powered off. 384 - powercycle: Power cycle the blade server. 385 386 Example: 387 388 .. code-block:: yaml 389 390 my-dell-chassis: 391 dellchassis.chassis: 392 - chassis_name: my-dell-chassis 393 - location: my-location 394 - datacenter: london 395 - mode: 2 396 - idrac_launch: 1 397 - slot_names: 398 - 1: my-slot-name 399 - 2: my-other-slot-name 400 - blade_power_states: 401 - server-1: on 402 - server-2: off 403 - server-3: powercycle 404 """ 405 ret = { 406 "name": chassis_name, 407 "chassis_name": chassis_name, 408 "result": True, 409 "changes": {}, 410 "comment": "", 411 } 412 413 chassis_cmd = "chassis.cmd" 414 cfg_tuning = "cfgRacTuning" 415 mode_cmd = "cfgRacTuneChassisMgmtAtServer" 416 launch_cmd = "cfgRacTuneIdracDNSLaunchEnable" 417 418 inventory = __salt__[chassis_cmd]("inventory") 419 420 if idrac_launch: 421 idrac_launch = str(idrac_launch) 422 423 current_name = __salt__[chassis_cmd]("get_chassis_name") 424 if chassis_name != current_name: 425 ret["changes"].update({"Name": {"Old": current_name, "New": chassis_name}}) 426 427 current_dc = __salt__[chassis_cmd]("get_chassis_datacenter") 428 if datacenter and datacenter != current_dc: 429 ret["changes"].update({"Datacenter": {"Old": current_dc, "New": datacenter}}) 430 431 if password: 432 ret["changes"].update({"Password": {"Old": "******", "New": "******"}}) 433 if location: 434 current_location = __salt__[chassis_cmd]("get_chassis_location") 435 if location != current_location: 436 ret["changes"].update( 437 {"Location": {"Old": current_location, "New": location}} 438 ) 439 if mode: 440 current_mode = __salt__[chassis_cmd]("get_general", cfg_tuning, mode_cmd) 441 if mode != current_mode: 442 ret["changes"].update( 443 {"Management Mode": {"Old": current_mode, "New": mode}} 444 ) 445 446 if idrac_launch: 447 current_launch_method = __salt__[chassis_cmd]( 448 "get_general", cfg_tuning, launch_cmd 449 ) 450 if idrac_launch != current_launch_method: 451 ret["changes"].update( 452 { 453 "iDrac Launch Method": { 454 "Old": current_launch_method, 455 "New": idrac_launch, 456 } 457 } 458 ) 459 460 if slot_names: 461 current_slot_names = __salt__[chassis_cmd]("list_slotnames") 462 for s in slot_names: 463 key = next(iter(s)) 464 new_name = s[key] 465 if key.startswith("slot-"): 466 key = key[5:] 467 468 current_slot_name = current_slot_names.get(key).get("slotname") 469 if current_slot_name != new_name: 470 old = {key: current_slot_name} 471 new = {key: new_name} 472 if ret["changes"].get("Slot Names") is None: 473 ret["changes"].update({"Slot Names": {"Old": {}, "New": {}}}) 474 ret["changes"]["Slot Names"]["Old"].update(old) 475 ret["changes"]["Slot Names"]["New"].update(new) 476 477 current_power_states = {} 478 target_power_states = {} 479 if blade_power_states: 480 for b in blade_power_states: 481 key = next(iter(b)) 482 status = __salt__[chassis_cmd]("server_powerstatus", module=key) 483 current_power_states[key] = status.get("status", -1) 484 if b[key] == "powerdown": 485 if current_power_states[key] != -1 and current_power_states[key]: 486 target_power_states[key] = "powerdown" 487 if b[key] == "powerup": 488 if current_power_states[key] != -1 and not current_power_states[key]: 489 target_power_states[key] = "powerup" 490 if b[key] == "powercycle": 491 if current_power_states[key] != -1 and not current_power_states[key]: 492 target_power_states[key] = "powerup" 493 if current_power_states[key] != -1 and current_power_states[key]: 494 target_power_states[key] = "powercycle" 495 for k, v in target_power_states.items(): 496 old = {k: current_power_states[k]} 497 new = {k: v} 498 if ret["changes"].get("Blade Power States") is None: 499 ret["changes"].update({"Blade Power States": {"Old": {}, "New": {}}}) 500 ret["changes"]["Blade Power States"]["Old"].update(old) 501 ret["changes"]["Blade Power States"]["New"].update(new) 502 503 if ret["changes"] == {}: 504 ret["comment"] = "Dell chassis is already in the desired state." 505 return ret 506 507 if __opts__["test"]: 508 ret["result"] = None 509 ret["comment"] = "Dell chassis configuration will change." 510 return ret 511 512 # Finally, set the necessary configurations on the chassis. 513 name = __salt__[chassis_cmd]("set_chassis_name", chassis_name) 514 if location: 515 location = __salt__[chassis_cmd]("set_chassis_location", location) 516 pw_result = True 517 if password: 518 pw_single = True 519 if __salt__[chassis_cmd]( 520 "change_password", username="root", uid=1, password=password 521 ): 522 for blade in inventory["server"]: 523 pw_single = __salt__[chassis_cmd]( 524 "deploy_password", username="root", password=password, module=blade 525 ) 526 if not pw_single: 527 pw_result = False 528 else: 529 pw_result = False 530 531 if datacenter: 532 datacenter_result = __salt__[chassis_cmd]("set_chassis_datacenter", datacenter) 533 if mode: 534 mode = __salt__[chassis_cmd]("set_general", cfg_tuning, mode_cmd, mode) 535 if idrac_launch: 536 idrac_launch = __salt__[chassis_cmd]( 537 "set_general", cfg_tuning, launch_cmd, idrac_launch 538 ) 539 if ret["changes"].get("Slot Names") is not None: 540 slot_rets = [] 541 for s in slot_names: 542 key = next(iter(s)) 543 new_name = s[key] 544 if key.startswith("slot-"): 545 key = key[5:] 546 slot_rets.append(__salt__[chassis_cmd]("set_slotname", key, new_name)) 547 548 if any(slot_rets) is False: 549 slot_names = False 550 else: 551 slot_names = True 552 553 powerchange_all_ok = True 554 for k, v in target_power_states.items(): 555 powerchange_ok = __salt__[chassis_cmd]("server_power", v, module=k) 556 if not powerchange_ok: 557 powerchange_all_ok = False 558 559 if ( 560 any([name, location, mode, idrac_launch, slot_names, powerchange_all_ok]) 561 is False 562 ): 563 ret["result"] = False 564 ret["comment"] = "There was an error setting the Dell chassis." 565 566 ret["comment"] = "Dell chassis was updated." 567 return ret 568 569 570def switch( 571 name, ip=None, netmask=None, gateway=None, dhcp=None, password=None, snmp=None 572): 573 """ 574 Manage switches in a Dell Chassis. 575 576 name 577 The switch designation (e.g. switch-1, switch-2) 578 579 ip 580 The Static IP Address of the switch 581 582 netmask 583 The netmask for the static IP 584 585 gateway 586 The gateway for the static IP 587 588 dhcp 589 True: Enable DHCP 590 False: Do not change DHCP setup 591 (disabling DHCP is automatic when a static IP is set) 592 593 password 594 The access (root) password for the switch 595 596 snmp 597 The SNMP community string for the switch 598 599 Example: 600 601 .. code-block:: yaml 602 603 my-dell-chassis: 604 dellchassis.switch: 605 - switch: switch-1 606 - ip: 192.168.1.1 607 - netmask: 255.255.255.0 608 - gateway: 192.168.1.254 609 - dhcp: True 610 - password: secret 611 - snmp: public 612 613 """ 614 ret = {"name": name, "result": True, "changes": {}, "comment": ""} 615 616 current_nic = __salt__["chassis.cmd"]("network_info", module=name) 617 try: 618 if current_nic.get("retcode", 0) != 0: 619 ret["result"] = False 620 ret["comment"] = current_nic["stdout"] 621 return ret 622 623 if ip or netmask or gateway: 624 if not ip: 625 ip = current_nic["Network"]["IP Address"] 626 if not netmask: 627 ip = current_nic["Network"]["Subnet Mask"] 628 if not gateway: 629 ip = current_nic["Network"]["Gateway"] 630 631 if current_nic["Network"]["DHCP Enabled"] == "0" and dhcp: 632 ret["changes"].update( 633 { 634 "DHCP": { 635 "Old": {"DHCP Enabled": current_nic["Network"]["DHCP Enabled"]}, 636 "New": {"DHCP Enabled": dhcp}, 637 } 638 } 639 ) 640 641 if ( 642 (ip or netmask or gateway) 643 and not dhcp 644 and ( 645 ip != current_nic["Network"]["IP Address"] 646 or netmask != current_nic["Network"]["Subnet Mask"] 647 or gateway != current_nic["Network"]["Gateway"] 648 ) 649 ): 650 ret["changes"].update( 651 { 652 "IP": { 653 "Old": current_nic["Network"], 654 "New": { 655 "IP Address": ip, 656 "Subnet Mask": netmask, 657 "Gateway": gateway, 658 }, 659 } 660 } 661 ) 662 663 if password: 664 if "New" not in ret["changes"]: 665 ret["changes"]["New"] = {} 666 ret["changes"]["New"].update({"Password": "*****"}) 667 668 if snmp: 669 if "New" not in ret["changes"]: 670 ret["changes"]["New"] = {} 671 ret["changes"]["New"].update({"SNMP": "*****"}) 672 673 if ret["changes"] == {}: 674 ret["comment"] = "Switch " + name + " is already in desired state" 675 return ret 676 except AttributeError: 677 ret["changes"] = {} 678 ret["comment"] = "Something went wrong retrieving the switch details" 679 return ret 680 681 if __opts__["test"]: 682 ret["result"] = None 683 ret["comment"] = "Switch " + name + " configuration will change" 684 return ret 685 686 # Finally, set the necessary configurations on the chassis. 687 dhcp_ret = net_ret = password_ret = snmp_ret = True 688 if dhcp: 689 dhcp_ret = __salt__["chassis.cmd"]("set_niccfg", module=name, dhcp=dhcp) 690 if ip or netmask or gateway: 691 net_ret = __salt__["chassis.cmd"]( 692 "set_niccfg", ip, netmask, gateway, module=name 693 ) 694 if password: 695 password_ret = __salt__["chassis.cmd"]( 696 "deploy_password", "root", password, module=name 697 ) 698 699 if snmp: 700 snmp_ret = __salt__["chassis.cmd"]("deploy_snmp", snmp, module=name) 701 702 if any([password_ret, snmp_ret, net_ret, dhcp_ret]) is False: 703 ret["result"] = False 704 ret["comment"] = "There was an error setting the switch {}.".format(name) 705 706 ret["comment"] = "Dell chassis switch {} was updated.".format(name) 707 return ret 708 709 710def _firmware_update(firmwarefile="", host="", directory=""): 711 """ 712 Update firmware for a single host 713 """ 714 dest = os.path.join(directory, firmwarefile[7:]) 715 716 __salt__["cp.get_file"](firmwarefile, dest) 717 718 username = __pillar__["proxy"]["admin_user"] 719 password = __pillar__["proxy"]["admin_password"] 720 __salt__["dracr.update_firmware"]( 721 dest, host=host, admin_username=username, admin_password=password 722 ) 723 724 725def firmware_update(hosts=None, directory=""): 726 """ 727 State to update the firmware on host 728 using the ``racadm`` command 729 730 firmwarefile 731 filename (string) starting with ``salt://`` 732 host 733 string representing the hostname 734 supplied to the ``racadm`` command 735 directory 736 Directory name where firmwarefile 737 will be downloaded 738 739 .. code-block:: yaml 740 741 dell-chassis-firmware-update: 742 dellchassis.firmware_update: 743 hosts: 744 cmc: 745 salt://firmware_cmc.exe 746 server-1: 747 salt://firmware.exe 748 directory: /opt/firmwares 749 """ 750 ret = {} 751 ret.changes = {} 752 success = True 753 for host, firmwarefile in hosts: 754 try: 755 _firmware_update(firmwarefile, host, directory) 756 ret["changes"].update( 757 { 758 "host": { 759 "comment": "Firmware update submitted for {}".format(host), 760 "success": True, 761 } 762 } 763 ) 764 except CommandExecutionError as err: 765 success = False 766 ret["changes"].update( 767 { 768 "host": { 769 "comment": "FAILED to update firmware for {}".format(host), 770 "success": False, 771 "reason": str(err), 772 } 773 } 774 ) 775 ret["result"] = success 776 return ret 777