diff --git a/launch/dns.py b/launch/dns.py
index c4f516df39..eb962692e9 100755
--- a/launch/dns.py
+++ b/launch/dns.py
@@ -18,21 +18,19 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import sys
 import os
 import argparse
 import utils
 
-NOVA_USERNAME = os.environ['OS_USERNAME']
-NOVA_PASSWORD = os.environ['OS_PASSWORD']
-NOVA_URL = os.environ['OS_AUTH_URL']
-NOVA_PROJECT_ID = os.environ['OS_TENANT_NAME']
-NOVA_REGION_NAME = os.environ['OS_REGION_NAME']
-
-SCRIPT_DIR = os.path.dirname(sys.argv[0])
-
 
 def get_client():
+    #TODO use shade
+    NOVA_USERNAME = os.environ['OS_USERNAME']
+    NOVA_PASSWORD = os.environ['OS_PASSWORD']
+    NOVA_URL = os.environ['OS_AUTH_URL']
+    NOVA_PROJECT_ID = os.environ['OS_TENANT_NAME']
+    NOVA_REGION_NAME = os.environ['OS_REGION_NAME']
+
     args = [NOVA_USERNAME, NOVA_PASSWORD, NOVA_PROJECT_ID, NOVA_URL]
     kwargs = {}
     kwargs['region_name'] = NOVA_REGION_NAME
@@ -84,6 +82,45 @@ def print_dns(client, name):
                 server.name, ip4))
 
 
+def shade_print_dns(server):
+    ip4 = server.public_v4
+    ip6 = server.public_v6
+    href = utils.get_href(server)
+
+    print
+    print "Run the following commands to set up DNS:"
+    print
+    print ". ~root/rackdns-venv/bin/activate"
+    print
+    print (
+        "rackdns rdns-create --name %s \\\n"
+        "    --data %s \\\n"
+        "    --server-href %s \\\n"
+        "    --ttl 3600" % (
+            server.name, ip6, href))
+    print
+    print (
+        "rackdns rdns-create --name %s \\\n"
+        "    --data %s \\\n"
+        "    --server-href %s \\\n"
+        "    --ttl 3600" % (
+            server.name, ip4, href))
+    print
+    print ". ~root/ci-launch/openstack-rs-nova.sh"
+    print
+    print (
+        "rackdns record-create --name %s \\\n"
+        "    --type AAAA --data %s \\\n"
+        "    --ttl 3600 openstack.org" % (
+            server.name, ip6))
+    print
+    print (
+        "rackdns record-create --name %s \\\n"
+        "    --type A --data %s \\\n"
+        "    --ttl 3600 openstack.org" % (
+            server.name, ip4))
+
+
 def main():
     parser = argparse.ArgumentParser()
     parser.add_argument("name", help="server name")
diff --git a/launch/shade-launch-node.py b/launch/shade-launch-node.py
new file mode 100644
index 0000000000..f8f3392d4b
--- /dev/null
+++ b/launch/shade-launch-node.py
@@ -0,0 +1,297 @@
+#!/usr/bin/env python
+
+# Launch a new OpenStack project infrastructure node.
+
+# Copyright (C) 2011-2012 OpenStack LLC.
+#
+# 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.
+
+import sys
+import os
+import time
+import traceback
+import argparse
+
+import dns
+import utils
+
+import os_client_config
+import paramiko
+import shade
+
+SCRIPT_DIR = os.path.dirname(sys.argv[0])
+
+try:
+    # This unactionable warning does not need to be printed over and over.
+    import requests.packages.urllib3
+    requests.packages.urllib3.disable_warnings()
+except:
+    pass
+
+
+def bootstrap_server(server, key, cert, environment, name,
+                     puppetmaster, volume, floating_ip_pool):
+    ip = server.public_v4
+    ssh_kwargs = dict(pkey=key)
+
+    print 'Public IP', ip
+    for username in ['root', 'ubuntu', 'centos', 'admin']:
+        ssh_client = utils.ssh_connect(ip, username, ssh_kwargs, timeout=600)
+        if ssh_client:
+            break
+
+    if not ssh_client:
+        raise Exception("Unable to log in via SSH")
+
+    # cloud-init puts the "please log in as user foo" message and
+    # subsequent exit() in root's authorized_keys -- overwrite it with
+    # a normal version to get root login working again.
+    if username != 'root':
+        ssh_client.ssh("sudo cp ~/.ssh/authorized_keys"
+                       " ~root/.ssh/authorized_keys")
+        ssh_client.ssh("sudo chmod 644 ~root/.ssh/authorized_keys")
+        ssh_client.ssh("sudo chown root.root ~root/.ssh/authorized_keys")
+
+    ssh_client = utils.ssh_connect(ip, 'root', ssh_kwargs, timeout=600)
+
+    if server.public_v6:
+        ssh_client.ssh('ping6 -c5 -Q 0x10 review.openstack.org '
+                       '|| ping6 -c5 -Q 0x10 wiki.openstack.org')
+
+    ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'make_swap.sh'),
+                   'make_swap.sh')
+    ssh_client.ssh('bash -x make_swap.sh')
+
+    if volume:
+        ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'mount_volume.sh'),
+                       'mount_volume.sh')
+        ssh_client.ssh('bash -x mount_volume.sh')
+
+    ssh_client.scp(os.path.join(SCRIPT_DIR, '..', 'install_puppet.sh'),
+                   'install_puppet.sh')
+    ssh_client.ssh('bash -x install_puppet.sh')
+
+    certname = cert[:(0 - len('.pem'))]
+    ssh_client.ssh("mkdir -p /var/lib/puppet/ssl/certs")
+    ssh_client.ssh("mkdir -p /var/lib/puppet/ssl/private_keys")
+    ssh_client.ssh("mkdir -p /var/lib/puppet/ssl/public_keys")
+    ssh_client.ssh("chown -R puppet:root /var/lib/puppet/ssl")
+    ssh_client.ssh("chown -R puppet:puppet /var/lib/puppet/ssl/private_keys")
+    ssh_client.ssh("chmod 0771 /var/lib/puppet/ssl")
+    ssh_client.ssh("chmod 0755 /var/lib/puppet/ssl/certs")
+    ssh_client.ssh("chmod 0750 /var/lib/puppet/ssl/private_keys")
+    ssh_client.ssh("chmod 0755 /var/lib/puppet/ssl/public_keys")
+
+    for ssldir in ['/var/lib/puppet/ssl/certs/',
+                   '/var/lib/puppet/ssl/private_keys/',
+                   '/var/lib/puppet/ssl/public_keys/']:
+        ssh_client.scp(os.path.join(ssldir, cert),
+                       os.path.join(ssldir, cert))
+
+    ssh_client.scp("/var/lib/puppet/ssl/crl.pem",
+                   "/var/lib/puppet/ssl/crl.pem")
+    ssh_client.scp("/var/lib/puppet/ssl/certs/ca.pem",
+                   "/var/lib/puppet/ssl/certs/ca.pem")
+
+    (rc, output) = ssh_client.ssh(
+        "puppet agent "
+        "--environment %s "
+        "--server %s "
+        "--detailed-exitcodes "
+        "--no-daemonize --verbose --onetime --pluginsync true "
+        "--certname %s" % (environment, puppetmaster, certname), error_ok=True)
+    utils.interpret_puppet_exitcodes(rc, output)
+
+    try:
+        ssh_client.ssh("reboot")
+    except Exception as e:
+        # Some init system kill the connection too fast after reboot.
+        # Deal with it by ignoring ssh errors when rebooting.
+        if e.rc == -1:
+            pass
+        else:
+            raise
+
+
+def build_server(cloud, name, image, flavor, cert, environment,
+                 puppetmaster, volume, keep, net_label,
+                 floating_ip_pool, boot_from_volume):
+    key = None
+    server = None
+
+    create_kwargs = dict(image=image, flavor=flavor, name=name,
+                         reuse_ips=False, wait=True)
+
+    #TODO: test with rax
+    #TODO: use shade
+    if boot_from_volume:
+        block_mapping = [{
+            'boot_index': '0',
+            'delete_on_termination': True,
+            'destination_type': 'volume',
+            'uuid': image.id,
+            'source_type': 'image',
+            'volume_size': '50',
+        }]
+        create_kwargs['image'] = None
+        create_kwargs['block_device_mapping_v2'] = block_mapping
+
+    #TODO: use shade
+    #if net_label:
+    #    nics = []
+    #    for net in client.networks.list():
+    #        if net.label == net_label:
+    #            nics.append({'net-id': net.id})
+    #    create_kwargs['nics'] = nics
+
+    key_name = 'launch-%i' % (time.time())
+    key = paramiko.RSAKey.generate(2048)
+    public_key = key.get_name() + ' ' + key.get_base64()
+    cloud.create_keypair(key_name, public_key)
+    create_kwargs['key_name'] = key_name
+
+    try:
+        server = cloud.create_server(**create_kwargs)
+    except Exception:
+        try:
+            cloud.delete_keypair(key_name)
+        except Exception:
+            print "Exception encountered deleting keypair:"
+            traceback.print_exc()
+        raise
+
+    try:
+        cloud.delete_keypair(key_name)
+
+        # TODO: use shade
+        if volume:
+            raise Exception("not implemented")
+            #vobj = client.volumes.create_server_volume(
+            #    server.id, volume, None)
+            #if not vobj:
+            #    raise Exception("Couldn't attach volume")
+
+        server = cloud.get_openstack_vars(server)
+        bootstrap_server(server, key, cert, environment, name,
+                         puppetmaster, volume, floating_ip_pool)
+        print('UUID=%s\nIPV4=%s\nIPV6=%s\n' % (server.id,
+                                               server.accessIPv4,
+                                               server.accessIPv6))
+    except Exception:
+        try:
+            if keep:
+                print "Server failed to build, keeping as requested."
+            else:
+                cloud.delete_server(server.id, delete_ips=True)
+        except Exception:
+            print "Exception encountered deleting server:"
+            traceback.print_exc()
+        # Raise the important exception that started this
+        raise
+
+    return server
+
+
+def main():
+    parser = argparse.ArgumentParser()
+    parser.add_argument("name", help="server name")
+    parser.add_argument("--cloud", dest="cloud", required=True,
+                        help="cloud name")
+    parser.add_argument("--region", dest="region",
+                        help="cloud region")
+    parser.add_argument("--flavor", dest="flavor", default='1GB',
+                        help="name (or substring) of flavor")
+    parser.add_argument("--image", dest="image",
+                        default="Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM)",
+                        help="image name")
+    parser.add_argument("--environment", dest="environment",
+                        default="production",
+                        help="puppet environment name")
+    parser.add_argument("--cert", dest="cert",
+                        help="name of signed puppet certificate file (e.g., "
+                        "hostname.example.com.pem)")
+    parser.add_argument("--server", dest="server", help="Puppetmaster to use.",
+                        default="puppetmaster.openstack.org")
+    parser.add_argument("--volume", dest="volume",
+                        help="UUID of volume to attach to the new server.",
+                        default=None)
+    parser.add_argument("--boot-from-volume", dest="boot_from_volume",
+                        help="Create a boot volume for the server and use it.",
+                        action='store_true',
+                        default=False)
+    parser.add_argument("--keep", dest="keep",
+                        help="Don't clean up or delete the server on error.",
+                        action='store_true',
+                        default=False)
+    parser.add_argument("--net-label", dest="net_label", default='',
+                        help="network label to attach instance to")
+    parser.add_argument("--fip-pool", dest="floating_ip_pool", default=None,
+                        help="pool to assign floating IP from")
+    options = parser.parse_args()
+
+    if options.cert:
+        cert = options.cert
+    else:
+        cert = options.name + ".pem"
+
+    if not os.path.exists(os.path.join("/var/lib/puppet/ssl/private_keys",
+                                       cert)):
+        raise Exception("Please specify the name of a signed puppet cert.")
+
+    cloud_kwargs = {}
+    if options.region:
+        cloud_kwargs['region_name'] = options.region
+    cloud_config = os_client_config.OpenStackConfig().get_one_cloud(
+        options.cloud, **cloud_kwargs)
+
+    cloud = shade.OpenStackCloud(cloud_config)
+
+    flavor = cloud.get_flavor(options.flavor)
+    if flavor:
+        print "Found flavor", flavor.name
+    else:
+        print "Unable to find matching flavor; flavor list:"
+        for i in cloud.list_flavors():
+            print i.name
+        sys.exit(1)
+
+    image = cloud.get_image_exclude(options.image, 'deprecated')
+    if image:
+        print "Found image", image.name
+    else:
+        print "Unable to find matching image; image list:"
+        for i in cloud.list_images():
+            print i.name
+        sys.exit(1)
+
+    if options.volume:
+        print "The --volume option does not support cinder; until it does"
+        print "it should not be used."
+        sys.exit(1)
+
+    server = build_server(cloud, options.name, image, flavor, cert,
+                          options.environment, options.server,
+                          options.volume, options.keep,
+                          options.net_label, options.floating_ip_pool,
+                          options.boot_from_volume)
+    dns.shade_print_dns(server)
+    # Remove the ansible inventory cache so that next run finds the new
+    # server
+    if os.path.exists('/var/cache/ansible-inventory/ansible-inventory.cache'):
+        os.unlink('/var/cache/ansible-inventory/ansible-inventory.cache')
+    os.system('/usr/local/bin/expand-groups.sh')
+
+if __name__ == '__main__':
+    main()
diff --git a/launch/utils.py b/launch/utils.py
index 1cb835fc10..a41e485583 100644
--- a/launch/utils.py
+++ b/launch/utils.py
@@ -115,6 +115,8 @@ def get_public_ip(server, version=4, floating_ip_pool=None):
 
 
 def get_href(server):
+    if not hasattr(server, 'links'):
+        return None
     for link in server.links:
         if link['rel'] == 'self':
             return link['href']