define_accessors accepts a list of classes if needed
[plcapi.git] / PLC / Accessors / Factory.py
1 #
2 # Thierry Parmentelat - INRIA
3 #
4 from types import NoneType
5
6 from PLC.Faults import *
7
8 from PLC.Auth import Auth
9 from PLC.Parameter import Parameter, Mixed
10 from PLC.Method import Method
11 from PLC.Accessor import Accessor, AccessorSingleton
12
13 from PLC.Nodes import Nodes, Node
14 from PLC.NodeTags import NodeTags, NodeTag
15 from PLC.Interfaces import Interfaces, Interface
16 from PLC.InterfaceTags import InterfaceTags, InterfaceTag
17 from PLC.Slices import Slices, Slice
18 from PLC.SliceTags import SliceTags, SliceTag
19 from PLC.Sites import Sites, Site
20 from PLC.SiteTags import SiteTags, SiteTag
21 from PLC.Persons import Persons, Person
22 from PLC.PersonTags import PersonTags, PersonTag
23
24 # need to import so the core classes get decorated with caller_may_write_tag
25 from PLC.AuthorizeHelpers import AuthorizeHelpers
26
27 # known classes : { class -> details }
28 taggable_classes = { Node : {'table_class' : Nodes,
29                              'joins_class' : NodeTags, 'join_class' : NodeTag,
30                              'secondary_key': 'hostname'},
31                      Interface : {'table_class' : Interfaces,
32                                   'joins_class': InterfaceTags, 'join_class': InterfaceTag,
33                                   'secondary_key' : 'ip'},
34                      Slice: {'table_class' : Slices,
35                              'joins_class': SliceTags, 'join_class': SliceTag,
36                              'secondary_key':'name'},
37                      Site: {'table_class' : Sites,
38                              'joins_class': SiteTags, 'join_class': SiteTag,
39                              'secondary_key':'login_base'},
40                      Person: {'table_class' : Persons,
41                              'joins_class': PersonTags, 'join_class': PersonTag,
42                              'secondary_key':'email'},
43                      }
44
45 # xxx probably defined someplace else
46 admin_roles = ['admin']
47 person_roles = [ 'admin', 'pi', 'tech', 'user' ]
48 all_roles = [ 'admin', 'pi', 'tech', 'user', 'node' ]
49 tech_roles = [ 'admin', 'pi', 'tech' ]
50
51 #
52 # generates 2 method classes:
53 # Get<classname><methodsuffix> (auth, id_or_name) -> value or None
54 # Set<classname><methodsuffix> (auth, id_or_name, value) -> value
55 # value is always a string, no cast nor typecheck for now
56 #
57 # The expose_in_api flag tells whether this tag may be handled
58 #   through the Add/Get/Update methods as a native field
59 #
60 # note: roles get attached to the tagtype instance,
61 # while get_roles and set_roles get attached to the created methods
62 # this might need a cleanup
63 #
64 # in addition a convenience method like e.g. LocateNodeArch is defined
65 # in the Accessor class; its purpose is to retrieve the tag, or to create it if needed
66
67 # Legacy NOTE:
68 # prior to plcapi-5.0-19, this used to accept an additional argument
69 # named min_role_id; this was redundant and confusing, it has been
70 # removed, we now use set_roles to restrict access on the corresponding tag
71
72 # the convention here is that methodsuffix should be mixed case, e.g. MyStuff
73 # while tagname is expected to be lowercase
74 # you then end up with e.g. GetPersonMyStuff
75
76 # the entry point accepts a single class or a list of classes
77 def define_accessors (module, objclasses, *args, **kwds):
78     if not isinstance(objclasses,list):
79         objclasses=[objclasses]
80     for objclass in objclasses:
81         define_accessors_ (module, objclass, *args, **kwds)
82
83 # this is for one class
84 def define_accessors_ (module, objclass, methodsuffix, tagname,
85                        category, description,
86                        get_roles=all_roles, set_roles=admin_roles, 
87                        expose_in_api = False):
88
89     if objclass not in taggable_classes:
90         try:
91             raise PLCInvalidArgument,"PLC.Accessors.Factory: unknown class %s"%objclass.__name__
92         except:
93             raise PLCInvalidArgument,"PLC.Accessors.Factory: unknown class ??"
94
95     # side-effect on, say, Node.tags, if required
96     if expose_in_api:
97         getattr(objclass,'tags')[tagname]=Parameter(str,"accessor")
98
99     classname=objclass.__name__
100     get_name = "Get" + classname + methodsuffix
101     set_name = "Set" + classname + methodsuffix
102     locator_name = "Locate" + classname + methodsuffix
103
104     # accessor method objects under PLC.Method.Method
105     get_class = type (get_name, (Method,),
106                       {"__doc__":"Accessor 'get' method designed for %s objects using tag %s"%\
107                            (classname,tagname)})
108     set_class = type (set_name, (Method,),
109                       {"__doc__":"Accessor 'set' method designed for %s objects using tag %s"%\
110                            (classname,tagname)})
111
112     # accepts
113     get_accepts = [ Auth () ]
114     primary_key=objclass.primary_key
115     secondary_key = taggable_classes[objclass]['secondary_key']
116     get_accepts += [ Mixed (objclass.fields[primary_key], objclass.fields[secondary_key]) ]
117     # for set, idem set of arguments + one additional arg, the new value
118     set_accepts = get_accepts + [ Parameter (str,"New tag value") ]
119
120     # returns
121     get_returns = Mixed (Parameter (str), Parameter(NoneType))
122     set_returns = Parameter(NoneType)
123
124     # store in classes
125     setattr(get_class,'roles',get_roles)
126     setattr(get_class,'accepts',get_accepts)
127     setattr(get_class,'returns', get_returns)
128 # that was useful for legacy method only, but we now need type_checking
129 #    setattr(get_class,'skip_type_check',True)
130
131     setattr(set_class,'roles',set_roles)
132     setattr(set_class,'accepts',set_accepts)
133     setattr(set_class,'returns', set_returns)
134 # that was useful for legacy method only, but we now need type_checking
135 #    setattr(set_class,'skip_type_check',True)
136
137     table_class = taggable_classes[objclass]['table_class']
138     joins_class = taggable_classes[objclass]['joins_class']
139     join_class = taggable_classes[objclass]['join_class']
140
141     # locate the tag and create it if needed
142     # this method is attached to the Accessor class
143     def tag_locator (self, enforce=False):
144         return self.locate_or_create_tag (tagname=tagname,
145                                           category=category,
146                                           description=description,
147                                           roles=set_roles,
148                                           enforce=enforce)
149
150     # attach it to the Accessor class
151     Accessor.register_tag_locator(locator_name,tag_locator)
152
153     # body of the get method
154     def get_call (self, auth, id_or_name):
155         # locate the tag, see above
156         tag_locator = Accessor.retrieve_tag_locator(locator_name)
157         tag_type = tag_locator(AccessorSingleton(self.api))
158         tag_type_id=tag_type['tag_type_id']
159
160         filter = {'tag_type_id':tag_type_id}
161         if isinstance (id_or_name,int):
162             filter[primary_key]=id_or_name
163         else:
164             filter[secondary_key]=id_or_name
165         joins = joins_class (self.api,filter,['value'])
166         if not joins:
167             # xxx - we return None even if id_or_name is not valid
168             return None
169         else:
170             return joins[0]['value']
171
172     # attach it
173     setattr (get_class,"call",get_call)
174
175     # body of the set method
176     def set_call (self, auth, id_or_name, value):
177         # locate the object
178         if isinstance (id_or_name, int):
179             filter={primary_key:id_or_name}
180         else:
181             filter={secondary_key:id_or_name}
182 # we need the full monty b/c of the permission system
183 #        objs = table_class(self.api, filter,[primary_key,secondary_key])
184         objs = table_class(self.api, filter)
185         if not objs:
186             raise PLCInvalidArgument, "Cannot set tag on %s %r"%(objclass.__name__,id_or_name)
187         # the object being tagged
188         obj=objs[0]
189         primary_id = obj[primary_key]
190
191         # locate the tag, see above
192         tag_locator = Accessor.retrieve_tag_locator(locator_name)
193         tag_type = tag_locator(AccessorSingleton(self.api))
194         tag_type_id = tag_type['tag_type_id']
195
196         # check authorization
197         if not hasattr(objclass,'caller_may_write_tag'):
198             raise PLCAuthenticationFailure, "class %s misses method caller_may_write_tag"%objclass.__name__
199         obj.caller_may_write_tag (self.api,self.caller,tag_type)
200
201         # locate the join object (e.g. NodeTag or similar)
202         filter = {'tag_type_id':tag_type_id}
203         if isinstance (id_or_name,int):
204             filter[primary_key]=id_or_name
205         else:
206             filter[secondary_key]=id_or_name
207         joins = joins_class (self.api,filter)
208         # setting to something non void
209         if value is not None:
210             if not joins:
211                 join = join_class (self.api)
212                 join['tag_type_id']=tag_type_id
213                 join[primary_key]=primary_id
214                 join['value']=value
215                 join.sync()
216             else:
217                 joins[0]['value']=value
218                 joins[0].sync()
219         # providing an empty value means clean up
220         else:
221             if joins:
222                 join=joins[0]
223                 join.delete()
224         # log it
225         self.event_objects= { objclass.__name__ : [primary_id] }
226         self.message=objclass.__name__
227         if secondary_key in objs[0]:
228             self.message += " %s "%objs[0][secondary_key]
229         else:
230             self.message += " %d "%objs[0][primary_key]
231         self.message += "updated"
232         return value
233
234     # attach it
235     setattr (set_class,"call",set_call)
236
237     # define in module
238     setattr(module,get_name,get_class)
239     setattr(module,set_name,set_class)
240     # add in <module>.methods
241     try:
242         methods=getattr(module,'methods')
243     except:
244         methods=[]
245     methods += [get_name,set_name]
246     setattr(module,'methods',methods)