store bwlimit as positive int
[plcapi.git] / PLC / NodeNetworks.py
1 #
2 # Functions for interacting with the nodenetworks table in the database
3 #
4 # Mark Huang <mlhuang@cs.princeton.edu>
5 # Copyright (C) 2006 The Trustees of Princeton University
6 #
7 # $Id: NodeNetworks.py,v 1.7 2006/10/11 19:51:09 mlhuang Exp $
8 #
9
10 from types import StringTypes
11 import socket
12 import struct
13
14 from PLC.Faults import *
15 from PLC.Parameter import Parameter
16 from PLC.Debug import profile
17 from PLC.Table import Row, Table
18 from PLC.NetworkTypes import NetworkType, NetworkTypes
19 from PLC.NetworkMethods import NetworkMethod, NetworkMethods
20 import PLC.Nodes
21
22 def valid_ip(ip):
23     try:
24         ip = socket.inet_ntoa(socket.inet_aton(ip))
25         return True
26     except socket.error:
27         return False
28
29 def in_same_network(address1, address2, netmask):
30     """
31     Returns True if two IPv4 addresses are in the same network. Faults
32     if an address is invalid.
33     """
34
35     address1 = struct.unpack('>L', socket.inet_aton(address1))[0]
36     address2 = struct.unpack('>L', socket.inet_aton(address2))[0]
37     netmask = struct.unpack('>L', socket.inet_aton(netmask))[0]
38
39     return (address1 & netmask) == (address2 & netmask)
40
41 class NodeNetwork(Row):
42     """
43     Representation of a row in the nodenetworks table. To use, optionally
44     instantiate with a dict of values. Update as you would a
45     dict. Commit to the database with sync().
46     """
47
48     table_name = 'nodenetworks'
49     primary_key = 'nodenetwork_id'
50     fields = {
51         'nodenetwork_id': Parameter(int, "Node interface identifier"),
52         'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"),
53         'type': Parameter(str, "Address type (e.g., 'ipv4')"),
54         'ip': Parameter(str, "IP address"),
55         'mac': Parameter(str, "MAC address"),
56         'gateway': Parameter(str, "IP address of primary gateway"),
57         'network': Parameter(str, "Subnet address"),
58         'broadcast': Parameter(str, "Network broadcast address"),
59         'netmask': Parameter(str, "Subnet mask"),
60         'dns1': Parameter(str, "IP address of primary DNS server"),
61         'dns2': Parameter(str, "IP address of secondary DNS server"),
62         'bwlimit': Parameter(int, "Bandwidth limit", min = 0),
63         'hostname': Parameter(str, "(Optional) Hostname"),
64         'node_id': Parameter(int, "Node associated with this interface (if any)"),
65         'is_primary': Parameter(bool, "Is the primary interface for this node"),
66         }
67
68     def __init__(self, api, fields = {}):
69         Row.__init__(self, fields)
70         self.api = api
71
72     def validate_method(self, method):
73         if method not in NetworkMethods(self.api):
74             raise PLCInvalidArgument, "Invalid addressing method"
75         return method
76
77     def validate_type(self, type):
78         if type not in NetworkTypes(self.api):
79             raise PLCInvalidArgument, "Invalid address type"
80         return type
81
82     def validate_ip(self, ip):
83         if ip and not valid_ip(ip):
84             raise PLCInvalidArgument, "Invalid IP address " + ip
85         return ip
86
87     def validate_mac(self, mac):
88         try:
89             bytes = mac.split(":")
90             if len(bytes) < 6:
91                 raise Exception
92             for i, byte in enumerate(bytes):
93                 byte = int(byte, 16)
94                 if byte < 0 or byte > 255:
95                     raise Exception
96                 bytes[i] = "%02x" % byte
97             mac = ":".join(bytes)
98         except:
99             raise PLCInvalidArgument, "Invalid MAC address"
100
101         return mac
102
103     validate_gateway = validate_ip
104     validate_network = validate_ip
105     validate_broadcast = validate_ip
106     validate_netmask = validate_ip
107     validate_dns1 = validate_ip
108     validate_dns2 = validate_ip
109
110     def validate_hostname(self, hostname):
111         # Optional
112         if not hostname:
113             return hostname
114
115         if not PLC.Nodes.valid_hostname(hostname):
116             raise PLCInvalidArgument, "Invalid hostname"
117
118         conflicts = NodeNetworks(self.api, [hostname])
119         for nodenetwork_id, nodenetwork in conflicts.iteritems():
120             if 'nodenetwork_id' not in self or self['nodenetwork_id'] != nodenetwork_id:
121                 raise PLCInvalidArgument, "Hostname already in use"
122
123         # Check for conflicts with a node hostname
124         conflicts = PLC.Nodes.Nodes(self.api, [hostname])
125         for node_id in conflicts.iteritems():
126             if 'node_id' not in self or self['node_id'] != node_id:
127                 raise PLCInvalidArgument, "Hostname already in use"
128
129         return hostname
130
131     def validate_node_id(self, node_id):
132         nodes = PLC.Nodes.Nodes(self.api, [node_id])
133         if not nodes:
134             raise PLCInvalidArgument, "No such node"
135
136         return node_id
137
138     def validate_is_primary(self, is_primary):
139         """
140         Set this interface to be the primary one.
141         """
142
143         if is_primary:
144             nodes = PLC.Nodes.Nodes(self.api, [self['node_id']]).values()
145             if not nodes:
146                 raise PLCInvalidArgument, "No such node"
147             node = nodes[0]
148
149             if node['nodenetwork_ids']:
150                 conflicts = NodeNetworks(self.api, node['nodenetwork_ids'])
151                 for nodenetwork_id, nodenetwork in conflicts.iteritems():
152                     if ('nodenetwork_id' not in self or self['nodenetwork_id'] != nodenetwork_id) and \
153                        nodenetwork['is_primary']:
154                         raise PLCInvalidArgument, "Can only set one primary interface per node"
155
156         return is_primary
157
158     def validate(self):
159         """
160         Flush changes back to the database.
161         """
162
163         # Basic validation
164         Row.validate(self)
165
166         assert 'method' in self
167         method = self['method']
168
169         if method == "proxy" or method == "tap":
170             if 'mac' in self and self['mac']:
171                 raise PLCInvalidArgument, "For %s method, mac should not be specified" % method
172             if 'ip' not in self or not self['ip']:
173                 raise PLCInvalidArgument, "For %s method, ip is required" % method
174             if method == "tap" and ('gateway' not in self or not self['gateway']):
175                 raise PLCInvalidArgument, "For tap method, gateway is required and should be " \
176                       "the IP address of the node that proxies for this address"
177             # Should check that the proxy address is reachable, but
178             # there's no way to tell if the only primary interface is
179             # DHCP!
180
181         elif method == "static":
182             for key in ['ip', 'gateway', 'network', 'broadcast', 'netmask', 'dns1']:
183                 if key not in self or not self[key]:
184                     raise PLCInvalidArgument, "For static method, %s is required" % key
185                 globals()[key] = self[key]
186             if not in_same_network(ip, network, netmask):
187                 raise PLCInvalidArgument, "IP address %s is inconsistent with network %s/%s" % \
188                       (ip, network, netmask)
189             if not in_same_network(broadcast, network, netmask):
190                 raise PLCInvalidArgument, "Broadcast address %s is inconsistent with network %s/%s" % \
191                       (broadcast, network, netmask)
192             if not in_same_network(ip, gateway, netmask):
193                 raise PLCInvalidArgument, "Gateway %s is not reachable from %s/%s" % \
194                       (gateway, ip, netmask)
195
196         elif method == "ipmi":
197             if 'ip' not in self or not self['ip']:
198                 raise PLCInvalidArgument, "For ipmi method, ip is required"
199
200     def delete(self, commit = True):
201         """
202         Delete existing nodenetwork.
203         """
204
205         assert 'nodenetwork_id' in self
206
207         # Delete ourself
208         self.api.db.do("DELETE FROM nodenetworks" \
209                        " WHERE nodenetwork_id = %d" % \
210                        self['nodenetwork_id'])
211         
212         if commit:
213             self.api.db.commit()
214
215 class NodeNetworks(Table):
216     """
217     Representation of row(s) from the nodenetworks table in the
218     database.
219     """
220
221     def __init__(self, api, nodenetwork_id_or_hostname_list = None):
222         self.api = api
223
224         sql = "SELECT %s FROM nodenetworks" % \
225               ", ".join(NodeNetwork.fields)
226
227         if nodenetwork_id_or_hostname_list:
228             # Separate the list into integers and strings
229             nodenetwork_ids = filter(lambda nodenetwork_id: isinstance(nodenetwork_id, (int, long)),
230                                      nodenetwork_id_or_hostname_list)
231             hostnames = filter(lambda hostname: isinstance(hostname, StringTypes),
232                                nodenetwork_id_or_hostname_list)
233             sql += " WHERE (False"
234             if nodenetwork_ids:
235                 sql += " OR nodenetwork_id IN (%s)" % ", ".join(map(str, nodenetwork_ids))
236             if hostnames:
237                 sql += " OR hostname IN (%s)" % ", ".join(api.db.quote(hostnames)).lower()
238             sql += ")"
239
240         rows = self.api.db.selectall(sql)
241
242         for row in rows:
243             self[row['nodenetwork_id']] = NodeNetwork(api, row)