Example #1
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 = self._assign_ips(self.env)

        # Create the Calico endpoint object.  For now, we only
        # support creating endpoints with IPv4.
        endpoint = self._create_endpoint([ipv4])

        # 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 policy_drivers.ApplyProfileError, e:
            _log.error("Failed to apply profile to endpoint %s", endpoint.name)
            self._remove_veth(endpoint)
            self._remove_endpoint()
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #2
0
    def _get_endpoint(self):
        """Gets endpoint matching the container_id.

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

        :param container_id:
        :return: Calico 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
Example #3
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 = self._assign_ips(self.env)

        # Create the Calico endpoint object.  For now, we only 
        # support creating endpoints with IPv4.
        endpoint = self._create_endpoint([ipv4])
    
        # 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 policy_drivers.ApplyProfileError, e:
            _log.error("Failed to apply profile to endpoint %s",
                       endpoint.name)
            self._remove_veth(endpoint)
            self._remove_endpoint()
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #4
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, e:
            _log.exception(
                "Failed to provision veth interface for endpoint %s",
                endpoint.name)
            self._remove_endpoint()
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #5
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 policy_drivers.ApplyProfileError, 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)
Example #6
0
    def _call_ipam_plugin(self, env):
        """Calls through to the specified IPAM plugin.
    
        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: 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.network_config['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
Example #7
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 policy_drivers.ApplyProfileError, 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)
Example #8
0
    def _get_endpoint(self):
        """Gets endpoint matching the container_id.

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

        :param container_id:
        :return: Calico 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
Example #9
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, e:
            _log.exception("Failed to provision veth interface for endpoint %s",
                           endpoint.name)
            self._remove_endpoint()
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #10
0
    def _call_ipam_plugin(self, env):
        """Calls through to the specified IPAM plugin.
    
        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: 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.network_config['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
Example #11
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)
    _log.debug("Exiting with rc=%s", code)
    sys.exit(code)
Example #12
0
    def _get_policy_driver(self):
        """Returns a policy driver based on CNI configuration arguments.

        :return: a policy driver of type BasePolicyDriver
        """
        try:
            self.cni_args[K8S_POD_NAME]
        except KeyError:
            _log.debug("Using default policy driver")
            try:
                driver = policy_drivers.DefaultPolicyDriver(self.network_name)
            except ValueError, 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)
Example #13
0
    def _get_policy_driver(self):
        """Returns a policy driver based on CNI configuration arguments.

        :return: a policy driver of type BasePolicyDriver
        """
        try:
            self.cni_args[K8S_POD_NAME]
        except KeyError:
            _log.debug("Using default policy driver")
            try:
                driver = policy_drivers.DefaultPolicyDriver(self.network_name)
            except ValueError, 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)
Example #14
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 has been already allocated
        :return Calico endpoint object
        """
        _log.debug("Creating Calico endpoint")
        try:
            endpoint = self._client.create_endpoint(HOSTNAME, ORCHESTRATOR_ID,
                                                    self.container_id, ip_list)
        except (AddrFormatError, KeyError), 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.")
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #15
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 has been already allocated
        :return Calico endpoint object
        """
        _log.debug("Creating Calico endpoint")
        try:
            endpoint = self._client.create_endpoint(HOSTNAME,
                                                    ORCHESTRATOR_ID,
                                                    self.container_id,
                                                    ip_list)
        except (AddrFormatError, KeyError), 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.")
            env = self.env.copy()
            env[CNI_COMMAND_ENV] = CNI_CMD_DELETE
            self._release_ip(env)
            print_cni_error(ERR_CODE_GENERIC, e.message)
            sys.exit(ERR_CODE_GENERIC)
Example #16
0
class CniPlugin(object):
    """
    Class which encapsulates the function of a CNI plugin.
    """
    def __init__(self, network_config, env):
        self.network_config = network_config
        """
        Network config as provided in the CNI network file passed in
        via stdout.
        """

        self.env = env
        """
        Copy of the environment variable dictionary. Contains CNI_* 
        variables.
        """

        self._client = DatastoreClient()
        """
        DatastoreClient for access to the Calico datastore.
        """

        self.command = env[CNI_COMMAND_ENV]
        """
        The command to execute for this plugin instance. Required. 
        One of:
          - CNI_CMD_ADD
          - CNI_CMD_DELETE
        """

        self.container_id = env[CNI_CONTAINERID_ENV]
        """
        The container's ID in the containerizer. Required.
        """

        self.cni_netns = env[CNI_NETNS_ENV]
        """
        Relative path to the network namespace of this container.
        """

        self.interface = env[CNI_IFNAME_ENV]
        """
        Name of the interface to create within the container.
        """

        self.cni_args = parse_cni_args(env[CNI_ARGS_ENV])
        """
        Dictionary of additional CNI arguments provided via
        the CNI_ARGS environment variable.
        """

        self.cni_path = env[CNI_PATH_ENV]
        """
        Path in which to search for CNI plugins.
        """

        self.network_name = network_config["name"]
        """
        Name of the network from the provided network config file.
        """

        self.ipam_result = None
        """
        Stores the output generated by the IPAM plugin.  This is printed
        to stdout at the end of execution.
        """

        self.policy_driver = self._get_policy_driver()
        """
        Chooses the correct policy driver based on the given configuration
        """

        self.container_engine = self._get_container_engine()
        """
        Chooses the correct container engine based on the given configuration.
        """

    def execute(self):
        """Executes this plugin.
        Handles unexpected Exceptions in plugin execution.

        :return The plugin return code.
        """
        rc = 0
        try:
            _log.info("Starting Calico CNI plugin execution")
            self._execute()
        except SystemExit, e:
            # SystemExit indicates an error that was handled earlier
            # in the stack.  Just set the return code.
            rc = e.code
        except BaseException:
            # An unexpected Exception has bubbled up - catch it and
            # log it out.
            _log.exception("Unhandled Exception killed plugin")
            rc = ERR_CODE_UNHANDLED
            print_cni_error(rc, "Unhandled Exception killed plugin")
Example #17
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
            self.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 = self.ipam_result.get("code", ERR_CODE_GENERIC)
            msg = self.ipam_result.get("msg", "Unknown IPAM error")
            details = self.ipam_result.get("details")
            print_cni_error(code, msg, details)
            sys.exit(int(code))

        try:
            ipv4 = IPNetwork(self.ipam_result["ip4"]["ip"])
        except KeyError:
            message = "IPAM plugin did not return an IPv4 address."
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv4 address: %s" % \
                      (self.ipam_result["ip4"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        try:
            ipv6 = IPNetwork(self.ipam_result["ip6"]["ip"])
        except KeyError:
            message = "IPAM plugin did not return an IPv6 address."
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv6 address: %s" % \
                      (self.ipam_result["ip6"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        _log.info("IPAM plugin assigned IPv4 address: %s", ipv4)
        _log.info("IPAM plugin assigned IPv6 address: %s", ipv6)
        return ipv4, ipv6
Example #18
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
            self.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 = self.ipam_result.get("code", ERR_CODE_GENERIC)
            msg = self.ipam_result.get("msg", "Unknown IPAM error")
            details = self.ipam_result.get("details")
            print_cni_error(code, msg, details)
            sys.exit(int(code))

        try:
            ipv4 = IPNetwork(self.ipam_result["ip4"]["ip"])
        except KeyError:
            message = "IPAM plugin did not return an IPv4 address."
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv4 address: %s" % \
                      (self.ipam_result["ip4"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        try:
            ipv6 = IPNetwork(self.ipam_result["ip6"]["ip"])
        except KeyError:
            message = "IPAM plugin did not return an IPv6 address."
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)
        except (AddrFormatError, ValueError):
            message = "Invalid or Empty IPv6 address: %s" % \
                      (self.ipam_result["ip6"]["ip"])
            print_cni_error(ERR_CODE_GENERIC, message)
            sys.exit(ERR_CODE_GENERIC)

        _log.info("IPAM plugin assigned IPv4 address: %s", ipv4)
        _log.info("IPAM plugin assigned IPv6 address: %s", ipv6)
        return ipv4, ipv6