Add a launch script.

Add a script to launch new OpenStack project servers.

Change-Id: I9f12ac0b7e38592128de1d6b999a5d540d621514
Reviewed-on: https://review.openstack.org/14246
Reviewed-by: Clark Boylan <clark.boylan@gmail.com>
Approved: Monty Taylor <mordred@inaugust.com>
Reviewed-by: Monty Taylor <mordred@inaugust.com>
Tested-by: Jenkins
This commit is contained in:
James E. Blair 2012-10-09 20:28:07 +00:00 committed by Jenkins
parent 1d6ae7dd83
commit 745cc18290
4 changed files with 470 additions and 0 deletions

41
launch/README Normal file
View File

@ -0,0 +1,41 @@
To launch a node in the OpenStack CI account (production servers)::
. openstackci-rs-nova.sh
To launch a node in the OpenStack Jenkins account (slave nodes)::
. openstackjenkins-rs-nova.sh
Then::
puppet cert generate servername.openstack.org
./launch-node.py servername.openstack.org --cert servername.openstack.org.pem
If you are launching a replacement server, you may skip the generate
step and specify the name of an existing puppet cert (as long as the
private key is on this host).
The server name and cert names may be different.
Manually add the hostname to DNS (the launch script does not do so
automatically).
DNS
===
There are no scripts to handle DNS at the moment due to a lack of
library support for the new Rackspace Cloud DNS (with IPv6). To
manually update DNS, you will need the hostname, v4 and v6 addresses
of the host, as well as the UUID. The environment variables used in
the URL should be satisfied by sourcing the "openstackci-rs-nova.sh"
script (or jenkins, as appropriate).
. ~/rackdns-venv/bin/activate
. openstackci-rs-nova.sh
rackdns rdns-create --name HOSTNAME.openstack.org --data IPV6ADDR --server-href https://$OS_REGION_NAME.servers.api.rackspacecloud.com/v2/$OS_TENANT_NAME/servers/UUID --ttl 300
rackdns rdns-create --name HOSTNAME.openstack.org --data IPV4ADDR --server-href https://$OS_REGION_NAME.servers.api.rackspacecloud.com/v2/$OS_TENANT_NAME/servers/UUID --ttl 300
. openstack-rs-nova.sh
rackdns record-create --name HOSTNAME.openstack.org --type AAAA --data IPV6ADDR --ttl 300 openstack.org
rackdns record-create --name HOSTNAME.openstack.org --type A --data IPV4ADDR --ttl 300 openstack.org

190
launch/launch-node.py Executable file
View File

@ -0,0 +1,190 @@
#!/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 commands
import time
import subprocess
import traceback
import socket
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']
def get_client():
args = [NOVA_USERNAME, NOVA_PASSWORD, NOVA_PROJECT_ID, NOVA_URL]
kwargs = {}
kwargs['region_name'] = NOVA_REGION_NAME
kwargs['service_type'] = 'compute'
from novaclient.v1_1.client import Client
client = Client(*args, **kwargs)
return client
def bootstrap_server(server, admin_pass, key, cert):
client = server.manager.api
ip = utils.get_public_ip(server)
if not ip:
raise Exception("Unable to find public ip of server")
ssh_kwargs = {}
if key:
ssh_kwargs['pkey'] = key
else:
ssh_kwargs['password'] = admin_pass
for username in ['root', 'ubuntu']:
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")
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)
ssh_client.ssh("apt-get update")
ssh_client.ssh("DEBIAN_FRONTEND=noninteractive apt-get --option"
" 'Dpkg::Options::=--force-confold'"
" --assume-yes upgrade")
ssh_client.ssh("apt-get install -y --force-yes puppet")
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("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")
ssh_client.ssh("puppet agent "
"--server ci-puppetmaster.openstack.org "
"--no-daemonize --verbose --onetime "
"--certname %s" % certname)
def build_server(client, name, image, flavor, cert):
key = None
server = None
create_kwargs = dict(image=image, flavor=flavor, name=name)
key_name = 'launch-%i' % (time.time())
if 'os-keypairs' in utils.get_extensions(client):
print "Adding keypair"
key, kp = utils.add_keypair(client, key_name)
create_kwargs['key_name'] = key_name
try:
server = client.servers.create(**create_kwargs)
except Exception, real_error:
try:
kp.delete()
except Exception, delete_error:
print "Exception encountered deleting keypair:"
traceback.print_exc()
raise
try:
admin_pass = server.adminPass
server = utils.wait_for_resource(server)
bootstrap_server(server, admin_pass, key, cert)
if key:
kp.delete()
except Exception, real_error:
try:
utils.delete_server(server)
except Exception, delete_error:
print "Exception encountered deleting server:"
traceback.print_exc()
# Raise the important exception that started this
raise
def main():
parser = argparse.ArgumentParser()
parser.add_argument("name", help="server name")
parser.add_argument("--ram", dest="ram", default=1024, type=int,
help="minimum amount of ram")
parser.add_argument("--image", dest="image",
default="Ubuntu 12.04 LTS (Precise Pangolin)",
help="image name")
parser.add_argument("--cert", dest="cert", required=True,
help="name of signed puppet certificate file (e.g., "
"hostname.example.com.pem)")
options = parser.parse_args()
client = get_client()
if not os.path.exists(os.path.join("/var/lib/puppet/ssl/private_keys",
options.cert)):
raise Exception("Please specify the name of a signed puppet cert.")
flavors = [f for f in client.flavors.list() if f.ram >= options.ram]
flavors.sort(lambda a, b: cmp(a.ram, b.ram))
flavor = flavors[0]
print "Found flavor", flavor
images = [i for i in client.images.list()
if (options.image.lower() in i.name.lower() and
not i.name.endswith('(Kernel)') and
not i.name.endswith('(Ramdisk)'))]
if len(images) > 1:
print "Ambiguous image name; matches:"
for i in images:
print i.name
sys.exit(1)
if len(images) == 0:
print "Unable to find matching image; image list:"
for i in client.images.list():
print i.name
sys.exit(1)
image = images[0]
print "Found image", image
build_server(client, options.name, image, flavor, options.cert)
if __name__ == '__main__':
main()

51
launch/sshclient.py Normal file
View File

@ -0,0 +1,51 @@
#!/usr/bin/env python
# Update the base image that is used for devstack VMs.
# 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 paramiko
import sys
class SSHClient(object):
def __init__(self, ip, username, password=None, pkey=None):
client = paramiko.SSHClient()
client.load_system_host_keys()
client.set_missing_host_key_policy(paramiko.WarningPolicy())
client.connect(ip, username=username, password=password, pkey=pkey)
self.client = client
def ssh(self, command, error_ok=False):
stdin, stdout, stderr = self.client.exec_command(command)
print command
output = ''
for x in stdout:
output += x
sys.stdout.write(x)
ret = stdout.channel.recv_exit_status()
print stderr.read()
if (not error_ok) and ret:
raise Exception("Unable to %s" % command)
return ret, output
def scp(self, source, dest):
print 'copy', source, dest
ftp = self.client.open_sftp()
ftp.put(source, dest)
ftp.close()

188
launch/utils.py Normal file
View File

@ -0,0 +1,188 @@
#!/usr/bin/env python
# Update the base image that is used for devstack VMs.
# 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 novaclient
from novaclient.v1_1 import client as Client11
try:
from v1_0 import client as Client10
except:
pass
import time
import os
import traceback
import paramiko
import socket
from sshclient import SSHClient
def iterate_timeout(max_seconds, purpose):
start = time.time()
count = 0
while (time.time() < start + max_seconds):
count += 1
yield count
time.sleep(2)
raise Exception("Timeout waiting for %s" % purpose)
def get_client(provider):
args = [provider.nova_username, provider.nova_api_key,
provider.nova_project_id, provider.nova_auth_url]
kwargs = {}
if provider.nova_service_type:
kwargs['service_type'] = provider.nova_service_type
if provider.nova_service_name:
kwargs['service_name'] = provider.nova_service_name
if provider.nova_service_region:
kwargs['region_name'] = provider.nova_service_region
if provider.nova_api_version == '1.0':
Client = Client10.Client
elif provider.nova_api_version == '1.1':
Client = Client11.Client
else:
raise Exception("API version not supported")
if provider.nova_rax_auth:
os.environ['NOVA_RAX_AUTH'] = '1'
client = Client(*args, **kwargs)
return client
extension_cache = {}
def get_extensions(client):
global extension_cache
cache = extension_cache.get(client)
if cache:
return cache
try:
resp, body = client.client.get('/extensions')
extensions = [x['alias'] for x in body['extensions']]
except novaclient.exceptions.NotFound:
extensions = []
extension_cache[client] = extensions
return extensions
def get_flavor(client, min_ram):
flavors = [f for f in client.flavors.list() if f.ram >= min_ram]
flavors.sort(lambda a, b: cmp(a.ram, b.ram))
return flavors[0]
def get_public_ip(server, version=4):
if 'os-floating-ips' in get_extensions(server.manager.api):
print 'using floating ips'
for addr in server.manager.api.floating_ips.list():
print 'checking addr', addr
if addr.instance_id == server.id:
print 'found addr', addr
return addr.ip
print 'no floating ip, addresses:'
print server.addresses
for addr in server.addresses.get('public', []):
if type(addr) == type(u''): # Rackspace/openstack 1.0
return addr
if addr['version'] == version: #Rackspace/openstack 1.1
return addr['addr']
for addr in server.addresses.get('private', []):
if addr['version'] == version and not addr['addr'].startswith('10.'): #HPcloud
return addr['addr']
return None
def add_public_ip(server):
ip = server.manager.api.floating_ips.create()
print "created floating ip", ip
server.add_floating_ip(ip)
for count in iterate_timeout(600, "ip to be added"):
try:
newip = ip.manager.get(ip.id)
except:
print "Unable to get ip details, will retry"
traceback.print_exc()
time.sleep(5)
continue
if newip.instance_id == server.id:
print 'ip has been added'
return
def add_keypair(client, name):
key = paramiko.RSAKey.generate(2048)
public_key = key.get_name() + ' ' + key.get_base64()
kp = client.keypairs.create(name, public_key)
return key, kp
def wait_for_resource(wait_resource):
last_progress = None
last_status = None
# It can take a _very_ long time for Rackspace 1.0 to save an image
for count in iterate_timeout(21600, "waiting for %s" % wait_resource):
try:
resource = wait_resource.manager.get(wait_resource.id)
except:
print "Unable to list resources, will retry"
traceback.print_exc()
time.sleep(5)
continue
# In Rackspace v1.0, there is no progress attribute while queued
if hasattr(resource, 'progress'):
if last_progress != resource.progress or last_status != resource.status:
print resource.status, resource.progress
last_progress = resource.progress
elif last_status != resource.status:
print resource.status
last_status = resource.status
if resource.status == 'ACTIVE':
return resource
def ssh_connect(ip, username, connect_kwargs={}, timeout=60):
# HPcloud may return errno 111 for about 30 seconds after adding the IP
for count in iterate_timeout(timeout, "ssh access"):
try:
client = SSHClient(ip, username, **connect_kwargs)
break
except socket.error, e:
print "While testing ssh access:", e
time.sleep(5)
ret, out = client.ssh("echo access okay")
if "access okay" in out:
return client
return None
def delete_server(server):
try:
if 'os-floating-ips' in get_extensions(server.manager.api):
for addr in server.manager.api.floating_ips.list():
if addr.instance_id == server.id:
server.remove_floating_ip(addr)
addr.delete()
except:
print "Unable to remove floating IP"
traceback.print_exc()
try:
if 'os-keypairs' in get_extensions(server.manager.api):
for kp in server.manager.api.keypairs.list():
if kp.name == server.key_name:
kp.delete()
except:
print "Unable to delete keypair"
traceback.print_exc()
print "Deleting server", server.id
server.delete()