diff options
Diffstat (limited to 'vnftest/contexts')
-rw-r--r-- | vnftest/contexts/base.py | 52 | ||||
-rw-r--r-- | vnftest/contexts/heat.py | 591 | ||||
-rw-r--r-- | vnftest/contexts/model.py | 433 |
3 files changed, 1071 insertions, 5 deletions
diff --git a/vnftest/contexts/base.py b/vnftest/contexts/base.py index 29e3a19..c10732a 100644 --- a/vnftest/contexts/base.py +++ b/vnftest/contexts/base.py @@ -13,27 +13,69 @@ ############################################################################## import abc import six -from vnftest.common import openstack_utils import vnftest.common.utils as utils -import yaml import logging + +from vnftest.common import constants + LOG = logging.getLogger(__name__) +class Flags(object): + """Class to represent the status of the flags in a context""" + + _FLAGS = {'no_setup': False, + 'no_teardown': False, + 'os_cloud_config': constants.OS_CLOUD_DEFAULT_CONFIG} + + def __init__(self, **kwargs): + for name, value in self._FLAGS.items(): + setattr(self, name, value) + + for name, value in ((name, value) for (name, value) in kwargs.items() + if name in self._FLAGS): + setattr(self, name, value) + + def parse(self, **kwargs): + """Read in values matching the flags stored in this object""" + if not kwargs: + return + + for name, value in ((name, value) for (name, value) in kwargs.items() + if name in self._FLAGS): + setattr(self, name, value) + + @six.add_metaclass(abc.ABCMeta) class Context(object): """Class that represents a context in the logical model""" - list = [] + _list = [] def __init__(self): - Context.list.append(self) + Context._list.append(self) + self._flags = Flags() self._task_id = None self._name = None + self._name_task_id = None def init(self, attrs): self._task_id = attrs['task_id'] self._name = attrs['name'] + self._flags.parse(**attrs.get('flags', {})) + self._name_task_id = '{}-{}'.format( + self._name, self._task_id[:8]) + + @property + def name(self): + if self._flags.no_setup or self._flags.no_teardown: + return self._name + else: + return self._name_task_id + + @property + def assigned_name(self): + return self._name @staticmethod def get_cls(context_type): @@ -50,7 +92,7 @@ class Context(object): return Context.get_cls(context_type)() def _delete_context(self): - Context.list.remove(self) + Context._list.remove(self) @abc.abstractmethod def deploy(self): diff --git a/vnftest/contexts/heat.py b/vnftest/contexts/heat.py new file mode 100644 index 0000000..f6ca611 --- /dev/null +++ b/vnftest/contexts/heat.py @@ -0,0 +1,591 @@ +############################################################################## +# 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/heat.py + +import collections +import logging +import os +import errno +from collections import OrderedDict + +import ipaddress +import pkg_resources + +from vnftest.contexts.base import Context +from vnftest.contexts.model import Network +from vnftest.contexts.model import PlacementGroup, ServerGroup +from vnftest.contexts.model import Server +from vnftest.contexts.model import update_scheduler_hints +from vnftest.common import exceptions as y_exc +from vnftest.common.openstack_utils import get_shade_client +from vnftest.orchestrator.heat import HeatStack +from vnftest.orchestrator.heat import HeatTemplate +from vnftest.common import constants as consts +from vnftest.common import utils +from vnftest.common.utils import source_env +from vnftest.common import openstack_utils + +LOG = logging.getLogger(__name__) + +DEFAULT_HEAT_TIMEOUT = 3600 + + +def join_args(sep, *args): + return sep.join(args) + + +def h_join(*args): + return '-'.join(args) + + +class HeatContext(Context): + """Class that represents a context in the logical model""" + + __context_type__ = "Heat" + + def __init__(self): + self.stack = None + self.networks = OrderedDict() + self.heat_timeout = None + self.servers = {} + self.placement_groups = [] + self.server_groups = [] + self.keypair_name = None + self.secgroup_name = None + self.security_group = None + self.attrs = {} + self._image = None + self._flavor = None + self.flavors = set() + self._user = None + self.template_file = None + self.heat_parameters = None + self.heat_timeout = None + self.key_filename = None + self._shade_client = None + self._operator_client = None + self.nodes = [] + self.controllers = [] + self.computes = [] + self.baremetals = [] + super(HeatContext, self).__init__() + + @staticmethod + def assign_external_network(networks): + if networks: + sorted_networks = sorted(networks.items()) + external_network = os.environ.get("EXTERNAL_NETWORK", "net04_ext") + + have_external_network = any(net.get("external_network") for net in networks.values()) + if not have_external_network: + # try looking for mgmt network first + try: + networks['mgmt']["external_network"] = external_network + except KeyError: + if sorted_networks: + # otherwise assign it to first network using os.environ + sorted_networks[0][1]["external_network"] = external_network + + return sorted_networks + return networks + + def init(self, attrs): + """Initializes itself from the supplied arguments""" + super(HeatContext, self).init(attrs) + + self.check_environment() + self._user = attrs.get("user") + + self.template_file = attrs.get("heat_template") + + self._shade_client = openstack_utils.get_shade_client() + self._operator_client = openstack_utils.get_shade_operator_client() + + self.heat_timeout = attrs.get("timeout", DEFAULT_HEAT_TIMEOUT) + if self.template_file: + self.heat_parameters = attrs.get("heat_parameters") + return + + self.keypair_name = None # h_join(self.name, "key") + + self.secgroup_name = None # h_join(self.name, "secgroup") + + self.security_group = attrs.get("security_group") + + self._image = attrs.get("image") + + self._flavor = attrs.get("flavor") + + self.placement_groups = [PlacementGroup(name, self, pg_attrs["policy"]) + for name, pg_attrs in attrs.get( + "placement_groups", {}).items()] + + self.server_groups = [ServerGroup(name, self, sg_attrs["policy"]) + for name, sg_attrs in attrs.get( + "server_groups", {}).items()] + + # we have to do this first, because we are injecting external_network + # into the dict + networks = attrs.get("networks", {}) + sorted_networks = self.assign_external_network(networks) + + if sorted_networks: + self.networks = OrderedDict( + (name, Network(name, self, net_attrs)) for name, net_attrs in + sorted_networks) + + for name, server_attrs in sorted(attrs["servers"].items()): + server = Server(name, self, server_attrs) + self.servers[name] = server + + self.attrs = attrs + + def check_environment(self): + try: + os.environ['OS_AUTH_URL'] + except KeyError: + try: + source_env(consts.OPENRC) + except IOError as e: + if e.errno != errno.EEXIST: + LOG.error('OPENRC file not found') + raise + else: + LOG.error('OS_AUTH_URL not found') + + @property + def image(self): + """returns application's default image name""" + return self._image + + @property + def flavor(self): + """returns application's default flavor name""" + return self._flavor + + @property + def user(self): + """return login user name corresponding to image""" + return self._user + + def _add_resources_to_template(self, template): + """add to the template the resources represented by this context""" + + if self.flavor: + if isinstance(self.flavor, dict): + flavor = self.flavor.setdefault("name", self.name + "-flavor") + template.add_flavor(**self.flavor) + self.flavors.add(flavor) + + # template.add_keypair(self.keypair_name, self.name) + # template.add_security_group(self.secgroup_name, self.security_group) + + for network in self.networks.values(): + # Using existing network + if network.is_existing(): + continue + template.add_network(network.stack_name, + network.physical_network, + network.provider, + network.segmentation_id, + network.port_security_enabled, + network.network_type) + template.add_subnet(network.subnet_stack_name, network.stack_name, + network.subnet_cidr, + network.enable_dhcp, + network.gateway_ip) + + if network.router: + template.add_router(network.router.stack_name, + network.router.external_gateway_info, + network.subnet_stack_name) + template.add_router_interface(network.router.stack_if_name, + network.router.stack_name, + network.subnet_stack_name) + + # create a list of servers sorted by increasing no of placement groups + list_of_servers = sorted(self.servers.itervalues(), + key=lambda s: len(s.placement_groups)) + + # + # add servers with scheduler hints derived from placement groups + # + + # create list of servers with availability policy + availability_servers = [] + for server in list_of_servers: + for pg in server.placement_groups: + if pg.policy == "availability": + availability_servers.append(server) + break + + for server in availability_servers: + if isinstance(server.flavor, dict): + try: + self.flavors.add(server.flavor["name"]) + except KeyError: + self.flavors.add(h_join(server.stack_name, "flavor")) + + # add servers with availability policy + added_servers = [] + for server in availability_servers: + scheduler_hints = {} + for pg in server.placement_groups: + update_scheduler_hints(scheduler_hints, added_servers, pg) + # workaround for openstack nova bug, check JIRA: vnftest-200 + # for details + if len(availability_servers) == 2: + if not scheduler_hints["different_host"]: + scheduler_hints.pop("different_host", None) + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + else: + scheduler_hints["different_host"] = \ + scheduler_hints["different_host"][0] + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + else: + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + added_servers.append(server.stack_name) + + # create list of servers with affinity policy + affinity_servers = [] + for server in list_of_servers: + for pg in server.placement_groups: + if pg.policy == "affinity": + affinity_servers.append(server) + break + + # add servers with affinity policy + for server in affinity_servers: + if server.stack_name in added_servers: + continue + scheduler_hints = {} + for pg in server.placement_groups: + update_scheduler_hints(scheduler_hints, added_servers, pg) + server.add_to_template(template, list(self.networks.values()), + scheduler_hints) + added_servers.append(server.stack_name) + + # add server group + for sg in self.server_groups: + template.add_server_group(sg.name, sg.policy) + + # add remaining servers with no placement group configured + for server in list_of_servers: + # TODO placement_group and server_group should combine + if not server.placement_groups: + scheduler_hints = {} + # affinity/anti-aff server group + sg = server.server_group + if sg: + scheduler_hints["group"] = {'get_resource': sg.name} + server.add_to_template(template, + list(self.networks.values()), + scheduler_hints) + + def get_neutron_info(self): + if not self._shade_client: + self._shade_client = get_shade_client() + + networks = self._shade_client.list_networks() + for network in self.networks.values(): + for neutron_net in (net for net in networks if net.name == network.stack_name): + network.segmentation_id = neutron_net.get('provider:segmentation_id') + # we already have physical_network + # network.physical_network = neutron_net.get('provider:physical_network') + network.network_type = neutron_net.get('provider:network_type') + network.neutron_info = neutron_net + + def _create_new_stack(self, heat_template): + try: + return heat_template.create(block=True, + timeout=self.heat_timeout) + except KeyboardInterrupt: + raise y_exc.StackCreationInterrupt + except Exception: + LOG.exception("stack failed") + # let the other failures happen, we want stack trace + raise + + def _retrieve_existing_stack(self, stack_name): + stack = HeatStack(stack_name) + if stack.get(): + return stack + else: + LOG.warning("Stack %s does not exist", self.name) + return None + + def deploy(self): + """deploys template into a stack using cloud""" + LOG.info("Deploying context '%s' START", self.name) + + # self.key_filename = ''.join( + # [consts.VNFTEST_ROOT_PATH, + # '/vnftest/resources/files/vnftest_key-', + # self.name]) + # Permissions may have changed since creation; this can be fixed. If we + # overwrite the file, we lose future access to VMs using this key. + # As long as the file exists, even if it is unreadable, keep it intact + # if not os.path.exists(self.key_filename): + # SSH.gen_keys(self.key_filename) + + heat_template = HeatTemplate( + self.name, template_file=self.template_file, + heat_parameters=self.heat_parameters, + os_cloud_config=self._flags.os_cloud_config) + + if self.template_file is None: + self._add_resources_to_template(heat_template) + + if self._flags.no_setup: + # Try to get an existing stack, returns a stack or None + self.stack = self._retrieve_existing_stack(self.name) + if not self.stack: + self.stack = self._create_new_stack(heat_template) + + else: + self.stack = self._create_new_stack(heat_template) + + # TODO: use Neutron to get segmentation-id + self.get_neutron_info() + + # copy some vital stack output into server objects + for server in self.servers.itervalues(): + if server.networks: + self.update_networks(server) + if server.ports: + self.add_server_port(server) + + if server.floating_ip: + server.public_ip = \ + self.stack.outputs[server.floating_ip["stack_name"]] + + LOG.info("Deploying context '%s' DONE", self.name) + + @staticmethod + def _port_net_is_existing(port_info): + net_flags = port_info.get('net_flags', {}) + return net_flags.get(consts.IS_EXISTING) + + @staticmethod + def _port_net_is_public(port_info): + net_flags = port_info.get('net_flags', {}) + return net_flags.get(consts.IS_PUBLIC) + + def update_networks(self, server): + for network in server.networks: + if 'network' in network: + network_name = network['network'] + output_key = server.stack_name + ".networks.%s-ip" % network_name + ip = self.stack.outputs[output_key] + network["ip"] = ip + + def add_server_port(self, server): + server_ports = server.ports.values() + for server_port in server_ports: + port_info = server_port[0] + port_ip = self.stack.outputs[port_info["stack_name"]] + port_net_is_existing = self._port_net_is_existing(port_info) + port_net_is_public = self._port_net_is_public(port_info) + if port_net_is_existing and (port_net_is_public or + len(server_ports) == 1): + server.public_ip = port_ip + if not server.private_ip or len(server_ports) == 1: + server.private_ip = port_ip + + server.interfaces = {} + for network_name, ports in server.ports.items(): + for port in ports: + # port['port'] is either port name from mapping or default network_name + if self._port_net_is_existing(port): + continue + server.interfaces[port['port']] = self.make_interface_dict(network_name, + port['port'], + port['stack_name'], + self.stack.outputs) + server.override_ip(network_name, port) + + def make_interface_dict(self, network_name, port, stack_name, outputs): + private_ip = outputs[stack_name] + mac_address = outputs[h_join(stack_name, "mac_address")] + # these are attributes of the network, not the port + output_subnet_cidr = outputs[h_join(self.name, network_name, + 'subnet', 'cidr')] + + # these are attributes of the network, not the port + output_subnet_gateway = outputs[h_join(self.name, network_name, + 'subnet', 'gateway_ip')] + + return { + # add default port name + "name": port, + "private_ip": private_ip, + "subnet_id": outputs[h_join(stack_name, "subnet_id")], + "subnet_cidr": output_subnet_cidr, + "network": str(ipaddress.ip_network(output_subnet_cidr).network_address), + "netmask": str(ipaddress.ip_network(output_subnet_cidr).netmask), + "gateway_ip": output_subnet_gateway, + "mac_address": mac_address, + "device_id": outputs[h_join(stack_name, "device_id")], + "network_id": outputs[h_join(stack_name, "network_id")], + # this should be == vld_id for NSB tests + "network_name": network_name, + # to match vnf_generic + "local_mac": mac_address, + "local_ip": private_ip, + } + + def _delete_key_file(self): + if self.key_filename is not None: + try: + utils.remove_file(self.key_filename) + utils.remove_file(self.key_filename + ".pub") + except OSError: + LOG.exception("There was an error removing the key file %s", + self.key_filename) + + def undeploy(self): + """undeploys stack from cloud""" + if self._flags.no_teardown: + LOG.info("Undeploying context '%s' SKIP", self.name) + return + + if self.stack: + LOG.info("Undeploying context '%s' START", self.name) + self.stack.delete() + self.stack = None + LOG.info("Undeploying context '%s' DONE", self.name) + + self._delete_key_file() + + super(HeatContext, self).undeploy() + + @staticmethod + def generate_routing_table(server): + routes = [ + { + "network": intf["network"], + "netmask": intf["netmask"], + "if": name, + # We have to encode a None gateway as '' for Jinja2 to YAML conversion + "gateway": intf["gateway_ip"] if intf["gateway_ip"] else '', + } + for name, intf in server.interfaces.items() + ] + return routes + + def _get_server(self, attr_name): + """lookup server info by name from context + attr_name: either a name for a server created by vnftest or a dict + with attribute name mapping when using external heat templates + """ + if isinstance(attr_name, collections.Mapping): + node_name, cname = self.split_host_name(attr_name['name']) + if cname is None or cname != self.name: + return None + + # Create a dummy server instance for holding the *_ip attributes + server = Server(node_name, self, {}) + server.public_ip = self.stack.outputs.get( + attr_name.get("public_ip_attr", object()), None) + + server.private_ip = self.stack.outputs.get( + attr_name.get("private_ip_attr", object()), None) + else: + try: + server = self.servers[attr_name] + except KeyError: + attr_name_no_suffix = attr_name.split("-")[0] + server = self.servers.get(attr_name_no_suffix, None) + if server is None: + return None + + pkey = pkg_resources.resource_string( + 'vnftest.resources', + h_join('files/vnftest_key', self.name)).decode('utf-8') + key_filename = pkg_resources.resource_filename('vnftest.resources', + h_join('files/vnftest_key', self.name)) + result = { + "user": server._context.user, + "pkey": pkey, + "key_filename": key_filename, + "private_ip": server.private_ip, + "interfaces": server.interfaces, + "routing_table": self.generate_routing_table(server), + # empty IPv6 routing table + "nd_route_tbl": [], + # we want to save the contex name so we can generate pod.yaml + "name": server.name, + } + # Target server may only have private_ip + if server.public_ip: + result["ip"] = server.public_ip + + return result + + def _get_network(self, attr_name): + if not isinstance(attr_name, collections.Mapping): + network = self.networks.get(attr_name, None) + + else: + # Only take the first key, value + key, value = next(iter(attr_name.items()), (None, None)) + if key is None: + return None + network_iter = (n for n in self.networks.values() if getattr(n, key) == value) + network = next(network_iter, None) + + if network is None: + return None + + result = { + "name": network.name, + "segmentation_id": network.segmentation_id, + "network_type": network.network_type, + "physical_network": network.physical_network, + } + return result + + def _get_physical_nodes(self): + return self.nodes + + def _get_physical_node_for_server(self, server_name): + node_name, ctx_name = self.split_host_name(server_name) + if ctx_name is None or self.name != ctx_name: + return None + + matching_nodes = [s for s in self.servers.itervalues() if s.name == node_name] + if len(matching_nodes) == 0: + return None + + server = openstack_utils.get_server(self._shade_client, + name_or_id=server_name) + + if server: + server = server.toDict() + list_hypervisors = self._operator_client.list_hypervisors() + + for hypervisor in list_hypervisors: + if hypervisor.hypervisor_hostname == server['OS-EXT-SRV-ATTR:hypervisor_hostname']: + for node in self.nodes: + if node['ip'] == hypervisor.host_ip: + return "{}.{}".format(node['name'], self.name) + + return None 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}) |