e5f1823b537fa42f244ce663165993489ca254cb
[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 spawn_instances(self, nodes):
125         # use the caller's nova keypair
126         keypairs = self.api.client_shell.nova.keypairs.list()
127         if not keypairs:
128             raise PLCInvalidArgument("caller has no nova key")
129         key_name = keypairs[0].name
130
131         # get all public keys
132         from PLC.Persons import Persons
133         from PLC.Keys import Keys
134         persons = Persons(self.api, self['person_ids'])
135         key_ids = []
136         for person in persons:
137             key_ids.extend(person['key_ids'])
138         keys = Keys(self.api, key_ids)
139         pubkeys = [k['key'] for k in keys]
140         authorized_keys = "\n".join(pubkeys)
141         files = {'/root/.ssh/authorized_keys': authorized_keys}
142   
143         # sort slice tags by node (sliver)
144         slice_tags = SliceTags(self.api, {'slice_id': self['slice_id']})
145         sliver_tags = defaultdict(dict)
146         for slice_tag in slice_tags:
147             node_id = slice_tag['node_id']
148             sliver_tags[node_id][slice_tag['tagname']] = slice_tag 
149
150         def get_image(node):
151             image = self.api.config.nova_default_image
152             slice_image = sliver_tags[None].get('image')
153             sliver_image = sliver_tags[node['node_id']].get('image') 
154             if sliver_image is not None:   # sliver tag
155                 image = sliver_image.get('value')
156             elif slice_image is not None:     # sliver tag
157                 image = slice_image.get('value')
158             return image
159
160         def get_flavor(node):
161             flavor = self.api.config.nova_default_flavor
162             slice_flavor = sliver_tags[None].get('flavor')
163             sliver_flavor = sliver_tags[node['node_id']].get('flavor')
164             if sliver_flavor is not None:  # sliver tag
165                 flavor = sliver_flavor.get('value') 
166             elif slice_flavor is not None:   # slice tag
167                 flavor = slice_flavor.get('value')
168
169             return flavor
170
171         def get_security_group(node):
172             security_group = self.api.config.nova_default_security_group
173             slice_security_group = sliver_tags[None].get('security_group')
174             sliver_security_group = sliver_tags[node['node_id']].get('security_group')
175             if sliver_security_group is not None:  # sliver tag
176                 security_group = slice_security_group.get('value')
177             elif slice_security_group is not None:   # slice tag
178                 security_group = slice_security_group.get('value')
179             return security_group
180
181         for node in nodes:
182             if self['slice_id'] not in node['slice_ids']:
183                 image = get_image(node)
184                 flavor = get_flavor(node)
185                 security_group = get_security_group(node)
186                 flavor_id = self.api.client_shell.nova.flavors.find(name=flavor)
187                 images = self.api.client_shell.glance.get_images(name=image)
188                 if not images:
189                     raise PLCInvalidArgument('Image bot found')
190                 image_id = images[0]['id']
191                 hints = {'force_hosts': node['hostname']}
192                 server = self.api.client_shell.nova.servers.create(
193                                                     name=self['name'],
194                                                     flavor=flavor_id,
195                                                     image=image_id,
196                                                     key_name = key_name,
197                                                     security_group = security_group,
198                                                     files=files,
199                                                     scheduler_hints=hints)
200                 slice_instance = SliceInstance(self.api, {'slice_id': self['slice_id'],
201                                                           'instance_id': server.id})
202                 slice_instance.sync()               
203
204     def destroy_instances(self, nodes):
205         hostnames = [n['hostname'] for n in nodes]
206         servers = self.api.client_shell.nova.servers.list()
207         for server in servers:
208             name = server.name
209             hostname = server._info['OS-EXT-SRV-ATTR:host']
210             if self['name'] == name and hostname in hostnames:
211                 instance_id = server.id
212                 self.api.client_shell.nova.servers.delete(server)
213                 AlchemyObj.delete(SliceInstance, filter={'slice_id': self['slice_id'],
214                                                          'instance_id': instance_id})
215              
216  
217     def remove_node(self, node_filter, commit=True):
218         from PLC.Nodes import Nodes 
219         assert 'slice_id' in self
220         nodes = Nodes(self.api, node_filter)
221         for node in nodes:
222             slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
223                                               'node_id': node['node_id']})
224             slice_node.delete()
225
226     #add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
227     #delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
228
229     def sync(self, commit = True, validate=True):
230         """
231         Add or update a slice.
232         """
233         # sync the nova record and the plc record
234         AlchemyObj.sync(self, commit=commit, validate=validate)
235         # create the nova record
236         nova_fields = ['enabled', 'description']
237         nova_can_update = lambda (field, value): field in nova_fields
238         nova_slice = dict(filter(nova_can_update, self.items()))
239         nova_slice['tenant_name'] = self['name']
240         if 'slice_id' not in self:
241             now = datetime.now()
242             # Before a new slice is added, delete expired slices
243             #expired = Slices(self.api, expires = -int(time.time()))
244             #for slice in expired:
245             #    slice.delete(commit)
246             self.object = self.api.client_shell.keystone.tenants.create(**nova_slice)
247             self['tenant_id'] = self.object.id
248             self['created'] = now
249             self['expires'] = now + timedelta(days=14)
250             AlchemyObj.insert(self, dict(self))
251             slice = AlchemyObj.select(self, filter={'tenant_id': self['tenant_id']})[0]
252             self['slice_id'] = slice.slice_id
253         else:
254             self.object = self.api.client_shell.keystone.tenants.update(self['tenant_id'], **nova_slice) 
255             AlchemyObj.updatedb(self, {'slice_id': self['slice_id']}, dict(self)) 
256
257     def delete(self, commit = True):
258         """
259         Delete existing slice.
260         """
261         assert 'slice_id' in self
262         assert 'tenant_id' in self
263
264         # delete the nova object
265         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
266         self.api.client_shell.keystone.tenants.delete(tenant)
267
268         # delete relationships
269         for slice_person in SlicePerson().select(filter={'slice_id': self['slice_id']}):
270             slice_person.delete()
271         for slice_node in SliceNode().select(filter={'slice_id': self['slice_id']}):
272             slice_node.delete()
273         for slice_instance in SliceInstance().select(filter={'slice_id': self['slice_id']}):
274             slice_instance.delete()      
275         for slice_tag in SliceTag().select(filter={'slice_id': self['slice_id']}):
276             slice_tag.delete()
277         
278         # delete slice 
279         AlchemyObj.delete(self, filter={'slice_id': self['slice_id']})
280
281 class Slices(list):
282     """
283     Representation of row(s) from the slices table in the
284     database.
285     """
286
287     def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
288          
289         # the view that we're selecting upon: start with view_slices
290         if not slice_filter:
291             slices = Slice().select()
292         elif isinstance (slice_filter, StringTypes):
293             slices = Slice().select(filter={'name': slice_filter})
294         elif isinstance(slice_filter, dict):
295             slices = Slice().select(filter=slice_filter)
296         elif isinstance(slice_filter, (list, tuple, set)):
297             slices = Slice().select()
298             slices = [slice for slice in slices if slice.slice_id in slice_filter or slice.name in slice_filter]
299         else:
300             raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
301
302         for slice in slices:
303             slice = Slice(api, object=slice)
304             if not columns or 'person_ids' in columns:
305                 slice_persons = SlicePerson().select(filter={'slice_id': slice['slice_id']})
306                 slice['person_ids'] = [rec.person_id for rec in slice_persons]
307             
308             # we need to get the instance ids if node_ids is  specified
309             if not columns or 'instance_ids' in columns or 'node_ids' in columns:  
310                 slice_instances = SliceInstance().select(filter={'slice_id': slice['slice_id']})
311                 slice['instance_ids'] = [rec.instance_id for rec in slice_instances]
312             if not columns or 'node_ids' in columns:
313                 #slice_nodes = SliceNode().select(filter={'slice_id': slice['slice_id']})
314                 #slice['node_ids'] = [rec.node_id for rec in slice_nodes]
315                 # need to look up the manually look up each instance's host and query plc
316                 # for the node ids
317                 instances = api.client_shell.nova.servers.list()
318                 hostnames = [s._info['OS-EXT-SRV-ATTR:host'] for s in instances \
319                              if s.id in slice['instance_ids']]
320                 nodes = Node().select(filter={'hostname': hostnames})
321                 slice['node_ids'] = [rec.node_id for rec in nodes]
322
323             if not columns or 'slice_tag_ids' in columns:
324                 slice_tags = SliceTag().select(filter={'slice_id': slice['slice_id']})
325                 slice['slice_tag_ids'] = [rec.slice_tag_id for rec in slice_tags]
326                 
327             self.append(slice)
328
329     def refresh(self, api):
330         """
331         Import tenants from keystone.
332         """
333         # get current slices
334         slices = Slice().select()
335         slice_names = [slice.name for slice in slices]
336
337         # get current tenants
338         tenants = api.client_shell.keystone.tenants.list()
339
340         # add tenants that dont already exist
341         for tenant in tenants:
342             # site tenants should not contain '_'
343             if '_' in tenant.name and tenant.name not in slice_names:
344                 description = tenant.description
345                 if not description: description  = tenant.name
346                 slice = Slice(api, {'name': tenant.name,
347                                     'tenant_id': tenant.id,
348                                     'enabled': tenant.enabled,
349                                     'description': description,
350                                     'is_public': True})
351                 try:
352                     slice.sync()
353                 except:
354                     # slice may have a login base prefix that doesn't exist yet.
355                     pass