svn keywords
[plcapi.git] / PLC / Slices.py
1 # $Id$
2 # $URL$
3 from types import StringTypes
4 import time
5 import re
6
7 from PLC.Faults import *
8 from PLC.Parameter import Parameter, Mixed
9 from PLC.Filter import Filter
10 from PLC.Debug import profile
11 from PLC.Table import Row, Table
12 from PLC.SliceInstantiations import SliceInstantiation, SliceInstantiations
13 from PLC.Nodes import Node
14 from PLC.Persons import Person, Persons
15 from PLC.SliceTags import SliceTag
16
17 class Slice(Row):
18     """
19     Representation of a row in the slices table. To use, optionally
20     instantiate with a dict of values. Update as you would a
21     dict. Commit to the database with sync().To use, instantiate
22     with a dict of values.
23     """
24
25     table_name = 'slices'
26     primary_key = 'slice_id'
27     join_tables = ['slice_node', 'slice_person', 'slice_tag', 'peer_slice', 'node_slice_whitelist']
28     fields = {
29         'slice_id': Parameter(int, "Slice identifier"),
30         'site_id': Parameter(int, "Identifier of the site to which this slice belongs"),
31         'name': Parameter(str, "Slice name", max = 32),
32         'instantiation': Parameter(str, "Slice instantiation state"),
33         'url': Parameter(str, "URL further describing this slice", max = 254, nullok = True),
34         'description': Parameter(str, "Slice description", max = 2048, nullok = True),
35         'max_nodes': Parameter(int, "Maximum number of nodes that can be assigned to this slice"),
36         'creator_person_id': Parameter(int, "Identifier of the account that created this slice"),
37         'created': Parameter(int, "Date and time when slice was created, in seconds since UNIX epoch", ro = True),
38         'expires': Parameter(int, "Date and time when slice expires, in seconds since UNIX epoch"),
39         'node_ids': Parameter([int], "List of nodes in this slice", ro = True),
40         'person_ids': Parameter([int], "List of accounts that can use this slice", ro = True),
41         'slice_tag_ids': Parameter([int], "List of slice attributes", ro = True),
42         'peer_id': Parameter(int, "Peer to which this slice belongs", nullok = True),
43         'peer_slice_id': Parameter(int, "Foreign slice identifier at peer", nullok = True),
44         }
45     related_fields = {
46         'persons': [Mixed(Parameter(int, "Person identifier"),
47                           Parameter(str, "Email address"))],
48         'nodes': [Mixed(Parameter(int, "Node identifier"),
49                         Parameter(str, "Fully qualified hostname"))]
50         }
51
52     view_tags_name="view_slice_tags"
53     tags = {}
54
55     def validate_name(self, name):
56         # N.B.: Responsibility of the caller to ensure that login_base
57         # portion of the slice name corresponds to a valid site, if
58         # desired.
59
60         # 1. Lowercase.
61         # 2. Begins with login_base (letters or numbers).
62         # 3. Then single underscore after login_base.
63         # 4. Then letters, numbers, or underscores.
64         good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
65         if not name or \
66            not re.match(good_name, name):
67             raise PLCInvalidArgument, "Invalid slice name"
68
69         conflicts = Slices(self.api, [name])
70         for slice in conflicts:
71             if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
72                 raise PLCInvalidArgument, "Slice name already in use, %s"%name
73
74         return name
75
76     def validate_instantiation(self, instantiation):
77         instantiations = [row['instantiation'] for row in SliceInstantiations(self.api)]
78         if instantiation not in instantiations:
79             raise PLCInvalidArgument, "No such instantiation state"
80
81         return instantiation
82
83     validate_created = Row.validate_timestamp
84
85     def validate_expires(self, expires):
86         # N.B.: Responsibility of the caller to ensure that expires is
87         # not too far into the future.
88         check_future = not ('is_deleted' in self and self['is_deleted'])
89         return Row.validate_timestamp(self, expires, check_future = check_future)
90
91     add_person = Row.add_object(Person, 'slice_person')
92     remove_person = Row.remove_object(Person, 'slice_person')
93
94     add_node = Row.add_object(Node, 'slice_node')
95     remove_node = Row.remove_object(Node, 'slice_node')
96
97     add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
98     delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
99
100     def associate_persons(self, auth, field, value):
101         """
102         Adds persons found in value list to this slice (using AddPersonToSlice).
103         Deletes persons not found in value list from this slice (using DeletePersonFromSlice).
104         """
105         
106         assert 'person_ids' in self
107         assert 'slice_id' in self
108         assert isinstance(value, list)
109
110         (person_ids, emails) = self.separate_types(value)[0:2]
111
112         # Translate emails into person_ids      
113         if emails:
114             persons = Persons(self.api, emails, ['person_id']).dict('person_id')
115             person_ids += persons.keys()
116         
117         # Add new ids, remove stale ids
118         if self['person_ids'] != person_ids:
119             from PLC.Methods.AddPersonToSlice import AddPersonToSlice
120             from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
121             new_persons = set(person_ids).difference(self['person_ids'])
122             stale_persons = set(self['person_ids']).difference(person_ids)
123
124             for new_person in new_persons:
125                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, new_person, self['slice_id'])
126             for stale_person in stale_persons:
127                 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, stale_person, self['slice_id'])
128
129     def associate_nodes(self, auth, field, value):
130         """
131         Adds nodes found in value list to this slice (using AddSliceToNodes).
132         Deletes nodes not found in value list from this slice (using DeleteSliceFromNodes).
133         """
134
135         from PLC.Nodes import Nodes
136
137         assert 'node_ids' in self
138         assert 'slice_id' in self
139         assert isinstance(value, list)
140         
141         (node_ids, hostnames) = self.separate_types(value)[0:2]
142         
143         # Translate hostnames into node_ids
144         if hostnames:
145             nodes = Nodes(self.api, hostnames, ['node_id']).dict('node_id')
146             node_ids += nodes.keys()
147         
148         # Add new ids, remove stale ids
149         if self['node_ids'] != node_ids:
150             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
151             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
152             new_nodes = set(node_ids).difference(self['node_ids'])
153             stale_nodes = set(self['node_ids']).difference(node_ids)
154             
155             if new_nodes:
156                 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, self['slice_id'], list(new_nodes))
157             if stale_nodes:
158                 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, self['slice_id'], list(stale_nodes))                        
159     def associate_slice_tags(self, auth, fields, value):
160         """
161         Deletes slice_tag_ids not found in value list (using DeleteSliceTag). 
162         Adds slice_tags if slice_fields w/o slice_id is found (using AddSliceTag).
163         Updates slice_tag if slice_fields w/ slice_id is found (using UpdateSlceiAttribute).  
164         """
165         
166         assert 'slice_tag_ids' in self
167         assert isinstance(value, list)
168
169         (attribute_ids, blank, attributes) = self.separate_types(value)
170         
171         # There is no way to add attributes by id. They are
172         # associated with a slice when they are created.
173         # So we are only looking to delete here 
174         if self['slice_tag_ids'] != attribute_ids:
175             from PLC.Methods.DeleteSliceTag import DeleteSliceTag
176             stale_attributes = set(self['slice_tag_ids']).difference(attribute_ids)
177         
178             for stale_attribute in stale_attributes:
179                 DeleteSliceTag.__call__(DeleteSliceTag(self.api), auth, stale_attribute['slice_tag_id'])                
180         
181         # If dictionary exists, we are either adding new
182         # attributes or updating existing ones.
183         if attributes:
184             from PLC.Methods.AddSliceTag import AddSliceTag
185             from PLC.Methods.UpdateSliceTag import UpdateSliceTag
186         
187             added_attributes = filter(lambda x: 'slice_tag_id' not in x, attributes)
188             updated_attributes = filter(lambda x: 'slice_tag_id' in x, attributes)
189
190             for added_attribute in added_attributes:
191                 if 'tag_type' in added_attribute:
192                     type = added_attribute['tag_type']
193                 elif 'tag_type_id' in added_attribute:
194                     type = added_attribute['tag_type_id']
195                 else:
196                     raise PLCInvalidArgument, "Must specify tag_type or tag_type_id"
197
198                 if 'value' in added_attribute:
199                     value = added_attribute['value']
200                 else:
201                     raise PLCInvalidArgument, "Must specify a value"
202                 
203                 if 'node_id' in added_attribute:
204                     node_id = added_attribute['node_id']
205                 else:
206                     node_id = None
207
208                 if 'nodegroup_id' in added_attribute:
209                     nodegroup_id = added_attribute['nodegroup_id']
210                 else:
211                     nodegroup_id = None 
212  
213                 AddSliceTag.__call__(AddSliceTag(self.api), auth, self['slice_id'], type, value, node_id, nodegroup_id)
214             for updated_attribute in updated_attributes:
215                 attribute_id = updated_attribute.pop('slice_tag_id')
216                 if attribute_id not in self['slice_tag_ids']:
217                     raise PLCInvalidArgument, "Attribute doesnt belong to this slice" 
218                 else:
219                     UpdateSliceTag.__call__(UpdateSliceTag(self.api), auth, attribute_id, updated_attribute)             
220         
221     def sync(self, commit = True):
222         """
223         Add or update a slice.
224         """
225
226         # Before a new slice is added, delete expired slices
227         if 'slice_id' not in self:
228             expired = Slices(self.api, expires = -int(time.time()))
229             for slice in expired:
230                 slice.delete(commit)
231
232         Row.sync(self, commit)
233
234     def delete(self, commit = True):
235         """
236         Delete existing slice.
237         """
238
239         assert 'slice_id' in self
240
241         # Clean up miscellaneous join tables
242         for table in self.join_tables:
243             self.api.db.do("DELETE FROM %s WHERE slice_id = %d" % \
244                            (table, self['slice_id']))
245
246         # Mark as deleted
247         self['is_deleted'] = True
248         self.sync(commit)
249
250
251 class Slices(Table):
252     """
253     Representation of row(s) from the slices table in the
254     database.
255     """
256
257     def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
258         Table.__init__(self, api, Slice, columns)
259
260         # the view that we're selecting upon: start with view_slices
261         view = "view_slices"
262         # as many left joins as requested tags
263         for tagname in self.tag_columns:
264             view= "%s left join %s using (%s)"%(view,Slice.tagvalue_view_name(tagname),
265                                                 Slice.primary_key)
266             
267         sql = "SELECT %s FROM %s WHERE is_deleted IS False" % \
268               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
269
270         if expires is not None:
271             if expires >= 0:
272                 sql += " AND expires > %d" % expires
273             else:
274                 expires = -expires
275                 sql += " AND expires < %d" % expires
276
277         if slice_filter is not None:
278             if isinstance(slice_filter, (list, tuple, set)):
279                 # Separate the list into integers and strings
280                 ints = filter(lambda x: isinstance(x, (int, long)), slice_filter)
281                 strs = filter(lambda x: isinstance(x, StringTypes), slice_filter)
282                 slice_filter = Filter(Slice.fields, {'slice_id': ints, 'name': strs})
283                 sql += " AND (%s) %s" % slice_filter.sql(api, "OR")
284             elif isinstance(slice_filter, dict):
285                 slice_filter = Filter(Slice.fields, slice_filter)
286                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
287             elif isinstance (slice_filter, StringTypes):
288                 slice_filter = Filter(Slice.fields, {'name':[slice_filter]})
289                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
290             elif isinstance (slice_filter, int):
291                 slice_filter = Filter(Slice.fields, {'slice_id':[slice_filter]})
292                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
293             else:
294                 raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
295
296         self.selectall(sql)