Enhance Shaker to support basic heat environment files

Issue: at AT&T we have large complex heat stacks that all use the basic
parameter functionality found in environment heat files. Using
environment files for heat stacks is a common standard practice that
improves reusability and portability of heat stacks. We would like
to have our Shaker tests also use parameters in environment heat files.

This commit adds the ability to define an environment file that gets
passed to the heat client during stack creation in deploy_from_hot
method

This will allow us to improve the reusability of our test stacks and
improve portability when dealing with large amount of sites. While I
have provided a sample/test template that demonstrates a simple/basic
use case, we're currently using more complex patterns.

For example several of our cloud deployments use Contrail, in those
sites we need to define networks such as:
  aic_trunk_vlan_net_name: 'default-domain:{{ CONF.os_project_name }}:{{
CONF.os_project_name }}_trunk_vlan_net'

Let's assume  we have 100 sites, which all have different unique project
names usually based on region, and a centrally located repo
containing all of our Shaker tests. Rather than changing X amount
of Shaker tests times 100 sites times X projects, we can define an
environment file with the setting above, deploy the "test bundle"
(everything for the test ready to go) as-is and then based on the
shaker.cfg input (every site has one) we can be sure that we're
referencing a valid network and have a successful test.

We also have a lot of existing VNF stacks that all make use of heat
environment files, having the ability to use environment files in
Shaker greatly speeds up our ability to translate those VNFs into
Shaker based tests.

Heat environment files make everything cleaner and better organized.
It may not be apparent with a simple example, but it definitely makes a
difference when dealing with large complex heat stacks.

Change-Id: I857aa292bc89f494b5731c2b9b54742b1e2236f1
This commit is contained in:
Oded Le'Sage 2018-10-26 10:30:45 -05:00
parent b300583ebb
commit dc5d0ce4ce
8 changed files with 234 additions and 4 deletions

View File

@ -302,10 +302,14 @@ class Deployment(object):
exit(1)
merged_parameters.update(specification.get('template_parameters', {}))
env_file = specification.get('env_file', None)
if env_file is not None:
env_file = self._render_env_template(env_file, base_dir)
self.has_stack = True
stack_id = heat.create_stack(
self.openstack_client.heat, self.stack_name, rendered_template,
merged_parameters)
merged_parameters, env_file)
# get info about deployed objects
outputs = heat.get_stack_outputs(self.openstack_client.heat, stack_id)
@ -330,6 +334,20 @@ class Deployment(object):
return functools.partial(override_ip,
ip_type=override_spec.get('ip'))
# translate jinja decorations in env files
def _render_env_template(self, env_file, base_dir):
env_template = utils.read_file(env_file,
base_dir=base_dir)
env_values = {
'CONF': cfg.CONF
}
compiled_env = jinja2.Template(env_template)
rendered_env = compiled_env.render(env_values)
environment = utils.read_yaml(rendered_env)
return environment
def deploy(self, deployment, base_dir=None, server_endpoint=None):
agents = {}

View File

@ -136,12 +136,16 @@ def write_file(data, file_name, base_dir=''):
def read_yaml_file(file_name):
raw = read_file(file_name)
return read_yaml(raw)
def read_yaml(raw):
try:
parsed = yaml.safe_load(raw)
return parsed
except Exception as e:
LOG.error('Failed to parse file %(file)s in YAML format: %(err)s',
dict(file=file_name, err=e))
LOG.error('Failed to parse input %(yaml)s in YAML format: %(err)s',
dict(yaml=raw, err=e))
raise

View File

@ -22,11 +22,12 @@ from oslo_log import log as logging
LOG = logging.getLogger(__name__)
def create_stack(heat_client, stack_name, template, parameters):
def create_stack(heat_client, stack_name, template, parameters, environment):
stack_params = {
'stack_name': stack_name,
'template': template,
'parameters': parameters,
'environment': environment,
}
stack = heat_client.stacks.create(**stack_params)['stack']

View File

@ -11,6 +11,8 @@ mapping:
mapping:
template:
type: str
env_file:
type: str
agents:
type: any
accommodation:

3
shaker/scenarios/test/env/sample.env vendored Normal file
View File

@ -0,0 +1,3 @@
parameters:
custom_cidr: '192.0.0.0/16'
agent_join_timeout: {{ CONF.agent_join_timeout }}

View File

@ -0,0 +1,112 @@
heat_template_version: 2013-05-23
description:
This Heat template creates a new Neutron network, a router to the external
network and plugs instances into this new network. All instances are located
in the same L2 domain.
parameters:
image:
type: string
description: Name of image to use for servers
flavor:
type: string
description: Flavor to use for servers
external_net:
type: string
description: ID or name of external network
server_endpoint:
type: string
description: Server endpoint address
dns_nameservers:
type: comma_delimited_list
description: DNS nameservers for the subnet
custom_cidr:
type: string
description: set a cidr from the env file
agent_join_timeout:
type: string
description: shake CONF setting for agent_join_timeout
resources:
private_net:
type: OS::Neutron::Net
properties:
name: {{ unique }}_net
private_subnet:
type: OS::Neutron::Subnet
properties:
network_id: { get_resource: private_net }
cidr: { get_param: custom_cidr }
dns_nameservers: { get_param: dns_nameservers }
router:
type: OS::Neutron::Router
properties:
external_gateway_info:
network: { get_param: external_net }
router_interface:
type: OS::Neutron::RouterInterface
properties:
router_id: { get_resource: router }
subnet_id: { get_resource: private_subnet }
server_security_group:
type: OS::Neutron::SecurityGroup
properties:
rules: [
{remote_ip_prefix: 0.0.0.0/0,
protocol: tcp,
port_range_min: 1,
port_range_max: 65535},
{remote_ip_prefix: 0.0.0.0/0,
protocol: udp,
port_range_min: 1,
port_range_max: 65535},
{remote_ip_prefix: 0.0.0.0/0,
protocol: icmp}]
{% for agent in agents.values() %}
{{ agent.id }}:
type: OS::Nova::Server
properties:
name: {{ agent.id }}
image: { get_param: image }
flavor: { get_param: flavor }
availability_zone: "{{ agent.availability_zone }}"
networks:
- port: { get_resource: {{ agent.id }}_port }
user_data_format: RAW
user_data:
str_replace:
template: |
#!/bin/sh
echo $AGENT_JOIN_TIMEOUT > /tmp/check_agent_timeout
screen -dmS shaker-agent-screen shaker-agent --server-endpoint=$SERVER_ENDPOINT --agent-id=$AGENT_ID
params:
"$SERVER_ENDPOINT": { get_param: server_endpoint }
"$AGENT_ID": {{ agent.id }}
"$AGENT_JOIN_TIMEOUT": { get_param: agent_join_timeout }
{{ agent.id }}_port:
type: OS::Neutron::Port
properties:
network_id: { get_resource: private_net }
fixed_ips:
- subnet_id: { get_resource: private_subnet }
security_groups: [{ get_resource: server_security_group }]
{% endfor %}
outputs:
{% for agent in agents.values() %}
{{ agent.id }}_instance_name:
value: { get_attr: [ {{ agent.id }}, instance_name ] }
{{ agent.id }}_ip:
value: { get_attr: [ {{ agent.id }}, networks, { get_attr: [private_net, name] }, 0 ] }
{% endfor %}

View File

@ -0,0 +1,23 @@
title: Sample TCP Test with Environment File
description:
This test definition demonstrates the use of an environment file.
In this scenario Shaker launches pairs of instances in the same tenant
network. Every instance is hosted on a separate compute node, 1 compute node
is utilized. The traffic goes within the tenant network (L2 domain)
deployment:
template: l2_with_env.hot
env_file: env/sample.env
accommodation: [pair, compute_nodes: 1]
execution:
tests:
-
title: tcp
class: iperf3
sla:
- "[type == 'agent'] >> (stats.bandwidth.avg > 5000)"

View File

@ -17,12 +17,15 @@ import collections
import copy
import itertools
import mock
import os
import re
import testtools
from oslo_config import cfg
from oslo_config import fixture as config_fixture_pkg
from shaker.engine import config
from shaker.engine import deploy
from shaker.engine import utils
from shaker.openstack.clients import nova
from shaker.tests import fakes
@ -562,6 +565,70 @@ class TestDeploy(testtools.TestCase):
self.assertRaises(deploy.DeploymentException,
deployment.deploy, {'template': 'foo'})
@mock.patch('shaker.openstack.clients.heat.get_stack_outputs')
@mock.patch('shaker.openstack.clients.heat.create_stack')
@mock.patch('shaker.openstack.clients.openstack.OpenStackClient')
@mock.patch('shaker.engine.deploy.Deployment._get_compute_nodes')
def test_deploy_from_hot_with_env_file(self, nova_nodes_mock,
openstack_mock, create_stack_mock,
stack_output_mock):
test_file = 'shaker/scenarios/test/sample_with_env.yaml'
absolute_path = utils.resolve_relative_path(test_file)
scenario = utils.read_yaml_file(absolute_path)
heat_id = 'shaker_abcdefg'
server_endpoint = "127.0.0.01"
base_dir = os.path.dirname(absolute_path)
deployment = deploy.Deployment()
deployment.stack_name = heat_id
deployment.external_net = 'test-external_net'
deployment.image_name = 'test-image'
deployment.flavor_name = 'test-flavor'
deployment.dns_nameservers = '8.8.8.8'
deployment.openstack_client = openstack_mock
# read the env file to determine what cidr is set to
# minus the last digit
env_file = utils.read_file(scenario['deployment']['env_file'],
base_dir)
cidr = re.findall(r'[0-9]+(?:\.[0-9]+){2}', env_file)[0]
nova_nodes_mock.return_value = [{'host': 'host-1', 'zone': 'nova'}]
create_stack_mock.create_stack.return_value = heat_id
heat_outputs = {
heat_id + '_master_0_instance_name': 'instance-0000052f',
heat_id + '_master_0_ip': '192.0.0.3',
heat_id + '_slave_0_ip': '192.0.0.4',
heat_id + '_slave_0_instance_name': 'instance-0000052c'}
stack_output_mock.return_value = heat_outputs
expected = {
'shaker_abcdefg_master_0': {'availability_zone': 'nova:host-1',
'id': 'shaker_abcdefg_master_0',
'ip': cidr + '.3',
'mode': 'master',
'node': 'host-1',
'slave_id': 'shaker_abcdefg_slave_0',
'zone': 'nova'},
'shaker_abcdefg_slave_0': {'availability_zone': 'nova:host-1',
'id': 'shaker_abcdefg_slave_0',
'ip': cidr + '.4',
'master_id': 'shaker_abcdefg_master_0',
'mode': 'slave',
'node': 'host-1',
'zone': 'nova'}}
agents = deployment._deploy_from_hot(scenario['deployment'],
server_endpoint,
base_dir=base_dir)
self.assertEqual(expected, agents)
@mock.patch('shaker.openstack.clients.openstack.OpenStackClient')
def test_get_compute_nodes_flavor_no_extra_specs(self,
nova_client_mock):