From: smbaker Date: Tue, 10 Apr 2012 04:43:41 +0000 (-0700) Subject: bring over newinterface branch from Verivue X-Git-Url: http://git.onelab.eu/?p=plcapi.git;a=commitdiff_plain;h=f1b4415be5ba4be9941d25b531c46696b377fa3a bring over newinterface branch from Verivue --- diff --git a/PLC/Accessors/Factory.py b/PLC/Accessors/Factory.py index fec0d63..62a4748 100644 --- a/PLC/Accessors/Factory.py +++ b/PLC/Accessors/Factory.py @@ -30,7 +30,7 @@ taggable_classes = { Node : {'table_class' : Nodes, 'secondary_key': 'hostname'}, Interface : {'table_class' : Interfaces, 'joins_class': InterfaceTags, 'join_class': InterfaceTag, - 'secondary_key' : 'ip'}, + 'secondary_key' : 'mac'}, Slice: {'table_class' : Slices, 'joins_class': SliceTags, 'join_class': SliceTag, 'secondary_key':'name'}, diff --git a/PLC/InterfaceTags.py b/PLC/InterfaceTags.py index af1deb4..76bee90 100644 --- a/PLC/InterfaceTags.py +++ b/PLC/InterfaceTags.py @@ -1,6 +1,10 @@ +# $Id$ +# $URL$ # # Thierry Parmentelat - INRIA # +# $Revision$ +# from PLC.Faults import * from PLC.Parameter import Parameter from PLC.Filter import Filter @@ -19,7 +23,6 @@ class InterfaceTag(Row): fields = { 'interface_tag_id': Parameter(int, "Interface setting identifier"), 'interface_id': Interface.fields['interface_id'], - 'ip': Interface.fields['ip'], 'tag_type_id': TagType.fields['tag_type_id'], 'tagname': TagType.fields['tagname'], 'description': TagType.fields['description'], diff --git a/PLC/Interfaces.py b/PLC/Interfaces.py index f4ddcc8..42e35e5 100644 --- a/PLC/Interfaces.py +++ b/PLC/Interfaces.py @@ -18,49 +18,25 @@ from PLC.NetworkTypes import NetworkType, NetworkTypes from PLC.NetworkMethods import NetworkMethod, NetworkMethods import PLC.Nodes -def valid_ipv4(ip): +def valid_ip(ip): try: ip = socket.inet_ntoa(socket.inet_aton(ip)) return True except socket.error: return False -def valid_ipv6(ip): - try: - ip = socket.inet_ntop(socket.AF_INET6, socket.inet_pton(socket.AF_INET6, ip)) - return True - except socket.error: - return False - -def valid_ip(ip): - return valid_ipv4(ip) or valid_ipv6(ip) - -def in_same_network_ipv4(address1, address2, netmask): +def in_same_network(address1, address2, netmask): """ Returns True if two IPv4 addresses are in the same network. Faults if an address is invalid. """ + address1 = struct.unpack('>L', socket.inet_aton(address1))[0] address2 = struct.unpack('>L', socket.inet_aton(address2))[0] netmask = struct.unpack('>L', socket.inet_aton(netmask))[0] return (address1 & netmask) == (address2 & netmask) -def in_same_network_ipv6(address1, address2, netmask): - """ - Returns True if two IPv6 addresses are in the same network. Faults - if an address is invalid. - """ - address1 = struct.unpack('>2Q', socket.inet_pton(socket.AF_INET6, address1))[0] - address2 = struct.unpack('>2Q', socket.inet_pton(socket.AF_INET6, address2))[0] - netmask = struct.unpack('>2Q', socket.inet_pton(socket.AF_INET6, netmask))[0] - - return (address1 & netmask) == (address2 & netmask) - -def in_same_network(address1, address2, netmask): - return in_same_network_ipv4(address1, address2, netmask) or \ - in_same_network_ipv6(address1, address2, netmask) - class Interface(Row): """ Representation of a row in the interfaces table. To use, optionally @@ -70,24 +46,18 @@ class Interface(Row): table_name = 'interfaces' primary_key = 'interface_id' - join_tables = ['interface_tag'] + join_tables = ['interface_tag', 'ip_addresses', 'routes'] fields = { 'interface_id': Parameter(int, "Node interface identifier"), 'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"), - 'type': Parameter(str, "Address type (e.g., 'ipv4')"), - 'ip': Parameter(str, "IP address", nullok = True), 'mac': Parameter(str, "MAC address", nullok = True), - 'gateway': Parameter(str, "IP address of primary gateway", nullok = True), - 'network': Parameter(str, "Subnet address", nullok = True), - 'broadcast': Parameter(str, "Network broadcast address", nullok = True), - 'netmask': Parameter(str, "Subnet mask", nullok = True), - 'dns1': Parameter(str, "IP address of primary DNS server", nullok = True), - 'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True), 'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True), 'hostname': Parameter(str, "(Optional) Hostname", nullok = True), 'node_id': Parameter(int, "Node associated with this interface"), 'is_primary': Parameter(bool, "Is the primary interface for this node"), + 'if_name': Parameter(str, "Interface name", nullok = True), 'interface_tag_ids' : Parameter([int], "List of interface settings"), + 'ip_address_ids': Parameter([int], "List of addresses"), 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), } @@ -100,17 +70,6 @@ class Interface(Row): raise PLCInvalidArgument, "Invalid addressing method %s"%method return method - def validate_type(self, type): - network_types = [row['type'] for row in NetworkTypes(self.api)] - if type not in network_types: - raise PLCInvalidArgument, "Invalid address type %s"%type - return type - - def validate_ip(self, ip): - if ip and not valid_ip(ip): - raise PLCInvalidArgument, "Invalid IP address %s"%ip - return ip - def validate_mac(self, mac): if not mac: return mac @@ -130,19 +89,15 @@ class Interface(Row): return mac - validate_gateway = validate_ip - validate_network = validate_ip - validate_broadcast = validate_ip - validate_netmask = validate_ip - validate_dns1 = validate_ip - validate_dns2 = validate_ip - def validate_bwlimit(self, bwlimit): if not bwlimit: return bwlimit - if bwlimit < 500000: - raise PLCInvalidArgument, 'Minimum bw is 500 kbs' + if bwlimit < 1: + raise PLCInvalidArgument, 'Minimum bw is 1 Mbps' + + if bwlimit >= 1000000: + raise PLCInvalidArgument, 'Maximum bw must be less than 1000000' return bwlimit @@ -195,43 +150,6 @@ class Interface(Row): assert 'method' in self method = self['method'] - if method == "proxy" or method == "tap": - if 'mac' in self and self['mac']: - raise PLCInvalidArgument, "For %s method, mac should not be specified" % method - if 'ip' not in self or not self['ip']: - raise PLCInvalidArgument, "For %s method, ip is required" % method - if method == "tap" and ('gateway' not in self or not self['gateway']): - raise PLCInvalidArgument, "For tap method, gateway is required and should be " \ - "the IP address of the node that proxies for this address" - # Should check that the proxy address is reachable, but - # there's no way to tell if the only primary interface is - # DHCP! - - elif method == "static": - for key in ['gateway', 'dns1']: - if key not in self or not self[key]: - if 'is_primary' in self and self['is_primary'] is True: - raise PLCInvalidArgument, "For static method primary network, %s is required" % key - else: - globals()[key] = self[key] - for key in ['ip', 'network', 'broadcast', 'netmask']: - if key not in self or not self[key]: - raise PLCInvalidArgument, "For static method, %s is required" % key - globals()[key] = self[key] - if not in_same_network(ip, network, netmask): - raise PLCInvalidArgument, "IP address %s is inconsistent with network %s/%s" % \ - (ip, network, netmask) - if not in_same_network(broadcast, network, netmask): - raise PLCInvalidArgument, "Broadcast address %s is inconsistent with network %s/%s" % \ - (broadcast, network, netmask) - if 'gateway' in globals() and not in_same_network(ip, gateway, netmask): - raise PLCInvalidArgument, "Gateway %s is not reachable from %s/%s" % \ - (gateway, ip, netmask) - - elif method == "ipmi": - if 'ip' not in self or not self['ip']: - raise PLCInvalidArgument, "For ipmi method, ip is required" - validate_last_updated = Row.validate_timestamp def update_timestamp(self, col_name, commit = True): @@ -276,11 +194,12 @@ class Interfaces(Table): (", ".join(self.columns.keys()+self.tag_columns.keys()),view) if interface_filter is not None: + # TODO: Deleted the ability here to filter by ipaddress; Need to make + # sure that wasn't used anywhere. if isinstance(interface_filter, (list, tuple, set)): # Separate the list into integers and strings ints = filter(lambda x: isinstance(x, (int, long)), interface_filter) - strs = filter(lambda x: isinstance(x, StringTypes), interface_filter) - interface_filter = Filter(Interface.fields, {'interface_id': ints, 'ip': strs}) + interface_filter = Filter(Interface.fields, {'interface_id': ints}) sql += " AND (%s) %s" % interface_filter.sql(api, "OR") elif isinstance(interface_filter, dict): allowed_fields=dict(Interface.fields.items()+Interface.tags.items()) @@ -289,9 +208,6 @@ class Interfaces(Table): elif isinstance(interface_filter, int): interface_filter = Filter(Interface.fields, {'interface_id': [interface_filter]}) sql += " AND (%s) %s" % interface_filter.sql(api) - elif isinstance (interface_filter, StringTypes): - interface_filter = Filter(Interface.fields, {'ip':[interface_filter]}) - sql += " AND (%s) %s" % interface_filter.sql(api, "AND") else: raise PLCInvalidArgument, "Wrong interface filter %r"%interface_filter diff --git a/PLC/IpAddresses.py b/PLC/IpAddresses.py new file mode 100644 index 0000000..6be9b7d --- /dev/null +++ b/PLC/IpAddresses.py @@ -0,0 +1,232 @@ +# +# Functions for interacting with the interfaces table in the database +# +# Mark Huang +# Copyright (C) 2006 The Trustees of Princeton University +# + +from types import StringTypes +import socket +import struct + +from PLC.Faults import * +from PLC.Parameter import Parameter +from PLC.Filter import Filter +from PLC.Debug import profile +from PLC.Table import Row, Table +from PLC.NetworkTypes import NetworkType, NetworkTypes +from PLC.NetworkMethods import NetworkMethod, NetworkMethods +import PLC.Nodes + +# a class for validating IP address strings and applying netmasks +class SimpleAddress: + def __init__(self, addrStr, type=None, parts=None): + if (not type): + if "." in addrStr: + type="ipv4" + elif ":" in addrStr: + type="ipv6" + else: + raise ValueError, "Unable to determine type of address: " + str(addrStr) + + self.type = type + + if (type=="ipv4"): + # e.g. 1.2.3.4 + self.delim = "." + self.count = 4 + self.base = 10 + self.fieldMask = 0xFF + elif (type=="ipv6"): + # e.g. 2001:db8:85a3:0:0:8a2e:370:7334 + self.delim = ":" + self.count = 8 + self.base = 16 + self.fieldMask = 0xFFFF + else: + raise ValueError, "Unknown type of address: " + str(type) + + if addrStr: + parts = addrStr.split(self.delim) + + # deal with ipv6 groups of zeros notation + # :: represents a group of 0:0:0... fields + if ('' in parts) and (self.type=="ipv6"): + expanded_parts = [] + parts = [elem for i, elem in enumerate(parts) if i == 0 or elem!="" or parts[i-1] != elem] + for i,part in enumerate(parts): + if part=="": + for j in range(0, self.count-len(parts)+1): + expanded_parts.append("0") + else: + expanded_parts.append(part) + parts = expanded_parts + + parts = [int(x, self.base) for x in parts] + else: + if (parts==None): + raise ValueError, "Must supply either addrStr or parts to SimpleAddress" + + if len(parts)!=self.count: + raise ValueError, "Wrong number of fields in address: " + str(addrStr) + + self.parts = parts + + def as_str(self): + if (self.base == 16): + textParts = ["%x"%x for x in self.parts] + else: # self.base == 10 + textParts = [str(x) for x in self.parts] + return self.delim.join(textParts) + + def compute_network(self, netmask): + if (type(netmask)==str) or (type(netmask)==unicode): + netmask = SimpleAddress(netmask) + + if (self.type != netmask.type): + raise ValueError, "Cannot apply " + netmask.type + " netmask to " + self.type + " ip address" + + result = [] + for i in range(0, self.count): + result.append(self.parts[i] & netmask.parts[i]) + + return SimpleAddress(addrStr=None, type=self.type, parts = result) + + def compute_broadcast(self, netmask): + if (type(netmask)==str) or (type(netmask)==unicode): + netmask = SimpleAddress(netmask) + + if (self.type != netmask.type): + raise ValueError, "Cannot apply " + netmask.type + " netmask to " + self.type + " ip address" + + result = [] + for i in range(0, self.count): + result.append(self.parts[i] | (~netmask.parts[i] & self.fieldMask)) + + return SimpleAddress(addrStr=None, type=self.type, parts = result) + +class Subnet: + def __init__(self, subnetStr): + if "/" in subnetStr: + (addrStr, netBitCountStr) = subnetStr.split("/") + self.addr = SimpleAddress(addrStr) + self.netBitCount = int(netBitCountStr) + else: + self.addr = SimpleAddress(subnetStr) + self.netBitCount = None + + def as_str(self): + if self.netBitCount is not None: + return self.addr.as_str() + "/" + str(self.netBitCount) + else: + return self.addr.as_str() + +class IpAddress(Row): + """ + Representation of a row in the ip_addresses table. To use, optionally + instantiate with a dict of values. Update as you would a + dict. Commit to the database with sync(). + """ + + table_name = 'ip_addresses' + primary_key = 'ip_address_id' + join_tables = [] + fields = { + 'ip_address_id': Parameter(int, "IP Address identifier"), + 'interface_id': Parameter(int, "Interface associated with this address"), + 'type': Parameter(str, "Address type (e.g., 'ipv4')"), + 'ip_addr': Parameter(str, "IP Address", nullok = False), + 'netmask': Parameter(str, "Subnet mask", nullok = False), + 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), + } + + tags = {} + + def validate_ip_addr(self, ip): + # SimpleAddress with throw exceptions if the ip + SimpleAddress(ip, self["type"]) + return ip + + validate_netmask = validate_ip_addr + + def validate_interface_id(self, interface_id): + interfaces = PLC.Interfaces.Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCInvalidArgument, "No such interface %d"%interface_id + + return interface_id + + def validate(self): + """ + Flush changes back to the database. + """ + + # Basic validation + Row.validate(self) + + validate_last_updated = Row.validate_timestamp + + def update_timestamp(self, col_name, commit = True): + """ + Update col_name field with current time + """ + + assert 'ip_address_id' in self + assert self.table_name + + self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \ + " where ip_address_id = %d" % (self['ip_address_id']) ) + self.sync(commit) + + def update_last_updated(self, commit = True): + self.update_timestamp('last_updated', commit) + + def delete(self,commit=True): + Row.delete(self) + + def get_network(self): + return SimpleAddress(self["ip_addr"], self["type"]).compute_network(self["netmask"]).as_str() + + def get_broadcast(self): + return SimpleAddress(self["ip_addr"], self["type"]).compute_broadcast(self["netmask"]).as_str() + +class IpAddresses(Table): + """ + Representation of row(s) from the ip_addresses table in the + database. + """ + + def __init__(self, api, ip_address_filter = None, columns = None): + Table.__init__(self, api, IpAddress, columns) + + # the view that we're selecting upon: start with view_ip_addresses + view = "view_ip_addresses" + # as many left joins as requested tags + for tagname in self.tag_columns: + view= "%s left join %s using (%s)"%(view,IpAddress.tagvalue_view_name(tagname), + IpAddress.primary_key) + + sql = "SELECT %s FROM %s WHERE True" % \ + (", ".join(self.columns.keys()+self.tag_columns.keys()),view) + + if ip_address_filter is not None: + if isinstance(ip_address_filter, (list, tuple, set)): + # Separate the list into integers and strings + ints = filter(lambda x: isinstance(x, (int, long)), ip_address_filter) + strs = filter(lambda x: isinstance(x, StringTypes), ip_address_filter) + ip_address_filter = Filter(IpAddress.fields, {'ip_address_id': ints, 'ip_addr': strs}) + sql += " AND (%s) %s" % ip_address_filter.sql(api, "OR") + elif isinstance(ip_address_filter, dict): + allowed_fields=dict(IpAddress.fields.items()+IpAddress.tags.items()) + ip_address_filter = Filter(allowed_fields, ip_address_filter) + sql += " AND (%s) %s" % ip_address_filter.sql(api) + elif isinstance(ip_address_filter, int): + ip_address_filter = Filter(IpAddress.fields, {'ip_address_id': [ip_address_filter]}) + sql += " AND (%s) %s" % ip_address_filter.sql(api) + elif isinstance (ip_address_filter, StringTypes): + ip_address_filter = Filter(IpAddresses.fields, {'ip':[ip_address_filter]}) + sql += " AND (%s) %s" % ip_address_filter.sql(api, "AND") + else: + raise PLCInvalidArgument, "Wrong ip_address filter %r"%ip_address_filter + + self.selectall(sql) diff --git a/PLC/Methods/AddIpAddress.py b/PLC/Methods/AddIpAddress.py new file mode 100644 index 0000000..9847c3c --- /dev/null +++ b/PLC/Methods/AddIpAddress.py @@ -0,0 +1,84 @@ +# +# Thierry Parmentelat - INRIA +# +from PLC.Faults import * +from PLC.Auth import Auth +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row + +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import IpAddresses, IpAddress +from PLC.TagTypes import TagTypes + +cannot_update = ['ip_address_id', 'interface_id'] + +class AddIpAddress(Method): + """ + + Adds a new address for an interface. Any values specified in + ip_address_fields are used, otherwise defaults are + used. + + PIs and techs may only add interfaces to their own nodes. Admins may + add interfaces to any node. + + Returns the new ip_address_id (> 0) if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, IpAddress.fields, exclude=True) + accepted_fields.update(IpAddress.tags) + + accepts = [ + Auth(), + Mixed(Interface.fields['interface_id']), + accepted_fields + ] + + returns = Parameter(int, 'New ip_address_id (> 0) if successful') + + + def call(self, auth, interface_id, ip_address_fields): + + [native,tags,rejected]=Row.split_fields(ip_address_fields,[IpAddress.fields,IpAddress.tags]) + + # type checking + native = Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot add IpAddress with column(s) %r"%rejected + + # Get interface information + interfaces = Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCInvalidArgument, "No such interfaces %r"%interface_id + interface = interfaces[0] + + # Check if node exists + nodes = Nodes(self.api, [interface['node_id']]) + if not nodes: + raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname + node = nodes[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to add an interface to the specified node" + + # Add interface + ip_address = IpAddress(self.api, native) + ip_address['interface_id'] = interface['interface_id'] + ip_address.sync() + + # Logging variables + self.event_objects = { 'Interace': [interface['interface_id']], + 'IpAddress' : [ip_address['ip_address_id']] } + self.message = "Address %d added" % ip_address['ip_address_id'] + + return ip_address['ip_address_id'] diff --git a/PLC/Methods/AddNode.py b/PLC/Methods/AddNode.py index 0ee13f5..a33cebd 100644 --- a/PLC/Methods/AddNode.py +++ b/PLC/Methods/AddNode.py @@ -1,3 +1,5 @@ +# $Id$ +# $URL$ from PLC.Faults import * from PLC.Auth import Auth from PLC.Method import Method @@ -12,7 +14,7 @@ from PLC.NodeTags import NodeTags, NodeTag from PLC.Methods.AddNodeTag import AddNodeTag from PLC.Methods.UpdateNodeTag import UpdateNodeTag -can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version'] +can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version', 'dns', 'ntp'] class AddNode(Method): """ diff --git a/PLC/Methods/AddRoute.py b/PLC/Methods/AddRoute.py new file mode 100644 index 0000000..4829028 --- /dev/null +++ b/PLC/Methods/AddRoute.py @@ -0,0 +1,78 @@ +# +# Thierry Parmentelat - INRIA +# +from PLC.Faults import * +from PLC.Auth import Auth +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row + +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.Routes import Routes, Route +from PLC.TagTypes import TagTypes + +cannot_update = ['route_id'] + +class AddRoute(Method): + """ + + Adds a new route to a node. Any values specified in + route_fields are used, otherwise defaults are + used. + + PIs and techs may only add routes to their own nodes. Admins may + add interfaces to any node. + + Returns the new route_id (> 0) if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, Route.fields, exclude=True) + accepted_fields.update(Route.tags) + + accepts = [ + Auth(), + Mixed(Node.fields['node_id']), + accepted_fields + ] + + returns = Parameter(int, 'New ip_address_id (> 0) if successful') + + + def call(self, auth, node_id, route_fields): + + [native,tags,rejected]=Row.split_fields(route_fields,[Route.fields,Route.tags]) + + # type checking + native = Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot add Route with column(s) %r"%rejected + + # Get interface information + nodes = Nodes(self.api, [node_id]) + if not nodes: + raise PLCInvalidArgument, "No such nodes %r"%node_id + node = nodes[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to add an interface to the specified node" + + # Add route + route = Route(self.api, native) + route['node_id'] = node['node_id'] + route.sync() + + # Logging variables + self.event_objects = { 'Node': [node['node_id']], + 'Route' : [route['route_id']] } + self.message = "Route %d added" % route['route_id'] + + return route['route_id'] diff --git a/PLC/Methods/DeleteIpAddress.py b/PLC/Methods/DeleteIpAddress.py new file mode 100644 index 0000000..06efce5 --- /dev/null +++ b/PLC/Methods/DeleteIpAddress.py @@ -0,0 +1,64 @@ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Auth import Auth +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import IpAddress, IpAddresses + +class DeleteIpAddress(Method): + """ + Deletes an existing ip address. + + Admins may delete any ip address. PIs and techs may only delete + ip addresses associated with interfaces on nodes at their sites. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + IpAddress.fields['ip_address_id'] + ] + + returns = Parameter(int, '1 if successful') + + + def call(self, auth, ip_address_id): + + # Get interface information + ip_addresses = IpAddresses(self.api, [ip_address_id]) + if not ip_addresses: + raise PLCInvalidArgument, "No such ip_address %r"%ip_address_id + ip_address = ip_addresses[0] + + # Authenticated functino + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site at which the node is located. + if 'admin' not in self.caller['roles']: + # Get interface information + interfaces = Interfaces(self.api, [ip_address['interface_id']]) + if not interfaces: + raise PLCInvalidArgument, "No such interface %r"%ip_address['interface_id'] + interface = interfaces[0] + + # Get node information + nodes = Nodes(self.api, [interface['node_id']]) + if not nodes: + raise PLCInvalidArgument, "No such node %r"%node_id + node = nodes[0] + + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete this ip address" + + ip_address.delete() + + # Logging variables + self.event_objects = {'IpAddress': [ip_address['ip_address_id']]} + self.message = "IpAddress %d deleted" % ip_address['ip_address_id'] + + return 1 diff --git a/PLC/Methods/DeleteRoute.py b/PLC/Methods/DeleteRoute.py new file mode 100644 index 0000000..9f3fff4 --- /dev/null +++ b/PLC/Methods/DeleteRoute.py @@ -0,0 +1,58 @@ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Auth import Auth +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.Routes import Route, Routes + +class DeleteRoute(Method): + """ + Deletes an existing ip address. + + Admins may delete any ip address. PIs and techs may only delete + ip addresses associated with interfaces on nodes at their sites. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + Route.fields['route_id'] + ] + + returns = Parameter(int, '1 if successful') + + + def call(self, auth, route_id): + + # Get route information + routes = Routes(self.api, [route_id]) + if not routes: + raise PLCInvalidArgument, "No such route %r"%route_id + route = routes[0] + + # Authenticated functino + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site at which the node is located. + if 'admin' not in self.caller['roles']: + # Get node information + nodes = Nodes(self.api, [route['node_id']]) + if not nodes: + raise PLCInvalidArgument, "Route is not associated with a valid node: %r" % route['node_id'] + node = nodes[0] + + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete this route" + + route.delete() + + # Logging variables + self.event_objects = {'Route': [route['route_id']]} + self.message = "Route %d deleted" % route['route_id'] + + return 1 diff --git a/PLC/Methods/GetInterfaces.py b/PLC/Methods/GetInterfaces.py index a154c8e..239aba9 100644 --- a/PLC/Methods/GetInterfaces.py +++ b/PLC/Methods/GetInterfaces.py @@ -1,3 +1,5 @@ +# $Id$ +# $URL$ from PLC.Faults import * from PLC.Method import Method from PLC.Parameter import Parameter, Mixed @@ -20,10 +22,8 @@ class GetInterfaces(Method): accepts = [ Auth(), - Mixed([Mixed(Interface.fields['interface_id'], - Interface.fields['ip'])], + Mixed([Mixed(Interface.fields['interface_id'])], Parameter (int, "interface id"), - Parameter (str, "ip address"), Filter(Interface.fields)), Parameter([str], "List of fields to return", nullok = True) ] diff --git a/PLC/Methods/GetIpAddresses.py b/PLC/Methods/GetIpAddresses.py new file mode 100644 index 0000000..90b88f3 --- /dev/null +++ b/PLC/Methods/GetIpAddresses.py @@ -0,0 +1,36 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Filter import Filter +from PLC.IpAddresses import IpAddress, IpAddresses +from PLC.Auth import Auth + +class GetIpAddresses(Method): + """ + Returns an array of structs containing details about network + interfaces. If interfaces_filter is specified and is an array of + interface identifiers, or a struct of interface fields and + values, only interfaces matching the filter will be + returned. + + If return_fields is given, only the specified details will be returned. + """ + + roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous'] + + accepts = [ + Auth(), + Mixed([Mixed(IpAddress.fields['ip_address_id'], + IpAddress.fields['ip_addr'])], + Parameter (int, "interface id"), + Parameter (str, "ip address"), + Filter(IpAddress.fields)), + Parameter([str], "List of fields to return", nullok = True) + ] + + returns = [IpAddress.fields] + + def call(self, auth, ip_address_filter = None, return_fields = None): + return IpAddresses(self.api, ip_address_filter, return_fields) diff --git a/PLC/Methods/GetRoutes.py b/PLC/Methods/GetRoutes.py new file mode 100644 index 0000000..a003773 --- /dev/null +++ b/PLC/Methods/GetRoutes.py @@ -0,0 +1,34 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Filter import Filter +from PLC.Routes import Route, Routes +from PLC.Auth import Auth + +class GetRoutes(Method): + """ + Returns an array of structs containing details about routes. + If interfaces_filter is specified and is an array of + interface identifiers, or a struct of interface fields and + values, only interfaces matching the filter will be + returned. + + If return_fields is given, only the specified details will be returned. + """ + + roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous'] + + accepts = [ + Auth(), + Mixed([Mixed(Route.fields['route_id'])], + Parameter (int, "route id"), + Filter(Route.fields)), + Parameter([str], "List of fields to return", nullok = True) + ] + + returns = [Route.fields] + + def call(self, auth, route_filter = None, return_fields = None): + return Routes(self.api, route_filter, return_fields) diff --git a/PLC/Methods/Legacy/AddInterface.py b/PLC/Methods/Legacy/AddInterface.py new file mode 100644 index 0000000..5d2774d --- /dev/null +++ b/PLC/Methods/Legacy/AddInterface.py @@ -0,0 +1,148 @@ +from PLC.Faults import * +from PLC.Auth import Auth +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row + +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import IpAddress, IpAddresses +from PLC.Routes import Route, Routes +from PLC.TagTypes import TagTypes +from PLC.InterfaceTags import InterfaceTags +from PLC.Methods.AddInterfaceTag import AddInterfaceTag +from PLC.Methods.UpdateInterfaceTag import UpdateInterfaceTag +from PLC.Methods.UpdateNode import UpdateNode + +cannot_update = ['interface_id', 'node_id'] + +legacy_interface_fields = { + 'interface_id': Parameter(int, "Node interface identifier"), + 'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"), + 'type': Parameter(str, "Address type (e.g., 'ipv4')"), + 'ip': Parameter(str, "IP address", nullok = True), + 'mac': Parameter(str, "MAC address", nullok = True), + 'gateway': Parameter(str, "IP address of primary gateway", nullok = True), + 'network': Parameter(str, "Subnet address", nullok = True), + 'broadcast': Parameter(str, "Network broadcast address", nullok = True), + 'netmask': Parameter(str, "Subnet mask", nullok = True), + 'dns1': Parameter(str, "IP address of primary DNS server", nullok = True), + 'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True), + 'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True), + 'hostname': Parameter(str, "(Optional) Hostname", nullok = True), + 'node_id': Parameter(int, "Node associated with this interface"), + 'is_primary': Parameter(bool, "Is the primary interface for this node"), + 'interface_tag_ids' : Parameter([int], "List of interface settings"), + 'last_updated': Parameter(int, "Date and time when the interface entry was last updated"), + } + +class AddInterface(Method): + """ + + Adds a new network for a node. Any values specified in + interface_fields are used, otherwise defaults are + used. + + If type is static, then ip, gateway, network, broadcast, netmask, + and dns1 must all be specified in interface_fields. If type is + dhcp, these parameters, even if specified, are ignored. + + PIs and techs may only add interfaces to their own nodes. Admins may + add interfaces to any node. + + Returns the new interface_id (> 0) if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, legacy_interface_fields, exclude=True) + accepted_fields.update(Interface.tags) + + accepts = [ + Auth(), + Mixed(Node.fields['node_id'], + Node.fields['hostname']), + accepted_fields + ] + + returns = Parameter(int, 'New interface_id (> 0) if successful') + + + def call(self, auth, node_id_or_hostname, interface_fields): + + [native,tags,rejected]=Row.split_fields(interface_fields,[legacy_interface_fields,Interface.tags]) + + # type checking + native = Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot add Interface with column(s) %r"%rejected + + # Check if node exists + nodes = Nodes(self.api, [node_id_or_hostname]) + if not nodes: + raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname + node = nodes[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to add an interface to the specified node" + + # Add interface + interface = Interface(self.api, native) + interface['node_id'] = node['node_id'] + # if this is the first interface, make it primary + if not node['interface_ids']: + interface['is_primary'] = True + interface.sync() + + # Add IpAddress to conform with the new object model + if native['method'] == 'static': + address_fields = {} + address_fields['interface_id'] = interface['interface_id'] + address_fields['ip_addr'] = native['ip'] + address_fields['netmask'] = native['netmask'] + address_fields['type'] = native['type'] + ip_address = IpAddress(self.api, address_fields) + ip_address.sync() + + # ADD DNS and ROUTES if this is a primary interface + if native['is_primary']: + route_fields = {} + route_fields['node_id'] = interface['node_id'] + route_fields['interface_id'] = interface['interface_id'] + route_fields['subnet'] = '0.0.0.0/0' + route_fields['next_hop'] = interface['gateway'] + route = Route(self.api, route_fields) + route.sync() + + dns = "" + if native.has_key('dns1'): + dns += native['dns1'] + if native.has_key('dns2'): + dns += ",%s" % native['dns2'] + + if dns: + UpdateNode(self.api).__call__(auth, interface['node_id'], {'dns':dns}) + + + # Logging variables + self.event_objects = { 'Node': [node['node_id']], + 'Interface' : [interface['interface_id']] } + self.message = "Interface %d added" % interface['interface_id'] + + for (tagname,value) in tags.iteritems(): + # the tagtype instance is assumed to exist, just check that + if not TagTypes(self.api,{'tagname':tagname}): + raise PLCInvalidArgument,"No such TagType %s"%tagname + interface_tags=InterfaceTags(self.api,{'tagname':tagname,'interface_id':interface['interface_id']}) + if not interface_tags: + AddInterfaceTag(self.api).__call__(auth,interface['interface_id'],tagname,value) + else: + UpdateInterfaceTag(self.api).__call__(auth,interface_tags[0]['interface_tag_id'],value) + + return interface['interface_id'] diff --git a/PLC/Methods/Legacy/AddNode.py b/PLC/Methods/Legacy/AddNode.py new file mode 100644 index 0000000..0fc7e5a --- /dev/null +++ b/PLC/Methods/Legacy/AddNode.py @@ -0,0 +1,141 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Auth import Auth +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row +from PLC.Namespace import hostname_to_hrn +from PLC.Peers import Peers +from PLC.Sites import Site, Sites +from PLC.Nodes import Node, Nodes +from PLC.TagTypes import TagTypes +from PLC.NodeTags import NodeTags, NodeTag +from PLC.Methods.AddNodeTag import AddNodeTag +from PLC.Methods.UpdateNodeTag import UpdateNodeTag + +can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version'] + +legacy_node_fields = { + 'node_id': Parameter(int, "Node identifier"), + 'node_type': Parameter(str,"Node type",max=20), + 'hostname': Parameter(str, "Fully qualified hostname", max = 255), + 'site_id': Parameter(int, "Site at which this node is located"), + 'boot_state': Parameter(str, "Boot state", max = 20), + 'run_level': Parameter(str, "Run level", max = 20), + 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True), + 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128), + 'version': Parameter(str, "Apparent Boot CD version", max = 64), + 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024), + 'date_created': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), + 'last_boot': Parameter(int, "Date and time when node last booted", ro = True), + 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True), + 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True), + 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True), + 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True), + 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True), + 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False), + 'key': Parameter(str, "(Admin only) Node key", max = 256), + 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True), + 'interface_ids': Parameter([int], "List of network interfaces that this node has"), + 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"), + # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"), + 'slice_ids': Parameter([int], "List of slices on this node"), + 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"), + 'pcu_ids': Parameter([int], "List of PCUs that control this node"), + 'ports': Parameter([int], "List of PCU ports that this node is connected to"), + 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True), + 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True), + 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"), + 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"), + } + + +class AddNode(Method): + """ + Adds a new node. Any values specified in node_fields are used, + otherwise defaults are used. + + PIs and techs may only add nodes to their own sites. Admins may + add nodes to any site. + + Returns the new node_id (> 0) if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(can_update,legacy_node_fields) + accepted_fields.update(Node.tags) + + accepts = [ + Auth(), + Mixed(Site.fields['site_id'], + Site.fields['login_base']), + accepted_fields + ] + + returns = Parameter(int, 'New node_id (> 0) if successful') + + def call(self, auth, site_id_or_login_base, node_fields): + + [native,tags,rejected]=Row.split_fields(node_fields,[legacy_node_fields,Node.tags]) + + # type checking + native = Row.check_fields(native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot add Node with column(s) %r"%rejected + + # Get site information + sites = Sites(self.api, [site_id_or_login_base]) + if not sites: + raise PLCInvalidArgument, "No such site" + + site = sites[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site. + if 'admin' not in self.caller['roles']: + if site['site_id'] not in self.caller['site_ids']: + assert self.caller['person_id'] not in site['person_ids'] + raise PLCPermissionDenied, "Not allowed to add nodes to specified site" + else: + assert self.caller['person_id'] in site['person_ids'] + + node = Node(self.api, native) + node['site_id'] = site['site_id'] + node.sync() + + # since hostname was specified lets add the 'hrn' node tag + root_auth = self.api.config.PLC_HRN_ROOT + login_base = site['login_base'] + tags['hrn'] = hostname_to_hrn(root_auth, login_base, node['hostname']) + + for (tagname,value) in tags.iteritems(): + # the tagtype instance is assumed to exist, just check that + tag_types = TagTypes(self.api,{'tagname':tagname}) + if not tag_types: + raise PLCInvalidArgument,"No such TagType %s"%tagname + tag_type = tag_types[0] + node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']}) + if not node_tags: + node_tag = NodeTag(self.api) + node_tag['node_id'] = node['node_id'] + node_tag['tag_type_id'] = tag_type['tag_type_id'] + node_tag['tagname'] = tagname + node_tag['value'] = value + node_tag.sync() + else: + node_tag = node_tags[0] + node_tag['value'] = value + node_tag.sync() + + self.event_objects = {'Site': [site['site_id']], + 'Node': [node['node_id']]} + self.message = "Node %d=%s created" % (node['node_id'],node['hostname']) + + return node['node_id'] diff --git a/PLC/Methods/Legacy/DeleteInterface.py b/PLC/Methods/Legacy/DeleteInterface.py new file mode 100644 index 0000000..82d0f7e --- /dev/null +++ b/PLC/Methods/Legacy/DeleteInterface.py @@ -0,0 +1,63 @@ +from PLC.Faults import * +from PLC.Auth import Auth +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row + +from PLC.Nodes import Node, Nodes +from PLC.Interfaces import Interface, Interfaces +from PLC.Methods.DeleteIpAddress import DeleteIpAddress + +class DeleteInterface(Method): + """ + Deletes an existing interface. + + Admins may delete any interface. PIs and techs may only delete + interface interfaces associated with nodes at their sites. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + Interface.fields['interface_id'] + ] + + returns = Parameter(int, '1 if successful') + + + def call(self, auth, interface_id): + + # Get interface information + interfaces = Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCInvalidArgument, "No such interface %r"%interface_id + interface = interfaces[0] + + # Get node information + nodes = Nodes(self.api, [interface['node_id']]) + if not nodes: + raise PLCInvalidArgument, "No such node %r"%node_id + node = nodes[0] + + # Authenticated functino + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site at which the node is located. + if 'admin' not in self.caller['roles']: + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete this interface" + + for ip_address_id in interface['ip_address_ids']: + DeleteIpAddress(self.api).__call__(auth, ip_address_id) + + interface.delete() + + # Logging variables + self.event_objects = {'Interface': [interface['interface_id']]} + self.message = "Interface %d deleted" % interface['interface_id'] + + return 1 diff --git a/PLC/Methods/Legacy/DeleteNode.py b/PLC/Methods/Legacy/DeleteNode.py new file mode 100644 index 0000000..01e382a --- /dev/null +++ b/PLC/Methods/Legacy/DeleteNode.py @@ -0,0 +1,57 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Auth import Auth +from PLC.Nodes import Node, Nodes + +class DeleteNode(Method): + """ + Mark an existing node as deleted. + + PIs and techs may only delete nodes at their own sites. ins may + delete nodes at any site. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepts = [ + Auth(), + Mixed(Node.fields['node_id'], + Node.fields['hostname']) + ] + + returns = Parameter(int, '1 if successful') + + def call(self, auth, node_id_or_hostname): + # Get account information + nodes = Nodes(self.api, [node_id_or_hostname]) + if not nodes: + raise PLCInvalidArgument, "No such node" + node = nodes[0] + + if node['peer_id'] is not None: + raise PLCInvalidArgument, "Not a local node" + + # If we are not an admin, make sure that the caller is a + # member of the site at which the node is located. + if 'admin' not in self.caller['roles']: + # Authenticated function + assert self.caller is not None + + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete nodes from specified site" + + node_id=node['node_id'] + site_id=node['site_id'] + node.delete() + + # Logging variables + # it's not much use to attach to the node as it's going to vanish... + self.event_objects = {'Node': [node_id], 'Site': [site_id] } + self.message = "Node %d deleted" % node['node_id'] + + return 1 diff --git a/PLC/Methods/Legacy/GetInterfaces.py b/PLC/Methods/Legacy/GetInterfaces.py new file mode 100644 index 0000000..d5938e9 --- /dev/null +++ b/PLC/Methods/Legacy/GetInterfaces.py @@ -0,0 +1,112 @@ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Filter import Filter +from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import IpAddress, IpAddresses +from PLC.Routes import Route, Routes +from PLC.Nodes import Node, Nodes +from PLC.Auth import Auth + + +legacy_interface_fields = { + 'interface_id': Parameter(int, "Node interface identifier"), + 'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"), + 'type': Parameter(str, "Address type (e.g., 'ipv4')"), + 'ip': Parameter(str, "IP address", nullok = True), + 'mac': Parameter(str, "MAC address", nullok = True), + 'gateway': Parameter(str, "IP address of primary gateway", nullok = True), + 'network': Parameter(str, "Subnet address", nullok = True), + 'broadcast': Parameter(str, "Network broadcast address", nullok = True), + 'netmask': Parameter(str, "Subnet mask", nullok = True), + 'dns1': Parameter(str, "IP address of primary DNS server", nullok = True), + 'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True), + 'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True), + 'hostname': Parameter(str, "(Optional) Hostname", nullok = True), + 'node_id': Parameter(int, "Node associated with this interface"), + 'is_primary': Parameter(bool, "Is the primary interface for this node"), + 'interface_tag_ids' : Parameter([int], "List of interface settings"), + 'last_updated': Parameter(int, "Date and time when the interface entry was last updated"), + } + +def clean_interface_fields(interface): + remove_keys = [key for key in interface.keys() if key not in legacy_interface_fields.keys()] + for key in remove_keys: + del interface[key] + return interface + + +class GetInterfaces(Method): + """ + Returns an array of structs containing details about network + interfaces. If interfaces_filter is specified and is an array of + interface identifiers, or a struct of interface fields and + values, only interfaces matching the filter will be + returned. + + If return_fields is given, only the specified details will be returned. + """ + + roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous'] + + accepts = [ + Auth(), + Mixed([Mixed(Interface.fields['interface_id'])], + Parameter (int, "interface id"), + Filter(legacy_interface_fields)), + Parameter([str], "List of fields to return", nullok = True) + ] + + returns = [Interface.fields] + ["type", "ip", "netmask", "network", "broadcast"] + + def call(self, auth, interface_filter = None, return_fields = None): + if (return_fields is not None): + interface_return_fields = return_fields[:] + if not ("ip_address_ids" in interface_return_fields): + interface_return_fields += ["ip_address_ids"] + interface_return_fields = [f for f in interface_return_fields if (f in Interface.fields)] + else: + interface_return_fields = return_fields + + interfaces = Interfaces(self.api, interface_filter, interface_return_fields) + + + for interface in interfaces: + ip_address_ids = interface["ip_address_ids"] + if ip_address_ids: + # we'll use the first IpAddress only + addresses = IpAddresses(self.api, ip_address_ids[0]) + if (not return_fields) or ("type" in return_fields): + interface["type"] = addresses[0]["type"] + if (not return_fields) or ("ip" in return_fields): + interface["ip"] = addresses[0]["ip_addr"] + if (not return_fields) or ("netmask" in return_fields): + interface["netmask"] = addresses[0]["netmask"] + if (not return_fields) or ("network" in return_fields): + interface["network"] = addresses[0].get_network() + if (not return_fields) or ("broadcast" in return_fields): + interface["broadcast"] = addresses[0].get_broadcast() + + # add default gw + routes = Routes(self.api, {'node_id' : interface['node_id'], 'subnet' : '0.0.0.0/0'}) + if routes: + interface['gateway'] = routes[0]['next_hop'] + + dns1 = None + dns2 = None + nodes = Node(self.api, {'node_id' : interface['node_id']}) + if nodes and nodes.has_key('dns'): + # we'll only get the first two dns records + dns_records = nodes[0]['dns'].split(',') + if dns_records: + dns1 = dns_records[0] + if len(dns_records) > 1: + dns2 = dns_records[1] + + interface['dns1'] = dns1 + interface['dns2'] = dns2 + + interface = clean_interface_fields(interface) + + return interfaces + diff --git a/PLC/Methods/Legacy/GetNodes.py b/PLC/Methods/Legacy/GetNodes.py new file mode 100644 index 0000000..84fddb9 --- /dev/null +++ b/PLC/Methods/Legacy/GetNodes.py @@ -0,0 +1,137 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Filter import Filter +from PLC.Nodes import Node, Nodes +from PLC.Persons import Person, Persons +from PLC.Auth import Auth + +admin_only = ['key', 'session', 'boot_nonce' ] + +legacy_node_fields = { + 'node_id': Parameter(int, "Node identifier"), + 'node_type': Parameter(str,"Node type",max=20), + 'hostname': Parameter(str, "Fully qualified hostname", max = 255), + 'site_id': Parameter(int, "Site at which this node is located"), + 'boot_state': Parameter(str, "Boot state", max = 20), + 'run_level': Parameter(str, "Run level", max = 20), + 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True), + 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128), + 'version': Parameter(str, "Apparent Boot CD version", max = 64), + 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024), + 'date_created': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), + 'last_boot': Parameter(int, "Date and time when node last booted", ro = True), + 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True), + 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True), + 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True), + 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True), + 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True), + 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False), + 'key': Parameter(str, "(Admin only) Node key", max = 256), + 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True), + 'interface_ids': Parameter([int], "List of network interfaces that this node has"), + 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"), + # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"), + 'slice_ids': Parameter([int], "List of slices on this node"), + 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"), + 'pcu_ids': Parameter([int], "List of PCUs that control this node"), + 'ports': Parameter([int], "List of PCU ports that this node is connected to"), + 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True), + 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True), + 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"), + 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"), + } + +def clean_node_fields(node): + remove_keys = [key for key in node.keys() if key not in legacy_node_fields.keys()] + for key in remove_keys: + del node[key] + return node + + +class GetNodes(Method): + """ + Returns an array of structs containing details about nodes. If + node_filter is specified and is an array of node identifiers or + hostnames, or a struct of node attributes, only nodes matching the + filter will be returned. + + If return_fields is specified, only the specified details will be + returned. NOTE that if return_fields is unspecified, the complete + set of native fields are returned, which DOES NOT include tags at + this time. + + Some fields may only be viewed by admins. + """ + + roles = ['admin', 'pi', 'user', 'tech', 'node', 'anonymous'] + + accepts = [ + Auth(), + Mixed([Mixed(Node.fields['node_id'], + Node.fields['hostname'])], + Parameter(str,"hostname"), + Parameter(int,"node_id"), + Filter(legacy_node_fields)), + Parameter([str], "List of fields to return", nullok = True), + ] + + returns = [legacy_node_fields] + + + def call(self, auth, node_filter = None, return_fields = None): + + # Must query at least slice_ids_whitelist + if return_fields is not None: + added_fields = set(['slice_ids_whitelist', 'site_id']).difference(return_fields) + return_fields += added_fields + else: + added_fields =[] + + # Get node information + nodes = Nodes(self.api, node_filter, return_fields) + + # Remove admin only fields + if not isinstance(self.caller, Person) or \ + 'admin' not in self.caller['roles']: + slice_ids = set() + site_ids = set() + + if self.caller: + slice_ids.update(self.caller['slice_ids']) + if isinstance(self.caller, Node): + site_ids.update([self.caller['site_id']]) + else: + site_ids.update(self.caller['site_ids']) + + # if node has whitelist, only return it if users is at + # the same site or user has a slice on the whitelist + for node in nodes[:]: + if 'site_id' in node and \ + site_ids.intersection([node['site_id']]): + continue + if 'slice_ids_whitelist' in node and \ + node['slice_ids_whitelist'] and \ + not slice_ids.intersection(node['slice_ids_whitelist']): + nodes.remove(node) + + # remove remaining admin only fields + for node in nodes: + for field in admin_only: + if field in node: + del node[field] + + # remove added fields if not specified + if added_fields: + for node in nodes: + for field in added_fields: + del node[field] + + for node in nodes: + node = clean_node_fields(node) + + return nodes diff --git a/PLC/Methods/Legacy/UpdateInterface.py b/PLC/Methods/Legacy/UpdateInterface.py new file mode 100644 index 0000000..d2119c9 --- /dev/null +++ b/PLC/Methods/Legacy/UpdateInterface.py @@ -0,0 +1,198 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row +from PLC.Auth import Auth + +from PLC.Nodes import Node, Nodes +from PLC.IpAddresses import IpAddress, IpAddresses +from PLC.TagTypes import TagTypes +from PLC.InterfaceTags import InterfaceTags +from PLC.Interfaces import Interface, Interfaces +from PLC.Methods.AddIpAddress import AddIpAddress +from PLC.Methods.DeleteIpAddress import DeleteIpAddress +from PLC.Methods.DeleteRoute import DeleteRoute +from PLC.Methods.AddRoute import AddRoute +from PLC.Methods.UpdateNode import UpdateNode +from PLC.Methods.AddInterfaceTag import AddInterfaceTag +from PLC.Methods.UpdateInterfaceTag import UpdateInterfaceTag + +cannot_update = ['interface_id','node_id'] + +legacy_interface_fields = { + 'interface_id': Parameter(int, "Node interface identifier"), + 'method': Parameter(str, "Addressing method (e.g., 'static' or 'dhcp')"), + 'type': Parameter(str, "Address type (e.g., 'ipv4')"), + 'ip': Parameter(str, "IP address", nullok = True), + 'mac': Parameter(str, "MAC address", nullok = True), + 'gateway': Parameter(str, "IP address of primary gateway", nullok = True), + 'network': Parameter(str, "Subnet address", nullok = True), + 'broadcast': Parameter(str, "Network broadcast address", nullok = True), + 'netmask': Parameter(str, "Subnet mask", nullok = True), + 'dns1': Parameter(str, "IP address of primary DNS server", nullok = True), + 'dns2': Parameter(str, "IP address of secondary DNS server", nullok = True), + 'bwlimit': Parameter(int, "Bandwidth limit", min = 0, nullok = True), + 'hostname': Parameter(str, "(Optional) Hostname", nullok = True), + 'node_id': Parameter(int, "Node associated with this interface"), + 'is_primary': Parameter(bool, "Is the primary interface for this node"), + 'interface_tag_ids' : Parameter([int], "List of interface settings"), + 'last_updated': Parameter(int, "Date and time when the interface entry was last updated"), + } + + +class UpdateInterface(Method): + """ + Updates an existing interface network. Any values specified in + interface_fields are used, otherwise defaults are + used. Acceptable values for method are dhcp and static. If type is + static, then ip, gateway, network, broadcast, netmask, and dns1 + must all be specified in interface_fields. If type is dhcp, + these parameters, even if specified, are ignored. + + PIs and techs may only update interfaces associated with their own + nodes. Admins may update any interface network. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, legacy_interface_fields,exclude=True) + accepted_fields.update(Interface.tags) + + accepts = [ + Auth(), + Interface.fields['interface_id'], + accepted_fields + ] + + returns = Parameter(int, '1 if successful') + + def call(self, auth, interface_id, interface_fields): + + [native,tags,rejected] = Row.split_fields(interface_fields,[legacy_interface_fields,Interface.tags]) + + # type checking + native= Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot update Interface column(s) %r"%rejected + + # Get interface information + interfaces = Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCInvalidArgument, "No such interface" + interface = interfaces[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + nodes = Nodes(self.api, [interface['node_id']]) + if not nodes: + raise PLCPermissionDenied, "Interface is not associated with a node" + node = nodes[0] + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to update interface" + + + # In case the user is updating one of (ip, type, network), but not the + # other two, then fetch the address and get the other two. If the + # interface has more than one address, then the behavior is undefined. + # we'll update the first IpAddress only + ip_address_ids = interface["ip_address_ids"] + address_fields = {'type': 'ipv4'} + if ip_address_ids: + address_fields = IpAddresses(self.api, ip_address_ids[0])[0] + + def clean_field(obj, key): + val = None + if obj.has_key(key): + val = obj[key] + del obj[key] + return val + + val = clean_field(native, 'ip') + if val: + address_fields["ip_addr"] = val + + val = clean_field(native, 'type') + if val: + address_fields["type"] = val + + val = clean_field(native, 'netmask') + if val: + address_fields["netmask"] = val + + for key in ("network", "broadcast"): + clean_field(native, key) + + for key in ("last_updated", "ip_address_id", "interface_id"): + clean_field(address_fields, key) + + + + # check if DNS or gateway is changed if this is primary interface + dns = "" + route_fields = {} + if interface['is_primary'] and native.has_key('gateway'): + route_fields['node_id'] = interface['node_id'] + route_fields['interface_id'] = interface['interface_id'] + route_fields['subnet'] = '0.0.0.0/0' + route_fields['next_hop'] = native['gateway'] + + dns = "" + if native.has_key('dns1'): + dns += native['dns1'] + if native.has_key('dns2'): + dns += ",%s" % native['dns2'] + + + interface.update(native) + interface.sync() + + + # we have no idea which one to delete if there's multiple interfaces, + # so delete them all. + for ip_address_id in ip_address_ids: + DeleteIpAddress(self.api).__call__(auth, ip_address_id) + + AddIpAddress(self.api).__call__(auth, interface_id, address_fields) + + + + # remove routes for interface and add new default gw if this is a primary interface + if route_fields: + routes = Routes(self.api, + {'interface_id' : route_fields['interface_id'], + 'subnet' : route_fields['subnet']}) + for route in routes: + DeleteRoute(self.api).__call__(auth, route['route_id']) + AddRoute(self.api).__call(auth, route_fields) + + # update dns if this is primary + if dns: + UpdateNode(self.api).__call__(auth, interface['node_id'], {'dns':dns}) + + + for (tagname,value) in tags.iteritems(): + # the tagtype instance is assumed to exist, just check that + if not TagTypes(self.api,{'tagname':tagname}): + raise PLCInvalidArgument,"No such TagType %s"%tagname + interface_tags=InterfaceTags(self.api,{'tagname':tagname,'interface_id':interface['interface_id']}) + if not interface_tags: + AddInterfaceTag(self.api).__call__(auth,interface['interface_id'],tagname,value) + else: + UpdateInterfaceTag(self.api).__call__(auth,interface_tags[0]['interface_tag_id'],value) + + self.event_objects = {'Interface': [interface['interface_id']]} + if 'ip' in interface: + self.message = "Interface %s updated"%interface['ip'] + else: + self.message = "Interface %d updated"%interface['interface_id'] + self.message += "[%s]." % ", ".join(interface_fields.keys()) + + return 1 diff --git a/PLC/Methods/Legacy/UpdateNode.py b/PLC/Methods/Legacy/UpdateNode.py new file mode 100644 index 0000000..a0e8ee4 --- /dev/null +++ b/PLC/Methods/Legacy/UpdateNode.py @@ -0,0 +1,164 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row +from PLC.Auth import Auth +from PLC.Namespace import hostname_to_hrn +from PLC.Peers import Peers +from PLC.Sites import Sites +from PLC.Nodes import Node, Nodes +from PLC.TagTypes import TagTypes +from PLC.NodeTags import NodeTags, NodeTag +from PLC.Methods.AddNodeTag import AddNodeTag +from PLC.Methods.UpdateNodeTag import UpdateNodeTag + +admin_only = [ 'key', 'session', 'boot_nonce', 'site_id'] +can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version'] + admin_only + +legacy_node_fields = { + 'node_id': Parameter(int, "Node identifier"), + 'node_type': Parameter(str,"Node type",max=20), + 'hostname': Parameter(str, "Fully qualified hostname", max = 255), + 'site_id': Parameter(int, "Site at which this node is located"), + 'boot_state': Parameter(str, "Boot state", max = 20), + 'run_level': Parameter(str, "Run level", max = 20), + 'model': Parameter(str, "Make and model of the actual machine", max = 255, nullok = True), + 'boot_nonce': Parameter(str, "(Admin only) Random value generated by the node at last boot", max = 128), + 'version': Parameter(str, "Apparent Boot CD version", max = 64), + 'ssh_rsa_key': Parameter(str, "Last known SSH host key", max = 1024), + 'date_created': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), + 'last_contact': Parameter(int, "Date and time when node last contacted plc", ro = True), + 'last_boot': Parameter(int, "Date and time when node last booted", ro = True), + 'last_download': Parameter(int, "Date and time when node boot image was created", ro = True), + 'last_pcu_reboot': Parameter(int, "Date and time when PCU reboot was attempted", ro = True), + 'last_pcu_confirmation': Parameter(int, "Date and time when PCU reboot was confirmed", ro = True), + 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True), + 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True), + 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False), + 'key': Parameter(str, "(Admin only) Node key", max = 256), + 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True), + 'interface_ids': Parameter([int], "List of network interfaces that this node has"), + 'conf_file_ids': Parameter([int], "List of configuration files specific to this node"), + # 'root_person_ids': Parameter([int], "(Admin only) List of people who have root access to this node"), + 'slice_ids': Parameter([int], "List of slices on this node"), + 'slice_ids_whitelist': Parameter([int], "List of slices allowed on this node"), + 'pcu_ids': Parameter([int], "List of PCUs that control this node"), + 'ports': Parameter([int], "List of PCU ports that this node is connected to"), + 'peer_id': Parameter(int, "Peer to which this node belongs", nullok = True), + 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True), + 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"), + 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"), + } + + +class UpdateNode(Method): + """ + Updates a node. Only the fields specified in node_fields are + updated, all other fields are left untouched. + + PIs and techs can update only the nodes at their sites. Only + admins can update the key, session, and boot_nonce fields. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(can_update,legacy_node_fields) + # xxx check the related_fields feature + accepted_fields.update(Node.related_fields) + accepted_fields.update(Node.tags) + + accepts = [ + Auth(), + Mixed(Node.fields['node_id'], + Node.fields['hostname']), + accepted_fields + ] + + returns = Parameter(int, '1 if successful') + + def call(self, auth, node_id_or_hostname, node_fields): + + # split provided fields + [native,related,tags,rejected] = Row.split_fields(node_fields,[legacy_node_fields,Node.related_fields,Node.tags]) + + # type checking + native = Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot update Node column(s) %r"%rejected + + # Authenticated function + assert self.caller is not None + + # Remove admin only fields + if 'admin' not in self.caller['roles']: + for key in admin_only: + if native.has_key(key): + del native[key] + + # Get account information + nodes = Nodes(self.api, [node_id_or_hostname]) + if not nodes: + raise PLCInvalidArgument, "No such node %r"%node_id_or_hostname + node = nodes[0] + + if node['peer_id'] is not None: + raise PLCInvalidArgument, "Not a local node %r"%node_id_or_hostname + + # If we are not an admin, make sure that the caller is a + # member of the site at which the node is located. + if 'admin' not in self.caller['roles']: + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to delete nodes from specified site" + + # Make requested associations + for (k,v) in related.iteritems(): + node.associate(auth, k,v) + + node.update(native) + node.update_last_updated(commit=False) + node.sync(commit=True) + + # if hostname was modifed make sure to update the hrn + # tag + if 'hostname' in native: + root_auth = self.api.config.PLC_HRN_ROOT + # sub auth is the login base of this node's site + sites = Sites(self.api, node['site_id'], ['login_base']) + site = sites[0] + login_base = site['login_base'] + tags['hrn'] = hostname_to_hrn(root_auth, login_base, node['hostname']) + + for (tagname,value) in tags.iteritems(): + # the tagtype instance is assumed to exist, just check that + tag_types = TagTypes(self.api,{'tagname':tagname}) + if not tag_types: + raise PLCInvalidArgument,"No such TagType %s"%tagname + tag_type = tag_types[0] + node_tags=NodeTags(self.api,{'tagname':tagname,'node_id':node['node_id']}) + if not node_tags: + node_tag = NodeTag(self.api) + node_tag['node_id'] = node['node_id'] + node_tag['tag_type_id'] = tag_type['tag_type_id'] + node_tag['tagname'] = tagname + node_tag['value'] = value + node_tag.sync() + else: + node_tag = node_tags[0] + node_tag['value'] = value + node_tag.sync() + # Logging variables + self.event_objects = {'Node': [node['node_id']]} + if 'hostname' in node: + self.message = 'Node %s updated'%node['hostname'] + else: + self.message = 'Node %d updated'%node['node_id'] + self.message += " [%s]." % (", ".join(node_fields.keys()),) + if 'boot_state' in node_fields.keys(): + self.message += ' boot_state updated to %s' % node_fields['boot_state'] + + return 1 diff --git a/PLC/Methods/Legacy/__init__.py b/PLC/Methods/Legacy/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/PLC/Methods/UpdateIpAddress.py b/PLC/Methods/UpdateIpAddress.py new file mode 100644 index 0000000..f07f1ff --- /dev/null +++ b/PLC/Methods/UpdateIpAddress.py @@ -0,0 +1,82 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row +from PLC.Auth import Auth + +from PLC.Nodes import Node, Nodes +from PLC.TagTypes import TagTypes +from PLC.InterfaceTags import InterfaceTags +from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import IpAddress, IpAddresses + +cannot_update = ['ip_address_id', 'interface_id'] + +class UpdateIpAddress(Method): + """ + PIs and techs may only update interfaces associated with their own + nodes. Admins may update any interface network. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, IpAddress.fields,exclude=True) + accepted_fields.update(IpAddress.tags) + + accepts = [ + Auth(), + IpAddress.fields['ip_address_id'], + accepted_fields + ] + + returns = Parameter(int, '1 if successful') + + def call(self, auth, ip_address_id, ip_address_fields): + + [native,tags,rejected] = Row.split_fields(ip_address_fields,[IpAddress.fields,IpAddress.tags]) + + # type checking + native= Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot update IpAddress column(s) %r"%rejected + + # Get ip_address information + ip_addresses = IpAddresses(self.api, [ip_address_id]) + if not ip_addresses: + raise PLCInvalidArgument, "No such ip address" + + ip_address = ip_addresses[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + # Get interface information + interface_id = ip_address["interface_id"] + interfaces = Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCPermissionDenied, "Ip address is not associated with an interface" + interface = interfaces[0] + + nodes = Nodes(self.api, [interface['node_id']]) + if not nodes: + raise PLCPermissionDenied, "Interface is not associated with a node" + node = nodes[0] + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to update address" + + ip_address.update(native) + ip_address.update_last_updated(commit=False) + ip_address.sync() + + self.event_objects = {'IpAddress': [ip_address['ip_address_id']]} + self.message = "Address %s updated"%ip_address['ip_addr'] + self.message += "[%s]." % ", ".join(ip_address_fields.keys()) + + return 1 diff --git a/PLC/Methods/UpdateNode.py b/PLC/Methods/UpdateNode.py index c09e10c..4f1f5c6 100644 --- a/PLC/Methods/UpdateNode.py +++ b/PLC/Methods/UpdateNode.py @@ -1,3 +1,5 @@ +# $Id$ +# $URL$ from PLC.Faults import * from PLC.Method import Method from PLC.Parameter import Parameter, Mixed @@ -13,7 +15,7 @@ from PLC.Methods.AddNodeTag import AddNodeTag from PLC.Methods.UpdateNodeTag import UpdateNodeTag admin_only = [ 'key', 'session', 'boot_nonce', 'site_id'] -can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version'] + admin_only +can_update = ['hostname', 'node_type', 'boot_state', 'model', 'version', 'dns', 'ntp'] + admin_only class UpdateNode(Method): """ diff --git a/PLC/Methods/UpdateRoute.py b/PLC/Methods/UpdateRoute.py new file mode 100644 index 0000000..4db4e31 --- /dev/null +++ b/PLC/Methods/UpdateRoute.py @@ -0,0 +1,75 @@ +# $Id$ +# $URL$ +from PLC.Faults import * +from PLC.Method import Method +from PLC.Parameter import Parameter, Mixed +from PLC.Table import Row +from PLC.Auth import Auth + +from PLC.Nodes import Node, Nodes +from PLC.TagTypes import TagTypes +from PLC.InterfaceTags import InterfaceTags +from PLC.Interfaces import Interface, Interfaces +from PLC.Routes import Route, Routes + +cannot_update = ['route_id', 'node_id'] + +class UpdateRoute(Method): + """ + PIs and techs may only update interfaces associated with their own + nodes. Admins may update any interface network. + + Returns 1 if successful, faults otherwise. + """ + + roles = ['admin', 'pi', 'tech'] + + accepted_fields = Row.accepted_fields(cannot_update, Route.fields,exclude=True) + accepted_fields.update(Route.tags) + + accepts = [ + Auth(), + Route.fields['route_id'], + accepted_fields + ] + + returns = Parameter(int, '1 if successful') + + def call(self, auth, route_id, route_fields): + + [native,tags,rejected] = Row.split_fields(route_fields,[Route.fields,Route.tags]) + + # type checking + native= Row.check_fields (native, self.accepted_fields) + if rejected: + raise PLCInvalidArgument, "Cannot update Route column(s) %r"%rejected + + # Get route information + routes = Routes(self.api, [route_id]) + if not routes: + raise PLCInvalidArgument, "No such route" + + route = routes[0] + + # Authenticated function + assert self.caller is not None + + # If we are not an admin, make sure that the caller is a + # member of the site where the node exists. + if 'admin' not in self.caller['roles']: + nodes = Nodes(self.api, [route['node_id']]) + if not nodes: + raise PLCPermissionDenied, "Route is not associated with a valid node: %r" % route['node_id'] + node = nodes[0] + if node['site_id'] not in self.caller['site_ids']: + raise PLCPermissionDenied, "Not allowed to update route" + + route.update(native) + route.update_last_updated(commit=False) + route.sync() + + self.event_objects = {'Route': [route['route_id']]} + self.message = "Route %r updated"%route['route_id'] + self.message += "[%s]." % ", ".join(route_fields.keys()) + + return 1 diff --git a/PLC/Nodes.py b/PLC/Nodes.py index 87789ea..537c080 100644 --- a/PLC/Nodes.py +++ b/PLC/Nodes.py @@ -16,6 +16,7 @@ from PLC.Table import Row, Table from PLC.NodeTypes import NodeTypes from PLC.BootStates import BootStates from PLC.Interfaces import Interface, Interfaces +from PLC.IpAddresses import SimpleAddress def valid_hostname(hostname): # 1. Each part begins and ends with a letter or number. @@ -39,7 +40,7 @@ class Node(Row): primary_key = 'node_id' join_tables = [ 'slice_node', 'peer_node', 'slice_tag', 'node_session', 'node_slice_whitelist', - 'node_tag', 'conf_file_node', 'pcu_node', 'leases', ] + 'node_tag', 'conf_file_node', 'pcu_node', 'leases', 'routes'] fields = { 'node_id': Parameter(int, "Node identifier"), 'node_type': Parameter(str,"Node type",max=20), @@ -61,6 +62,8 @@ class Node(Row): 'last_time_spent_online': Parameter(int, "Length of time the node was last online before shutdown/failure", ro = True), 'last_time_spent_offline': Parameter(int, "Length of time the node was last offline after failure and before reboot", ro = True), 'verified': Parameter(bool, "Whether the node configuration is verified correct", ro=False), + 'dns': Parameter(str, "Comma-separated list of dns servers", max=255, nullok=True), + 'ntp': Parameter(str, "Comma-separated list of ntp servers", max=255, nullok=True), 'key': Parameter(str, "(Admin only) Node key", max = 256), 'session': Parameter(str, "(Admin only) Node session value", max = 256, ro = True), 'interface_ids': Parameter([int], "List of network interfaces that this node has"), @@ -74,6 +77,7 @@ class Node(Row): 'peer_node_id': Parameter(int, "Foreign node identifier at peer", nullok = True), 'node_tag_ids' : Parameter ([int], "List of tags attached to this node"), 'nodegroup_ids': Parameter([int], "List of node groups that this node is in"), + 'route_ids': Parameter([int], "List of routes attached to this node"), } related_fields = { 'interfaces': [Mixed(Parameter(int, "Interface identifier"), @@ -122,6 +126,16 @@ class Node(Row): validate_last_pcu_reboot = Row.validate_timestamp validate_last_pcu_confirmation = Row.validate_timestamp + def validate_dns(self, dns): + addrs = dns.split(",") + addrs = [x.strip() for x in addrs] + for addr in addrs: + SimpleAddress(addr) + + return dns + + validate_ntp = validate_dns + def update_readonly_int(self, col_name, commit = True): assert 'node_id' in self diff --git a/PLC/Routes.py b/PLC/Routes.py new file mode 100644 index 0000000..20fd69c --- /dev/null +++ b/PLC/Routes.py @@ -0,0 +1,127 @@ +# +# Functions for interacting with the routes table in the database +# + +from types import StringTypes +import socket +import struct + +from PLC.Faults import * +from PLC.Parameter import Parameter +from PLC.Filter import Filter +from PLC.Debug import profile +from PLC.Table import Row, Table +from PLC.NetworkTypes import NetworkType, NetworkTypes +from PLC.NetworkMethods import NetworkMethod, NetworkMethods +from PLC.IpAddresses import SimpleAddress, Subnet +import PLC.Nodes + +class Route(Row): + """ + Representation of a row in the routees table. To use, optionally + instantiate with a dict of values. Update as you would a + dict. Commit to the database with sync(). + """ + + table_name = 'routes' + primary_key = 'route_id' + join_tables = [] + fields = { + 'route_id': Parameter(int, "Route identifier"), + 'node_id': Parameter(int, "Node this route belongs to"), + 'subnet': Parameter(str, "Subnet for this route", nullok = True), + 'next_hop': Parameter(str, "IP address to send outgoing traffic"), + 'interface_id': Parameter(int, "Interface to send outgoing traffic"), + 'last_updated': Parameter(int, "Date and time when node entry was created", ro = True), + } + + tags = {} + + # TODO - validate next_hop and subnet + + def validate_node_id(self, node_id): + nodes = PLC.Nodes.Nodes(self.api, [node_id]) + if not nodes: + raise PLCInvalidArgument, "No such node %d"%node_id + + return node_id + + def validate_interface_id(self, interface_id): + interfaces = PLC.Interfaces.Interfaces(self.api, [interface_id]) + if not interfaces: + raise PLCInvalidArgument, "No such interface %d"%interface_id + + if (interfaces[0]["node_id"] != self["node_id"]): + raise PLCInvalidArgument, "Interface is not for the same node as this route" + + return interface_id + + def validate_subnet(self, subnet): + Subnet(subnet) + return subnet + + def validate_next_hop(self, next_hop): + SimpleAddress(next_hop) + return next_hop + + def validate(self): + """ + Flush changes back to the database. + """ + + # Basic validation + Row.validate(self) + + validate_last_updated = Row.validate_timestamp + + def update_timestamp(self, col_name, commit = True): + """ + Update col_name field with current time + """ + + assert 'route_id' in self + assert self.table_name + + self.api.db.do("UPDATE %s SET %s = CURRENT_TIMESTAMP " % (self.table_name, col_name) + \ + " where route_id = %d" % (self['route_id']) ) + self.sync(commit) + + def update_last_updated(self, commit = True): + self.update_timestamp('last_updated', commit) + +class Routes(Table): + """ + Representation of row(s) from the routees table in the + database. + """ + + def __init__(self, api, route_filter = None, columns = None): + Table.__init__(self, api, Route, columns) + + # the view that we're selecting upon: start with view_nodes + view = "view_routes" + # as many left joins as requested tags + for tagname in self.tag_columns: + view= "%s left join %s using (%s)"%(view,Route.tagvalue_view_name(tagname), + Route.primary_key) + + sql = "SELECT %s FROM %s WHERE True" % \ + (", ".join(self.columns.keys()+self.tag_columns.keys()),view) + + if route_filter is not None: + if isinstance(route_filter, (list, tuple, set)): + # Separate the list into integers and strings + ints = filter(lambda x: isinstance(x, (int, long)), route_filter) + route_filter = Filter(Route.fields, {'route_id': ints}) + sql += " AND (%s) %s" % route_filter.sql(api, "OR") + elif isinstance(route_filter, dict): + allowed_fields=dict(Route.fields.items()+Route.tags.items()) + route_filter = Filter(allowed_fields, route_filter) + sql += " AND (%s) %s" % route_filter.sql(api) + elif isinstance(route_filter, int): + route_filter = Filter(Route.fields, {'route_id': [route_filter]}) + sql += " AND (%s) %s" % route_filter.sql(api) + else: + raise PLCInvalidArgument, "Wrong route filter %r"%route_filter + + self.selectall(sql) diff --git a/setup.py b/setup.py index d29222f..0133427 100755 --- a/setup.py +++ b/setup.py @@ -5,12 +5,15 @@ # Mark Huang # Copyright (C) 2006 The Trustees of Princeton University # +# $Id$ +# $URL$ +# from distutils.core import setup from glob import glob setup(py_modules = ['ModPython'], - packages = ['PLC', 'PLC/Methods', 'PLC/Methods/system', 'PLC/Accessors', 'aspects'], + packages = ['PLC', 'PLC/Methods', 'PLC/Methods/system', 'PLC/Methods/Legacy', 'PLC/Accessors', 'aspects'], scripts = ['plcsh', 'Server.py'], data_files = [ ('', ['planetlab5.sql']),