defining view_name in the Row class does not work, as Table does not know about its...
[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         'nodegroups': [Mixed(Parameter(int, "NodeGroup identifier"),
76                              Parameter(str, "NodeGroup name"))],
77         'conf_files': [Parameter(int, "ConfFile identifier")],
78         'slices': [Mixed(Parameter(int, "Slice identifier"),
79                          Parameter(str, "Slice name"))],
80         'slices_whitelist': [Mixed(Parameter(int, "Slice identifier"),
81                                    Parameter(str, "Slice name"))]
82         }
83
84     view_tags_name = "view_node_tags"
85     tags = {
86         # regular
87         'arch': Parameter(str, "node/config", ro=True),
88         'deployment': Parameter(str, "node/operation"),
89         # dummynet
90         }
91
92     def validate_hostname(self, hostname):
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
119     def update_last_contact(self, commit = True):
120         """
121         Update last_contact field with current time
122         """
123         
124         assert 'node_id' in self
125         assert self.table_name
126
127         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
128                        " where node_id = %d" % ( self['node_id']) )
129         self.sync(commit)
130
131
132     def update_last_updated(self, commit = True):
133         """
134         Update last_updated field with current time
135         """
136
137         assert 'node_id' in self
138         assert self.table_name
139
140         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
141                        " where node_id = %d" % (self['node_id']) )
142         self.sync(commit)
143
144     def associate_interfaces(self, auth, field, value):
145         """
146         Delete interfaces not found in value list (using DeleteInterface)       
147         Add interfaces found in value list (using AddInterface)
148         Updates interfaces found w/ interface_id in value list (using UpdateInterface) 
149         """
150
151         assert 'interface_ids' in self
152         assert 'node_id' in self
153         assert isinstance(value, list)
154
155         (interface_ids, blank, interfaces) = self.separate_types(value)
156
157         if self['interface_ids'] != interface_ids:
158             from PLC.Methods.DeleteInterface import DeleteInterface
159
160             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
161
162             for stale_interface in stale_interfaces:
163                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
164
165     def associate_conf_files(self, auth, field, value):
166         """
167         Add conf_files found in value list (AddConfFileToNode)
168         Delets conf_files not found in value list (DeleteConfFileFromNode)
169         """
170         
171         assert 'conf_file_ids' in self
172         assert 'node_id' in self
173         assert isinstance(value, list)
174         
175         conf_file_ids = self.separate_types(value)[0]
176         
177         if self['conf_file_ids'] != conf_file_ids:
178             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
179             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
180             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
181             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
182         
183             for new_conf_file in new_conf_files:
184                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
185             for stale_conf_file in stale_conf_files:
186                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
187
188     def associate_slices(self, auth, field, value):
189         """
190         Add slices found in value list to (AddSliceToNode)
191         Delete slices not found in value list (DeleteSliceFromNode)
192         """
193         
194         from PLC.Slices import Slices
195         
196         assert 'slice_ids' in self
197         assert 'node_id' in self
198         assert isinstance(value, list)
199         
200         (slice_ids, slice_names) = self.separate_types(value)[0:2]
201
202         if slice_names:
203             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
204             slice_ids += slices.keys()
205
206         if self['slice_ids'] != slice_ids:
207             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
208             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
209             new_slices = set(slice_ids).difference(self['slice_ids'])
210             stale_slices = set(self['slice_ids']).difference(slice_ids)
211         
212         for new_slice in new_slices:
213             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
214         for stale_slice in stale_slices:
215             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])                         
216
217     def associate_slices_whitelist(self, auth, field, value):
218         """
219         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
220         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
221         """
222
223         from PLC.Slices import Slices
224
225         assert 'slice_ids_whitelist' in self
226         assert 'node_id' in self
227         assert isinstance(value, list)
228
229         (slice_ids, slice_names) = self.separate_types(value)[0:2]
230
231         if slice_names:
232             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
233             slice_ids += slices.keys()
234
235         if self['slice_ids_whitelist'] != slice_ids:
236             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
237             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
238             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
239             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
240
241         for new_slice in new_slices:
242             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
243         for stale_slice in stale_slices:
244             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']]) 
245                 
246
247     def delete(self, commit = True):
248         """
249         Delete existing node.
250         """
251
252         assert 'node_id' in self
253         assert 'interface_ids' in self
254
255         # we need to clean up InterfaceTags, so handling interfaces as part of join_tables does not work
256         for interface in Interfaces(self.api,self['interface_ids']):
257             interface.delete()
258
259         # Clean up miscellaneous join tables
260         for table in self.join_tables:
261             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
262                            (table, self['node_id']))
263
264         # Mark as deleted
265         self['deleted'] = True
266         self.sync(commit)
267
268
269 class Nodes(Table):
270     """
271     Representation of row(s) from the nodes table in the
272     database.
273     """
274
275     def __init__(self, api, node_filter = None, columns = None):
276         Table.__init__(self, api, Node, columns)
277
278         # the view that we're selecting upon: start with view_nodes
279         view = "view_nodes"
280         # as many left joins as requested tags
281         for tagname in self.tag_columns:
282             view= "%s left join %s using (%s)"%(view,Node.tagvalue_view_name(tagname),Node.primary_key)
283             
284         sql = "SELECT %s FROM %s WHERE deleted IS False" % \
285               (", ".join(self.columns.keys()+self.tag_columns.keys()),view)
286
287         if node_filter is not None:
288             if isinstance(node_filter, (list, tuple, set)):
289                 # Separate the list into integers and strings
290                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
291                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
292                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
293                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
294             elif isinstance(node_filter, dict):
295                 node_filter = Filter(Node.fields, node_filter)
296                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
297             elif isinstance (node_filter, StringTypes):
298                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
299                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
300             elif isinstance (node_filter, int):
301                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
302                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
303             else:
304                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
305
306         self.selectall(sql)