blob: 52b8d34af7679a7654d224bcf0cbd23a56f3dbc3 [file] [log] [blame]
##############################################################################
# Copyright 2018 EuropeanSoftwareMarketingLtd.
# ===================================================================
# Licensed under the ApacheLicense, Version2.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
#
# 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
##############################################################################
# vnftest comment: this is a modified copy of
# yardstick/benchmark/contexts/model.py
""" Logical model
"""
from __future__ import absolute_import
import six
import logging
from collections import Mapping
from six.moves import range
from vnftest.common import constants as consts
LOG = logging.getLogger(__name__)
class Object(object):
"""Base class for classes in the logical model
Contains common attributes and methods
"""
def __init__(self, name, context):
# model identities and reference
self.name = name
self._context = context
# stack identities
self.stack_name = None
self.stack_id = None
@property
def dn(self):
"""returns distinguished name for object"""
return self.name + "." + self._context.name
class PlacementGroup(Object):
"""Class that represents a placement group in the logical model
Concept comes from the OVF specification. Policy should be one of
"availability" or "affinity (there are more but they are not supported)"
"""
map = {}
def __init__(self, name, context, policy):
if policy not in ["affinity", "availability"]:
raise ValueError("placement group '%s', policy '%s' is not valid" %
(name, policy))
self.name = name
self.members = set()
self.stack_name = context.name + "-" + name
self.policy = policy
PlacementGroup.map[name] = self
def add_member(self, name):
self.members.add(name)
@staticmethod
def get(name):
return PlacementGroup.map.get(name)
class ServerGroup(Object): # pragma: no cover
"""Class that represents a server group in the logical model
Policy should be one of "anti-affinity" or "affinity"
"""
map = {}
def __init__(self, name, context, policy):
super(ServerGroup, self).__init__(name, context)
if policy not in {"affinity", "anti-affinity"}:
raise ValueError("server group '%s', policy '%s' is not valid" %
(name, policy))
self.name = name
self.members = set()
self.stack_name = context.name + "-" + name
self.policy = policy
ServerGroup.map[name] = self
def add_member(self, name):
self.members.add(name)
@staticmethod
def get(name):
return ServerGroup.map.get(name)
class Router(Object):
"""Class that represents a router in the logical model"""
def __init__(self, name, network_name, context, external_gateway_info):
super(Router, self).__init__(name, context)
self.stack_name = context.name + "-" + network_name + "-" + self.name
self.stack_if_name = self.stack_name + "-if0"
self.external_gateway_info = external_gateway_info
class Network(Object):
"""Class that represents a network in the logical model"""
list = []
def __init__(self, name, context, attrs):
super(Network, self).__init__(name, context)
self.stack_name = context.name + "-" + self.name
self.subnet_stack_name = self.stack_name + "-subnet"
self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
self.enable_dhcp = attrs.get('enable_dhcp', 'true')
self.router = None
self.physical_network = attrs.get('physical_network', 'physnet1')
self.provider = attrs.get('provider')
self.segmentation_id = attrs.get('segmentation_id')
self.network_type = attrs.get('network_type')
self.port_security_enabled = attrs.get('port_security_enabled')
self.vnic_type = attrs.get('vnic_type', 'normal')
self.allowed_address_pairs = attrs.get('allowed_address_pairs', [])
try:
# we require 'null' or '' to disable setting gateway_ip
self.gateway_ip = attrs['gateway_ip']
except KeyError:
# default to explicit None
self.gateway_ip = None
else:
# null is None in YAML, so we have to convert back to string
if self.gateway_ip is None:
self.gateway_ip = "null"
self.net_flags = attrs.get('net_flags', {})
if self.is_existing():
self.subnet = attrs.get('subnet')
if not self.subnet:
raise Warning('No subnet set in existing netwrok!')
else:
if "external_network" in attrs:
self.router = Router("router", self.name,
context, attrs["external_network"])
Network.list.append(self)
def is_existing(self):
net_is_existing = self.net_flags.get(consts.IS_EXISTING)
if net_is_existing and not isinstance(net_is_existing, bool):
raise SyntaxError('Network flags should be bool type!')
return net_is_existing
def is_public(self):
net_is_public = self.net_flags.get(consts.IS_PUBLIC)
if net_is_public and not isinstance(net_is_public, bool):
raise SyntaxError('Network flags should be bool type!')
return net_is_public
def has_route_to(self, network_name):
"""determines if this network has a route to the named network"""
if self.router and self.router.external_gateway_info == network_name:
return True
return False
@staticmethod
def find_by_route_to(external_network):
"""finds a network that has a route to the specified network"""
for network in Network.list:
if network.has_route_to(external_network):
return network
@staticmethod
def find_external_network():
"""return the name of an external network some network in this
context has a route to
"""
for network in Network.list:
if network.router:
return network.router.external_gateway_info
return None
class Server(Object): # pragma: no cover
"""Class that represents a server in the logical model"""
list = []
def __init__(self, name, context, attrs):
super(Server, self).__init__(name, context)
self.stack_name = self.name + "." + context.name
self.keypair_name = context.keypair_name
self.secgroup_name = context.secgroup_name
self.user = context.user
self._context = context
self.public_ip = None
self.private_ip = None
self.user_data = ''
self.interfaces = {}
self.networks = None
self.ports = {}
self.floating_ip = {}
if attrs is None:
attrs = {}
self.placement_groups = []
placement = attrs.get("placement", [])
placement = placement if isinstance(placement, list) else [placement]
for p in placement:
pg = PlacementGroup.get(p)
if not pg:
raise ValueError("server '%s', placement '%s' is invalid" %
(name, p))
self.placement_groups.append(pg)
pg.add_member(self.stack_name)
self.volume = None
if "volume" in attrs:
self.volume = attrs.get("volume")
self.volume_mountpoint = None
if "volume_mountpoint" in attrs:
self.volume_mountpoint = attrs.get("volume_mountpoint")
# support servergroup attr
self.server_group = None
sg = attrs.get("server_group")
if sg:
server_group = ServerGroup.get(sg)
if not server_group:
raise ValueError("server '%s', server_group '%s' is invalid" %
(name, sg))
self.server_group = server_group
server_group.add_member(self.stack_name)
self.instances = 1
if "instances" in attrs:
self.instances = attrs["instances"]
if "networks" in attrs:
self.networks = attrs["networks"]
else:
# dict with key network name, each item is a dict with port name and ip
self.network_ports = attrs.get("network_ports", {})
self.floating_ip = None
self.floating_ip_assoc = None
if "floating_ip" in attrs:
self.floating_ip_assoc = {}
if self.floating_ip is not None:
ext_net = Network.find_external_network()
assert ext_net is not None
self.floating_ip["external_network"] = ext_net
self._image = None
if "image" in attrs:
self._image = attrs["image"]
self._flavor = None
if "flavor" in attrs:
self._flavor = attrs["flavor"]
self.user_data = attrs.get('user_data', '')
self.availability_zone = attrs.get('availability_zone')
Server.list.append(self)
def override_ip(self, network_name, port):
def find_port_overrides():
for p in ports:
# p can be string or dict
# we can't just use p[port['port'] in case p is a string
# and port['port'] is an int?
if isinstance(p, Mapping):
g = p.get(port['port'])
# filter out empty dicts
if g:
yield g
ports = self.network_ports.get(network_name, [])
intf = self.interfaces[port['port']]
for override in find_port_overrides():
intf['local_ip'] = override.get('local_ip', intf['local_ip'])
intf['netmask'] = override.get('netmask', intf['netmask'])
# only use the first value
break
@property
def image(self):
"""returns a server's image name"""
if self._image:
return self._image
else:
return self._context.image
@property
def flavor(self):
"""returns a server's flavor name"""
if self._flavor:
return self._flavor
else:
return self._context.flavor
def _add_instance(self, template, server_name, networks, scheduler_hints):
"""adds to the template one server and corresponding resources"""
port_name_list = None
if self.networks is None:
port_name_list = []
for network in networks:
# if explicit mapping skip unused networks
if self.network_ports:
try:
ports = self.network_ports[network.name]
except KeyError:
# no port for this network
continue
else:
if isinstance(ports, six.string_types):
# because strings are iterable we have to check specifically
raise SyntaxError("network_port must be a list '{}'".format(ports))
# convert port subdicts into their just port name
# port subdicts are used to override Heat IP address,
# but we just need the port name
# we allow duplicates here and let Heat raise the error
ports = [next(iter(p)) if isinstance(p, dict) else p for p in ports]
# otherwise add a port for every network with port name as network name
else:
ports = [network.name]
net_flags = network.net_flags
for port in ports:
port_name = "{0}-{1}-port".format(server_name, port)
port_info = {"stack_name": port_name, "port": port}
if net_flags:
port_info['net_flags'] = net_flags
self.ports.setdefault(network.name, []).append(port_info)
# we can't use secgroups if port_security_enabled is False
if network.port_security_enabled is False:
sec_group_id = None
else:
# if port_security_enabled is None we still need to add to secgroup
sec_group_id = self.secgroup_name
# don't refactor to pass in network object, that causes JSON
# circular ref encode errors
template.add_port(port_name, network,
sec_group_id=sec_group_id,
provider=network.provider,
allowed_address_pairs=network.allowed_address_pairs)
if network.is_public():
port_name_list.insert(0, port_name)
else:
port_name_list.append(port_name)
if self.floating_ip:
external_network = self.floating_ip["external_network"]
if network.has_route_to(external_network):
self.floating_ip["stack_name"] = server_name + "-fip"
template.add_floating_ip(self.floating_ip["stack_name"],
external_network,
port_name,
network.router.stack_if_name,
sec_group_id)
self.floating_ip_assoc["stack_name"] = \
server_name + "-fip-assoc"
template.add_floating_ip_association(
self.floating_ip_assoc["stack_name"],
self.floating_ip["stack_name"],
port_name)
if self.flavor:
if isinstance(self.flavor, dict):
self.flavor["name"] = \
self.flavor.setdefault("name", self.stack_name + "-flavor")
template.add_flavor(**self.flavor)
self.flavor_name = self.flavor["name"]
else:
self.flavor_name = self.flavor
if self.volume:
if isinstance(self.volume, dict):
self.volume["name"] = \
self.volume.setdefault("name", server_name + "-volume")
template.add_volume(**self.volume)
template.add_volume_attachment(server_name, self.volume["name"],
mountpoint=self.volume_mountpoint)
else:
template.add_volume_attachment(server_name, self.volume,
mountpoint=self.volume_mountpoint)
template.add_server(server_name, self.image, flavor=self.flavor_name,
flavors=self._context.flavors, ports=port_name_list,
networks=self.networks,
scheduler_hints=scheduler_hints, user=self.user,
key_name=self.keypair_name, user_data=self.user_data,
availability_zone=self.availability_zone)
def add_to_template(self, template, networks, scheduler_hints=None):
"""adds to the template one or more servers (instances)"""
if self.instances == 1:
server_name = self.stack_name
self._add_instance(template, server_name, networks,
scheduler_hints=scheduler_hints)
else:
# TODO(hafe) fix or remove, no test/sample for this
for i in range(self.instances):
server_name = "%s-%d" % (self.stack_name, i)
self._add_instance(template, server_name, networks,
scheduler_hints=scheduler_hints)
def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
"""update scheduler hints from server's placement configuration
TODO: this code is openstack specific and should move somewhere else
"""
if placement_group.policy == "affinity":
if "same_host" in scheduler_hints:
host_list = scheduler_hints["same_host"]
else:
host_list = scheduler_hints["same_host"] = []
else:
if "different_host" in scheduler_hints:
host_list = scheduler_hints["different_host"]
else:
host_list = scheduler_hints["different_host"] = []
for name in added_servers:
if name in placement_group.members:
host_list.append({'get_resource': name})