fix deploy without placement groups
[yardstick.git] / yardstick / benchmark / context / 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
14 import sys
15 import os
16
17 from yardstick.orchestrator.heat import HeatTemplate
18
19
20 class Object(object):
21     '''Base class for classes in the logical model
22     Contains common attributes and methods
23     '''
24     def __init__(self, name, context):
25         # model identities and reference
26         self.name = name
27         self._context = context
28
29         # stack identities
30         self.stack_name = None
31         self.stack_id = None
32
33     @property
34     def dn(self):
35         '''returns distinguished name for object'''
36         return self.name + "." + self._context.name
37
38
39 class PlacementGroup(Object):
40     '''Class that represents a placement group in the logical model
41     Concept comes from the OVF specification. Policy should be one of
42     "availability" or "affinity (there are more but they are not supported)"
43     '''
44     map = {}
45
46     def __init__(self, name, context, policy):
47         if policy not in ["affinity", "availability"]:
48             raise ValueError("placement group '%s', policy '%s' is not valid" %
49                              (name, policy))
50         self.name = name
51         self.members = set()
52         self.stack_name = context.name + "-" + name
53         self.policy = policy
54         PlacementGroup.map[name] = self
55
56     def add_member(self, name):
57         self.members.add(name)
58
59     @staticmethod
60     def get(name):
61         if name in PlacementGroup.map:
62             return PlacementGroup.map[name]
63         else:
64             return None
65
66
67 class Router(Object):
68     '''Class that represents a router in the logical model'''
69     def __init__(self, name, network_name, context, external_gateway_info):
70         super(Router, self).__init__(name, context)
71
72         self.stack_name = context.name + "-" + network_name + "-" + self.name
73         self.stack_if_name = self.stack_name + "-if0"
74         self.external_gateway_info = external_gateway_info
75
76
77 class Network(Object):
78     '''Class that represents a network in the logical model'''
79     list = []
80
81     def __init__(self, name, context, attrs):
82         super(Network, self).__init__(name, context)
83         self.stack_name = context.name + "-" + self.name
84         self.subnet_stack_name = self.stack_name + "-subnet"
85         self.subnet_cidr = attrs.get('cidr', '10.0.1.0/24')
86         self.router = None
87
88         if "external_network" in attrs:
89             self.router = Router("router", self.name,
90                                  context, attrs["external_network"])
91
92         Network.list.append(self)
93
94     def has_route_to(self, network_name):
95         '''determines if this network has a route to the named network'''
96         if self.router and self.router.external_gateway_info == network_name:
97             return True
98         return False
99
100     @staticmethod
101     def find_by_route_to(external_network):
102         '''finds a network that has a route to the specified network'''
103         for network in Network.list:
104             if network.has_route_to(external_network):
105                 return network
106
107     @staticmethod
108     def find_external_network():
109         '''return the name of an external network some network in this
110         context has a route to'''
111         for network in Network.list:
112             if network.router:
113                 return network.router.external_gateway_info
114         return None
115
116
117 class Server(Object):
118     '''Class that represents a server in the logical model'''
119     list = []
120
121     def __init__(self, name, context, attrs):
122         super(Server, self).__init__(name, context)
123         self.stack_name = context.name + "-" + self.name
124         self.keypair_name = context.keypair_name
125         self.secgroup_name = context.secgroup_name
126         self.context = context
127
128         if attrs is None:
129             attrs = {}
130
131         self.placement_groups = []
132         placement = attrs.get("placement", [])
133         placement = placement if type(placement) is list else [placement]
134         for p in placement:
135             pg = PlacementGroup.get(p)
136             if not pg:
137                 raise ValueError("server '%s', placement '%s' is invalid" %
138                                  (name, p))
139             self.placement_groups.append(pg)
140             pg.add_member(self.stack_name)
141
142         self.instances = 1
143         if "instances" in attrs:
144             self.instances = attrs["instances"]
145
146         # dict with key network name, each item is a dict with port name and ip
147         self.ports = {}
148
149         self.floating_ip = None
150         if "floating_ip" in attrs:
151             self.floating_ip = {}
152
153         if self.floating_ip is not None:
154             ext_net = Network.find_external_network()
155             assert ext_net is not None
156             self.floating_ip["external_network"] = ext_net
157
158         self._image = None
159         if "image" in attrs:
160             self._image = attrs["image"]
161
162         self._flavor = None
163         if "flavor" in attrs:
164             self._flavor = attrs["flavor"]
165
166         Server.list.append(self)
167
168     @property
169     def image(self):
170         '''returns a server's image name'''
171         if self._image:
172             return self._image
173         else:
174             return self._context.image
175
176     @property
177     def flavor(self):
178         '''returns a server's flavor name'''
179         if self._flavor:
180             return self._flavor
181         else:
182             return self._context.flavor
183
184     def _add_instance(self, template, server_name, networks, scheduler_hints):
185         '''adds to the template one server and corresponding resources'''
186         port_name_list = []
187         for network in networks:
188             port_name = server_name + "-" + network.name + "-port"
189             self.ports[network.name] = {"stack_name": port_name}
190             template.add_port(port_name, network.stack_name,
191                               network.subnet_stack_name,
192                               sec_group_id=self.secgroup_name)
193             port_name_list.append(port_name)
194
195             if self.floating_ip:
196                 external_network = self.floating_ip["external_network"]
197                 if network.has_route_to(external_network):
198                     self.floating_ip["stack_name"] = server_name + "-fip"
199                     template.add_floating_ip(self.floating_ip["stack_name"],
200                                              external_network,
201                                              port_name,
202                                              network.router.stack_if_name,
203                                              self.secgroup_name)
204
205         template.add_server(server_name, self.image, self.flavor,
206                             ports=port_name_list,
207                             key_name=self.keypair_name,
208                             scheduler_hints=scheduler_hints)
209
210     def add_to_template(self, template, networks, scheduler_hints=None):
211         '''adds to the template one or more servers (instances)'''
212         if self.instances == 1:
213             server_name = "%s-%s" % (template.name, self.name)
214             self._add_instance(template, server_name, networks,
215                                scheduler_hints=scheduler_hints)
216         else:
217             for i in range(self.instances):
218                 server_name = "%s-%s-%d" % (template.name, self.name, i)
219                 self._add_instance(template, server_name, networks,
220                                    scheduler_hints=scheduler_hints)
221
222
223 def update_scheduler_hints(scheduler_hints, added_servers, placement_group):
224     ''' update scheduler hints from server's placement configuration
225     TODO: this code is openstack specific and should move somewhere else
226     '''
227     if placement_group.policy == "affinity":
228         if "same_host" in scheduler_hints:
229             host_list = scheduler_hints["same_host"]
230         else:
231             host_list = scheduler_hints["same_host"] = []
232     else:
233         if "different_host" in scheduler_hints:
234             host_list = scheduler_hints["different_host"]
235         else:
236             host_list = scheduler_hints["different_host"] = []
237
238     for name in added_servers:
239         if name in placement_group.members:
240             host_list.append({'get_resource': name})
241
242
243 class Context(object):
244     '''Class that represents a context in the logical model'''
245     list = []
246
247     def __init__(self):
248         self.name = None
249         self.stack = None
250         self.networks = []
251         self.servers = []
252         self.placement_groups = []
253         self.keypair_name = None
254         self.secgroup_name = None
255         self._server_map = {}
256         self._image = None
257         self._flavor = None
258         self._user = None
259         Context.list.append(self)
260
261     def init(self, attrs):
262         '''initializes itself from the supplied arguments'''
263         self.name = attrs["name"]
264         self.keypair_name = self.name + "-key"
265         self.secgroup_name = self.name + "-secgroup"
266
267         if "image" in attrs:
268             self._image = attrs["image"]
269
270         if "flavor" in attrs:
271             self._flavor = attrs["flavor"]
272
273         if "user" in attrs:
274             self._user = attrs["user"]
275
276         if "placement_groups" in attrs:
277             for name, pgattrs in attrs["placement_groups"].items():
278                 pg = PlacementGroup(name, self, pgattrs["policy"])
279                 self.placement_groups.append(pg)
280
281         for name, netattrs in attrs["networks"].items():
282             network = Network(name, self, netattrs)
283             self.networks.append(network)
284
285         for name, serverattrs in attrs["servers"].items():
286             server = Server(name, self, serverattrs)
287             self.servers.append(server)
288             self._server_map[server.dn] = server
289
290     @property
291     def image(self):
292         '''returns application's default image name'''
293         return self._image
294
295     @property
296     def flavor(self):
297         '''returns application's default flavor name'''
298         return self._flavor
299
300     @property
301     def user(self):
302         '''return login user name corresponding to image'''
303         return self._user
304
305     def _add_resources_to_template(self, template):
306         '''add to the template the resources represented by this context'''
307         template.add_keypair(self.keypair_name)
308         template.add_security_group(self.secgroup_name)
309
310         for network in self.networks:
311             template.add_network(network.stack_name)
312             template.add_subnet(network.subnet_stack_name, network.stack_name,
313                                 network.subnet_cidr)
314
315             if network.router:
316                 template.add_router(network.router.stack_name,
317                                     network.router.external_gateway_info,
318                                     network.subnet_stack_name)
319                 template.add_router_interface(network.router.stack_if_name,
320                                               network.router.stack_name,
321                                               network.subnet_stack_name)
322
323         # create a list of servers sorted by increasing no of placement groups
324         list_of_servers = sorted(self.servers,
325                                  key=lambda s: len(s.placement_groups))
326
327         #
328         # add servers with scheduler hints derived from placement groups
329         #
330
331         # create list of servers with availability policy
332         availability_servers = []
333         for server in list_of_servers:
334             for pg in server.placement_groups:
335                 if pg.policy == "availability":
336                     availability_servers.append(server)
337                     break
338
339         # add servers with availability policy
340         added_servers = []
341         for server in availability_servers:
342             scheduler_hints = {}
343             for pg in server.placement_groups:
344                 update_scheduler_hints(scheduler_hints, added_servers, pg)
345             server.add_to_template(template, self.networks, scheduler_hints)
346             added_servers.append(server.stack_name)
347
348         # create list of servers with affinity policy
349         affinity_servers = []
350         for server in list_of_servers:
351             for pg in server.placement_groups:
352                 if pg.policy == "affinity":
353                     affinity_servers.append(server)
354                     break
355
356         # add servers with affinity policy
357         for server in affinity_servers:
358             if server.stack_name in added_servers:
359                 continue
360             scheduler_hints = {}
361             for pg in server.placement_groups:
362                 update_scheduler_hints(scheduler_hints, added_servers, pg)
363             server.add_to_template(template, self.networks, scheduler_hints)
364             added_servers.append(server.stack_name)
365
366         # add remaining servers with no placement group configured
367         for server in list_of_servers:
368             if len(server.placement_groups) == 0:
369                 server.add_to_template(template, self.networks, {})
370
371     def deploy(self):
372         '''deploys template into a stack using cloud'''
373         print "Deploying context as stack '%s' using auth_url %s" % (
374             self.name, os.environ.get('OS_AUTH_URL'))
375
376         template = HeatTemplate(self.name)
377         self._add_resources_to_template(template)
378
379         try:
380             self.stack = template.create()
381         except KeyboardInterrupt:
382             sys.exit("\nStack create interrupted")
383         except RuntimeError as err:
384             sys.exit("error: failed to deploy stack: '%s'" % err.args)
385         except Exception as err:
386             sys.exit("error: failed to deploy stack: '%s'" % err)
387
388         # Iterate the servers in this context and copy out needed info
389         for server in self.servers:
390             for port in server.ports.itervalues():
391                 port["ipaddr"] = self.stack.outputs[port["stack_name"]]
392
393             if server.floating_ip:
394                 server.floating_ip["ipaddr"] = \
395                     self.stack.outputs[server.floating_ip["stack_name"]]
396
397         print "Context deployed"
398
399     def undeploy(self):
400         '''undeploys stack from cloud'''
401         if self.stack:
402             print "Undeploying context (stack) '%s'" % self.name
403             self.stack.delete()
404             self.stack = None
405             print "Context undeployed"
406
407     @staticmethod
408     def get_server(dn):
409         '''lookup server object by DN
410
411         dn is a distinguished name including the context name'''
412         if "." not in dn:
413             raise ValueError("dn '%s' is malformed" % dn)
414
415         for context in Context.list:
416             if dn in context._server_map:
417                 return context._server_map[dn]
418
419         return None