implent AddressTypes, Addresses, Nodes, PUCs and SiteTags
[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.Storage.AlchemyObj import AlchemyObj
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(AlchemyObj):
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     tablename = 'nodes'
39     join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
40                     'node_session', 'node_slice_whitelist',
41                     'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
42     fields = {
43         'node_id': Parameter(int, "Node identifier", primary_key=True),
44         'node_type': Parameter(str,"Node type",max=20),
45         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
46         'site_id': Parameter(int, "Site at which this node is located"),
47         'boot_state': Parameter(str, "Boot state", max = 20),
48         'run_level': Parameter(str, "Run level", max = 20),
49         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
50         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
51         'version': Parameter(str, "Apparent Boot CD version", max = 64),
52         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
53         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
54         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
55         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True),
56         'last_boot': Parameter(int, "Date and time when node last booted", ro = True),
57         'last_download': Parameter(int, "Date and time when node boot image was created", ro = True),
58         'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True),
59         'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True),
60         'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True),
61         'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True),
62         'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False),
63         'key': Parameter(str, "(Admin only) Node key", max = 256),
64         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
65         'interface_ids': Parameter([int], "List of network interfaces that this node has", joined=True),
66         'conf_file_ids': Parameter([int], "List of configuration files specific to this node", joined=True),
67         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
68         'slice_ids': Parameter([int], "List of slices on this node", joined=True),
69         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node", joined=True),
70         'pcu_ids': Parameter([int], "List of PCUs that control this node", joined=True),
71         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
72         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
73         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
74         'node_tag_ids' : Parameter ([int], "List of tags attached to this node", joined=True),
75         'nodegroup_ids': Parameter([int], "List of node groups that this node is in", joined=True),
76         }
77     tags = { }
78
79     def validate_hostname(self, hostname):
80         hostname = hostname.lower()
81         if not valid_hostname(hostname):
82             raise PLCInvalidArgument, "Invalid hostname"
83
84         conflicts = Nodes(self.api, [hostname])
85         for node in conflicts:
86             if 'node_id' not in self or self['node_id'] != node['node_id']:
87                 raise PLCInvalidArgument, "Hostname already in use"
88
89         return hostname
90
91     def validate_node_type(self, node_type):
92         node_types = [row['node_type'] for row in NodeTypes(self.api)]
93         if node_type not in node_types:
94             raise PLCInvalidArgument, "Invalid node type %r"%node_type
95         return node_type
96
97     def validate_boot_state(self, boot_state):
98         boot_states = [row['boot_state'] for row in BootStates(self.api)]
99         if boot_state not in boot_states:
100             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
101         return boot_state
102
103     validate_date_created = AlchemyObj.validate_timestamp
104     validate_last_updated = AlchemyObj.validate_timestamp
105     validate_last_contact = AlchemyObj.validate_timestamp
106     validate_last_boot = AlchemyObj.validate_timestamp
107     validate_last_download = AlchemyObj.validate_timestamp
108     validate_last_pcu_reboot = AlchemyObj.validate_timestamp
109     validate_last_pcu_confirmation = AlchemyObj.validate_timestamp
110
111     def update_readonly_int(self, col_name, commit = True):
112
113         assert 'node_id' in self
114         assert self.table_name
115
116         self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
117                         " where node_id = %d" % (self['node_id']) )
118         self.sync(commit)
119
120     def update_timestamp(self, col_name, commit = True):
121         """
122         Update col_name 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 %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \
129                        " where node_id = %d" % (self['node_id']) )
130         self.sync(commit)
131
132     def update_last_boot(self, commit = True):
133         self.update_timestamp('last_boot', commit)
134     def update_last_download(self, commit = True):
135         self.update_timestamp('last_download', commit)
136     def update_last_pcu_reboot(self, commit = True):
137         self.update_timestamp('last_pcu_reboot', commit)
138     def update_last_pcu_confirmation(self, commit = True):
139         self.update_timestamp('last_pcu_confirmation', commit)
140
141     def update_last_contact(self, commit = True):
142         self.update_timestamp('last_contact', commit)
143     def update_last_updated(self, commit = True):
144         self.update_timestamp('last_updated', commit)
145
146     def update_tags(self, tags):
147         from PLC.Shell import Shell
148         from PLC.NodeTags import NodeTags
149         from PLC.Methods.AddNodeTag import AddNodeTag
150         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
151         shell = Shell()
152         for (tagname,value) in tags.iteritems():
153             # the tagtype instance is assumed to exist, just check that
154             if not TagTypes(self.api,{'tagname':tagname}):
155                 raise PLCInvalidArgument,"No such TagType %s"%tagname
156             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
157             if not node_tags:
158                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
159             else:
160                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
161
162     def associate_interfaces(self, auth, field, value):
163         """
164         Delete interfaces not found in value list (using DeleteInterface)
165         Add interfaces found in value list (using AddInterface)
166         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
167         """
168
169         assert 'interface_ids' in self
170         assert 'node_id' in self
171         assert isinstance(value, list)
172
173         (interface_ids, blank, interfaces) = self.separate_types(value)
174
175         if self['interface_ids'] != interface_ids:
176             from PLC.Methods.DeleteInterface import DeleteInterface
177
178             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
179
180             for stale_interface in stale_interfaces:
181                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
182
183     def associate_conf_files(self, auth, field, value):
184         """
185         Add conf_files found in value list (AddConfFileToNode)
186         Delets conf_files not found in value list (DeleteConfFileFromNode)
187         """
188
189         assert 'conf_file_ids' in self
190         assert 'node_id' in self
191         assert isinstance(value, list)
192
193         conf_file_ids = self.separate_types(value)[0]
194
195         if self['conf_file_ids'] != conf_file_ids:
196             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
197             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
198             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
199             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
200
201             for new_conf_file in new_conf_files:
202                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
203             for stale_conf_file in stale_conf_files:
204                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
205
206     def associate_slices(self, auth, field, value):
207         """
208         Add slices found in value list to (AddSliceToNode)
209         Delete slices not found in value list (DeleteSliceFromNode)
210         """
211
212         from PLC.Slices import Slices
213
214         assert 'slice_ids' in self
215         assert 'node_id' in self
216         assert isinstance(value, list)
217
218         (slice_ids, slice_names) = self.separate_types(value)[0:2]
219
220         if slice_names:
221             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
222             slice_ids += slices.keys()
223
224         if self['slice_ids'] != slice_ids:
225             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
226             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
227             new_slices = set(slice_ids).difference(self['slice_ids'])
228             stale_slices = set(self['slice_ids']).difference(slice_ids)
229
230         for new_slice in new_slices:
231             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
232         for stale_slice in stale_slices:
233             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
234
235     def associate_slices_whitelist(self, auth, field, value):
236         """
237         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
238         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
239         """
240
241         from PLC.Slices import Slices
242
243         assert 'slice_ids_whitelist' in self
244         assert 'node_id' in self
245         assert isinstance(value, list)
246
247         (slice_ids, slice_names) = self.separate_types(value)[0:2]
248
249         if slice_names:
250             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
251             slice_ids += slices.keys()
252
253         if self['slice_ids_whitelist'] != slice_ids:
254             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
255             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
256             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
257             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
258
259         for new_slice in new_slices:
260             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
261         for stale_slice in stale_slices:
262             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
263
264
265
266     def sync(self, insert=False, validate=True):
267         AlchemyObj.sync(self, insert, validate)
268         if insert == True or 'node_id' not in self:
269             AlchemyObj.insert(self, dict(self))
270         else:
271             AlchemyObj.update(self, dict(self))
272
273     def delete(self, commit = True):
274         """
275         Delete existing node.
276         """
277
278         assert 'node_id' in self
279         assert 'interface_ids' in self
280         Interface().delete(filter={'interface_id': self['interface_ids']})
281         AlchemyObj.delete(self, dict(self)
282
283 class Nodes(list):
284     """
285     Representation of row(s) from the nodes table in the
286     database.
287     """
288
289     def __init__(self, api, node_filter = None, columns = None):
290         # as many left joins as requested tags
291         if not node_filter:
292             nodes = Node().select()
293         elif isinstance(node_filter, (list, tuple, set)):
294             # Separate the list into integers and strings
295             ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
296             strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
297             nodes = Node().select(filter={'node_id': ints, 'hostname': strs})
298         elif isinstance(node_filter, dict):
299             nodes = Node().select(filter={'node_id': ints, 'hostname': strs})
300         elif isinstance (node_filter, StringTypes):
301             nodes = Node().select(filter={'hostname': strs})
302         elif isinstance (node_filter, (int, long)):
303             nodes = Node().select(filter={'node_id': ints})
304         else:
305             raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
306
307         for node in nodes:
308             self.append(node)