|
| 1 | +""" |
| 2 | +ONTAP REST API Python Client Library Sample Scripts |
| 3 | +
|
| 4 | +This script was developed by NetApp to help demonstrate NetApp |
| 5 | +technologies. This script is not officially supported as a |
| 6 | +standard NetApp product. |
| 7 | +
|
| 8 | +NOTE: AUTOBOOT must already be set to true or else ANDU will complain and it |
| 9 | + cannot be set via REST today (not even the CLI passthrough) so this script |
| 10 | + isn't doing it. If you need to, run this on your cluster: |
| 11 | +
|
| 12 | + run * bootargs set AUTOBOOT true |
| 13 | +
|
| 14 | +workflow: |
| 15 | + 1. Show the current cluster software version and available packages |
| 16 | + 2. Download a new software package |
| 17 | + 3. PATCH the cluster software to use the new package |
| 18 | + 4. Monitor the progress |
| 19 | +
|
| 20 | +Copyright (c) 2020 NetApp, Inc. All Rights Reserved. |
| 21 | +
|
| 22 | +Licensed under the BSD 3-Clause "New" or "Revised" License (the "License"); |
| 23 | +you may not use this file except in compliance with the License. |
| 24 | +
|
| 25 | +You may obtain a copy of the License at |
| 26 | +https://opensource.org/licenses/BSD-3-Clause |
| 27 | +
|
| 28 | +""" |
| 29 | + |
| 30 | +import argparse |
| 31 | +from http.server import SimpleHTTPRequestHandler |
| 32 | +import os |
| 33 | +from pprint import pprint |
| 34 | +import socket |
| 35 | +import socketserver |
| 36 | +from threading import Thread |
| 37 | +import time |
| 38 | + |
| 39 | +from netapp_ontap.error import NetAppRestError |
| 40 | +from netapp_ontap.resources import CLI, Node, Software, SoftwarePackage, SoftwarePackageDownload |
| 41 | + |
| 42 | +from utils import ( |
| 43 | + Argument, parse_args, setup_logging, setup_connection, step, substep, |
| 44 | + LiveMultilineOutput |
| 45 | +) |
| 46 | + |
| 47 | + |
| 48 | +def show_current_cluster_image(): |
| 49 | + """Verify our current software status""" |
| 50 | + |
| 51 | + step("Show the packages") |
| 52 | + substep("Show the current cluster image") |
| 53 | + software = Software() |
| 54 | + software.get() |
| 55 | + print("Current software package:", software) |
| 56 | + |
| 57 | + substep("Show the available software packages") |
| 58 | + print("Available software packages:", list(SoftwarePackage.get_collection())) |
| 59 | + return software |
| 60 | + |
| 61 | + |
| 62 | +def download_new_cluster_image(parsed_args: argparse.Namespace): |
| 63 | + """Start our HTTP server and tell ONTAP to start downloading the new image""" |
| 64 | + |
| 65 | + step("Download a new software package") |
| 66 | + substep("Start an HTTP server to serve the file") |
| 67 | + image_server = Thread(target=serve_image_download, args=(parsed_args,)) |
| 68 | + image_server.start() |
| 69 | + |
| 70 | + substep("Have ONTAP download the package") |
| 71 | + local_ip = socket.gethostbyname(socket.gethostname()) |
| 72 | + url = "http://%s:%s/%s" % (local_ip, parsed_args.port, parsed_args.image_path) |
| 73 | + download_package = SoftwarePackageDownload(url=url) |
| 74 | + download_package.post(poll_timeout=1200, poll_interval=60) |
| 75 | + image_server.join() |
| 76 | + |
| 77 | + substep("Show the new package") |
| 78 | + packages = list(SoftwarePackage.get_collection()) |
| 79 | + print("Available software packages:", packages) |
| 80 | + return packages[0] |
| 81 | + |
| 82 | + |
| 83 | +def update_cluster_image(software, new_package): |
| 84 | + """Set the new version of the software on the cluster""" |
| 85 | + |
| 86 | + step("Update the cluster to the new image") |
| 87 | + software.version = new_package.version |
| 88 | + try: |
| 89 | + if Node.count_collection() > 1: |
| 90 | + # make sure cluster HA is enabled |
| 91 | + cli = CLI() |
| 92 | + cli.execute("cluster ha modify", body={"configured": "true"}) |
| 93 | + software.patch(poll_timeout=1200, poll_interval=30, skip_warnings="true") |
| 94 | + except NetAppRestError: |
| 95 | + print("Current cluster software status:") |
| 96 | + software.get() |
| 97 | + pprint(software) |
| 98 | + |
| 99 | + |
| 100 | +def monitor_progress(software): |
| 101 | + """Until all nodes are complete, monitor and print the status of the upgrade""" |
| 102 | + |
| 103 | + step("Watch the progress") |
| 104 | + |
| 105 | + errors = 0 |
| 106 | + with LiveMultilineOutput() as output: |
| 107 | + while True: |
| 108 | + try: |
| 109 | + software.get(fields="state,status_details,elapsed_duration,estimated_duration") |
| 110 | + if not software.state == "in_progress": |
| 111 | + break |
| 112 | + errors = 0 |
| 113 | + new_list = [] |
| 114 | + for detail in software.status_details: |
| 115 | + new_list.append( |
| 116 | + "[%s]: %s - %s" |
| 117 | + % (detail.node.name, detail.name, detail.issue.message) |
| 118 | + ) |
| 119 | + percent_complete = ( |
| 120 | + software.elapsed_duration / software.estimated_duration |
| 121 | + ) * 100 |
| 122 | + new_list.append("%.2f%% complete (estimate)" % percent_complete) |
| 123 | + output.change(new_list) |
| 124 | + except Exception: # pylint: disable=broad-except |
| 125 | + errors += 1 |
| 126 | + if errors > 10: |
| 127 | + break |
| 128 | + finally: |
| 129 | + time.sleep(30) |
| 130 | + if errors > 10: |
| 131 | + raise RuntimeError("ERROR: No longer could communicate with the server. Is it down?") |
| 132 | + |
| 133 | + |
| 134 | +def serve_image_download(parsed_args): |
| 135 | + """Start a temporary HTTP server to allow ONTAP to download the image file""" |
| 136 | + |
| 137 | + if parsed_args.image_path.startswith("/"): |
| 138 | + os.chdir("/") |
| 139 | + port = parsed_args.port |
| 140 | + local_ip = socket.gethostbyname(socket.gethostname()) |
| 141 | + httpd = socketserver.TCPServer(("0.0.0.0", port), SimpleHTTPRequestHandler) |
| 142 | + print("serving package download at http://%s:%s" % (local_ip, port)) |
| 143 | + |
| 144 | + # ONTAP does 3 HEAD requests followed by the GET we care about |
| 145 | + httpd.handle_request() |
| 146 | + httpd.handle_request() |
| 147 | + httpd.handle_request() |
| 148 | + httpd.handle_request() |
| 149 | + |
| 150 | + |
| 151 | +def main() -> None: |
| 152 | + """Main function""" |
| 153 | + |
| 154 | + arguments = [ |
| 155 | + Argument("-c", "--cluster", "API server IP:port details", required=True), |
| 156 | + Argument( |
| 157 | + "-p", "--port", "The port to open as an HTTP server to serve the ONTAP image from", |
| 158 | + default=7654, arg_type=int, |
| 159 | + ), |
| 160 | + Argument( |
| 161 | + "-i", "--image-path", "The path to an ONTAP image which will be downloaded by ONTAP", |
| 162 | + required=True, |
| 163 | + ) |
| 164 | + ] |
| 165 | + args = parse_args("This script will update the nodes of an ONTAP cluster.", arguments) |
| 166 | + setup_logging() |
| 167 | + setup_connection(args.cluster, args.api_user, args.api_pass) |
| 168 | + |
| 169 | + software = show_current_cluster_image() |
| 170 | + new_package = download_new_cluster_image(args) |
| 171 | + update_cluster_image(software, new_package) |
| 172 | + monitor_progress(software) |
| 173 | + |
| 174 | + software.get() |
| 175 | + print("Upgrade complete! Current version: %s" % software.version) |
| 176 | + |
| 177 | + |
| 178 | +if __name__ == "__main__": |
| 179 | + main() |
0 commit comments