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