Merge "Dockerfile: apt-get clean to save layer space" into stable/euphrates
[yardstick.git] / yardstick / benchmark / contexts / model.py
1 ##############################################################################
2 # Copyright (c) 2015 Ericsson AB and others.
3 #
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 ##############################################################################
9
10 """ Logical model
11
12 """
13 from __future__ import absolute_import
14
15 import six
16 import logging
17
18 from collections import Mapping
19 from six.moves import range
20
21
22 LOG = logging.getLogger(__name__)
23
24
25 class Object(object):
26     """Base class for classes in the logical model
27     Contains common attributes and methods
28     """
29
30     def __init__(self, name, context):
31         # model identities and reference
32         self.name = name
33         self._context = context
34
35         # stack identities
36         self.stack_name = None
37         self.stack_id = None
38
39     @property
40     def dn(self):
41         """returns distinguished name for object"""
42         return self.name + "." + self._context.name
43
44
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)"
49     """
50     map = {}
51
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" %
55                              (name, policy))
56         self.name = name
57         self.members = set()
58         self.stack_name = context.name + "-" + name
59         self.policy = policy
60         PlacementGroup.map[name] = self
61
62     def add_member(self, name):
63         self.members.add(name)
64
65     @staticmethod
66     def get(name):
67         return PlacementGroup.map.get(name)
68
69
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"
73     """
74     map = {}
75
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" %
80                              (name, policy))
81         self.name = name
82         self.members = set()
83         self.stack_name = context.name + "-" + name
84         self.policy = policy
85         ServerGroup.map[name] = self
86
87     def add_member(self, name):
88         self.members.add(name)
89
90     @staticmethod
91     def get(name):
92         return ServerGroup.map.get(name)
93
94
95 class Router(Object):
96     """Class that represents a router in the logical model"""
97
98     def __init__(self, name, network_name, context, external_gateway_info):
99         super(Router, self).__init__(name, context)
100
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
104
105
106 class Network(Object):
107     """Class that represents a network in the logical model"""
108     list = []
109
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')
116         self.router = None
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', [])
124         try:
125             # we require 'null' or '' to disable setting gateway_ip
126             self.gateway_ip = attrs['gateway_ip']
127         except KeyError:
128             # default to explicit None
129             self.gateway_ip = None
130         else:
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"
134
135         if "external_network" in attrs:
136             self.router = Router("router", self.name,
137                                  context, attrs["external_network"])
138
139         Network.list.append(self)
140
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:
144             return True
145         return False
146
147     @staticmethod
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):
152                 return network
153
154     @staticmethod
155     def find_external_network():
156         """return the name of an external network some network in this
157         context has a route to
158         """
159         for network in Network.list:
160             if network.router:
161                 return network.router.external_gateway_info
162         return None
163
164
165 class Server(Object):     # pragma: no cover
166     """Class that represents a server in the logical model"""
167     list = []
168
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
178         self.user_data = ''
179         self.interfaces = {}
180
181         if attrs is None:
182             attrs = {}
183
184         self.placement_groups = []
185         placement = attrs.get("placement", [])
186         placement = placement if isinstance(placement, list) else [placement]
187         for p in placement:
188             pg = PlacementGroup.get(p)
189             if not pg:
190                 raise ValueError("server '%s', placement '%s' is invalid" %
191                                  (name, p))
192             self.placement_groups.append(pg)
193             pg.add_member(self.stack_name)
194
195         self.volume = None
196         if "volume" in attrs:
197             self.volume = attrs.get("volume")
198
199         self.volume_mountpoint = None
200         if "volume_mountpoint" in attrs:
201             self.volume_mountpoint = attrs.get("volume_mountpoint")
202
203         # support servergroup attr
204         self.server_group = None
205         sg = attrs.get("server_group")
206         if sg:
207             server_group = ServerGroup.get(sg)
208             if not server_group:
209                 raise ValueError("server '%s', server_group '%s' is invalid" %
210                                  (name, sg))
211             self.server_group = server_group
212             server_group.add_member(self.stack_name)
213
214         self.instances = 1
215         if "instances" in attrs:
216             self.instances = attrs["instances"]
217
218         # dict with key network name, each item is a dict with port name and ip
219         self.network_ports = attrs.get("network_ports", {})
220         self.ports = {}
221
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 = {}
227
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
232
233         self._image = None
234         if "image" in attrs:
235             self._image = attrs["image"]
236
237         self._flavor = None
238         if "flavor" in attrs:
239             self._flavor = attrs["flavor"]
240
241         self.user_data = attrs.get('user_data', '')
242         self.availability_zone = attrs.get('availability_zone')
243
244         Server.list.append(self)
245
246     def override_ip(self, network_name, port):
247         def find_port_overrides():
248             for p in ports:
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
255                     if g:
256                         yield g
257
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
264             break
265
266     @property
267     def image(self):
268         """returns a server's image name"""
269         if self._image:
270             return self._image
271         else:
272             return self._context.image
273
274     @property
275     def flavor(self):
276         """returns a server's flavor name"""
277         if self._flavor:
278             return self._flavor
279         else:
280             return self._context.flavor
281
282     def _add_instance(self, template, server_name, networks, scheduler_hints):
283         """adds to the template one server and corresponding resources"""
284         port_name_list = []
285         for network in networks:
286             # if explicit mapping skip unused networks
287             if self.network_ports:
288                 try:
289                     ports = self.network_ports[network.name]
290                 except KeyError:
291                     # no port for this network
292                     continue
293                 else:
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
303             else:
304                 ports = [network.name]
305             for port in ports:
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:
311                     sec_group_id = None
312                 else:
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)
322
323                 if self.floating_ip:
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"],
328                                                  external_network,
329                                                  port_name,
330                                                  network.router.stack_if_name,
331                                                  sec_group_id)
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"],
337                             port_name)
338         if self.flavor:
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"]
344             else:
345                 self.flavor_name = self.flavor
346
347         if self.volume:
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)
354             else:
355                 template.add_volume_attachment(server_name, self.volume,
356                                                mountpoint=self.volume_mountpoint)
357
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)
363
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)
370         else:
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)
376
377
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
381     """
382     if placement_group.policy == "affinity":
383         if "same_host" in scheduler_hints:
384             host_list = scheduler_hints["same_host"]
385         else:
386             host_list = scheduler_hints["same_host"] = []
387     else:
388         if "different_host" in scheduler_hints:
389             host_list = scheduler_hints["different_host"]
390         else:
391             host_list = scheduler_hints["different_host"] = []
392
393     for name in added_servers:
394         if name in placement_group.members:
395             host_list.append({'get_resource': name})