| # |
| # |
| |
| # Copyright (C) 2010, 2011, 2012 Google Inc. |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions are |
| # met: |
| # |
| # 1. Redistributions of source code must retain the above copyright notice, |
| # this list of conditions and the following disclaimer. |
| # |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS |
| # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED |
| # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR |
| # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, |
| # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR |
| # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF |
| # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING |
| # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS |
| # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| |
| |
| """Ganeti network utility module. |
| |
| This module holds functions that can be used in both daemons (all) and |
| the command line scripts. |
| |
| """ |
| |
| |
| import errno |
| import os |
| import re |
| import socket |
| import struct |
| import IN |
| import logging |
| |
| from ganeti import constants |
| from ganeti import errors |
| from ganeti import utils |
| from ganeti import vcluster |
| |
| # Structure definition for getsockopt(SOL_SOCKET, SO_PEERCRED, ...): |
| # struct ucred { pid_t pid; uid_t uid; gid_t gid; }; |
| # |
| # The GNU C Library defines gid_t and uid_t to be "unsigned int" and |
| # pid_t to "int". |
| # |
| # IEEE Std 1003.1-2008: |
| # "nlink_t, uid_t, gid_t, and id_t shall be integer types" |
| # "blksize_t, pid_t, and ssize_t shall be signed integer types" |
| _STRUCT_UCRED = "iII" |
| _STRUCT_UCRED_SIZE = struct.calcsize(_STRUCT_UCRED) |
| |
| # Workaround a bug in some linux distributions that don't define SO_PEERCRED |
| try: |
| # pylint: disable=E1101 |
| _SO_PEERCRED = IN.SO_PEERCRED |
| except AttributeError: |
| _SO_PEERCRED = 17 |
| |
| # Regexes used to find IP addresses in the output of ip. |
| _IP_RE_TEXT = r"[.:a-z0-9]+" # separate for testing purposes |
| _IP_FAMILY_RE = re.compile(r"(?P<family>inet6?)\s+(?P<ip>%s)/" % _IP_RE_TEXT, |
| re.IGNORECASE) |
| |
| # Dict used to convert from a string representing an IP family to an IP |
| # version |
| _NAME_TO_IP_VER = { |
| "inet": constants.IP4_VERSION, |
| "inet6": constants.IP6_VERSION, |
| } |
| |
| |
| def _GetIpAddressesFromIpOutput(ip_output): |
| """Parses the output of the ip command and retrieves the IP addresses and |
| version. |
| |
| @param ip_output: string containing the output of the ip command; |
| @rtype: dict; (int, list) |
| @return: a dict having as keys the IP versions and as values the |
| corresponding list of addresses found in the IP output. |
| |
| """ |
| addr = dict((i, []) for i in _NAME_TO_IP_VER.values()) |
| |
| for row in ip_output.splitlines(): |
| match = _IP_FAMILY_RE.search(row) |
| if match and IPAddress.IsValid(match.group("ip")): |
| addr[_NAME_TO_IP_VER[match.group("family")]].append(match.group("ip")) |
| |
| return addr |
| |
| |
| def GetSocketCredentials(sock): |
| """Returns the credentials of the foreign process connected to a socket. |
| |
| @param sock: Unix socket |
| @rtype: tuple; (number, number, number) |
| @return: The PID, UID and GID of the connected foreign process. |
| |
| """ |
| peercred = sock.getsockopt(socket.SOL_SOCKET, _SO_PEERCRED, |
| _STRUCT_UCRED_SIZE) |
| return struct.unpack(_STRUCT_UCRED, peercred) |
| |
| |
| def IsValidInterface(ifname): |
| """Validate an interface name. |
| |
| @type ifname: string |
| @param ifname: Name of the network interface |
| @return: boolean indicating whether the interface name is valid or not. |
| |
| """ |
| return os.path.exists(utils.PathJoin("/sys/class/net", ifname)) |
| |
| |
| def GetInterfaceIpAddresses(ifname): |
| """Returns the IP addresses associated to the interface. |
| |
| @type ifname: string |
| @param ifname: Name of the network interface |
| @return: A dict having for keys the IP version (either |
| L{constants.IP4_VERSION} or L{constants.IP6_VERSION}) and for |
| values the lists of IP addresses of the respective version |
| associated to the interface |
| |
| """ |
| result = utils.RunCmd([constants.IP_COMMAND_PATH, "-o", "addr", "show", |
| ifname]) |
| |
| if result.failed: |
| logging.error("Error running the ip command while getting the IP" |
| " addresses of %s", ifname) |
| return None |
| |
| return _GetIpAddressesFromIpOutput(result.output) |
| |
| |
| def GetHostname(name=None, family=None): |
| """Returns a Hostname object. |
| |
| @type name: str |
| @param name: hostname or None |
| @type family: int |
| @param family: AF_INET | AF_INET6 | None |
| @rtype: L{Hostname} |
| @return: Hostname object |
| @raise errors.OpPrereqError: in case of errors in resolving |
| |
| """ |
| try: |
| return Hostname(name=name, family=family) |
| except errors.ResolverError, err: |
| raise errors.OpPrereqError("The given name (%s) does not resolve: %s" % |
| (err[0], err[2]), errors.ECODE_RESOLVER) |
| |
| |
| class Hostname(object): |
| """Class implementing resolver and hostname functionality. |
| |
| """ |
| _VALID_NAME_RE = re.compile("^[a-z0-9._-]{1,255}$") |
| |
| def __init__(self, name=None, family=None): |
| """Initialize the host name object. |
| |
| If the name argument is None, it will use this system's name. |
| |
| @type family: int |
| @param family: AF_INET | AF_INET6 | None |
| @type name: str |
| @param name: hostname or None |
| |
| """ |
| self.name = self.GetFqdn(name) |
| self.ip = self.GetIP(self.name, family=family) |
| |
| @classmethod |
| def GetSysName(cls): |
| """Legacy method the get the current system's name. |
| |
| """ |
| return cls.GetFqdn() |
| |
| @classmethod |
| def GetFqdn(cls, hostname=None): |
| """Return fqdn. |
| |
| If hostname is None the system's fqdn is returned. |
| |
| @type hostname: str |
| @param hostname: name to be fqdn'ed |
| @rtype: str |
| @return: fqdn of given name, if it exists, unmodified name otherwise |
| |
| """ |
| if hostname is None: |
| virtfqdn = vcluster.GetVirtualHostname() |
| if virtfqdn: |
| result = virtfqdn |
| else: |
| result = socket.getfqdn() |
| else: |
| result = socket.getfqdn(hostname) |
| |
| return cls.GetNormalizedName(result) |
| |
| @staticmethod |
| def GetIP(hostname, family=None): |
| """Return IP address of given hostname. |
| |
| Supports both IPv4 and IPv6. |
| |
| @type hostname: str |
| @param hostname: hostname to look up |
| @type family: int |
| @param family: AF_INET | AF_INET6 | None |
| @rtype: str |
| @return: IP address |
| @raise errors.ResolverError: in case of errors in resolving |
| |
| """ |
| try: |
| if family in (socket.AF_INET, socket.AF_INET6): |
| result = socket.getaddrinfo(hostname, None, family) |
| else: |
| result = socket.getaddrinfo(hostname, None) |
| except (socket.gaierror, socket.herror, socket.error), err: |
| # hostname not found in DNS, or other socket exception in the |
| # (code, description format) |
| raise errors.ResolverError(hostname, err.args[0], err.args[1]) |
| |
| # getaddrinfo() returns a list of 5-tupes (family, socktype, proto, |
| # canonname, sockaddr). We return the first tuple's first address in |
| # sockaddr |
| try: |
| return result[0][4][0] |
| except IndexError, err: |
| # we don't have here an actual error code, it's just that the |
| # data type returned by getaddrinfo is not what we expected; |
| # let's keep the same format in the exception arguments with a |
| # dummy error code |
| raise errors.ResolverError(hostname, 0, |
| "Unknown error in getaddrinfo(): %s" % err) |
| |
| @classmethod |
| def GetNormalizedName(cls, hostname): |
| """Validate and normalize the given hostname. |
| |
| @attention: the validation is a bit more relaxed than the standards |
| require; most importantly, we allow underscores in names |
| @raise errors.OpPrereqError: when the name is not valid |
| |
| """ |
| hostname = hostname.lower() |
| if (not cls._VALID_NAME_RE.match(hostname) or |
| # double-dots, meaning empty label |
| ".." in hostname or |
| # empty initial label |
| hostname.startswith(".")): |
| raise errors.OpPrereqError("Invalid hostname '%s'" % hostname, |
| errors.ECODE_INVAL) |
| if hostname.endswith("."): |
| hostname = hostname.rstrip(".") |
| return hostname |
| |
| |
| def ValidatePortNumber(port): |
| """Returns the validated integer port number if it is valid. |
| |
| @param port: the port number to be validated |
| |
| @raise ValueError: if the port is not valid |
| @rtype: int |
| @return: the validated value. |
| |
| """ |
| |
| try: |
| port = int(port) |
| except TypeError: |
| raise errors.ProgrammerError("ValidatePortNumber called with non-numeric" |
| " type %s." % port.__class__.__name__) |
| except ValueError: |
| raise ValueError("Invalid port value: '%s'" % port) |
| |
| if not 0 < port < 2 ** 16: |
| raise ValueError("Invalid port value: '%d'" % port) |
| |
| return port |
| |
| |
| def TcpPing(target, port, timeout=10, live_port_needed=False, source=None): |
| """Simple ping implementation using TCP connect(2). |
| |
| Check if the given IP is reachable by doing attempting a TCP connect |
| to it. |
| |
| @type target: str |
| @param target: the IP to ping |
| @type port: int |
| @param port: the port to connect to |
| @type timeout: int |
| @param timeout: the timeout on the connection attempt |
| @type live_port_needed: boolean |
| @param live_port_needed: whether a closed port will cause the |
| function to return failure, as if there was a timeout |
| @type source: str or None |
| @param source: if specified, will cause the connect to be made |
| from this specific source address; failures to bind other |
| than C{EADDRNOTAVAIL} will be ignored |
| |
| """ |
| logging.debug("Attempting to reach TCP port %s on target %s with a timeout" |
| " of %s seconds", port, target, timeout) |
| |
| try: |
| family = IPAddress.GetAddressFamily(target) |
| except errors.IPAddressError, err: |
| raise errors.ProgrammerError("Family of IP address given in parameter" |
| " 'target' can't be determined: %s" % err) |
| |
| sock = socket.socket(family, socket.SOCK_STREAM) |
| success = False |
| |
| if source is not None: |
| try: |
| sock.bind((source, 0)) |
| except socket.error, err: |
| if err[0] == errno.EADDRNOTAVAIL: |
| success = False |
| |
| sock.settimeout(timeout) |
| |
| try: |
| sock.connect((target, port)) |
| sock.close() |
| success = True |
| except socket.timeout: |
| success = False |
| except socket.error, err: |
| success = (not live_port_needed) and (err[0] == errno.ECONNREFUSED) |
| |
| return success |
| |
| |
| def GetDaemonPort(daemon_name): |
| """Get the daemon port for this cluster. |
| |
| Note that this routine does not read a ganeti-specific file, but |
| instead uses C{socket.getservbyname} to allow pre-customization of |
| this parameter outside of Ganeti. |
| |
| @type daemon_name: string |
| @param daemon_name: daemon name (in constants.DAEMONS_PORTS) |
| @rtype: int |
| |
| """ |
| if daemon_name not in constants.DAEMONS_PORTS: |
| raise errors.ProgrammerError("Unknown daemon: %s" % daemon_name) |
| |
| (proto, default_port) = constants.DAEMONS_PORTS[daemon_name] |
| try: |
| port = socket.getservbyname(daemon_name, proto) |
| except socket.error: |
| port = default_port |
| |
| return port |
| |
| |
| class IPAddress(object): |
| """Class that represents an IP address. |
| |
| """ |
| iplen = 0 |
| family = None |
| loopback_cidr = None |
| |
| @staticmethod |
| def _GetIPIntFromString(address): |
| """Abstract method to please pylint. |
| |
| """ |
| raise NotImplementedError |
| |
| @classmethod |
| def IsValid(cls, address): |
| """Validate a IP address. |
| |
| @type address: str |
| @param address: IP address to be checked |
| @rtype: bool |
| @return: True if valid, False otherwise |
| |
| """ |
| if cls.family is None: |
| try: |
| family = cls.GetAddressFamily(address) |
| except errors.IPAddressError: |
| return False |
| else: |
| family = cls.family |
| |
| try: |
| socket.inet_pton(family, address) |
| return True |
| except socket.error: |
| return False |
| |
| @classmethod |
| def ValidateNetmask(cls, netmask): |
| """Validate a netmask suffix in CIDR notation. |
| |
| @type netmask: int |
| @param netmask: netmask suffix to validate |
| @rtype: bool |
| @return: True if valid, False otherwise |
| |
| """ |
| assert (isinstance(netmask, (int, long))) |
| |
| return 0 < netmask <= cls.iplen |
| |
| @classmethod |
| def Own(cls, address): |
| """Check if the current host has the the given IP address. |
| |
| This is done by trying to bind the given address. We return True if we |
| succeed or false if a socket.error is raised. |
| |
| @type address: str |
| @param address: IP address to be checked |
| @rtype: bool |
| @return: True if we own the address, False otherwise |
| |
| """ |
| if cls.family is None: |
| try: |
| family = cls.GetAddressFamily(address) |
| except errors.IPAddressError: |
| return False |
| else: |
| family = cls.family |
| |
| s = socket.socket(family, socket.SOCK_DGRAM) |
| success = False |
| try: |
| try: |
| s.bind((address, 0)) |
| success = True |
| except socket.error: |
| success = False |
| finally: |
| s.close() |
| return success |
| |
| @classmethod |
| def InNetwork(cls, cidr, address): |
| """Determine whether an address is within a network. |
| |
| @type cidr: string |
| @param cidr: Network in CIDR notation, e.g. '192.0.2.0/24', '2001:db8::/64' |
| @type address: str |
| @param address: IP address |
| @rtype: bool |
| @return: True if address is in cidr, False otherwise |
| |
| """ |
| address_int = cls._GetIPIntFromString(address) |
| subnet = cidr.split("/") |
| assert len(subnet) == 2 |
| try: |
| prefix = int(subnet[1]) |
| except ValueError: |
| return False |
| |
| assert 0 <= prefix <= cls.iplen |
| target_int = cls._GetIPIntFromString(subnet[0]) |
| # Convert prefix netmask to integer value of netmask |
| netmask_int = (2 ** cls.iplen) - 1 ^ ((2 ** cls.iplen) - 1 >> prefix) |
| # Calculate hostmask |
| hostmask_int = netmask_int ^ (2 ** cls.iplen) - 1 |
| # Calculate network address by and'ing netmask |
| network_int = target_int & netmask_int |
| # Calculate broadcast address by or'ing hostmask |
| broadcast_int = target_int | hostmask_int |
| |
| return network_int <= address_int <= broadcast_int |
| |
| @staticmethod |
| def GetAddressFamily(address): |
| """Get the address family of the given address. |
| |
| @type address: str |
| @param address: ip address whose family will be returned |
| @rtype: int |
| @return: C{socket.AF_INET} or C{socket.AF_INET6} |
| @raise errors.GenericError: for invalid addresses |
| |
| """ |
| try: |
| return IP4Address(address).family |
| except errors.IPAddressError: |
| pass |
| |
| try: |
| return IP6Address(address).family |
| except errors.IPAddressError: |
| pass |
| |
| raise errors.IPAddressError("Invalid address '%s'" % address) |
| |
| @staticmethod |
| def GetVersionFromAddressFamily(family): |
| """Convert an IP address family to the corresponding IP version. |
| |
| @type family: int |
| @param family: IP address family, one of socket.AF_INET or socket.AF_INET6 |
| @return: an int containing the IP version, one of L{constants.IP4_VERSION} |
| or L{constants.IP6_VERSION} |
| @raise errors.ProgrammerError: for unknown families |
| |
| """ |
| if family == socket.AF_INET: |
| return constants.IP4_VERSION |
| elif family == socket.AF_INET6: |
| return constants.IP6_VERSION |
| |
| raise errors.ProgrammerError("%s is not a valid IP address family" % family) |
| |
| @staticmethod |
| def GetAddressFamilyFromVersion(version): |
| """Convert an IP version to the corresponding IP address family. |
| |
| @type version: int |
| @param version: IP version, one of L{constants.IP4_VERSION} or |
| L{constants.IP6_VERSION} |
| @return: an int containing the IP address family, one of C{socket.AF_INET} |
| or C{socket.AF_INET6} |
| @raise errors.ProgrammerError: for unknown IP versions |
| |
| """ |
| if version == constants.IP4_VERSION: |
| return socket.AF_INET |
| elif version == constants.IP6_VERSION: |
| return socket.AF_INET6 |
| |
| raise errors.ProgrammerError("%s is not a valid IP version" % version) |
| |
| @staticmethod |
| def GetClassFromIpVersion(version): |
| """Return the IPAddress subclass for the given IP version. |
| |
| @type version: int |
| @param version: IP version, one of L{constants.IP4_VERSION} or |
| L{constants.IP6_VERSION} |
| @return: a subclass of L{netutils.IPAddress} |
| @raise errors.ProgrammerError: for unknowo IP versions |
| |
| """ |
| if version == constants.IP4_VERSION: |
| return IP4Address |
| elif version == constants.IP6_VERSION: |
| return IP6Address |
| |
| raise errors.ProgrammerError("%s is not a valid IP version" % version) |
| |
| @staticmethod |
| def GetClassFromIpFamily(family): |
| """Return the IPAddress subclass for the given IP family. |
| |
| @param family: IP family (one of C{socket.AF_INET} or C{socket.AF_INET6} |
| @return: a subclass of L{netutils.IPAddress} |
| @raise errors.ProgrammerError: for unknowo IP versions |
| |
| """ |
| return IPAddress.GetClassFromIpVersion( |
| IPAddress.GetVersionFromAddressFamily(family)) |
| |
| @classmethod |
| def IsLoopback(cls, address): |
| """Determine whether it is a loopback address. |
| |
| @type address: str |
| @param address: IP address to be checked |
| @rtype: bool |
| @return: True if loopback, False otherwise |
| |
| """ |
| try: |
| return cls.InNetwork(cls.loopback_cidr, address) |
| except errors.IPAddressError: |
| return False |
| |
| |
| class IP4Address(IPAddress): |
| """IPv4 address class. |
| |
| """ |
| iplen = 32 |
| family = socket.AF_INET |
| loopback_cidr = "127.0.0.0/8" |
| |
| def __init__(self, address): |
| """Constructor for IPv4 address. |
| |
| @type address: str |
| @param address: IP address |
| @raises errors.IPAddressError: if address invalid |
| |
| """ |
| IPAddress.__init__(self) |
| if not self.IsValid(address): |
| raise errors.IPAddressError("IPv4 Address %s invalid" % address) |
| |
| self.address = address |
| |
| @staticmethod |
| def _GetIPIntFromString(address): |
| """Get integer value of IPv4 address. |
| |
| @type address: str |
| @param address: IPv6 address |
| @rtype: int |
| @return: integer value of given IP address |
| |
| """ |
| address_int = 0 |
| parts = address.split(".") |
| assert len(parts) == 4 |
| for part in parts: |
| address_int = (address_int << 8) | int(part) |
| |
| return address_int |
| |
| |
| class IP6Address(IPAddress): |
| """IPv6 address class. |
| |
| """ |
| iplen = 128 |
| family = socket.AF_INET6 |
| loopback_cidr = "::1/128" |
| |
| def __init__(self, address): |
| """Constructor for IPv6 address. |
| |
| @type address: str |
| @param address: IP address |
| @raises errors.IPAddressError: if address invalid |
| |
| """ |
| IPAddress.__init__(self) |
| if not self.IsValid(address): |
| raise errors.IPAddressError("IPv6 Address [%s] invalid" % address) |
| self.address = address |
| |
| @staticmethod |
| def _GetIPIntFromString(address): |
| """Get integer value of IPv6 address. |
| |
| @type address: str |
| @param address: IPv6 address |
| @rtype: int |
| @return: integer value of given IP address |
| |
| """ |
| doublecolons = address.count("::") |
| assert not doublecolons > 1 |
| if doublecolons == 1: |
| # We have a shorthand address, expand it |
| parts = [] |
| twoparts = address.split("::") |
| sep = len(twoparts[0].split(":")) + len(twoparts[1].split(":")) |
| parts = twoparts[0].split(":") |
| parts.extend(["0"] * (8 - sep)) |
| parts += twoparts[1].split(":") |
| else: |
| parts = address.split(":") |
| |
| address_int = 0 |
| for part in parts: |
| address_int = (address_int << 16) + int(part or "0", 16) |
| |
| return address_int |
| |
| |
| def FormatAddress(address, family=None): |
| """Format a socket address |
| |
| @type address: family specific (usually tuple) |
| @param address: address, as reported by this class |
| @type family: integer |
| @param family: socket family (one of socket.AF_*) or None |
| |
| """ |
| if family is None: |
| try: |
| family = IPAddress.GetAddressFamily(address[0]) |
| except errors.IPAddressError: |
| raise errors.ParameterError(address) |
| |
| if family == socket.AF_UNIX and len(address) == 3: |
| return "pid=%s, uid=%s, gid=%s" % address |
| |
| if family in (socket.AF_INET, socket.AF_INET6) and len(address) == 2: |
| host, port = address |
| if family == socket.AF_INET6: |
| res = "[%s]" % host |
| else: |
| res = host |
| |
| if port is not None: |
| res += ":%s" % port |
| |
| return res |
| |
| raise errors.ParameterError(family, address) |