| #!/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 itertools |
| import json |
| import os |
| import queue |
| import subprocess |
| import sys |
| import tempfile |
| import threading |
| |
| import gcloud |
| import gcloud_utils |
| |
| DEBUG = False |
| |
| LOCATION = 'europe-west1-d' |
| |
| IMAGE_CREATION_VMS = { |
| # Find the newest FreeBSD 11 image via: |
| # gcloud compute images list --project freebsd-org-cloud-dev \ |
| # --no-standard-images |
| # ('buildkite-freebsd11',): { |
| # 'source_image': 'https://www.googleapis.com/compute/v1/projects/freebsd-org-cloud-dev/global/images/freebsd-11-1-stable-amd64-2017-12-28', |
| # 'scripts': [ |
| # 'setup-freebsd.sh', |
| # 'install-buildkite-agent.sh' |
| # ] |
| # }, |
| ('buildkite-ubuntu1404',): { |
| 'source_image_project': 'ubuntu-os-cloud', |
| 'source_image_family': 'ubuntu-1404-lts', |
| 'scripts': [ |
| 'shell-utils.sh', |
| 'setup-ubuntu.sh', |
| 'install-image-version.sh', |
| 'install-azul-zulu.sh', |
| 'install-bazel.sh', |
| 'install-buildkite-agent.sh', |
| 'install-docker.sh', |
| 'install-nodejs.sh', |
| 'install-python36.sh', |
| 'install-kvm.sh', |
| 'install-android-sdk.sh', |
| 'shutdown.sh' |
| ], |
| 'licenses': [ |
| 'https://www.googleapis.com/compute/v1/projects/vm-options/global/licenses/enable-vmx' |
| ] |
| }, |
| ('buildkite-ubuntu1604', 'buildkite-trusted-ubuntu1604', 'buildkite-pipeline-ubuntu1604'): { |
| 'source_image_project': 'ubuntu-os-cloud', |
| 'source_image_family': 'ubuntu-1604-lts', |
| 'scripts': [ |
| 'shell-utils.sh', |
| 'setup-ubuntu.sh', |
| 'install-image-version.sh', |
| 'install-azul-zulu.sh', |
| 'install-bazel.sh', |
| 'install-buildkite-agent.sh', |
| 'install-docker.sh', |
| 'install-nodejs.sh', |
| 'install-python36.sh', |
| 'install-kvm.sh', |
| 'install-android-sdk.sh', |
| 'shutdown.sh' |
| ], |
| 'licenses': [ |
| 'https://www.googleapis.com/compute/v1/projects/vm-options/global/licenses/enable-vmx' |
| ] |
| }, |
| ('buildkite-windows',): { |
| 'source_image_project': 'windows-cloud', |
| 'source_image_family': 'windows-1709-core', |
| 'scripts': [ |
| 'setup-windows-manual.ps1' |
| ] |
| } |
| } |
| |
| WORK_QUEUE = queue.Queue() |
| |
| |
| def run(args, **kwargs): |
| return subprocess.run(args, **kwargs) |
| |
| |
| def merge_setup_scripts(instance_name, scripts): |
| newline = '\r\n' if 'windows' in instance_name else None |
| # Merge all setup scripts into one. |
| merged_script_path = tempfile.mkstemp()[1] |
| with open(merged_script_path, 'w', newline=newline) as merged_script_file: |
| for script in scripts: |
| with open(script, 'r') as script_file: |
| merged_script_file.write(script_file.read() + '\n') |
| return merged_script_path |
| |
| |
| def create_instance(instance_name, params, git_commit): |
| merged_script_path = merge_setup_scripts(instance_name, params['scripts']) |
| try: |
| if 'windows' in instance_name: |
| startup_script = 'windows-startup-script-ps1=' + merged_script_path |
| else: |
| startup_script = 'startup-script=' + merged_script_path |
| |
| 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, |
| zone=LOCATION, |
| machine_type='n1-standard-8', |
| network='buildkite', |
| metadata='image-version={}'.format(git_commit), |
| metadata_from_file=startup_script, |
| min_cpu_platform='Intel Skylake', |
| boot_disk_type='pd-ssd', |
| boot_disk_size='50GB', |
| **image) |
| finally: |
| os.remove(merged_script_path) |
| |
| |
| # 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(instance_name): |
| tail_start = gcloud_utils.tail_serial_console(instance_name, zone=LOCATION, until='Finished running startup scripts') |
| |
| pw = json.loads(gcloud.reset_windows_password(instance_name, format='json', zone=LOCATION).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') |
| 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, zone=LOCATION, start=tail_start, until='Finished running startup scripts') |
| 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, git_commit): |
| instance_name = "%s-image-%s-%s" % (name, int(datetime.now().timestamp()), git_commit) |
| try: |
| # Create the VM. |
| create_instance(instance_name, params, git_commit) |
| |
| # Wait for the VM to become ready. |
| gcloud_utils.wait_for_instance(instance_name, zone=LOCATION, status='RUNNING') |
| |
| if 'windows' in instance_name: |
| # Wait for VM to be ready, then print setup instructions. |
| tail_start = print_windows_instructions(instance_name) |
| # Continue printing the serial console until the VM shuts down. |
| gcloud_utils.tail_serial_console(instance_name, zone=LOCATION, start=tail_start) |
| else: |
| # Continuously print the serial console. |
| gcloud_utils.tail_serial_console(instance_name, zone=LOCATION) |
| |
| # Wait for the VM to completely shutdown. |
| gcloud_utils.wait_for_instance(instance_name, zone=LOCATION, status='TERMINATED') |
| |
| # Create a new image from our VM. |
| gcloud.create_image( |
| instance_name, |
| family=name, |
| source_disk=instance_name, |
| source_disk_zone=LOCATION, |
| licenses=params.get('licenses', [])) |
| finally: |
| gcloud.delete_instance(instance_name) |
| |
| |
| 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(itertools.chain(*IMAGE_CREATION_VMS.keys())))) |
| return 1 |
| |
| try: |
| git_commit = subprocess.check_output(['git', 'rev-parse', '--verify', '--short=16', 'HEAD'], |
| universal_newlines=True).strip() |
| except subprocess.CalledProcessError: |
| print("Could not get current Git commit hash. You have to run " |
| "create_images.py from a Git repository.", file=sys.stderr) |
| return 1 |
| |
| if subprocess.check_output(['git', 'status', '--porcelain'], universal_newlines=True).strip(): |
| print("There are pending changes in your Git repository. You have to " |
| "commit them, before create_images.py can continue.", file=sys.stderr) |
| return 1 |
| |
| # Put VM creation instructions into the work queue. |
| for names, params in IMAGE_CREATION_VMS.items(): |
| for name in names: |
| if argv and name not in argv: |
| continue |
| WORK_QUEUE.put({ |
| 'name': name, |
| 'params': params, |
| 'git_commit': git_commit, |
| }) |
| |
| # 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()) |