32ad8a96b0
- uplifted/downgraded some python modules - fixed falcon.API deprecation - -> falcon.App - uplifted deckhand reference for python deps - fixed formatting style using yapf linter - added bindep role and bindep.txt file with required deps - fixed quai docker image publishing - re-enabled openstack-tox-py38 gate job Change-Id: I0e248182efad75630721a1291bc86a5edc79c22a
291 lines
9.7 KiB
Python
291 lines
9.7 KiB
Python
from . import exceptions, logging, validation
|
|
from . import design_ref as dr
|
|
import jinja2
|
|
import jsonpath_ng
|
|
import yaml
|
|
|
|
from deckhand.engine import layering
|
|
from deckhand import errors as dh_errors
|
|
|
|
__all__ = ['Configuration']
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class Configuration:
|
|
|
|
def __init__(self,
|
|
*,
|
|
documents,
|
|
debug=False,
|
|
substitute=True,
|
|
allow_missing_substitutions=True,
|
|
leave_kubectl=False,
|
|
validate=True):
|
|
LOG.info("Parsing document schemas.")
|
|
LOG.info("Building config from %d documents." % len(documents))
|
|
if substitute:
|
|
LOG.info("Rendering documents via Deckhand engine.")
|
|
try:
|
|
deckhand_eng = layering.DocumentLayering(
|
|
documents,
|
|
fail_on_missing_sub_src=not allow_missing_substitutions)
|
|
documents = [dict(d) for d in deckhand_eng.render()]
|
|
except dh_errors.DeckhandException as e:
|
|
LOG.exception(
|
|
'An unknown Deckhand exception occurred while trying'
|
|
' to render documents.')
|
|
raise exceptions.DeckhandException(str(e))
|
|
|
|
LOG.info("Deckhand engine returned %d documents." % len(documents))
|
|
self.debug = debug
|
|
self.documents = documents
|
|
self.leave_kubectl = leave_kubectl
|
|
|
|
if validate:
|
|
validation.validate_all(self)
|
|
|
|
@classmethod
|
|
def from_streams(cls, *, streams, **kwargs):
|
|
documents = []
|
|
for stream in streams:
|
|
stream_name = getattr(stream, 'name')
|
|
if stream_name is not None:
|
|
LOG.info('Loading documents from %s', stream_name)
|
|
stream_documents = list(yaml.safe_load_all(stream))
|
|
if stream_name is not None:
|
|
LOG.info('Successfully loaded %d documents from %s',
|
|
len(stream_documents), stream_name)
|
|
documents.extend(stream_documents)
|
|
|
|
return cls(documents=documents, **kwargs)
|
|
|
|
@classmethod
|
|
def from_design_ref(cls, design_ref, ctx=None, **kwargs):
|
|
documents, use_dh_engine = dr.get_documents(design_ref, ctx)
|
|
|
|
return cls(documents=documents,
|
|
substitute=use_dh_engine,
|
|
validate=use_dh_engine,
|
|
**kwargs)
|
|
|
|
def __getitem__(self, path):
|
|
return self.get_path(
|
|
path, jinja2.StrictUndefined('No match found for path %s' % path))
|
|
|
|
def get_first(self, *paths, default=None):
|
|
result = self._get_first(*paths)
|
|
if result:
|
|
return result
|
|
else:
|
|
if default is not None:
|
|
return default
|
|
else:
|
|
return jinja2.StrictUndefined(
|
|
'Nothing found matching paths: %s' % ','.join(paths))
|
|
|
|
def get(self, *, kind=None, name=None, schema=None, default=None):
|
|
result = _get(self.documents, kind=kind, schema=schema, name=name)
|
|
|
|
if result:
|
|
return result['data']
|
|
else:
|
|
if default is not None:
|
|
return default
|
|
else:
|
|
return jinja2.StrictUndefined(
|
|
'No document found matching kind=%s schema=%s name=%s' %
|
|
(kind, schema, name))
|
|
|
|
def iterate(self, *, kind=None, schema=None, labels=None, name=None):
|
|
if kind is not None:
|
|
if schema is not None:
|
|
raise AssertionError(
|
|
'Logic error: specified both kind and schema')
|
|
schema = 'promenade/%s/v1' % kind
|
|
|
|
for document in self.documents:
|
|
if _matches_filter(document,
|
|
schema=schema,
|
|
labels=labels,
|
|
name=name):
|
|
yield document
|
|
|
|
def find(self, *args, **kwargs):
|
|
for doc in self.iterate(*args, **kwargs):
|
|
return doc
|
|
|
|
def extract_genesis_config(self):
|
|
LOG.debug('Extracting genesis config.')
|
|
documents = []
|
|
for document in self.documents:
|
|
if document['schema'] != 'promenade/KubernetesNode/v1':
|
|
documents.append(document)
|
|
else:
|
|
LOG.debug('Excluding schema=%s metadata.name=%s',
|
|
document['schema'], _mg(document, 'name'))
|
|
return Configuration(debug=self.debug,
|
|
documents=documents,
|
|
leave_kubectl=self.leave_kubectl,
|
|
substitute=False,
|
|
validate=False)
|
|
|
|
def extract_node_config(self, name):
|
|
LOG.debug('Extracting node config for %s.', name)
|
|
documents = []
|
|
for document in self.documents:
|
|
schema = document['schema']
|
|
if schema == 'promenade/Genesis/v1':
|
|
LOG.debug('Excluding schema=%s metadata.name=%s', schema,
|
|
_mg(document, 'name'))
|
|
continue
|
|
elif schema == 'promenade/KubernetesNode/v1' and _mg(
|
|
document, 'name') != name:
|
|
LOG.debug('Excluding schema=%s metadata.name=%s', schema,
|
|
_mg(document, 'name'))
|
|
continue
|
|
else:
|
|
documents.append(document)
|
|
return Configuration(debug=self.debug,
|
|
documents=documents,
|
|
leave_kubectl=self.leave_kubectl,
|
|
substitute=False,
|
|
validate=False)
|
|
|
|
@property
|
|
def kubelet_name(self):
|
|
for document in self.iterate(kind='Genesis'):
|
|
return 'genesis'
|
|
|
|
for document in self.iterate(kind='KubernetesNode'):
|
|
return document['data']['hostname']
|
|
|
|
return jinja2.StrictUndefined(
|
|
'No Genesis or KubernetesNode found while getting kubelet name')
|
|
|
|
def _get_first(self, *paths):
|
|
for path in paths:
|
|
value = self.get_path(path)
|
|
if value:
|
|
return value
|
|
|
|
@property
|
|
def enable_units(self):
|
|
""" Get systemd unit names where enable is ``true``."""
|
|
return self.get_units_by_action('enable')
|
|
|
|
@property
|
|
def start_units(self):
|
|
""" Get systemd unit names where start is ``true``."""
|
|
return self.get_units_by_action('start')
|
|
|
|
@property
|
|
def stop_units(self):
|
|
""" Get systemd unit names where stop is ``true``."""
|
|
return self.get_units_by_action('stop')
|
|
|
|
@property
|
|
def disable_units(self):
|
|
""" Get systemd unit names where disable is ``true``."""
|
|
return self.get_units_by_action('disable')
|
|
|
|
def get_units_by_action(self, action):
|
|
""" Select systemd unit names by ``action``
|
|
|
|
Get all units that are ``true`` for ``action``.
|
|
"""
|
|
return [
|
|
k for k, v in self.systemd_units.items() if v.get(action, False)
|
|
]
|
|
|
|
@property
|
|
def systemd_units(self):
|
|
""" Return a dictionary of systemd units to be managed during join.
|
|
|
|
The dictionary key is the systemd unit name, each will have a four
|
|
boolean keys: ``enable``, ``disable``, ``start``, ``stop`` on the
|
|
actions to be taken at the end of genesis/node join. The steps
|
|
are ordered: enable, start, stop, disable.
|
|
"""
|
|
all_units = {}
|
|
|
|
for document in self.iterate(kind='HostSystem'):
|
|
all_units.update(document['data'].get('systemd_units', {}))
|
|
|
|
return all_units
|
|
|
|
@property
|
|
def join_ips(self):
|
|
maybe_ips = self.get_path('KubernetesNode:join_ips')
|
|
if maybe_ips is not None:
|
|
return maybe_ips
|
|
else:
|
|
maybe_ip = self._get_first('KubernetesNode:join_ip', 'Genesis:ip')
|
|
if maybe_ip:
|
|
return [maybe_ip]
|
|
else:
|
|
return jinja2.StrictUndefined('Could not find join IPs')
|
|
|
|
def get_path(self, path, default=None):
|
|
kind, jsonpath = path.split(':')
|
|
document = _get(self.documents, kind=kind)
|
|
if document:
|
|
data = _extract(document['data'], jsonpath)
|
|
if data:
|
|
return data
|
|
return default
|
|
|
|
def append(self, item):
|
|
validation.check_schema(item)
|
|
self.documents.append(item)
|
|
|
|
def bootstrap_apiserver_prefix(self):
|
|
return self.get_path('Genesis:apiserver.command_prefix',
|
|
['kube-apiserver'])
|
|
|
|
|
|
def _matches_filter(document, *, schema, labels, name):
|
|
matches = True
|
|
if schema is not None and not document.get('schema',
|
|
'').startswith(schema):
|
|
matches = False
|
|
|
|
if labels is not None:
|
|
document_labels = _mg(document, 'labels', [])
|
|
for key, value in labels.items():
|
|
if key not in document_labels:
|
|
matches = False
|
|
else:
|
|
if document_labels[key] != value:
|
|
matches = False
|
|
|
|
if name is not None:
|
|
if _mg(document, 'name') != name:
|
|
matches = False
|
|
|
|
return matches
|
|
|
|
|
|
def _get(documents, kind=None, schema=None, name=None):
|
|
if kind is not None:
|
|
if schema is not None:
|
|
msg = "Only kind or schema may be specified, not both"
|
|
raise exceptions.ValidationException(msg)
|
|
schema = 'promenade/%s/v1' % kind
|
|
|
|
for document in documents:
|
|
if (schema == document.get('schema')
|
|
and (name is None or name == _mg(document, 'name'))):
|
|
return document
|
|
|
|
|
|
def _extract(document, jsonpath):
|
|
p = jsonpath_ng.parse(jsonpath)
|
|
matches = p.find(document)
|
|
if matches:
|
|
return matches[0].value
|
|
|
|
|
|
def _mg(document, field, default=None):
|
|
return document.get('metadata', {}).get(field, default)
|