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