Merge branch 'master' of github.com:angst/cc

This commit is contained in:
Joshua McKenty 2010-07-14 22:11:48 -07:00
commit 46865fb454
9 changed files with 503 additions and 10 deletions

View File

@ -0,0 +1,82 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
"""
Download images from Canonical Image Store
"""
import json
import os
import tempfile
import shutil
import subprocess
import sys
import urllib2
from nova.objectstore import image
from nova import flags
from nova import utils
FLAGS = flags.FLAGS
api_url = 'https://imagestore.canonical.com/api/dashboard'
image_cache = None
def images():
global image_cache
if not image_cache:
try:
images = json.load(urllib2.urlopen(api_url))['images']
image_cache = [i for i in images if i['title'].find('amd64') > -1]
except Exception:
print 'unable to download canonical image list'
sys.exit(1)
return image_cache
# FIXME(ja): add checksum/signature checks
def download(img):
tempdir = tempfile.mkdtemp(prefix='cis-')
kernel_id = None
ramdisk_id = None
for f in img['files']:
if f['kind'] == 'kernel':
dest = os.path.join(tempdir, 'kernel')
subprocess.call(['curl', f['url'], '-o', dest])
kernel_id = image.Image.add(dest,
description='kernel/' + img['title'], kernel=True)
for f in img['files']:
if f['kind'] == 'ramdisk':
dest = os.path.join(tempdir, 'ramdisk')
subprocess.call(['curl', f['url'], '-o', dest])
ramdisk_id = image.Image.add(dest,
description='ramdisk/' + img['title'], ramdisk=True)
for f in img['files']:
if f['kind'] == 'image':
dest = os.path.join(tempdir, 'image')
subprocess.call(['curl', f['url'], '-o', dest])
ramdisk_id = image.Image.add(dest,
description=img['title'], kernel=kernel_id, ramdisk=ramdisk_id)
shutil.rmtree(tempdir)
def main():
utils.default_flagfile()
argv = FLAGS(sys.argv)
if len(argv) == 2:
for img in images():
if argv[1] == 'all' or argv[1] == img['title']:
download(img)
else:
print 'usage: %s (title|all)'
print 'available images:'
for image in images():
print image['title']
if __name__ == '__main__':
main()

58
bin/nova-rsapi Executable file
View File

@ -0,0 +1,58 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright [2010] [Anso Labs, 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.
"""
WSGI daemon for the main API endpoint.
"""
import logging
from wsgiref import simple_server
from nova import vendor
from tornado import ioloop
from nova import flags
from nova import rpc
from nova import server
from nova import utils
from nova.auth import users
from nova.endpoint import rackspace
FLAGS = flags.FLAGS
flags.DEFINE_integer('cc_port', 8773, 'cloud controller port')
def main(_argv):
user_manager = users.UserManager()
api_instance = rackspace.Api(user_manager)
conn = rpc.Connection.instance()
rpc_consumer = rpc.AdapterConsumer(connection=conn,
topic=FLAGS.cloud_topic,
proxy=api_instance)
# TODO: fire rpc response listener (without attach to tornado)
# io_inst = ioloop.IOLoop.instance()
# _injected = consumer.attach_to_tornado(io_inst)
http_server = simple_server.WSGIServer(('0.0.0.0', FLAGS.cc_port), simple_server.WSGIRequestHandler)
http_server.set_app(api_instance.handler)
logging.debug('Started HTTP server on port %i' % FLAGS.cc_port)
while True:
http_server.handle_request()
# io_inst.start()
if __name__ == '__main__':
utils.default_flagfile()
server.serve('nova-rsapi', main)

33
exercise_rsapi.py Normal file
View File

@ -0,0 +1,33 @@
import cloudservers
class IdFake:
def __init__(self, id):
self.id = id
# to get your access key:
# from nova.auth import users
# users.UserManger.instance().get_users()[0].access
rscloud = cloudservers.CloudServers(
'admin',
'6cca875e-5ab3-4c60-9852-abf5c5c60cc6'
)
rscloud.client.AUTH_URL = 'http://localhost:8773/v1.0'
rv = rscloud.servers.list()
print "SERVERS: %s" % rv
if len(rv) == 0:
server = rscloud.servers.create(
"test-server",
IdFake("ami-tiny"),
IdFake("m1.tiny")
)
print "LAUNCH: %s" % server
else:
server = rv[0]
print "Server to kill: %s" % server
raw_input("press enter key to kill the server")
server.delete()

View File

@ -514,6 +514,18 @@ class CloudController(object):
# vpn image is private so it doesn't show up on lists
if kwargs['image_id'] != FLAGS.vpn_image_id:
image = self._get_image(context, kwargs['image_id'])
# FIXME(ja): if image is cloudpipe, this breaks
# get defaults from imagestore
image_id = image['imageId']
kernel_id = image.get('kernelId', None)
ramdisk_id = image.get('ramdiskId', None)
# API parameters overrides of defaults
kernel_id = kwargs.get('kernel_id', kernel_id)
ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id)
logging.debug("Going to run instances...")
reservation_id = utils.generate_uid('r')
launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
@ -534,12 +546,9 @@ class CloudController(object):
bridge_name = net['bridge_name']
for num in range(int(kwargs['max_count'])):
inst = self.instdir.new()
# TODO(ja): add ari, aki
inst['image_id'] = kwargs['image_id']
if 'kernel_id' in kwargs:
inst['kernel_id'] = kwargs['kernel_id']
if 'ramdisk_id' in kwargs:
inst['ramdisk_id'] = kwargs['ramdisk_id']
inst['image_id'] = image_id
inst['kernel_id'] = kernel_id
inst['ramdisk_id'] = ramdisk_id
inst['user_data'] = kwargs.get('user_data', '')
inst['instance_type'] = kwargs.get('instance_type', 'm1.small')
inst['reservation_id'] = reservation_id

226
nova/endpoint/rackspace.py Normal file
View File

@ -0,0 +1,226 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright [2010] [Anso Labs, 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.
"""
Rackspace API
"""
import base64
import json
import logging
import multiprocessing
import os
import time
from nova import vendor
import tornado.web
from twisted.internet import defer
from nova import datastore
from nova import flags
from nova import rpc
from nova import utils
from nova import exception
from nova.auth import users
from nova.compute import model
from nova.compute import network
from nova.endpoint import wsgi
from nova.endpoint import images
from nova.volume import storage
FLAGS = flags.FLAGS
flags.DEFINE_string('cloud_topic', 'cloud', 'the topic clouds listen on')
# TODO(todd): subclass Exception so we can bubble meaningful errors
class Api(object):
def __init__(self, rpc_mechanism):
self.controllers = {
"v1.0": RackspaceAuthenticationApi(),
"servers": RackspaceCloudServerApi()
}
self.rpc_mechanism = rpc_mechanism
def handler(self, environ, responder):
environ['nova.context'] = self.build_context(environ)
controller, path = wsgi.Util.route(
environ['PATH_INFO'],
self.controllers
)
if not controller:
# TODO(todd): Exception (404)
raise Exception("Missing Controller")
rv = controller.process(path, environ)
if type(rv) is tuple:
responder(rv[0], rv[1])
rv = rv[2]
else:
responder("200 OK", [])
return rv
def build_context(self, env):
rv = {}
if env.has_key("HTTP_X_AUTH_TOKEN"):
rv['user'] = users.UserManager.instance().get_user_from_access_key(
env['HTTP_X_AUTH_TOKEN']
)
if rv['user']:
rv['project'] = users.UserManager.instance().get_project(
rv['user'].name
)
return rv
class RackspaceApiEndpoint(object):
def process(self, path, env):
if not self.check_authentication(env):
# TODO(todd): Exception (Unauthorized)
raise Exception("Unable to authenticate")
if len(path) == 0:
return self.index(env)
action = path.pop(0)
if hasattr(self, action):
method = getattr(self, action)
return method(path, env)
else:
# TODO(todd): Exception (404)
raise Exception("Missing method %s" % path[0])
def check_authentication(self, env):
if hasattr(self, "process_without_authentication") \
and getattr(self, "process_without_authentication"):
return True
if not env['nova.context']['user']:
return False
return True
class RackspaceAuthenticationApi(RackspaceApiEndpoint):
def __init__(self):
self.process_without_authentication = True
# TODO(todd): make a actual session with a unique token
# just pass the auth key back through for now
def index(self, env):
response = '204 No Content'
headers = [
('X-Server-Management-Url', 'http://%s' % env['HTTP_HOST']),
('X-Storage-Url', 'http://%s' % env['HTTP_HOST']),
('X-CDN-Managment-Url', 'http://%s' % env['HTTP_HOST']),
('X-Auth-Token', env['HTTP_X_AUTH_KEY'])
]
body = ""
return (response, headers, body)
class RackspaceCloudServerApi(RackspaceApiEndpoint):
def __init__(self):
self.instdir = model.InstanceDirectory()
self.network = network.PublicNetworkController()
def index(self, env):
if env['REQUEST_METHOD'] == 'GET':
return self.detail(env)
elif env['REQUEST_METHOD'] == 'POST':
return self.launch_server(env)
def detail(self, args, env):
value = {
"servers":
[]
}
for inst in self.instdir.all:
value["servers"].append(self.instance_details(inst))
return json.dumps(value)
##
##
def launch_server(self, env):
data = json.loads(env['wsgi.input'].read(int(env['CONTENT_LENGTH'])))
inst = self.build_server_instance(data, env['nova.context'])
self.schedule_launch_of_instance(inst)
return json.dumps({"server": self.instance_details(inst)})
def instance_details(self, inst):
return {
"id": inst.get("instance_id", None),
"imageId": inst.get("image_id", None),
"flavorId": inst.get("instacne_type", None),
"hostId": inst.get("node_name", None),
"status": inst.get("state", "pending"),
"addresses": {
"public": [self.network.get_public_ip_for_instance(
inst.get("instance_id", None)
)],
"private": [inst.get("private_dns_name", None)]
},
# implemented only by Rackspace, not AWS
"name": inst.get("name", "Not-Specified"),
# not supported
"progress": "Not-Supported",
"metadata": {
"Server Label": "Not-Supported",
"Image Version": "Not-Supported"
}
}
def build_server_instance(self, env, context):
reservation = utils.generate_uid('r')
ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
inst = self.instdir.new()
inst['name'] = env['server']['name']
inst['image_id'] = env['server']['imageId']
inst['instance_type'] = env['server']['flavorId']
inst['user_id'] = context['user'].id
inst['project_id'] = context['project'].id
inst['reservation_id'] = reservation
inst['launch_time'] = ltime
inst['mac_address'] = utils.generate_mac()
address = network.allocate_ip(
inst['user_id'],
inst['project_id'],
mac=inst['mac_address']
)
inst['private_dns_name'] = str(address)
inst['bridge_name'] = network.BridgedNetwork.get_network_for_project(
inst['user_id'],
inst['project_id'],
'default' # security group
)['bridge_name']
# key_data, key_name, ami_launch_index
# TODO(todd): key data or root password
inst.save()
return inst
def schedule_launch_of_instance(self, inst):
rpc.cast(
FLAGS.compute_topic,
{
"method": "run_instance",
"args": {"instance_id": inst.instance_id}
}
)

23
nova/endpoint/wsgi.py Normal file
View File

@ -0,0 +1,23 @@
'''
Utility methods for working with WSGI servers
'''
class Util(object):
@staticmethod
def route(reqstr, controllers):
if len(reqstr) == 0:
return Util.select_root_controller(controllers), []
parts = [x for x in reqstr.split("/") if len(x) > 0]
if len(parts) == 0:
return Util.select_root_controller(controllers), []
return controllers[parts[0]], parts[1:]
@staticmethod
def select_root_controller(controllers):
if '' in controllers:
return controllers['']
else:
return None

View File

@ -286,8 +286,8 @@ class ImageHandler(BaseRequestHandler):
if not bucket_object.is_authorized(self.context):
raise web.HTTPError(403)
p = multiprocessing.Process(target=image.Image.create,args=
(image_id, image_location, self.context))
p = multiprocessing.Process(target=image.Image.register_aws_image,
args=(image_id, image_location, self.context))
p.start()
self.finish()

View File

@ -100,7 +100,69 @@ class Image(object):
return json.load(f)
@staticmethod
def create(image_id, image_location, context):
def add(src, description, kernel=None, ramdisk=None, public=True):
"""adds an image to imagestore
@type src: str
@param src: location of the partition image on disk
@type description: str
@param description: string describing the image contents
@type kernel: bool or str
@param kernel: either TRUE meaning this partition is a kernel image or
a string of the image id for the kernel
@type ramdisk: bool or str
@param ramdisk: either TRUE meaning this partition is a ramdisk image or
a string of the image id for the ramdisk
@type public: bool
@param public: determine if this is a public image or private
@rtype: str
@return: a string with the image id
"""
image_type = 'machine'
image_id = utils.generate_uid('ami')
if kernel is True:
image_type = 'kernel'
image_id = utils.generate_uid('aki')
if ramdisk is True:
image_type = 'ramdisk'
image_id = utils.generate_uid('ari')
image_path = os.path.join(FLAGS.images_path, image_id)
os.makedirs(image_path)
shutil.copyfile(src, os.path.join(image_path, 'image'))
info = {
'imageId': image_id,
'imageLocation': description,
'imageOwnerId': 'system',
'isPublic': public,
'architecture': 'x86_64',
'type': image_type,
'state': 'available'
}
if type(kernel) is str and len(kernel) > 0:
info['kernelId'] = kernel
if type(ramdisk) is str and len(ramdisk) > 0:
info['ramdiskId'] = ramdisk
with open(os.path.join(image_path, 'info.json'), "w") as f:
json.dump(info, f)
return image_id
@staticmethod
def register_aws_image(image_id, image_location, context):
image_path = os.path.join(FLAGS.images_path, image_id)
os.makedirs(image_path)

View File

@ -155,7 +155,7 @@ class ObjectStoreTestCase(test.BaseTestCase):
bucket[os.path.basename(path)] = open(path, 'rb').read()
# register an image
objectstore.image.Image.create('i-testing', 'image_bucket/1mb.manifest.xml', self.context)
objectstore.image.Image.register_aws_image('i-testing', 'image_bucket/1mb.manifest.xml', self.context)
# verify image
my_img = objectstore.image.Image('i-testing')