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