bring over newinterface branch from Verivue
authorsmbaker <smbaker@fc8clean.lan>
Tue, 10 Apr 2012 04:43:41 +0000 (21:43 -0700)
committersmbaker <smbaker@fc8clean.lan>
Tue, 10 Apr 2012 04:43:41 +0000 (21:43 -0700)
27 files changed:
PLC/Accessors/Factory.py
PLC/InterfaceTags.py
PLC/Interfaces.py
PLC/IpAddresses.py [new file with mode: 0644]
PLC/Methods/AddIpAddress.py [new file with mode: 0644]
PLC/Methods/AddNode.py
PLC/Methods/AddRoute.py [new file with mode: 0644]
PLC/Methods/DeleteIpAddress.py [new file with mode: 0644]
PLC/Methods/DeleteRoute.py [new file with mode: 0644]
PLC/Methods/GetInterfaces.py
PLC/Methods/GetIpAddresses.py [new file with mode: 0644]
PLC/Methods/GetRoutes.py [new file with mode: 0644]
PLC/Methods/Legacy/AddInterface.py [new file with mode: 0644]
PLC/Methods/Legacy/AddNode.py [new file with mode: 0644]
PLC/Methods/Legacy/DeleteInterface.py [new file with mode: 0644]
PLC/Methods/Legacy/DeleteNode.py [new file with mode: 0644]
PLC/Methods/Legacy/GetInterfaces.py [new file with mode: 0644]
PLC/Methods/Legacy/GetNodes.py [new file with mode: 0644]
PLC/Methods/Legacy/UpdateInterface.py [new file with mode: 0644]
PLC/Methods/Legacy/UpdateNode.py [new file with mode: 0644]
PLC/Methods/Legacy/__init__.py [new file with mode: 0644]
PLC/Methods/UpdateIpAddress.py [new file with mode: 0644]
PLC/Methods/UpdateNode.py
PLC/Methods/UpdateRoute.py [new file with mode: 0644]
PLC/Nodes.py
PLC/Routes.py [new file with mode: 0644]
setup.py

index fec0d63..62a4748 100644 (file)
@@ -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'},
index af1deb4..76bee90 100644 (file)
@@ -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'],
index f4ddcc8..42e35e5 100644 (file)
@@ -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 (file)
index 0000000..6be9b7d
--- /dev/null
@@ -0,0 +1,232 @@
+#
+# Functions for interacting with the interfaces table in the database
+#
+# Mark Huang <mlhuang@cs.princeton.edu>
+# 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 (file)
index 0000000..9847c3c
--- /dev/null
@@ -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']
index 0ee13f5..a33cebd 100644 (file)
@@ -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 (file)
index 0000000..4829028
--- /dev/null
@@ -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 (file)
index 0000000..06efce5
--- /dev/null
@@ -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 (file)
index 0000000..9f3fff4
--- /dev/null
@@ -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
index a154c8e..239aba9 100644 (file)
@@ -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 (file)
index 0000000..90b88f3
--- /dev/null
@@ -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 (file)
index 0000000..a003773
--- /dev/null
@@ -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 (file)
index 0000000..5d2774d
--- /dev/null
@@ -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 (file)
index 0000000..0fc7e5a
--- /dev/null
@@ -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 (file)
index 0000000..82d0f7e
--- /dev/null
@@ -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 (file)
index 0000000..01e382a
--- /dev/null
@@ -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 (file)
index 0000000..d5938e9
--- /dev/null
@@ -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 (file)
index 0000000..84fddb9
--- /dev/null
@@ -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 (file)
index 0000000..d2119c9
--- /dev/null
@@ -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 (file)
index 0000000..a0e8ee4
--- /dev/null
@@ -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 (file)
index 0000000..e69de29
diff --git a/PLC/Methods/UpdateIpAddress.py b/PLC/Methods/UpdateIpAddress.py
new file mode 100644 (file)
index 0000000..f07f1ff
--- /dev/null
@@ -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
index c09e10c..4f1f5c6 100644 (file)
@@ -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 (file)
index 0000000..4db4e31
--- /dev/null
@@ -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
index 87789ea..537c080 100644 (file)
@@ -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 (file)
index 0000000..20fd69c
--- /dev/null
@@ -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)
index d29222f..0133427 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -5,12 +5,15 @@
 # Mark Huang <mlhuang@cs.princeton.edu>
 # 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']),