set svn:keywords property for proper keywords expansion
[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
15 from PLC.Filter import Filter
16 from PLC.Debug import profile
17 from PLC.Table import Row, Table
18 from PLC.NodeNetworks import NodeNetwork, NodeNetworks
19 from PLC.BootStates import BootStates
20
21 def valid_hostname(hostname):
22     # 1. Each part begins and ends with a letter or number.
23     # 2. Each part except the last can contain letters, numbers, or hyphens.
24     # 3. Each part is between 1 and 64 characters, including the trailing dot.
25     # 4. At least two parts.
26     # 5. Last part can only contain between 2 and 6 letters.
27     good_hostname = r'^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+' \
28                     r'[a-z]{2,6}$'
29     return hostname and \
30            re.match(good_hostname, hostname, re.IGNORECASE)
31
32 class Node(Row):
33     """
34     Representation of a row in the nodes table. To use, optionally
35     instantiate with a dict of values. Update as you would a
36     dict. Commit to the database with sync().
37     """
38
39     table_name = 'nodes'
40     primary_key = 'node_id'
41     # Thierry -- we use delete on nodenetworks so the related NodeNetworkSettings get deleted too
42     join_tables = ['nodegroup_node', 'conf_file_node', 'pcu_node', 'slice_node', 'slice_attribute', 'node_session', 'peer_node','node_slice_whitelist']
43     fields = {
44         'node_id': Parameter(int, "Node identifier"),
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         'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True),
49         'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128),
50         'version': Parameter(str, "Apparent Boot CD version", max = 64),
51         'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024),
52         'date_created': Parameter(int, "Date and time when node entry was created", ro = True),
53         'last_updated': Parameter(int, "Date and time when node entry was created", ro = True),
54         'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), 
55         'key': Parameter(str, "(Admin only) Node key", max = 256),
56         'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True),
57         'nodenetwork_ids': Parameter([int], "List of network interfaces that this node has"),
58         'nodegroup_ids': Parameter([int], "List of node groups that this node is in"),
59         'conf_file_ids': Parameter([int], "List of configuration files specific to this node"),
60         # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"),
61         'slice_ids': Parameter([int], "List of slices on this node"),
62         'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"),
63         'pcu_ids': Parameter([int], "List of PCUs that control this node"),
64         'ports': Parameter([int], "List of PCU ports that this node is connected to"),
65         'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True),
66         'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True),
67         }
68
69     # for Cache
70     class_key = 'hostname'
71     foreign_fields = ['boot_state','model','version']
72     # forget about these ones, they are read-only anyway
73     # handling them causes Cache to re-sync all over again 
74     # 'date_created','last_updated'
75     foreign_xrefs = [
76         # in this case, we dont need the 'table' but Cache will look it up, so...
77         {'field' : 'site_id' , 'class' : 'Site' , 'table' : 'unused-on-direct-refs' } ,
78         ]
79
80     def validate_hostname(self, hostname):
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_boot_state(self, boot_state):
92         boot_states = [row['boot_state'] for row in BootStates(self.api)]
93         if boot_state not in boot_states:
94             raise PLCInvalidArgument, "Invalid boot state"
95
96         return boot_state
97
98     validate_date_created = Row.validate_timestamp
99     validate_last_updated = Row.validate_timestamp
100     validate_last_contact = Row.validate_timestamp
101
102     def update_last_contact(self, commit = True):
103         """
104         Update last_contact field with current time
105         """
106         
107         assert 'node_id' in self
108         assert self.table_name
109
110         self.api.db.do("UPDATE %s SET last_contact = CURRENT_TIMESTAMP " % (self.table_name) + \
111                        " where node_id = %d" % ( self['node_id']) )
112         self.sync(commit)
113
114
115     def update_last_updated(self, commit = True):
116         """
117         Update last_updated field with current time
118         """
119
120         assert 'node_id' in self
121         assert self.table_name
122
123         self.api.db.do("UPDATE %s SET last_updated = CURRENT_TIMESTAMP " % (self.table_name) + \
124                        " where node_id = %d" % (self['node_id']) )
125         self.sync(commit)
126
127     def delete(self, commit = True):
128         """
129         Delete existing node.
130         """
131
132         assert 'node_id' in self
133         assert 'nodenetwork_ids' in self
134
135         # we need to clean up NodeNetworkSettings, so handling nodenetworks as part of join_tables does not work
136         for nodenetwork in NodeNetworks(self.api,self['nodenetwork_ids']):
137             nodenetwork.delete()
138
139         # Clean up miscellaneous join tables
140         for table in self.join_tables:
141             self.api.db.do("DELETE FROM %s WHERE node_id = %d" % \
142                            (table, self['node_id']))
143
144         # Mark as deleted
145         self['deleted'] = True
146         self.sync(commit)
147
148
149 class Nodes(Table):
150     """
151     Representation of row(s) from the nodes table in the
152     database.
153     """
154
155     def __init__(self, api, node_filter = None, columns = None):
156         Table.__init__(self, api, Node, columns)
157
158         sql = "SELECT %s FROM view_nodes WHERE deleted IS False" % \
159               ", ".join(self.columns)
160
161         if node_filter is not None:
162             if isinstance(node_filter, (list, tuple, set)):
163                 # Separate the list into integers and strings
164                 ints = filter(lambda x: isinstance(x, (int, long)), node_filter)
165                 strs = filter(lambda x: isinstance(x, StringTypes), node_filter)
166                 node_filter = Filter(Node.fields, {'node_id': ints, 'hostname': strs})
167                 sql += " AND (%s) %s" % node_filter.sql(api, "OR")
168             elif isinstance(node_filter, dict):
169                 node_filter = Filter(Node.fields, node_filter)
170                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
171             elif isinstance (node_filter, StringTypes):
172                 node_filter = Filter(Node.fields, {'hostname':[node_filter]})
173                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
174             elif isinstance (node_filter, int):
175                 node_filter = Filter(Node.fields, {'node_id':[node_filter]})
176                 sql += " AND (%s) %s" % node_filter.sql(api, "AND")
177             else:
178                 raise PLCInvalidArgument, "Wrong node filter %r"%node_filter
179
180         self.selectall(sql)