blob: 57ab9133afa83ce3722466d88a8aceeb122751e6 [file] [log] [blame]
#!/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())