diff options
-rw-r--r-- | Authors | 1 | ||||
-rw-r--r-- | nova/scheduler/filters/trusted_filter.py | 207 | ||||
-rw-r--r-- | nova/tests/scheduler/test_host_filters.py | 81 |
3 files changed, 289 insertions, 0 deletions
@@ -55,6 +55,7 @@ Devdeep Singh <devdeep.singh@citrix.com> Devendra Modium <dmodium@isi.edu> Devin Carlen <devin.carlen@gmail.com> Dina Belova <dbelova@mirantis.com> +Don Dugger <donald.d.dugger@intel.com> Donal Lafferty <donal.lafferty@citrix.com> Dong-In David Kang <dkang@isi.edu> Doug Hellmann <doug.hellmann@dreamhost.com> diff --git a/nova/scheduler/filters/trusted_filter.py b/nova/scheduler/filters/trusted_filter.py new file mode 100644 index 000000000..e081daf37 --- /dev/null +++ b/nova/scheduler/filters/trusted_filter.py @@ -0,0 +1,207 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright (c) 2012 Intel, Inc. +# Copyright (c) 2011-2012 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. + +""" +Filter to add support for Trusted Computing Pools. + +Filter that only schedules tasks on a host if the integrity (trust) +of that host matches the trust requested in the `extra_specs' for the +flavor. The `extra_specs' will contain a key/value pair where the +key is `trust'. The value of this pair (`trusted'/`untrusted') must +match the integrity of that host (obtained from the Attestation +service) before the task can be scheduled on that host. + +Note that the parameters to control access to the Attestation Service +are in the `nova.conf' file in a separate `trust' section. For example, +the config file will look something like: + + [DEFAULT] + verbose=True + ... + [trust] + server=attester.mynetwork.com + +Details on the specific parameters can be found in the file `trust_attest.py'. + +Details on setting up and using an Attestation Service can be found at +the Open Attestation project at: + + https://github.com/OpenAttestation/OpenAttestation +""" + +import httplib +import json +import socket +import ssl + +from nova import flags +from nova import log as logging +from nova.openstack.common import cfg +from nova.scheduler import filters +from nova import utils + + +LOG = logging.getLogger(__name__) + +trusted_opts = [ + cfg.StrOpt('server', + default=None, + help='attestation server http'), + cfg.StrOpt('server_ca_file', + default=None, + help='attestation server Cert file for Identity verification'), + cfg.StrOpt('port', + default='8443', + help='attestation server port'), + cfg.StrOpt('api_url', + default='/OpenAttestationWebServices/V1.0', + help='attestation web API URL'), + cfg.StrOpt('auth_blob', + default=None, + help='attestation authorization blob - must change'), +] + +FLAGS = flags.FLAGS +trust_group = cfg.OptGroup(name='trusted_computing', title='Trust parameters') +FLAGS.register_group(trust_group) +FLAGS.register_opts(trusted_opts, group='trusted_computing') + + +class HTTPSClientAuthConnection(httplib.HTTPSConnection): + """ + Class to make a HTTPS connection, with support for full client-based + SSL Authentication + """ + + def __init__(self, host, port, key_file, cert_file, ca_file, timeout=None): + httplib.HTTPSConnection.__init__(self, host, + key_file=key_file, + cert_file=cert_file) + self.host = host + self.port = port + self.key_file = key_file + self.cert_file = cert_file + self.ca_file = ca_file + self.timeout = timeout + + def connect(self): + """ + Connect to a host on a given (SSL) port. + If ca_file is pointing somewhere, use it to check Server Certificate. + + Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). + This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to + ssl.wrap_socket(), which forces SSL to check server certificate + against our client certificate. + """ + sock = socket.create_connection((self.host, self.port), self.timeout) + self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file, + ca_certs=self.ca_file, + cert_reqs=ssl.CERT_REQUIRED) + + +class AttestationService(httplib.HTTPSConnection): + # Provide access wrapper to attestation server to get integrity report. + + def __init__(self): + self.api_url = FLAGS.trusted_computing.api_url + self.host = FLAGS.trusted_computing.server + self.port = FLAGS.trusted_computing.port + self.auth_blob = FLAGS.trusted_computing.auth_blob + self.key_file = None + self.cert_file = None + self.ca_file = FLAGS.trusted_computing.server_ca_file + self.request_count = 100 + + def _do_request(self, method, action_url, body, headers): + # Connects to the server and issues a request. + # :returns: result data + # :raises: IOError if the request fails + + action_url = "%s/%s" % (self.api_url, action_url) + try: + c = HTTPSClientAuthConnection(self.host, self.port, + key_file=self.key_file, + cert_file=self.cert_file, + ca_file=self.ca_file) + c.request(method, action_url, body, headers) + res = c.getresponse() + status_code = res.status + if status_code in (httplib.OK, + httplib.CREATED, + httplib.ACCEPTED, + httplib.NO_CONTENT): + return httplib.OK, res + return status_code, None + + except (socket.error, IOError) as e: + return IOError, None + + def _request(self, cmd, subcmd, host): + body = {} + body['count'] = 1 + body['hosts'] = host + cooked = json.dumps(body) + headers = {} + headers['content-type'] = 'application/json' + headers['Accept'] = 'application/json' + if self.auth_blob: + headers['x-auth-blob'] = self.auth_blob + status, res = self._do_request(cmd, subcmd, cooked, headers) + if status == httplib.OK: + data = res.read() + return status, json.loads(data) + else: + return status, None + + def _check_trust(self, data, host): + for item in data: + for state in item['hosts']: + if state['host_name'] == host: + return state['trust_lvl'] + return "" + + def do_attestation(self, host): + state = [] + status, data = self._request("POST", "PollHosts", host) + if status != httplib.OK: + return {} + state.append(data) + return self._check_trust(state, host) + + +class TrustedFilter(filters.BaseHostFilter): + """Trusted filter to support Trusted Compute Pools.""" + + def __init__(self): + self.attestation_service = AttestationService() + + def _is_trusted(self, host, trust): + level = self.attestation_service.do_attestation(host) + LOG.debug(_("TCP: trust state of " + "%(host)s:%(level)s(%(trust)s)") % locals()) + return trust == level + + def host_passes(self, host_state, filter_properties): + instance = filter_properties.get('instance_type', {}) + extra = instance.get('extra_specs', {}) + trust = extra.get('trusted_host') + host = host_state.host + if trust: + return self._is_trusted(host, trust) + return True diff --git a/nova/tests/scheduler/test_host_filters.py b/nova/tests/scheduler/test_host_filters.py index 22a162aa2..b7a5402c8 100644 --- a/nova/tests/scheduler/test_host_filters.py +++ b/nova/tests/scheduler/test_host_filters.py @@ -15,17 +15,45 @@ Tests For Scheduler Host Filters. """ +import httplib import json +import stubout from nova import context from nova import exception from nova import flags from nova.scheduler import filters +from nova.scheduler.filters.trusted_filter import AttestationService from nova import test from nova.tests.scheduler import fakes from nova import utils +DATA = '' + + +def stub_out_https_backend(stubs): + """ + Stubs out the httplib.HTTPRequest.getresponse to return + faked-out data instead of grabbing actual contents of a resource + + The stubbed getresponse() returns an iterator over + the data "I am a teapot, short and stout\n" + + :param stubs: Set of stubout stubs + """ + + class FakeHTTPResponse(object): + + def read(self): + return DATA + + def fake_do_request(self, *args, **kwargs): + return httplib.OK, FakeHTTPResponse() + + stubs.Set(AttestationService, '_do_request', fake_do_request) + + class TestFilter(filters.BaseHostFilter): pass @@ -40,6 +68,8 @@ class HostFiltersTestCase(test.TestCase): def setUp(self): super(HostFiltersTestCase, self).setUp() + self.stubs = stubout.StubOutForTesting() + stub_out_https_backend(self.stubs) self.context = context.RequestContext('fake', 'fake') self.json_query = json.dumps( ['and', ['>=', '$free_ram_mb', 1024], @@ -586,6 +616,57 @@ class HostFiltersTestCase(test.TestCase): filter_properties = {'scheduler_hints': {'query': json.dumps(raw)}} self.assertTrue(filt_cls.host_passes(host, filter_properties)) + def test_trusted_filter_default_passes(self): + self._stub_service_is_up(True) + filt_cls = self.class_map['TrustedFilter']() + filter_properties = {'instance_type': {'memory_mb': 1024}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_trusted_filter_trusted_and_trusted_passes(self): + global DATA + DATA = '{"hosts":[{"host_name":"host1","trust_lvl":"trusted"}]}' + self._stub_service_is_up(True) + filt_cls = self.class_map['TrustedFilter']() + extra_specs = {'trusted_host': 'trusted'} + filter_properties = {'instance_type': {'memory_mb': 1024, + 'extra_specs': extra_specs}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + + def test_trusted_filter_trusted_and_untrusted_fails(self): + global DATA + DATA = '{"hosts":[{"host_name":"host1","trust_lvl":"untrusted"}]}' + self._stub_service_is_up(True) + filt_cls = self.class_map['TrustedFilter']() + extra_specs = {'trusted_host': 'trusted'} + filter_properties = {'instance_type': {'memory_mb': 1024, + 'extra_specs': extra_specs}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertFalse(filt_cls.host_passes(host, filter_properties)) + + def test_trusted_filter_untrusted_and_trusted_fails(self): + global DATA + DATA = '{"hosts":[{"host_name":"host1","trust_lvl":"trusted"}]}' + self._stub_service_is_up(True) + filt_cls = self.class_map['TrustedFilter']() + extra_specs = {'trusted_host': 'untrusted'} + filter_properties = {'instance_type': {'memory_mb': 1024, + 'extra_specs': extra_specs}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertFalse(filt_cls.host_passes(host, filter_properties)) + + def test_trusted_filter_untrusted_and_untrusted_passes(self): + global DATA + DATA = '{"hosts":[{"host_name":"host1","trust_lvl":"untrusted"}]}' + self._stub_service_is_up(True) + filt_cls = self.class_map['TrustedFilter']() + extra_specs = {'trusted_host': 'untrusted'} + filter_properties = {'instance_type': {'memory_mb': 1024, + 'extra_specs': extra_specs}} + host = fakes.FakeHostState('host1', 'compute', {}) + self.assertTrue(filt_cls.host_passes(host, filter_properties)) + def test_core_filter_passes(self): filt_cls = self.class_map['CoreFilter']() filter_properties = {'instance_type': {'vcpus': 1}} |