keep things simple, use 'value' rather than 'tagvalue' everywhere
[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 #
9
10 from types import StringTypes
11 import re
12
13 from PLC.Faults import *
14 from PLC.Parameter import Parameter, Mixed
15 from PLC.Filter import Filter
16 from PLC.Debug import profile
17 from PLC.Table import Row, Table
18 from PLC.NodeTypes import NodeTypes
19 from PLC.BootStates import BootStates
20 from PLC.Interfaces import Interface, Interfaces
21
22 def valid_hostname(hostname):
23     # 1. Each part begins and ends with a letter or number.
24     # 2. Each part except the last can contain letters, numbers, or hyphens.
25     # 3. Each part is between 1 and 64 characters, including the trailing dot.
26     # 4. At least two parts.
27     # 5. Last part can only contain between 2 and 6 letters.
28     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
29                     r'[a-z]{2,6}$'
30     return hostname and \
31            re.match(good_hostname, hostname, re.IGNORECASE)
32
33 class Node(Row):
34     """
35     Representation of a row in the nodes table. To use, optionally
36     instantiate with a dict of values. Update as you would a
37     dict. Commit to the database with sync().
38     """
39
40     table_name = 'nodes'
41     primary_key = 'node_id'
42     join_tables = [ 'slice_node', 'peer_node', 'slice_tag', 
43                     'node_session', 'node_slice_whitelist', 
44                     'node_tag', 'conf_file_node', 'pcu_node', ]
45     fields = {
46         'node_id': Parameter(int, "Node identifier"),
47         'node_type': Parameter(str,"Node type",max=20),
48         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
49         'site_id': Parameter(int, "Site at which this node is located"),
50         'boot_state': Parameter(str, "Boot state", 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         'key': Parameter(str, "(Admin only) Node key", max = 256),
59         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
60         'interface_ids': Parameter([int], "List of network interfaces that this node has"),
61         'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
62         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
63         'slice_ids': Parameter([int], "List of slices on this node"),
64         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
65         'pcu_ids': Parameter([int], "List of PCUs that control this node"),
66         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
67         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
68         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
69         'tag_ids' : Parameter ([int], "List of tags attached to this node"),
70         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
71         }
72     related_fields = {
73         'interfaces': [Mixed(Parameter(int, "Interface identifier"),
74                              Filter(Interface.fields))],
75         'conf_files': [Parameter(int, "ConfFile identifier")],
76         'slices': [Mixed(Parameter(int, "Slice identifier"),
77                          Parameter(str, "Slice name"))],
78         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
79                                    Parameter(str, "Slice name"))]
80         }
81
82     view_tags_name = "view_node_tags"
83     # tags declared here should also be defined as Accessors to ensure that the TagType is created
84     # type info not used yet
85     tags = {
86         # regular nodes
87         'arch': str,
88         'pldistro' : str,
89         'deployment': str,
90         # dummynet nodes
91         }
92
93     def validate_hostname(self, hostname):
94         if not valid_hostname(hostname):
95             raise PLCInvalidArgument, "Invalid hostname"
96
97         conflicts = Nodes(self.api, [hostname])
98         for node in conflicts:
99             if 'node_id' not in self or self['node_id'] != node['node_id']:
100                 raise PLCInvalidArgument, "Hostname already in use"
101
102         return hostname
103
104     def validate_node_type(self, node_type):
105         node_types = [row['node_type'] for row in NodeTypes(self.api)]
106         if node_type not in node_types:
107             raise PLCInvalidArgument, "Invalid node type %r"%node_type
108         return node_type
109
110     def validate_boot_state(self, boot_state):
111         boot_states = [row['boot_state'] for row in BootStates(self.api)]
112         if boot_state not in boot_states:
113             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
114         return boot_state
115
116     validate_date_created = Row.validate_timestamp
117     validate_last_updated = Row.validate_timestamp
118     validate_last_contact = Row.validate_timestamp
119
120     def update_last_contact(self, commit = True):
121         """
122         Update last_contact field with current time
123         """
124         
125         assert 'node_id' in self
126         assert self.table_name
127
128         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
129                        " where node_id = %d" % ( self['node_id']) )
130         self.sync(commit)
131
132
133     def update_last_updated(self, commit = True):
134         """
135         Update last_updated field with current time
136         """
137
138         assert 'node_id' in self
139         assert self.table_name
140
141         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
142                        " where node_id = %d" % (self['node_id']) )
143         self.sync(commit)
144
145     def associate_interfaces(self, auth, field, value):
146         """
147         Delete interfaces not found in value list (using DeleteInterface)       
148         Add interfaces found in value list (using AddInterface)
149         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
150         """
151
152         assert 'interface_ids' in self
153         assert 'node_id' in self
154         assert isinstance(value, list)
155
156         (interface_ids, blank, interfaces) = self.separate_types(value)
157
158         if self['interface_ids'] != interface_ids:
159             from PLC.Methods.DeleteInterface import DeleteInterface
160
161             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
162
163             for stale_interface in stale_interfaces:
164                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
165
166     def associate_conf_files(self, auth, field, value):
167         """
168         Add conf_files found in value list (AddConfFileToNode)
169         Delets conf_files not found in value list (DeleteConfFileFromNode)
170         """
171         
172         assert 'conf_file_ids' in self
173         assert 'node_id' in self
174         assert isinstance(value, list)
175         
176         conf_file_ids = self.separate_types(value)[0]
177         
178         if self['conf_file_ids'] != conf_file_ids:
179             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
180             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
181             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
182             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
183         
184             for new_conf_file in new_conf_files:
185                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
186             for stale_conf_file in stale_conf_files:
187                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
188
189     def associate_slices(self, auth, field, value):
190         """
191         Add slices found in value list to (AddSliceToNode)
192         Delete slices not found in value list (DeleteSliceFromNode)
193         """
194         
195         from PLC.Slices import Slices
196         
197         assert 'slice_ids' in self
198         assert 'node_id' in self
199         assert isinstance(value, list)
200         
201         (slice_ids, slice_names) = self.separate_types(value)[0:2]
202
203         if slice_names:
204             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
205             slice_ids += slices.keys()
206
207         if self['slice_ids'] != slice_ids:
208             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
209             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
210             new_slices = set(slice_ids).difference(self['slice_ids'])
211             stale_slices = set(self['slice_ids']).difference(slice_ids)
212         
213         for new_slice in new_slices:
214             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
215         for stale_slice in stale_slices:
216             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
217
218     def associate_slices_whitelist(self, auth, field, value):
219         """
220         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
221         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
222         """
223
224         from PLC.Slices import Slices
225
226         assert 'slice_ids_whitelist' in self
227         assert 'node_id' in self
228         assert isinstance(value, list)
229
230         (slice_ids, slice_names) = self.separate_types(value)[0:2]
231
232         if slice_names:
233             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
234             slice_ids += slices.keys()
235
236         if self['slice_ids_whitelist'] != slice_ids:
237             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
238             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
239             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
240             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
241
242         for new_slice in new_slices:
243             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
244         for stale_slice in stale_slices:
245             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
246                 
247
248     def delete(self, commit = True):
249         """
250         Delete existing node.
251         """
252
253         assert 'node_id' in self
254         assert 'interface_ids' in self
255
256         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
257         for interface in Interfaces(self.api,self['interface_ids']):
258             interface.delete()
259
260         # Clean up miscellaneous join tables
261         for table in self.join_tables:
262             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
263                            (table, self['node_id']))
264
265         # Mark as deleted
266         self['deleted'] = True
267         self.sync(commit)
268
269
270 class Nodes(Table):
271     """
272     Representation of row(s) from the nodes table in the
273     database.
274     """
275
276     def __init__(self, api, node_filter = None, columns = None):
277         Table.__init__(self, api, Node, columns)
278
279         # the view that we're selecting upon: start with view_nodes
280         view = "view_nodes"
281         # as many left joins as requested tags
282         for tagname in self.tag_columns:
283             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),Node.primary_key)
284             
285         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
286               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
287
288         if node_filter is not None:
289             if isinstance(node_filter, (list, tuple, set)):
290                 # Separate the list into integers and strings
291                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
292                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
293                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
294                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
295             elif isinstance(node_filter, dict):
296                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
297                 node_filter = Filter(allowed_fields, node_filter)
298                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
299             elif isinstance (node_filter, StringTypes):
300                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
301                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
302             elif isinstance (node_filter, int):
303                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
304                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
305             else:
306                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
307
308         self.selectall(sql)