87789ea93f158fa85d5b4f2511b5c877d4271fce
[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
8 from types import StringTypes
9 import re
10
11 from PLC.Faults import *
12 from PLC.Parameter import Parameter, Mixed
13 from PLC.Filter import Filter
14 from PLC.Debug import profile
15 from PLC.Table import Row, Table
16 from PLC.NodeTypes import NodeTypes
17 from PLC.BootStates import BootStates
18 from PLC.Interfaces import Interface, Interfaces
19
20 def valid_hostname(hostname):
21     # 1. Each part begins and ends with a letter or number.
22     # 2. Each part except the last can contain letters, numbers, or hyphens.
23     # 3. Each part is between 1 and 64 characters, including the trailing dot.
24     # 4. At least two parts.
25     # 5. Last part can only contain between 2 and 6 letters.
26     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
27                     r'[a-z]{2,6}$'
28     return hostname and \
29            re.match(good_hostname, hostname, re.IGNORECASE)
30
31 class Node(Row):
32     """
33     Representation of a row in the nodes table. To use, optionally
34     instantiate with a dict of values. Update as you would a
35     dict. Commit to the database with sync().
36     """
37
38     table_name = 'nodes'
39     primary_key = 'node_id'
40     join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
41                     'node_session', 'node_slice_whitelist',
42                     'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
43     fields = {
44         'node_id': Parameter(int, "Node identifier"),
45         'node_type': Parameter(str,"Node type",max=20),
46         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
47         'site_id': Parameter(int, "Site at which this node is located"),
48         'boot_state': Parameter(str, "Boot state", max = 20),
49         'run_level': Parameter(str, "Run level", 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         'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
58         'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
59         'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
60         'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
61         'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True),
62         'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True),
63         'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
64         'key': Parameter(str, "(Admin only) Node key", max = 256),
65         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
66         'interface_ids': Parameter([int], "List of network interfaces that this node has"),
67         'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
68         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
69         'slice_ids': Parameter([int], "List of slices on this node"),
70         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
71         'pcu_ids': Parameter([int], "List of PCUs that control this node"),
72         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
73         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
74         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
75         'node_tag_ids' : Parameter ([int], "List of tags attached to this node"),
76         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
77         }
78     related_fields = {
79         'interfaces': [Mixed(Parameter(int, "Interface identifier"),
80                              Filter(Interface.fields))],
81         'conf_files': [Parameter(int, "ConfFile identifier")],
82         'slices': [Mixed(Parameter(int, "Slice identifier"),
83                          Parameter(str, "Slice name"))],
84         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
85                                    Parameter(str, "Slice name"))]
86         }
87
88     view_tags_name = "view_node_tags"
89     # tags are used by the Add/Get/Update methods to expose tags
90     # this is initialized here and updated by the accessors factory
91     tags = { }
92
93     def validate_hostname(self, hostname):
94         hostname = hostname.lower()
95         if not valid_hostname(hostname):
96             raise PLCInvalidArgument, "Invalid hostname"
97
98         conflicts = Nodes(self.api, [hostname])
99         for node in conflicts:
100             if 'node_id' not in self or self['node_id'] != node['node_id']:
101                 raise PLCInvalidArgument, "Hostname already in use"
102
103         return hostname
104
105     def validate_node_type(self, node_type):
106         node_types = [row['node_type'] for row in NodeTypes(self.api)]
107         if node_type not in node_types:
108             raise PLCInvalidArgument, "Invalid node type %r"%node_type
109         return node_type
110
111     def validate_boot_state(self, boot_state):
112         boot_states = [row['boot_state'] for row in BootStates(self.api)]
113         if boot_state not in boot_states:
114             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
115         return boot_state
116
117     validate_date_created = Row.validate_timestamp
118     validate_last_updated = Row.validate_timestamp
119     validate_last_contact = Row.validate_timestamp
120     validate_last_boot = Row.validate_timestamp
121     validate_last_download = Row.validate_timestamp
122     validate_last_pcu_reboot = Row.validate_timestamp
123     validate_last_pcu_confirmation = Row.validate_timestamp
124
125     def update_readonly_int(self, col_name, commit = True):
126
127         assert 'node_id' in self
128         assert self.table_name
129
130         self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
131                         " where node_id = %d" % (self['node_id']) )
132         self.sync(commit)
133
134     def update_timestamp(self, col_name, commit = True):
135         """
136         Update col_name field with current time
137         """
138
139         assert 'node_id' in self
140         assert self.table_name
141
142         self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
143                        " where node_id = %d" % (self['node_id']) )
144         self.sync(commit)
145
146     def update_last_boot(self, commit = True):
147         self.update_timestamp('last_boot', commit)
148     def update_last_download(self, commit = True):
149         self.update_timestamp('last_download', commit)
150     def update_last_pcu_reboot(self, commit = True):
151         self.update_timestamp('last_pcu_reboot', commit)
152     def update_last_pcu_confirmation(self, commit = True):
153         self.update_timestamp('last_pcu_confirmation', commit)
154
155     def update_last_contact(self, commit = True):
156         self.update_timestamp('last_contact', commit)
157     def update_last_updated(self, commit = True):
158         self.update_timestamp('last_updated', commit)
159
160     def update_tags(self, tags):
161         from PLC.Shell import Shell
162         from PLC.NodeTags import NodeTags
163         from PLC.Methods.AddNodeTag import AddNodeTag
164         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
165         shell = Shell()
166         for (tagname,value) in tags.iteritems():
167             # the tagtype instance is assumed to exist, just check that
168             if not TagTypes(self.api,{'tagname':tagname}):
169                 raise PLCInvalidArgument,"No such TagType %s"%tagname
170             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
171             if not node_tags:
172                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
173             else:
174                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
175
176     def associate_interfaces(self, auth, field, value):
177         """
178         Delete interfaces not found in value list (using DeleteInterface)
179         Add interfaces found in value list (using AddInterface)
180         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
181         """
182
183         assert 'interface_ids' in self
184         assert 'node_id' in self
185         assert isinstance(value, list)
186
187         (interface_ids, blank, interfaces) = self.separate_types(value)
188
189         if self['interface_ids'] != interface_ids:
190             from PLC.Methods.DeleteInterface import DeleteInterface
191
192             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
193
194             for stale_interface in stale_interfaces:
195                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
196
197     def associate_conf_files(self, auth, field, value):
198         """
199         Add conf_files found in value list (AddConfFileToNode)
200         Delets conf_files not found in value list (DeleteConfFileFromNode)
201         """
202
203         assert 'conf_file_ids' in self
204         assert 'node_id' in self
205         assert isinstance(value, list)
206
207         conf_file_ids = self.separate_types(value)[0]
208
209         if self['conf_file_ids'] != conf_file_ids:
210             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
211             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
212             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
213             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
214
215             for new_conf_file in new_conf_files:
216                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
217             for stale_conf_file in stale_conf_files:
218                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
219
220     def associate_slices(self, auth, field, value):
221         """
222         Add slices found in value list to (AddSliceToNode)
223         Delete slices not found in value list (DeleteSliceFromNode)
224         """
225
226         from PLC.Slices import Slices
227
228         assert 'slice_ids' in self
229         assert 'node_id' in self
230         assert isinstance(value, list)
231
232         (slice_ids, slice_names) = self.separate_types(value)[0:2]
233
234         if slice_names:
235             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
236             slice_ids += slices.keys()
237
238         if self['slice_ids'] != slice_ids:
239             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
240             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
241             new_slices = set(slice_ids).difference(self['slice_ids'])
242             stale_slices = set(self['slice_ids']).difference(slice_ids)
243
244         for new_slice in new_slices:
245             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
246         for stale_slice in stale_slices:
247             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
248
249     def associate_slices_whitelist(self, auth, field, value):
250         """
251         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
252         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
253         """
254
255         from PLC.Slices import Slices
256
257         assert 'slice_ids_whitelist' in self
258         assert 'node_id' in self
259         assert isinstance(value, list)
260
261         (slice_ids, slice_names) = self.separate_types(value)[0:2]
262
263         if slice_names:
264             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
265             slice_ids += slices.keys()
266
267         if self['slice_ids_whitelist'] != slice_ids:
268             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
269             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
270             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
271             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
272
273         for new_slice in new_slices:
274             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
275         for stale_slice in stale_slices:
276             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
277
278
279     def delete(self, commit = True):
280         """
281         Delete existing node.
282         """
283
284         assert 'node_id' in self
285
286         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
287         # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
288         if 'peer_id' in self and self['peer_id']:
289             pass
290         else:
291             assert 'interface_ids' in self
292             for interface in Interfaces(self.api,self['interface_ids']):
293                 interface.delete()
294
295         # Clean up miscellaneous join tables
296         for table in self.join_tables:
297             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
298                            (table, self['node_id']))
299
300         # Mark as deleted
301         self['deleted'] = True
302         self.sync(commit)
303
304 class Nodes(Table):
305     """
306     Representation of row(s) from the nodes table in the
307     database.
308     """
309
310     def __init__(self, api, node_filter = None, columns = None):
311         Table.__init__(self, api, Node, columns)
312
313         # the view that we're selecting upon: start with view_nodes
314         view = "view_nodes"
315         # as many left joins as requested tags
316         for tagname in self.tag_columns:
317             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
318                                                 Node.primary_key)
319
320         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
321               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
322
323         if node_filter is not None:
324             if isinstance(node_filter, (list, tuple, set)):
325                 # Separate the list into integers and strings
326                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
327                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
328                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
329                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
330             elif isinstance(node_filter, dict):
331                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
332                 node_filter = Filter(allowed_fields, node_filter)
333                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
334             elif isinstance (node_filter, StringTypes):
335                 node_filter = Filter(Node.fields, {'hostname':node_filter})
336                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
337             elif isinstance (node_filter, (int, long)):
338                 node_filter = Filter(Node.fields, {'node_id':node_filter})
339                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
340             else:
341                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
342
343         self.selectall(sql)