summaryrefslogtreecommitdiffstats
path: root/vnftest/contexts/model.py
diff options
context:
space:
mode:
Diffstat (limited to 'vnftest/contexts/model.py')
-rw-r--r--vnftest/contexts/model.py433
1 files changed, 433 insertions, 0 deletions
diff --git a/vnftest/contexts/model.py b/vnftest/contexts/model.py
new file mode 100644
index 0000000..52b8d34
--- /dev/null
+++ b/vnftest/contexts/model.py
@@ -0,0 +1,433 @@
+##############################################################################
+# 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})