added spawn_instances()
[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.SliceTags import SliceTag, SliceTags
16 from PLC.Timestamp import Timestamp
17 from PLC.Storage.AlchemyObject import AlchemyObj
18
19 class Slice(AlchemyObj):
20     """
21     Representation of a row in the slices table. To use, optionally
22     instantiate with a dict of values. Update as you would a
23     dict. Commit to the database with sync().To use, instantiate
24     with a dict of values.
25     """
26
27     tablename = 'slices'
28  
29     fields = {
30         'slice_id': Parameter(int, "Slice identifier", primary_key=True),
31         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
32         'tenant_id': Parameter(int, "Keystone tenant identifier"), 
33         'name': Parameter(str, "Slice name", max = 32),
34         'instantiation': Parameter(str, "Slice instantiation state", nullok=True),
35         'url': Parameter(str, "URL further describing this slice", max = 254, nullok = True),
36         'description': Parameter(str, "Slice description", max = 2048, nullok = True),
37         'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice", default=100),
38         'creator_person_id': Parameter(str, "Identifier of the account that created this slice"),
39         'created': Parameter(datetime, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
40         'expires': Parameter(datetime, "Date and time when slice expires, in seconds since UNIX epoch"),
41         'node_ids': Parameter([str], "List of nodes in this slice", joined = True),
42         'person_ids': Parameter([str], "List of accounts that can use this slice", joined = True),
43         'slice_tag_ids': Parameter([int], "List of slice attributes", joined = True),
44         'peer_id': Parameter(int, "Peer to which this slice belongs", nullok = True),
45         'peer_slice_id': Parameter(int, "Foreign slice identifier at peer", nullok = True),
46         }
47     tags = {}
48
49     def validate_name(self, name):
50         # N.B.: Responsibility of the caller to ensure that login_base
51         # portion of the slice name corresponds to a valid site, if
52         # desired.
53
54         # 1. Lowercase.
55         # 2. Begins with login_base (letters or numbers).
56         # 3. Then single underscore after login_base.
57         # 4. Then letters, numbers, or underscores.
58         good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
59         if not name or \
60            not re.match(good_name, name):
61             raise PLCInvalidArgument, "Invalid slice name"
62
63         conflicts = Slices(self.api, [name])
64         for slice in conflicts:
65             if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
66                 raise PLCInvalidArgument, "Slice name already in use, %s"%name
67
68         return name
69
70     def validate_expires(self, expires):
71         # N.B.: Responsibility of the caller to ensure that expires is
72         # not too far into the future.
73         check_future = not ('is_deleted' in self and self['is_deleted'])
74         return Timestamp.sql_validate( expires, check_future = check_future)
75
76     def add_person(self, person_filter, role_name=None):
77         assert 'slice_id' in self
78         assert 'tenant_id' in self
79         if not role_name:
80             role_name = 'user'
81         roles = Roles(self.api, role_name)
82         if not roles:
83             raise PLCInvalidArgument, "No such role %s" % role_name
84         role = roles[0]
85         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
86         persons = Persons(self.api, person_filter)
87         for person in persons:
88             keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
89             tenant.add_user(keystone_user, role.object)
90             slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
91                                                   'person_id': person['person_id']})
92             slice_person.sync()
93
94     def remove_person(self, person_filter, role=None):
95         assert 'slice_id' in self
96         assert 'tenant_id' in self
97         if not role_name:
98             role_name = 'user'
99         roles = Roles(self.api, role_name)
100         if not roles:
101             raise PLCInvalidArgument, "No such role %s" % role_name
102         role = roles[0]
103         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
104         persons = Persons(self.api, person_filter)
105         for person in persons:
106             keystone_user = self.api.client_shell.keystone.users.find(id=person['keystone_id'])
107             tenant.remove_user(keystone_user, role.object)
108             slice_person = SlicePerson(self.api, {'slice_id': self['slice_id'],
109                                                   'person_id': person['person_id']})
110             slice_person.delete()
111  
112
113     def add_node(self, node_filter, commit=True):
114         from PLC.Nodes import Nodes
115         assert 'slice_id' in self
116         nodes = Nodes(self.api, node_filter)
117         for node in nodes:
118             slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
119                                               'node_id': node['node_id']})
120             slice_node.sync()
121
122     def spawn_instance(self, nodes, caller):
123         # defaults
124         default_image = self.api.config.nova_defualt_image
125         defualt_flavor = self.api.config.nova_default_flavor
126
127         # use the caller's nova keypair
128         keypairs = self.api.client_shell.nova.keypairs.list()
129         if not keypairs:
130             raise PLCInvalidArgument("caller has no nova key")
131         key_name = keypairs[0].name
132
133         # get all public keys
134         from PLC.Persons import Persons
135         from PLC.Keys import Keys
136         persons = Persons(self.api, self['person_ids'])
137         key_ids = []
138         for person in persons:
139             key_ids.extend(person['key_ids'])
140         keys = Keys(self.api, key_ids)
141         pubkeys = [k['key'] for k in keys]
142         authorized_keys = "\n".join(pubkeys)
143         files = {'/root/.ssh/authorized_keys': authorized_keys}
144   
145         # sort slice tags by node (sliver)
146         slice_tags = SliceTags(self.api, {'slice_id': self['slice_id']})
147         sliver_tags = defaultdict(dict)
148         for slice_tag in slice_tags:
149             node_id = slice_tag['node_id']
150             sliver_tags[node_id][slice_tag['tagname']] = slice_tag 
151
152         def get_image(node):
153             if sliver_tags[node['node_id']].get('image'):
154                 image = sliver_tags[node['node_id']].get('image')
155             else:
156                 image = default_image
157
158         def get_flavor(node):
159             if sliver_tags[node['node_id']].get('flavor'):
160                 flavor = sliver_tags[node['node_id']].get('flavor')
161             else:
162                 flavor = default_image            
163
164         for node in node:
165             if self['slice_id'] not in node['slice_ids']:
166                 image = get_image(node)
167                 flavor = get_flavor(node)
168                 flavor_id = api.client_shell.nova.flavors.find(name=flavor)
169                 image_id = api.client_shell.glance.get_image(name=image)
170                 api.client_shell.nova.servers.create(flavor=flavor_id,
171                                                      image=image_id,
172                                                      key_name = key_name,
173                                                      files=files,
174                                                      name=self['name'])              
175  
176     def remove_node(self, node_filter, commit=True):
177         from PLC.Nodes import Nodes 
178         assert 'slice_id' in self
179         nodes = Nodes(self.api, node_filter)
180         for node in nodes:
181             slice_node = SliceNode(self.api, {'slice_id': self['slice_id'],
182                                               'node_id': node['node_id']})
183             slice_node.delete()
184
185     #add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
186     #delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
187
188     def sync(self, commit = True, validate=True):
189         """
190         Add or update a slice.
191         """
192         # sync the nova record and the plc record
193         AlchemyObj.sync(self, commit=commit, validate=validate)
194         # create the nova record
195         nova_fields = ['enabled', 'description']
196         nova_can_update = lambda (field, value): field in nova_fields
197         nova_slice = dict(filter(nova_can_update, self.items()))
198         nova_slice['tenant_name'] = self['name']
199         if 'slice_id' not in self:
200             now = datetime.now()
201             # Before a new slice is added, delete expired slices
202             #expired = Slices(self.api, expires = -int(time.time()))
203             #for slice in expired:
204             #    slice.delete(commit)
205             self.object = self.api.client_shell.keystone.tenants.create(**nova_slice)
206             self['tenant_id'] = self.object.id
207             self['created'] = now
208             self['expires'] = now + timedelta(days=14)
209             AlchemyObj.insert(self, dict(self))
210             slice = AlchemyObj.select(self, filter={'tenant_id': self['tenant_id']})[0]
211             self['slice_id'] = slice.slice_id
212         else:
213             self.object = self.api.client_shell.keystone.tenants.update(self['tenant_id'], **nova_slice) 
214             AlchemyObj.updatedb(self, {'slice_id': self['slice_id']}, dict(self)) 
215
216     def delete(self, commit = True):
217         """
218         Delete existing slice.
219         """
220         assert 'slice_id' in self
221         assert 'tenant_id' in self
222
223         # delete the nova object
224         tenant = self.api.client_shell.keystone.tenants.find(id=self['tenant_id'])
225         self.api.client_shell.keystone.tenants.delete(tenant)
226
227         # delete relationships
228         for slice_person in SlicePerson().select(filter={'slice_id': self['slice_id']}):
229             slice_person.delete()
230         for slice_node in SliceNode().select(filter={'slice_id': self['slice_id']}):
231             slice_node.delete()
232         for slice_tag in SliceTag().select(filter={'slice_id': self['slice_id']}):
233             slice_tag.delete()
234         
235         # delete slice 
236         AlchemyObj.delete(self, filter={'slice_id': self['slice_id']})
237
238 class Slices(list):
239     """
240     Representation of row(s) from the slices table in the
241     database.
242     """
243
244     def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
245          
246         # the view that we're selecting upon: start with view_slices
247         if not slice_filter:
248             slices = Slice().select()
249         elif isinstance (slice_filter, StringTypes):
250             slices = Slice().select(filter={'name': slice_filter})
251         elif isinstance(slice_filter, dict):
252             slices = Slice().select(filter=slice_filter)
253         elif isinstance(slice_filter, (list, tuple, set)):
254             slices = Slice().select()
255             slices = [slice for slice in slices if slice.slice_id in slice_filter or slice.name in slice_filter]
256         else:
257             raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
258
259         for slice in slices:
260             slice = Slice(api, object=slice)
261             if not columns or 'person_ids' in columns:
262                 slice_persons = SlicePerson().select(filter={'slice_id': slice['slice_id']})
263                 slice['person_ids'] = [rec.person_id for rec in slice_persons] 
264                 
265             if not columns or 'node_ids' in columns:
266                 slice_nodes = SliceNode().select(filter={'slice_id': slice['slice_id']})
267                 slice['node_ids'] = [rec.node_id for rec in slice_nodes]
268
269             if not columns or 'slice_tag_ids' in columns:
270                 slice_tags = SliceTag().select(filter={'slice_id': slice['slice_id']})
271                 slice['slice_tag_ids'] = [rec.slice_tag_id for rec in slice_tags]
272                 
273             self.append(slice)
274
275     def refresh(self, api):
276         """
277         Import tenants from keystone.
278         """
279         # get current slices
280         slices = Slice().select()
281         slice_names = [slice.name for slice in slices]
282
283         # get current tenants
284         tenants = api.client_shell.keystone.tenants.list()
285
286         # add tenants that dont already exist
287         for tenant in tenants:
288             # site tenants should not contain '_'
289             if '_' in tenant.name and tenant.name not in slice_names:
290                 description = tenant.description
291                 if not description: description  = tenant.name
292                 slice = Slice(api, {'name': tenant.name,
293                                     'tenant_id': tenant.id,
294                                     'enabled': tenant.enabled,
295                                     'description': description,
296                                     'is_public': True})
297                 try:
298                     slice.sync()
299                 except:
300                     # slice may have a login base prefix that doesn't exist yet.
301                     pass