integrating new tables
[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 from datetime import datetime
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.AlchemyObject import AlchemyObj
16 from PLC.NodeTypes import NodeTypes
17 from PLC.BootStates import BootStates
18 from PLC.Interfaces import Interface, Interfaces
19 from PLC.ConfFileNodes import ConfFileNode
20 from PLC.SliceNodes import SliceNode
21 from PLC.SliceNodeWhitelists import SliceNodeWhitelist
22 from PLC.PCUNodes import PCUNode
23 from PLC.PCUNodePorts import PCUNodePort
24 from PLC.NodeTags import NodeTag
25 from PLC.NodeGroups import NodeGroup 
26
27 def valid_hostname(hostname):
28     # 1. Each part begins and ends with a letter or number.
29     # 2. Each part except the last can contain letters, numbers, or hyphens.
30     # 3. Each part is between 1 and 64 characters, including the trailing dot.
31     # 4. At least two parts.
32     # 5. Last part can only contain between 2 and 6 letters.
33     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
34                     r'[a-z]{2,6}$'
35     return hostname and \
36            re.match(good_hostname, hostname, re.IGNORECASE)
37
38 class Node(AlchemyObj):
39     """
40     Representation of a row in the nodes table. To use, optionally
41     instantiate with a dict of values. Update as you would a
42     dict. Commit to the database with sync().
43     """
44
45     tablename = 'nodes'
46     join_tables = [ 'slice_node', 'peer_node', 'slice_tag',
47                     'node_session', 'node_slice_whitelist',
48                     'node_tag', 'conf_file_node', 'pcu_node', 'leases', ]
49     fields = {
50         'node_id': Parameter(int, "Node identifier", primary_key=True),
51         'node_type': Parameter(str,"Node type",max=20),
52         'hostname': Parameter(str, "Fully qualified hostname", max = 255),
53         'site_id': Parameter(int, "Site at which this node is located"),
54         'boot_state': Parameter(str, "Boot state", max = 20, nullok=True),
55         'run_level': Parameter(str, "Run level", max = 20, nullok=True),
56         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
57         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128, nullok=True),
58         'version': Parameter(str, "Apparent Boot CD version", max = 64, nullok=True),
59         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024, nullok=True),
60         'date_created': Parameter(datetime, "Date and time when node entry was created", ro = True),
61         'last_updated': Parameter(datetime, "Date and time when node entry was created", ro = True),
62         'last_contact': Parameter(datetime, "Date and time when node last contacted plc", ro = True, nullok=True),
63         'last_boot': Parameter(datetime, "Date and time when node last booted", ro = True, nullok=True),
64         'last_download': Parameter(datetime, "Date and time when node boot image was created", ro = True, nullok=True),
65         'last_pcu_reboot': Parameter(datetime, "Date and time when PCU reboot was attempted", ro = True, nullok=True),
66         'last_pcu_confirmation': Parameter(datetime, "Date and time when PCU reboot was confirmed", ro = True, nullok=True),
67         'last_time_spent_online': Parameter(datetime, "Length of time the node was last online before shutdown/failure", ro = True, nullok=True),
68         'last_time_spent_offline': Parameter(datetime, "Length of time the node was last offline after failure and before reboot", ro = True, nullok=True),
69         'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False, nullok=True),
70         'key': Parameter(str, "(Admin only) Node key", max = 256, nullok=True),
71         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True, nullok=True),
72         'interface_ids': Parameter([int], "List of network interfaces that this node has", joined=True),
73         'conf_file_ids': Parameter([int], "List of configuration files specific to this node", joined=True),
74         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
75         'slice_ids': Parameter([int], "List of slices on this node", joined=True),
76         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node", joined=True),
77         'pcu_ids': Parameter([int], "List of PCUs that control this node", joined=True),
78         'ports': Parameter([int], "List of PCU ports that this node is connected to", joined=True),
79         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
80         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
81         'node_tag_ids' : Parameter ([int], "List of tags attached to this node", joined=True),
82         'nodegroup_ids': Parameter([int], "List of node groups that this node is in", joined=True),
83         }
84     tags = { }
85
86     def validate_hostname(self, hostname):
87         hostname = hostname.lower()
88         if not valid_hostname(hostname):
89             raise PLCInvalidArgument, "Invalid hostname"
90
91         conflicts = Node().select(filter={'hostname': 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         # Make sure node type does not alredy exist
100         conflicts = NodeTypes(self.api, [name])
101         if not conflicts:
102             raise PLCInvalidArgument, "Invalid node_type"
103         return node_type
104
105     def validate_boot_state(self, boot_state):
106         boot_states = [row['boot_state'] for row in BootStates(self.api)]
107         if boot_state not in boot_states:
108             raise PLCInvalidArgument, "Invalid boot state %r"%boot_state
109         return boot_state
110
111     validate_date_created = AlchemyObj.validate_timestamp
112     validate_last_updated = AlchemyObj.validate_timestamp
113     validate_last_contact = AlchemyObj.validate_timestamp
114     validate_last_boot = AlchemyObj.validate_timestamp
115     validate_last_download = AlchemyObj.validate_timestamp
116     validate_last_pcu_reboot = AlchemyObj.validate_timestamp
117     validate_last_pcu_confirmation = AlchemyObj.validate_timestamp
118
119     def update_readonly_int(self, col_name, commit = True):
120
121         assert 'node_id' in self
122         assert self.table_name
123
124         self.api.db.do("UPDATE %s SET %s = %s" % (self.table_name, col_name, self[col_name]) + \
125                         " where node_id = %d" % (self['node_id']) )
126         self.sync(commit)
127
128     def update_timestamp(self, col_name, commit = True):
129         """
130         Update col_name field with current time
131         """
132         assert 'node_id' in self
133         self[col_name] = datetime.now()
134         fields = {
135             'node_id': self['node_id'],         
136             col_name: datetime.now()
137         } 
138         Node(self.api, fields).sync()
139         
140     def update_last_boot(self, commit = True):
141         self.update_timestamp('last_boot', commit)
142     def update_last_download(self, commit = True):
143         self.update_timestamp('last_download', commit)
144     def update_last_pcu_reboot(self, commit = True):
145         self.update_timestamp('last_pcu_reboot', commit)
146     def update_last_pcu_confirmation(self, commit = True):
147         self.update_timestamp('last_pcu_confirmation', commit)
148
149     def update_last_contact(self, commit = True):
150         self.update_timestamp('last_contact', commit)
151     def update_last_updated(self, commit = True):
152         self.update_timestamp('last_updated', commit)
153
154     def update_tags(self, tags):
155         from PLC.Shell import Shell
156         from PLC.NodeTags import NodeTags
157         from PLC.Methods.AddNodeTag import AddNodeTag
158         from PLC.Methods.UpdateNodeTag import UpdateNodeTag
159         shell = Shell()
160         for (tagname,value) in tags.iteritems():
161             # the tagtype instance is assumed to exist, just check that
162             if not TagTypes(self.api,{'tagname':tagname}):
163                 raise PLCInvalidArgument,"No such TagType %s"%tagname
164             node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']})
165             if not node_tags:
166                 AddNodeTag(self.api).__call__(shell.auth,node['node_id'],tagname,value)
167             else:
168                 UpdateNodeTag(self.api).__call__(shell.auth,node_tags[0]['node_tag_id'],value)
169
170     def associate_interfaces(self, auth, field, value):
171         """
172         Delete interfaces not found in value list (using DeleteInterface)
173         Add interfaces found in value list (using AddInterface)
174         Updates interfaces found w/ interface_id in value list (using UpdateInterface)
175         """
176
177         assert 'interface_ids' in self
178         assert 'node_id' in self
179         assert isinstance(value, list)
180
181         (interface_ids, blank, interfaces) = self.separate_types(value)
182
183         if self['interface_ids'] != interface_ids:
184             from PLC.Methods.DeleteInterface import DeleteInterface
185
186             stale_interfaces = set(self['interface_ids']).difference(interface_ids)
187
188             for stale_interface in stale_interfaces:
189                 DeleteInterface.__call__(DeleteInterface(self.api), auth, stale_interface['interface_id'])
190
191     def associate_conf_files(self, auth, field, value):
192         """
193         Add conf_files found in value list (AddConfFileToNode)
194         Delets conf_files not found in value list (DeleteConfFileFromNode)
195         """
196
197         assert 'conf_file_ids' in self
198         assert 'node_id' in self
199         assert isinstance(value, list)
200
201         conf_file_ids = self.separate_types(value)[0]
202
203         if self['conf_file_ids'] != conf_file_ids:
204             from PLC.Methods.AddConfFileToNode import AddConfFileToNode
205             from PLC.Methods.DeleteConfFileFromNode import DeleteConfFileFromNode
206             new_conf_files = set(conf_file_ids).difference(self['conf_file_ids'])
207             stale_conf_files = set(self['conf_file_ids']).difference(conf_file_ids)
208
209             for new_conf_file in new_conf_files:
210                 AddConfFileToNode.__call__(AddConfFileToNode(self.api), auth, new_conf_file, self['node_id'])
211             for stale_conf_file in stale_conf_files:
212                 DeleteConfFileFromNode.__call__(DeleteConfFileFromNode(self.api), auth, stale_conf_file, self['node_id'])
213
214     def associate_slices(self, auth, field, value):
215         """
216         Add slices found in value list to (AddSliceToNode)
217         Delete slices not found in value list (DeleteSliceFromNode)
218         """
219
220         from PLC.Slices import Slices
221
222         assert 'slice_ids' in self
223         assert 'node_id' in self
224         assert isinstance(value, list)
225
226         (slice_ids, slice_names) = self.separate_types(value)[0:2]
227
228         if slice_names:
229             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
230             slice_ids += slices.keys()
231
232         if self['slice_ids'] != slice_ids:
233             from PLC.Methods.AddSliceToNodes import AddSliceToNodes
234             from PLC.Methods.DeleteSliceFromNodes import DeleteSliceFromNodes
235             new_slices = set(slice_ids).difference(self['slice_ids'])
236             stale_slices = set(self['slice_ids']).difference(slice_ids)
237
238         for new_slice in new_slices:
239             AddSliceToNodes.__call__(AddSliceToNodes(self.api), auth, new_slice, [self['node_id']])
240         for stale_slice in stale_slices:
241             DeleteSliceFromNodes.__call__(DeleteSliceFromNodes(self.api), auth, stale_slice, [self['node_id']])
242
243     def associate_slices_whitelist(self, auth, field, value):
244         """
245         Add slices found in value list to whitelist (AddSliceToNodesWhitelist)
246         Delete slices not found in value list from whitelist (DeleteSliceFromNodesWhitelist)
247         """
248
249         from PLC.Slices import Slices
250
251         assert 'slice_ids_whitelist' in self
252         assert 'node_id' in self
253         assert isinstance(value, list)
254
255         (slice_ids, slice_names) = self.separate_types(value)[0:2]
256
257         if slice_names:
258             slices = Slices(self.api, slice_names, ['slice_id']).dict('slice_id')
259             slice_ids += slices.keys()
260
261         if self['slice_ids_whitelist'] != slice_ids:
262             from PLC.Methods.AddSliceToNodesWhitelist import AddSliceToNodesWhitelist
263             from PLC.Methods.DeleteSliceFromNodesWhitelist import DeleteSliceFromNodesWhitelist
264             new_slices = set(slice_ids).difference(self['slice_ids_whitelist'])
265             stale_slices = set(self['slice_ids_whitelist']).difference(slice_ids)
266
267         for new_slice in new_slices:
268             AddSliceToNodesWhitelist.__call__(AddSliceToNodesWhitelist(self.api), auth, new_slice, [self['node_id']])
269         for stale_slice in stale_slices:
270             DeleteSliceFromNodesWhitelist.__call__(DeleteSliceFromNodesWhitelist(self.api), auth, stale_slice, [self['node_id']])
271
272
273
274     def sync(self, commit=True, validate=True):
275         AlchemyObj.sync(self, commit=commit, validate=validate)
276         ts = datetime.now() 
277         self['last_updated'] = ts 
278         if 'node_id' not in self:
279             self['date_created'] = ts
280             AlchemyObj.insert(self, dict(self))
281         else:
282             AlchemyObj.update(self, {'node_id': self['node_id']}, dict(self))
283
284     def delete(self, commit = True):
285         """
286         Delete existing node.
287         """
288
289         assert 'node_id' in self
290         assert 'interface_ids' in self
291         Interface().delete(filter={'interface_id': self['interface_ids']})
292         AlchemyObj.delete(self, dict(self))
293
294 class Nodes(list):
295     """
296     Representation of row(s) from the nodes table in the
297     database.
298     """
299
300     def __init__(self, api, node_filter = None, columns = None):
301         self.api = api 
302         self.refresh(api)
303         # as many left joins as requested tags
304         if not node_filter:
305             nodes = Node().select()
306         elif isinstance(node_filter, (list, tuple, set)):
307             # Separate the list into integers and strings
308             ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
309             strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
310             nodes = Node().select(filter={'node_id': ints, 'hostname': strs})
311         elif isinstance(node_filter, dict):
312             nodes = Node().select(filter={'node_id': ints, 'hostname': strs})
313         elif isinstance (node_filter, StringTypes):
314             nodes = Node().select(filter={'hostname': strs})
315         elif isinstance (node_filter, (int, long)):
316             nodes = Node().select(filter={'node_id': ints})
317         else:
318             raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
319
320         for node in nodes:
321             node = Node(api, object=node)
322             if not columns or 'interface_ids' in columns:
323                 interfaces = Interface().select(filter={'node_id': node['node_id']})
324                 node['interface_ids'] = [rec.interface_id for rec in interfaces]
325             if not columns or 'conf_file_ids' in columns:
326                 conf_files = ConfFileNode().select(filter={'node_id': node['node_id']})
327                 node['conf_file_ids'] = [rec.conf_file_id for rec in conf_files]
328             if not columns or 'slice_ids' in columns:
329                 slice_nodes = SliceNode().select(filter={'node_id': node['node_id']})
330                 node['slice_ids'] = [rec.slice_id for rec in slices]            
331             if not columns or 'slice_ids_whitelist' in columns:
332                 slice_whitelist = SliceNodeWhitelist().select(filter={'node_id': node['node_id']})
333                 node['slice_ids_whitelist'] = [rec.slice_id for rec in slice_whitelist]
334             if not columns or 'pcu_ids' in columns:
335                 pcus = PCUNode().select(filter={'node_id': node['node_id']})
336                 node['pcu_ids'] = [rec.pcu_id in rec in pcus]
337             if not columns or 'pcu_ports' in columns:
338                 pcu_ports = PCUNodePort().select(filter={'node_id': node['node_id']})
339                 node['pcu_ports'] = [rec.port for rec in pcu_ports]             
340             if not columns or 'node_tag_ids' in columns:
341                 node_tags = NodeTag().select(filter={'node_id': node['node_id']})
342                 node['node_tag_ids'] = [rec.node_tag_id for rec in node_tags]
343             if not columns or 'nodegroup_ids' in columns:
344                 nodegroups = NodeGroup().select(filter={'node_id': node['node_id']})
345                 node['nodegroup_ids'] = [rec.nodegroup_id for rec in nodegroups] 
346             self.append(node)
347
348     def refresh(self, api):
349         from PLC.Sites import Sites
350         default_site = Sites(api, site_filter={'login_base': 'default'})[0]
351         # get current list of compute nodes
352         hypervisors = api.client_shell.nova.hypervisors.list()
353         compute_hosts = [h.hypervisor_hostname for h in hypervisors]
354  
355         nodes = Node().select()
356         hostnames = [node.hostname for node in nodes]
357         
358         added_nodes = set(compute_hosts).difference(hostnames)
359         for added_node in added_nodes:
360             node = Node(api, {'hostname': added_node,
361                               'node_type': 'regular', 
362                               'site_id': default_site['site_id']})
363             node.sync()
364                    
365         
366