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