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