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