added create_network(), delete_network(), create_subnet(), delete_subnet(), process_t...
[plcapi.git] / PLC / Slices.py
1 from types import StringTypes
2 import time
3 import re
4 from datetime import datetime, timedelta
5 from collections import defaultdict
6
7 from PLC.Faults import *
8 from PLC.Parameter import Parameter, Mixed
9 from PLC.Debug import profile
10 from PLC.Roles import Role, Roles
11 from PLC.Nodes import Node
12 from PLC.Persons import Person, Persons
13 from PLC.SlicePersons import SlicePerson, SlicePersons
14 from PLC.SliceNodes import SliceNode, SliceNodes
15 from PLC.SliceInstances import SliceInstance, SliceInstances
16 from PLC.SliceTags import SliceTag, SliceTags
17 from PLC.Timestamp import Timestamp
18 from PLC.Storage.AlchemyObject import AlchemyObj
19
20 class Slice(AlchemyObj):
21     """
22     Representation of a row in the slices table. To use, optionally
23     instantiate with a dict of values. Update as you would a
24     dict. Commit to the database with sync().To use, instantiate
25     with a dict of values.
26     """
27
28     tablename = 'slices'
29  
30     fields = {
31         'slice_id': Parameter(int, "Slice identifier", primary_key=True),
32         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
33         'tenant_id': Parameter(int, "Keystone tenant identifier"), 
34         'name': Parameter(str, "Slice name", max = 32),
35         'instantiation': Parameter(str, "Slice instantiation state", nullok=True),
36         'url': Parameter(str, "URL further describing this slice", max = 254, nullok = True),
37         'description': Parameter(str, "Slice description", max = 2048, nullok = True),
38         'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice", default=100),
39         'creator_person_id': Parameter(str, "Identifier of the account that created this slice"),
40         'created': Parameter(datetime, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
41         'expires': Parameter(datetime, "Date and time when slice expires, in seconds since UNIX epoch"),
42         'node_ids': Parameter([str], "List of nodes in this slice", joined = True),
43         'instance_ids': Parameter([str], "List of instances running under this slice", joined = True),
44         'person_ids': Parameter([str], "List of accounts that can use this slice", joined = True),
45         'slice_tag_ids': Parameter([int], "List of slice attributes", joined = True),
46         'peer_id': Parameter(int, "Peer to which this slice belongs", nullok = True),
47         'peer_slice_id': Parameter(int, "Foreign slice identifier at peer", nullok = True),
48         }
49     tags = {}
50
51     def validate_name(self, name):
52         # N.B.: Responsibility of the caller to ensure that login_base
53         # portion of the slice name corresponds to a valid site, if
54         # desired.
55
56         # 1. Lowercase.
57         # 2. Begins with login_base (letters or numbers).
58         # 3. Then single underscore after login_base.
59         # 4. Then letters, numbers, or underscores.
60         good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
61         if not name or \
62            not re.match(good_name, name):
63             raise PLCInvalidArgument, "Invalid slice name"
64
65         conflicts = Slices(self.api, [name])
66         for slice in conflicts:
67             if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
68                 raise PLCInvalidArgument, "Slice name already in use, %s"%name
69
70         return name
71
72     def validate_expires(self, expires):
73         # N.B.: Responsibility of the caller to ensure that expires is
74         # not too far into the future.
75         check_future = not ('is_deleted' in self and self['is_deleted'])
76         return Timestamp.sql_validate( expires, check_future = check_future)
77
78     def add_person(self, person_filter, role_name=None):
79         assert 'slice_id' in self
80         assert 'tenant_id' in self
81         if not role_name:
82             role_name = 'user'
83         roles = Roles(self.api, role_name)
84         if not roles:
85             raise PLCInvalidArgument, "No such role %s" % role_name
86         role = roles[0]
87         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
88         persons = Persons(self.api, person_filter)
89         for person in persons:
90             keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
91             tenant.add_user(keystone_user, role.object)
92             slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
93                                                   'person_id': person['person_id']})
94             slice_person.sync()
95
96     def remove_person(self, person_filter, role=None):
97         assert 'slice_id' in self
98         assert 'tenant_id' in self
99         if not role_name:
100             role_name = 'user'
101         roles = Roles(self.api, role_name)
102         if not roles:
103             raise PLCInvalidArgument, "No such role %s" % role_name
104         role = roles[0]
105         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
106         persons = Persons(self.api, person_filter)
107         for person in persons:
108             keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
109             tenant.remove_user(keystone_user, role.object)
110             slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
111                                                   'person_id': person['person_id']})
112             slice_person.delete()
113  
114
115     def add_node(self, node_filter, commit=True):
116         from PLC.Nodes import Nodes
117         assert 'slice_id' in self
118         nodes = Nodes(self.api, node_filter)
119         for node in nodes:
120             slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
121                                               'node_id': node['node_id']})
122             slice_node.sync()
123
124     def remove_node(self, node_filter, commit=True):
125         from PLC.Nodes import Nodes
126         assert 'slice_id' in self
127         nodes = Nodes(self.api, node_filter)
128         for node in nodes:
129             slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
130                                               'node_id': node['node_id']})
131             slice_node.delete()
132     
133
134     def spawn_instances(self, nodes):
135         # use the caller's nova keypair
136         keypairs = self.api.client_shell.nova.keypairs.list()
137         if not keypairs:
138             raise PLCInvalidArgument("caller has no nova key")
139         key_name = keypairs[0].name
140
141         # get all public keys
142         from PLC.Persons import Persons
143         from PLC.Keys import Keys
144         persons = Persons(self.api, self['person_ids'])
145         key_ids = []
146         for person in persons:
147             key_ids.extend(person['key_ids'])
148         keys = Keys(self.api, key_ids)
149         pubkeys = [k['key'] for k in keys]
150         authorized_keys = "\n".join(pubkeys)
151         files = {'/root/.ssh/authorized_keys': authorized_keys}
152   
153         # sort slice tags by node (sliver)
154         slice_tags = SliceTags(self.api, {'slice_id': self['slice_id']})
155         sliver_tags = defaultdict(dict)
156         for slice_tag in slice_tags:
157             node_id = slice_tag['node_id']
158             sliver_tags[node_id][slice_tag['tagname']] = slice_tag 
159
160         def get_image(node):
161             image = self.api.config.nova_default_image
162             slice_image = sliver_tags[None].get('image')
163             sliver_image = sliver_tags[node['node_id']].get('image') 
164             if sliver_image is not None:   # sliver tag
165                 image = sliver_image.get('value')
166             elif slice_image is not None:     # sliver tag
167                 image = slice_image.get('value')
168             return image
169
170         def get_flavor(node):
171             flavor = self.api.config.nova_default_flavor
172             slice_flavor = sliver_tags[None].get('flavor')
173             sliver_flavor = sliver_tags[node['node_id']].get('flavor')
174             if sliver_flavor is not None:  # sliver tag
175                 flavor = sliver_flavor.get('value') 
176             elif slice_flavor is not None:   # slice tag
177                 flavor = slice_flavor.get('value')
178
179             return flavor
180
181         def get_security_group(node):
182             security_group = self.api.config.nova_default_security_group
183             slice_security_group = sliver_tags[None].get('security_group')
184             sliver_security_group = sliver_tags[node['node_id']].get('security_group')
185             if sliver_security_group is not None:  # sliver tag
186                 security_group = slice_security_group.get('value')
187             elif slice_security_group is not None:   # slice tag
188                 security_group = slice_security_group.get('value')
189             return security_group
190
191         for node in nodes:
192             if self['slice_id'] not in node['slice_ids']:
193                 image = get_image(node)
194                 flavor = get_flavor(node)
195                 security_group = get_security_group(node)
196                 flavor_id = self.api.client_shell.nova.flavors.find(name=flavor)
197                 images = self.api.client_shell.glance.get_images(name=image)
198                 if not images:
199                     raise PLCInvalidArgument('Image bot found')
200                 image_id = images[0]['id']
201                 hints = {'force_hosts': node['hostname']}
202                 server = self.api.client_shell.nova.servers.create(
203                                                     name=self['name'],
204                                                     flavor=flavor_id,
205                                                     image=image_id,
206                                                     key_name = key_name,
207                                                     security_group = security_group,
208                                                     files=files,
209                                                     scheduler_hints=hints)
210                 slice_instance = SliceInstance(self.api, {'slice_id': self['slice_id'],
211                                                           'instance_id': server.id})
212                 slice_instance.sync()               
213
214     def destroy_instances(self, nodes):
215         hostnames = [n['hostname'] for n in nodes]
216         servers = self.api.client_shell.nova.servers.list()
217         for server in servers:
218             name = server.name
219             hostname = server._info['OS-EXT-SRV-ATTR:host']
220             if self['name'] == name and hostname in hostnames:
221                 instance_id = server.id
222                 self.api.client_shell.nova.servers.delete(server)
223                 AlchemyObj.delete(SliceInstance, filter={'slice_id': self['slice_id'],
224                                                          'instance_id': instance_id})
225              
226
227     def create_network(self):
228         self.api.client_shell.quantum.create_network(name=self['name'],
229                                                      admin_state_up=False)
230     def delete_network(self):
231         nets = self.api.client_shell.quantum.list_networks(name=self['name'],
232                                                            tenant_id=self['tenant_id'])['networks']
233         for net in nets:
234             # delete all subnets:
235             #subnets = self.api.client_shell.quantum.list_subnets(network_id=net['network_id'])['subnets']
236             for subnet_id in net['subnets']:
237                 self.delete_subnet(subnet_id)
238             self.api.client_shell.quantum.delete_network(net['id']) 
239     
240  
241     def create_subnet(self, cidr_ip, ip_version, start, end):
242         nets = self.api.client_shell.quantum.list_networks(name=self['name'],
243                                                            tenant_id=self['tenant_id'])['networks']
244         # cannot create a subnet if there is no network 
245         if not nets:
246             return
247         net = nets[0]
248         allocation_pools = [{'start': start, 'end': end}]
249         self.api.client_shell.quantum.create_subnet(network_id=net['id'],
250                                                     ip_version=ip_version,
251                                                     cidr=cidr_ip,
252                                                     allocation_pools=allocation_pools)        
253                                                             
254     def delete_subnet(self, id=None):
255         if id:
256             self.api.client_shell.quantum.delete_subnet(id=id)
257         else:
258             # delete all subnets
259             subnets = self.api.client_shell.quantum.list_subnets(name=self['name'],
260                                                                  tenant_id=self['tenant_id'])['subnets']
261             for subnet in subnets:
262                 self.api.client_shell.quantum.delete_subnet(id=id)
263
264
265     def process_tags(self):
266         # create a subnet for each subnet tag if one doesn't alredy exist
267         tags = SliceTags(self.api, filter={'slice_id': self['slice_id']})
268         subnet_cidr = None
269         subnet_start = None
270         subnet_end = None
271
272         for tag in tags:
273             if tag['tagname'] == 'subnet_cidr': 
274                 subnet_cidr = tag['value']
275             elif tag['tagname'] == 'subnet_start':
276                 subnet_start = tag['value']
277             elif tag['tagname'] == 'subnet_end':
278                 subnet_end = tag['value']
279
280         if subnet_cidr and subnet_start and subnet_end:
281             allocation_pools = [{'start': subnet_start, 'end': subnet_end}]
282             subnets = self.api.client_shell.quantum.list_subnets(name=self['name'],
283                                                                  tenant_id=self['tenant_id'],
284                                                                  cidr=subnet_cidr,
285                                                                  allocation_pools=allocation_pools)
286                                         
287     #add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
288     #delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
289
290     def sync(self, commit = True, validate=True):
291         """
292         Add or update a slice.
293         """
294         # sync the nova record and the plc record
295         AlchemyObj.sync(self, commit=commit, validate=validate)
296         # create the nova record
297         nova_fields = ['enabled', 'description']
298         nova_can_update = lambda (field, value): field in nova_fields
299         nova_slice = dict(filter(nova_can_update, self.items()))
300         nova_slice['tenant_name'] = self['name']
301         if 'slice_id' not in self:
302             now = datetime.now()
303             # Before a new slice is added, delete expired slices
304             #expired = Slices(self.api, expires = -int(time.time()))
305             #for slice in expired:
306             #    slice.delete(commit)
307             self.object = self.api.client_shell.keystone.tenants.create(**nova_slice)
308             self['tenant_id'] = self.object.id
309             self['created'] = now
310             self['expires'] = now + timedelta(days=14)
311             AlchemyObj.insert(self, dict(self))
312             slice = AlchemyObj.select(self, filter={'tenant_id': self['tenant_id']})[0]
313             self['slice_id'] = slice.slice_id
314         
315             # create quantum network
316             self.create_network()
317         else:
318             self.object = self.api.client_shell.keystone.tenants.update(self['tenant_id'], **nova_slice) 
319             AlchemyObj.updatedb(self, {'slice_id': self['slice_id']}, dict(self)) 
320
321     def delete(self, commit = True):
322         """
323         Delete existing slice.
324         """
325         assert 'slice_id' in self
326         assert 'tenant_id' in self
327
328         # delete quantum networks
329         self.delete_network()
330
331         # delete the nova object
332         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
333         self.api.client_shell.keystone.tenants.delete(tenant)
334
335         # delete relationships
336         for slice_person in SlicePerson().select(filter={'slice_id': self['slice_id']}):
337             slice_person.delete()
338         for slice_node in SliceNode().select(filter={'slice_id': self['slice_id']}):
339             slice_node.delete()
340         for slice_instance in SliceInstance().select(filter={'slice_id': self['slice_id']}):
341             slice_instance.delete()      
342         for slice_tag in SliceTag().select(filter={'slice_id': self['slice_id']}):
343             slice_tag.delete()
344         
345         # delete slice 
346         AlchemyObj.delete(self, filter={'slice_id': self['slice_id']})
347
348 class Slices(list):
349     """
350     Representation of row(s) from the slices table in the
351     database.
352     """
353
354     def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
355          
356         # the view that we're selecting upon: start with view_slices
357         if not slice_filter:
358             slices = Slice().select()
359         elif isinstance (slice_filter, StringTypes):
360             slices = Slice().select(filter={'name': slice_filter})
361         elif isinstance(slice_filter, dict):
362             slices = Slice().select(filter=slice_filter)
363         elif isinstance(slice_filter, (list, tuple, set)):
364             slices = Slice().select()
365             slices = [slice for slice in slices if slice.slice_id in slice_filter or slice.name in slice_filter]
366         else:
367             raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
368
369         for slice in slices:
370             slice = Slice(api, object=slice)
371             if not columns or 'person_ids' in columns:
372                 slice_persons = SlicePerson().select(filter={'slice_id': slice['slice_id']})
373                 slice['person_ids'] = [rec.person_id for rec in slice_persons]
374             
375             # we need to get the instance ids if node_ids is  specified
376             if not columns or 'instance_ids' in columns or 'node_ids' in columns:  
377                 slice_instances = SliceInstance().select(filter={'slice_id': slice['slice_id']})
378                 slice['instance_ids'] = [rec.instance_id for rec in slice_instances]
379             if not columns or 'node_ids' in columns:
380                 #slice_nodes = SliceNode().select(filter={'slice_id': slice['slice_id']})
381                 #slice['node_ids'] = [rec.node_id for rec in slice_nodes]
382                 # need to look up the manually look up each instance's host and query plc
383                 # for the node ids
384                 instances = api.client_shell.nova.servers.list()
385                 hostnames = [s._info['OS-EXT-SRV-ATTR:host'] for s in instances \
386                              if s.id in slice['instance_ids']]
387                 nodes = Node().select(filter={'hostname': hostnames})
388                 slice['node_ids'] = [rec.node_id for rec in nodes]
389
390             if not columns or 'slice_tag_ids' in columns:
391                 slice_tags = SliceTag().select(filter={'slice_id': slice['slice_id']})
392                 slice['slice_tag_ids'] = [rec.slice_tag_id for rec in slice_tags]
393                 
394             self.append(slice)
395
396     def refresh(self, api):
397         """
398         Import tenants from keystone.
399         """
400         # get current slices
401         slices = Slice().select()
402         slice_names = [slice.name for slice in slices]
403
404         # get current tenants
405         tenants = api.client_shell.keystone.tenants.list()
406
407         # add tenants that dont already exist
408         for tenant in tenants:
409             # site tenants should not contain '_'
410             if '_' in tenant.name and tenant.name not in slice_names:
411                 description = tenant.description
412                 if not description: description  = tenant.name
413                 slice = Slice(api, {'name': tenant.name,
414                                     'tenant_id': tenant.id,
415                                     'enabled': tenant.enabled,
416                                     'description': description,
417                                     'is_public': True})
418                 try:
419                     slice.sync()
420                 except:
421                     # slice may have a login base prefix that doesn't exist yet.
422                     pass