Be consistent in the API.
[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 # $URL$
9 #
10
11 from types import StringTypes
12 import re
13
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter, Mixed
16 from PLC.Filter import Filter
17 from PLC.Debug import profile
18 from PLC.Table import Row, Table
19 from PLC.NodeTypes import NodeTypes
20 from PLC.BootStates import BootStates
21 from PLC.Interfaces import Interface, Interfaces
22
23 def valid_hostname(hostname):
24     # 1. Each part begins and ends with a letter or number.
25     # 2. Each part except the last can contain letters, numbers, or hyphens.
26     # 3. Each part is between 1 and 64 characters, including the trailing dot.
27     # 4. At least two parts.
28     # 5. Last part can only contain between 2 and 6 letters.
29     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
30                     r'[a-z]{2,6}$'
31     return hostname and \
32            re.match(good_hostname, hostname, re.IGNORECASE)
33
34 class Node(Row):
35     """
36     Representation of a row in the nodes table. To use, optionally
37     instantiate with a dict of values. Update as you would a
38     dict. Commit to the database with sync().
39     """
40
41     table_name = 'nodes'
42     primary_key = 'node_id'
43     join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
44                     'node_session', 'node_slice_whitelist',
45                     'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
46     fields = {
47         'node_id': Parameter(int, "Node identifier"),
48         'node_type': Parameter(str,"Node type",max=20),
49         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
50         'site_id': Parameter(int, "Site at which this node is located"),
51         'boot_state': Parameter(str, "Boot state", max = 20),
52         'run_level': Parameter(str, "Run level", max = 20),
53         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
54         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
55         'version': Parameter(str, "Apparent Boot CD version", max = 64),
56         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
57         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
58         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
59         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
60         'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
61         'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
62         'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
63         'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", 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_timestamp(self, col_name, commit = True):
127         """
128         Update col_name 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 %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
135                        " where node_id = %d" % (self['node_id']) )
136         self.sync(commit)
137
138     def update_last_boot(self, commit = True):
139         self.update_timestamp('last_boot', commit)
140     def update_last_download(self, commit = True):
141         self.update_timestamp('last_download', commit)
142     def update_last_pcu_reboot(self, commit = True):
143         self.update_timestamp('last_pcu_reboot', commit)
144     def update_last_pcu_confirmation(self, commit = True):
145         self.update_timestamp('last_pcu_confirmation', commit)
146
147     def update_last_contact(self, commit = True):
148         self.update_timestamp('last_contact', commit)
149     def update_last_updated(self, commit = True):
150         self.update_timestamp('last_updated', commit)
151
152     def update_tags(self, tags):
153         from PLC.Shell import Shell
154         from PLC.NodeTags import NodeTags
155         from PLC.Methods.AddNodeTag import AddNodeTag
156         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
157         shell = Shell()
158         for (tagname,value) in tags.iteritems():
159             # the tagtype instance is assumed to exist, just check that
160             if not TagTypes(self.api,{'tagname':tagname}):
161                 raise PLCInvalidArgument,"No such TagType %s"%tagname
162             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
163             if not node_tags:
164                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
165             else:
166                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
167
168     def associate_interfaces(self, auth, field, value):
169         """
170         Delete interfaces not found in value list (using DeleteInterface)
171         Add interfaces found in value list (using AddInterface)
172         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
173         """
174
175         assert 'interface_ids' in self
176         assert 'node_id' in self
177         assert isinstance(value, list)
178
179         (interface_ids, blank, interfaces) = self.separate_types(value)
180
181         if self['interface_ids'] != interface_ids:
182             from PLC.Methods.DeleteInterface import DeleteInterface
183
184             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
185
186             for stale_interface in stale_interfaces:
187                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
188
189     def associate_conf_files(self, auth, field, value):
190         """
191         Add conf_files found in value list (AddConfFileToNode)
192         Delets conf_files not found in value list (DeleteConfFileFromNode)
193         """
194
195         assert 'conf_file_ids' in self
196         assert 'node_id' in self
197         assert isinstance(value, list)
198
199         conf_file_ids = self.separate_types(value)[0]
200
201         if self['conf_file_ids'] != conf_file_ids:
202             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
203             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
204             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
205             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
206
207             for new_conf_file in new_conf_files:
208                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
209             for stale_conf_file in stale_conf_files:
210                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
211
212     def associate_slices(self, auth, field, value):
213         """
214         Add slices found in value list to (AddSliceToNode)
215         Delete slices not found in value list (DeleteSliceFromNode)
216         """
217
218         from PLC.Slices import Slices
219
220         assert 'slice_ids' in self
221         assert 'node_id' in self
222         assert isinstance(value, list)
223
224         (slice_ids, slice_names) = self.separate_types(value)[0:2]
225
226         if slice_names:
227             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
228             slice_ids += slices.keys()
229
230         if self['slice_ids'] != slice_ids:
231             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
232             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
233             new_slices = set(slice_ids).difference(self['slice_ids'])
234             stale_slices = set(self['slice_ids']).difference(slice_ids)
235
236         for new_slice in new_slices:
237             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
238         for stale_slice in stale_slices:
239             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
240
241     def associate_slices_whitelist(self, auth, field, value):
242         """
243         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
244         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
245         """
246
247         from PLC.Slices import Slices
248
249         assert 'slice_ids_whitelist' in self
250         assert 'node_id' in self
251         assert isinstance(value, list)
252
253         (slice_ids, slice_names) = self.separate_types(value)[0:2]
254
255         if slice_names:
256             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
257             slice_ids += slices.keys()
258
259         if self['slice_ids_whitelist'] != slice_ids:
260             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
261             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
262             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
263             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
264
265         for new_slice in new_slices:
266             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
267         for stale_slice in stale_slices:
268             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
269
270
271     def delete(self, commit = True):
272         """
273         Delete existing node.
274         """
275
276         assert 'node_id' in self
277
278         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
279         # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
280         if 'peer_id' in self and self['peer_id']:
281             pass
282         else:
283             assert 'interface_ids' in self
284             for interface in Interfaces(self.api,self['interface_ids']):
285                 interface.delete()
286
287         # Clean up miscellaneous join tables
288         for table in self.join_tables:
289             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
290                            (table, self['node_id']))
291
292         # Mark as deleted
293         self['deleted'] = True
294         self.sync(commit)
295
296
297 class Nodes(Table):
298     """
299     Representation of row(s) from the nodes table in the
300     database.
301     """
302
303     def __init__(self, api, node_filter = None, columns = None):
304         Table.__init__(self, api, Node, columns)
305
306         # the view that we're selecting upon: start with view_nodes
307         view = "view_nodes"
308         # as many left joins as requested tags
309         for tagname in self.tag_columns:
310             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
311                                                 Node.primary_key)
312
313         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
314               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
315
316         if node_filter is not None:
317             if isinstance(node_filter, (list, tuple, set)):
318                 # Separate the list into integers and strings
319                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
320                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
321                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
322                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
323             elif isinstance(node_filter, dict):
324                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
325                 node_filter = Filter(allowed_fields, node_filter)
326                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
327             elif isinstance (node_filter, StringTypes):
328                 node_filter = Filter(Node.fields, {'hostname':node_filter})
329                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
330             elif isinstance (node_filter, (int, long)):
331                 node_filter = Filter(Node.fields, {'node_id':node_filter})
332                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
333             else:
334                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
335
336         self.selectall(sql)