Ejemplo n.º 1
0
    def _get_endpoint(self):
        """Get endpoint matching self.container_id.

        Return None if no endpoint is found.
        Exits with an error if multiple endpoints are found.

        :return: Endpoint object if found, None if not found
        """
        try:
            _log.debug("Looking for endpoint that matches container ID %s",
                       self.container_id)
            endpoint = self._client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=ORCHESTRATOR_ID,
                workload_id=self.container_id
            )
        except KeyError:
            _log.debug("No endpoint found matching ID %s", self.container_id)
            endpoint = None
        except MultipleEndpointsMatch:
            message = "Multiple Endpoints found matching ID %s" % \
                    self.container_id
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        return endpoint
Ejemplo n.º 2
0
    def _provision_veth(self, endpoint):
        """Provisions veth for given endpoint.

        Uses the netns relative path passed in through CNI_NETNS_ENV and
        interface passed in through CNI_IFNAME_ENV.

        :param endpoint
        :return Calico endpoint object
        """
        _log.debug("Provisioning Calico veth interface")
        netns_path = os.path.abspath(os.path.join(os.getcwd(), self.cni_netns))
        _log.debug("netns path: %s", netns_path)

        try:
            endpoint.mac = endpoint.provision_veth(Namespace(netns_path),
                                                   self.interface)
        except CalledProcessError as e:
            _log.exception(
                "Failed to provision veth interface for endpoint %s",
                endpoint.name)
            self._remove_workload()
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        _log.debug("Endpoint has mac address: %s", endpoint.mac)

        self._client.set_endpoint(endpoint)
        _log.info("Provisioned %s in netns %s", self.interface, netns_path)
        return endpoint
Ejemplo n.º 3
0
    def _call_binary_ipam_plugin(self, env):
        """Calls through to the specified IPAM plugin binary.
    
        Utilizes the IPAM config as specified in the CNI network
        configuration file.  A dictionary with the following form:
            {
              type: <IPAM TYPE>
            }

        :param env - A dictionary of environment variables to pass to the
        IPAM plugin
        :return: Tuple of return code, response from the IPAM plugin.
        """
        # Find the correct plugin based on the given type.
        plugin_path = self._find_ipam_plugin()
        if not plugin_path:
            message = "Could not find IPAM plugin of type %s in path %s." % \
                      (self.ipam_type, self.cni_path)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)
    
        # Execute the plugin and return the result.
        _log.info("Using IPAM plugin at: %s", plugin_path)
        _log.debug("Passing in environment to IPAM plugin: \n%s",
                   json.dumps(env, indent=2))
        p = Popen(plugin_path, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
        stdout, stderr = p.communicate(json.dumps(self.network_config))
        _log.debug("IPAM plugin return code: %s", p.returncode)
        _log.debug("IPAM plugin output: \nstdout:\n%s\nstderr:\n%s", 
                   stdout, stderr)
        return p.returncode, stdout
Ejemplo n.º 4
0
    def _create_endpoint(self, ip_list):
        """Creates an endpoint in the Calico datastore with the client.

        :param ip_list - list of IP addresses that have been already allocated
        :return Calico endpoint object
        """
        _log.debug("Creating Calico endpoint with workload_id=%s",
                   self.workload_id)
        try:
            endpoint = self._client.create_endpoint(HOSTNAME,
                                                    self.orchestrator_id,
                                                    self.workload_id,
                                                    ip_list)
        except (AddrFormatError, KeyError) as e:
            # AddrFormatError: Raised when an IP address type is not
            #                  compatible with the node.
            # KeyError: Raised when BGP config for host is not found.
            _log.exception("Failed to create Calico endpoint.")
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        _log.info("Created Calico endpoint with IP address(es) %s", ip_list)
        return endpoint
Ejemplo n.º 5
0
    def _provision_veth(self, endpoint):
        """Provisions veth for given endpoint.

        Uses the netns relative path passed in through CNI_NETNS_ENV and
        interface passed in through CNI_IFNAME_ENV.

        :param endpoint
        :return Calico endpoint object
        """
        _log.debug("Provisioning Calico veth interface")
        netns_path = os.path.abspath(os.path.join(os.getcwd(), self.cni_netns))
        _log.debug("netns path: %s", netns_path)

        try:
            endpoint.mac = endpoint.provision_veth(
                Namespace(netns_path), self.interface)
        except CalledProcessError as e:
            _log.exception("Failed to provision veth interface for endpoint %s",
                           endpoint.name)
            self._remove_workload()
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        _log.debug("Endpoint has mac address: %s", endpoint.mac)

        self._client.set_endpoint(endpoint)
        _log.info("Provisioned %s in netns %s", self.interface, netns_path)
        return endpoint
Ejemplo n.º 6
0
    def _add_existing_endpoint(self, endpoint):
        """
        Handles adding an existing container to a new Calico network.

        We've already assigned an IP address and created the veth,
        we just need to apply a new profile to this endpoint.
        """
        # Get the already existing IP information for this Endpoint. 
        try:
            ip4 = next(iter(endpoint.ipv4_nets))
        except StopIteration:
            # No IPv4 address on this endpoint.
            _log.warning("No IPV4 address attached to existing endpoint")
            ip4 = IPNetwork("0.0.0.0/32")

        try:
            ip6 = next(iter(endpoint.ipv6_nets))
        except StopIteration:
            # No IPv6 address on this endpoint.
            _log.warning("No IPV6 address attached to existing endpoint")
            ip6 = IPNetwork("::/128")

        # Apply a new profile to this endpoint.
        try:
            self.policy_driver.apply_profile(endpoint)
        except ApplyProfileError as e:
            # Hit an exception applying the profile.  We haven't configured
            # anything, so we don't need to clean anything up.  Just exit.
            _log.error("Failed to apply profile to endpoint %s",
                       endpoint.name)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        return {"ip4": {"ip": str(ip4.cidr)}, 
                "ip6": {"ip": str(ip6.cidr)}}
Ejemplo n.º 7
0
    def _add_new_endpoint(self):
        """
        Handled adding a new container to a Calico network.
        """
        # Assign IP addresses using the given IPAM plugin.
        _log.info("Configuring a new Endpoint")
        ipv4, ipv6, ipam_result = self._assign_ips(self.ipam_env)

        # Filter out addresses that didn't get assigned.
        ip_list = [ip for ip in [ipv4, ipv6] if ip is not None]

        # Create the Calico endpoint object.
        endpoint = self._create_endpoint(ip_list)

        # Provision the veth for this endpoint.
        endpoint = self._provision_veth(endpoint)

        # Provision / apply profile on the created endpoint.
        try:
            self.policy_driver.apply_profile(endpoint)
        except PolicyException as e:
            _log.error("Failed to apply profile to endpoint %s", endpoint.name)
            self._remove_veth(endpoint)
            self._remove_workload()
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message, e.details)
            sys.exit(ERR_CODE_GENERIC)

        # Return the IPAM plugin's result.
        return ipam_result
Ejemplo n.º 8
0
    def _add_existing_endpoint(self, endpoint):
        """
        Handles adding an existing container to a new Calico network.

        We've already assigned an IP address and created the veth,
        we just need to apply a new profile to this endpoint.
        """
        # Get the already existing IP information for this Endpoint.
        try:
            ip4 = next(iter(endpoint.ipv4_nets))
        except StopIteration:
            # No IPv4 address on this endpoint.
            _log.warning("No IPV4 address attached to existing endpoint")
            ip4 = IPNetwork("0.0.0.0/32")

        try:
            ip6 = next(iter(endpoint.ipv6_nets))
        except StopIteration:
            # No IPv6 address on this endpoint.
            _log.warning("No IPV6 address attached to existing endpoint")
            ip6 = IPNetwork("::/128")

        # Apply a new profile to this endpoint.
        try:
            self.policy_driver.apply_profile(endpoint)
        except PolicyException as e:
            # Hit an exception applying the profile.  We haven't configured
            # anything, so we don't need to clean anything up.  Just exit.
            _log.error("Failed to apply profile to endpoint %s", endpoint.name)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        return {"ip4": {"ip": str(ip4.cidr)}, "ip6": {"ip": str(ip6.cidr)}}
Ejemplo n.º 9
0
    def _add_new_endpoint(self):
        """
        Handled adding a new container to a Calico network.
        """
        # Assign IP addresses using the given IPAM plugin.
        ipv4, ipv6, ipam_result = self._assign_ips(self.ipam_env)

        # Filter out addresses that didn't get assigned.
        ip_list = [ip for ip in [ipv4, ipv6] if ip is not None]

        # Create the Calico endpoint object.
        endpoint = self._create_endpoint(ip_list)
    
        # Provision the veth for this endpoint.
        endpoint = self._provision_veth(endpoint)
        
        # Provision / apply profile on the created endpoint.
        try:
            self.policy_driver.apply_profile(endpoint)
        except ApplyProfileError as e:
            _log.error("Failed to apply profile to endpoint %s",
                       endpoint.name)
            self._remove_veth(endpoint)
            self._remove_workload()
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message, e.details)
            sys.exit(ERR_CODE_GENERIC)

        # Return the IPAM plugin's result.
        return ipam_result
Ejemplo n.º 10
0
    def _call_binary_ipam_plugin(self, env):
        """Calls through to the specified IPAM plugin binary.

        Utilizes the IPAM config as specified in the CNI network
        configuration file.  A dictionary with the following form:
            {
              type: <IPAM TYPE>
            }

        :param env - A dictionary of environment variables to pass to the
        IPAM plugin
        :return: Tuple of return code, response from the IPAM plugin.
        """
        # Find the correct plugin based on the given type.
        plugin_path = self._find_ipam_plugin()
        if not plugin_path:
            message = "Could not find IPAM plugin of type %s in path %s." % \
                      (self.ipam_type, self.cni_path)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        # Execute the plugin and return the result.
        _log.info("Using IPAM plugin at: %s", plugin_path)
        _log.debug("Passing in environment to IPAM plugin: \n%s",
                   json.dumps(env, indent=2))
        p = Popen(plugin_path, stdin=PIPE, stdout=PIPE, stderr=PIPE, env=env)
        stdout, stderr = p.communicate(json.dumps(self.network_config))
        _log.debug("IPAM plugin return code: %s", p.returncode)
        _log.debug("IPAM plugin output: \nstdout:\n%s\nstderr:\n%s", stdout,
                   stderr)
        return p.returncode, stdout
Ejemplo n.º 11
0
def main():
    """
    Main function - configures and runs the plugin.
    """
    # Read the network config file from stdin. Replace newline characters
    # so that we can properly load it as json.
    # The python json library loads strings as as the unicode type. This causes
    # problems when loading values into os.environ.
    # Rather than try to encode them as strings, just use the yaml library.
    # For more details see http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-ones-from-json-in-python
    config_raw = ''.join(sys.stdin.readlines()).replace('\n', '')
    network_config = yaml.safe_load(config_raw)

    # Get the log level from the config file, default to INFO.
    log_level = network_config.get(LOG_LEVEL_KEY, "INFO").upper()

    # Configure logging for CNI
    configure_logging(_log, LOG_FILENAME, log_level=log_level)

    # Configure logging for libcalico (pycalico)
    configure_logging(logging.getLogger("pycalico"), LOG_FILENAME,
                      log_level=log_level)

    _log.debug("Loaded network config:\n%s",
               json.dumps(network_config, indent=2))

    # Get the etcd configuration from the config file. Set the
    # environment variables.
    etcd_authority = network_config.get(ETCD_AUTHORITY_KEY)
    etcd_endpoints = network_config.get(ETCD_ENDPOINTS_KEY)
    if etcd_authority:
        os.environ[ETCD_AUTHORITY_ENV] = etcd_authority
        _log.debug("Using %s=%s", ETCD_AUTHORITY_ENV, etcd_authority)
    if etcd_endpoints:
        os.environ[ETCD_ENDPOINTS_ENV] = etcd_endpoints
        _log.debug("Using %s=%s", ETCD_ENDPOINTS_ENV, etcd_endpoints)

    # Get the CNI environment.
    env = os.environ.copy()
    _log.debug("Loaded environment:\n%s", json.dumps(env, indent=2))

    # Call the CNI plugin and handle any errors.
    rc = 0
    try:
        _log.info("Starting Calico CNI plugin execution")
        CniPlugin(network_config, env).execute()
    except SystemExit as e:
        # SystemExit indicates an error that was handled earlier
        # in the stack.  Just set the return code.
        rc = e.code
    except Exception:
        # An unexpected Exception has bubbled up - catch it and
        # log it out.
        _log.exception("Unhandled Exception killed plugin")
        rc = ERR_CODE_GENERIC
        print_cni_error(rc, "Unhandled Exception killed plugin")
    finally:
        _log.info("Calico CNI execution complete, rc=%s", rc)
        sys.exit(rc)
Ejemplo n.º 12
0
def _exit_on_error(code, message, details=""):
    """
    Return failure information to the calling plugin as specified in the
    CNI spec and exit.
    :param code: Error code to return (int)
    :param message: Short error message to return.
    :param details: Detailed error message to return.
    :return:
    """
    print_cni_error(code, message, details)
    sys.exit(code)
Ejemplo n.º 13
0
def _exit_on_error(code, message, details=""):
    """
    Return failure information to the calling plugin as specified in the
    CNI spec and exit.
    :param code: Error code to return (int)
    :param message: Short error message to return.
    :param details: Detailed error message to return.
    :return:
    """
    print_cni_error(code, message, details)
    sys.exit(code)
Ejemplo n.º 14
0
    def _call_ipam_plugin(self, env):
        """
        Executes a CNI IPAM plugin.  If `calico-ipam` is the provided IPAM
        type, then calls directly into ipam.py as a performance optimization.

        For all other types of IPAM, searches the CNI_PATH for the
        correct binary and executes it.

        :return: Tuple of return code, response from the IPAM plugin.
        """
        if self.ipam_type == "calico-ipam":
            _log.info("Using Calico IPAM")
            try:
                response = IpamPlugin(env,
                                      self.network_config["ipam"]).execute()
                code = 0
            except CniError as e:
                # We hit a CNI error - return the appropriate CNI formatted
                # error dictionary.
                response = json.dumps({
                    "code": e.code,
                    "msg": e.msg,
                    "details": e.details
                })
                code = e.code
        elif self.ipam_type == "host-local":
            # We've been told to use the "host-local" IPAM plugin.
            # Check if we need to use the Kubernetes podCidr for this node, and
            # if so replace the subnet field with the correct value.
            if self.network_config["ipam"].get("subnet") == "usePodCidr":
                if not self.running_under_k8s:
                    print_cni_error(
                        ERR_CODE_GENERIC, "Invalid network config",
                        "Must be running under Kubernetes to use 'subnet: usePodCidr'"
                    )
                    sys.exit(ERR_CODE_GENERIC)
                _log.info("Using Kubernetes podCIDR for node: %s",
                          self.k8s_node_name)
                pod_cidr = self._get_kubernetes_pod_cidr()
                self.network_config["ipam"]["subnet"] = str(pod_cidr)

            # Call the IPAM plugin.
            _log.debug("Calling host-local IPAM plugin")
            code, response = self._call_binary_ipam_plugin(env)
        else:
            # Using some other IPAM plugin - call it.
            _log.debug("Using binary plugin")
            code, response = self._call_binary_ipam_plugin(env)

        # Return the IPAM return code and output.
        _log.debug("IPAM response (rc=%s): %s", code, response)
        return code, response
Ejemplo n.º 15
0
def get_policy_driver(k8s_pod_name, k8s_namespace, net_config):
    """Returns a policy driver based on CNI configuration arguments.

    :return: a policy driver 
    """
    # Extract policy config and network name.
    policy_config = net_config.get(POLICY_KEY, {})
    network_name = net_config["name"]
    policy_type = policy_config.get("type")

    # Determine which policy driver to use.
    if policy_type == POLICY_MODE_DENY_INBOUND:
        # Use the deny-inbound driver. 
        driver_cls = DefaultDenyInboundDriver 
        driver_args = [network_name]
    elif k8s_pod_name:
        # Runing under Kubernetes - decide which Kubernetes driver to use.
        if policy_type == POLICY_MODE_ANNOTATIONS: 
            _log.debug("Using Kubernetes Annotation Policy Driver")
            assert k8s_namespace, "Missing Kubernetes namespace"
            k8s_auth_token = policy_config.get(AUTH_TOKEN_KEY)
            k8s_api_root = policy_config.get(API_ROOT_KEY, 
                                             "https://10.100.0.1:443/api/v1/")
            driver_cls = KubernetesAnnotationDriver
            driver_args = [k8s_pod_name, 
                           k8s_namespace, 
                           k8s_auth_token,
                           k8s_api_root]
        else:
            _log.debug("Using Default Kubernetes Policy Driver")
            driver_cls = KubernetesDefaultPolicyDriver
            driver_args = [network_name]
    else:
        _log.debug("Using default policy driver")
        driver_cls = DefaultPolicyDriver
        driver_args = [network_name]

    # Create an instance of the driver class.
    try:
        _log.debug("Creating instance of %s with args %s", 
                   driver_cls, driver_args)
        driver = driver_cls(*driver_args)
    except ValueError as e:
        # ValueError raised because profile name passed into
        # policy driver contains illegal characters.
        print_cni_error(ERR_CODE_GENERIC, e.message)
        sys.exit(ERR_CODE_GENERIC)

    return driver
Ejemplo n.º 16
0
    def _call_ipam_plugin(self, env):
        """
        Executes a CNI IPAM plugin.  If `calico-ipam` is the provided IPAM
        type, then calls directly into ipam.py as a performance optimization.

        For all other types of IPAM, searches the CNI_PATH for the
        correct binary and executes it.

        :return: Tuple of return code, response from the IPAM plugin.
        """
        if self.ipam_type == "calico-ipam":
            _log.info("Using Calico IPAM")
            try:
                response = IpamPlugin(env,
                                      self.network_config["ipam"]).execute()
                code = 0
            except CniError as e:
                # We hit a CNI error - return the appropriate CNI formatted
                # error dictionary.
                response = json.dumps({"code": e.code,
                                       "msg": e.msg,
                                       "details": e.details})
                code = e.code
        elif self.ipam_type == "host-local":
            # We've been told to use the "host-local" IPAM plugin.
            # Check if we need to use the Kubernetes podCidr for this node, and
            # if so replace the subnet field with the correct value.
            if self.network_config["ipam"].get("subnet") == "usePodCidr":
                if not self.running_under_k8s:
                    print_cni_error(ERR_CODE_GENERIC, "Invalid network config",
                            "Must be running under Kubernetes to use 'subnet: usePodCidr'")
                    sys.exit(ERR_CODE_GENERIC)
                _log.info("Using Kubernetes podCIDR for node: %s", self.k8s_node_name)
                pod_cidr = self._get_kubernetes_pod_cidr()
                self.network_config["ipam"]["subnet"] = str(pod_cidr)

            # Call the IPAM plugin.
            _log.debug("Calling host-local IPAM plugin")
            code, response = self._call_binary_ipam_plugin(env)
        else:
            # Using some other IPAM plugin - call it.
            _log.debug("Using binary plugin")
            code, response = self._call_binary_ipam_plugin(env)

        # Return the IPAM return code and output.
        _log.debug("IPAM response (rc=%s): %s", code, response)
        return code, response
Ejemplo n.º 17
0
def main():
    """
    Main function - configures and runs the plugin.
    """
    # Read the network config file from stdin. Replace newline characters
    # so that we can properly load it as json.
    config_raw = ''.join(sys.stdin.readlines()).replace('\n', '')
    network_config = json.loads(config_raw)

    # Get the log level from the config file, default to INFO.
    log_level = network_config.get(LOG_LEVEL_KEY, "INFO").upper()

    # Configure logging.
    configure_logging(_log, LOG_FILENAME, log_level=log_level)
    _log.debug("Loaded network config:\n%s", 
               json.dumps(network_config, indent=2))

    # Get the etcd authority from the config file. Set the 
    # environment variable.
    etcd_authority = network_config.get(ETCD_AUTHORITY_KEY, 
                                        ETCD_AUTHORITY_DEFAULT)
    os.environ[ETCD_AUTHORITY_ENV] = etcd_authority
    _log.debug("Using ETCD_AUTHORITY=%s", etcd_authority)

    # Get the CNI environment. 
    env = os.environ.copy()
    _log.debug("Loaded environment:\n%s", json.dumps(env, indent=2))

    # Call the CNI plugin and handle any errors.
    rc = 0
    try:
        _log.info("Starting Calico CNI plugin execution")
        CniPlugin(network_config, env).execute()
    except SystemExit as e:
        # SystemExit indicates an error that was handled earlier
        # in the stack.  Just set the return code.
        rc = e.code
    except Exception:
        # An unexpected Exception has bubbled up - catch it and
        # log it out.
        _log.exception("Unhandled Exception killed plugin")
        rc = ERR_CODE_GENERIC
        print_cni_error(rc, "Unhandled Exception killed plugin")
    finally:
        _log.info("Calico CNI execution complete, rc=%s", rc)
        sys.exit(rc)
Ejemplo n.º 18
0
    def _get_endpoint(self):
        """Get endpoint matching self.workload_id.

        If we cannot find an endpoint using self.workload_id, try
        using self.container_id.

        Return None if no endpoint is found.
        Exits with an error if multiple endpoints are found.

        :return: Endpoint object if found, None if not found
        """
        try:
            _log.debug("Looking for endpoint that matches workload ID %s",
                       self.workload_id)
            endpoint = self._client.get_endpoint(
                hostname=HOSTNAME,
                orchestrator_id=self.orchestrator_id,
                workload_id=self.workload_id
            )
        except KeyError:
            # Try to find using the container ID.  In earlier version of the
            # plugin, the container ID was used as the workload ID.
            _log.debug("No endpoint found matching workload ID %s",
                       self.workload_id)
            try:
                endpoint = self._client.get_endpoint(
                    hostname=HOSTNAME,
                    orchestrator_id="cni",
                    workload_id=self.container_id
                )
            except KeyError:
                # We were unable to find an endpoint using either the
                # workload ID or the container ID.
                _log.debug("No endpoint found matching container ID %s",
                           self.container_id)
                endpoint = None
        except MultipleEndpointsMatch:
            message = "Multiple Endpoints found matching ID %s" % \
                    self.workload_id
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        return endpoint
Ejemplo n.º 19
0
    def _get_endpoint(self):
        """Get endpoint matching self.workload_id.

        If we cannot find an endpoint using self.workload_id, try
        using self.container_id.

        Return None if no endpoint is found.
        Exits with an error if multiple endpoints are found.

        :return: Endpoint object if found, None if not found
        """
        try:
            _log.debug("Looking for endpoint that matches workload ID %s",
                       self.workload_id)
            endpoint = self._client.get_endpoint(
                hostname=self.hostname,
                orchestrator_id=self.orchestrator_id,
                workload_id=self.workload_id)
        except KeyError:
            # Try to find using the container ID.  In earlier version of the
            # plugin, the container ID was used as the workload ID.
            _log.debug("No endpoint found matching workload ID %s",
                       self.workload_id)
            try:
                endpoint = self._client.get_endpoint(
                    hostname=self.hostname,
                    orchestrator_id="cni",
                    workload_id=self.container_id)
            except KeyError:
                # We were unable to find an endpoint using either the
                # workload ID or the container ID.
                _log.debug("No endpoint found matching container ID %s",
                           self.container_id)
                endpoint = None
        except MultipleEndpointsMatch:
            message = "Multiple Endpoints found matching ID %s" % \
                    self.workload_id
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        return endpoint
Ejemplo n.º 20
0
    def _create_endpoint(self, ip_list):
        """Creates an endpoint in the Calico datastore with the client.

        :param ip_list - list of IP addresses that have been already allocated
        :return Calico endpoint object
        """
        _log.debug("Creating Calico endpoint with workload_id=%s",
                   self.workload_id)
        try:
            endpoint = self._client.create_endpoint(self.hostname,
                                                    self.orchestrator_id,
                                                    self.workload_id, ip_list)
        except (AddrFormatError, KeyError) as e:
            # AddrFormatError: Raised when an IP address type is not
            #                  compatible with the node.
            # KeyError: Raised when BGP config for host is not found.
            _log.exception("Failed to create Calico endpoint.")
            self.ipam_env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(self.ipam_env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)

        _log.info("Created Calico endpoint with IP address(es) %s", ip_list)
        return endpoint
Ejemplo n.º 21
0
    def _get_kubernetes_pod_cidr(self):
        """
        Attempt to get the Kubernetes pod CIDR for this node.
        First check if we've written it to disk.  If so, use that value.  If
        not, then query the Kubernetes API for it.
        """
        _log.info("Getting node.spec.podCidr from API, kubeconfig: %s",
                  self.kubeconfig_path)
        if not self.kubeconfig_path:
            # For now, kubeconfig is the only supported auth method.
            print_cni_error(
                ERR_CODE_GENERIC, "Missing kubeconfig",
                "usePodCidr requires specification of kubeconfig file")
            sys.exit(ERR_CODE_GENERIC)

        # Query the API for this node.  Default node name to the hostname.
        try:
            api = HTTPClient(KubeConfig.from_file(self.kubeconfig_path))
            node = None
            for n in Node.objects(api):
                _log.debug("Checking node: %s", n.obj["metadata"]["name"])
                if n.obj["metadata"]["name"] == self.k8s_node_name:
                    node = n
                    break
            if not node:
                raise KeyError("Unable to find node in API: %s",
                               self.k8s_node_name)
            _log.debug("Found node %s: %s: ", node.obj["metadata"]["name"],
                       node.obj["spec"])
        except Exception:
            print_cni_error(ERR_CODE_GENERIC, "Error querying Kubernetes API",
                            "Failed to get podCidr from Kubernetes API")
            sys.exit(ERR_CODE_GENERIC)
        else:
            pod_cidr = node.obj["spec"].get("podCIDR")
            if not pod_cidr:
                print_cni_error(ERR_CODE_GENERIC, "Missing podCidr",
                                "No podCidr for node %s" % self.k8s_node_name)
                sys.exit(ERR_CODE_GENERIC)
        _log.debug("Using podCidr: %s", pod_cidr)
        return pod_cidr
Ejemplo n.º 22
0
    def _get_kubernetes_pod_cidr(self):
        """
        Attempt to get the Kubernetes pod CIDR for this node.
        First check if we've written it to disk.  If so, use that value.  If
        not, then query the Kubernetes API for it.
        """
        _log.info("Getting node.spec.podCidr from API, kubeconfig: %s",
                  self.kubeconfig_path)
        if not self.kubeconfig_path:
            # For now, kubeconfig is the only supported auth method.
            print_cni_error(ERR_CODE_GENERIC, "Missing kubeconfig",
                    "usePodCidr requires specification of kubeconfig file")
            sys.exit(ERR_CODE_GENERIC)

        # Query the API for this node.  Default node name to the hostname.
        try:
            api = HTTPClient(KubeConfig.from_file(self.kubeconfig_path))
            node = None
            for n in Node.objects(api):
                _log.debug("Checking node: %s", n.obj["metadata"]["name"])
                if n.obj["metadata"]["name"] == self.k8s_node_name:
                    node = n
                    break
            if not node:
                raise KeyError("Unable to find node in API: %s", self.k8s_node_name)
            _log.debug("Found node %s: %s: ", node.obj["metadata"]["name"],
                       node.obj["spec"])
        except Exception:
            print_cni_error(ERR_CODE_GENERIC, "Error querying Kubernetes API",
                    "Failed to get podCidr from Kubernetes API")
            sys.exit(ERR_CODE_GENERIC)
        else:
            pod_cidr = node.obj["spec"].get("podCIDR")
            if not pod_cidr:
                print_cni_error(ERR_CODE_GENERIC, "Missing podCidr",
                        "No podCidr for node %s" % self.k8s_node_name)
                sys.exit(ERR_CODE_GENERIC)
        _log.debug("Using podCidr: %s", pod_cidr)
        return pod_cidr
Ejemplo n.º 23
0
def get_policy_driver(cni_plugin):
    """Returns a policy driver based on CNI configuration arguments.

    :return: a policy driver
    """
    # Extract policy config and network name.
    policy_config = cni_plugin.network_config.get(POLICY_KEY, {})
    network_name = cni_plugin.network_config["name"]
    policy_type = policy_config.get("type")
    k8s_config = cni_plugin.network_config.get("kubernetes", {})
    supported_policy_types = [
        None, POLICY_MODE_KUBERNETES, POLICY_MODE_KUBERNETES_ANNOTATIONS
    ]
    if policy_type not in supported_policy_types:
        print_cni_error(
            ERR_CODE_GENERIC, "policy type set to unsupported value (%s). "
            "Supported values = %s" %
            (policy_type, [x for x in supported_policy_types if x]))
        sys.exit(ERR_CODE_GENERIC)

    # Determine which policy driver to use.
    if cni_plugin.running_under_k8s:
        # Running under Kubernetes - decide which Kubernetes driver to use.
        if policy_type == POLICY_MODE_KUBERNETES_ANNOTATIONS or \
           policy_type == POLICY_MODE_KUBERNETES:
            assert cni_plugin.k8s_namespace, "Missing Kubernetes namespace"
            auth_token = policy_config.get(AUTH_TOKEN_KEY)
            api_root = policy_config.get(API_ROOT_KEY,
                                         "https://10.100.0.1:443/api/v1/")
            client_certificate = policy_config.get(K8S_CLIENT_CERTIFICATE_VAR)
            client_key = policy_config.get(K8S_CLIENT_KEY_VAR)
            certificate_authority = policy_config.get(
                K8S_CERTIFICATE_AUTHORITY_VAR)
            kubeconfig_path = k8s_config.get("kubeconfig")

            if (client_key and not os.path.isfile(client_key)) or \
               (client_certificate and not os.path.isfile(client_certificate)) or \
               (certificate_authority and not os.path.isfile(certificate_authority)):
                print_cni_error(ERR_CODE_GENERIC,
                                "certificates provided but files don't exist")
                sys.exit(ERR_CODE_GENERIC)

            if policy_type == POLICY_MODE_KUBERNETES:
                _log.debug("Using Kubernetes Policy Driver")
                driver_cls = KubernetesPolicyDriver
            elif policy_type == POLICY_MODE_KUBERNETES_ANNOTATIONS:
                _log.debug("Using Kubernetes Annotation Policy Driver")
                driver_cls = KubernetesAnnotationDriver

            driver_args = [
                cni_plugin.k8s_pod_name, cni_plugin.k8s_namespace, auth_token,
                api_root, client_certificate, client_key,
                certificate_authority, kubeconfig_path
            ]
        else:
            _log.debug("Using Kubernetes Driver - no policy")
            driver_cls = KubernetesNoPolicyDriver
            driver_args = [network_name]
    else:
        _log.debug("Using default policy driver")
        driver_cls = DefaultPolicyDriver
        driver_args = [network_name]

    # Create an instance of the driver class.
    try:
        _log.debug("Creating instance of %s with args %s", driver_cls,
                   driver_args)
        driver = driver_cls(*driver_args)
    except ValueError as e:
        # ValueError raised because profile name passed into
        # policy driver contains illegal characters.
        print_cni_error(ERR_CODE_GENERIC, e.message)
        sys.exit(ERR_CODE_GENERIC)

    return driver
Ejemplo n.º 24
0
    def _assign_ips(self, env):
        """Assigns and returns an IPv4 address using the IPAM plugin
        specified in the network config file.

        :return: ipv4, ipv6 - The IP addresses assigned by the IPAM plugin.
        """
        # Call the IPAM plugin.  Returns the plugin returncode,
        # as well as the CNI result from stdout.
        _log.debug("Assigning IP address")
        assert env[CNI_COMMAND_ENV] == CNI_CMD_ADD
        rc, result = self._call_ipam_plugin(env)

        try:
            # Load the response - either the assigned IP addresses or
            # a CNI error message.
            ipam_result = json.loads(result)
        except ValueError:
            message = "Failed to parse IPAM response, exiting"
            _log.exception(message)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        if rc:
            # The IPAM plugin failed to assign an IP address. At this point in
            # execution, we haven't done anything yet, so we don't have to
            # clean up.
            _log.error("IPAM plugin error (rc=%s): %s", rc, result)
            code = ipam_result.get("code", ERR_CODE_GENERIC)
            msg = ipam_result.get("msg", "Unknown IPAM error")
            details = ipam_result.get("details")
            print_cni_error(code, msg, details)
            sys.exit(int(code))

        try:
            ipv4 = IPNetwork(ipam_result["ip4"]["ip"])
            _log.info("IPAM plugin assigned IPv4 address: %s", ipv4)
        except KeyError:
            ipv4 = None
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv4 address: %s" % \
                      (ipam_result["ip4"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        try:
            ipv6 = IPNetwork(ipam_result["ip6"]["ip"])
            _log.info("IPAM plugin assigned IPv6 address: %s", ipv6)
        except KeyError:
            ipv6 = None
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv6 address: %s" % \
                      (ipam_result["ip6"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        if not ipv4 and not ipv6:
            message = "IPAM plugin did not return any valid addresses."
            _log.warning("Bad IPAM plugin response: %s", ipam_result)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        return ipv4, ipv6, ipam_result
Ejemplo n.º 25
0
    # Call the CNI plugin and handle any errors.
    rc = 0
    try:
        _log.info("Starting Calico CNI plugin execution")
        CniPlugin(network_config, env).execute()
    except SystemExit as e:
        # SystemExit indicates an error that was handled earlier
        # in the stack.  Just set the return code.
        rc = e.code
    except Exception:
        # An unexpected Exception has bubbled up - catch it and
        # log it out.
        _log.exception("Unhandled Exception killed plugin")
        rc = ERR_CODE_GENERIC
        print_cni_error(rc, "Unhandled Exception killed plugin")
    finally:
        _log.info("Calico CNI execution complete, rc=%s", rc)
        sys.exit(rc)


if __name__ == '__main__': # pragma: no cover
    try:
        main()
    except Exception as e:
        # Catch any unhandled exceptions in the main() function.  Any errors
        # in CniPlugin.execute() are already handled.
        print_cni_error(ERR_CODE_GENERIC, 
                        "Unhandled Exception in main()", 
                        e.message)
        sys.exit(ERR_CODE_GENERIC)
Ejemplo n.º 26
0
    def _assign_ips(self, env):
        """Assigns and returns an IPv4 address using the IPAM plugin
        specified in the network config file.

        :return: ipv4, ipv6 - The IP addresses assigned by the IPAM plugin.
        """
        # Call the IPAM plugin.  Returns the plugin returncode,
        # as well as the CNI result from stdout.
        _log.debug("Assigning IP address")
        assert env[CNI_COMMAND_ENV] == CNI_CMD_ADD
        rc, result = self._call_ipam_plugin(env)

        try:
            # Load the response - either the assigned IP addresses or 
            # a CNI error message.
            ipam_result = json.loads(result)
        except ValueError:
            message = "Failed to parse IPAM response, exiting"
            _log.exception(message)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        if rc:
            # The IPAM plugin failed to assign an IP address. At this point in
            # execution, we haven't done anything yet, so we don't have to
            # clean up.
            _log.error("IPAM plugin error (rc=%s): %s", rc, result)
            code = ipam_result.get("code", ERR_CODE_GENERIC)
            msg = ipam_result.get("msg", "Unknown IPAM error")
            details = ipam_result.get("details")
            print_cni_error(code, msg, details)
            sys.exit(int(code))

        try:
            ipv4 = IPNetwork(ipam_result["ip4"]["ip"])
            _log.info("IPAM plugin assigned IPv4 address: %s", ipv4)
        except KeyError:
            ipv4 = None
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv4 address: %s" % \
                      (ipam_result["ip4"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        try:
            ipv6 = IPNetwork(ipam_result["ip6"]["ip"])
            _log.info("IPAM plugin assigned IPv6 address: %s", ipv6)
        except KeyError:
            ipv6 = None
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv6 address: %s" % \
                      (ipam_result["ip6"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        if not ipv4 and not ipv6:
            message = "IPAM plugin did not return any valid addresses."
            _log.warning("Bad IPAM plugin response: %s", ipam_result)
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        return ipv4, ipv6, ipam_result
Ejemplo n.º 27
0
        print_cni_error(rc, "Unhandled Exception killed plugin")
    finally:
        _log.info("Calico CNI execution complete, rc=%s", rc)
        sys.exit(rc)


if __name__ == '__main__':  # pragma: no cover
    # Parse out the provided arguments.
    command_args = docopt(__doc__)

    # If the version argument was given, print version and exit.
    if command_args.get("--version"):
        print(
            json.dumps(
                {
                    "Version": __version__,
                    "Commit": __commit__,
                    "Branch": __branch__
                },
                indent=2))
        sys.exit(0)

    try:
        main()
    except Exception as e:
        # Catch any unhandled exceptions in the main() function.  Any errors
        # in CniPlugin.execute() are already handled.
        print_cni_error(ERR_CODE_GENERIC, "Unhandled Exception in main()",
                        e.message)
        sys.exit(ERR_CODE_GENERIC)
Ejemplo n.º 28
0
def main():
    """
    Main function - configures and runs the plugin.
    """
    # Read the network config file from stdin. Replace newline characters
    # so that we can properly load it as json.
    # The python json library loads strings as as the unicode type. This causes
    # problems when loading values into os.environ.
    # Rather than try to encode them as strings, just use the yaml library.
    # For more details see http://stackoverflow.com/questions/956867/how-to-get-string-objects-instead-of-unicode-ones-from-json-in-python
    config_raw = ''.join(sys.stdin.readlines()).replace('\n',
                                                        '').replace('\t', '')
    network_config = yaml.safe_load(config_raw)

    # Get the log level from the config file, default to INFO.
    log_level_file = network_config.get(LOG_LEVEL_FILE_KEY, "INFO").upper()
    log_level_stderr = network_config.get(LOG_LEVEL_STDERR_KEY,
                                          "ERROR").upper()

    log_filename = "cni.log"
    # Configure logging for CNI
    configure_logging(_log, log_level_file, log_level_stderr, log_filename)

    # Configure logging for libcalico (pycalico)
    configure_logging(logging.getLogger("pycalico"), "WARNING", "WARNING",
                      log_filename)

    _log.debug("Loaded network config:\n%s",
               json.dumps(network_config, indent=2))

    # Get the etcd configuration from the config file. Set the
    # environment variables.
    etcd_authority = network_config.get(ETCD_AUTHORITY_KEY)
    etcd_endpoints = network_config.get(ETCD_ENDPOINTS_KEY)
    if etcd_authority:
        os.environ[ETCD_AUTHORITY_ENV] = etcd_authority
        _log.debug("Using %s=%s", ETCD_AUTHORITY_ENV, etcd_authority)
    if etcd_endpoints:
        os.environ[ETCD_ENDPOINTS_ENV] = etcd_endpoints
        _log.debug("Using %s=%s", ETCD_ENDPOINTS_ENV, etcd_endpoints)

    # Get the CNI environment.
    env = os.environ.copy()
    _log.debug("Loaded environment:\n%s", json.dumps(env, indent=2))

    # Call the CNI plugin and handle any errors.
    rc = 0
    try:
        _log.info("Starting Calico CNI plugin execution")
        CniPlugin(network_config, env).execute()
    except SystemExit as e:
        # SystemExit indicates an error that was handled earlier
        # in the stack.  Just set the return code.
        rc = e.code
    except Exception:
        # An unexpected Exception has bubbled up - catch it and
        # log it out.
        _log.exception("Unhandled Exception killed plugin")
        rc = ERR_CODE_GENERIC
        print_cni_error(rc, "Unhandled Exception killed plugin")
    finally:
        _log.info("Calico CNI execution complete, rc=%s", rc)
        sys.exit(rc)
Ejemplo n.º 29
0
def get_policy_driver(cni_plugin):
    """Returns a policy driver based on CNI configuration arguments.

    :return: a policy driver
    """
    # Extract policy config and network name.
    policy_config = cni_plugin.network_config.get(POLICY_KEY, {})
    network_name = cni_plugin.network_config["name"]
    policy_type = policy_config.get("type")
    k8s_config = cni_plugin.network_config.get("kubernetes", {})
    supported_policy_types = [None,
                              POLICY_MODE_KUBERNETES,
                              POLICY_MODE_KUBERNETES_ANNOTATIONS]
    if policy_type not in supported_policy_types:
        print_cni_error(ERR_CODE_GENERIC,
                        "policy type set to unsupported value (%s). "
                        "Supported values = %s" %
                        (policy_type, [x for x in supported_policy_types if x]))
        sys.exit(ERR_CODE_GENERIC)

    # Determine which policy driver to use.
    if cni_plugin.running_under_k8s:
        # Running under Kubernetes - decide which Kubernetes driver to use.
        if policy_type == POLICY_MODE_KUBERNETES_ANNOTATIONS or \
           policy_type == POLICY_MODE_KUBERNETES:
            assert cni_plugin.k8s_namespace, "Missing Kubernetes namespace"
            auth_token = policy_config.get(AUTH_TOKEN_KEY)
            api_root = policy_config.get(API_ROOT_KEY,
                                         "https://10.100.0.1:443/api/v1/")
            client_certificate = policy_config.get(K8S_CLIENT_CERTIFICATE_VAR)
            client_key = policy_config.get(K8S_CLIENT_KEY_VAR)
            certificate_authority = policy_config.get(
                K8S_CERTIFICATE_AUTHORITY_VAR)
            kubeconfig_path = k8s_config.get("kubeconfig")

            if (client_key and not os.path.isfile(client_key)) or \
               (client_certificate and not os.path.isfile(client_certificate)) or \
               (certificate_authority and not os.path.isfile(certificate_authority)):
                print_cni_error(ERR_CODE_GENERIC,
                                "certificates provided but files don't exist")
                sys.exit(ERR_CODE_GENERIC)

            if policy_type == POLICY_MODE_KUBERNETES:
                _log.debug("Using Kubernetes Policy Driver")
                driver_cls = KubernetesPolicyDriver
            elif policy_type == POLICY_MODE_KUBERNETES_ANNOTATIONS:
                _log.debug("Using Kubernetes Annotation Policy Driver")
                driver_cls = KubernetesAnnotationDriver

            driver_args = [cni_plugin.k8s_pod_name,
                           cni_plugin.k8s_namespace,
                           auth_token,
                           api_root,
                           client_certificate,
                           client_key,
                           certificate_authority,
                           kubeconfig_path]
        else:
            _log.debug("Using Kubernetes Driver - no policy")
            driver_cls = KubernetesNoPolicyDriver
            driver_args = [network_name]
    else:
        _log.debug("Using default policy driver")
        driver_cls = DefaultPolicyDriver
        driver_args = [network_name]

    # Create an instance of the driver class.
    try:
        _log.debug("Creating instance of %s with args %s",
                   driver_cls, driver_args)
        driver = driver_cls(*driver_args)
    except ValueError as e:
        # ValueError raised because profile name passed into
        # policy driver contains illegal characters.
        print_cni_error(ERR_CODE_GENERIC, e.message)
        sys.exit(ERR_CODE_GENERIC)

    return driver