
The current patch proposes the addition of a new data source strategy for filtering the strategies by a given list of versions. Each data source exports a new API, called ``version()``, which should return the underlying version of the said data source. This implies that each data source module can export multiple versioned data sources and it's the duty of an external strategy to select the appropiate data source with regard to a particular set of versions. Change-Id: I6ef2fdd6520a53fc600232247f23bd22b848461e
218 lines
6.8 KiB
Python
218 lines
6.8 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`.
|
|
"""
|
|
|
|
@abc.abstractmethod
|
|
def version(self):
|
|
"""Get the version of the current data source."""
|
|
|
|
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
|