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', '')
242 self.availability_zone = attrs.get('availability_zone')
244 Server.list.append(self)
246 def override_ip(self, network_name, port):
247 def find_port_overrides():
249 # p can be string or dict
250 # we can't just use p[port['port'] in case p is a string
251 # and port['port'] is an int?
252 if isinstance(p, Mapping):
253 g = p.get(port['port'])
254 # filter out empty dicts
258 ports = self.network_ports.get(network_name, [])
259 intf = self.interfaces[port['port']]
260 for override in find_port_overrides():
261 intf['local_ip'] = override.get('local_ip', intf['local_ip'])
262 intf['netmask'] = override.get('netmask', intf['netmask'])
263 # only use the first value
268 """returns a server's image name"""
272 return self._context.image
276 """returns a server's flavor name"""
280 return self._context.flavor
282 def _add_instance(self, template, server_name, networks, scheduler_hints):
283 """adds to the template one server and corresponding resources"""
285 for network in networks:
286 # if explicit mapping skip unused networks
287 if self.network_ports:
289 ports = self.network_ports[network.name]
291 # no port for this network
294 if isinstance(ports, six.string_types):
295 # because strings are iterable we have to check specifically
296 raise SyntaxError("network_port must be a list '{}'".format(ports))
297 # convert port subdicts into their just port name
298 # port subdicts are used to override Heat IP address,
299 # but we just need the port name
300 # we allow duplicates here and let Heat raise the error
301 ports = [next(iter(p)) if isinstance(p, dict) else p for p in ports]
302 # otherwise add a port for every network with port name as network name
304 ports = [network.name]
306 port_name = "{0}-{1}-port".format(server_name, port)
307 self.ports.setdefault(network.name, []).append(
308 {"stack_name": port_name, "port": port})
309 # we can't use secgroups if port_security_enabled is False
310 if network.port_security_enabled is False:
313 # if port_security_enabled is None we still need to add to secgroup
314 sec_group_id = self.secgroup_name
315 # don't refactor to pass in network object, that causes JSON
316 # circular ref encode errors
317 template.add_port(port_name, network.stack_name, network.subnet_stack_name,
318 network.vnic_type, sec_group_id=sec_group_id,
319 provider=network.provider,
320 allowed_address_pairs=network.allowed_address_pairs)
321 port_name_list.append(port_name)
324 external_network = self.floating_ip["external_network"]
325 if network.has_route_to(external_network):
326 self.floating_ip["stack_name"] = server_name + "-fip"
327 template.add_floating_ip(self.floating_ip["stack_name"],
330 network.router.stack_if_name,
332 self.floating_ip_assoc["stack_name"] = \
333 server_name + "-fip-assoc"
334 template.add_floating_ip_association(
335 self.floating_ip_assoc["stack_name"],
336 self.floating_ip["stack_name"],
339 if isinstance(self.flavor, dict):
340 self.flavor["name"] = \
341 self.flavor.setdefault("name", self.stack_name + "-flavor")
342 template.add_flavor(**self.flavor)
343 self.flavor_name = self.flavor["name"]
345 self.flavor_name = self.flavor
348 if isinstance(self.volume, dict):
349 self.volume["name"] = \
350 self.volume.setdefault("name", server_name + "-volume")
351 template.add_volume(**self.volume)
352 template.add_volume_attachment(server_name, self.volume["name"],
353 mountpoint=self.volume_mountpoint)
355 template.add_volume_attachment(server_name, self.volume,
356 mountpoint=self.volume_mountpoint)
358 template.add_server(server_name, self.image, flavor=self.flavor_name,
359 flavors=self.context.flavors, ports=port_name_list,
360 scheduler_hints=scheduler_hints, user=self.user,
361 key_name=self.keypair_name, user_data=self.user_data,
362 availability_zone=self.availability_zone)
364 def add_to_template(self, template, networks, scheduler_hints=None):
365 """adds to the template one or more servers (instances)"""
366 if self.instances == 1:
367 server_name = self.stack_name
368 self._add_instance(template, server_name, networks,
369 scheduler_hints=scheduler_hints)
371 # TODO(hafe) fix or remove, no test/sample for this
372 for i in range(self.instances):
373 server_name = "%s-%d" % (self.stack_name, i)
374 self._add_instance(template, server_name, networks,
375 scheduler_hints=scheduler_hints)
378 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
379 """update scheduler hints from server's placement configuration
380 TODO: this code is openstack specific and should move somewhere else
382 if placement_group.policy == "affinity":
383 if "same_host" in scheduler_hints:
384 host_list = scheduler_hints["same_host"]
386 host_list = scheduler_hints["same_host"] = []
388 if "different_host" in scheduler_hints:
389 host_list = scheduler_hints["different_host"]
391 host_list = scheduler_hints["different_host"] = []
393 for name in added_servers:
394 if name in placement_group.members:
395 host_list.append({'get_resource': name})