Common code for deployment groups in Shipyard

Adds in the classes and traversal logic for groups as
part of a deployment strategy. Nothing is using these yet,
but will be in subsequent patchsets

There are some artifacts being removed that are left over
from a prior refactor and were existing in two places.

Change-Id: Id38b211d3cf8112f4cb34c2ef5dcf440d3d20e4c
This commit is contained in:
Bryan Strassner 2018-04-23 18:53:27 -05:00
parent b944ba666f
commit dffb8d3b93
24 changed files with 1342 additions and 410 deletions

View File

@ -1,74 +0,0 @@
---
schema: 'deckhand/DataSchema/v1'
metadata:
schema: metadata/Control/v1
name: shipyard/DeploymentStrategy/v1
labels:
application: shipyard
data:
$schema: 'http://json-schema.org/schema#'
id: 'https://github.com/att-comdev/shipyard/deploymentStrategy.yaml'
type: 'object'
required:
- groups
properties:
groups:
type: 'array'
minItems: 0
items:
type: 'object'
required:
- name
- critical
- depends_on
- selectors
properties:
name:
type: 'string'
minLength: 1
critical:
type: 'boolean'
depends_on:
type: 'array'
minItems: 0
items:
type: 'string'
selectors:
type: 'array'
minItems: 0
items:
type: 'object'
minProperties: 1
properties:
node_names:
type: 'array'
items:
type: 'string'
node_labels:
type: 'array'
items:
type: 'string'
node_tags:
type: 'array'
items:
type: 'string'
rack_names:
type: 'array'
items:
type: 'string'
additionalProperties: false
success_criteria:
type: 'object'
minProperties: 1
properties:
percent_successful_nodes:
type: 'integer'
minimum: 0
maximum: 100
minimum_successful_nodes:
type: 'integer'
minimum: 0
maximum_failed_nodes:
type: 'integer'
minimum: 0
additionalProperties: false

View File

@ -21,6 +21,7 @@ falcon==1.2.0
jsonschema==2.6.0
keystoneauth1==3.4.0
keystonemiddleware==4.21.0
networkx==2.1
oslo.config==5.2.0
oslo.policy==1.33.1
PasteDeploy==1.5.2

View File

@ -1,4 +1,4 @@
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
# Copyright 2018 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
@ -11,21 +11,14 @@
# 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 os
#
"""Common Modules
import pytest
import shutil
The various packages in this common package should each be stand-alone
modules having no dependencies on prior logic running in Shipyard (e.g.
Setup of configuration files, Shipyard/Airflow database access, etc...). It is
ok if these modules use imports found in requirements.txt
@pytest.fixture(scope='module')
def input_files(tmpdir_factory, request):
tmpdir = tmpdir_factory.mktemp('data')
samples_dir = os.path.dirname(str(
request.fspath)) + "/" + "../yaml_samples"
samples = os.listdir(samples_dir)
for f in samples:
src_file = samples_dir + "/" + f
dst_file = str(tmpdir) + "/" + f
shutil.copyfile(src_file, dst_file)
return tmpdir
These modules are intended to be safe for reuse outside of the context of
the Shipyard_Airflow/Api service as well as within.
"""

View File

@ -0,0 +1,330 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
#
"""Deployment group module
Encapsulates classes and functions that provide core deployment group
functionality used during baremetal provisioning.
"""
import collections
from enum import Enum
import logging
import operator
from .errors import DeploymentGroupStageError
from .errors import InvalidDeploymentGroupError
from .errors import InvalidDeploymentGroupNodeLookupError
LOG = logging.getLogger(__name__)
class Stage(Enum):
"""Valid values for baremetal node and deployment group stages of
deployment
"""
# A node that has not yet started deployment. The default.
NOT_STARTED = 'NOT_STARTED'
# A node that has finished the prepare_node stage successfully
PREPARED = 'PREPARED'
# A node that has finished the deploy_node stage successfully
DEPLOYED = 'DEPLOYED'
# A node that has failed to complete in any step.
FAILED = 'FAILED'
@classmethod
def is_complete(cls, stage):
return stage in [cls.DEPLOYED, cls.FAILED]
@classmethod
def previous_stage(cls, stage):
"""The valid states before the supplied state"""
if stage == cls.NOT_STARTED:
return []
if stage == cls.PREPARED:
return [cls.NOT_STARTED]
if stage == cls.DEPLOYED:
return [cls.PREPARED]
if stage == cls.FAILED:
return [cls.NOT_STARTED, cls.PREPARED]
else:
raise DeploymentGroupStageError("{} is not a valid stage".format(
str(stage)))
class GroupNodeSelector:
"""GroupNodeSelector object
:param selector_dict: dictionary representing the possible selector values
Encapsulates the criteria defining the selector for a deployment group.
Example selector_dict::
{
'node_names': [],
'node_labels': [],
'node_tags': ['control'],
'rack_names': ['rack03'],
}
"""
def __init__(self, selector_dict):
self.node_names = selector_dict.get('node_names', [])
self.node_labels = selector_dict.get('node_labels', [])
self.node_tags = selector_dict.get('node_tags', [])
self.rack_names = selector_dict.get('rack_names', [])
# A selector is an "all_selector" if there are no criteria specified.
self.all_selector = not any([self.node_names, self.node_labels,
self.node_tags, self.rack_names])
if self.all_selector:
LOG.debug("Selector values select all available nodes")
class SuccessCriteria:
"""Defines the success criteria for a deployment group
:param criteria: a dictionary containing up to 3 fields in
percent_successful_nodes, minimum_successful_nodes,
maximum_failed_nodes
If no criteria are specified, all results are considered a success
"""
def __init__(self, criteria):
if not criteria:
self._always_succeed = True
return
self._always_succeed = False
# set the criteria or let them be None
self.pct_succ_nodes = criteria.get('percent_successful_nodes')
self.min_succ_nodes = criteria.get('minimum_successful_nodes')
self.max_failed_nodes = criteria.get('maximum_failed_nodes')
def get_failed(self, succ_list, all_nodes_list):
"""Determine which criteria have failed.
:param succ_list: A list of names of nodes that have successfully
completed a stage
:param all_nodes_list: A list of all node names that are to be
evaluated against.
Using the provided list of successful nodes, and the list of all
nodes, check which of the success criteria have failed to have been
met.
"""
failures = []
# If no criteria, or list of all nodes is empty, return empty list
if self._always_succeed or len(all_nodes_list) == 0:
return failures
succ_set = set(succ_list)
all_set = set(all_nodes_list)
all_size = len(all_set)
succ_size = len(succ_set.intersection(all_set))
fail_size = len(all_set.difference(succ_set))
actual_pct_succ = succ_size / all_size * 100
failures.extend(self._check("percent_successful_nodes",
actual_pct_succ, operator.ge,
self.pct_succ_nodes))
failures.extend(self._check("minimum_successful_nodes", succ_size,
operator.ge, self.min_succ_nodes))
failures.extend(self._check("maximum_failed_nodes", fail_size,
operator.le, self.max_failed_nodes))
return failures
def _check(self, name, actual, op, needed):
"""Evaluates a single criteria
:param name: name of the check
:param actual: the result that was achieved (LHS)
:param op: operator used for comparison
:param needed: the threshold of success (RHS). If this parameter
is None, the criteria is ignored as "successful" because it
was not set as a needed criteria
Returns a list containing the failure dictionary if the comparison
fails or and empty list if check is successful.
"""
if needed is None:
LOG.info(" - %s criteria not specified, not evaluated", name)
return []
if op(actual, needed):
LOG.info(" - %s succeeded, %s %s %s", name, actual, op.__name__,
needed)
return []
else:
fail = {"criteria": name, "needed": needed, "actual": actual}
LOG.info(" - %s failed, %s %s %s", name, actual, op.__name__,
needed)
return [fail]
class DeploymentGroup:
"""DeploymentGroup object representing a deployment group
:param group_dict: dictionary representing a group
:param node_lookup: an injected function that will perform node lookup for
a group. Function must accept an iterable of GroupNodeSelector and
return a string list of node names
Example group_dict::
{
'name': 'control-nodes',
'critical': True,
'depends_on': ['ntp-node'],
'selectors': [
{
'node_names': [],
'node_labels': [],
'node_tags': ['control'],
'rack_names': ['rack03'],
},
],
'success_criteria': {
'percent_successful_nodes': 90,
'minimum_successful_nodes': 3,
'maximum_failed_nodes': 1,
},
}
"""
def __init__(self, group_dict, node_lookup):
# store the original dictionary
self._group_dict = group_dict
# fields required by schema
self._check_required_fields()
self.critical = group_dict['critical']
self.depends_on = group_dict['depends_on']
self.name = group_dict['name']
self.selectors = []
for selector_dict in group_dict['selectors']:
self.selectors.append(GroupNodeSelector(selector_dict))
if not self.selectors:
# no selectors means add an "all" selector
self.selectors.append(GroupNodeSelector({}))
self.success_criteria = SuccessCriteria(
group_dict.get('success_criteria', {})
)
# all groups start as NOT_STARTED
self.__stage = None
self.stage = Stage.NOT_STARTED
# node_lookup function for use with this deployment group
# lookup the full list of nodes for this group's selectors
self.node_lookup = node_lookup
self.full_nodes = self._calculate_all_nodes()
# actionable_nodes is set up based on multi-group interaction.
# Only declaring the field here. Used for deduplicaiton.
self.actionable_nodes = []
@property
def stage(self):
return self.__stage
@stage.setter
def stage(self, stage):
valid_prior = Stage.previous_stage(stage)
pre_change_stage = self.__stage
if self.__stage == stage:
return
elif self.__stage is None and not valid_prior:
self.__stage = stage
elif self.__stage in valid_prior:
self.__stage = stage
else:
raise DeploymentGroupStageError(
"{} is not a valid stage for a group in stage {}".format(
stage, self.__stage
))
LOG.info("Setting group %s with %s -> %s",
self.name,
pre_change_stage,
stage)
def _check_required_fields(self):
"""Checks for required input fields and errors if any are missing"""
for attr in ['critical', 'depends_on', 'name', 'selectors']:
try:
value = self._group_dict[attr]
LOG.debug("Attribute %s has value %s", attr, str(value))
except KeyError:
raise InvalidDeploymentGroupError(
"Attribute '{}' is required as input to create a "
"DeploymentGroup".format(attr))
def _calculate_all_nodes(self):
"""Invoke the node_lookup to retrieve nodes
After construction of the DeploymentGroup, this method is generally
not useful as the results are stored in self.full_nodes
"""
LOG.debug("Beginning lookup of nodes for group %s", self.name)
node_list = self.node_lookup(self.selectors)
if node_list is None:
node_list = []
if not isinstance(node_list, collections.Sequence):
raise InvalidDeploymentGroupNodeLookupError(
"The node lookup function supplied to the DeploymentGroup "
"does not return a valid result of an iterable"
)
if not all(isinstance(node, str) for node in node_list):
raise InvalidDeploymentGroupNodeLookupError(
"The node lookup function supplied to the DeploymentGroup "
"has returned an iterable, but not all strings"
)
LOG.info("Group %s selectors have resolved to nodes: %s",
self.name, ", ".join(node_list))
return node_list
def get_failed_success_criteria(self, success_node_list):
"""Check the success criteria for this group.
:param success_node_list: list of nodes that are deemed successful
to be compared to the success criteria
Using the list of all nodes, and the provided success_node_list,
use the SuccessCriteria for this group to see if that list of
successes meets the criteria.
Note that this is not checking for any particular stage of deployment,
simply the comparison of the total list of nodes to the provided list.
Returns a list of failures. An empty list indicates successful
comparison with all criteria.
A good pattern for use of this method is to provide a list of all
nodes being deployed across all groups that are successful for a
given stage of deployment (e.g. all prepared, all deployed).
Calculations are done using set comparisons, so nodes that are not
important for this group will be ignored. It is important *not* to
provide only a list of nodes that were recently acted upon as part of
this group, as deduplication from overlapping groups may cause the
calculations to be skewed and report false failures.
"""
LOG.info('Assessing success criteria for group %s', self.name)
sc = self.success_criteria.get_failed(success_node_list,
self.full_nodes)
if sc:
LOG.info('Group %s failed success criteria', self.name)
else:
LOG.info('Group %s success criteria passed', self.name)
return sc

View File

@ -0,0 +1,262 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
#
"""Deployment group manager module
Encapsulates classes and functions related to the management and use of
deployment groups used during baremetal provisioning.
"""
import logging
import networkx as nx
from .deployment_group import DeploymentGroup
from .deployment_group import Stage
from .errors import DeploymentGroupCycleError
from .errors import DeploymentGroupStageError
from .errors import UnknownDeploymentGroupError
from .errors import UnknownNodeError
LOG = logging.getLogger(__name__)
class DeploymentGroupManager:
"""Manager object to control ordering and cross-group interactions
:param group_dict_list: list of group entries translated from a
DeploymentStrategy document.
:param node_lookup: function to lookup nodes based on group selectors
"""
def __init__(self, group_dict_list, node_lookup):
LOG.debug("Initializing DeploymentGroupManager")
# the raw input
self._group_dict_list = group_dict_list
# A dictionary of all groups by group name. E.g.:
# {
# 'group-1': DeploymentGroup(...),
# }
self._all_groups = {}
for group_dict in group_dict_list:
group = DeploymentGroup(group_dict, node_lookup)
self._all_groups[group.name] = group
self._group_graph = _generate_group_graph(
self._all_groups.values()
)
self._group_order = list(nx.topological_sort(self._group_graph))
# Setup nodes.
# self.all_nodes is a dictionary of all nodes by node name,
# representing each node's status of deployment. E.g.:
# { 'node-01' : Stage.NOT_STARTED}
#
# each group is also updated with group.actionable_nodes based on group
# ordering (deduplication)
self._all_nodes = {}
self._calculate_nodes()
def get_next_group(self, stage):
"""Get the next eligible group name to use for the provided stage
Finds the next group that has as status eligible for the stage
provided.
Returns None if there are no groups ready for the stage
"""
prev_stage = Stage.previous_stage(stage)
for group in self._group_order:
if self._all_groups[group].stage in prev_stage:
return self._all_groups[group]
return None
#
# Methods that support setup of the nodes in groups
#
def _calculate_nodes(self):
"""Calculate the mapping of all compute nodes
Uses self.group_order, self.all_groups
"""
for name in self._group_order:
group = self._all_groups[name]
known_nodes = set(self._all_nodes.keys())
_update_group_actionable_nodes(group, known_nodes)
for node in group.full_nodes:
self._all_nodes[node] = Stage.NOT_STARTED
#
# Methods for managing marking the stage of processing for a group
#
def mark_group_failed(self, group_name):
"""Sets status for a group and all successors(dependents) to failed
:param group_name: The name of the group to fail
"""
group = self._find_group(group_name)
group.stage = Stage.FAILED
successors = list(self._group_graph.successors(group_name))
if successors:
LOG.info("Group %s (now FAILED) has dependent groups %s",
group_name, ", ".join(successors))
for name in successors:
self.mark_group_failed(name)
def mark_group_prepared(self, group_name):
"""Sets a group to the Stage.PREPARED stage"""
group = self._find_group(group_name)
group.stage = Stage.PREPARED
def mark_group_deployed(self, group_name):
"""Sets a group to the Stage.DEPLOYED stage"""
group = self._find_group(group_name)
group.stage = Stage.DEPLOYED
def _find_group(self, group_name):
"""Wrapper for accessing groups from self.all_groups"""
group = self._all_groups.get(group_name)
if group is None:
raise UnknownDeploymentGroupError(
"Group name {} does not refer to a known group".format(
group_name)
)
return group
def get_group_failures_for_stage(self, group_name, stage):
"""Check if the nodes of a group cause the group to fail
Returns the list of failed success criteria, or [] if the group is
successful
This is only for checking transitions to PREPARED and DEPLOYED. The
valid stages for input to this method are Stage.PREPARED and
Stage.DEPLOYED.
Note that nodes that are DEPLOYED count as PREPARED, but not
the other way around.
"""
if stage not in [Stage.DEPLOYED, Stage.PREPARED]:
raise DeploymentGroupStageError(
"The stage {} is not valid for checking group"
" failures.".format(stage))
success_nodes = set()
# deployed nodes count as success for prepared and deployed
success_nodes.update(self.get_nodes(Stage.DEPLOYED))
if stage == Stage.PREPARED:
success_nodes.update(self.get_nodes(Stage.PREPARED))
group = self._find_group(group_name)
return group.get_failed_success_criteria(success_nodes)
#
# Methods for handling nodes
#
def mark_node_deployed(self, node_name):
"""Mark a node as deployed"""
self._set_node_stage(node_name, Stage.DEPLOYED)
def mark_node_prepared(self, node_name):
"""Mark a node as prepared"""
self._set_node_stage(node_name, Stage.PREPARED)
def mark_node_failed(self, node_name):
"""Mark a node as failed"""
self._set_node_stage(node_name, Stage.FAILED)
def _set_node_stage(self, node_name, stage):
"""Find and set a node's stage to the specified stage"""
if node_name in self._all_nodes:
self._all_nodes[node_name] = stage
else:
raise UnknownNodeError("The specified node {} does not"
" exist in this manager".format(node_name))
def get_nodes(self, stage=None):
"""Get a list of nodes that have the specified status"""
if stage is None:
return [name for name in self._all_nodes]
return [name for name, n_stage
in self._all_nodes.items()
if n_stage == stage]
def _update_group_actionable_nodes(group, known_nodes):
"""Updates a group's actionable nodes
Acitonable nodes is the group's (full_nodes - known_nodes)
"""
LOG.debug("Known nodes before processing group %s is %s",
group.name,
", ".join(known_nodes))
group_nodes = set(group.full_nodes)
group.actionable_nodes = group_nodes.difference(known_nodes)
LOG.debug("Group %s set actionable_nodes to %s. "
"Full node list for this group is %s",
group.name,
", ".join(group.actionable_nodes),
", ".join(group.full_nodes))
def _generate_group_graph(groups):
"""Create the directed graph of groups
:param groups: An iterable of DeploymentGroup objects
returns a directed graph of group names
"""
LOG.debug("Generating directed graph of groups based on dependencies")
graph = nx.DiGraph()
# Add all groups as graph nodes. It is not strictly necessary to do two
# loops here, but n is small and for obviousness.
for group in groups:
graph.add_node(group.name)
# Add all edges
for group in groups:
if group.depends_on:
for parent in group.depends_on:
LOG.debug("%s has parent %s", group.name, parent)
graph.add_edge(parent, group.name)
else:
LOG.debug("%s is not dependent upon any other groups")
_detect_cycles(graph)
return graph
def _detect_cycles(graph):
"""Detect if there are cycles between the groups
Raise a DeploymentGroupCycleError if there are any circular
dependencies
"""
LOG.debug("Detecting cycles in graph")
circ_deps = []
try:
circ_deps = list(nx.find_cycle(graph))
except nx.NetworkXNoCycle:
LOG.info('There are no cycles detected in the graph')
pass
if circ_deps:
involved_nodes = set()
# a value in this list is like: ('group1', 'group2')
for dep in circ_deps:
involved_nodes.update(dep)
raise DeploymentGroupCycleError(
"The following are involved in a circular dependency:"
" %s", ", ".join(involved_nodes)
)

View File

@ -0,0 +1,64 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
#
class InvalidDeploymentGroupError(Exception):
"""InvalidDeploymentGroupError
Represents that a deployment group's configuration is invalid
"""
pass
class InvalidDeploymentGroupNodeLookupError(InvalidDeploymentGroupError):
"""InvalidDeploymentGroupNodeLookupError
Indicates that there is a problem with the node lookup function
provided to the deployment group
"""
pass
class DeploymentGroupCycleError(Exception):
"""DeploymentGroupCycleError
Raised when a set of deployment groups have a dependency cycle
"""
pass
class DeploymentGroupStageError(Exception):
"""DeploymentGroupStageError
Raised for invalid operations while processing stages
"""
pass
class UnknownDeploymentGroupError(Exception):
"""UnknownDeploymentGroupError
Raised when there is an attempt to access a deployment group that isn't
recognized by the system
"""
pass
class UnknownNodeError(Exception):
"""UnknownNodeError
Raised when trying to access a node that does not exist
"""
pass

View File

@ -0,0 +1,85 @@
# Copyright 2017 AT&T Intellectual Property. All other 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.
"""Stubs that play the role of a node lookup for testing of DeploymentGroup
related functionality"""
# Lookups for testing different node selectors
_NODE_LABELS = {
'label1:label1': ['node1', 'node3', 'node5', 'node7', 'node9', 'node11'],
'label2:label2': ['node2', 'node4', 'node6', 'node8', 'node10', 'node12'],
'label3:label3': ['node1', 'node2', 'node3', 'node4', 'node5', 'node6'],
'label4:label4': ['node7', 'node8', 'node9', 'node10', 'node11', 'node12'],
'compute:true': ['node4', 'node5', 'node7', 'node8', 'node10', 'node11']
}
_NODE_TAGS = {
'tag1': ['node2', 'node5', 'node8'],
'tag2': ['node3', 'node6', 'node9'],
'monitoring': ['node6', 'node9', 'node12']
}
_RACK_NAMES = {
'rack1': ['node1', 'node2', 'node3'],
'rack2': ['node4', 'node5', 'node6'],
'rack3': ['node7', 'node8', 'node9'],
'rack4': ['node10', 'node11', 'node12'],
}
_ALL_NODES = {'node1', 'node2', 'node3', 'node4', 'node5', 'node6', 'node7',
'node8', 'node9', 'node10', 'node11', 'node12'}
def node_lookup(selectors):
"""A method that can be used in place of a real node lookup
Performs a simple intersection of the selector criteria using the
lookup fields defined above.
"""
def get_nodes(lookup, keys):
nodes = []
for key in keys:
nodes.extend(lookup[key])
return set(nodes)
nodes_full = []
for selector in selectors:
nl_list = []
if selector.all_selector:
nl_list.append(_ALL_NODES)
else:
if selector.node_names:
nl_list.append(set(selector.node_names))
if selector.node_labels:
nl_list.append(get_nodes(_NODE_LABELS,
selector.node_labels))
if selector.node_tags:
nl_list.append(get_nodes(_NODE_TAGS, selector.node_tags))
if selector.rack_names:
nl_list.append(get_nodes(_RACK_NAMES, selector.rack_names))
nodes = set.intersection(*nl_list)
nodes_full.extend(nodes)
return nodes_full
def crummy_node_lookup(selectors):
"""Returns None"""
return None
def broken_node_lookup_1(selectors):
"""Doesn't return a list"""
return {"this": "that"}
def broken_node_lookup_2(selectors):
"""Returns a list of various garbage, not strings"""
return [{"this": "that"}, 7, "node3"]

View File

@ -0,0 +1,268 @@
# Copyright 2017 AT&T Intellectual Property. All other 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.
"""Tests to validate behavior of the classes in the deployment_group module"""
import pytest
import yaml
from shipyard_airflow.common.deployment_group.deployment_group import (
DeploymentGroup, Stage
)
from shipyard_airflow.common.deployment_group.errors import (
DeploymentGroupStageError, InvalidDeploymentGroupError,
InvalidDeploymentGroupNodeLookupError
)
from .node_lookup_stubs import node_lookup
from .node_lookup_stubs import crummy_node_lookup
from .node_lookup_stubs import broken_node_lookup_1
from .node_lookup_stubs import broken_node_lookup_2
_GROUP_YAML_1 = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels: []
node_tags:
- tag1
rack_names:
- rack3
success_criteria:
percent_successful_nodes: 90
minimum_successful_nodes: 3
maximum_failed_nodes: 1
"""
_GROUP_YAML_MULTI_SELECTOR = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels: []
node_tags:
- tag1
rack_names:
- rack3
- node_names: []
node_labels:
- label1:label1
node_tags: []
rack_names:
- rack3
- rack4
success_criteria:
percent_successful_nodes: 79
minimum_successful_nodes: 3
maximum_failed_nodes: 1
"""
_GROUP_YAML_EXCLUDES_ALL = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels: []
node_tags:
- tag2
rack_names:
- rack4
success_criteria:
percent_successful_nodes: 90
minimum_successful_nodes: 3
maximum_failed_nodes: 1
"""
_GROUP_YAML_MISSING = """
name: control-nodes
"""
_GROUP_YAML_NO_SUCC_CRITERIA = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels:
- label1:label1
node_tags: []
rack_names:
- rack3
- rack4
"""
_GROUP_YAML_MINIMAL_SUCC_CRITERIA = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels: []
node_tags:
- tag1
rack_names:
- rack3
- node_names: []
node_labels:
- label1:label1
node_tags: []
rack_names:
- rack3
- rack4
success_criteria:
maximum_failed_nodes: 1
"""
_GROUP_YAML_ALL_SELECTOR = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors: []
"""
_GROUP_YAML_ALL_SELECTOR_2 = """
name: control-nodes
critical: true
depends_on:
- ntp-node
selectors: [
node_names: []
]
"""
class TestDeploymentGroup:
def test_basic_class(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_1), node_lookup)
assert set(dg.full_nodes) == {'node8'}
assert dg.critical
assert dg.name == "control-nodes"
assert set(dg.depends_on) == {"ntp-node"}
assert len(dg.selectors) == 1
assert not dg.success_criteria._always_succeed
assert dg.success_criteria.pct_succ_nodes == 90
assert dg.success_criteria.min_succ_nodes == 3
assert dg.success_criteria.max_failed_nodes == 1
assert dg.stage == Stage.NOT_STARTED
def test_basic_class_multi_selector(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_MULTI_SELECTOR),
node_lookup)
assert set(dg.full_nodes) == {'node7', 'node8', 'node9', 'node11'}
def test_basic_class_missing_req(self):
with pytest.raises(InvalidDeploymentGroupError):
DeploymentGroup(yaml.safe_load(_GROUP_YAML_MISSING),
node_lookup)
def test_basic_class_no_succ_criteria(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_NO_SUCC_CRITERIA),
node_lookup)
assert dg.success_criteria._always_succeed
assert not dg.get_failed_success_criteria([])
def test_succ_criteria_success(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_MULTI_SELECTOR),
node_lookup)
assert set(dg.full_nodes) == {'node7', 'node8', 'node9', 'node11'}
assert not dg.get_failed_success_criteria(
success_node_list=['node7', 'node8', 'node11', 'node9']
)
def test_succ_criteria_minimal_criteria(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_MINIMAL_SUCC_CRITERIA),
node_lookup)
assert set(dg.full_nodes) == {'node7', 'node8', 'node9', 'node11'}
assert not dg.get_failed_success_criteria(
success_node_list=['node8', 'node11', 'node9']
)
def test_succ_criteria_failure(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_MULTI_SELECTOR),
node_lookup)
assert set(dg.full_nodes) == {'node7', 'node8', 'node9', 'node11'}
failed = dg.get_failed_success_criteria(
success_node_list=['node8', 'node11', 'node9']
)
assert len(failed) == 1
assert failed[0] == {'actual': 75.0,
'criteria': 'percent_successful_nodes',
'needed': 79}
def test_all_selector_group(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_ALL_SELECTOR),
node_lookup)
assert dg.selectors[0].all_selector
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_ALL_SELECTOR_2),
node_lookup)
assert dg.selectors[0].all_selector
def test_selector_excludes_all(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_EXCLUDES_ALL),
node_lookup)
assert dg.full_nodes == []
def test_handle_none_node_lookup(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_1),
crummy_node_lookup)
assert dg.full_nodes == []
def test_handle_broken_node_lookup(self):
with pytest.raises(InvalidDeploymentGroupNodeLookupError) as err:
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_1),
broken_node_lookup_1)
assert str(err).endswith("iterable")
with pytest.raises(InvalidDeploymentGroupNodeLookupError) as err:
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_1),
broken_node_lookup_2)
assert str(err).endswith("but not all strings")
def test_set_stage(self):
dg = DeploymentGroup(yaml.safe_load(_GROUP_YAML_ALL_SELECTOR),
node_lookup)
with pytest.raises(DeploymentGroupStageError):
dg.stage = Stage.DEPLOYED
dg.stage = Stage.PREPARED
assert dg.stage == Stage.PREPARED
dg.stage = Stage.DEPLOYED
assert dg.stage == Stage.DEPLOYED
class TestStage:
def test_is_complete(self):
assert not Stage.is_complete(Stage.NOT_STARTED)
assert not Stage.is_complete(Stage.PREPARED)
assert Stage.is_complete(Stage.DEPLOYED)
assert Stage.is_complete(Stage.FAILED)
def test_previous_stage(self):
assert Stage.previous_stage(Stage.NOT_STARTED) == []
assert Stage.previous_stage(Stage.PREPARED) == [Stage.NOT_STARTED]
assert Stage.previous_stage(Stage.DEPLOYED) == [Stage.PREPARED]
assert Stage.previous_stage(Stage.FAILED) == [Stage.NOT_STARTED,
Stage.PREPARED]
with pytest.raises(DeploymentGroupStageError) as de:
Stage.previous_stage('Chickens and Turkeys')
assert str(de).endswith("Chickens and Turkeys is not a valid stage")

View File

@ -0,0 +1,322 @@
# Copyright 2017 AT&T Intellectual Property. All other 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.
"""Tests to validate behavior of the classes in the deployment_group_manager
module
"""
import pytest
import yaml
from shipyard_airflow.common.deployment_group.deployment_group import (
Stage
)
from shipyard_airflow.common.deployment_group.deployment_group_manager import (
DeploymentGroupManager
)
from shipyard_airflow.common.deployment_group.errors import (
DeploymentGroupCycleError, DeploymentGroupStageError,
UnknownDeploymentGroupError, UnknownNodeError
)
from .node_lookup_stubs import node_lookup
_GROUPS_YAML = """
- name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names:
- node1
- node2
node_labels: []
node_tags: []
rack_names:
- rack1
success_criteria:
percent_successful_nodes: 100
- name: compute-nodes-1
critical: false
depends_on:
- control-nodes
selectors:
- node_names: []
node_labels:
- compute:true
rack_names:
- rack2
node_tags: []
success_criteria:
percent_successful_nodes: 50
- name: compute-nodes-2
critical: false
depends_on:
- control-nodes
selectors:
- node_names: []
node_labels:
- compute:true
rack_names:
- rack3
node_tags: []
success_criteria:
percent_successful_nodes: 50
- name: spare-compute-nodes
critical: false
depends_on:
- compute-nodes-2
- compute-nodes-1
selectors:
- node_names: []
node_labels:
- compute:true
rack_names:
- rack4
node_tags: []
- name: all-compute-nodes
critical: false
depends_on:
- compute-nodes-2
- compute-nodes-1
- spare-compute-nodes
selectors:
- node_names: []
node_labels:
- compute:true
rack_names: []
node_tags: []
- name: monitoring-nodes
critical: false
depends_on: []
selectors:
- node_names: []
node_labels: []
node_tags:
- monitoring
rack_names: []
success_criteria:
minimum_successful_nodes: 3
- name: ntp-node
critical: true
depends_on: []
selectors:
- node_names:
- node3
node_labels: []
node_tags: []
rack_names:
- rack1
success_criteria:
minimum_successful_nodes: 1
"""
_CYCLE_GROUPS_YAML = """
- name: group-a
critical: true
depends_on:
- group-c
selectors: []
- name: group-b
critical: true
depends_on:
- group-a
selectors: []
- name: group-c
critical: true
depends_on:
- group-d
selectors: []
- name: group-d
critical: true
depends_on:
- group-a
selectors: []
"""
class TestDeploymentGroupManager:
def test_basic_class(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
assert dgm is not None
# topological sort doesn't guarantee a specific order.
assert dgm.get_next_group(Stage.PREPARED).name in ['ntp-node',
'monitoring-nodes']
assert len(dgm._all_groups) == 7
assert len(dgm._all_nodes) == 12
for name, group in dgm._all_groups.items():
assert name == group.name
def test_cycle_error(self):
with pytest.raises(DeploymentGroupCycleError) as ce:
DeploymentGroupManager(yaml.safe_load(_CYCLE_GROUPS_YAML),
node_lookup)
assert 'The following are involved' in str(ce)
for g in ['group-a', 'group-c', 'group-d']:
assert g in str(ce)
assert 'group-b' not in str(ce)
def test_no_next_group(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
assert dgm.get_next_group(Stage.DEPLOYED) is None
def test_ordering_stages_flow_failure(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
group = dgm.get_next_group(Stage.PREPARED)
if group.name == 'monitoring-nodes':
dgm.mark_group_prepared(group.name)
dgm.mark_group_deployed(group.name)
group = dgm.get_next_group(Stage.PREPARED)
if group.name == 'ntp-node':
dgm.mark_group_failed(group.name)
group = dgm.get_next_group(Stage.PREPARED)
if group and group.name == 'monitoring-nodes':
dgm.mark_group_prepared(group.name)
dgm.mark_group_deployed(group.name)
group = dgm.get_next_group(Stage.PREPARED)
# all remaining groups should be failed, so no more to prepare
for name, grp in dgm._all_groups.items():
if (name == 'monitoring-nodes'):
assert grp.stage == Stage.DEPLOYED
else:
assert grp.stage == Stage.FAILED
assert group is None
def test_deduplication(self):
"""all-compute-nodes is a duplicate of things it's dependent on, it
should have no actionable nodes"""
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
acn = dgm._all_groups['all-compute-nodes']
assert len(acn.actionable_nodes) == 0
assert len(acn.full_nodes) == 6
def test_bad_group_name_lookup(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
with pytest.raises(UnknownDeploymentGroupError) as udge:
dgm.mark_group_prepared('Limburger Cheese')
assert "Group name Limburger Cheese does not refer" in str(udge)
def test_get_group_failures_for_stage_bad_input(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
with pytest.raises(DeploymentGroupStageError):
dgm.get_group_failures_for_stage('group1', Stage.FAILED)
def test_get_group_failures_for_stage(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
dgm._all_nodes = {
'node1': Stage.DEPLOYED,
'node2': Stage.DEPLOYED,
'node3': Stage.DEPLOYED,
'node4': Stage.DEPLOYED,
'node5': Stage.DEPLOYED,
'node6': Stage.DEPLOYED,
'node7': Stage.DEPLOYED,
'node8': Stage.DEPLOYED,
'node9': Stage.DEPLOYED,
'node10': Stage.DEPLOYED,
'node11': Stage.DEPLOYED,
'node12': Stage.DEPLOYED,
}
for group_name in dgm._all_groups:
assert not dgm.get_group_failures_for_stage(group_name,
Stage.DEPLOYED)
assert not dgm.get_group_failures_for_stage(group_name,
Stage.PREPARED)
dgm._all_nodes = {
'node1': Stage.PREPARED,
'node2': Stage.PREPARED,
'node3': Stage.PREPARED,
'node4': Stage.PREPARED,
'node5': Stage.PREPARED,
'node6': Stage.PREPARED,
'node7': Stage.PREPARED,
'node8': Stage.PREPARED,
'node9': Stage.PREPARED,
'node10': Stage.PREPARED,
'node11': Stage.PREPARED,
'node12': Stage.PREPARED,
}
for group_name in dgm._all_groups:
assert not dgm.get_group_failures_for_stage(group_name,
Stage.PREPARED)
for group_name in ['compute-nodes-1',
'monitoring-nodes',
'compute-nodes-2',
'control-nodes',
'ntp-node']:
# assert that these have a failure
assert dgm.get_group_failures_for_stage(group_name, Stage.DEPLOYED)
dgm._all_nodes = {
'node1': Stage.FAILED,
'node2': Stage.PREPARED,
'node3': Stage.FAILED,
'node4': Stage.PREPARED,
'node5': Stage.FAILED,
'node6': Stage.PREPARED,
'node7': Stage.FAILED,
'node8': Stage.PREPARED,
'node9': Stage.FAILED,
'node10': Stage.PREPARED,
'node11': Stage.FAILED,
'node12': Stage.PREPARED,
}
for group_name in dgm._all_groups:
scf = dgm.get_group_failures_for_stage(group_name,
Stage.PREPARED)
if group_name == 'monitoring-nodes':
assert scf[0] == {'criteria': 'minimum_successful_nodes',
'needed': 3,
'actual': 2}
if group_name == 'control-nodes':
assert scf[0] == {'criteria': 'percent_successful_nodes',
'needed': 100,
'actual': 50.0}
if group_name == 'ntp-node':
assert scf[0] == {'criteria': 'minimum_successful_nodes',
'needed': 1,
'actual': 0}
def test_mark_node_deployed(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
dgm.mark_node_deployed('node1')
assert dgm.get_nodes(Stage.DEPLOYED) == ['node1']
def test_mark_node_prepared(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
dgm.mark_node_prepared('node1')
assert dgm.get_nodes(Stage.PREPARED) == ['node1']
def test_mark_node_failed(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
dgm.mark_node_failed('node1')
assert dgm.get_nodes(Stage.FAILED) == ['node1']
def test_mark_node_failed_unknown(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
with pytest.raises(UnknownNodeError):
dgm.mark_node_failed('not_node')
def test_get_nodes_all(self):
dgm = DeploymentGroupManager(yaml.safe_load(_GROUPS_YAML), node_lookup)
assert set(dgm.get_nodes()) == set(
['node1', 'node2', 'node3', 'node4', 'node5', 'node6', 'node7',
'node8', 'node9', 'node10', 'node11', 'node12']
)

View File

@ -1,51 +0,0 @@
# Copyright 2018 AT&T Intellectual Property. All other 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.
import logging
import os
import yaml
import jsonschema
import pkg_resources
import pytest
from jsonschema.exceptions import ValidationError
LOG = logging.getLogger(__name__)
class BaseSchemaValidationTest(object):
def _test_validate(self, schema, expect_failure, input_files, input):
"""validates input yaml against schema.
:param schema: schema yaml file
:param expect_failure: should the validation pass or fail.
:param input_files: pytest fixture used to access the test input files
:param input: test input yaml doc filename"""
schema_dir = pkg_resources.resource_filename('shipyard_airflow',
'schemas')
schema_filename = os.path.join(schema_dir, schema)
schema_file = open(schema_filename, 'r')
schema = yaml.safe_load(schema_file)
input_file = input_files.join(input)
instance_file = open(str(input_file), 'r')
instance = yaml.safe_load(instance_file)
LOG.info('Input: %s, Schema: %s', input_file, schema_filename)
if expect_failure:
# TypeError is raised when he input document is not well formed.
with pytest.raises((ValidationError, TypeError)):
jsonschema.validate(instance['data'], schema['data'])
else:
jsonschema.validate(instance['data'], schema['data'])

View File

@ -1,38 +0,0 @@
# Copyright 2017 AT&T Intellectual Property. All other 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.
from .base_schema_validation import BaseSchemaValidationTest
class TestValidation(BaseSchemaValidationTest):
def test_validate_deploy_config_full_valid(self, input_files):
self._test_validate('deploymentStrategy.yaml', False, input_files,
'deploymentStrategy_full_valid.yaml')
self._test_validate('deploymentStrategy.yaml', False, input_files,
'deploymentStrategy_minimal.yaml')
self._test_validate('deploymentStrategy.yaml', False, input_files,
'deploymentStrategy_min_with_content.yaml')
for testnum in range(1, 5):
self._test_validate(
'deploymentStrategy.yaml', True, input_files,
'deploymentStrategy_bad_values_{}.yaml'.format(testnum)
)
self._test_validate('deploymentStrategy.yaml', True, input_files,
'total_garbage.yaml')
self._test_validate('deploymentStrategy.yaml', True, input_files,
'empty.yaml')

View File

@ -1,16 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
# name is a min length of 1
- name: ""
critical: false
depends_on: []
selectors: []

View File

@ -1,16 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: a_group
# critical is boolean
critical: cheese sandwich
depends_on: []
selectors: []

View File

@ -1,15 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: some_group
critical: true
# depends_on missing
selectors: []

View File

@ -1,18 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: my_name_is
critical: false
depends_on: []
selectors:
# node_names are strings
- node_names: [false, true, false]
node_labels: []

View File

@ -1,20 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: my_group
critical: false
depends_on: []
selectors:
- node_names: []
node_labels: []
success_criteria:
# should be 100 or less
percent_successful_nodes: 190

View File

@ -1,75 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: control-nodes
critical: true
depends_on:
- ntp-node
selectors:
- node_names: []
node_labels: []
node_tags:
- control
rack_names:
- rack03
success_criteria:
percent_successful_nodes: 90
minimum_successful_nodes: 3
maximum_failed_nodes: 1
- name: compute-nodes-1
critical: false
depends_on:
- control-nodes
selectors:
- node_names: []
node_labels: []
rack_names:
- rack01
node_tags:
- compute
success_criteria:
percent_successful_nodes: 50
- name: compute-nodes-2
critical: false
depends_on:
- control-nodes
selectors:
- node_names: []
node_labels: []
rack_names:
- rack02
node_tags:
- compute
success_criteria:
percent_successful_nodes: 50
- name: monitoring-nodes
critical: false
depends_on: []
selectors:
- node_names: []
node_labels: []
node_tags:
- monitoring
rack_names:
- rack03
- rack02
- rack01
- name: ntp-node
critical: true
depends_on: []
selectors:
- node_names:
- ntp01
node_labels: []
node_tags: []
rack_names: []
success_criteria:
minimum_successful_nodes: 1

View File

@ -1,15 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups:
- name: some-nodes
critical: false
depends_on: []
selectors: []

View File

@ -1,11 +0,0 @@
---
schema: shipyard/DeploymentStrategy/v1
metadata:
schema: metadata/Document/v1
name: deployment-strategy
layeringDefinition:
abstract: false
layer: global
storagePolicy: cleartext
data:
groups: []

View File

@ -1,44 +0,0 @@
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras euismod
sed urna nec posuere. Phasellus vel arcu vestibulum, mattis ligula eu,
pulvinar magna. Cras mollis velit quis maximus gravida. Morbi nec ligula
neque. Cras vitae cursus tellus, ut ornare enim. Nulla vel suscipit
arcu, in auctor ipsum. Ut a maximus magna. Integer massa risus,
tristique sit amet urna sit amet, facilisis bibendum lectus. Vivamus
vehicula urna in purus lacinia, sit amet tincidunt diam consequat.
Quisque ut metus vitae mauris condimentum sollicitudin. Pellentesque
urna nibh, mollis eu dui ac, molestie malesuada quam. Aliquam fringilla
faucibus orci, a tincidunt dui sollicitudin ac. Nullam enim velit,
imperdiet ut nulla quis, efficitur tincidunt eros. Curabitur nisi justo,
tristique non ornare vitae, mollis eu tortor. In non congue libero.
Mauris et tincidunt sem. Quisque sed congue diam, non ultrices turpis.
Pellentesque lobortis quam justo, facilisis sollicitudin mi imperdiet
sed. Ut nec leo placerat, gravida odio id, hendrerit erat. Praesent
placerat diam mi, et blandit mi sollicitudin et. Proin ligula sapien,
faucibus eget arcu vel, rhoncus vestibulum ipsum. Morbi tristique
pharetra diam non faucibus. Nam scelerisque, leo ut tempus fermentum,
dolor odio tempus nisl, et volutpat ante est id enim. Integer venenatis
scelerisque augue, quis porta lorem dapibus non. Sed arcu purus, iaculis
vitae sem sit amet, ultrices pretium leo. Nulla ultricies eleifend
tempus. Aenean elementum ipsum id auctor faucibus. Cras quis ipsum
vehicula, auctor velit et, dignissim sem. Duis sed nunc sagittis,
interdum dui consequat, iaculis purus. Curabitur quam ex, pellentesque
nec sapien ut, sodales lacinia enim. Etiam hendrerit sem eu turpis
euismod, quis luctus tortor iaculis. Vivamus a rutrum orci. Class aptent
taciti sociosqu ad litora torquent per conubia nostra, per inceptos
himenaeos. Pellentesque habitant morbi tristique senectus et netus et
malesuada fames ac turpis egestas. Aenean non neque ultrices, consequat
erat vitae, porta lorem. Phasellus fringilla fringilla imperdiet.
Quisque in nulla at elit sodales vestibulum ac eget mauris. Ut vel purus
nec metus ultrices aliquet in sed leo. Mauris vel congue velit. Donec
quam turpis, venenatis tristique sem nec, condimentum fringilla orci.
Sed eu feugiat dui. Proin vulputate lacus id blandit tempor. Vivamus
sollicitudin tincidunt ultrices. Aenean sit amet orci efficitur,
condimentum mi vel, condimentum nisi. Cras pellentesque, felis vel
maximus volutpat, turpis arcu dapibus metus, vitae fringilla massa lorem
et elit. Interdum et malesuada fames ac ante ipsum primis in faucibus.
Duis lorem velit, laoreet tincidunt fringilla at, vestibulum eget risus.
Pellentesque ullamcorper venenatis lectus, a mattis lectus feugiat vel.
Suspendisse potenti. Duis suscipit malesuada risus nec egestas. Vivamus
maximus, neque quis egestas rhoncus, mauris purus fringilla nisl, ut
fringilla odio nunc sit amet justo. Phasellus at dui quis magna
elementum sagittis. Nullam sed luctus felis, ac tincidunt erat.