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