summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSandy Walsh <sandy.walsh@rackspace.com>2011-05-11 06:28:07 -0700
committerSandy Walsh <sandy.walsh@rackspace.com>2011-05-11 06:28:07 -0700
commiteb0619c91b4756d355b7a5cd5c1f16d342f14a6b (patch)
tree9ff60eeffb5581dfb2509e5cae64f5a5cc0de67f
parent5f2bfe56cf12d8f45ae24a5c9dd0c99e6c4d0310 (diff)
downloadnova-eb0619c91b4756d355b7a5cd5c1f16d342f14a6b.tar.gz
First cut with tests passing
-rw-r--r--nova/api/openstack/__init__.py3
-rw-r--r--nova/api/openstack/zones.py48
-rw-r--r--nova/crypto.py45
-rw-r--r--nova/scheduler/api.py6
-rw-r--r--nova/scheduler/zone_aware_scheduler.py88
-rw-r--r--nova/tests/api/openstack/test_zones.py40
-rw-r--r--nova/tests/test_crypto.py48
7 files changed, 277 insertions, 1 deletions
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 348b70d5bc..b743d306be 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -98,7 +98,8 @@ class APIRouter(wsgi.Router):
server_members['inject_network_info'] = 'POST'
mapper.resource("zone", "zones", controller=zones.Controller(),
- collection={'detail': 'GET', 'info': 'GET'}),
+ collection={'detail': 'GET', 'info': 'GET',
+ 'select': 'GET',}),
mapper.resource("user", "users", controller=users.Controller(),
collection={'detail': 'GET'})
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 227ffecdc7..70653dc0e0 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -13,6 +13,10 @@
# License for the specific language governing permissions and limitations
# under the License.
+import json
+import urlparse
+
+from nova import crypto
from nova import db
from nova import flags
from nova import log as logging
@@ -21,6 +25,12 @@ from nova.scheduler import api
FLAGS = flags.FLAGS
+flags.DEFINE_string('build_plan_encryption_key',
+ None,
+ '128bit (hex) encryption key for scheduler build plans.')
+
+
+LOG = logging.getLogger('nova.api.openstack.zones')
def _filter_keys(item, keys):
@@ -41,6 +51,14 @@ def _scrub_zone(zone):
'deleted', 'deleted_at', 'updated_at'))
+def check_encryption_key(func):
+ def wrapped(*args, **kwargs):
+ if not FLAGS.build_plan_encryption_key:
+ raise exception.Error(_("--build_plan_encryption_key not set"))
+ return func(*args, **kwargs)
+ return wrapped
+
+
class Controller(common.OpenstackController):
_serialization_metadata = {
@@ -97,3 +115,33 @@ class Controller(common.OpenstackController):
zone_id = int(id)
zone = api.zone_update(context, zone_id, env["zone"])
return dict(zone=_scrub_zone(zone))
+
+ @check_encryption_key
+ def select(self, req):
+ """Returns a weighted list of costs to create instances
+ of desired capabilities."""
+ ctx = req.environ['nova.context']
+ qs = req.environ['QUERY_STRING']
+ param_dict = urlparse.parse_qs(qs)
+ param_dict.pop("fresh", None)
+ # parse_qs returns a dict where the values are lists,
+ # since query strings can have multiple values for the
+ # same key. We need to convert that to single values.
+ for key in param_dict:
+ param_dict[key] = param_dict[key][0]
+ build_plan = api.select(ctx, specs=param_dict)
+ cooked = self._scrub_build_plan(build_plan)
+ return {"weights": cooked}
+
+ def _scrub_build_plan(self, build_plan):
+ """Remove all the confidential data and return a sanitized
+ version of the build plan. Include an encrypted full version
+ of the weighting entry so we can get back to it later."""
+ encryptor = crypto.encryptor(FLAGS.build_plan_encryption_key)
+ cooked = []
+ for entry in build_plan:
+ json_entry = json.dumps(entry)
+ cipher_text = encryptor(json_entry)
+ cooked.append(dict(weight=entry['weight'],
+ blob=cipher_text))
+ return cooked
diff --git a/nova/crypto.py b/nova/crypto.py
index 14b9cbef6b..bdc32482ab 100644
--- a/nova/crypto.py
+++ b/nova/crypto.py
@@ -332,6 +332,51 @@ def mkcacert(subject='nova', years=1):
return cert, pk, pkey
+def _build_cipher(key, iv, encode=True):
+ """Make a 128bit AES CBC encode/decode Cipher object.
+ Padding is handled internally."""
+ operation = 1 if encode else 0
+ return M2Crypto.EVP.Cipher(alg='aes_128_cbc', key=key, iv=iv, op=operation)
+
+
+def encryptor(key, iv=None):
+ """Simple symmetric key encryption."""
+ key = base64.b64decode(key)
+ if iv is None:
+ iv = '\0' * 16
+ else:
+ iv = base64.b64decode(iv)
+
+ def encrypt(data):
+ cipher = _build_cipher(key, iv, encode=True)
+ v = cipher.update(data)
+ v = v + cipher.final()
+ del cipher
+ v = base64.b64encode(v)
+ return v
+
+ return encrypt
+
+
+def decryptor(key, iv=None):
+ """Simple symmetric key decryption."""
+ key = base64.b64decode(key)
+ if iv is None:
+ iv = '\0' * 16
+ else:
+ iv = base64.b64decode(iv)
+
+ def decrypt(data):
+ data = base64.b64decode(data)
+ cipher = _build_cipher(key, iv, encode=False)
+ v = cipher.update(data)
+ v = v + cipher.final()
+ del cipher
+ return v
+
+ return decrypt
+
+
# Copyright (c) 2006-2009 Mitch Garnaat http://garnaat.org/
#
# Permission is hereby granted, free of charge, to any person obtaining a
diff --git a/nova/scheduler/api.py b/nova/scheduler/api.py
index 816ae55131..d8a0025edc 100644
--- a/nova/scheduler/api.py
+++ b/nova/scheduler/api.py
@@ -81,6 +81,12 @@ def get_zone_capabilities(context):
return _call_scheduler('get_zone_capabilities', context=context)
+def select(context, specs=None):
+ """Returns a list of hosts."""
+ return _call_scheduler('select', context=context,
+ params={"specs": specs})
+
+
def update_service_capabilities(context, service_name, host, capabilities):
"""Send an update to all the scheduler services informing them
of the capabilities of this service."""
diff --git a/nova/scheduler/zone_aware_scheduler.py b/nova/scheduler/zone_aware_scheduler.py
new file mode 100644
index 0000000000..b849e8de10
--- /dev/null
+++ b/nova/scheduler/zone_aware_scheduler.py
@@ -0,0 +1,88 @@
+# Copyright (c) 2011 Openstack, LLC.
+# All Rights Reserved.
+#
+# 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.
+
+"""
+The Zone Aware Scheduler is a base class Scheduler for creating instances
+across zones. There are two expansion points to this class for:
+1. Assigning Weights to hosts for requested instances
+2. Filtering Hosts based on required instance capabilities
+"""
+
+import operator
+
+from nova import log as logging
+from nova.scheduler import api
+
+LOG = logging.getLogger('nova.scheduler.zone_aware_scheduler')
+
+
+class ZoneAwareScheduler(object):
+ """Base class for creating Zone Aware Schedulers."""
+
+ def _call_zone_method(self, context, method, specs):
+ """Call novaclient zone method. Broken out for testing."""
+ return api.call_zone_method(context, method, specs=specs)
+
+ def select(self, context, *args, **kwargs):
+ """Select returns a list of weights and zone/host information
+ corresponding to the best hosts to service the request. Any
+ child zone information has been encrypted so as not to reveal
+ anything about the children."""
+ return self._schedule(context, "compute", *args, **kwargs)
+
+ def schedule(self, context, topic, *args, **kwargs):
+ """The schedule() contract requires we return the one
+ best-suited host for this request.
+ """
+ res = self._schedule(context, topic, *args, **kwargs)
+ return res[0]
+
+ def _schedule(self, context, topic, *args, **kwargs):
+ """Returns a list of hosts that meet the required specs,
+ ordered by their fitness.
+ """
+ # Filter local hosts based on requirements ...
+ host_list = self.filter_hosts()
+
+ # then weigh the selected hosts.
+ # weighted = [ { 'weight':#, 'name':host, ...}, ]
+ weighted = self.weight_hosts(host_list)
+
+ # Next, tack on the best weights from the child zones ...
+ child_results = self._call_zone_method(context, "select",
+ specs=specs)
+ for child_zone, result in child_results:
+ for weighting in result:
+ # Remember the child_zone so we can get back to
+ # it later if needed. This implicitly builds a zone
+ # path structure.
+ host_dict = {
+ "weight": weighting["weight"],
+ "child_zone": child_zone,
+ "child_blob": weighting["blob"]}
+ weighted.append(host_dict)
+
+ weighted.sort(key=operator.itemgetter('weight'))
+ return weighted
+
+ def filter_hosts(self):
+ """Derived classes must override this method and return
+ a list of hosts in [?] format."""
+ raise NotImplemented()
+
+ def weigh_hosts(self, hosts):
+ """Derived classes must override this method and return
+ a lists of hosts in [?] format."""
+ raise NotImplemented()
diff --git a/nova/tests/api/openstack/test_zones.py b/nova/tests/api/openstack/test_zones.py
index 5d5799b59f..8790390915 100644
--- a/nova/tests/api/openstack/test_zones.py
+++ b/nova/tests/api/openstack/test_zones.py
@@ -20,6 +20,7 @@ import json
import nova.db
from nova import context
+from nova import crypto
from nova import flags
from nova import test
from nova.api.openstack import zones
@@ -79,6 +80,17 @@ def zone_capabilities(method, context):
return dict()
+GLOBAL_BUILD_PLAN = [
+ dict(name='host1', weight=10, ip='10.0.0.1', zone='zone1'),
+ dict(name='host2', weight=9, ip='10.0.0.2', zone='zone2'),
+ dict(name='host3', weight=8, ip='10.0.0.3', zone='zone3'),
+ dict(name='host4', weight=7, ip='10.0.0.4', zone='zone4'),
+ ]
+
+
+def zone_select(context, specs):
+ return GLOBAL_BUILD_PLAN
+
class ZonesTest(test.TestCase):
def setUp(self):
super(ZonesTest, self).setUp()
@@ -190,3 +202,31 @@ class ZonesTest(test.TestCase):
self.assertEqual(res_dict['zone']['name'], 'darksecret')
self.assertEqual(res_dict['zone']['cap1'], 'a;b')
self.assertEqual(res_dict['zone']['cap2'], 'c;d')
+
+ def test_zone_select(self):
+ FLAGS.build_plan_encryption_key = 'c286696d887c9aa0611bbb3e2025a45a'
+ self.stubs.Set(api, 'select', zone_select)
+
+ req = webob.Request.blank('/v1.0/zones/select')
+
+ res = req.get_response(fakes.wsgi_app())
+ res_dict = json.loads(res.body)
+ self.assertEqual(res.status_int, 200)
+
+ self.assertTrue('weights' in res_dict)
+
+ for item in res_dict['weights']:
+ blob = item['blob']
+ decrypt = crypto.decryptor(FLAGS.build_plan_encryption_key)
+ secret_item = json.loads(decrypt(blob))
+ found = False
+ for original_item in GLOBAL_BUILD_PLAN:
+ if original_item['name'] != secret_item['name']:
+ continue
+ found = True
+ for key in ('weight', 'ip', 'zone'):
+ self.assertEqual(secret_item[key], original_item[key])
+
+ self.assertTrue(found)
+ self.assertEqual(len(item), 2)
+ self.assertTrue('weight' in item)
diff --git a/nova/tests/test_crypto.py b/nova/tests/test_crypto.py
new file mode 100644
index 0000000000..945d787948
--- /dev/null
+++ b/nova/tests/test_crypto.py
@@ -0,0 +1,48 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# 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.
+"""
+Tests for Crypto module.
+"""
+
+from nova import crypto
+from nova import test
+
+
+class SymmetricKeyTestCase(test.TestCase):
+ """Test case for Encrypt/Decrypt"""
+ def test_encrypt_decrypt(self):
+ key = 'c286696d887c9aa0611bbb3e2025a45a'
+ plain_text = "The quick brown fox jumped over the lazy dog."
+
+ # No IV supplied (all 0's)
+ encrypt = crypto.encryptor(key)
+ cipher_text = encrypt(plain_text)
+ self.assertNotEquals(plain_text, cipher_text)
+
+ decrypt = crypto.decryptor(key)
+ plain = decrypt(cipher_text)
+
+ self.assertEquals(plain_text, plain)
+
+ # IV supplied ...
+ iv = '562e17996d093d28ddb3ba695a2e6f58'
+ encrypt = crypto.encryptor(key, iv)
+ cipher_text = encrypt(plain_text)
+ self.assertNotEquals(plain_text, cipher_text)
+
+ decrypt = crypto.decryptor(key, iv)
+ plain = decrypt(cipher_text)
+
+ self.assertEquals(plain_text, plain)