Add timestamps to Nodes, PCUs and Interfaces to make concrete
[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         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_timestamp(self, col_name, commit = True):
126         """
127         Update col_name field with current time
128         """
129
130         assert 'node_id' in self
131         assert self.table_name
132
133         self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
134                        " where node_id = %d" % (self['node_id']) )
135         self.sync(commit)
136
137     def update_last_boot(self, commit = True):
138         self.update_timestamp('last_boot', commit)
139     def update_last_download(self, commit = True):
140         self.update_timestamp('last_download', commit)
141     def update_last_pcu_reboot(self, commit = True):
142         self.update_timestamp('last_pcu_reboot', commit)
143     def update_last_pcu_confirmation(self, commit = True):
144         self.update_timestamp('last_pcu_confirmation', commit)
145
146     def update_last_contact(self, commit = True):
147         self.update_timestamp('last_contact', commit)
148     def update_last_updated(self, commit = True):
149         self.update_timestamp('last_updated', commit)
150
151     def update_tags(self, tags):
152         from PLC.Shell import Shell
153         from PLC.NodeTags import NodeTags
154         from PLC.Methods.AddNodeTag import AddNodeTag
155         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
156         shell = Shell()
157         for (tagname,value) in tags.iteritems():
158             # the tagtype instance is assumed to exist, just check that
159             if not TagTypes(self.api,{'tagname':tagname}):
160                 raise PLCInvalidArgument,"No such TagType %s"%tagname
161             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
162             if not node_tags:
163                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
164             else:
165                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
166
167     def associate_interfaces(self, auth, field, value):
168         """
169         Delete interfaces not found in value list (using DeleteInterface)
170         Add interfaces found in value list (using AddInterface)
171         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
172         """
173
174         assert 'interface_ids' in self
175         assert 'node_id' in self
176         assert isinstance(value, list)
177
178         (interface_ids, blank, interfaces) = self.separate_types(value)
179
180         if self['interface_ids'] != interface_ids:
181             from PLC.Methods.DeleteInterface import DeleteInterface
182
183             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
184
185             for stale_interface in stale_interfaces:
186                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
187
188     def associate_conf_files(self, auth, field, value):
189         """
190         Add conf_files found in value list (AddConfFileToNode)
191         Delets conf_files not found in value list (DeleteConfFileFromNode)
192         """
193
194         assert 'conf_file_ids' in self
195         assert 'node_id' in self
196         assert isinstance(value, list)
197
198         conf_file_ids = self.separate_types(value)[0]
199
200         if self['conf_file_ids'] != conf_file_ids:
201             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
202             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
203             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
204             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
205
206             for new_conf_file in new_conf_files:
207                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
208             for stale_conf_file in stale_conf_files:
209                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
210
211     def associate_slices(self, auth, field, value):
212         """
213         Add slices found in value list to (AddSliceToNode)
214         Delete slices not found in value list (DeleteSliceFromNode)
215         """
216
217         from PLC.Slices import Slices
218
219         assert 'slice_ids' in self
220         assert 'node_id' in self
221         assert isinstance(value, list)
222
223         (slice_ids, slice_names) = self.separate_types(value)[0:2]
224
225         if slice_names:
226             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
227             slice_ids += slices.keys()
228
229         if self['slice_ids'] != slice_ids:
230             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
231             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
232             new_slices = set(slice_ids).difference(self['slice_ids'])
233             stale_slices = set(self['slice_ids']).difference(slice_ids)
234
235         for new_slice in new_slices:
236             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
237         for stale_slice in stale_slices:
238             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
239
240     def associate_slices_whitelist(self, auth, field, value):
241         """
242         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
243         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
244         """
245
246         from PLC.Slices import Slices
247
248         assert 'slice_ids_whitelist' in self
249         assert 'node_id' in self
250         assert isinstance(value, list)
251
252         (slice_ids, slice_names) = self.separate_types(value)[0:2]
253
254         if slice_names:
255             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
256             slice_ids += slices.keys()
257
258         if self['slice_ids_whitelist'] != slice_ids:
259             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
260             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
261             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
262             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
263
264         for new_slice in new_slices:
265             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
266         for stale_slice in stale_slices:
267             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
268
269
270     def delete(self, commit = True):
271         """
272         Delete existing node.
273         """
274
275         assert 'node_id' in self
276
277         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
278         # federated nodes don't have interfaces though so for smooth transition from 4.2 to 4.3
279         if 'peer_id' in self and self['peer_id']:
280             pass
281         else:
282             assert 'interface_ids' in self
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         # the view that we're selecting upon: start with view_nodes
306         view = "view_nodes"
307         # as many left joins as requested tags
308         for tagname in self.tag_columns:
309             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),
310                                                 Node.primary_key)
311
312         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
313               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
314
315         if node_filter is not None:
316             if isinstance(node_filter, (list, tuple, set)):
317                 # Separate the list into integers and strings
318                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
319                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
320                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
321                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
322             elif isinstance(node_filter, dict):
323                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
324                 node_filter = Filter(allowed_fields, node_filter)
325                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
326             elif isinstance (node_filter, StringTypes):
327                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
328                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
329             elif isinstance (node_filter, int):
330                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
331                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
332             else:
333                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
334
335         self.selectall(sql)