work in progress - passes simple tests
[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
50     def validate_name(self, name):
51         # N.B.: Responsibility of the caller to ensure that login_base
52         # portion of the slice name corresponds to a valid site, if
53         # desired.
54
55         # 1. Lowercase.
56         # 2. Begins with login_base (letters or numbers).
57         # 3. Then single underscore after login_base.
58         # 4. Then letters, numbers, or underscores.
59         good_name = r'^[a-z0-9]+_[a-zA-Z0-9_]+$'
60         if not name or \
61            not re.match(good_name, name):
62             raise PLCInvalidArgument, "Invalid slice name"
63
64         conflicts = Slices(self.api, [name])
65         for slice in conflicts:
66             if 'slice_id' not in self or self['slice_id'] != slice['slice_id']:
67                 raise PLCInvalidArgument, "Slice name already in use, %s"%name
68
69         return name
70
71     def validate_instantiation(self, instantiation):
72         instantiations = [row['instantiation'] for row in SliceInstantiations(self.api)]
73         if instantiation not in instantiations:
74             raise PLCInvalidArgument, "No such instantiation state"
75
76         return instantiation
77
78     validate_created = Row.validate_timestamp
79
80     def validate_expires(self, expires):
81         # N.B.: Responsibility of the caller to ensure that expires is
82         # not too far into the future.
83         check_future = not ('is_deleted' in self and self['is_deleted'])
84         return Row.validate_timestamp(self, expires, check_future = check_future)
85
86     add_person = Row.add_object(Person, 'slice_person')
87     remove_person = Row.remove_object(Person, 'slice_person')
88
89     add_node = Row.add_object(Node, 'slice_node')
90     remove_node = Row.remove_object(Node, 'slice_node')
91
92     add_to_node_whitelist = Row.add_object(Node, 'node_slice_whitelist')
93     delete_from_node_whitelist = Row.remove_object(Node, 'node_slice_whitelist')
94
95     def associate_persons(self, auth, field, value):
96         """
97         Adds persons found in value list to this slice (using AddPersonToSlice).
98         Deletes persons not found in value list from this slice (using DeletePersonFromSlice).
99         """
100         
101         assert 'person_ids' in self
102         assert 'slice_id' in self
103         assert isinstance(value, list)
104
105         (person_ids, emails) = self.separate_types(value)[0:2]
106
107         # Translate emails into person_ids      
108         if emails:
109             persons = Persons(self.api, emails, ['person_id']).dict('person_id')
110             person_ids += persons.keys()
111         
112         # Add new ids, remove stale ids
113         if self['person_ids'] != person_ids:
114             from PLC.Methods.AddPersonToSlice import AddPersonToSlice
115             from PLC.Methods.DeletePersonFromSlice import DeletePersonFromSlice
116             new_persons = set(person_ids).difference(self['person_ids'])
117             stale_persons = set(self['person_ids']).difference(person_ids)
118
119             for new_person in new_persons:
120                 AddPersonToSlice.__call__(AddPersonToSlice(self.api), auth, new_person, self['slice_id'])
121             for stale_person in stale_persons:
122                 DeletePersonFromSlice.__call__(DeletePersonFromSlice(self.api), auth, stale_person, self['slice_id'])
123
124     def associate_nodes(self, auth, field, value):
125         """
126         Adds nodes found in value list to this slice (using AddSliceToNodes).
127         Deletes nodes not found in value list from this slice (using DeleteSliceFromNodes).
128         """
129
130         from PLC.Nodes import Nodes
131
132         assert 'node_ids' in self
133         assert 'slice_id' in self
134         assert isinstance(value, list)
135         
136         (node_ids, hostnames) = self.separate_types(value)[0:2]
137         
138         # Translate hostnames into node_ids
139         if hostnames:
140             nodes = Nodes(self.api, hostnames, ['node_id']).dict('node_id')
141             node_ids += nodes.keys()
142         
143         # Add new ids, remove stale ids
144         if self['node_ids'] != node_ids:
145             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
146             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
147             new_nodes = set(node_ids).difference(self['node_ids'])
148             stale_nodes = set(self['node_ids']).difference(node_ids)
149             
150             if new_nodes:
151                 AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, self['slice_id'], list(new_nodes))
152             if stale_nodes:
153                 DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, self['slice_id'], list(stale_nodes))                        
154     def associate_slice_attributes(self, auth, fields, value):
155         """
156         Deletes slice_attribute_ids not found in value list (using DeleteSliceAttribute). 
157         Adds slice_attributes if slice_fields w/o slice_id is found (using AddSliceAttribute).
158         Updates slice_attribute if slice_fields w/ slice_id is found (using UpdateSlceiAttribute).  
159         """
160         
161         assert 'slice_attribute_ids' in self
162         assert isinstance(value, list)
163
164         (attribute_ids, blank, attributes) = self.separate_types(value)
165         
166         # There is no way to add attributes by id. They are
167         # associated with a slice when they are created.
168         # So we are only looking to delete here 
169         if self['slice_attribute_ids'] != attribute_ids:
170             from PLC.Methods.DeleteSliceAttribute import DeleteSliceAttribute
171             stale_attributes = set(self['slice_attribute_ids']).difference(attribute_ids)
172         
173             for stale_attribute in stale_attributes:
174                 DeleteSliceAttribute.__call__(DeleteSliceAttribute(self.api), auth, stale_attribute['slice_attribute_id'])              
175         
176         # If dictionary exists, we are either adding new
177         # attributes or updating existing ones.
178         if attributes:
179             from PLC.Methods.AddSliceAttribute import AddSliceAttribute
180             from PLC.Methods.UpdateSliceAttribute import UpdateSliceAttribute
181         
182             added_attributes = filter(lambda x: 'slice_attribute_id' not in x, attributes)
183             updated_attributes = filter(lambda x: 'slice_attribute_id' in x, attributes)
184
185             for added_attribute in added_attributes:
186                 if 'attribute_type' in added_attribute:
187                     type = added_attribute['attribute_type']
188                 elif 'attribute_type_id' in added_attribute:
189                     type = added_attribute['attribute_type_id']
190                 else:
191                     raise PLCInvalidArgument, "Must specify attribute_type or attribute_type_id"
192
193                 if 'value' in added_attribute:
194                     value = added_attribute['value']
195                 else:
196                     raise PLCInvalidArgument, "Must specify a value"
197                 
198                 if 'node_id' in added_attribute:
199                     node_id = added_attribute['node_id']
200                 else:
201                     node_id = None
202
203                 if 'nodegroup_id' in added_attribute:
204                     nodegroup_id = added_attribute['nodegroup_id']
205                 else:
206                     nodegroup_id = None 
207  
208                 AddSliceAttribute.__call__(AddSliceAttribute(self.api), auth, self['slice_id'], type, value, node_id, nodegroup_id)
209             for updated_attribute in updated_attributes:
210                 attribute_id = updated_attribute.pop('slice_attribute_id')
211                 if attribute_id not in self['slice_attribute_ids']:
212                     raise PLCInvalidArgument, "Attribute doesnt belong to this slice" 
213                 else:
214                     UpdateSliceAttribute.__call__(UpdateSliceAttribute(self.api), auth, attribute_id, updated_attribute)                 
215         
216     def sync(self, commit = True):
217         """
218         Add or update a slice.
219         """
220
221         # Before a new slice is added, delete expired slices
222         if 'slice_id' not in self:
223             expired = Slices(self.api, expires = -int(time.time()))
224             for slice in expired:
225                 slice.delete(commit)
226
227         Row.sync(self, commit)
228
229     def delete(self, commit = True):
230         """
231         Delete existing slice.
232         """
233
234         assert 'slice_id' in self
235
236         # Clean up miscellaneous join tables
237         for table in self.join_tables:
238             self.api.db.do("DELETE FROM %s WHERE slice_id = %d" % \
239                            (table, self['slice_id']))
240
241         # Mark as deleted
242         self['is_deleted'] = True
243         self.sync(commit)
244
245
246 class Slices(Table):
247     """
248     Representation of row(s) from the slices table in the
249     database.
250     """
251
252     def __init__(self, api, slice_filter = None, columns = None, expires = int(time.time())):
253         Table.__init__(self, api, Slice, columns)
254
255         sql = "SELECT %s FROM view_slices WHERE is_deleted IS False" % \
256               ", ".join(self.columns)
257
258         if expires is not None:
259             if expires >= 0:
260                 sql += " AND expires > %d" % expires
261             else:
262                 expires = -expires
263                 sql += " AND expires < %d" % expires
264
265         if slice_filter is not None:
266             if isinstance(slice_filter, (list, tuple, set)):
267                 # Separate the list into integers and strings
268                 ints = filter(lambda x: isinstance(x, (int, long)), slice_filter)
269                 strs = filter(lambda x: isinstance(x, StringTypes), slice_filter)
270                 slice_filter = Filter(Slice.fields, {'slice_id': ints, 'name': strs})
271                 sql += " AND (%s) %s" % slice_filter.sql(api, "OR")
272             elif isinstance(slice_filter, dict):
273                 slice_filter = Filter(Slice.fields, slice_filter)
274                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
275             elif isinstance (slice_filter, StringTypes):
276                 slice_filter = Filter(Slice.fields, {'name':[slice_filter]})
277                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
278             elif isinstance (slice_filter, int):
279                 slice_filter = Filter(Slice.fields, {'slice_id':[slice_filter]})
280                 sql += " AND (%s) %s" % slice_filter.sql(api, "AND")
281             else:
282                 raise PLCInvalidArgument, "Wrong slice filter %r"%slice_filter
283
284         self.selectall(sql)