diff options
-rw-r--r-- | debian/control | 8 | ||||
-rw-r--r-- | debian/dirs | 3 | ||||
-rw-r--r-- | debian/install | 6 | ||||
-rwxr-xr-x | debian/rules | 14 | ||||
-rwxr-xr-x | ec2-fetch-credentials.py | 86 | ||||
-rwxr-xr-x | ec2-run-user-data.py | 124 | ||||
-rwxr-xr-x | ec2-set-apt-sources.py | 77 | ||||
-rwxr-xr-x | ec2-set-defaults.py | 95 | ||||
-rwxr-xr-x | ec2-set-hostname.py | 42 | ||||
-rw-r--r-- | ec2init/__init__.py | 93 | ||||
-rwxr-xr-x | setup.py | 21 |
11 files changed, 287 insertions, 282 deletions
diff --git a/debian/control b/debian/control index a2812cae..b171f568 100644 --- a/debian/control +++ b/debian/control @@ -2,12 +2,14 @@ Source: ec2-init Section: admin Priority: extra Maintainer: Soren Hansen <soren@ubuntu.com> -Build-Depends: cdbs, debhelper (>= 5) +Build-Depends: cdbs, debhelper (>= 5), debhelper (>= 5.0.38), python-all-dev (>= 2.3.5-11), python-central (>= 0.5.6) +XS-Python-Version: all Standards-Version: 3.8.0 Package: ec2-init Architecture: i386 amd64 -Depends: python, procps, python-configobj, python-cheetah, python-apt, update-motd -Description: Init scripts for EC2 instances, +Depends: python, procps, python-configobj, python-cheetah, update-motd, ${python:Depends} +XB-Python-Version: ${python:Versions} +Description: Init scripts for EC2 instances EC2 instances need special scripts to run during initialisation to retrieve and install ssh keys and to let the user run various scripts. diff --git a/debian/dirs b/debian/dirs deleted file mode 100644 index 3c349733..00000000 --- a/debian/dirs +++ /dev/null @@ -1,3 +0,0 @@ -var/ec2 -usr/sbin -etc/ec2-init diff --git a/debian/install b/debian/install index 33e2382f..7cea0e92 100644 --- a/debian/install +++ b/debian/install @@ -1,3 +1,3 @@ -debian/tmp/usr/sbin/* -debian/tmp/etc/ec2-init/templates/* -debian/ec2-config.cfg etc/ec2-init +#debian/ec2-init/usr/bin/* +#debian/ec2-init/etc/ec2-init/templates/* +#debian/ec2-config.cfg etc/ec2-init diff --git a/debian/rules b/debian/rules index 488b93d9..a96b88fa 100755 --- a/debian/rules +++ b/debian/rules @@ -1,18 +1,10 @@ #!/usr/bin/make -f +DEB_PYTHON_SYSTEM=pycentral include /usr/share/cdbs/1/rules/debhelper.mk +include /usr/share/cdbs/1/class/python-distutils.mk DEB_UPDATE_RCD_PARAMS:= start 15 2 3 4 5 . stop 20 1 . -build/ec2-init:: - install -d debian/tmp/usr/sbin - mkdir -p debian/tmp/var/ec2 - mkdir -p debian/tmp/etc/ec2-init/templates - install -m 775 ec2-fetch-credentials.py debian/tmp/usr/sbin/ec2-fetch-credentials - install -m 775 ec2-run-user-data.py debian/tmp/usr/sbin/ec2-run-user-data - install -m 755 ec2-set-hostname.py debian/tmp/usr/sbin/ec2-set-hostname - install -m 755 ec2-set-apt-sources.py debian/tmp/usr/sbin/ec2-set-apt-sources - install -m755 ec2-get-info.py debian/tmp/usr/sbin/ec2-get-info - install -m755 ec2-set-defaults.py debian/tmp/usr/sbin/ec2-set-defaults - install -m755 templates/* debian/tmp/etc/ec2-init/templates +binary-install/ec2-init:: dh_installinit -p ec2-init --name rightscale-init -- defaults 99 09 diff --git a/ec2-fetch-credentials.py b/ec2-fetch-credentials.py index 05bc7a9c..9ebc4634 100755 --- a/ec2-fetch-credentials.py +++ b/ec2-fetch-credentials.py @@ -18,76 +18,42 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # -import urllib import os -import socket -import sys -from configobj import ConfigObj +import pwd -api_ver = '2008-02-01' -metadata = None -filename='/etc/ec2-init/ec2-config.cfg' +import ec2init -config = ConfigObj(filename) -user = config['user'] -config_root = config['DISABLE_ROOT'] +def setup_user_keys(keys, user, key_prefix): + pwent = pwd.getpwnam(user) -def get_ssh_keys(): - base_url = 'http://169.254.169.254/%s/meta-data' % api_ver - data = urllib.urlopen('%s/public-keys/' % base_url).read() - keyids = [line.split('=')[0] for line in data.split('\n')] - return [urllib.urlopen('%s/public-keys/%d/openssh-key' % (base_url, int(keyid))).read().rstrip() for keyid in keyids] + os.umask(077) + if not os.path.exists('%s/.ssh' % pwent.pw_dir): + os.mkdir('%s/.ssh' % pwent.pw_dir) -def setup_user_keys(k,user,filename): - if not os.path.exists('/home/%s/.ssh' %(user)): - os.mkdir('/home/%s/.ssh' %(user)) - - authorized_keys = '/home/%s/.ssh/authorized_keys' % user + authorized_keys = '%s/.ssh/authorized_keys' % pwent.pw_dir fp = open(authorized_keys, 'a') - fp.write(''.join(['%s\n' % key for key in keys])) + fp.write(''.join(['%s%s\n' % (key_prefix, key) for key in keys])) fp.close() - os.system('chown -R %s:%s /home/%s/.ssh' %(user,user,user)) - os.system('touch %s' %(filename)) -def setup_root_user(k,root_config): - if root_config == "1": - if not os.path.exists('/root/.ssh'): - os.mkdir('/root/.ssh/') + os.chown(authorized_keys, pwent.pw_uid, pwent.pw_gid) - fp = open('/root/.ssh/authorized_keys', 'a') - fp.write("command=\"echo \'Please login as the ubuntu user rather than root user.\';echo;sleep 10\" ") - fp.write(''.join(['%s\n' % key for key in keys])) - fp.close() - elif root_config == "0": - print "You choose to disable the root user, god help you." - else: - print "%s - I dont understand that opion." +def main(): + ec2 = ec2init.EC2Init() -def checkServer(): - s = socket.socket() - try: - address = '169.254.169.254' - port = 80 - s.connect((address,port)) - except socket.error, e: - print "!!! Unable to connect to %s" % address - sys.exit(0) + user = ec2.get_cfg_option_str('user') + disable_root = ec2.get_cfg_option_bool('disable_root') -def get_ami_id(): - url = 'http://169.254.169.254/%s/meta-data' % api_ver - ami_id = urllib.urlopen('%s/ami-id/' %url).read() - return ami_id + keys = ec2.get_ssh_keys() + + if user: + setup_user_keys(keys, user, '') + + if disable_root: + key_prefix = 'command="echo \'Please login as the ubuntu user rather than root user.\';echo;sleep 10" ' + else: + key_prefix = '' -amid = get_ami_id() -filename = '/var/ec2/.ssh-keys-ran.%s' %amid -if os.path.exists(filename): - print "ec2-fetch-credentials already ran....skipping." -else: - os.umask(077) - if user == "": - print "User must exist in %s" %(filename) - sys.exit(0) + setup_root_user(keys, 'root', key_prefix) - keys = get_ssh_keys() - setup_user_keys(keys,user,filename) - setup_root_user(keys,config_root) +if __name__ == '__main__': + main() diff --git a/ec2-run-user-data.py b/ec2-run-user-data.py index 513c64ff..890bb2bf 100755 --- a/ec2-run-user-data.py +++ b/ec2-run-user-data.py @@ -1,9 +1,9 @@ #!/usr/bin/python # # Fetch and run user-data from EC2 -# Copyright 2008 Canonical Ltd. +# Copyright 2009 Canonical Ltd. # -# Original-Author: Soren Hansen <soren@canonical.com> +# Author: Soren Hansen <soren@canonical.com> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -19,54 +19,80 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # +import email import os +import subprocess import sys import tempfile -import urllib -import socket -from time import gmtime, strftime - -api_ver = '2008-02-01' -metadata = None - -def checkServer(): - s = socket.socket() - try: - address = '169.254.169.254' - port = 80 - s.connect((address,port)) - except socket.error, e: - print "!!!! Unable to connect to %s" % address - sys.exit(0) - -def get_user_data(): - url = 'http://169.254.169.254/%s/user-data' % api_ver - fp = urllib.urlopen(url) - data = fp.read() + +import ec2init + +content_type_handlers = { 'text/x-shellscript' : handle_shell_script, + 'text/x-ebs-mount-description' : handle_ebs_mount_description } + +def main(): + ec2 = ec2init.EC2Init() + + semaphore = '/var/lib/ec2/already-ran.%s' % amiId + amiId = ec2.get_ami_id() + + if os.path.exists(semaphore): + print "ec2-run-user-data already ran for this instance." + return 0 + + user_data = ec2.get_user_data() + + msg = email.message_from_string(user_data) + if msg.is_multipart(): + handle_part(msg) + else: + handle_payload(user_data) + + # Touch the semaphore file + file(semaphore, 'a').close() + +def handle_part(part): + if part.is_multipart(): + for p in part.get_payload(): + handle_part(p) + else: + if part.get_content_type() in content_type_handlers: + content_type_handlers[part.get_content_type](part.get_payload()) + return + + handle_unknown_payload(part.get_payload()) + +def handle_unknown_payload(payload): + # Try to detect magic + if payload.startswith('#!'): + content_type_handlers['text/x-shellscript'](payload) + +def handle_ebs_mount_description(payload): + (volume_description, path) = payload.split(':') + (identifier_type, identifier) = volume_description.split('=') + + if identifier_type == 'device': + device = identifier +# Perhaps some day the volume id -> device path mapping +# will be exposed through meta-data. +# elif identifier_type == 'volume': +# device = extract_device_name_from_meta_data + else: + return + +def handle_shell_script(payload): + (fd, path) = tempfile.mkstemp() + fp = os.fdopen(fd, 'a') + fp.write(payload) fp.close() - return data - -def get_ami_id(): - url = 'http://169.254.169.254/%s/meta-data' % api_ver - ami_id = urllib.urlopen('%s/ami-id/' %url).read() - return ami_id - -checkServer() -user_data = get_user_data() -amiId = get_ami_id() -filename = '/var/ec2/.already-ran.%s' % amiId - -if os.path.exists(filename): - print "ec2-run-user-data already ran for this instance." - sys.exit(0) -elif user_data.startswith('#!'): - # run it - (fp, path) = tempfile.mkstemp() - os.write(fp,user_data) - os.close(fp); - os.chmod(path, 0700) - status = os.system('%s | logger -t "user-data" ' % path) - os.unlink(path) - os.system('touch %s' %(filename)) - -sys.exit(0) + os.chmod(path, 0700) + + # Run the user data script and pipe its output to logger + user_data_process = subprocess.Popen([path], stdout=subprocess.PIPE) + logger_process = subprocess.Popen(['logger', '-t', 'user-data'], stdin=user_data_process.stdout) + logger_process.communicate() + + os.unlink(path) + +if __name__ == '__main__': + main() diff --git a/ec2-set-apt-sources.py b/ec2-set-apt-sources.py index d48e5167..10fea8ec 100755 --- a/ec2-set-apt-sources.py +++ b/ec2-set-apt-sources.py @@ -19,70 +19,31 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # import os -import sys -import urllib -import socket -import apt -import apt_pkg +import subprocess from Cheetah.Template import Template -def checkServer(): - s = socket.socket() - try: - address = '169.254.169.254' - port = 80 - s.connect((address,port)) - except socket.error, e: - print "!!! Unable to connect to %s." % address - sys.exit(0) +import ec2init -def detectZone(): - api_ver = '2008-02-01' +def main(): + ec2 = ec2init.EC2Init() - base_url = 'http://169.254.169.254/%s/meta-data' % api_ver - zone = urllib.urlopen('%s/placement/availability-zone' % base_url).read() - if zone.startswith("us"): - archive = "http://us.ec2.archive.ubuntu.com/ubuntu/" - elif zone.startswith("eu"): - archive = "http://eu.ec2.archive.ubuntu.com/ubuntu/" + mirror = ec2.get_mirror_for_availability_zone() - return(archive) + if not os.path.exists("/var/run/ec2/sources.lists"): + t = os.popen("lsb_release -cs").read() + codename = t.strip() -def updateList(filename): - mirror = detectZone() - if not os.path.exists("/var/ec2/sources.lists"): - t = os.popen("lsb_release -c").read() - codename = t.split() - distro = codename[1] - - mp = {'mirror' : mirror, 'codename' : distro} - t = Template(file="/etc/ec2-init/templates/sources.list.tmpl", searchList=[mp]) - f = open("/var/ec2/sources.list", "w") - f.write('%s' %(t)) + mp = { 'mirror' : mirror, 'codename' : codename } + t = Template(file='/etc/ec2-init/templates/sources.list.tmpl', searchList=[mp]) + f = open("/var/run/ec2/sources.list", "w") + f.write(t.respond()) f.close() - os.system("mv /etc/apt/sources.list /etc/apt/sources.list-ec2-init") - os.symlink("/var/ec2/sources.list", "/etc/apt/sources.list") - cache = apt.Cache(apt.progress.OpProgress()) - prog = apt.progress.FetchProgress() - cache.update(prog) - - os.system('touch %s' %(filename)) - -def get_ami_id(): - api_ver = '2008-02-01' - - url = 'http://169.254.169.254/%s/meta-data' % api_ver - ami_id = urllib.urlopen('%s/ami-id/' %url).read() - return ami_id - - -checkServer() - -ami_id = get_ami_id() -filename = '/var/ec2/.apt-already-ran.%s' %ami_id + if not os.path.exists("/etc/apt/sources.list-ec2-init"): + os.rename('/etc/apt/sources.list', '/etc/apt/sources.list-ec2-init') + os.symlink('/var/run/ec2/sources.list', '/etc/apt/sources.list') + aptget = subprocess.Popen(['apt-get', 'update']) + aptget.communicate() -if os.path.exists(filename): - print "ec2-set-apt-sources already ran....skipping." -else: - updateList(filename) +if __name__ == '__main__': + main() diff --git a/ec2-set-defaults.py b/ec2-set-defaults.py index 410e57d8..eec696e4 100755 --- a/ec2-set-defaults.py +++ b/ec2-set-defaults.py @@ -19,66 +19,35 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # -import urllib -import os -import socket -import time -from Cheetah.Template import Template - -api_ver = '2008-02-01' -metadata = None - -def checkServer(): - for x in range(30*60): - s = socket.socket() - try: - address = '169.254.169.254' - port = 80 - s.connect((address,port)) - s.close() - return - except socket.error, e: - time.sleep(1) - -checkServer() - -base_url = 'http://169.254.169.254/%s/meta-data' % api_ver -zone = urllib.urlopen('%s/placement/availability-zone' % base_url).read() - -if zone.startswith("us"): - archive = "http://us.ec2.archive.ubuntu.com/ubuntu" -elif zone.startswith("eu"): - archive = "http://eu.ec2.archive.ubuntu.com/ubuntu" - -def set_language(location,filename): - if location.startswith("us"): - lang='en_US.UTF-8' - elif location.startswith("eu"): - lang='en_GB.UTF-8' - - os.system('locale-gen %s' %(lang)) - - mp = {'lang' : lang } - T = Template(file="/etc/ec2-init/templates/locale.tmpl", searchList=[mp]) - f = open("/var/ec2/locale", "w") - f.write('%s' %(T)) - f.close() - - os.system("mv /etc/default/locale /etc/default/locale-ec2-init") - os.system("ln -s /var/ec2/locale /etc/default/locale") - os.system(". /etc/default/locale") - - os.system('touch %s' %(filename)) - -def get_amid(): - url = 'http://169.254.169.254/%s/meta-data' % api_ver - ami_id = urllib.urlopen('%s/ami-id/' %url).read() - return ami_id - -ami = get_amid() -filename = '/var/ec2/.defaults-already-ran.%s' %ami - -if os.path.exists(filename): - print "ec2-set-defaults already ran...skipping" -else: - set_language(zone,filename) +import subprocess + +import ec2init + +def get_location_from_availability_zone(availability_zone): + if availability.startswith('us-'): + return 'us' + elif availability.startswith('eu-'): + return 'eu' + raise Exception('Could not determine location') + +location_archive_map = { + 'us' : 'http://us.ec2.archive.ubuntu.com/ubuntu', + 'eu' : 'http://eu.ec2.archive.ubuntu.com/ubuntu' +} + +location_locale_map = { + 'us' : 'en_US.UTF-8', + 'eu' : 'en_GB.UTF-8' +} + +def main(): + ec2 = ec2init.EC2Init() + + location = get_location_from_availability_zone(ec2.get_availability_zone()) + + locale = location_locale_map[location] + subprocess.Popen(['locale-gen', locale]).communicate() + subprocess.Popen(['update-locale', locale]).communicate() + +if __name__ == '__main__': + main() diff --git a/ec2-set-hostname.py b/ec2-set-hostname.py index 108fe5da..985f4ada 100755 --- a/ec2-set-hostname.py +++ b/ec2-set-hostname.py @@ -1,6 +1,6 @@ #!/usr/bin/python # -# Set up the hostname for ec2. +# Fetch login credentials for EC2 # Copyright 2008 Canonical Ltd. # # Author: Chuck Short <chuck.short@canonical.com> @@ -22,44 +22,22 @@ import urllib import os from Cheetah.Template import Template -api_ver = '2008-02-01' -metadata = None +import ec2init -def get_ami_id(): - api_ver = '2008-02-01' - metadata = None +def main(): + ec2 = ec2init.EC2Init() - url = 'http://169.254.169.254/%s/meta-data' % api_ver - ami_id = urllib.urlopen('%s/ami-id/' %url).read() - return ami_id + hostname = ec2.get_hostname() -def set_hostname(filename): - api_ver = '2008-02-01' - metadata = None - - base_url = 'http://169.254.169.254/%s/meta-data' % api_ver - my_hostname = urllib.urlopen('%s/local-hostname/' % base_url).read() - os.system('hostname %s' % my_hostname) + subprocess.Popen(['hostname', hostname']).communicate() # replace the ubuntu hostname in /etc/hosts - mp = {'hostname': my_hostname} + mp = {'hostname': hostname} t = Template(file="/etc/ec2-init/templates/hosts.tmpl", searchList=[mp]) - os.system("rm /etc/hosts") f = open("/etc/hosts", "w") - f.write('%s' %(t)) - f.close() - - os.system("rm /etc/hostname") - f = open("/etc/hostname", "w") - f.write('%s' %(my_hostname)) + f.write(t.respond()) f.close() - os.system('touch %s' %(filename)) - -id = get_ami_id() -filename = '/var/ec2/.hostname-already-ran.%s' %id -if os.path.exists(filename): - print "Hostname already set previously....skipping!" -else: - set_hostname(filename) +if __name__ == '__main__': + main() diff --git a/ec2init/__init__.py b/ec2init/__init__.py new file mode 100644 index 00000000..3fa0de1d --- /dev/null +++ b/ec2init/__init__.py @@ -0,0 +1,93 @@ +# +# Common code for the EC2 initialisation scripts in Ubuntu +# Copyright 2009 Canonical Ltd. +# +# Author: Soren Hansen <soren@canonical.com> +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +# + +from configobj import ConfigObj +import os +import socket + +import boto.utils + +class EC2Init(): + api_ver = '2008-02-01' + filename = '/etc/ec2-init/ec2-config.cfg' + + def __init__(self): + self.meta_data_base_url = 'http://169.254.169.254/%s/meta-data' % self.api_ver + self.user_data_base_url = 'http://169.254.169.254/%s/user-data' % self.api_ver + self.config = ConfigObj(filename) + self.wait_for_metadata_service() + bailout_command = self.get_cfg_option_str('bailout_command') + if bailout_command: + os.system(bailout_command) + + def get_cfg_option_bool(self, key): + val = self.config[key] + if val.lower() in ['1', 'on', 'yes']: + return True + return False + + def get_cfg_option_str(self, key): + return config[key] + + def get_ssh_keys(self): + data = urllib.urlopen('%s/public-keys/' % self.meta_data_base_url).read() + keyids = [line.split('=')[0] for line in data.split('\n')] + return [urllib.urlopen('%s/public-keys/%d/openssh-key' % (self.meta_data_base_url, int(keyid))).read().rstrip() for keyid in keyids] + + def get_user_data(self): + return boto.utils.get_instance_userdata() + + def get_instance_metadata(self): + self.instance_metadata = getattr(self, 'instance_metadata', boto.utils.get_instance_metadata()) + return self.instance_metadata + + def get_ami_id(self): + return self.get_instance_metadata()['ami-id'] + + def get_availability_zone(self): + return self.get_instance_metadata()['availability-zone'] + + def get_hostname(self): + return self.get_instance_metadata()['local-hostname'] + + def get_mirror_for_availability_zone(self): + availability_zone = self.get_availability_zone() + if zone.startswith("us"): + return 'http://us.ec2.archive.ubuntu.com/ubuntu/' + elif zone.startswith("eu"): + return 'http://eu.ec2.archive.ubuntu.com/ubuntu/' + + return 'http://archive.ubuntu.com/ubuntu/' + + def wait_for_metadata_service(self): + timeout = 2 + # This gives us about half an hour before we ultimately bail out + for x in range(10): + s = socket.socket() + try: + address = '169.254.169.254' + port = 80 + s.connect((address,port)) + s.close() + return True + except socket.error, e: + time.sleep(timeout) + timeout = timeout * 2 + return False diff --git a/setup.py b/setup.py new file mode 100755 index 00000000..851e559f --- /dev/null +++ b/setup.py @@ -0,0 +1,21 @@ +from distutils.core import setup +from glob import glob +import os.path +import subprocess + +setup(name='EC2-init', + version='0.5', + description='EC2 initialisation magic', + author='Soren Hansen', + author_email='soren@canonical.com', + url='http://launchpad.net/ec2-init/', + packages=['ec2init'], + scripts=['ec2-fetch-credentials.py', + 'ec2-get-info.py', + 'ec2-run-user-data.py', + 'ec2-set-apt-sources.py', + 'ec2-set-defaults.py', + 'ec2-set-hostname.py'], + data_files=[('/etc/ec2-init', ['debian/ec2-config.cfg']), + ('/etc/ec2-init/templates', glob('templates/*'))], + ) |