361 lines
12 KiB
Python
361 lines
12 KiB
Python
# Copyright (c) 2015 Mirantis 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 collections
|
|
import functools
|
|
import random
|
|
|
|
import jinja2
|
|
from oslo_config import cfg
|
|
from oslo_log import log as logging
|
|
|
|
from shaker.engine import utils
|
|
from shaker.openstack.clients import heat
|
|
from shaker.openstack.clients import neutron
|
|
from shaker.openstack.clients import nova
|
|
from shaker.openstack.clients import openstack
|
|
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class DeploymentException(Exception):
|
|
pass
|
|
|
|
|
|
def prepare_for_cross_az(compute_nodes, zones):
|
|
if len(zones) != 2:
|
|
LOG.warn('cross_az is specified, but len(zones) is not 2')
|
|
return compute_nodes
|
|
|
|
masters = []
|
|
slaves = []
|
|
for node in compute_nodes:
|
|
if node['zone'] == zones[0]:
|
|
masters.append(node)
|
|
else:
|
|
slaves.append(node)
|
|
|
|
res = []
|
|
for i in range(min(len(masters), len(slaves))):
|
|
res.append(masters[i])
|
|
res.append(slaves[i])
|
|
|
|
return res
|
|
|
|
|
|
def generate_agents(compute_nodes, accommodation, unique):
|
|
density = accommodation.get('density') or 1
|
|
|
|
zones = accommodation.get('zones')
|
|
if zones:
|
|
compute_nodes = [c for c in compute_nodes if c['zone'] in zones]
|
|
if 'cross_az' in accommodation:
|
|
# sort nodes to interleave hosts from different zones
|
|
compute_nodes = prepare_for_cross_az(compute_nodes, zones)
|
|
|
|
compute_nodes_requested = accommodation.get('compute_nodes')
|
|
if compute_nodes_requested:
|
|
if compute_nodes_requested > len(compute_nodes):
|
|
raise DeploymentException(
|
|
'Not enough compute nodes %(cn)s for requested '
|
|
'instance accommodation %(acc)s' %
|
|
dict(cn=compute_nodes, acc=accommodation))
|
|
compute_nodes = random.sample(compute_nodes, compute_nodes_requested)
|
|
|
|
cn_count = len(compute_nodes)
|
|
iterations = cn_count * density
|
|
|
|
if 'single_room' in accommodation and 'pair' in accommodation:
|
|
iterations //= 2
|
|
node_formula = lambda x: compute_nodes[x % cn_count]
|
|
|
|
agents = {}
|
|
|
|
for i in range(iterations):
|
|
if 'pair' in accommodation:
|
|
master_id = '%s_master_%s' % (unique, i)
|
|
slave_id = '%s_slave_%s' % (unique, i)
|
|
master = dict(id=master_id, mode='master', slave_id=slave_id)
|
|
slave = dict(id=slave_id, mode='slave', master_id=master_id)
|
|
|
|
if 'single_room' in accommodation:
|
|
master_formula = lambda x: i * 2
|
|
slave_formula = lambda x: i * 2 + 1
|
|
elif 'double_room' in accommodation:
|
|
master_formula = lambda x: i
|
|
slave_formula = lambda x: i
|
|
else: # mixed_room
|
|
master_formula = lambda x: i
|
|
slave_formula = lambda x: i + 1
|
|
|
|
m = node_formula(master_formula(i))
|
|
master['node'], master['zone'] = m['host'], m['zone']
|
|
s = node_formula(slave_formula(i))
|
|
slave['node'], slave['zone'] = s['host'], s['zone']
|
|
|
|
agents[master['id']] = master
|
|
agents[slave['id']] = slave
|
|
else:
|
|
if 'single_room' in accommodation:
|
|
agent_id = '%s_agent_%s' % (unique, i)
|
|
agents[agent_id] = dict(id=agent_id,
|
|
node=node_formula(i)['host'],
|
|
zone=node_formula(i)['zone'],
|
|
mode='alone')
|
|
|
|
if not agents:
|
|
raise DeploymentException('Not enough compute nodes %(cn)s for '
|
|
'requested instance accommodation %(acc)s' %
|
|
dict(cn=compute_nodes, acc=accommodation))
|
|
|
|
# inject availability zone
|
|
for agent in agents.values():
|
|
az = agent['zone']
|
|
if agent['node']:
|
|
az += ':' + agent['node']
|
|
agent['availability_zone'] = az
|
|
|
|
return agents
|
|
|
|
|
|
def _get_stack_values(stack_outputs, vm_name, params):
|
|
result = {}
|
|
for param in params:
|
|
o = stack_outputs.get(vm_name + '_' + param)
|
|
if o:
|
|
result[param] = o
|
|
return result
|
|
|
|
|
|
def filter_agents(agents, stack_outputs, override=None):
|
|
deployed_agents = {}
|
|
|
|
# first pass, ignore non-deployed
|
|
for agent in agents.values():
|
|
stack_values = _get_stack_values(stack_outputs, agent['id'], ['ip'])
|
|
|
|
if override:
|
|
stack_values.update(override(agent))
|
|
|
|
if not stack_values.get('ip'):
|
|
LOG.info('Ignore non-deployed agent: %s', agent)
|
|
continue
|
|
|
|
agent.update(stack_values)
|
|
|
|
# workaround of Nova bug 1422686
|
|
if agent.get('mode') == 'slave' and not agent.get('ip'):
|
|
LOG.info('IP address is missing in agent: %s', agent)
|
|
continue
|
|
|
|
deployed_agents[agent['id']] = agent
|
|
|
|
# second pass, check pairs
|
|
result = {}
|
|
for agent in deployed_agents.values():
|
|
if (agent.get('mode') == 'alone' or
|
|
(agent.get('mode') == 'master' and
|
|
agent.get('slave_id') in deployed_agents) or
|
|
(agent.get('mode') == 'slave' and
|
|
agent.get('master_id') in deployed_agents)):
|
|
result[agent['id']] = agent
|
|
|
|
return result
|
|
|
|
|
|
def distribute_agents(agents, get_host_fn):
|
|
result = {}
|
|
|
|
hosts = set()
|
|
buckets = collections.defaultdict(list)
|
|
for agent in agents.values():
|
|
agent_id = agent['id']
|
|
# we assume that server name equals to agent_id
|
|
host_id = get_host_fn(agent_id)
|
|
|
|
if host_id not in hosts:
|
|
hosts.add(host_id)
|
|
agent['node'] = host_id
|
|
buckets[agent['mode']].append(agent)
|
|
else:
|
|
LOG.info('Filter out agent %s, host %s is already occupied',
|
|
agent_id, host_id)
|
|
|
|
if buckets['alone']:
|
|
result = dict((a['id'], a) for a in buckets['alone'])
|
|
else:
|
|
for master, slave in zip(buckets['master'], buckets['slave']):
|
|
master['slave_id'] = slave['id']
|
|
slave['master_id'] = master['id']
|
|
|
|
result[master['id']] = master
|
|
result[slave['id']] = slave
|
|
|
|
return result
|
|
|
|
|
|
def normalize_accommodation(accommodation):
|
|
result = {}
|
|
|
|
for s in accommodation:
|
|
if isinstance(s, dict):
|
|
result.update(s)
|
|
else:
|
|
result[s] = True
|
|
|
|
# override scenario's availability zone accommodation
|
|
if cfg.CONF.scenario_availability_zone:
|
|
result['zones'] = cfg.CONF.scenario_availability_zone
|
|
|
|
# override scenario's compute_nodes accommodation
|
|
if cfg.CONF.scenario_compute_nodes:
|
|
result['compute_nodes'] = cfg.CONF.scenario_compute_nodes
|
|
|
|
return result
|
|
|
|
|
|
class Deployment(object):
|
|
def __init__(self):
|
|
self.openstack_client = None
|
|
self.has_stack = False
|
|
self.privileged_mode = True
|
|
|
|
def connect_to_openstack(self, openstack_params, flavor_name, image_name,
|
|
external_net, dns_nameservers):
|
|
LOG.debug('Connecting to OpenStack')
|
|
|
|
self.openstack_client = openstack.OpenStackClient(openstack_params)
|
|
|
|
self.flavor_name = flavor_name
|
|
self.image_name = image_name
|
|
self.stack_name = 'shaker_%s' % utils.random_string()
|
|
self.dns_nameservers = dns_nameservers
|
|
# intiailizing self.external_net last so that other attributes don't
|
|
# remain uninitialized in case user forgets to create external network
|
|
self.external_net = (external_net or
|
|
neutron.choose_external_net(
|
|
self.openstack_client.neutron))
|
|
|
|
def _get_compute_nodes(self, accommodation):
|
|
try:
|
|
return nova.get_available_compute_nodes(self.openstack_client.nova,
|
|
self.flavor_name)
|
|
except nova.ForbiddenException:
|
|
# user has no permissions to list compute nodes
|
|
LOG.info('OpenStack user does not have permission to list compute '
|
|
'nodes - treat him as non-admin')
|
|
self.privileged_mode = False
|
|
count = accommodation.get('compute_nodes')
|
|
if not count:
|
|
raise DeploymentException(
|
|
'When run with non-admin user the scenario must specify '
|
|
'number of compute nodes to use')
|
|
|
|
zones = accommodation.get('zones') or ['nova']
|
|
return [dict(host=None, zone=zones[n % len(zones)])
|
|
for n in range(count)]
|
|
|
|
def _deploy_from_hot(self, specification, server_endpoint, base_dir=None):
|
|
accommodation = normalize_accommodation(
|
|
specification.get('accommodation') or
|
|
specification.get('vm_accommodation'))
|
|
|
|
agents = generate_agents(self._get_compute_nodes(accommodation),
|
|
accommodation, self.stack_name)
|
|
|
|
# render template by jinja
|
|
vars_values = {
|
|
'agents': agents,
|
|
'unique': self.stack_name,
|
|
}
|
|
heat_template = utils.read_file(specification['template'],
|
|
base_dir=base_dir)
|
|
compiled_template = jinja2.Template(heat_template)
|
|
rendered_template = compiled_template.render(vars_values)
|
|
LOG.debug('Rendered template: %s', rendered_template)
|
|
|
|
# create stack by Heat
|
|
try:
|
|
merged_parameters = {
|
|
'server_endpoint': server_endpoint,
|
|
'external_net': self.external_net,
|
|
'image': self.image_name,
|
|
'flavor': self.flavor_name,
|
|
'dns_nameservers': self.dns_nameservers,
|
|
}
|
|
except AttributeError as e:
|
|
LOG.error('Failed to gather required parameters to create '
|
|
'heat stack: %s', e)
|
|
exit(1)
|
|
merged_parameters.update(specification.get('template_parameters', {}))
|
|
|
|
self.has_stack = True
|
|
stack_id = heat.create_stack(
|
|
self.openstack_client.heat, self.stack_name, rendered_template,
|
|
merged_parameters)
|
|
|
|
# get info about deployed objects
|
|
outputs = heat.get_stack_outputs(self.openstack_client.heat, stack_id)
|
|
override = self._get_override(specification.get('override'))
|
|
|
|
agents = filter_agents(agents, outputs, override)
|
|
|
|
if (not self.privileged_mode) and accommodation.get('density', 1) == 1:
|
|
get_host_fn = functools.partial(nova.get_server_host_id,
|
|
self.openstack_client.nova)
|
|
agents = distribute_agents(agents, get_host_fn)
|
|
|
|
return agents
|
|
|
|
def _get_override(self, override_spec):
|
|
def override_ip(agent, ip_type):
|
|
return dict(ip=nova.get_server_ip(
|
|
self.openstack_client.nova, agent['id'], ip_type))
|
|
|
|
if override_spec:
|
|
if override_spec.get('ip'):
|
|
return functools.partial(override_ip,
|
|
ip_type=override_spec.get('ip'))
|
|
|
|
def deploy(self, deployment, base_dir=None, server_endpoint=None):
|
|
agents = {}
|
|
|
|
if not deployment:
|
|
# local mode, create fake agent
|
|
agents.update(dict(local=dict(id='local', mode='alone',
|
|
node='localhost')))
|
|
|
|
if deployment.get('template'):
|
|
if not self.openstack_client:
|
|
raise DeploymentException(
|
|
'OpenStack client is not initialized. '
|
|
'Template-based deployment is ignored.')
|
|
else:
|
|
# deploy topology specified by HOT
|
|
agents.update(self._deploy_from_hot(
|
|
deployment, server_endpoint, base_dir=base_dir))
|
|
|
|
if deployment.get('agents'):
|
|
# agents are specified statically
|
|
agents.update(dict((a['id'], a) for a in deployment.get('agents')))
|
|
|
|
return agents
|
|
|
|
def cleanup(self):
|
|
if self.has_stack and cfg.CONF.cleanup_on_error:
|
|
LOG.debug('Cleaning up the stack: %s', self.stack_name)
|
|
self.openstack_client.heat.stacks.delete(self.stack_name)
|