Dockerfile: apt-get clean to save layer space
[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
243         Server.list.append(self)
244
245     def override_ip(self, network_name, port):
246         def find_port_overrides():
247             for p in ports:
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
254                     if g:
255                         yield g
256
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
263             break
264
265     @property
266     def image(self):
267         """returns a server's image name"""
268         if self._image:
269             return self._image
270         else:
271             return self._context.image
272
273     @property
274     def flavor(self):
275         """returns a server's flavor name"""
276         if self._flavor:
277             return self._flavor
278         else:
279             return self._context.flavor
280
281     def _add_instance(self, template, server_name, networks, scheduler_hints):
282         """adds to the template one server and corresponding resources"""
283         port_name_list = []
284         for network in networks:
285             # if explicit mapping skip unused networks
286             if self.network_ports:
287                 try:
288                     ports = self.network_ports[network.name]
289                 except KeyError:
290                     # no port for this network
291                     continue
292                 else:
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
302             else:
303                 ports = [network.name]
304             for port in ports:
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:
310                     sec_group_id = None
311                 else:
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)
321
322                 if self.floating_ip:
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"],
327                                                  external_network,
328                                                  port_name,
329                                                  network.router.stack_if_name,
330                                                  sec_group_id)
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"],
336                             port_name)
337         if self.flavor:
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"]
343             else:
344                 self.flavor_name = self.flavor
345
346         if self.volume:
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)
353             else:
354                 template.add_volume_attachment(server_name, self.volume,
355                                                mountpoint=self.volume_mountpoint)
356
357         template.add_server(server_name, self.image, flavor=self.flavor_name,
358                             flavors=self.context.flavors,
359                             ports=port_name_list,
360                             user=self.user,
361                             key_name=self.keypair_name,
362                             user_data=self.user_data,
363                             scheduler_hints=scheduler_hints)
364
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)
371         else:
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)
377
378
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
382     """
383     if placement_group.policy == "affinity":
384         if "same_host" in scheduler_hints:
385             host_list = scheduler_hints["same_host"]
386         else:
387             host_list = scheduler_hints["same_host"] = []
388     else:
389         if "different_host" in scheduler_hints:
390             host_list = scheduler_hints["different_host"]
391         else:
392             host_list = scheduler_hints["different_host"] = []
393
394     for name in added_servers:
395         if name in placement_group.members:
396             host_list.append({'get_resource': name})