|  | #!/usr/bin/env python3 | 
|  | # | 
|  | # Copyright 2018 The Bazel Authors. All rights reserved. | 
|  | # | 
|  | # Licensed under the Apache License, Version 2.0 (the "License"); | 
|  | # you may not use this file except in compliance with the License. | 
|  | # You may obtain a copy of the License at | 
|  | # | 
|  | #    http://www.apache.org/licenses/LICENSE-2.0 | 
|  | # | 
|  | # Unless required by applicable law or agreed to in writing, software | 
|  | # distributed under the License is distributed on an "AS IS" BASIS, | 
|  | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 
|  | # See the License for the specific language governing permissions and | 
|  | # limitations under the License. | 
|  |  | 
|  | from datetime import datetime | 
|  | import json | 
|  | import os | 
|  | import queue | 
|  | import subprocess | 
|  | import sys | 
|  | import tempfile | 
|  | import threading | 
|  |  | 
|  | import gcloud | 
|  | import gcloud_utils | 
|  |  | 
|  | DEBUG = False | 
|  |  | 
|  | IMAGE_CREATION_VMS = { | 
|  | "bk-testing-docker": { | 
|  | "project": "bazel-public", | 
|  | "zone": "us-central1-f", | 
|  | "source_image_project": "ubuntu-os-cloud", | 
|  | "source_image_family": "ubuntu-2004-lts", | 
|  | "setup_script": "setup-docker.sh", | 
|  | "guest_os_features": ["VIRTIO_SCSI_MULTIQUEUE"], | 
|  | "licenses": [ | 
|  | "https://www.googleapis.com/compute/v1/projects/vm-options/global/licenses/enable-vmx" | 
|  | ], | 
|  | }, | 
|  | "bk-testing-windows": { | 
|  | "project": "bazel-public", | 
|  | "zone": "us-central1-f", | 
|  | "source_image_project": "windows-cloud", | 
|  | "source_image_family": "windows-2019-core", | 
|  | "setup_script": "setup-windows.ps1", | 
|  | "guest_os_features": ["VIRTIO_SCSI_MULTIQUEUE"], | 
|  | }, | 
|  | "windows-playground": { | 
|  | "project": "di-cloud-exp", | 
|  | "zone": "us-central1-f", | 
|  | "network": "default", | 
|  | "source_image_project": "windows-cloud", | 
|  | "source_image_family": "windows-2019", | 
|  | "setup_script": "setup-windows.ps1", | 
|  | "guest_os_features": ["VIRTIO_SCSI_MULTIQUEUE"], | 
|  | }, | 
|  | } | 
|  |  | 
|  | WORK_QUEUE = queue.Queue() | 
|  |  | 
|  |  | 
|  | def run(args, **kwargs): | 
|  | return subprocess.run(args, **kwargs) | 
|  |  | 
|  |  | 
|  | def preprocess_setup_script(setup_script, is_windows): | 
|  | output_file = tempfile.mkstemp()[1] | 
|  | newline = "\r\n" if is_windows else "\n" | 
|  | with open(output_file, "w", newline=newline) as f: | 
|  | with open(setup_script, "r") as setup_script_file: | 
|  | if is_windows: | 
|  | f.write("$setup_script = @'\n") | 
|  | f.write(setup_script_file.read() + "\n") | 
|  | if is_windows: | 
|  | f.write("'@\n") | 
|  | f.write('[System.IO.File]::WriteAllLines("c:\\setup.ps1", $setup_script)\n') | 
|  | return output_file | 
|  |  | 
|  |  | 
|  | def create_instance(instance_name, params): | 
|  | is_windows = "windows" in instance_name | 
|  | setup_script = preprocess_setup_script(params["setup_script"], is_windows) | 
|  | try: | 
|  | if is_windows: | 
|  | startup_script = "windows-startup-script-ps1=" + setup_script | 
|  | else: | 
|  | startup_script = "startup-script=" + setup_script | 
|  |  | 
|  | if "source_image" in params: | 
|  | image = {"image": params["source_image"]} | 
|  | else: | 
|  | image = { | 
|  | "image-project": params["source_image_project"], | 
|  | "image-family": params["source_image_family"], | 
|  | } | 
|  |  | 
|  | gcloud.create_instance( | 
|  | instance_name, | 
|  | project=params["project"], | 
|  | zone=params["zone"], | 
|  | machine_type="c2-standard-8", | 
|  | network=params.get("network", "default"), | 
|  | metadata_from_file=startup_script, | 
|  | boot_disk_type="pd-ssd", | 
|  | boot_disk_size=params.get("boot_disk_size", "500GB"), | 
|  | **image, | 
|  | ) | 
|  | finally: | 
|  | os.remove(setup_script) | 
|  |  | 
|  |  | 
|  | # https://stackoverflow.com/a/25802742 | 
|  | def write_to_clipboard(output): | 
|  | process = subprocess.Popen("pbcopy", env={"LANG": "en_US.UTF-8"}, stdin=subprocess.PIPE) | 
|  | process.communicate(output.encode("utf-8")) | 
|  |  | 
|  |  | 
|  | def print_windows_instructions(project, zone, instance_name): | 
|  | tail_start = gcloud_utils.tail_serial_console( | 
|  | instance_name, project=project, zone=zone, until="Finished running startup scripts" | 
|  | ) | 
|  |  | 
|  | pw = json.loads( | 
|  | gcloud.reset_windows_password( | 
|  | instance_name, format="json", project=project, zone=zone | 
|  | ).stdout | 
|  | ) | 
|  | rdp_file = tempfile.mkstemp(suffix=".rdp")[1] | 
|  | with open(rdp_file, "w") as f: | 
|  | f.write("full address:s:" + pw["ip_address"] + "\n") | 
|  | f.write("username:s:" + pw["username"] + "\n") | 
|  | print("Opening ", rdp_file) | 
|  | subprocess.run(["open", rdp_file]) | 
|  | write_to_clipboard(pw["password"]) | 
|  | with gcloud.PRINT_LOCK: | 
|  | print("Use this password to connect to the Windows VM: " + pw["password"]) | 
|  | print("Please run the setup script C:\\setup.ps1 once you're logged in.") | 
|  |  | 
|  | # Wait until the VM reboots once, then open RDP again. | 
|  | tail_start = gcloud_utils.tail_serial_console( | 
|  | instance_name, | 
|  | project=project, | 
|  | zone=zone, | 
|  | start=tail_start, | 
|  | until="GCEGuestAgent: GCE Agent Started", | 
|  | ) | 
|  | print("Connecting via RDP a second time to finish the setup...") | 
|  | write_to_clipboard(pw["password"]) | 
|  | run(["open", rdp_file]) | 
|  | return tail_start | 
|  |  | 
|  |  | 
|  | def workflow(name, params): | 
|  | instance_name = "%s-image-%s" % (name, int(datetime.now().timestamp())) | 
|  | project = params["project"] | 
|  | zone = params["zone"] | 
|  | try: | 
|  | # Create the VM. | 
|  | create_instance(instance_name, params) | 
|  |  | 
|  | # Wait for the VM to become ready. | 
|  | gcloud_utils.wait_for_instance(instance_name, project=project, zone=zone, status="RUNNING") | 
|  |  | 
|  | if "windows" in instance_name: | 
|  | # Wait for VM to be ready, then print setup instructions. | 
|  | tail_start = print_windows_instructions(project, zone, instance_name) | 
|  | # Continue printing the serial console until the VM shuts down. | 
|  | gcloud_utils.tail_serial_console( | 
|  | instance_name, project=project, zone=zone, start=tail_start | 
|  | ) | 
|  | else: | 
|  | # Continuously print the serial console. | 
|  | gcloud_utils.tail_serial_console(instance_name, project=project, zone=zone) | 
|  |  | 
|  | # Wait for the VM to completely shutdown. | 
|  | gcloud_utils.wait_for_instance( | 
|  | instance_name, project=project, zone=zone, status="TERMINATED" | 
|  | ) | 
|  |  | 
|  | # Create a new image from our VM. | 
|  | gcloud.create_image( | 
|  | instance_name, | 
|  | project=project, | 
|  | family=name, | 
|  | source_disk=instance_name, | 
|  | source_disk_zone=zone, | 
|  | licenses=params.get("licenses", []), | 
|  | guest_os_features=params.get("guest_os_features", []), | 
|  | ) | 
|  | finally: | 
|  | gcloud.delete_instance(instance_name, project=project, zone=zone) | 
|  |  | 
|  |  | 
|  | def worker(): | 
|  | while True: | 
|  | item = WORK_QUEUE.get() | 
|  | if not item: | 
|  | break | 
|  | try: | 
|  | workflow(**item) | 
|  | finally: | 
|  | WORK_QUEUE.task_done() | 
|  |  | 
|  |  | 
|  | def main(argv=None): | 
|  | if argv is None: | 
|  | argv = sys.argv[1:] | 
|  |  | 
|  | if not argv: | 
|  | print("Usage: create_images.py {}".format(" ".join(IMAGE_CREATION_VMS.keys()))) | 
|  | return 1 | 
|  |  | 
|  | unknown_args = set(argv).difference(IMAGE_CREATION_VMS.keys()) | 
|  | if unknown_args: | 
|  | print( | 
|  | "Unknown platforms: {}\nAvailable platforms: {}".format( | 
|  | ", ".join(unknown_args), ", ".join(IMAGE_CREATION_VMS.keys()) | 
|  | ) | 
|  | ) | 
|  | return 1 | 
|  |  | 
|  | # Put VM creation instructions into the work queue. | 
|  | for name in argv: | 
|  | WORK_QUEUE.put({"name": name, "params": IMAGE_CREATION_VMS[name]}) | 
|  |  | 
|  | # Spawn worker threads that will create the VMs. | 
|  | threads = [] | 
|  | for _ in range(WORK_QUEUE.qsize()): | 
|  | t = threading.Thread(target=worker) | 
|  | t.start() | 
|  | threads.append(t) | 
|  |  | 
|  | # Wait for all VMs to be created. | 
|  | WORK_QUEUE.join() | 
|  |  | 
|  | # Signal worker threads to exit. | 
|  | for _ in range(len(threads)): | 
|  | WORK_QUEUE.put(None) | 
|  |  | 
|  | # Wait for worker threads to exit. | 
|  | for t in threads: | 
|  | t.join() | 
|  |  | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | if __name__ == "__main__": | 
|  | sys.exit(main()) |