
The patch brings a couple of new concepts into cloudinit. We have a general miniframework for defining plugins and discovering them, with an actual implementation which uses the *pkgutil* builtin module. Built atop of this framework, we have a new data source loader, found in cloudinit.bases.DataSourceLoader. The loader operates on three concepts: - the *module iterator*, which is used to list modules from a specific location (in our case, from cloudinit.sources.__path__). The data source loader takes care to use only the modules that exports a given API. - the data source discovery API assumes that each data source module exports a function called `data_sources`, which should return a tuple of data source classes that the said module exports. The loader filters the modules that provides this API. - the data source loader uses a new concept called *search strategy* for discovering a potential data source. The search strategies are classes whose purpose is to select one or more data sources from a data source stream (any iterable). There are multiple ways to implement a strategy, the search can be either serial or parallel, there's no additional requirement as long as they return an iterable. Also, the strategies can be stacked together, for instance, having two strategies, one for selecting only the network data sources and another for selecting the available data sources from a list of potential data sources. This patch also adds a new API that uses the DataSourceLoader with a given module iterator and strategies for selecting one data source that cloudinit can use. Change-Id: I30f312191ce40e45445ed9e3fc8a3d4651903280
214 lines
6.7 KiB
Python
214 lines
6.7 KiB
Python
# Copyright 2015 Canonical Ltd.
|
|
# This file is part of cloud-init. See LICENCE file for license information.
|
|
#
|
|
# vi: ts=4 expandtab
|
|
|
|
import abc
|
|
import itertools
|
|
|
|
import six
|
|
|
|
from cloudinit import exceptions
|
|
from cloudinit import logging
|
|
from cloudinit import sources
|
|
from cloudinit.sources import strategy
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class APIResponse(object):
|
|
"""Holds API response content
|
|
|
|
To access the content in the binary format, use the
|
|
`buffer` attribute, while the unicode content can be
|
|
accessed by calling `str` over this (or by accessing
|
|
the `decoded_buffer` property).
|
|
"""
|
|
|
|
def __init__(self, buffer, encoding="utf-8"):
|
|
self.buffer = buffer
|
|
self.encoding = encoding
|
|
self._decoded_buffer = None
|
|
|
|
@property
|
|
def decoded_buffer(self):
|
|
# Avoid computing this again and again (although multiple threads
|
|
# may decode it if they all get in here at the same time, but meh
|
|
# thats ok).
|
|
if self._decoded_buffer is None:
|
|
self._decoded_buffer = self.buffer.decode(self.encoding)
|
|
return self._decoded_buffer
|
|
|
|
def __str__(self):
|
|
return self.decoded_buffer
|
|
|
|
|
|
class DataSourceLoader(object):
|
|
"""Class for retrieving an available data source instance
|
|
|
|
:param names:
|
|
A list of possible data source names, from which the loader
|
|
should pick. This can be used to filter the data sources
|
|
that can be found from outside of cloudinit control.
|
|
|
|
:param module_iterator:
|
|
An instance of :class:`cloudinit.plugin_finder.BaseModuleIterator`,
|
|
which is used to find possible modules where the data sources
|
|
can be found.
|
|
|
|
:param strategies:
|
|
An iterator of search strategy classes, where each strategy is capable
|
|
of filtering the data sources that can be used by cloudinit.
|
|
Possible strategies includes serial data source search or
|
|
parallel data source or filtering data sources according to
|
|
some criteria (only network data sources)
|
|
|
|
"""
|
|
|
|
def __init__(self, names, module_iterator, strategies):
|
|
self._names = names
|
|
self._module_iterator = module_iterator
|
|
self._strategies = strategies
|
|
|
|
@staticmethod
|
|
def _implements_source_api(module):
|
|
"""Check if the given module implements the data source API."""
|
|
return hasattr(module, 'data_sources')
|
|
|
|
def _valid_modules(self):
|
|
"""Return all the modules that are *valid*
|
|
|
|
Valid modules are those that implements a particular API
|
|
for declaring the data sources it exports.
|
|
"""
|
|
modules = self._module_iterator.list_modules()
|
|
return filter(self._implements_source_api, modules)
|
|
|
|
def all_data_sources(self):
|
|
"""Get all the data source classes that this finder knows about."""
|
|
return itertools.chain.from_iterable(
|
|
module.data_sources()
|
|
for module in self._valid_modules())
|
|
|
|
def valid_data_sources(self):
|
|
"""Get the data sources that are valid for this run."""
|
|
data_sources = self.all_data_sources()
|
|
# Instantiate them before passing to the strategies.
|
|
data_sources = (data_source() for data_source in data_sources)
|
|
|
|
for strategy_instance in self._strategies:
|
|
data_sources = strategy_instance.search_data_sources(data_sources)
|
|
return data_sources
|
|
|
|
|
|
@six.add_metaclass(abc.ABCMeta)
|
|
class BaseDataSource(object):
|
|
"""Base class for the data sources."""
|
|
|
|
datasource_config = {}
|
|
|
|
def __init__(self, config=None):
|
|
self._cache = {}
|
|
# TODO(cpopa): merge them instead.
|
|
self._config = config or self.datasource_config
|
|
|
|
def _get_cache_data(self, path):
|
|
"""Do a metadata lookup for the given *path*
|
|
|
|
This will return the available metadata under *path*,
|
|
while caching the result, so that a next call will not do
|
|
an additional API call.
|
|
"""
|
|
if path not in self._cache:
|
|
self._cache[path] = self._get_data(path)
|
|
|
|
return self._cache[path]
|
|
|
|
@abc.abstractmethod
|
|
def load(self):
|
|
"""Try to load this metadata service.
|
|
|
|
This should return ``True`` if the service was loaded properly,
|
|
``False`` otherwise.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def _get_data(self, path):
|
|
"""Retrieve the metadata exported under the `path` key.
|
|
|
|
This should return an instance of :class:`APIResponse`.
|
|
"""
|
|
|
|
def instance_id(self):
|
|
"""Get this instance's id."""
|
|
|
|
def user_data(self):
|
|
"""Get the user data available for this instance."""
|
|
|
|
def vendor_data(self):
|
|
"""Get the vendor data available for this instance."""
|
|
|
|
def host_name(self):
|
|
"""Get the hostname available for this instance."""
|
|
|
|
def public_keys(self):
|
|
"""Get the public keys available for this instance."""
|
|
|
|
def network_config(self):
|
|
"""Get the specified network config, if any."""
|
|
|
|
def admin_password(self):
|
|
"""Get the admin password."""
|
|
|
|
def post_password(self, password):
|
|
"""Post the password to the metadata service."""
|
|
|
|
def can_update_password(self):
|
|
"""Check if this data source can update the admin password."""
|
|
|
|
def is_password_changed(self):
|
|
"""Check if the data source has a new password for this instance."""
|
|
return False
|
|
|
|
def is_password_set(self):
|
|
"""Check if the password was already posted to the metadata service."""
|
|
|
|
|
|
def get_data_source(names, module_iterator, strategies=None):
|
|
"""Get an instance of any data source available.
|
|
|
|
:param names:
|
|
A list of possible data source names, from which the loader
|
|
should pick. This can be used to filter the data sources
|
|
that can be found from outside of cloudinit control.
|
|
|
|
:param module_iterator:
|
|
A subclass of :class:`cloudinit.plugin_finder.BaseModuleIterator`,
|
|
which is used to find possible modules where the data sources
|
|
can be found.
|
|
|
|
:param strategies:
|
|
An iterator of search strategy classes, where each strategy is capable
|
|
of filtering the data sources that can be used by cloudinit.
|
|
"""
|
|
if names:
|
|
default_strategies = [strategy.FilterNameStrategy(names)]
|
|
else:
|
|
default_strategies = []
|
|
if strategies is None:
|
|
strategies = []
|
|
|
|
strategy_instances = [strategy_cls() for strategy_cls in strategies]
|
|
strategies = default_strategies + strategy_instances
|
|
|
|
iterator = module_iterator(sources.__path__)
|
|
loader = DataSourceLoader(names, iterator, strategies)
|
|
valid_sources = loader.valid_data_sources()
|
|
|
|
data_source = next(valid_sources, None)
|
|
if not data_source:
|
|
raise exceptions.CloudInitError('No available data source found')
|
|
|
|
return data_source
|