cleanup - make node/interface/slice similar
[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         'node_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 are used by the Add/Get/Update methods to expose tags
84     # this is initialized here and updated by the accessors factory
85     tags = { }
86
87     def validate_hostname(self, hostname):
88         if not valid_hostname(hostname):
89             raise PLCInvalidArgument, "Invalid hostname"
90
91         conflicts = Nodes(self.api, [hostname])
92         for node in conflicts:
93             if 'node_id' not in self or self['node_id'] != node['node_id']:
94                 raise PLCInvalidArgument, "Hostname already in use"
95
96         return hostname
97
98     def validate_node_type(self, node_type):
99         node_types = [row['node_type'] for row in NodeTypes(self.api)]
100         if node_type not in node_types:
101             raise PLCInvalidArgument, "Invalid node type %r"%node_type
102         return node_type
103
104     def validate_boot_state(self, boot_state):
105         boot_states = [row['boot_state'] for row in BootStates(self.api)]
106         if boot_state not in boot_states:
107             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
108         return boot_state
109
110     validate_date_created = Row.validate_timestamp
111     validate_last_updated = Row.validate_timestamp
112     validate_last_contact = Row.validate_timestamp
113
114     def update_last_contact(self, commit = True):
115         """
116         Update last_contact field with current time
117         """
118         
119         assert 'node_id' in self
120         assert self.table_name
121
122         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
123                        " where node_id = %d" % ( self['node_id']) )
124         self.sync(commit)
125
126
127     def update_last_updated(self, commit = True):
128         """
129         Update last_updated field with current time
130         """
131
132         assert 'node_id' in self
133         assert self.table_name
134
135         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
136                        " where node_id = %d" % (self['node_id']) )
137         self.sync(commit)
138
139     def associate_interfaces(self, auth, field, value):
140         """
141         Delete interfaces not found in value list (using DeleteInterface)       
142         Add interfaces found in value list (using AddInterface)
143         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
144         """
145
146         assert 'interface_ids' in self
147         assert 'node_id' in self
148         assert isinstance(value, list)
149
150         (interface_ids, blank, interfaces) = self.separate_types(value)
151
152         if self['interface_ids'] != interface_ids:
153             from PLC.Methods.DeleteInterface import DeleteInterface
154
155             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
156
157             for stale_interface in stale_interfaces:
158                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
159
160     def associate_conf_files(self, auth, field, value):
161         """
162         Add conf_files found in value list (AddConfFileToNode)
163         Delets conf_files not found in value list (DeleteConfFileFromNode)
164         """
165         
166         assert 'conf_file_ids' in self
167         assert 'node_id' in self
168         assert isinstance(value, list)
169         
170         conf_file_ids = self.separate_types(value)[0]
171         
172         if self['conf_file_ids'] != conf_file_ids:
173             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
174             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
175             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
176             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
177         
178             for new_conf_file in new_conf_files:
179                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
180             for stale_conf_file in stale_conf_files:
181                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
182
183     def associate_slices(self, auth, field, value):
184         """
185         Add slices found in value list to (AddSliceToNode)
186         Delete slices not found in value list (DeleteSliceFromNode)
187         """
188         
189         from PLC.Slices import Slices
190         
191         assert 'slice_ids' in self
192         assert 'node_id' in self
193         assert isinstance(value, list)
194         
195         (slice_ids, slice_names) = self.separate_types(value)[0:2]
196
197         if slice_names:
198             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
199             slice_ids += slices.keys()
200
201         if self['slice_ids'] != slice_ids:
202             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
203             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
204             new_slices = set(slice_ids).difference(self['slice_ids'])
205             stale_slices = set(self['slice_ids']).difference(slice_ids)
206         
207         for new_slice in new_slices:
208             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
209         for stale_slice in stale_slices:
210             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
211
212     def associate_slices_whitelist(self, auth, field, value):
213         """
214         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
215         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
216         """
217
218         from PLC.Slices import Slices
219
220         assert 'slice_ids_whitelist' 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_whitelist'] != slice_ids:
231             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
232             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
233             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
234             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
235
236         for new_slice in new_slices:
237             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
238         for stale_slice in stale_slices:
239             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
240                 
241
242     def delete(self, commit = True):
243         """
244         Delete existing node.
245         """
246
247         assert 'node_id' in self
248         assert 'interface_ids' in self
249
250         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
251         for interface in Interfaces(self.api,self['interface_ids']):
252             interface.delete()
253
254         # Clean up miscellaneous join tables
255         for table in self.join_tables:
256             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
257                            (table, self['node_id']))
258
259         # Mark as deleted
260         self['deleted'] = True
261         self.sync(commit)
262
263
264 class Nodes(Table):
265     """
266     Representation of row(s) from the nodes table in the
267     database.
268     """
269
270     def __init__(self, api, node_filter = None, columns = None):
271         Table.__init__(self, api, Node, columns)
272
273         # the view that we're selecting upon: start with view_nodes
274         view = "view_nodes"
275         # as many left joins as requested tags
276         for tagname in self.tag_columns:
277             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),Node.primary_key)
278             
279         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
280               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
281
282         if node_filter is not None:
283             if isinstance(node_filter, (list, tuple, set)):
284                 # Separate the list into integers and strings
285                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
286                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
287                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
288                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
289             elif isinstance(node_filter, dict):
290                 allowed_fields=dict(Node.fields.items()+Node.tags.items())
291                 node_filter = Filter(allowed_fields, node_filter)
292                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
293             elif isinstance (node_filter, StringTypes):
294                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
295                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
296             elif isinstance (node_filter, int):
297                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
298                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
299             else:
300                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
301
302         self.selectall(sql)