step2 : basic functions for handling nodetags and nodegroups - still highly volatile
[plcapi.git] / PLC / Nodes.py
1 #
2 # Functions for interacting with the nodes table in the database
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id$
8 #
9
10 from types import StringTypes
11 import re
12
13 from PLC.Faults import *
14 from PLC.Parameter import Parameter, Mixed
15 from PLC.Filter import Filter
16 from PLC.Debug import profile
17 from PLC.Table import Row, Table
18 from PLC.Interfaces import Interface, Interfaces
19 from PLC.BootStates import BootStates
20
21 def valid_hostname(hostname):
22     # 1. Each part begins and ends with a letter or number.
23     # 2. Each part except the last can contain letters, numbers, or hyphens.
24     # 3. Each part is between 1 and 64 characters, including the trailing dot.
25     # 4. At least two parts.
26     # 5. Last part can only contain between 2 and 6 letters.
27     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
28                     r'[a-z]{2,6}$'
29     return hostname and \
30            re.match(good_hostname, hostname, re.IGNORECASE)
31
32 class Node(Row):
33     """
34     Representation of a row in the nodes table. To use, optionally
35     instantiate with a dict of values. Update as you would a
36     dict. Commit to the database with sync().
37     """
38
39     table_name = 'nodes'
40     primary_key = 'node_id'
41     # Thierry -- we use delete on interfaces so the related InterfaceSettings get deleted too
42     join_tables = [ 'slice_node', 'peer_node', 'slice_attribute', 
43                     'node_session', 'node_slice_whitelist', 
44                     'node_tag', 'conf_file_node', 'pcu_node', ]
45     fields = {
46         'node_id': Parameter(int, "Node identifier"),
47         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
48         'site_id': Parameter(int, "Site at which this node is located"),
49         'boot_state': Parameter(str, "Boot state", max = 20),
50         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
51         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
52         'version': Parameter(str, "Apparent Boot CD version", max = 64),
53         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
54         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
55         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
56         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), 
57         'key': Parameter(str, "(Admin only) Node key", max = 256),
58         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
59         'interface_ids': Parameter([int], "List of network interfaces that this node has"),
60         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
61         'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
62         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
63         'slice_ids': Parameter([int], "List of slices on this node"),
64         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
65         'pcu_ids': Parameter([int], "List of PCUs that control this node"),
66         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
67         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
68         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
69         'tag_ids' : Parameter ([int], "List of tags attached to this node"),
70         }
71     related_fields = {
72         'interfaces': [Mixed(Parameter(int, "Interface identifier"),
73                                Filter(Interface.fields))],
74         'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
75                              Parameter(str, "NodeGroup name"))],
76         'conf_files': [Parameter(int, "ConfFile identifier")],
77         'slices': [Mixed(Parameter(int, "Slice identifier"),
78                          Parameter(str, "Slice name"))],
79         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
80                                    Parameter(str, "Slice name"))]
81         }
82     # for Cache
83     class_key = 'hostname'
84     foreign_fields = ['boot_state','model','version']
85     # forget about these ones, they are read-only anyway
86     # handling them causes Cache to re-sync all over again 
87     # 'date_created','last_updated'
88     foreign_xrefs = [
89         # in this case, we dont need the 'table' but Cache will look it up, so...
90         {'field' : 'site_id' , 'class' : 'Site' , 'table' : 'unused-on-direct-refs' } ,
91         ]
92
93     def validate_hostname(self, hostname):
94         if not valid_hostname(hostname):
95             raise PLCInvalidArgument, "Invalid hostname"
96
97         conflicts = Nodes(self.api, [hostname])
98         for node in conflicts:
99             if 'node_id' not in self or self['node_id'] != node['node_id']:
100                 raise PLCInvalidArgument, "Hostname already in use"
101
102         return hostname
103
104     def validate_boot_state(self, boot_state):
105         boot_states = [row['boot_state'] for row in BootStates(self.api)]
106         if boot_state not in boot_states:
107             raise PLCInvalidArgument, "Invalid boot state"
108
109         return boot_state
110
111     validate_date_created = Row.validate_timestamp
112     validate_last_updated = Row.validate_timestamp
113     validate_last_contact = Row.validate_timestamp
114
115     def update_last_contact(self, commit = True):
116         """
117         Update last_contact field with current time
118         """
119         
120         assert 'node_id' in self
121         assert self.table_name
122
123         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
124                        " where node_id = %d" % ( self['node_id']) )
125         self.sync(commit)
126
127
128     def update_last_updated(self, commit = True):
129         """
130         Update last_updated field with current time
131         """
132
133         assert 'node_id' in self
134         assert self.table_name
135
136         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
137                        " where node_id = %d" % (self['node_id']) )
138         self.sync(commit)
139
140     def associate_interfaces(self, auth, field, value):
141         """
142         Delete interfaces not found in value list (using DeleteNodeNetwor)k     
143         Add interfaces found in value list (using AddInterface)
144         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
145         """
146
147         assert 'interfacep_ids' in self
148         assert 'node_id' in self
149         assert isinstance(value, list)
150
151         (interface_ids, blank, interfaces) = self.separate_types(value)
152
153         if self['interface_ids'] != interface_ids:
154             from PLC.Methods.DeleteInterface import DeleteInterface
155
156             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
157
158             for stale_interface in stale_interfaces:
159                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
160
161     def associate_nodegroups(self, auth, field, value):
162         """
163         Add node to nodegroups found in value list (AddNodeToNodegroup)
164         Delete node from nodegroup not found in value list (DeleteNodeFromNodegroup)
165         """
166         
167         from PLC.NodeGroups import NodeGroups
168         
169         assert 'nodegroup_ids' in self
170         assert 'node_id' in self
171         assert isinstance(value, list)
172
173         (nodegroup_ids, nodegroup_names) = self.separate_types(value)[0:2]
174         
175         if nodegroup_names:
176             nodegroups = NodeGroups(self.api, nodegroup_names, ['nodegroup_id']).dict('nodegroup_id')
177             nodegroup_ids += nodegroups.keys()
178
179         if self['nodegroup_ids'] != nodegroup_ids:
180             from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
181             from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
182         
183             new_nodegroups = set(nodegroup_ids).difference(self['nodegroup_ids'])
184             stale_nodegroups = set(self['nodegroup_ids']).difference(nodegroup_ids)
185         
186             for new_nodegroup in new_nodegroups:
187                 AddNodeToNodeGroup.__call__(AddNodeToNodeGroup(self.api), auth, self['node_id'], new_nodegroup)
188             for stale_nodegroup in stale_nodegroups:
189                 DeleteNodeFromNodeGroup.__call__(DeleteNodeFromNodeGroup(self.api), auth, self['node_id'], stale_nodegroup)
190           
191
192  
193     def associate_conf_files(self, auth, field, value):
194         """
195         Add conf_files found in value list (AddConfFileToNode)
196         Delets conf_files not found in value list (DeleteConfFileFromNode)
197         """
198         
199         assert 'conf_file_ids' in self
200         assert 'node_id' in self
201         assert isinstance(value, list)
202         
203         conf_file_ids = self.separate_types(value)[0]
204         
205         if self['conf_file_ids'] != conf_file_ids:
206             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
207             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
208             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
209             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
210         
211             for new_conf_file in new_conf_files:
212                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
213             for stale_conf_file in stale_conf_files:
214                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
215
216  
217     def associate_slices(self, auth, field, value):
218         """
219         Add slices found in value list to (AddSliceToNode)
220         Delete slices not found in value list (DeleteSliceFromNode)
221         """
222         
223         from PLC.Slices import Slices
224         
225         assert 'slice_ids' in self
226         assert 'node_id' in self
227         assert isinstance(value, list)
228         
229         (slice_ids, slice_names) = self.separate_types(value)[0:2]
230
231         if slice_names:
232             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
233             slice_ids += slices.keys()
234
235         if self['slice_ids'] != slice_ids:
236             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
237             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
238             new_slices = set(slice_ids).difference(self['slice_ids'])
239             stale_slices = set(self['slice_ids']).difference(slice_ids)
240         
241         for new_slice in new_slices:
242             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
243         for stale_slice in stale_slices:
244             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
245
246     def associate_slices_whitelist(self, auth, field, value):
247         """
248         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
249         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
250         """
251
252         from PLC.Slices import Slices
253
254         assert 'slice_ids_whitelist' in self
255         assert 'node_id' in self
256         assert isinstance(value, list)
257
258         (slice_ids, slice_names) = self.separate_types(value)[0:2]
259
260         if slice_names:
261             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
262             slice_ids += slices.keys()
263
264         if self['slice_ids_whitelist'] != slice_ids:
265             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
266             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
267             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
268             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
269
270         for new_slice in new_slices:
271             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
272         for stale_slice in stale_slices:
273             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
274                 
275
276     def delete(self, commit = True):
277         """
278         Delete existing node.
279         """
280
281         assert 'node_id' in self
282         assert 'interface_ids' in self
283
284         # we need to clean up InterfaceSettings, so handling interfaces as part of join_tables does not work
285         for interface in Interfaces(self.api,self['interface_ids']):
286             interface.delete()
287
288         # Clean up miscellaneous join tables
289         for table in self.join_tables:
290             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
291                            (table, self['node_id']))
292
293         # Mark as deleted
294         self['deleted'] = True
295         self.sync(commit)
296
297
298 class Nodes(Table):
299     """
300     Representation of row(s) from the nodes table in the
301     database.
302     """
303
304     def __init__(self, api, node_filter = None, columns = None):
305         Table.__init__(self, api, Node, columns)
306
307         sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
308               ", ".join(self.columns)
309
310         if node_filter is not None:
311             if isinstance(node_filter, (list, tuple, set)):
312                 # Separate the list into integers and strings
313                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
314                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
315                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
316                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
317             elif isinstance(node_filter, dict):
318                 node_filter = Filter(Node.fields, node_filter)
319                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
320             elif isinstance (node_filter, StringTypes):
321                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
322                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
323             elif isinstance (node_filter, int):
324                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
325                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
326             else:
327                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
328
329         self.selectall(sql)