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