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)
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
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)
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)
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
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
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)
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)
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)
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)
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")
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