Metadata service refactoring

This refeactoring is required in order to support services
that provide metadata in a format which differs from the OpenStack
one.
This commit is contained in:
Alessandro Pilotti 2014-02-17 19:39:13 +02:00
parent 16798c76b6
commit 209592e2d6
16 changed files with 241 additions and 165 deletions

View File

@ -20,17 +20,18 @@ from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.utils import classloader
opts = [
cfg.ListOpt('metadata_services',
default=[
'cloudbaseinit.metadata.services.httpservice.HttpService',
'cloudbaseinit.metadata.services.configdrive.configdrive.'
'ConfigDriveService',
'cloudbaseinit.metadata.services.ec2service.EC2Service'
],
help='List of enabled metadata service classes, '
'to be tested fro availability in the provided order. '
'The first available service will be used to retrieve '
'metadata')
cfg.ListOpt(
'metadata_services',
default=[
'cloudbaseinit.metadata.services.httpservice.HttpService',
'cloudbaseinit.metadata.services.configdrive.configdrive.'
'ConfigDriveService',
'cloudbaseinit.metadata.services.ec2service.EC2Service'
],
help='List of enabled metadata service classes, '
'to be tested fro availability in the provided order. '
'The first available service will be used to retrieve '
'metadata')
]
CONF = cfg.CONF

View File

@ -15,8 +15,6 @@
# under the License.
import abc
import json
import posixpath
import time
from oslo.config import cfg
@ -53,10 +51,6 @@ class BaseMetadataService(object):
def load(self):
self._cache = {}
@property
def can_post_password(self):
return False
@abc.abstractmethod
def _get_data(self, path):
pass
@ -84,41 +78,37 @@ class BaseMetadataService(object):
self._cache[path] = data
return data
def get_content(self, data_type, name):
path = posixpath.normpath(
posixpath.join(data_type, 'content', name))
return self._get_cache_data(path)
def get_content(self, name):
pass
def get_user_data(self, data_type, version='latest'):
path = posixpath.normpath(
posixpath.join(data_type, version, 'user_data'))
return self._get_cache_data(path)
def get_user_data(self):
pass
def get_meta_data(self, data_type, version='latest'):
path = posixpath.normpath(
posixpath.join(data_type, version, 'meta_data.json'))
data = self._get_cache_data(path)
if type(data) is str:
return json.loads(self._get_cache_data(path))
else:
return data
def get_host_name(self):
pass
def _post_data(self, path, data):
raise NotExistingMetadataException()
def get_public_keys(self):
pass
def _get_password_path(self, version='latest'):
return posixpath.normpath(posixpath.join('openstack',
version,
'password'))
def get_network_config(self):
pass
def is_password_set(self, version='latest'):
path = self._get_password_path(version)
return len(self._get_data(path)) > 0
def get_admin_password(self):
pass
def post_password(self, enc_password_b64, version='latest'):
path = self._get_password_path(version)
action = lambda: self._post_data(path, enc_password_b64)
return self._exec_with_retry(action)
@property
def can_post_password(self):
return False
@property
def is_password_set(self):
return False
def post_password(self, enc_password_b64):
pass
def get_client_auth_certs(self):
pass
def cleanup(self):
pass

View File

@ -0,0 +1,113 @@
# Copyright 2014 Cloudbase Solutions Srl
#
# 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 json
import posixpath
import urllib2
import urlparse
from oslo.config import cfg
from cloudbaseinit.metadata.services import base
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.osutils import factory as osutils_factory
from cloudbaseinit.plugins.windows import x509
opts = [
cfg.StrOpt('metadata_base_url', default='http://169.254.169.254/',
help='The base URL where the service looks for metadata'),
]
CONF = cfg.CONF
CONF.register_opts(opts)
LOG = logging.getLogger(__name__)
class BaseOpenStackService(base.BaseMetadataService):
def get_content(self, name):
path = posixpath.normpath(
posixpath.join('openstack', 'content', name))
return self._get_cache_data(path)
def get_user_data(self):
path = posixpath.normpath(
posixpath.join('openstack', 'latest', 'user_data'))
return self._get_cache_data(path)
def _get_meta_data(self, version='latest'):
path = posixpath.normpath(
posixpath.join('openstack', version, 'meta_data.json'))
data = self._get_cache_data(path)
if type(data) is str:
return json.loads(self._get_cache_data(path))
else:
return data
def get_host_name(self):
return self._get_meta_data().get('hostname')
def get_public_keys(self):
public_keys = self._get_meta_data().get('public_keys')
if public_keys:
return public_keys.values()
def get_network_config(self):
return self._get_meta_data().get('network_config')
def get_admin_password(self):
meta_data = self._get_meta_data()
meta = meta_data.get('meta')
if meta and 'admin_pass' in meta:
password = meta['admin_pass']
elif 'admin_pass' in meta_data:
password = meta_data['admin_pass']
else:
password = None
return password
def get_client_auth_certs(self):
cert_data = None
meta_data = self._get_meta_data()
meta = meta_data.get('meta')
if meta:
i = 0
while True:
# Chunking is necessary as metadata items can be
# max. 255 chars long
cert_chunk = meta.get('admin_cert%d' % i)
if not cert_chunk:
break
if not cert_data:
cert_data = cert_chunk
else:
cert_data += cert_chunk
i += 1
if not cert_data:
# Look if the user_data contains a PEM certificate
try:
user_data = self.get_user_data()
if user_data.startswith(x509.PEM_HEADER):
cert_data = user_data
except base.NotExistingMetadataException:
LOG.debug("user_data metadata not present")
if cert_data:
return [cert_data]

View File

@ -22,6 +22,7 @@ import uuid
from oslo.config import cfg
from cloudbaseinit.metadata.services import base
from cloudbaseinit.metadata.services import baseopenstackservice
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.metadata.services.configdrive import manager
@ -38,7 +39,7 @@ CONF.register_opts(opts)
LOG = logging.getLogger(__name__)
class ConfigDriveService(base.BaseMetadataService):
class ConfigDriveService(baseopenstackservice.BaseOpenStackService):
def __init__(self):
super(ConfigDriveService, self).__init__()
self._metadata_path = None

View File

@ -21,6 +21,7 @@ import urlparse
from oslo.config import cfg
from cloudbaseinit.metadata.services import base
from cloudbaseinit.metadata.services import baseopenstackservice
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.osutils import factory as osutils_factory
@ -35,7 +36,9 @@ CONF.register_opts(opts)
LOG = logging.getLogger(__name__)
class HttpService(base.BaseMetadataService):
class HttpService(baseopenstackservice.BaseOpenStackService):
_POST_PASSWORD_MD_VER = '2013-04-04'
def __init__(self):
super(HttpService, self).__init__()
self._enable_retry = True
@ -72,17 +75,13 @@ class HttpService(base.BaseMetadataService):
self._check_metadata_ip_route()
try:
self.get_meta_data('openstack')
self._get_meta_data()
return True
except:
except Exception:
LOG.debug('Metadata not found at URL \'%s\'' %
CONF.metadata_base_url)
return False
@property
def can_post_password(self):
return True
def _get_response(self, req):
try:
return urllib2.urlopen(req)
@ -106,10 +105,27 @@ class HttpService(base.BaseMetadataService):
self._get_response(req)
return True
def post_password(self, enc_password_b64, version='latest'):
def _get_password_path(self):
return 'openstack/%s/password' % self._POST_PASSWORD_MD_VER
@property
def can_post_password(self):
try:
return super(HttpService, self).post_password(enc_password_b64,
version)
self._get_meta_data(self._POST_PASSWORD_MD_VER)
return True
except base.NotExistingMetadataException:
return False
@property
def is_password_set(self):
path = self._get_password_path()
return len(self._get_data(path)) > 0
def post_password(self, enc_password_b64):
try:
path = self._get_password_path()
action = lambda: self._post_data(path, enc_password_b64)
return self._exec_with_retry(action)
except urllib2.HTTPError as ex:
if ex.code == 409:
# Password already set

View File

@ -16,7 +16,8 @@
A temporary helper which emulates cloudbaseinit.messaging.Notifier.
This helper method allows us to do the tedious porting to the new Notifier API
as a standalone commit so that the commit which switches us to cloudbaseinit.messaging
as a standalone commit so that the commit which switches us to
cloudbaseinit.messaging
is smaller and easier to review. This file will be removed as part of that
commit.
"""

View File

@ -36,17 +36,16 @@ CONF.register_opts(opts)
class NetworkConfigPlugin(base.BasePlugin):
def execute(self, service, shared_data):
meta_data = service.get_meta_data('openstack')
if 'network_config' not in meta_data:
network_config = service.get_network_config()
if not network_config:
return (base.PLUGIN_EXECUTION_DONE, False)
network_config = meta_data['network_config']
if 'content_path' not in network_config:
return (base.PLUGIN_EXECUTION_DONE, False)
content_path = network_config['content_path']
content_name = content_path.rsplit('/', 1)[-1]
debian_network_conf = service.get_content('openstack', content_name)
debian_network_conf = service.get_content(content_name)
LOG.debug('network config content:\n%s' % debian_network_conf)

View File

@ -36,14 +36,14 @@ NETBIOS_HOST_NAME_MAX_LEN = 15
class SetHostNamePlugin(base.BasePlugin):
def execute(self, service, shared_data):
meta_data = service.get_meta_data('openstack')
if 'hostname' not in meta_data:
osutils = osutils_factory.OSUtilsFactory().get_os_utils()
metadata_host_name = service.get_host_name()
if not metadata_host_name:
LOG.debug('Hostname not found in metadata')
return (base.PLUGIN_EXECUTION_DONE, False)
osutils = osutils_factory.OSUtilsFactory().get_os_utils()
metadata_host_name = meta_data['hostname'].split('.', 1)[0]
metadata_host_name = metadata_host_name.split('.', 1)[0]
if (len(metadata_host_name) > NETBIOS_HOST_NAME_MAX_LEN and
CONF.netbios_host_name_compatibility):
@ -56,6 +56,7 @@ class SetHostNamePlugin(base.BasePlugin):
else:
new_host_name = metadata_host_name
LOG.info("Setting hostname: %s" % new_host_name)
reboot_required = osutils.set_host_name(new_host_name)
return (base.PLUGIN_EXECUTION_DONE, reboot_required)

View File

@ -18,7 +18,6 @@ import base64
from oslo.config import cfg
from cloudbaseinit.metadata.services import base as services_base
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.osutils import factory as osutils_factory
from cloudbaseinit.plugins import base
@ -38,8 +37,6 @@ LOG = logging.getLogger(__name__)
class SetUserPasswordPlugin(base.BasePlugin):
_post_password_md_ver = '2013-04-04'
def _encrypt_password(self, ssh_pub_key, password):
cm = crypt.CryptManager()
with cm.load_ssh_rsa_public_key(ssh_pub_key) as rsa:
@ -47,29 +44,15 @@ class SetUserPasswordPlugin(base.BasePlugin):
return base64.b64encode(enc_password)
def _get_ssh_public_key(self, service):
meta_data = service.get_meta_data('openstack',
self._post_password_md_ver)
if not 'public_keys' in meta_data:
return False
public_keys = meta_data['public_keys']
ssh_pub_key = None
for k in public_keys:
# Get the first key
ssh_pub_key = public_keys[k]
break
return ssh_pub_key
public_keys = service.get_public_keys()
if public_keys:
return public_keys[0]
def _get_password(self, service, osutils):
meta_data = service.get_meta_data('openstack')
meta = meta_data.get('meta')
password = None
if CONF.inject_user_password:
if meta and 'admin_pass' in meta:
password = meta['admin_pass']
elif 'admin_pass' in meta_data:
password = meta_data['admin_pass']
password = service.get_admin_password()
else:
password = None
if password:
LOG.warn('Using admin_pass metadata user password. Consider '
@ -86,21 +69,13 @@ class SetUserPasswordPlugin(base.BasePlugin):
return password
def _set_metadata_password(self, password, service):
try:
ssh_pub_key = self._get_ssh_public_key(service)
if ssh_pub_key:
enc_password_b64 = self._encrypt_password(ssh_pub_key,
password)
return service.post_password(enc_password_b64,
self._post_password_md_ver)
else:
LOG.info('No SSH public key available for password encryption')
return True
except services_base.NotExistingMetadataException:
# Requested version not available or password feature
# not implemented
LOG.info('Cannot set the password in the metadata as it is not '
'supported by this metadata version')
ssh_pub_key = self._get_ssh_public_key(service)
if ssh_pub_key:
enc_password_b64 = self._encrypt_password(ssh_pub_key,
password)
return service.post_password(enc_password_b64)
else:
LOG.info('No SSH public key available for password encryption')
return True
def _set_password(self, service, osutils, user_name):
@ -115,8 +90,7 @@ class SetUserPasswordPlugin(base.BasePlugin):
user_name = shared_data.get(constants.SHARED_DATA_USERNAME,
CONF.username)
if (service.can_post_password and
service.is_password_set(self._post_password_md_ver)):
if service.can_post_password and service.is_password_set:
LOG.debug('User\'s password already set in the instance metadata')
else:
osutils = osutils_factory.OSUtilsFactory().get_os_utils()

View File

@ -28,8 +28,9 @@ LOG = logging.getLogger(__name__)
class SetUserSSHPublicKeysPlugin(base.BasePlugin):
def execute(self, service, shared_data):
meta_data = service.get_meta_data('openstack')
if not 'public_keys' in meta_data:
public_keys = service.get_public_keys()
if not public_keys:
LOG.debug('Public keys not found in metadata')
return (base.PLUGIN_EXECUTION_DONE, False)
username = CONF.username
@ -47,9 +48,9 @@ class SetUserSSHPublicKeysPlugin(base.BasePlugin):
os.makedirs(user_ssh_dir)
authorized_keys_path = os.path.join(user_ssh_dir, "authorized_keys")
LOG.info("Writing SSH public keys in: %s" % authorized_keys_path)
with open(authorized_keys_path, 'w') as f:
public_keys = meta_data['public_keys']
for k in public_keys:
f.write(public_keys[k])
for public_key in public_keys:
f.write(public_key)
return (base.PLUGIN_EXECUTION_DONE, False)

View File

@ -30,7 +30,7 @@ class UserDataPlugin(base.BasePlugin):
def execute(self, service, shared_data):
try:
user_data = service.get_user_data('openstack')
user_data = service.get_user_data()
except metadata_services_base.NotExistingMetadataException:
return (base.PLUGIN_EXECUTION_DONE, False)
@ -127,7 +127,7 @@ class UserDataPlugin(base.BasePlugin):
def _end_part_process_event(self, handler_func):
LOG.debug("Calling part handler \"__end__\" event")
handler_func(None, "__end__", None, None)
handler_func(None, "__end__", None, None)
def _get_plugin_return_value(self, ret_val):
plugin_status = base.PLUGIN_EXECUTION_DONE

View File

@ -265,7 +265,7 @@ class IVdsDisk(comtypes.IUnknown):
comtypes.COMMETHOD([], comtypes.HRESULT, 'QueryExtents',
(['out'], ctypes.POINTER(ctypes.POINTER(
VDS_DISK_EXTENT)),
'ppExtentArray'),
'ppExtentArray'),
(['out'], ctypes.POINTER(wintypes.LONG),
'plNumberOfExtents')),
]

View File

@ -14,7 +14,6 @@
# License for the specific language governing permissions and limitations
# under the License.
from cloudbaseinit.metadata.services import base as metadata_services_base
from cloudbaseinit.openstack.common import log as logging
from cloudbaseinit.plugins import base
from cloudbaseinit.plugins import constants
@ -25,37 +24,6 @@ LOG = logging.getLogger(__name__)
class ConfigWinRMCertificateAuthPlugin(base.BasePlugin):
def _get_client_auth_cert(self, service):
cert_data = None
meta_data = service.get_meta_data('openstack')
meta = meta_data.get('meta')
if meta:
i = 0
while True:
# Chunking is necessary as metadata items can be
# max. 255 chars long
cert_chunk = meta.get('admin_cert%d' % i)
if not cert_chunk:
break
if not cert_data:
cert_data = cert_chunk
else:
cert_data += cert_chunk
i += 1
if not cert_data:
# Look if the user_data contains a PEM certificate
try:
user_data = service.get_user_data('openstack')
if user_data.startswith(x509.PEM_HEADER):
cert_data = user_data
except metadata_services_base.NotExistingMetadataException:
LOG.debug("user_data metadata not present")
return cert_data
def _get_credentials(self, shared_data):
user_name = shared_data.get(constants.SHARED_DATA_USERNAME)
if not user_name:
@ -76,28 +44,34 @@ class ConfigWinRMCertificateAuthPlugin(base.BasePlugin):
def execute(self, service, shared_data):
user_name, password = self._get_credentials(shared_data)
cert_data = self._get_client_auth_cert(service)
if not cert_data:
certs_data = service.get_client_auth_certs()
if not certs_data:
LOG.info("WinRM certificate authentication cannot be configured "
"as a certificate has not been provided in the metadata")
return (base.PLUGIN_EXECUTION_DONE, False)
cert_manager = x509.CryptoAPICertManager()
cert_thumprint, cert_upn = cert_manager.import_cert(
cert_data, store_name=x509.STORE_NAME_ROOT)
if not cert_upn:
LOG.error("WinRM certificate authentication cannot be configured "
"as the provided certificate lacks a subject alt name "
"containing an UPN (OID 1.3.6.1.4.1.311.20.2.3)")
return (base.PLUGIN_EXECUTION_DONE, False)
winrm_config = winrmconfig.WinRMConfig()
winrm_config.set_auth_config(certificate=True)
if winrm_config.get_cert_mapping(cert_thumprint, cert_upn):
winrm_config.delete_cert_mapping(cert_thumprint, cert_upn)
winrm_config.create_cert_mapping(cert_thumprint, cert_upn,
user_name, password)
for cert_data in certs_data:
cert_manager = x509.CryptoAPICertManager()
cert_thumprint, cert_upn = cert_manager.import_cert(
cert_data, store_name=x509.STORE_NAME_ROOT)
if not cert_upn:
LOG.error("WinRM certificate authentication cannot be "
"configured as the provided certificate lacks a "
"subject alt name containing an UPN (OID "
"1.3.6.1.4.1.311.20.2.3)")
continue
if winrm_config.get_cert_mapping(cert_thumprint, cert_upn):
winrm_config.delete_cert_mapping(cert_thumprint, cert_upn)
LOG.info("Creating WinRM certificate mapping for user "
"%(user_name)s with UPN %(cert_upn)s",
{'user_name': user_name, 'cert_upn': cert_upn})
winrm_config.create_cert_mapping(cert_thumprint, cert_upn,
user_name, password)
return (base.PLUGIN_EXECUTION_DONE, False)

View File

@ -37,6 +37,7 @@ STORE_NAME_TRUSTED_PEOPLE = "TrustedPeople"
PEM_HEADER = "-----BEGIN CERTIFICATE-----"
PEM_FOOTER = "-----END CERTIFICATE-----"
class CryptoAPICertManager(object):
def _get_cert_thumprint(self, cert_context_p):
thumbprint = None

View File

@ -29,3 +29,7 @@ def main():
logging.setup('cloudbaseinit')
init.InitManager().configure_host()
if __name__ == "__main__":
main()

View File

@ -192,7 +192,7 @@ class CryptoAPICertManagerTests(unittest.TestCase):
mock_CertOpenStore.return_value = store_handle
mock_CertAddCertificateContextToStore.return_value = context_to_store
if (certstr is None or certificate is None or enhanced_key is None
or store_handle is None or context_to_store is None):
or store_handle is None or context_to_store is None):
self.assertRaises(cryptoapi.CryptoAPIException,
self._x509.create_self_signed_cert,
'fake subject', 10, True, x509.STORE_NAME_MY)
@ -333,7 +333,7 @@ class CryptoAPICertManagerTests(unittest.TestCase):
0, None, mock_create_unicode_buffer(), 2)]
if (not crypttstr or store_handle is None or add_enc_cert is None or
upn_len != 2):
upn_len != 2):
self.assertRaises(cryptoapi.CryptoAPIException,
self._x509.import_cert, fake_cert_data, True,
x509.STORE_NAME_MY)