Merge "Add Template Definitions"
This commit is contained in:
commit
898787db74
105
contrib/templates/example/README.rst
Normal file
105
contrib/templates/example/README.rst
Normal file
@ -0,0 +1,105 @@
|
||||
====================
|
||||
Example Bay Template
|
||||
====================
|
||||
|
||||
This project is an example to demonstrate the necessary pieces of a Bay
|
||||
template. There are three key pieces to a bay template:
|
||||
|
||||
1. Heat template - The Heat template that Magnum will use to generate a Bay.
|
||||
2. Template definition - Magnum's interface for interacting with the Heat template.
|
||||
3. Definition Entry Point - Used to advertise the available template definitions.
|
||||
|
||||
The Heat Template
|
||||
-----------------
|
||||
|
||||
The heat template is where most of the real work happens. The result of the Heat
|
||||
template should be a full Container Orchestration Environment.
|
||||
|
||||
The Template Definition
|
||||
-----------------------
|
||||
|
||||
Template definitions are a mapping of Magnum object attributes and Heat template
|
||||
parameters, along with Magnum consumable template outputs. Each definition also
|
||||
denotes which Bay Types it can provide. Bay Types are how Magnum determines which
|
||||
of the enabled Template Definitions it will use for a given Bay.
|
||||
|
||||
The Definition Entry Point
|
||||
--------------------------
|
||||
|
||||
Entry points are a standard discovery and import mechanism for Python objects.
|
||||
Each Template Definition should have an Entry Point in the `magnum.template_definitions`
|
||||
group. This example exposes it's Template Definition as `example_template = example_template:ExampleTemplate`
|
||||
in the `magnum.template_definitions` group.
|
||||
|
||||
Installing Bay Templates
|
||||
------------------------
|
||||
|
||||
Because Bay Templates are basically Python projects, they can be worked with like
|
||||
any other Python project. They can be cloned from version control and installed
|
||||
or uploaded to a package index and installed via utilities such as pip.
|
||||
|
||||
Enabling a template is as simple as adding it's Entry Point to the
|
||||
`enabled_definitions` config option in magnum.conf.::
|
||||
|
||||
# Setup python environment and install Magnum
|
||||
|
||||
$ virtualenv .venv
|
||||
$ source .venv/bin/active
|
||||
(.venv)$ git clone https://github.com/openstack/magnum.git
|
||||
(.venv)$ cd magnum
|
||||
(.venv)$ python setup.py install
|
||||
|
||||
# List installed templates, notice default templates are enabled
|
||||
|
||||
(.venv)$ magnum-template-manage list-templates
|
||||
Enabled Templates
|
||||
magnum_vm_atomic_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster.yaml
|
||||
magnum_vm_coreos_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster-coreos.yaml
|
||||
Disabled Templates
|
||||
|
||||
# Install example template
|
||||
|
||||
(.venv)$ cd contrib/templates/example
|
||||
(.venv)$ python setup.py install
|
||||
|
||||
# List installed templates, notice example template is disabled
|
||||
|
||||
(.venv)$ magnum-template-manage list-templates
|
||||
Enabled Templates
|
||||
magnum_vm_atomic_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster.yaml
|
||||
magnum_vm_coreos_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster-coreos.yaml
|
||||
Disabled Templates
|
||||
example_template: /home/example/.venv/local/lib/python2.7/site-packages/ExampleTemplate-0.1-py2.7.egg/example_template/example.yaml
|
||||
|
||||
# Enable example template by setting enabled_definitions in magnum.conf
|
||||
|
||||
(.venv)$ sudo mkdir /etc/magnum
|
||||
(.venv)$ sudo bash -c "cat > /etc/magnum/magnum.conf << END_CONF
|
||||
[bay]
|
||||
enabled_definitions=magnum_vm_atomic_k8s,magnum_vm_coreos_k8s,example_template
|
||||
END_CONF"
|
||||
|
||||
# List installed templates, notice example template is now enabled
|
||||
|
||||
(.venv)$ magnum-template-manage list-templates
|
||||
Enabled Templates
|
||||
example_template: /home/example/.venv/local/lib/python2.7/site-packages/ExampleTemplate-0.1-py2.7.egg/example_template/example.yaml
|
||||
magnum_vm_atomic_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster.yaml
|
||||
magnum_vm_coreos_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster-coreos.yaml
|
||||
Disabled Templates
|
||||
|
||||
# Use --details argument to get more details about each template
|
||||
|
||||
(.venv)$ magnum-template-manage list-templates --details
|
||||
Enabled Templates
|
||||
example_template: /home/example/.venv/local/lib/python2.7/site-packages/ExampleTemplate-0.1-py2.7.egg/example_template/example.yaml
|
||||
Platform OS CoE
|
||||
vm example example_coe
|
||||
magnum_vm_atomic_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster.yaml
|
||||
Platform OS CoE
|
||||
vm fedora-atomic kubernetes
|
||||
magnum_vm_coreos_k8s: /home/example/.venv/local/lib/python2.7/site-packages/magnum/templates/heat-kubernetes/kubecluster-coreos.yaml
|
||||
Platform OS CoE
|
||||
vm coreos kubernetes
|
||||
Disabled Templates
|
||||
|
36
contrib/templates/example/example_template/__init__.py
Normal file
36
contrib/templates/example/example_template/__init__.py
Normal file
@ -0,0 +1,36 @@
|
||||
# Copyright (c) 2015 Rackspace Inc.
|
||||
#
|
||||
# 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 os
|
||||
|
||||
from magnum.conductor import template_definition
|
||||
|
||||
|
||||
class ExampleTemplate(template_definition.BaseTemplateDefinition):
|
||||
provides = [
|
||||
{'platform': 'vm', 'os': 'example', 'coe': 'example_coe'},
|
||||
{'platform': 'vm', 'os': 'example2', 'coe': 'example_coe'},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(ExampleTemplate, self).__init__()
|
||||
|
||||
self.add_output('server_address',
|
||||
bay_attr='api_address')
|
||||
self.add_output('node_addresses',
|
||||
bay_attr='node_addresses')
|
||||
|
||||
def template_path(self):
|
||||
return os.path.join(os.path.dirname(__file__), 'example.yaml')
|
45
contrib/templates/example/example_template/example.yaml
Normal file
45
contrib/templates/example/example_template/example.yaml
Normal file
@ -0,0 +1,45 @@
|
||||
heat_template_version: 2013-05-23
|
||||
|
||||
description: >
|
||||
This is just an example template. It does not produce a usable bay.
|
||||
|
||||
parameters:
|
||||
#
|
||||
# REQUIRED PARAMETERS
|
||||
#
|
||||
ssh_key_name:
|
||||
type: string
|
||||
description: name of ssh key to be provisioned on our server
|
||||
|
||||
#
|
||||
# OPTIONAL PARAMETERS
|
||||
#
|
||||
server_image:
|
||||
type: string
|
||||
default: centos-atomic
|
||||
description: glance image used to boot the server
|
||||
|
||||
server_flavor:
|
||||
type: string
|
||||
default: m1.small
|
||||
description: flavor to use when booting the server
|
||||
|
||||
resources:
|
||||
|
||||
example_server:
|
||||
type: "OS::Nova::Server"
|
||||
properties:
|
||||
image:
|
||||
get_param: server_image
|
||||
flavor:
|
||||
get_param: server_flavor
|
||||
key_name:
|
||||
get_param: ssh_key_name
|
||||
|
||||
outputs:
|
||||
|
||||
server_address:
|
||||
value: {get_attr: [example_server, accessIPv4]}
|
||||
|
||||
node_addresses:
|
||||
value: []
|
37
contrib/templates/example/setup.py
Normal file
37
contrib/templates/example/setup.py
Normal file
@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2015 Rackspace Inc.
|
||||
#
|
||||
# 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 setuptools
|
||||
|
||||
setuptools.setup(
|
||||
name="ExampleTemplate",
|
||||
version="0.1",
|
||||
packages=['example_template'],
|
||||
install_requires=['magnum'],
|
||||
package_data={
|
||||
'example_template': ['example.yaml']
|
||||
},
|
||||
author="Me",
|
||||
author_email="me@example.com",
|
||||
description="This is an Example Template",
|
||||
license="Apache",
|
||||
keywords="magnum template example",
|
||||
entry_points={
|
||||
'magnum.template_definitions': [
|
||||
'example_template = example_template:ExampleTemplate'
|
||||
]
|
||||
}
|
||||
)
|
96
magnum/cmd/template_manage.py
Normal file
96
magnum/cmd/template_manage.py
Normal file
@ -0,0 +1,96 @@
|
||||
#
|
||||
# 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.
|
||||
|
||||
"""Starter script for magnum-template-manage."""
|
||||
import operator
|
||||
|
||||
from oslo_config import cfg
|
||||
|
||||
from magnum.conductor import template_definition as tdef
|
||||
from magnum.openstack.common import cliutils
|
||||
from magnum.openstack.common import log as logging
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
|
||||
def is_enabled(name):
|
||||
return name in CONF.bay.enabled_definitions
|
||||
|
||||
|
||||
def print_rows(rows):
|
||||
fields = ['name', 'enabled']
|
||||
field_labels = ['Name', 'Enabled']
|
||||
|
||||
if CONF.command.details:
|
||||
fields.extend(['platform', 'os', 'coe'])
|
||||
field_labels.extend(['Platform', 'OS', 'COE'])
|
||||
if CONF.command.paths:
|
||||
fields.append('path')
|
||||
field_labels.append('Template Path')
|
||||
|
||||
formatters = dict((key, operator.itemgetter(key)) for key in fields)
|
||||
|
||||
cliutils.print_list(rows, fields,
|
||||
formatters=formatters,
|
||||
field_labels=field_labels)
|
||||
|
||||
|
||||
def list_templates():
|
||||
rows = []
|
||||
|
||||
for entry_point, cls in tdef.TemplateDefinition.load_entry_points():
|
||||
name = entry_point.name
|
||||
if ((is_enabled(name) and not CONF.command.disabled) or
|
||||
(not is_enabled(name) and not CONF.command.enabled)):
|
||||
definition = cls()
|
||||
template = dict(name=name, enabled=is_enabled(name),
|
||||
path=definition.template_path)
|
||||
|
||||
if CONF.command.details:
|
||||
for bay_type in definition.provides:
|
||||
row = dict()
|
||||
row.update(template)
|
||||
row.update(bay_type)
|
||||
rows.append(row)
|
||||
else:
|
||||
rows.append(template)
|
||||
|
||||
print_rows(rows)
|
||||
|
||||
|
||||
def add_command_parsers(subparsers):
|
||||
parser = subparsers.add_parser('list-templates')
|
||||
parser.set_defaults(func=list_templates)
|
||||
|
||||
parser.add_argument('-d', '--details', action='store_true',
|
||||
help='display the bay types provided by each template')
|
||||
parser.add_argument('-p', '--paths', action='store_true',
|
||||
help='display the path to each template file')
|
||||
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument('--enabled', action='store_true',
|
||||
help="display only enabled templates")
|
||||
group.add_argument('--disabled', action='store_true',
|
||||
help="display only disabled templates")
|
||||
|
||||
|
||||
def main():
|
||||
command_opt = cfg.SubCommandOpt('command',
|
||||
title='Command',
|
||||
help='Available commands',
|
||||
handler=add_command_parsers)
|
||||
CONF.register_cli_opt(command_opt)
|
||||
|
||||
CONF(project='magnum')
|
||||
CONF.command.func()
|
@ -415,3 +415,15 @@ class ContainerException(Exception):
|
||||
class NotSupported(MagnumException):
|
||||
message = _("%(operation)s is not supported.")
|
||||
code = 400
|
||||
|
||||
|
||||
class BayTypeNotSupported(MagnumException):
|
||||
message = _("Bay type (%(platform)s, %(os)s, %(coe)s) not supported.")
|
||||
|
||||
|
||||
class BayTypeNotEnabled(MagnumException):
|
||||
message = _("Bay type (%(platform)s, %(os)s, %(coe)s) not enabled.")
|
||||
|
||||
|
||||
class RequiredParameterNotProvided(MagnumException):
|
||||
message = _("Required parameter %(heat_param)s not provided.")
|
||||
|
@ -12,17 +12,14 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import requests
|
||||
import uuid
|
||||
|
||||
from heatclient.common import template_utils
|
||||
from heatclient import exc
|
||||
from oslo_config import cfg
|
||||
|
||||
from magnum.common import clients
|
||||
from magnum.common import exception
|
||||
from magnum.common import paths
|
||||
from magnum.common import short_id
|
||||
from magnum.conductor import template_definition as tdef
|
||||
from magnum import objects
|
||||
from magnum.openstack.common._i18n import _
|
||||
from magnum.openstack.common._i18n import _LE
|
||||
@ -32,17 +29,9 @@ from magnum.openstack.common import loopingcall
|
||||
|
||||
|
||||
k8s_heat_opts = [
|
||||
cfg.StrOpt('template_path',
|
||||
default=paths.basedir_def('templates/heat-kubernetes/'
|
||||
'kubecluster.yaml'),
|
||||
help=_(
|
||||
'Location of template to build a k8s cluster. ')),
|
||||
cfg.StrOpt('cluster_type',
|
||||
default=None,
|
||||
default='fedora-atomic',
|
||||
help=_('Cluster types are fedora-atomic, coreos, ironic.')),
|
||||
cfg.StrOpt('discovery_token_url',
|
||||
default=None,
|
||||
help=_('coreos discovery token url.')),
|
||||
cfg.IntOpt('max_attempts',
|
||||
default=2000,
|
||||
help=('Number of attempts to query the Heat stack for '
|
||||
@ -60,62 +49,23 @@ cfg.CONF.register_opts(k8s_heat_opts, group='k8s_heat')
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_coreos_token(context):
|
||||
if cfg.CONF.k8s_heat.cluster_type == 'coreos':
|
||||
token = ""
|
||||
discovery_url = cfg.CONF.k8s_heat.discovery_token_url
|
||||
if discovery_url:
|
||||
coreos_token_url = requests.get(discovery_url)
|
||||
token = str(coreos_token_url.text.split('/')[3])
|
||||
else:
|
||||
token = uuid.uuid4().hex
|
||||
return token
|
||||
else:
|
||||
return None
|
||||
|
||||
|
||||
def _extract_bay_definition(context, bay):
|
||||
def _extract_template_definition(context, bay):
|
||||
baymodel = objects.BayModel.get_by_uuid(context, bay.baymodel_id)
|
||||
token = _get_coreos_token(context)
|
||||
bay_definition = {
|
||||
'ssh_key_name': baymodel.keypair_id,
|
||||
'external_network_id': baymodel.external_network_id,
|
||||
}
|
||||
if token is not None:
|
||||
bay_definition['token'] = token
|
||||
if baymodel.dns_nameserver:
|
||||
bay_definition['dns_nameserver'] = baymodel.dns_nameserver
|
||||
if baymodel.image_id:
|
||||
bay_definition['server_image'] = baymodel.image_id
|
||||
if baymodel.flavor_id:
|
||||
bay_definition['server_flavor'] = baymodel.flavor_id
|
||||
if baymodel.master_flavor_id:
|
||||
bay_definition['master_flavor'] = baymodel.master_flavor_id
|
||||
# TODO(yuanying): Add below lines if apiserver_port parameter is supported
|
||||
# if baymodel.apiserver_port:
|
||||
# bay_definition['apiserver_port'] = baymodel.apiserver_port
|
||||
if bay.node_count is not None:
|
||||
bay_definition['number_of_minions'] = str(bay.node_count)
|
||||
if baymodel.docker_volume_size:
|
||||
bay_definition['docker_volume_size'] = baymodel.docker_volume_size
|
||||
if baymodel.fixed_network:
|
||||
bay_definition['fixed_network'] = baymodel.fixed_network
|
||||
if baymodel.ssh_authorized_key:
|
||||
bay_definition['ssh_authorized_key'] = baymodel.ssh_authorized_key
|
||||
|
||||
return bay_definition
|
||||
definition = tdef.TemplateDefinition.get_template_definition('vm',
|
||||
cfg.CONF.k8s_heat.cluster_type,
|
||||
'kubernetes')
|
||||
return definition.extract_definition(baymodel, bay)
|
||||
|
||||
|
||||
def _create_stack(context, osc, bay):
|
||||
bay_definition = _extract_bay_definition(context, bay)
|
||||
template_path, heat_params = _extract_template_definition(context, bay)
|
||||
|
||||
tpl_files, template = template_utils.get_template_contents(
|
||||
cfg.CONF.k8s_heat.template_path)
|
||||
tpl_files, template = template_utils.get_template_contents(template_path)
|
||||
# Make sure no duplicate stack name
|
||||
stack_name = '%s-%s' % (bay.name, short_id.generate_id())
|
||||
fields = {
|
||||
'stack_name': stack_name,
|
||||
'parameters': bay_definition,
|
||||
'parameters': heat_params,
|
||||
'template': template,
|
||||
'files': dict(list(tpl_files.items()))
|
||||
}
|
||||
@ -125,12 +75,11 @@ def _create_stack(context, osc, bay):
|
||||
|
||||
|
||||
def _update_stack(context, osc, bay):
|
||||
bay_definition = _extract_bay_definition(context, bay)
|
||||
template_path, heat_params = _extract_template_definition(context, bay)
|
||||
|
||||
tpl_files, template = template_utils.get_template_contents(
|
||||
cfg.CONF.k8s_heat.template_path)
|
||||
tpl_files, template = template_utils.get_template_contents(template_path)
|
||||
fields = {
|
||||
'parameters': bay_definition,
|
||||
'parameters': heat_params,
|
||||
'template': template,
|
||||
'files': dict(list(tpl_files.items()))
|
||||
}
|
||||
@ -138,20 +87,11 @@ def _update_stack(context, osc, bay):
|
||||
return osc.heat().stacks.update(bay.stack_id, **fields)
|
||||
|
||||
|
||||
def _parse_stack_outputs(outputs):
|
||||
parsed_outputs = {}
|
||||
|
||||
for output in outputs:
|
||||
output_key = output["output_key"]
|
||||
output_value = output["output_value"]
|
||||
if output_key == "kube_minions_external":
|
||||
parsed_outputs["kube_minions_external"] = output_value
|
||||
if output_key == "kube_minions":
|
||||
parsed_outputs["kube_minions"] = output_value
|
||||
if output_key == "kube_master":
|
||||
parsed_outputs["kube_master"] = output_value
|
||||
|
||||
return parsed_outputs
|
||||
def _update_stack_outputs(stack, bay):
|
||||
definition = tdef.TemplateDefinition.get_template_definition('vm',
|
||||
cfg.CONF.k8s_heat.cluster_type,
|
||||
'kubernetes')
|
||||
return definition.update_outputs(stack, bay)
|
||||
|
||||
|
||||
class Handler(object):
|
||||
@ -257,9 +197,7 @@ class HeatPoller(object):
|
||||
self.bay.destroy()
|
||||
raise loopingcall.LoopingCallDone()
|
||||
if (stack.stack_status in ['CREATE_COMPLETE', 'UPDATE_COMPLETE']):
|
||||
parsed_outputs = _parse_stack_outputs(stack.outputs)
|
||||
self.bay.api_address = parsed_outputs["kube_master"]
|
||||
self.bay.node_addresses = parsed_outputs["kube_minions_external"]
|
||||
_update_stack_outputs(stack, self.bay)
|
||||
self.bay.status = stack.stack_status
|
||||
self.bay.save()
|
||||
raise loopingcall.LoopingCallDone()
|
||||
|
341
magnum/conductor/template_definition.py
Normal file
341
magnum/conductor/template_definition.py
Normal file
@ -0,0 +1,341 @@
|
||||
# Copyright 2014 Rackspace Inc. All 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 abc
|
||||
import uuid
|
||||
|
||||
from oslo_config import cfg
|
||||
from pkg_resources import iter_entry_points
|
||||
import requests
|
||||
import six
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.openstack.common._i18n import _
|
||||
|
||||
from magnum.common import paths
|
||||
|
||||
|
||||
template_def_opts = [
|
||||
cfg.StrOpt('k8s_atomic_template_path',
|
||||
default=paths.basedir_def('templates/heat-kubernetes/'
|
||||
'kubecluster.yaml'),
|
||||
deprecated_name='template_path',
|
||||
deprecated_group='k8s_heat',
|
||||
help=_(
|
||||
'Location of template to build a k8s cluster on atomic. ')),
|
||||
cfg.StrOpt('k8s_coreos_template_path',
|
||||
default=paths.basedir_def('templates/heat-kubernetes/'
|
||||
'kubecluster-coreos.yaml'),
|
||||
help=_(
|
||||
'Location of template to build a k8s cluster on coreos. ')),
|
||||
cfg.StrOpt('coreos_discovery_token_url',
|
||||
default=None,
|
||||
deprecated_name='discovery_token_url',
|
||||
deprecated_group='k8s_heat',
|
||||
help=_('coreos discovery token url.')),
|
||||
cfg.ListOpt('enabled_definitions',
|
||||
default=['magnum_vm_atomic_k8s', 'magnum_vm_coreos_k8s'],
|
||||
help=_('Enabled bay definition entry points. ')),
|
||||
]
|
||||
|
||||
cfg.CONF.register_opts(template_def_opts, group='bay')
|
||||
|
||||
|
||||
class ParameterMapping(object):
|
||||
"""A ParameterMapping is an association of a Heat parameter name with
|
||||
an attribute on a Bay, Baymodel, or both.
|
||||
|
||||
In the case of both baymodel_attr and bay_attr being set, the Baymodel
|
||||
will be checked first and then Bay if the attribute isn't set on the
|
||||
Baymodel.
|
||||
|
||||
Parameters can also be set as 'required'. If a required parameter
|
||||
isn't set, a RequiredArgumentNotProvided exception will be raised.
|
||||
"""
|
||||
def __init__(self, heat_param, baymodel_attr=None,
|
||||
bay_attr=None, required=False,
|
||||
param_type=lambda x: x):
|
||||
self.heat_param = heat_param
|
||||
self.baymodel_attr = baymodel_attr
|
||||
self.bay_attr = bay_attr
|
||||
self.required = required
|
||||
self.param_type = param_type
|
||||
|
||||
def set_param(self, params, baymodel, bay):
|
||||
value = None
|
||||
|
||||
if (self.baymodel_attr and
|
||||
getattr(baymodel, self.baymodel_attr, None)):
|
||||
value = getattr(baymodel, self.baymodel_attr)
|
||||
elif (self.bay_attr and
|
||||
getattr(bay, self.bay_attr, None)):
|
||||
value = getattr(bay, self.bay_attr)
|
||||
elif self.required:
|
||||
kwargs = dict(heat_param=self.heat_param)
|
||||
raise exception.RequiredParameterNotProvided(**kwargs)
|
||||
|
||||
if value:
|
||||
value = self.param_type(value)
|
||||
params[self.heat_param] = value
|
||||
|
||||
|
||||
class OutputMapping(object):
|
||||
"""An OutputMapping is an association of a Heat output with a key
|
||||
Magnum understands.
|
||||
"""
|
||||
|
||||
def __init__(self, heat_output, bay_attr=None):
|
||||
self.bay_attr = bay_attr
|
||||
self.heat_output = heat_output
|
||||
|
||||
def set_output(self, stack, bay):
|
||||
for output in stack.outputs:
|
||||
if output['output_key'] == self.heat_output:
|
||||
setattr(bay, self.bay_attr, output['output_value'])
|
||||
break
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class TemplateDefinition(object):
|
||||
'''A TemplateDefinition is essentially a mapping between Magnum objects
|
||||
and Heat templates. Each TemplateDefinition has a mapping of Heat
|
||||
parameters.
|
||||
'''
|
||||
definitions = None
|
||||
provides = list()
|
||||
|
||||
def __init__(self):
|
||||
self.param_mappings = list()
|
||||
self.output_mappings = list()
|
||||
|
||||
@staticmethod
|
||||
def load_entry_points():
|
||||
for entry_point in iter_entry_points('magnum.template_definitions'):
|
||||
yield entry_point, entry_point.load()
|
||||
|
||||
@classmethod
|
||||
def get_template_definitions(cls):
|
||||
'''Retrieves bay definitions from python entry_points.
|
||||
|
||||
Example:
|
||||
|
||||
With the following classes:
|
||||
class TemplateDefinition1(TemplateDefinition):
|
||||
provides = [
|
||||
('platform1', 'os1', 'coe1')
|
||||
]
|
||||
|
||||
class TemplateDefinition2(TemplateDefinition):
|
||||
provides = [
|
||||
('platform2', 'os2', 'coe2')
|
||||
]
|
||||
|
||||
And the following entry_points:
|
||||
|
||||
magnum.template_definitions =
|
||||
template_name_1 = some.python.path:TemplateDefinition1
|
||||
template_name_2 = some.python.path:TemplateDefinition2
|
||||
|
||||
get_template_definitions will return:
|
||||
{
|
||||
(platform1, os1, coe1):
|
||||
{'template_name_1': TemplateDefinition1},
|
||||
(platform2, os2, coe2):
|
||||
{'template_name_2': TemplateDefinition2}
|
||||
}
|
||||
|
||||
:return: dict
|
||||
'''
|
||||
|
||||
if not cls.definitions:
|
||||
cls.definitions = dict()
|
||||
for entry_point, def_class in cls.load_entry_points():
|
||||
for bay_type in def_class.provides:
|
||||
bay_type_tuple = (bay_type['platform'],
|
||||
bay_type['os'],
|
||||
bay_type['coe'])
|
||||
providers = cls.definitions.setdefault(bay_type_tuple,
|
||||
dict())
|
||||
providers[entry_point.name] = def_class
|
||||
|
||||
return cls.definitions
|
||||
|
||||
@classmethod
|
||||
def get_template_definition(cls, platform, os, coe):
|
||||
'''Returns the enabled TemplateDefinition class for the provided
|
||||
bay_type.
|
||||
|
||||
With the following classes:
|
||||
class TemplateDefinition1(TemplateDefinition):
|
||||
provides = [
|
||||
('platform1', 'os1', 'coe1')
|
||||
]
|
||||
|
||||
class TemplateDefinition2(TemplateDefinition):
|
||||
provides = [
|
||||
('platform2', 'os2', 'coe2')
|
||||
]
|
||||
|
||||
And the following entry_points:
|
||||
|
||||
magnum.template_definitions =
|
||||
template_name_1 = some.python.path:TemplateDefinition1
|
||||
template_name_2 = some.python.path:TemplateDefinition2
|
||||
|
||||
get_template_name_1_definition('platform2', 'os2', 'coe2')
|
||||
will return: TemplateDefinition2
|
||||
|
||||
:param platform: The platform the bay definition will build on
|
||||
:param os: The operation system the bay definition will build on
|
||||
:param coe: The Container Orchestration Environment the bay will
|
||||
produce
|
||||
|
||||
:return: class
|
||||
'''
|
||||
|
||||
definition_map = cls.get_template_definitions()
|
||||
bay_type = (platform, os, coe)
|
||||
|
||||
if bay_type not in definition_map:
|
||||
raise exception.BayTypeNotSupported(platform=platform, os=os,
|
||||
coe=coe)
|
||||
type_definitions = definition_map[bay_type]
|
||||
|
||||
for name in cfg.CONF.bay.enabled_definitions:
|
||||
if name in type_definitions:
|
||||
return type_definitions[name]()
|
||||
|
||||
raise exception.BayTypeNotEnabled(platform=platform, os=os, coe=coe)
|
||||
|
||||
def add_parameter(self, *args, **kwargs):
|
||||
param = ParameterMapping(*args, **kwargs)
|
||||
self.param_mappings.append(param)
|
||||
|
||||
def add_output(self, *args, **kwargs):
|
||||
output = OutputMapping(*args, **kwargs)
|
||||
self.output_mappings.append(output)
|
||||
|
||||
def get_params(self, baymodel, bay, extra_params=None):
|
||||
"""Pulls template parameters from Baymodel and Bay.
|
||||
|
||||
:param baymodel: Baymodel to pull template parameters from
|
||||
:param bay: Bay to pull template parameters from
|
||||
:param extra_params: Any extra params to provide to the template
|
||||
|
||||
:return: dict of template parameters
|
||||
"""
|
||||
template_params = dict()
|
||||
|
||||
for mapping in self.param_mappings:
|
||||
mapping.set_param(template_params, baymodel, bay)
|
||||
|
||||
if extra_params:
|
||||
template_params.update(extra_params)
|
||||
|
||||
return template_params
|
||||
|
||||
def update_outputs(self, stack, bay):
|
||||
for output in self.output_mappings:
|
||||
output.set_output(stack, bay)
|
||||
|
||||
@abc.abstractproperty
|
||||
def template_path(self):
|
||||
pass
|
||||
|
||||
def extract_definition(self, baymodel, bay, extra_params=None):
|
||||
return self.template_path, self.get_params(baymodel, bay,
|
||||
extra_params=extra_params)
|
||||
|
||||
|
||||
class BaseTemplateDefinition(TemplateDefinition):
|
||||
def __init__(self):
|
||||
super(BaseTemplateDefinition, self).__init__()
|
||||
self.add_parameter('ssh_key_name',
|
||||
baymodel_attr='keypair_id',
|
||||
required=True)
|
||||
|
||||
self.add_parameter('server_image',
|
||||
baymodel_attr='image_id')
|
||||
self.add_parameter('server_flavor',
|
||||
baymodel_attr='flavor_id')
|
||||
|
||||
|
||||
class AtomicK8sTemplateDefinition(BaseTemplateDefinition):
|
||||
provides = [
|
||||
{'platform': 'vm', 'os': 'fedora-atomic', 'coe': 'kubernetes'},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(AtomicK8sTemplateDefinition, self).__init__()
|
||||
self.add_parameter('external_network_id',
|
||||
baymodel_attr='external_network_id',
|
||||
required=True)
|
||||
|
||||
self.add_parameter('dns_nameserver',
|
||||
baymodel_attr='dns_nameserver')
|
||||
self.add_parameter('master_flavor',
|
||||
baymodel_attr='master_flavor_id')
|
||||
self.add_parameter('fixed_network',
|
||||
baymodel_attr='fixed_network')
|
||||
self.add_parameter('number_of_minions',
|
||||
bay_attr='node_count',
|
||||
param_type=str)
|
||||
self.add_parameter('docker_volume_size',
|
||||
baymodel_attr='docker_volume_size')
|
||||
# TODO(yuanying): Add below lines if apiserver_port parameter
|
||||
# is supported
|
||||
# self.add_parameter('apiserver_port',
|
||||
# baymodel_attr='apiserver_port')
|
||||
|
||||
self.add_output('kube_master',
|
||||
bay_attr='api_address')
|
||||
self.add_output('kube_minions_external',
|
||||
bay_attr='node_addresses')
|
||||
|
||||
@property
|
||||
def template_path(self):
|
||||
return cfg.CONF.bay.k8s_atomic_template_path
|
||||
|
||||
|
||||
class CoreOSK8sTemplateDefinition(AtomicK8sTemplateDefinition):
|
||||
provides = [
|
||||
{'platform': 'vm', 'os': 'coreos', 'coe': 'kubernetes'},
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
super(CoreOSK8sTemplateDefinition, self).__init__()
|
||||
self.add_parameter('ssh_authorized_key',
|
||||
baymodel_attr='ssh_authorized_key')
|
||||
|
||||
@staticmethod
|
||||
def get_token():
|
||||
discovery_url = cfg.CONF.bay.coreos_discovery_token_url
|
||||
if discovery_url:
|
||||
coreos_token_url = requests.get(discovery_url)
|
||||
token = str(coreos_token_url.text.split('/')[3])
|
||||
else:
|
||||
token = uuid.uuid4().hex
|
||||
return token
|
||||
|
||||
def get_params(self, baymodel, bay, extra_params=None):
|
||||
if not extra_params:
|
||||
extra_params = dict()
|
||||
|
||||
extra_params['token'] = self.get_token()
|
||||
|
||||
return super(CoreOSK8sTemplateDefinition,
|
||||
self).get_params(baymodel, bay, extra_params=extra_params)
|
||||
|
||||
@property
|
||||
def template_path(self):
|
||||
return cfg.CONF.bay.k8s_coreos_template_path
|
271
magnum/openstack/common/cliutils.py
Normal file
271
magnum/openstack/common/cliutils.py
Normal file
@ -0,0 +1,271 @@
|
||||
# Copyright 2012 Red Hat, Inc.
|
||||
#
|
||||
# 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.
|
||||
|
||||
# W0603: Using the global statement
|
||||
# W0621: Redefining name %s from outer scope
|
||||
# pylint: disable=W0603,W0621
|
||||
|
||||
from __future__ import print_function
|
||||
|
||||
import getpass
|
||||
import inspect
|
||||
import os
|
||||
import sys
|
||||
import textwrap
|
||||
|
||||
from oslo_utils import encodeutils
|
||||
from oslo_utils import strutils
|
||||
import prettytable
|
||||
import six
|
||||
from six import moves
|
||||
|
||||
from magnum.openstack.common._i18n import _
|
||||
|
||||
|
||||
class MissingArgs(Exception):
|
||||
"""Supplied arguments are not sufficient for calling a function."""
|
||||
def __init__(self, missing):
|
||||
self.missing = missing
|
||||
msg = _("Missing arguments: %s") % ", ".join(missing)
|
||||
super(MissingArgs, self).__init__(msg)
|
||||
|
||||
|
||||
def validate_args(fn, *args, **kwargs):
|
||||
"""Check that the supplied args are sufficient for calling a function.
|
||||
|
||||
>>> validate_args(lambda a: None)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): a
|
||||
>>> validate_args(lambda a, b, c, d: None, 0, c=1)
|
||||
Traceback (most recent call last):
|
||||
...
|
||||
MissingArgs: Missing argument(s): b, d
|
||||
|
||||
:param fn: the function to check
|
||||
:param arg: the positional arguments supplied
|
||||
:param kwargs: the keyword arguments supplied
|
||||
"""
|
||||
argspec = inspect.getargspec(fn)
|
||||
|
||||
num_defaults = len(argspec.defaults or [])
|
||||
required_args = argspec.args[:len(argspec.args) - num_defaults]
|
||||
|
||||
def isbound(method):
|
||||
return getattr(method, '__self__', None) is not None
|
||||
|
||||
if isbound(fn):
|
||||
required_args.pop(0)
|
||||
|
||||
missing = [arg for arg in required_args if arg not in kwargs]
|
||||
missing = missing[len(args):]
|
||||
if missing:
|
||||
raise MissingArgs(missing)
|
||||
|
||||
|
||||
def arg(*args, **kwargs):
|
||||
"""Decorator for CLI args.
|
||||
|
||||
Example:
|
||||
|
||||
>>> @arg("name", help="Name of the new entity")
|
||||
... def entity_create(args):
|
||||
... pass
|
||||
"""
|
||||
def _decorator(func):
|
||||
add_arg(func, *args, **kwargs)
|
||||
return func
|
||||
return _decorator
|
||||
|
||||
|
||||
def env(*args, **kwargs):
|
||||
"""Returns the first environment variable set.
|
||||
|
||||
If all are empty, defaults to '' or keyword arg `default`.
|
||||
"""
|
||||
for arg in args:
|
||||
value = os.environ.get(arg)
|
||||
if value:
|
||||
return value
|
||||
return kwargs.get('default', '')
|
||||
|
||||
|
||||
def add_arg(func, *args, **kwargs):
|
||||
"""Bind CLI arguments to a shell.py `do_foo` function."""
|
||||
|
||||
if not hasattr(func, 'arguments'):
|
||||
func.arguments = []
|
||||
|
||||
# NOTE(sirp): avoid dups that can occur when the module is shared across
|
||||
# tests.
|
||||
if (args, kwargs) not in func.arguments:
|
||||
# Because of the semantics of decorator composition if we just append
|
||||
# to the options list positional options will appear to be backwards.
|
||||
func.arguments.insert(0, (args, kwargs))
|
||||
|
||||
|
||||
def unauthenticated(func):
|
||||
"""Adds 'unauthenticated' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
>>> @unauthenticated
|
||||
... def mymethod(f):
|
||||
... pass
|
||||
"""
|
||||
func.unauthenticated = True
|
||||
return func
|
||||
|
||||
|
||||
def isunauthenticated(func):
|
||||
"""Checks if the function does not require authentication.
|
||||
|
||||
Mark such functions with the `@unauthenticated` decorator.
|
||||
|
||||
:returns: bool
|
||||
"""
|
||||
return getattr(func, 'unauthenticated', False)
|
||||
|
||||
|
||||
def print_list(objs, fields, formatters=None, sortby_index=0,
|
||||
mixed_case_fields=None, field_labels=None):
|
||||
"""Print a list or objects as a table, one row per object.
|
||||
|
||||
:param objs: iterable of :class:`Resource`
|
||||
:param fields: attributes that correspond to columns, in order
|
||||
:param formatters: `dict` of callables for field formatting
|
||||
:param sortby_index: index of the field for sorting table rows
|
||||
:param mixed_case_fields: fields corresponding to object attributes that
|
||||
have mixed case names (e.g., 'serverId')
|
||||
:param field_labels: Labels to use in the heading of the table, default to
|
||||
fields.
|
||||
"""
|
||||
formatters = formatters or {}
|
||||
mixed_case_fields = mixed_case_fields or []
|
||||
field_labels = field_labels or fields
|
||||
if len(field_labels) != len(fields):
|
||||
raise ValueError(_("Field labels list %(labels)s has different number "
|
||||
"of elements than fields list %(fields)s"),
|
||||
{'labels': field_labels, 'fields': fields})
|
||||
|
||||
if sortby_index is None:
|
||||
kwargs = {}
|
||||
else:
|
||||
kwargs = {'sortby': field_labels[sortby_index]}
|
||||
pt = prettytable.PrettyTable(field_labels)
|
||||
pt.align = 'l'
|
||||
|
||||
for o in objs:
|
||||
row = []
|
||||
for field in fields:
|
||||
if field in formatters:
|
||||
row.append(formatters[field](o))
|
||||
else:
|
||||
if field in mixed_case_fields:
|
||||
field_name = field.replace(' ', '_')
|
||||
else:
|
||||
field_name = field.lower().replace(' ', '_')
|
||||
data = getattr(o, field_name, '')
|
||||
row.append(data)
|
||||
pt.add_row(row)
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string(**kwargs)))
|
||||
|
||||
|
||||
def print_dict(dct, dict_property="Property", wrap=0):
|
||||
"""Print a `dict` as a table of two columns.
|
||||
|
||||
:param dct: `dict` to print
|
||||
:param dict_property: name of the first column
|
||||
:param wrap: wrapping for the second column
|
||||
"""
|
||||
pt = prettytable.PrettyTable([dict_property, 'Value'])
|
||||
pt.align = 'l'
|
||||
for k, v in six.iteritems(dct):
|
||||
# convert dict to str to check length
|
||||
if isinstance(v, dict):
|
||||
v = six.text_type(v)
|
||||
if wrap > 0:
|
||||
v = textwrap.fill(six.text_type(v), wrap)
|
||||
# if value has a newline, add in multiple rows
|
||||
# e.g. fault with stacktrace
|
||||
if v and isinstance(v, six.string_types) and r'\n' in v:
|
||||
lines = v.strip().split(r'\n')
|
||||
col1 = k
|
||||
for line in lines:
|
||||
pt.add_row([col1, line])
|
||||
col1 = ''
|
||||
else:
|
||||
pt.add_row([k, v])
|
||||
|
||||
if six.PY3:
|
||||
print(encodeutils.safe_encode(pt.get_string()).decode())
|
||||
else:
|
||||
print(encodeutils.safe_encode(pt.get_string()))
|
||||
|
||||
|
||||
def get_password(max_password_prompts=3):
|
||||
"""Read password from TTY."""
|
||||
verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD"))
|
||||
pw = None
|
||||
if hasattr(sys.stdin, "isatty") and sys.stdin.isatty():
|
||||
# Check for Ctrl-D
|
||||
try:
|
||||
for __ in moves.range(max_password_prompts):
|
||||
pw1 = getpass.getpass("OS Password: ")
|
||||
if verify:
|
||||
pw2 = getpass.getpass("Please verify: ")
|
||||
else:
|
||||
pw2 = pw1
|
||||
if pw1 == pw2 and pw1:
|
||||
pw = pw1
|
||||
break
|
||||
except EOFError:
|
||||
pass
|
||||
return pw
|
||||
|
||||
|
||||
def service_type(stype):
|
||||
"""Adds 'service_type' attribute to decorated function.
|
||||
|
||||
Usage:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
@service_type('volume')
|
||||
def mymethod(f):
|
||||
...
|
||||
"""
|
||||
def inner(f):
|
||||
f.service_type = stype
|
||||
return f
|
||||
return inner
|
||||
|
||||
|
||||
def get_service_type(f):
|
||||
"""Retrieves service type from function."""
|
||||
return getattr(f, 'service_type', None)
|
||||
|
||||
|
||||
def pretty_choice_list(l):
|
||||
return ', '.join("'%s'" % i for i in l)
|
||||
|
||||
|
||||
def exit(msg=''):
|
||||
if msg:
|
||||
print (msg, file=sys.stderr)
|
||||
sys.exit(1)
|
@ -23,6 +23,7 @@ import magnum.common.magnum_keystoneclient
|
||||
import magnum.conductor.config
|
||||
import magnum.conductor.handlers.bay_k8s_heat
|
||||
import magnum.conductor.handlers.docker_conductor
|
||||
import magnum.conductor.template_definition
|
||||
import magnum.db.sqlalchemy.models
|
||||
import magnum.openstack.common.eventlet_backdoor
|
||||
import magnum.openstack.common.log
|
||||
@ -46,6 +47,7 @@ def list_opts():
|
||||
magnum.openstack.common.periodic_task.periodic_opts,
|
||||
)),
|
||||
('api', magnum.api.app.API_SERVICE_OPTS),
|
||||
('bay', magnum.conductor.template_definition.template_def_opts),
|
||||
('conductor', magnum.conductor.config.SERVICE_OPTS),
|
||||
('database', magnum.db.sqlalchemy.models.sql_opts),
|
||||
('docker', magnum.conductor.handlers.docker_conductor.docker_opts),
|
||||
|
@ -52,14 +52,15 @@ class TestBayK8sHeat(base.TestCase):
|
||||
}
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition(self,
|
||||
def test_extract_template_definition(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel = objects.BayModel(self.context, **self.baymodel_dict)
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -71,29 +72,29 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('requests.get')
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_coreos_with_disovery(self,
|
||||
def test_extract_template_definition_coreos_with_disovery(self,
|
||||
mock_objects_baymodel_get_by_uuid,
|
||||
reqget):
|
||||
cfg.CONF.set_override('cluster_type',
|
||||
'coreos',
|
||||
group='k8s_heat')
|
||||
cfg.CONF.set_override('discovery_token_url',
|
||||
cfg.CONF.set_override('coreos_discovery_token_url',
|
||||
'http://tokentest',
|
||||
group='k8s_heat')
|
||||
group='bay')
|
||||
mock_req = mock.MagicMock(text='/h1/h2/h3')
|
||||
reqget.return_value = mock_req
|
||||
baymodel = objects.BayModel(self.context, **self.baymodel_dict)
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -108,27 +109,28 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
'token': 'h3'
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('uuid.uuid4')
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_coreos_no_discoveryurl(self,
|
||||
def test_extract_template_definition_coreos_no_discoveryurl(self,
|
||||
mock_objects_baymodel_get_by_uuid,
|
||||
mock_uuid):
|
||||
cfg.CONF.set_override('cluster_type',
|
||||
'coreos',
|
||||
group='k8s_heat')
|
||||
cfg.CONF.set_override('discovery_token_url',
|
||||
cfg.CONF.set_override('coreos_discovery_token_url',
|
||||
None,
|
||||
group='k8s_heat')
|
||||
group='bay')
|
||||
mock_uuid.return_value = mock.MagicMock(
|
||||
hex='ba3d1866282848ddbedc76112110c208')
|
||||
baymodel = objects.BayModel(self.context, **self.baymodel_dict)
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -143,10 +145,10 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
'token': 'ba3d1866282848ddbedc76112110c208'
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_dns(self,
|
||||
def test_extract_template_definition_without_dns(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['dns_nameserver'] = None
|
||||
@ -154,8 +156,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -165,13 +168,12 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'master_flavor': 'master_flavor_id',
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
'docker_volume_size': 20
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_server_image(self,
|
||||
def test_extract_template_definition_without_server_image(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['image_id'] = None
|
||||
@ -179,8 +181,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -190,13 +193,12 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'master_flavor': 'master_flavor_id',
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
'docker_volume_size': 20
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_server_flavor(self,
|
||||
def test_extract_template_definition_without_server_flavor(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['flavor_id'] = None
|
||||
@ -204,8 +206,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -216,12 +219,11 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_docker_volume_size(self,
|
||||
def test_extract_template_definition_without_docker_volume_size(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['docker_volume_size'] = None
|
||||
@ -229,8 +231,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -241,12 +244,11 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'fixed_network': 'private',
|
||||
'master_flavor': 'master_flavor_id',
|
||||
'number_of_minions': '1',
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_fixed_network(self,
|
||||
def test_extract_template_definition_without_fixed_network(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['fixed_network'] = None
|
||||
@ -254,8 +256,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -266,12 +269,11 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'server_flavor': 'flavor_id',
|
||||
'number_of_minions': '1',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_master_flavor(self,
|
||||
def test_extract_template_definition_without_master_flavor(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['master_flavor_id'] = None
|
||||
@ -279,8 +281,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -291,21 +294,24 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_ssh_authorized_key(self,
|
||||
def test_extract_template_definition_without_ssh_authorized_key(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
cfg.CONF.set_override('cluster_type',
|
||||
'coreos',
|
||||
group='k8s_heat')
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['ssh_authorized_key'] = None
|
||||
baymodel = objects.BayModel(self.context, **baymodel_dict)
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -318,10 +324,12 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertIn('token', definition)
|
||||
del definition['token']
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_apiserver_port(self,
|
||||
def test_extract_template_definition_without_apiserver_port(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
baymodel_dict = self.baymodel_dict
|
||||
baymodel_dict['apiserver_port'] = None
|
||||
@ -329,8 +337,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **self.bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -342,12 +351,11 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'number_of_minions': '1',
|
||||
'fixed_network': 'private',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
@patch('magnum.objects.BayModel.get_by_uuid')
|
||||
def test_extract_bay_definition_without_node_count(self,
|
||||
def test_extract_template_definition_without_node_count(self,
|
||||
mock_objects_baymodel_get_by_uuid):
|
||||
bay_dict = self.bay_dict
|
||||
bay_dict['node_count'] = None
|
||||
@ -355,8 +363,9 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_objects_baymodel_get_by_uuid.return_value = baymodel
|
||||
bay = objects.Bay(self.context, **bay_dict)
|
||||
|
||||
bay_definition = bay_k8s_heat._extract_bay_definition(self.context,
|
||||
bay)
|
||||
(template_path,
|
||||
definition) = bay_k8s_heat._extract_template_definition(self.context,
|
||||
bay)
|
||||
|
||||
expected = {
|
||||
'ssh_key_name': 'keypair_id',
|
||||
@ -367,46 +376,40 @@ class TestBayK8sHeat(base.TestCase):
|
||||
'fixed_network': 'private',
|
||||
'master_flavor': 'master_flavor_id',
|
||||
'docker_volume_size': 20,
|
||||
'ssh_authorized_key': 'ssh_authorized_key',
|
||||
}
|
||||
self.assertEqual(expected, bay_definition)
|
||||
self.assertEqual(expected, definition)
|
||||
|
||||
def test_parse_stack_outputs(self):
|
||||
def test_update_stack_outputs(self):
|
||||
expected_api_address = 'api_address'
|
||||
expected_minion_address = ['minion', 'address']
|
||||
expected_minion_external_address = ['ex_minion', 'address']
|
||||
expected_return_value = {
|
||||
'kube_master': expected_api_address,
|
||||
'kube_minions': expected_minion_address,
|
||||
'kube_minions_external': expected_minion_external_address
|
||||
}
|
||||
expected_node_addresses = ['ex_minion', 'address']
|
||||
|
||||
outputs = [
|
||||
{
|
||||
"output_value": expected_minion_external_address,
|
||||
"output_value": expected_node_addresses,
|
||||
"description": "No description given",
|
||||
"output_key": "kube_minions_external"
|
||||
},
|
||||
{
|
||||
"output_value": expected_minion_address,
|
||||
"description": "No description given",
|
||||
"output_key": "kube_minions"
|
||||
},
|
||||
{
|
||||
"output_value": expected_api_address,
|
||||
"description": "No description given",
|
||||
"output_key": "kube_master"
|
||||
}
|
||||
]
|
||||
mock_stack = mock.MagicMock()
|
||||
mock_stack.outputs = outputs
|
||||
mock_bay = mock.MagicMock()
|
||||
|
||||
parsed_outputs = bay_k8s_heat._parse_stack_outputs(outputs)
|
||||
self.assertEqual(expected_return_value, parsed_outputs)
|
||||
bay_k8s_heat._update_stack_outputs(mock_stack, mock_bay)
|
||||
|
||||
self.assertEqual(mock_bay.api_address, expected_api_address)
|
||||
self.assertEqual(mock_bay.node_addresses, expected_node_addresses)
|
||||
|
||||
@patch('magnum.common.short_id.generate_id')
|
||||
@patch('heatclient.common.template_utils.get_template_contents')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat._extract_bay_definition')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat'
|
||||
'._extract_template_definition')
|
||||
def test_create_stack(self,
|
||||
mock_extract_bay_definition,
|
||||
mock_extract_template_definition,
|
||||
mock_get_template_contents,
|
||||
mock_generate_id):
|
||||
|
||||
@ -420,7 +423,8 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_tpl_files.items.return_value = exptected_files
|
||||
mock_get_template_contents.return_value = [
|
||||
mock_tpl_files, expected_template_contents]
|
||||
mock_extract_bay_definition.return_value = {}
|
||||
mock_extract_template_definition.return_value = ('template/path',
|
||||
{})
|
||||
mock_heat_client = mock.MagicMock()
|
||||
mock_osc = mock.MagicMock()
|
||||
mock_osc.heat.return_value = mock_heat_client
|
||||
@ -438,9 +442,10 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_heat_client.stacks.create.assert_called_once_with(**expected_args)
|
||||
|
||||
@patch('heatclient.common.template_utils.get_template_contents')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat._extract_bay_definition')
|
||||
@patch('magnum.conductor.handlers.bay_k8s_heat'
|
||||
'._extract_template_definition')
|
||||
def test_update_stack(self,
|
||||
mock_extract_bay_definition,
|
||||
mock_extract_template_definition,
|
||||
mock_get_template_contents):
|
||||
|
||||
mock_stack_id = 'xx-xx-xx-xx'
|
||||
@ -451,7 +456,8 @@ class TestBayK8sHeat(base.TestCase):
|
||||
mock_tpl_files.items.return_value = exptected_files
|
||||
mock_get_template_contents.return_value = [
|
||||
mock_tpl_files, expected_template_contents]
|
||||
mock_extract_bay_definition.return_value = {}
|
||||
mock_extract_template_definition.return_value = ('template/path',
|
||||
{})
|
||||
mock_heat_client = mock.MagicMock()
|
||||
mock_osc = mock.MagicMock()
|
||||
mock_osc.heat.return_value = mock_heat_client
|
||||
|
71
magnum/tests/unit/conductor/test_template_definition.py
Normal file
71
magnum/tests/unit/conductor/test_template_definition.py
Normal file
@ -0,0 +1,71 @@
|
||||
# Copyright 2015 Rackspace Inc. All 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 mock
|
||||
from oslo_config import cfg
|
||||
|
||||
from magnum.common import exception
|
||||
from magnum.conductor import template_definition as tdef
|
||||
from magnum.tests import base
|
||||
|
||||
|
||||
class TemplateDefinitionTestCase(base.TestCase):
|
||||
def test_get_template_definitions(self):
|
||||
defs = tdef.TemplateDefinition.get_template_definitions()
|
||||
|
||||
vm_atomic_k8s = defs[('vm', 'fedora-atomic', 'kubernetes')]
|
||||
vm_coreos_k8s = defs[('vm', 'coreos', 'kubernetes')]
|
||||
|
||||
self.assertEqual(len(vm_atomic_k8s), 1)
|
||||
self.assertEqual(vm_atomic_k8s['magnum_vm_atomic_k8s'],
|
||||
tdef.AtomicK8sTemplateDefinition)
|
||||
self.assertEqual(len(vm_coreos_k8s), 1)
|
||||
self.assertEqual(vm_coreos_k8s['magnum_vm_coreos_k8s'],
|
||||
tdef.CoreOSK8sTemplateDefinition)
|
||||
|
||||
def test_get_vm_atomic_kubernetes_definition(self):
|
||||
definition = tdef.TemplateDefinition.get_template_definition('vm',
|
||||
'fedora-atomic', 'kubernetes')
|
||||
|
||||
self.assertIsInstance(definition,
|
||||
tdef.AtomicK8sTemplateDefinition)
|
||||
|
||||
def test_get_vm_coreos_kubernetes_definition(self):
|
||||
definition = tdef.TemplateDefinition.get_template_definition('vm',
|
||||
'coreos', 'kubernetes')
|
||||
|
||||
self.assertIsInstance(definition,
|
||||
tdef.CoreOSK8sTemplateDefinition)
|
||||
|
||||
def test_get_definition_not_supported(self):
|
||||
self.assertRaises(exception.BayTypeNotSupported,
|
||||
tdef.TemplateDefinition.get_template_definition,
|
||||
'vm', 'not_supported', 'kubernetes')
|
||||
|
||||
def test_get_definition_not_enabled(self):
|
||||
cfg.CONF.set_override('enabled_definitions',
|
||||
['magnum_vm_atomic_k8s'],
|
||||
group='bay')
|
||||
self.assertRaises(exception.BayTypeNotEnabled,
|
||||
tdef.TemplateDefinition.get_template_definition,
|
||||
'vm', 'coreos', 'kubernetes')
|
||||
|
||||
def test_required_param_not_set(self):
|
||||
param = tdef.ParameterMapping('test', baymodel_attr='test',
|
||||
required=True)
|
||||
mock_baymodel = mock.MagicMock()
|
||||
mock_baymodel.test = None
|
||||
|
||||
self.assertRaises(exception.RequiredParameterNotProvided,
|
||||
param.set_param, {}, mock_baymodel, None)
|
@ -1,6 +1,7 @@
|
||||
[DEFAULT]
|
||||
|
||||
# The list of modules to copy from oslo-incubator.git
|
||||
module=cliutils
|
||||
module=eventlet_backdoor
|
||||
module=loopingcall
|
||||
module=periodic_task
|
||||
|
@ -48,9 +48,14 @@ console_scripts =
|
||||
magnum-api = magnum.cmd.api:main
|
||||
magnum-conductor = magnum.cmd.conductor:main
|
||||
magnum-db-manage = magnum.cmd.db_manage:main
|
||||
magnum-template-manage = magnum.cmd.template_manage:main
|
||||
|
||||
oslo.config.opts =
|
||||
magnum = magnum.opts:list_opts
|
||||
|
||||
magnum.template_definitions =
|
||||
magnum_vm_atomic_k8s = magnum.conductor.template_definition:AtomicK8sTemplateDefinition
|
||||
magnum_vm_coreos_k8s = magnum.conductor.template_definition:CoreOSK8sTemplateDefinition
|
||||
|
||||
[wheel]
|
||||
universal = 1
|
||||
|
Loading…
Reference in New Issue
Block a user