work in progress - passes simple tests
[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
83     def validate_hostname(self, hostname):
84         if not valid_hostname(hostname):
85             raise PLCInvalidArgument, "Invalid hostname"
86
87         conflicts = Nodes(self.api, [hostname])
88         for node in conflicts:
89             if 'node_id' not in self or self['node_id'] != node['node_id']:
90                 raise PLCInvalidArgument, "Hostname already in use"
91
92         return hostname
93
94     def validate_boot_state(self, boot_state):
95         boot_states = [row['boot_state'] for row in BootStates(self.api)]
96         if boot_state not in boot_states:
97             raise PLCInvalidArgument, "Invalid boot state"
98
99         return boot_state
100
101     validate_date_created = Row.validate_timestamp
102     validate_last_updated = Row.validate_timestamp
103     validate_last_contact = Row.validate_timestamp
104
105     def update_last_contact(self, commit = True):
106         """
107         Update last_contact field with current time
108         """
109         
110         assert 'node_id' in self
111         assert self.table_name
112
113         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
114                        " where node_id = %d" % ( self['node_id']) )
115         self.sync(commit)
116
117
118     def update_last_updated(self, commit = True):
119         """
120         Update last_updated field with current time
121         """
122
123         assert 'node_id' in self
124         assert self.table_name
125
126         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
127                        " where node_id = %d" % (self['node_id']) )
128         self.sync(commit)
129
130     def associate_interfaces(self, auth, field, value):
131         """
132         Delete interfaces not found in value list (using DeleteNodeNetwor)k     
133         Add interfaces found in value list (using AddInterface)
134         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
135         """
136
137         assert 'interfacep_ids' in self
138         assert 'node_id' in self
139         assert isinstance(value, list)
140
141         (interface_ids, blank, interfaces) = self.separate_types(value)
142
143         if self['interface_ids'] != interface_ids:
144             from PLC.Methods.DeleteInterface import DeleteInterface
145
146             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
147
148             for stale_interface in stale_interfaces:
149                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
150
151     def associate_nodegroups(self, auth, field, value):
152         """
153         Add node to nodegroups found in value list (AddNodeToNodegroup)
154         Delete node from nodegroup not found in value list (DeleteNodeFromNodegroup)
155         """
156         
157         from PLC.NodeGroups import NodeGroups
158         
159         assert 'nodegroup_ids' in self
160         assert 'node_id' in self
161         assert isinstance(value, list)
162
163         (nodegroup_ids, nodegroup_names) = self.separate_types(value)[0:2]
164         
165         if nodegroup_names:
166             nodegroups = NodeGroups(self.api, nodegroup_names, ['nodegroup_id']).dict('nodegroup_id')
167             nodegroup_ids += nodegroups.keys()
168
169         if self['nodegroup_ids'] != nodegroup_ids:
170             from PLC.Methods.AddNodeToNodeGroup import AddNodeToNodeGroup
171             from PLC.Methods.DeleteNodeFromNodeGroup import DeleteNodeFromNodeGroup
172         
173             new_nodegroups = set(nodegroup_ids).difference(self['nodegroup_ids'])
174             stale_nodegroups = set(self['nodegroup_ids']).difference(nodegroup_ids)
175         
176             for new_nodegroup in new_nodegroups:
177                 AddNodeToNodeGroup.__call__(AddNodeToNodeGroup(self.api), auth, self['node_id'], new_nodegroup)
178             for stale_nodegroup in stale_nodegroups:
179                 DeleteNodeFromNodeGroup.__call__(DeleteNodeFromNodeGroup(self.api), auth, self['node_id'], stale_nodegroup)
180           
181
182  
183     def associate_conf_files(self, auth, field, value):
184         """
185         Add conf_files found in value list (AddConfFileToNode)
186         Delets conf_files not found in value list (DeleteConfFileFromNode)
187         """
188         
189         assert 'conf_file_ids' in self
190         assert 'node_id' in self
191         assert isinstance(value, list)
192         
193         conf_file_ids = self.separate_types(value)[0]
194         
195         if self['conf_file_ids'] != conf_file_ids:
196             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
197             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
198             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
199             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
200         
201             for new_conf_file in new_conf_files:
202                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
203             for stale_conf_file in stale_conf_files:
204                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
205
206  
207     def associate_slices(self, auth, field, value):
208         """
209         Add slices found in value list to (AddSliceToNode)
210         Delete slices not found in value list (DeleteSliceFromNode)
211         """
212         
213         from PLC.Slices import Slices
214         
215         assert 'slice_ids' in self
216         assert 'node_id' in self
217         assert isinstance(value, list)
218         
219         (slice_ids, slice_names) = self.separate_types(value)[0:2]
220
221         if slice_names:
222             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
223             slice_ids += slices.keys()
224
225         if self['slice_ids'] != slice_ids:
226             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
227             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
228             new_slices = set(slice_ids).difference(self['slice_ids'])
229             stale_slices = set(self['slice_ids']).difference(slice_ids)
230         
231         for new_slice in new_slices:
232             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
233         for stale_slice in stale_slices:
234             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
235
236     def associate_slices_whitelist(self, auth, field, value):
237         """
238         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
239         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
240         """
241
242         from PLC.Slices import Slices
243
244         assert 'slice_ids_whitelist' in self
245         assert 'node_id' in self
246         assert isinstance(value, list)
247
248         (slice_ids, slice_names) = self.separate_types(value)[0:2]
249
250         if slice_names:
251             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
252             slice_ids += slices.keys()
253
254         if self['slice_ids_whitelist'] != slice_ids:
255             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
256             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
257             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
258             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
259
260         for new_slice in new_slices:
261             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
262         for stale_slice in stale_slices:
263             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
264                 
265
266     def delete(self, commit = True):
267         """
268         Delete existing node.
269         """
270
271         assert 'node_id' in self
272         assert 'interface_ids' in self
273
274         # we need to clean up InterfaceSettings, so handling interfaces as part of join_tables does not work
275         for interface in Interfaces(self.api,self['interface_ids']):
276             interface.delete()
277
278         # Clean up miscellaneous join tables
279         for table in self.join_tables:
280             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
281                            (table, self['node_id']))
282
283         # Mark as deleted
284         self['deleted'] = True
285         self.sync(commit)
286
287
288 class Nodes(Table):
289     """
290     Representation of row(s) from the nodes table in the
291     database.
292     """
293
294     def __init__(self, api, node_filter = None, columns = None):
295         Table.__init__(self, api, Node, columns)
296
297         sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
298               ", ".join(self.columns)
299
300         if node_filter is not None:
301             if isinstance(node_filter, (list, tuple, set)):
302                 # Separate the list into integers and strings
303                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
304                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
305                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
306                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
307             elif isinstance(node_filter, dict):
308                 node_filter = Filter(Node.fields, node_filter)
309                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
310             elif isinstance (node_filter, StringTypes):
311                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
312                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
313             elif isinstance (node_filter, int):
314                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
315                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
316             else:
317                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
318
319         self.selectall(sql)