diff options
Diffstat (limited to 'vnftest/contexts/model.py')
-rw-r--r-- | vnftest/contexts/model.py | 433 |
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}) |