1 ##############################################################################
2 # Copyright (c) 2015 Ericsson AB and others.
4 # All rights reserved. This program and the accompanying materials
5 # are made available under the terms of the Apache License, Version 2.0
6 # which accompanies this distribution, and is available at
7 # http://www.apache.org/licenses/LICENSE-2.0
8 ##############################################################################
13 from __future__ import absolute_import
18 from collections import Mapping
19 from six.moves import range
22 LOG = logging.getLogger(__name__)
26 """Base class for classes in the logical model
27 Contains common attributes and methods
30 def __init__(self, name, context):
31 # model identities and reference
33 self._context = context
36 self.stack_name = None
41 """returns distinguished name for object"""
42 return self.name + "." + self._context.name
45 class PlacementGroup(Object):
46 """Class that represents a placement group in the logical model
47 Concept comes from the OVF specification. Policy should be one of
48 "availability" or "affinity (there are more but they are not supported)"
52 def __init__(self, name, context, policy):
53 if policy not in ["affinity", "availability"]:
54 raise ValueError("placement group '%s', policy '%s' is not valid" %
58 self.stack_name = context.name + "-" + name
60 PlacementGroup.map[name] = self
62 def add_member(self, name):
63 self.members.add(name)
67 return PlacementGroup.map.get(name)
70 class ServerGroup(Object): # pragma: no cover
71 """Class that represents a server group in the logical model
72 Policy should be one of "anti-affinity" or "affinity"
76 def __init__(self, name, context, policy):
77 super(ServerGroup, self).__init__(name, context)
78 if policy not in {"affinity", "anti-affinity"}:
79 raise ValueError("server group '%s', policy '%s' is not valid" %
83 self.stack_name = context.name + "-" + name
85 ServerGroup.map[name] = self
87 def add_member(self, name):
88 self.members.add(name)
92 return ServerGroup.map.get(name)
96 """Class that represents a router in the logical model"""
98 def __init__(self, name, network_name, context, external_gateway_info):
99 super(Router, self).__init__(name, context)
101 self.stack_name = context.name + "-" + network_name + "-" + self.name
102 self.stack_if_name = self.stack_name + "-if0"
103 self.external_gateway_info = external_gateway_info
106 class Network(Object):
107 """Class that represents a network in the logical model"""
110 def __init__(self, name, context, attrs):
111 super(Network, self).__init__(name, context)
112 self.stack_name = context.name + "-" + self.name
113 self.subnet_stack_name = self.stack_name + "-subnet"
114 self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
115 self.enable_dhcp = attrs.get('enable_dhcp', 'true')
117 self.physical_network = attrs.get('physical_network', 'physnet1')
118 self.provider = attrs.get('provider')
119 self.segmentation_id = attrs.get('segmentation_id')
120 self.network_type = attrs.get('network_type')
121 self.port_security_enabled = attrs.get('port_security_enabled')
122 self.vnic_type = attrs.get('vnic_type', 'normal')
123 self.allowed_address_pairs = attrs.get('allowed_address_pairs', [])
125 # we require 'null' or '' to disable setting gateway_ip
126 self.gateway_ip = attrs['gateway_ip']
128 # default to explicit None
129 self.gateway_ip = None
131 # null is None in YAML, so we have to convert back to string
132 if self.gateway_ip is None:
133 self.gateway_ip = "null"
135 if "external_network" in attrs:
136 self.router = Router("router", self.name,
137 context, attrs["external_network"])
139 Network.list.append(self)
141 def has_route_to(self, network_name):
142 """determines if this network has a route to the named network"""
143 if self.router and self.router.external_gateway_info == network_name:
148 def find_by_route_to(external_network):
149 """finds a network that has a route to the specified network"""
150 for network in Network.list:
151 if network.has_route_to(external_network):
155 def find_external_network():
156 """return the name of an external network some network in this
157 context has a route to
159 for network in Network.list:
161 return network.router.external_gateway_info
165 class Server(Object): # pragma: no cover
166 """Class that represents a server in the logical model"""
169 def __init__(self, name, context, attrs):
170 super(Server, self).__init__(name, context)
171 self.stack_name = self.name + "." + context.name
172 self.keypair_name = context.keypair_name
173 self.secgroup_name = context.secgroup_name
174 self.user = context.user
175 self.context = context
176 self.public_ip = None
177 self.private_ip = None
184 self.placement_groups = []
185 placement = attrs.get("placement", [])
186 placement = placement if isinstance(placement, list) else [placement]
188 pg = PlacementGroup.get(p)
190 raise ValueError("server '%s', placement '%s' is invalid" %
192 self.placement_groups.append(pg)
193 pg.add_member(self.stack_name)
196 if "volume" in attrs:
197 self.volume = attrs.get("volume")
199 self.volume_mountpoint = None
200 if "volume_mountpoint" in attrs:
201 self.volume_mountpoint = attrs.get("volume_mountpoint")
203 # support servergroup attr
204 self.server_group = None
205 sg = attrs.get("server_group")
207 server_group = ServerGroup.get(sg)
209 raise ValueError("server '%s', server_group '%s' is invalid" %
211 self.server_group = server_group
212 server_group.add_member(self.stack_name)
215 if "instances" in attrs:
216 self.instances = attrs["instances"]
218 # dict with key network name, each item is a dict with port name and ip
219 self.network_ports = attrs.get("network_ports", {})
222 self.floating_ip = None
223 self.floating_ip_assoc = None
224 if "floating_ip" in attrs:
225 self.floating_ip = {}
226 self.floating_ip_assoc = {}
228 if self.floating_ip is not None:
229 ext_net = Network.find_external_network()
230 assert ext_net is not None
231 self.floating_ip["external_network"] = ext_net
235 self._image = attrs["image"]
238 if "flavor" in attrs:
239 self._flavor = attrs["flavor"]
241 self.user_data = attrs.get('user_data', '')
243 Server.list.append(self)
245 def override_ip(self, network_name, port):
246 def find_port_overrides():
248 # p can be string or dict
249 # we can't just use p[port['port'] in case p is a string
250 # and port['port'] is an int?
251 if isinstance(p, Mapping):
252 g = p.get(port['port'])
253 # filter out empty dicts
257 ports = self.network_ports.get(network_name, [])
258 intf = self.interfaces[port['port']]
259 for override in find_port_overrides():
260 intf['local_ip'] = override.get('local_ip', intf['local_ip'])
261 intf['netmask'] = override.get('netmask', intf['netmask'])
262 # only use the first value
267 """returns a server's image name"""
271 return self._context.image
275 """returns a server's flavor name"""
279 return self._context.flavor
281 def _add_instance(self, template, server_name, networks, scheduler_hints):
282 """adds to the template one server and corresponding resources"""
284 for network in networks:
285 # if explicit mapping skip unused networks
286 if self.network_ports:
288 ports = self.network_ports[network.name]
290 # no port for this network
293 if isinstance(ports, six.string_types):
294 # because strings are iterable we have to check specifically
295 raise SyntaxError("network_port must be a list '{}'".format(ports))
296 # convert port subdicts into their just port name
297 # port subdicts are used to override Heat IP address,
298 # but we just need the port name
299 # we allow duplicates here and let Heat raise the error
300 ports = [next(iter(p)) if isinstance(p, dict) else p for p in ports]
301 # otherwise add a port for every network with port name as network name
303 ports = [network.name]
305 port_name = "{0}-{1}-port".format(server_name, port)
306 self.ports.setdefault(network.name, []).append(
307 {"stack_name": port_name, "port": port})
308 # we can't use secgroups if port_security_enabled is False
309 if network.port_security_enabled is False:
312 # if port_security_enabled is None we still need to add to secgroup
313 sec_group_id = self.secgroup_name
314 # don't refactor to pass in network object, that causes JSON
315 # circular ref encode errors
316 template.add_port(port_name, network.stack_name, network.subnet_stack_name,
317 network.vnic_type, sec_group_id=sec_group_id,
318 provider=network.provider,
319 allowed_address_pairs=network.allowed_address_pairs)
320 port_name_list.append(port_name)
323 external_network = self.floating_ip["external_network"]
324 if network.has_route_to(external_network):
325 self.floating_ip["stack_name"] = server_name + "-fip"
326 template.add_floating_ip(self.floating_ip["stack_name"],
329 network.router.stack_if_name,
331 self.floating_ip_assoc["stack_name"] = \
332 server_name + "-fip-assoc"
333 template.add_floating_ip_association(
334 self.floating_ip_assoc["stack_name"],
335 self.floating_ip["stack_name"],
338 if isinstance(self.flavor, dict):
339 self.flavor["name"] = \
340 self.flavor.setdefault("name", self.stack_name + "-flavor")
341 template.add_flavor(**self.flavor)
342 self.flavor_name = self.flavor["name"]
344 self.flavor_name = self.flavor
347 if isinstance(self.volume, dict):
348 self.volume["name"] = \
349 self.volume.setdefault("name", server_name + "-volume")
350 template.add_volume(**self.volume)
351 template.add_volume_attachment(server_name, self.volume["name"],
352 mountpoint=self.volume_mountpoint)
354 template.add_volume_attachment(server_name, self.volume,
355 mountpoint=self.volume_mountpoint)
357 template.add_server(server_name, self.image, flavor=self.flavor_name,
358 flavors=self.context.flavors,
359 ports=port_name_list,
361 key_name=self.keypair_name,
362 user_data=self.user_data,
363 scheduler_hints=scheduler_hints)
365 def add_to_template(self, template, networks, scheduler_hints=None):
366 """adds to the template one or more servers (instances)"""
367 if self.instances == 1:
368 server_name = self.stack_name
369 self._add_instance(template, server_name, networks,
370 scheduler_hints=scheduler_hints)
372 # TODO(hafe) fix or remove, no test/sample for this
373 for i in range(self.instances):
374 server_name = "%s-%d" % (self.stack_name, i)
375 self._add_instance(template, server_name, networks,
376 scheduler_hints=scheduler_hints)
379 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
380 """update scheduler hints from server's placement configuration
381 TODO: this code is openstack specific and should move somewhere else
383 if placement_group.policy == "affinity":
384 if "same_host" in scheduler_hints:
385 host_list = scheduler_hints["same_host"]
387 host_list = scheduler_hints["same_host"] = []
389 if "different_host" in scheduler_hints:
390 host_list = scheduler_hints["different_host"]
392 host_list = scheduler_hints["different_host"] = []
394 for name in added_servers:
395 if name in placement_group.members:
396 host_list.append({'get_resource': name})