diff options
Diffstat (limited to 'buildscripts/makedist.py')
-rw-r--r-- | buildscripts/makedist.py | 799 |
1 files changed, 799 insertions, 0 deletions
diff --git a/buildscripts/makedist.py b/buildscripts/makedist.py new file mode 100644 index 0000000..35383b9 --- /dev/null +++ b/buildscripts/makedist.py @@ -0,0 +1,799 @@ +#!/usr/bin/env python + +# makedist.py: make a distro package (on an EC2 instance) + +# For ease of use, put a file called settings.py someplace in your +# sys.path, containing something like the following: + +# makedist = { +# # ec2-api-tools needs the following two set in the process +# # environment. +# "EC2_HOME": "/path/to/ec2-api-tools", +# # The EC2 tools won't run at all unless this variable is set to a directory +# # relative to which a "bin/java" exists. +# "JAVA_HOME" : "/usr", +# # All the ec2-api-tools take these two as arguments. +# # Alternatively, you can set the environment variables EC2_PRIVATE_KEY and EC2_CERT +# # respectively, leave these two out of settings.py, and let the ec2 tools default. +# "ec2_pkey": "/path/to/pk-file.pem" +# "ec2_cert" : "/path/to/cert-file.pem" +# # This gets supplied to ec2-run-instances to rig up an ssh key for +# # the remote user. +# "ec2_sshkey" : "key-id", +# # And so we need to tell our ssh processes where to find the +# # appropriate public key file. +# "ssh_keyfile" : "/path/to/key-id-file" +# } + +# Notes: although there is a Python library for accessing EC2 as a web +# service, it seemed as if it would be less work to just shell out to +# the three EC2 management tools we use. + +# To make a distribution we must: + +# 1. Fire up an EC2 AMI suitable for building. +# 2. Get any build-dependencies and configurations onto the remote host. +# 3. Fetch the mongodb source. +# 4. Run the package building tools. +# 5. Save the package archives someplace permanent (eventually we +# ought to install them into a public repository for the distro). +# Unimplemented: +# 6. Fire up an EC2 AMI suitable for testing whether the packages +# install. +# 7. Check whether the packages install and run. + +# The implementations of steps 1, 2, 4, 5, 6, and 7 will depend on the +# distro of host we're talking to (Ubuntu, CentOS, Debian, etc.). + +from __future__ import with_statement +import subprocess +import sys +import signal +import getopt +import socket +import time +import os.path +import tempfile + +# For the moment, we don't handle any of the errors we raise, so it +# suffices to have a simple subclass of Exception that just +# stringifies according to a desired format. +class SimpleError(Exception): + def __init__(self, *args): + self.args = args + def __str__(self): + return self.args[0] % self.args[1:] + +class SubcommandError(SimpleError): + def __init__(self, *args): + self.status = args[2] + super(SubcommandError, self).__init__(*args) + +class BaseConfigurator (object): + def __init__ (self, **kwargs): + self.configuration = [] + self.arch=kwargs["arch"] + self.distro_name=kwargs["distro_name"] + self.distro_version=kwargs["distro_version"] + + def lookup(self, what, dist, vers, arch): + for (wht, seq) in self.configuration: + if what == wht: + for ((dpat, vpat, apat), payload) in seq: + # For the moment, our pattern facility is just "*" or exact match. + if ((dist == dpat or dpat == "*") and + (vers == vpat or vpat == "*") and + (arch == apat or apat == "*")): + return payload + if getattr(self, what, False): + return getattr(self, what) + else: + raise SimpleError("couldn't find a%s %s configuration for dist=%s, version=%s, arch=%s", + "n" if ("aeiouAEIOU".find(what[0]) > -1) else "", + what, dist, vers, arch) + + def default(self, what): + return self.lookup(what, self.distro_name, self.distro_version, self.arch) + def findOrDefault(self, dict, what): + return (dict[what] if what in dict else self.lookup(what, self.distro_name, self.distro_version, self.arch)) + +class BaseHostConfigurator (BaseConfigurator): + def __init__(self, **kwargs): + super(BaseHostConfigurator, self).__init__(**kwargs) + self.configuration += [("distro_arch", + ((("debian", "*", "x86_64"), "amd64"), + (("ubuntu", "*", "x86_64"), "amd64"), + (("debian", "*", "x86"), "i386"), + (("ubuntu", "*", "x86"), "i386"), + (("centos", "*", "x86_64"), "x86_64"), + (("fedora", "*", "x86_64"), "x86_64"), + (("centos", "*", "x86"), "i386"), + (("fedora", "*", "x86"), "i386"), + (("*", "*", "x86_64"), "x86_64"), + (("*", "*", "x86"), "x86"))) , + ] + +class LocalHost(object): + @classmethod + def runLocally(cls, argv): + print "running %s" % argv + r = subprocess.Popen(argv).wait() + if r != 0: + raise SubcommandError("subcommand %s exited %d", argv, r) + +class EC2InstanceConfigurator(BaseConfigurator): + def __init__(self, **kwargs): + super(EC2InstanceConfigurator, self).__init__(**kwargs) + self.configuration += [("ec2_ami", + ((("ubuntu", "10.4", "x86_64"), "ami-bf07ead6"), + (("ubuntu", "10.4", "x86"), "ami-f707ea9e"), + (("ubuntu", "9.10", "x86_64"), "ami-55739e3c"), + (("ubuntu", "9.10", "x86"), "ami-bb709dd2"), + (("ubuntu", "9.4", "x86_64"), "ami-eef61587"), + (("ubuntu", "9.4", "x86"), "ami-ccf615a5"), + (("ubuntu", "8.10", "x86"), "ami-c0f615a9"), + (("ubuntu", "8.10", "x86_64"), "ami-e2f6158b"), + (("ubuntu", "8.4", "x86"), "ami59b35f30"), + (("ubuntu", "8.4", "x86_64"), "ami-27b35f4e"), + (("debian", "5.0", "x86"), "ami-dcf615b5"), + (("debian", "5.0", "x86_64"), "ami-f0f61599"), + (("centos", "5.4", "x86"), "ami-f8b35e91"), + (("centos", "5.4", "x86_64"), "ami-ccb35ea5"), + (("fedora", "8", "x86_64"), "ami-2547a34c"), + (("fedora", "8", "x86"), "ami-5647a33f"))), + ("ec2_mtype", + ((("*", "*", "x86"), "m1.small"), + (("*", "*", "x86_64"), "m1.large"))), + ] + + +class EC2Instance (object): + def __init__(self, configurator, **kwargs): + # Stuff we need to start an instance: AMI name, key and cert + # files. AMI and mtype default to configuration in this file, + # but can be overridden. + self.ec2_ami = configurator.findOrDefault(kwargs, "ec2_ami") + self.ec2_mtype = configurator.findOrDefault(kwargs, "ec2_mtype") + + self.use_internal_name = True if "use_internal_name" in kwargs else False + + # Authentication stuff defaults according to the conventions + # of the ec2-api-tools. + self.ec2_cert=kwargs["ec2_cert"] + self.ec2_pkey=kwargs["ec2_pkey"] + self.ec2_sshkey=kwargs["ec2_sshkey"] + + # FIXME: this needs to be a commandline option + self.ec2_groups = ["default", "buildbot-slave", "dist-slave"] + self.terminate = False if "no_terminate" in kwargs else True + + def parsedesc (self, hdl): + line1=hdl.readline() + splitline1=line1.split() + (_, reservation, unknown1, groupstr) = splitline1[:4] + groups = groupstr.split(',') + self.ec2_reservation = reservation + self.ec2_unknown1 = unknown1 + self.ec2_groups = groups + # I haven't seen more than 4 data fields in one of these + # descriptions, but what do I know? + if len(splitline1)>4: + print >> sys.stderr, "more than 4 fields in description line 1\n%s\n" % line1 + self.ec2_extras1 = splitline1[4:] + line2=hdl.readline() + splitline2=line2.split() + # The jerks make it tricky to parse line 2: the fields are + # dependent on the instance's state. + (_, instance, ami, status_or_hostname) = splitline2[:4] + self.ec2_instance = instance + if ami != self.ec2_ami: + print >> sys.stderr, "warning: AMI in description isn't AMI we invoked\nwe started %s, but got\n%s", (self.ec2_ami, line2) + # FIXME: are there other non-running statuses? + if status_or_hostname in ["pending", "terminated"]: + self.ec2_status = status_or_hostname + self.ec2_running = False + index = 4 + self.ec2_storage = splitline2[index+8] + else: + self.ec2_running = True + index = 6 + self.ec2_status = splitline2[5] + self.ec2_external_hostname = splitline2[3] + self.ec2_internal_hostname = splitline2[4] + self.ec2_external_ipaddr = splitline2[index+8] + self.ec2_internal_ipaddr = splitline2[index+9] + self.ec2_storage = splitline2[index+10] + (sshkey, unknown2, mtype, starttime, zone, unknown3, unknown4, monitoring) = splitline2[index:index+8] + # FIXME: potential disagreement with the supplied sshkey? + self.ec2_sshkey = sshkey + self.ec2_unknown2 = unknown2 + # FIXME: potential disagreement with the supplied mtype? + self.ec2_mtype = mtype + self.ec2_starttime = starttime + self.ec2_zone = zone + self.ec2_unknown3 = unknown3 + self.ec2_unknown4 = unknown4 + self.ec2_monitoring = monitoring + + def start(self): + "Fire up a fresh EC2 instance." + groups = reduce(lambda x, y : x+y, [["-g", i] for i in self.ec2_groups], []) + argv = ["ec2-run-instances", + self.ec2_ami, "-K", self.ec2_pkey, "-C", self.ec2_cert, + "-k", self.ec2_sshkey, "-t", self.ec2_mtype] + groups + self.ec2_running = False + print "running %s" % argv + proc = subprocess.Popen(argv, stdout=subprocess.PIPE) + try: + self.parsedesc(proc.stdout) + if self.ec2_instance == "": + raise SimpleError("instance id is empty") + else: + print "Instance id: %s" % self.ec2_instance + finally: + r = proc.wait() + if r != 0: + raise SimpleError("ec2-run-instances exited %d", r) + + def initwait(self): + # poll the instance description until we get a hostname. + # Note: it seems there can be a time interval after + # ec2-run-instance finishes during which EC2 will tell us that + # the instance ID doesn't exist. This is sort of bad. + state = "pending" + numtries = 0 + giveup = 5 + + while not self.ec2_running: + time.sleep(15) # arbitrary + argv = ["ec2-describe-instances", "-K", self.ec2_pkey, "-C", self.ec2_cert, self.ec2_instance] + proc = subprocess.Popen(argv, stdout=subprocess.PIPE) + try: + self.parsedesc(proc.stdout) + except Exception, e: + r = proc.wait() + if r < giveup: + print sys.stderr, str(e) + continue + else: + raise SimpleError("ec2-describe-instances exited %d", r) + numtries+=1 + + def stop(self): + if self.terminate: + LocalHost.runLocally(["ec2-terminate-instances", "-K", self.ec2_pkey, "-C", self.ec2_cert, self.ec2_instance]) + else: + print "Not terminating EC2 instance %s." % self.ec2_instance + + def __enter__(self): + self.start() + return self + + def __exit__(self, type, value, traceback): + self.stop() + + def getHostname(self): + return self.ec2_internal_hostname if self.use_internal_name else self.ec2_external_hostname + +class SshConnectionConfigurator (BaseConfigurator): + def __init__(self, **kwargs): + super(SshConnectionConfigurator, self).__init__(**kwargs) + self.configuration += [("ssh_login", + # FLAW: this actually depends more on the AMI + # than the triple. + ((("debian", "*", "*"), "root"), + (("ubuntu", "10.4", "*"), "ubuntu"), + (("ubuntu", "9.10", "*"), "ubuntu"), + (("ubuntu", "9.4", "*"), "root"), + (("ubuntu", "8.10", "*"), "root"), + (("ubuntu", "8.4", "*"), "ubuntu"), + (("centos", "*", "*"), "root"))), + ] + +class SshConnection (object): + def __init__(self, configurator, **kwargs): + # Stuff we need to talk to the thing properly + self.ssh_login = configurator.findOrDefault(kwargs, "ssh_login") + + self.ssh_host = kwargs["ssh_host"] + self.ssh_keyfile=kwargs["ssh_keyfile"] + # Gets set to False when we think we can ssh in. + self.sshwait = True + + def sshWait(self): + "Poll until somebody's listening on port 22" + + if self.sshwait == False: + return + while self.sshwait: + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + try: + try: + s.connect((self.ssh_host, 22)) + self.sshwait = False + print "connected on port 22 (ssh)" + time.sleep(15) # arbitrary timeout, in case the + # remote sshd is slow. + except socket.error, err: + pass + finally: + s.close() + time.sleep(3) # arbitrary timeout + + def initSsh(self): + self.sshWait() + ctlpath="/tmp/ec2-ssh-%s-%s-%s" % (self.ssh_host, self.ssh_login, os.getpid()) + argv = ["ssh", "-o", "StrictHostKeyChecking no", + "-M", "-o", "ControlPath %s" % ctlpath, + "-v", "-l", self.ssh_login, "-i", self.ssh_keyfile, + self.ssh_host] + print "Setting up ssh master connection with %s" % argv + self.sshproc = subprocess.Popen(argv) + self.ctlpath = ctlpath + + + def __enter__(self): + self.initSsh() + return self + + def __exit__(self, type, value, traceback): + os.kill(self.sshproc.pid, signal.SIGTERM) + self.sshproc.wait() + + def runRemotely(self, argv): + """Run a command on the host.""" + LocalHost.runLocally(["ssh", "-o", "StrictHostKeyChecking no", + "-S", self.ctlpath, + "-l", self.ssh_login, + "-i", self.ssh_keyfile, + self.ssh_host] + argv) + + def sendFiles(self, files): + self.sshWait() + for (localfile, remotefile) in files: + LocalHost.runLocally(["scp", "-o", "StrictHostKeyChecking no", + "-o", "ControlMaster auto", + "-o", "ControlPath %s" % self.ctlpath, + "-i", self.ssh_keyfile, + "-rv", localfile, + self.ssh_login + "@" + self.ssh_host + ":" + + ("" if remotefile is None else remotefile) ]) + + def recvFiles(self, files): + self.sshWait() + print files + for (remotefile, localfile) in files: + LocalHost.runLocally(["scp", "-o", "StrictHostKeyChecking no", + "-o", "ControlMaster auto", + "-o", "ControlPath %s" % self.ctlpath, + "-i", self.ssh_keyfile, + "-rv", + self.ssh_login + "@" + self.ssh_host + + ":" + remotefile, + "." if localfile is None else localfile ]) + + +class ScriptFileConfigurator (BaseConfigurator): + deb_productdir = "dists" + rpm_productdir = "/usr/src/redhat/RPMS" # FIXME: this could be + # ~/redhat/RPMS or + # something elsewhere + + preamble_commands = """ +set -x # verbose execution, for debugging +set -e # errexit, stop on errors +""" + # Strictly speaking, we don't need to mangle debian files on rpm + # systems (and vice versa), but (a) it doesn't hurt anything to do + # so, and (b) mangling files the same way everywhere could + # conceivably help uncover bugs in the hideous hideous sed + # programs we're running here. (N.B., for POSIX wonks: POSIX sed + # doesn't support either in-place file editing, which we use + # below. So if we end up wanting to run these mangling commands + # e.g., on a BSD, we'll need to make them fancier.) + mangle_files_commands =""" +# On debianoids, the package names in the changelog and control file +# must agree, and only files in a subdirectory of debian/ matching the +# package name will get included in the .deb, so we also have to mangle +# the rules file. +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i '1s/.*([^)]*)/{pkg_name}{pkg_name_suffix} ({pkg_version})/' debian/changelog ) || exit 1 +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i 's/^Source:.*/Source: {pkg_name}{pkg_name_suffix}/; +s/^Package:.*mongodb/Package: {pkg_name}{pkg_name_suffix}\\ +Conflicts: {pkg_name_conflicts}/' debian/control; ) || exit 1 +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i 's|$(CURDIR)/debian/mongodb/|$(CURDIR)/debian/{pkg_name}{pkg_name_suffix}/|g' debian/rules) || exit 1 +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i 's|debian/mongodb.manpages|debian/{pkg_name}{pkg_name_suffix}.manpages|g' debian/rules) || exit 1 +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i '/^Name:/s/.*/Name: {pkg_name}{pkg_name_suffix}/; /^Version:/s/.*/Version: {pkg_version}/;' rpm/mongo.spec ) +# Debian systems require some ridiculous workarounds to get an init +# script at /etc/init.d/mongodb when the packge name isn't the init +# script name. Note: dh_installinit --name won't work, because that +# option would require the init script under debian/ to be named +# mongodb. +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && +ln debian/init.d debian/{pkg_name}{pkg_name_suffix}.mongodb.init && +ln debian/mongodb.upstart debian/{pkg_name}{pkg_name_suffix}.mongodb.upstart && +sed -i 's/dh_installinit/dh_installinit --name=mongodb/' debian/rules) || exit 1 +""" + + mangle_files_for_ancient_redhat_commands = """ +# Ancient RedHats ship with very old boosts and non-UTF8-aware js +# libraries, so we need to link statically to those. +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && sed -i 's|^scons.*((inst)all)|scons --prefix=$RPM_BUILD_ROOT/usr --extralib=nspr4 --staticlib=boost_system-mt,boost_thread-mt,boost_filesystem-mt,boost_program_options-mt,js $1|' rpm/mongo.spec ) +""" + + deb_prereq_commands = """ +# Configure debconf to never prompt us for input. +export DEBIAN_FRONTEND=noninteractive +apt-get update +apt-get install -y {pkg_prereq_str} +""" + + deb_build_commands=""" +mkdir -p "{pkg_product_dir}/{distro_version}/10gen/binary-{distro_arch}" +mkdir -p "{pkg_product_dir}/{distro_version}/10gen/source" +( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}"; debuild ) || exit 1 +# Try installing it +dpkg -i *.deb +ps ax | grep mongo || {{ echo "no running mongo" >/dev/stderr; exit 1; }} +cp {pkg_name}{pkg_name_suffix}*.deb "{pkg_product_dir}/{distro_version}/10gen/binary-{distro_arch}" +cp {pkg_name}{pkg_name_suffix}*.dsc "{pkg_product_dir}/{distro_version}/10gen/source" +cp {pkg_name}{pkg_name_suffix}*.tar.gz "{pkg_product_dir}/{distro_version}/10gen/source" +dpkg-scanpackages "{pkg_product_dir}/{distro_version}/10gen/binary-{distro_arch}" /dev/null | gzip -9c > "{pkg_product_dir}/{distro_version}/10gen/binary-{distro_arch}/Packages.gz" +dpkg-scansources "{pkg_product_dir}/{distro_version}/10gen/source" /dev/null | gzip -9c > "{pkg_product_dir}/{distro_version}/10gen/source/Sources.gz" +""" + rpm_prereq_commands = """ +rpm -Uvh http://download.fedora.redhat.com/pub/epel/5/{distro_arch}/epel-release-5-3.noarch.rpm +yum -y install {pkg_prereq_str} +""" + rpm_build_commands=""" +for d in BUILD BUILDROOT RPMS SOURCES SPECS SRPMS; do mkdir -p /usr/src/redhat/$d; done +cp -v "{pkg_name}{pkg_name_suffix}-{pkg_version}/rpm/mongo.spec" /usr/src/redhat/SPECS +tar -cpzf /usr/src/redhat/SOURCES/"{pkg_name}{pkg_name_suffix}-{pkg_version}".tar.gz "{pkg_name}{pkg_name_suffix}-{pkg_version}" +rpmbuild -ba /usr/src/redhat/SPECS/mongo.spec +""" + # FIXME: this is clean, but adds 40 minutes or so to the build process. + old_rpm_precommands = """ +yum install -y bzip2-devel python-devel libicu-devel chrpath zlib-devel nspr-devel readline-devel ncurses-devel +# FIXME: this is just some random URL found on rpmfind some day in 01/2010. +wget ftp://194.199.20.114/linux/EPEL/5Client/SRPMS/js-1.70-8.el5.src.rpm +rpm -ivh js-1.70-8.el5.src.rpm +sed -i 's/XCFLAGS.*$/XCFLAGS=\"%{{optflags}} -fPIC -DJS_C_STRINGS_ARE_UTF8\" \\\\/' /usr/src/redhat/SPECS/js.spec +rpmbuild -ba /usr/src/redhat/SPECS/js.spec +rpm -Uvh /usr/src/redhat/RPMS/{distro_arch}/js-1.70-8.{distro_arch}.rpm +rpm -Uvh /usr/src/redhat/RPMS/{distro_arch}/js-devel-1.70-8.{distro_arch}.rpm +# FIXME: this is just some random URL found on rpmfind some day in 01/2010. +wget ftp://195.220.108.108/linux/sourceforge/g/project/gr/gridiron2/support-files/FC10%20source%20RPMs/boost-1.38.0-1.fc10.src.rpm +rpm -ivh boost-1.38.0-1.fc10.src.rpm +rpmbuild -ba /usr/src/redhat/SPECS/boost.spec +rpm -ivh /usr/src/redhat/RPMS/{distro_arch}/boost-1.38.0-1.{distro_arch}.rpm +rpm -ivh /usr/src/redhat/RPMS/{distro_arch}/boost-devel-1.38.0-1.{distro_arch}.rpm +""" + + # This horribleness is an attempt to work around ways that you're + # not really meant to package things for Debian unless you are + # Debian. + + # On very old Debianoids, libboost-<foo>-dev will be some old + # boost that's not as thready as we want, but which Eliot says + # will work. + very_old_deb_prereqs = ["libboost-thread-dev", "libboost-filesystem-dev", "libboost-program-options-dev", "libboost-date-time-dev", "libboost-dev", "xulrunner1.9-dev"] + + # On less old (but still old!) Debianoids, libboost-<foo>-dev is + # still a 1.34, but 1.35 packages are available, so we want those. + old_deb_prereqs = ["libboost-thread1.35-dev", "libboost-filesystem1.35-dev", "libboost-program-options1.35-dev", "libboost-date-time1.35-dev", "libboost1.35-dev", "xulrunner-dev"] + + # On newer Debianoids, libbost-<foo>-dev is some sufficiently new + # thing. + new_deb_prereqs = [ "libboost-thread-dev", "libboost-filesystem-dev", "libboost-program-options-dev", "libboost-date-time-dev", "libboost-dev", "xulrunner-dev" ] + + common_deb_prereqs = [ "build-essential", "dpkg-dev", "libreadline-dev", "libpcap-dev", "libpcre3-dev", "git-core", "scons", "debhelper", "devscripts", "git-core" ] + + centos_preqres = ["js-devel", "readline-devel", "pcre-devel", "gcc-c++", "scons", "rpm-build", "git" ] + fedora_prereqs = ["js-devel", "readline-devel", "pcre-devel", "gcc-c++", "scons", "rpm-build", "git" ] + + def __init__(self, **kwargs): + super(ScriptFileConfigurator, self).__init__(**kwargs) + if kwargs["mongo_version"][0] == 'r': + self.get_mongo_commands = """ +wget -Otarball.tgz "http://github.com/mongodb/mongo/tarball/{mongo_version}"; +tar xzf tarball.tgz +mv "`tar tzf tarball.tgz | sed 's|/.*||' | sort -u | head -n1`" "{pkg_name}{pkg_name_suffix}-{pkg_version}" +""" + else: + self.get_mongo_commands = """ +git clone git://github.com/mongodb/mongo.git +""" + if kwargs['mongo_version'][0] == 'v': + self.get_mongo_commands +=""" +( cd mongo && git archive --prefix="{pkg_name}{pkg_name_suffix}-{pkg_version}/" "`git log origin/{mongo_version} | sed -n '1s/^commit //p;q'`" ) | tar xf - +""" + else: + self.get_mongo_commands += """ +( cd mongo && git archive --prefix="{pkg_name}{pkg_name_suffix}-{pkg_version}/" "{mongo_version}" ) | tar xf - +""" + + if "local_mongo_dir" in kwargs: + self.mangle_files_commands = """( cd "{pkg_name}{pkg_name_suffix}-{pkg_version}" && rm -rf debian rpm && cp -pvR ~/pkg/* . ) +""" + self.mangle_files_commands + + self.configuration += [("pkg_product_dir", + ((("ubuntu", "*", "*"), self.deb_productdir), + (("debian", "*", "*"), self.deb_productdir), + (("fedora", "*", "*"), self.rpm_productdir), + (("centos", "*", "*"), self.rpm_productdir))), + ("pkg_prereqs", + ((("ubuntu", "9.4", "*"), + self.old_deb_prereqs + self.common_deb_prereqs), + (("ubuntu", "9.10", "*"), + self.new_deb_prereqs + self.common_deb_prereqs), + (("ubuntu", "10.4", "*"), + self.new_deb_prereqs + self.common_deb_prereqs), + (("ubuntu", "8.10", "*"), + self.old_deb_prereqs + self.common_deb_prereqs), + (("ubuntu", "8.4", "*"), + self.very_old_deb_prereqs + self.common_deb_prereqs), + (("debian", "5.0", "*"), + self.old_deb_prereqs + self.common_deb_prereqs), + (("fedora", "8", "*"), + self.fedora_prereqs), + (("centos", "5.4", "*"), + self.centos_preqres))), + ("commands", + ((("debian", "*", "*"), + self.preamble_commands + self.deb_prereq_commands + self.get_mongo_commands + self.mangle_files_commands + self.deb_build_commands), + (("ubuntu", "*", "*"), + self.preamble_commands + self.deb_prereq_commands + self.get_mongo_commands + self.mangle_files_commands + self.deb_build_commands), + (("centos", "*", "*"), + self.preamble_commands + self.old_rpm_precommands + self.rpm_prereq_commands + self.get_mongo_commands + self.mangle_files_commands + self.mangle_files_for_ancient_redhat_commands + self.rpm_build_commands), + (("fedora", "*", "*"), + self.preamble_commands + self.old_rpm_precommands + self.rpm_prereq_commands + self.get_mongo_commands + self.mangle_files_commands + self.rpm_build_commands))), + ("pkg_name", + ((("debian", "*", "*"), "mongodb"), + (("ubuntu", "*", "*"), "mongodb"), + (("centos", "*", "*"), "mongo"), + + (("fedora", "*", "*"), "mongo") + )), + ("pkg_name_conflicts", + ((("*", "*", "*"), ["", "-stable", "-unstable", "-snapshot"]), + )) + ] + + + + +class ScriptFile(object): + def __init__(self, configurator, **kwargs): + self.mongo_version = kwargs["mongo_version"] + self.pkg_version = kwargs["pkg_version"] + self.pkg_name_suffix = kwargs["pkg_name_suffix"] if "pkg_name_suffix" in kwargs else "" + self.pkg_prereqs = configurator.default("pkg_prereqs") + self.pkg_name = configurator.default("pkg_name") + self.pkg_product_dir = configurator.default("pkg_product_dir") + self.pkg_name_conflicts = configurator.default("pkg_name_conflicts") if self.pkg_name_suffix else [] + self.pkg_name_conflicts.remove(self.pkg_name_suffix) if self.pkg_name_suffix and self.pkg_name_suffix in self.pkg_name_conflicts else [] + self.formatter = configurator.default("commands") + self.distro_name = configurator.default("distro_name") + self.distro_version = configurator.default("distro_version") + self.distro_arch = configurator.default("distro_arch") + + def genscript(self): + return self.formatter.format(mongo_version=self.mongo_version, + distro_name=self.distro_name, + distro_version=self.distro_version, + distro_arch=self.distro_arch, + pkg_prereq_str=" ".join(self.pkg_prereqs), + pkg_name=self.pkg_name, + pkg_name_suffix=self.pkg_name_suffix, + pkg_version=self.pkg_version, + pkg_product_dir=self.pkg_product_dir, + # KLUDGE: rpm specs and deb + # control files use + # comma-separated conflicts, + # but there's no reason to + # suppose this works elsewhere + pkg_name_conflicts = ", ".join([self.pkg_name+conflict for conflict in self.pkg_name_conflicts]) + ) + + def __enter__(self): + self.localscript=None + # One of tempfile or I is very stupid. + (fh, name) = tempfile.mkstemp('', "makedist.", ".") + try: + pass + finally: + os.close(fh) + with open(name, 'w+') as fh: + fh.write(self.genscript()) + self.localscript=name + return self + + def __exit__(self, type, value, traceback): + if self.localscript: + os.unlink(self.localscript) + +class Configurator(SshConnectionConfigurator, EC2InstanceConfigurator, ScriptFileConfigurator, BaseHostConfigurator): + def __init__(self, **kwargs): + super(Configurator, self).__init__(**kwargs) + +def main(): +# checkEnvironment() + + (kwargs, args) = processArguments() + (rootdir, distro_name, distro_version, arch, mongo_version_spec) = args[:5] + # FIXME: there are a few other characters that we can't use in + # file names on Windows, in case this program really needs to run + # there. + distro_name = distro_name.replace('/', '-').replace('\\', '-') + distro_version = distro_version.replace('/', '-').replace('\\', '-') + arch = arch.replace('/', '-').replace('\\', '-') + try: + import settings + if "makedist" in dir ( settings ): + for key in ["EC2_HOME", "JAVA_HOME"]: + if key in settings.makedist: + os.environ[key] = settings.makedist[key] + for key in ["ec2_pkey", "ec2_cert", "ec2_sshkey", "ssh_keyfile" ]: + if key not in kwargs and key in settings.makedist: + kwargs[key] = settings.makedist[key] + except Exception, err: + print "No settings: %s. Continuing anyway..." % err + pass + + # Ensure that PATH contains $EC2_HOME/bin + vars = ["EC2_HOME", "JAVA_HOME"] + for var in vars: + if os.getenv(var) == None: + raise SimpleError("Environment variable %s is unset; did you create a settings.py?", var) + + if len([True for x in os.environ["PATH"].split(":") if x.find(os.environ["EC2_HOME"]) > -1]) == 0: + os.environ["PATH"]=os.environ["EC2_HOME"]+"/bin:"+os.environ["PATH"] + + + kwargs["distro_name"] = distro_name + kwargs["distro_version"] = distro_version + kwargs["arch"] = arch + + foo = mongo_version_spec.split(":") + kwargs["mongo_version"] = foo[0] # this can be a commit id, a + # release id "r1.2.2", or a + # branch name starting with v. + if len(foo) > 1: + kwargs["pkg_name_suffix"] = foo[1] + if len(foo) > 2 and foo[2]: + kwargs["pkg_version"] = foo[2] + else: + kwargs["pkg_version"] = time.strftime("%Y%m%d") + + # FIXME: this should also include the mongo version or something. + if "subdirs" in kwargs: + kwargs["localdir"] = "%s/%s/%s/%s" % (rootdir, distro_name, distro_version, arch, kwargs["mongo_version"]) + else: + kwargs["localdir"] = rootdir + + if "pkg_name_suffix" not in kwargs: + if kwargs["mongo_version"][0] in ["r", "v"]: + nums = kwargs["mongo_version"].split(".") + if int(nums[1]) % 2 == 0: + kwargs["pkg_name_suffix"] = "-stable" + else: + kwargs["pkg_name_suffix"] = "-unstable" + else: + kwargs["pkg_name_suffix"] = "" + + + kwargs['local_gpg_dir'] = kwargs["local_gpg_dir"] if "local_gpg_dir" in kwargs else os.path.expanduser("~/.gnupg") + configurator = Configurator(**kwargs) + LocalHost.runLocally(["mkdir", "-p", kwargs["localdir"]]) + with ScriptFile(configurator, **kwargs) as script: + with open(script.localscript) as f: + print """# Going to run the following on a fresh AMI:""" + print f.read() + time.sleep(10) + with EC2Instance(configurator, **kwargs) as ec2: + ec2.initwait() + kwargs["ssh_host"] = ec2.getHostname() + with SshConnection(configurator, **kwargs) as ssh: + ssh.runRemotely(["uname -a; ls /"]) + ssh.runRemotely(["mkdir", "pkg"]) + if "local_mongo_dir" in kwargs: + ssh.sendFiles([(kwargs["local_mongo_dir"]+'/'+d, "pkg") for d in ["rpm", "debian"]]) + ssh.sendFiles([(kwargs['local_gpg_dir'], ".gnupg")]) + ssh.sendFiles([(script.localscript, "makedist.sh")]) + ssh.runRemotely((["sudo"] if ssh.ssh_login != "root" else [])+ ["sh", "makedist.sh"]) + ssh.recvFiles([(script.pkg_product_dir, kwargs['localdir'])]) + +def processArguments(): + # flagspec [ (short, long, argument?, description, argname)* ] + flagspec = [ ("?", "usage", False, "Print a (useless) usage message", None), + ("h", "help", False, "Print a help message and exit", None), + ("N", "no-terminate", False, "Leave the EC2 instance running at the end of the job", None), + ("S", "subdirs", False, "Create subdirectories of the output directory based on distro name, version, and architecture", None), + ("I", "use-internal-name", False, "Use the EC2 internal hostname for sshing", None), + (None, "local-gpg-dir", True, "Local directory of gpg junk", "STRING"), + (None, "local-mongo-dir", True, "Copy packaging files from local mongo checkout", "DIRECTORY"), + ] + shortopts = "".join([t[0] + (":" if t[2] else "") for t in flagspec if t[0] is not None]) + longopts = [t[1] + ("=" if t[2] else "") for t in flagspec] + + try: + opts, args = getopt.getopt(sys.argv[1:], shortopts, longopts) + except getopt.GetoptError, err: + print str(err) + sys.exit(2) + + # Normalize the getopt-parsed options. + kwargs = {} + for (opt, arg) in opts: + flag = opt + opt = opt.lstrip("-") + if flag[:2] == '--': #long opt + kwargs[opt.replace('-', '_')] = arg + elif flag[:1] == "-": #short opt + ok = False + for tuple in flagspec: + if tuple[0] == opt: + ok = True + kwargs[tuple[1].replace('-', '_')] = arg + break + if not ok: + raise SimpleError("this shouldn't happen: unrecognized option flag: %s", opt) + else: + raise SimpleError("this shouldn't happen: non-option returned from getopt()") + + if "help" in kwargs: + print "Usage: %s [OPTIONS] DIRECTORY DISTRO DISTRO-VERSION ARCHITECTURE MONGO-VERSION-SPEC" % sys.argv[0] + print """Build some packages on new EC2 AMI instances, leave packages under DIRECTORY. + +MONGO-VERSION-SPEC has the syntax +Commit(:Pkg-Name-Suffix(:Pkg-Version)). If Commit starts with an 'r', +build from a tagged release; if Commit starts with a 'v', build from +the HEAD of a version branch; otherwise, build whatever git commit is +identified by Commit. Pkg-Name-Suffix gets appended to the package +name, and defaults to "-stable" and "-unstable" if Commit looks like +it designates a stable or unstable release/branch, respectively. +Pkg-Version is used as the package version, and defaults to YYYYMMDD. +Examples: + + HEAD # build a snapshot of HEAD, name the package + # "mongodb", use YYYYMMDD for the version + + HEAD:-snap # build a snapshot of HEAD, name the package + # "mongodb-snap", use YYYYMMDD for the version + + HEAD:-snap:123 # build a snapshot of HEAD, name the package + # "mongodb-snap", use 123 for the version + + HEAD:-suffix:1.3 # build a snapshot of HEAD, name the package + # "mongodb-snapshot", use "1.3 for the version + + r1.2.3 # build a package of the 1.2.3 release, call it "mongodb-stable", + # make the package version YYYYMMDD. + + v1.2:-stable: # build a package of the HEAD of the 1.2 branch + + decafbad:-foo:123 # build git commit "decafbad", call the package + # "mongodb-foo" with package version 123. + +Options:""" + for t in flagspec: + print "%-20s\t%s." % ("%4s--%s%s:" % ("-%s, " % t[0] if t[0] else "", t[1], ("="+t[4]) if t[4] else ""), t[3]) + print """ +Mandatory arguments to long options are also mandatory for short +options. Some EC2 arguments default to (and override) environment +variables; see the ec2-api-tools documentation.""" + sys.exit(0) + + if "usage" in kwargs: + print "Usage: %s [OPTIONS] OUTPUT-DIR DISTRO-NAME DISTRO-VERSION ARCHITECTURE MONGO-VERSION-SPEC" % sys.argv[0] + sys.exit(0) + + + return (kwargs, args) + + +if __name__ == "__main__": + main() + +# Examples: + +# ./makedist.py --local-gpg-dir=$HOME/10gen/dst/dist-gnupg /tmp/ubuntu ubuntu 8.10 x86_64 HEAD:-snapshot |