зеркало из https://github.com/microsoft/spinnaker.git
233 строки
8.8 KiB
Python
Executable File
233 строки
8.8 KiB
Python
Executable File
#!/usr/bin/python
|
|
#
|
|
# Copyright 2015 Google Inc. 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.
|
|
"""
|
|
Acts as a "bootloader" for setting up GCE instances from scratch.
|
|
|
|
If this is run as a startup script, it will extract files from the
|
|
instance metadata, then run another startup script. This makes it convienent
|
|
to write startup scripts that use existing modules that may span multiple
|
|
files so that setting up a Google Compute Engine instance (such as an image)
|
|
can use the same basic scripts and procedures as non-GCE instances. Thisjbootloader is specific to GCE in that it "bootloads" off GCE metadata. However, once
|
|
it does that (thus preparing the filesystem with the files that are needed
|
|
for the "real" startup script), it forks the specified standard script.
|
|
If additional GCE specific initialization is required, the standard script
|
|
can still conditionally perform that.
|
|
|
|
To use this as a bootloader:
|
|
|
|
add a metadata entry for each file to attach. The metadata key is the encoded
|
|
filename to extract to. Because '.' is not a valid metadata key char,
|
|
the encoding is in the form ext_basename where the first '_' acts as
|
|
the suffix. So ext_basename will be extracted as basename.ext.
|
|
Additional underscores are left as is. A leading '_' (or no '_' at all)
|
|
indicates no extension.
|
|
|
|
set the "startup_loader_files" metadata value to the keys of the attached
|
|
files that should be extracted into /opt/spinnaker/install.
|
|
|
|
set the "startup_py_command" metadata value to the command to execute after
|
|
the bootloader extracts the files. This can include commandline
|
|
arguments. The command will be run with an implied "python". The
|
|
filename to run is the literal name, not the encoded name.
|
|
|
|
set the "startup-script" metadata key to install_loader.py
|
|
|
|
The attached files will be extracted to /opt/spinnaker/install.
|
|
The attached file metadata and startup command will be cleared,
|
|
and the startup-script will be rewritten to a generated
|
|
/opt/spinnaker/install/startup_script.py that calls the specified command.
|
|
"""
|
|
import os
|
|
import shutil
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import urllib2
|
|
|
|
|
|
GOOGLE_METADATA_URL = 'http://metadata.google.internal/computeMetadata/v1'
|
|
GOOGLE_INSTANCE_METADATA_URL = '{url}/instance'.format(url=GOOGLE_METADATA_URL)
|
|
_MY_ZONE = None
|
|
|
|
|
|
def fetch(url, google=False):
|
|
request = urllib2.Request(url)
|
|
if google:
|
|
request.add_header('Metadata-Flavor', 'Google')
|
|
try:
|
|
response = urllib2.urlopen(request)
|
|
return response.getcode(), response.read()
|
|
except urllib2.HTTPError as e:
|
|
return e.code, str(e.reason)
|
|
except urllib2.URLError as e:
|
|
return -1, str(e.reason)
|
|
|
|
|
|
def get_zone():
|
|
global _MY_ZONE
|
|
if _MY_ZONE != None:
|
|
return _MY_ZONE
|
|
code, output = fetch('{url}/zone'.format(url=GOOGLE_INSTANCE_METADATA_URL),
|
|
google=True)
|
|
if code == 200:
|
|
_MY_ZONE = os.path.basename(output)
|
|
else:
|
|
_MY_ZONE = ''
|
|
return _MY_ZONE
|
|
|
|
|
|
def running_on_gce():
|
|
return get_zone() != ''
|
|
|
|
|
|
def get_instance_metadata_attribute(name):
|
|
code, output = fetch(
|
|
'{url}/attributes/{name}'.format(url=GOOGLE_INSTANCE_METADATA_URL,
|
|
name=name),
|
|
google=True)
|
|
if code == 200:
|
|
return output
|
|
else:
|
|
return None
|
|
|
|
|
|
def clear_metadata_to_file(name, path):
|
|
value = get_instance_metadata_attribute(name)
|
|
if value != None:
|
|
with open(path, 'w') as f:
|
|
f.write(value)
|
|
clear_instance_metadata(name)
|
|
|
|
|
|
def clear_instance_metadata(name):
|
|
p = subprocess.Popen('gcloud compute instances remove-metadata'
|
|
' {hostname} --zone={zone} --keys={name}'
|
|
.format(hostname=socket.gethostname(),
|
|
zone=get_zone(),
|
|
name=name),
|
|
shell=True, close_fds=True)
|
|
if p.wait():
|
|
raise SystemExit('Unexpected failure clearing metadata.')
|
|
|
|
|
|
def write_instance_metadata(name, value):
|
|
p = subprocess.Popen('gcloud compute instances add-metadata'
|
|
' {hostname} --zone={zone} --metadata={name}={value}'
|
|
.format(hostname=socket.gethostname(),
|
|
zone=get_zone(),
|
|
name=name, value=value),
|
|
shell=True, close_fds=True)
|
|
if p.wait():
|
|
raise SystemExit('Unexpected failure writing metadata.')
|
|
|
|
|
|
def unpack_files(key_list):
|
|
"""Args unpack and clear the specified keys into their corresponding files.
|
|
|
|
Key names correspond to file names using the following encoding:
|
|
'.' is not permitted in a metadata name, so we'll use a leading
|
|
underscore separator in the file name to indicate the extension.
|
|
a value in the form "ext_base_name" means the file "base_name.ext"
|
|
a value in the form "_ext_base_name" means the file "ext_base_name"
|
|
a value in the form "basename" means the file "basename"
|
|
|
|
Args: key_list a list of strings denoting the metadata keys contianing the
|
|
file content.
|
|
"""
|
|
|
|
for key in key_list:
|
|
underscore = key.find('_')
|
|
if underscore <= 0:
|
|
filename = key if underscore < 0 else key[1:]
|
|
else:
|
|
filename = '{basename}.{ext}'.format(
|
|
basename = key[underscore + 1:],
|
|
ext=key[:underscore])
|
|
clear_metadata_to_file(key, filename)
|
|
|
|
|
|
def __unpack_and_run():
|
|
"""Unpack the files from metadata, and run the main script.
|
|
|
|
This is intended to be used where a startup [python] script needs a bunch
|
|
of different files for a startup script that is passed through metadata
|
|
in a GCE instance.
|
|
|
|
The actual startup acts like a bootloader that unpacks all the
|
|
files from the metadata, then passes control the specific startup script
|
|
for the installation.
|
|
|
|
The bootloader unpacks the files mentioned in the 'startup_loader_files'
|
|
metadata, which is a space-delimited list of other metadata keys that
|
|
contain the files. Because the keys cannot contain '.', we encode the
|
|
filenames as <ext>_<basename> using the leading '_' to separate the
|
|
extension, whichi s added as a prefix.
|
|
|
|
The true startup script is denoted by the 'startup_py_command' attribute,
|
|
which specifies the name of a python script to run (presumably packed
|
|
into the startup_loader_files). The script uses the unencoded name once
|
|
unpacked. The python command itself is ommited and will be added here.
|
|
"""
|
|
|
|
script_keys = get_instance_metadata_attribute('startup_loader_files')
|
|
key_list = script_keys.split('+') if script_keys else []
|
|
unpack_files(key_list)
|
|
if script_keys:
|
|
clear_instance_metadata('startup_loader_files')
|
|
|
|
startup_py_command = get_instance_metadata_attribute('startup_py_command')
|
|
if not startup_py_command:
|
|
sys.stderr.write('No "startup_py_command" metadata key.\n')
|
|
raise SystemExit('No "startup_py_command" metadata key.')
|
|
|
|
# Change the startup script to the final command that we run
|
|
# so that future boots will just run that command. And take down
|
|
# the rest of the boostrap metadata since we dont need it anymore.
|
|
command = 'python ' + startup_py_command.replace('+', ' ')
|
|
|
|
with open('__startup_script__.sh', 'w') as f:
|
|
f.write('#!/bin/bash\ncd /opt/spinnaker/install\n{command}\n'
|
|
.format(command=command))
|
|
os.chmod('__startup_script__.sh', 0555)
|
|
write_instance_metadata('startup-script',
|
|
'/opt/spinnaker/install/__startup_script__.sh')
|
|
clear_instance_metadata('startup_py_command')
|
|
|
|
# Now run the command (which is also the future startup script).
|
|
p = subprocess.Popen(command, shell=True, close_fds=True)
|
|
p.communicate()
|
|
return p.returncode
|
|
|
|
|
|
if __name__ == '__main__':
|
|
if not running_on_gce():
|
|
sys.stderr.write('You do not appear to be on Google Compute Engine.\n')
|
|
sys.exit(-1)
|
|
|
|
try:
|
|
os.makedirs('/opt/spinnaker/install')
|
|
os.chdir('/opt/spinnaker/install')
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
# Copy this script to /opt/spinnaker/install as install_loader.py
|
|
# since other scripts will reference it that way.
|
|
shutil.copyfile('/var/run/google.startup.script',
|
|
'/opt/spinnaker/install/google_install_loader.py')
|
|
print 'RUNNING with argv={0}'.format(sys.argv)
|
|
sys.exit(__unpack_and_run())
|