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